├── scripts ├── requirements.txt └── update_clang_format.py ├── .flake8 ├── tests ├── unit │ ├── general │ │ ├── CMakeLists.txt │ │ ├── test_noop_settings_persistence.cpp │ │ ├── test_noop_audio_context.cpp │ │ ├── test_edid_parsing.cpp │ │ ├── test_file_settings_persistence.cpp │ │ ├── test_json_converter.cpp │ │ ├── test_comparison.cpp │ │ └── test_logging.cpp │ ├── windows │ │ ├── CMakeLists.txt │ │ ├── utils │ │ │ ├── helpers.h │ │ │ ├── guards.h │ │ │ ├── guards.cpp │ │ │ ├── mock_win_display_device.cpp │ │ │ ├── comparison.h │ │ │ ├── helpers.cpp │ │ │ ├── mock_win_display_device.h │ │ │ ├── mock_win_api_layer.h │ │ │ ├── comparison.cpp │ │ │ └── mock_win_api_layer.cpp │ │ ├── test_comparison.cpp │ │ ├── test_json_converter.cpp │ │ ├── test_win_playground.cpp │ │ ├── test_settings_manager_general.cpp │ │ └── test_persistent_state.cpp │ └── CMakeLists.txt ├── fixtures │ ├── include │ │ └── fixtures │ │ │ ├── mock_audio_context.h │ │ │ ├── mock_settings_persistence.h │ │ │ ├── json_converter_test.h │ │ │ ├── test_utils.h │ │ │ └── fixtures.h │ ├── CMakeLists.txt │ ├── test_utils.cpp │ └── fixtures.cpp └── CMakeLists.txt ├── .gitmodules ├── codecov.yml ├── src ├── common │ ├── noop_audio_context.cpp │ ├── noop_settings_persistence.cpp │ ├── retry_scheduler.cpp │ ├── json.cpp │ ├── include │ │ └── display_device │ │ │ ├── noop_audio_context.h │ │ │ ├── noop_settings_persistence.h │ │ │ ├── detail │ │ │ ├── json_serializer.h │ │ │ ├── json_converter.h │ │ │ └── json_serializer_details.h │ │ │ ├── json.h │ │ │ ├── audio_context_interface.h │ │ │ ├── file_settings_persistence.h │ │ │ ├── settings_persistence_interface.h │ │ │ ├── settings_manager_interface.h │ │ │ ├── types.h │ │ │ └── logging.h │ ├── CMakeLists.txt │ ├── json_serializer.cpp │ ├── file_settings_persistence.cpp │ ├── logging.cpp │ └── types.cpp ├── windows │ ├── include │ │ └── display_device │ │ │ └── windows │ │ │ ├── json.h │ │ │ ├── detail │ │ │ └── json_serializer.h │ │ │ ├── persistent_state.h │ │ │ ├── win_api_layer.h │ │ │ ├── win_display_device.h │ │ │ ├── types.h │ │ │ └── settings_manager.h │ ├── json.cpp │ ├── json_serializer.cpp │ ├── CMakeLists.txt │ ├── types.cpp │ ├── settings_manager_general.cpp │ ├── persistent_state.cpp │ ├── win_display_device_primary.cpp │ ├── win_display_device_hdr.cpp │ ├── win_display_device_general.cpp │ └── win_display_device_topology.cpp └── CMakeLists.txt ├── .gitignore ├── .github ├── semantic.yml ├── workflows │ ├── _common-lint.yml │ ├── _codeql.yml │ └── _update-docs.yml └── dependabot.yml ├── cmake ├── Json_DD.cmake └── Boost_DD.cmake ├── .readthedocs.yaml ├── docs └── Doxyfile ├── CMakeLists.txt ├── README.md └── .clang-format /scripts/requirements.txt: -------------------------------------------------------------------------------- 1 | clang-format==21.* 2 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | filename = 3 | *.py 4 | max-line-length = 120 5 | extend-exclude = 6 | .venv/ 7 | venv/ 8 | -------------------------------------------------------------------------------- /tests/unit/general/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Add the test files in this directory 2 | add_dd_test_dir( 3 | ADDITIONAL_LIBRARIES 4 | nlohmann_json::nlohmann_json 5 | ) 6 | -------------------------------------------------------------------------------- /tests/unit/windows/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Add the test files in this directory 2 | add_dd_test_dir( 3 | ADDITIONAL_LIBRARIES 4 | Boost::scope 5 | 6 | ADDITIONAL_SOURCES 7 | utils/*.h 8 | utils/*.cpp 9 | ) 10 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "third-party/doxyconfig"] 2 | path = third-party/doxyconfig 3 | url = https://github.com/LizardByte/doxyconfig.git 4 | branch = master 5 | [submodule "third-party/googletest"] 6 | path = third-party/googletest 7 | url = https://github.com/google/googletest.git 8 | branch = v1.14.x 9 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | --- 2 | codecov: 3 | branch: master 4 | notify: 5 | after_n_builds: 3 6 | 7 | coverage: 8 | status: 9 | project: 10 | default: 11 | target: auto 12 | threshold: 10% 13 | 14 | comment: 15 | layout: "diff, flags, files" 16 | behavior: default 17 | require_changes: false # if true: only post the comment if coverage changes 18 | 19 | ignore: 20 | - "tests" 21 | - "third-party" 22 | -------------------------------------------------------------------------------- /tests/unit/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # General platform-agnostic tests (or without hard platform dependencies) 2 | add_subdirectory(general) 3 | 4 | # Platform specific tests 5 | if(WIN32) 6 | add_subdirectory(windows) 7 | elseif(APPLE) 8 | message(WARNING "MacOS is not supported yet.") 9 | elseif(UNIX) 10 | message(WARNING "Linux is not supported yet.") 11 | else() 12 | message(FATAL_ERROR "Unsupported platform") 13 | endif() 14 | -------------------------------------------------------------------------------- /tests/fixtures/include/fixtures/mock_audio_context.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // system includes 4 | #include 5 | 6 | // local includes 7 | #include "display_device/audio_context_interface.h" 8 | 9 | namespace display_device { 10 | class MockAudioContext: public AudioContextInterface { 11 | public: 12 | MOCK_METHOD(bool, capture, (), (override)); 13 | MOCK_METHOD(bool, isCaptured, (), (const, override)); 14 | MOCK_METHOD(void, release, (), (override)); 15 | }; 16 | } // namespace display_device 17 | -------------------------------------------------------------------------------- /tests/unit/windows/utils/helpers.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // system includes 4 | #include 5 | 6 | // local includes 7 | #include "display_device/windows/types.h" 8 | #include "display_device/windows/win_api_layer.h" 9 | 10 | // Generic helper functions 11 | std::optional> getAvailableDevices(display_device::WinApiLayer &layer, bool only_valid_output = true); 12 | 13 | std::optional> serializeState(const std::optional &state); 14 | -------------------------------------------------------------------------------- /src/common/noop_audio_context.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file src/common/noop_audio_context.cpp 3 | * @brief Definitions for the NoopAudioContext. 4 | */ 5 | // local includes 6 | #include "display_device/noop_audio_context.h" 7 | 8 | namespace display_device { 9 | bool NoopAudioContext::capture() { 10 | m_is_captured = true; 11 | return true; 12 | } 13 | 14 | bool NoopAudioContext::isCaptured() const { 15 | return m_is_captured; 16 | } 17 | 18 | void NoopAudioContext::release() { 19 | m_is_captured = false; 20 | } 21 | } // namespace display_device 22 | -------------------------------------------------------------------------------- /tests/fixtures/include/fixtures/mock_settings_persistence.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // system includes 4 | #include 5 | 6 | // local includes 7 | #include "display_device/settings_persistence_interface.h" 8 | 9 | namespace display_device { 10 | class MockSettingsPersistence: public SettingsPersistenceInterface { 11 | public: 12 | MOCK_METHOD(bool, store, (const std::vector &), (override)); 13 | MOCK_METHOD(std::optional>, load, (), (const, override)); 14 | MOCK_METHOD(bool, clear, (), (override)); 15 | }; 16 | } // namespace display_device 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files 20 | *.mod 21 | *.smod 22 | 23 | # Compiled Static libraries 24 | *.lai 25 | *.la 26 | *.a 27 | *.lib 28 | 29 | # Executables 30 | *.exe 31 | *.out 32 | *.app 33 | 34 | # JetBrains IDE 35 | .idea/ 36 | 37 | # VSCode IDE 38 | .vscode/ 39 | 40 | # build directories 41 | build/ 42 | cmake-*/ 43 | 44 | # doxyconfig 45 | docs/doxyconfig* 46 | 47 | # CTest 48 | Testing/ 49 | -------------------------------------------------------------------------------- /src/common/noop_settings_persistence.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file src/common/noop_settings_persistence.cpp 3 | * @brief Definitions for NoopSettingsPersistence. 4 | */ 5 | // local includes 6 | #include "display_device/noop_settings_persistence.h" 7 | 8 | namespace display_device { 9 | bool NoopSettingsPersistence::store(const std::vector &) { 10 | return true; 11 | } 12 | 13 | std::optional> NoopSettingsPersistence::load() const { 14 | return std::vector {}; 15 | } 16 | 17 | bool NoopSettingsPersistence::clear() { 18 | return true; 19 | } 20 | } // namespace display_device 21 | -------------------------------------------------------------------------------- /.github/semantic.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # This file is centrally managed in https://github.com//.github/ 3 | # Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in 4 | # the above-mentioned repo. 5 | 6 | # This is the configuration file for https://github.com/Ezard/semantic-prs 7 | 8 | enabled: true 9 | titleOnly: true # We only use the PR title as we squash and merge 10 | commitsOnly: false 11 | titleAndCommits: false 12 | anyCommit: false 13 | allowMergeCommits: false 14 | allowRevertCommits: false 15 | targetUrl: https://docs.lizardbyte.dev/latest/developers/contributing.html#creating-a-pull-request 16 | -------------------------------------------------------------------------------- /src/windows/include/display_device/windows/json.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file src/windows/include/display_device/windows/json.h 3 | * @brief Declarations for JSON conversion functions (Windows-only). 4 | */ 5 | #pragma once 6 | 7 | // local includes 8 | #include "display_device/json.h" 9 | #include "types.h" 10 | 11 | // Windows' converters (add as needed) 12 | namespace display_device { 13 | DD_JSON_DECLARE_CONVERTER(ActiveTopology) 14 | DD_JSON_DECLARE_CONVERTER(DeviceDisplayModeMap) 15 | DD_JSON_DECLARE_CONVERTER(HdrStateMap) 16 | DD_JSON_DECLARE_CONVERTER(SingleDisplayConfigState) 17 | DD_JSON_DECLARE_CONVERTER(WinWorkarounds) 18 | } // namespace display_device 19 | -------------------------------------------------------------------------------- /cmake/Json_DD.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Loads the nlohmann_json library giving the priority to the system package first, with a fallback 3 | # to the submodule. 4 | # 5 | include_guard(GLOBAL) 6 | 7 | find_package(nlohmann_json 3.11 QUIET GLOBAL) 8 | if(NOT nlohmann_json_FOUND) 9 | message(STATUS "nlohmann_json v3.11.x package not found in the system. Falling back to FetchContent.") 10 | include(FetchContent) 11 | 12 | FetchContent_Declare( 13 | json 14 | URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz 15 | URL_HASH MD5=c23a33f04786d85c29fda8d16b5f0efd 16 | DOWNLOAD_EXTRACT_TIMESTAMP 17 | ) 18 | FetchContent_MakeAvailable(json) 19 | endif() 20 | -------------------------------------------------------------------------------- /src/common/retry_scheduler.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file src/common/retry_scheduler.cpp 3 | * @brief Definitions for the RetryScheduler. 4 | */ 5 | // header include 6 | #include "display_device/retry_scheduler.h" 7 | 8 | namespace display_device { 9 | SchedulerStopToken::SchedulerStopToken(std::function cleanup): 10 | m_cleanup {std::move(cleanup)} { 11 | } 12 | 13 | SchedulerStopToken::~SchedulerStopToken() { 14 | if (m_stop_requested && m_cleanup) { 15 | m_cleanup(); 16 | } 17 | } 18 | 19 | void SchedulerStopToken::requestStop() { 20 | m_stop_requested = true; 21 | } 22 | 23 | bool SchedulerStopToken::stopRequested() const { 24 | return m_stop_requested; 25 | } 26 | } // namespace display_device 27 | -------------------------------------------------------------------------------- /src/windows/include/display_device/windows/detail/json_serializer.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file src/windows/include/display_device/windows/detail/json_serializer.h 3 | * @brief Declarations for private JSON serialization helpers (Windows-only). 4 | */ 5 | #pragma once 6 | 7 | // local includes 8 | #include "display_device/detail/json_serializer.h" 9 | 10 | #ifdef DD_JSON_DETAIL 11 | namespace display_device { 12 | // Structs 13 | DD_JSON_DECLARE_SERIALIZE_TYPE(DisplayMode) 14 | DD_JSON_DECLARE_SERIALIZE_TYPE(SingleDisplayConfigState::Initial) 15 | DD_JSON_DECLARE_SERIALIZE_TYPE(SingleDisplayConfigState::Modified) 16 | DD_JSON_DECLARE_SERIALIZE_TYPE(SingleDisplayConfigState) 17 | DD_JSON_DECLARE_SERIALIZE_TYPE(WinWorkarounds) 18 | } // namespace display_device 19 | #endif 20 | -------------------------------------------------------------------------------- /.github/workflows/_common-lint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # This workflow is centrally managed in https://github.com/LizardByte/.github/ 3 | # Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in 4 | # the above-mentioned repo. 5 | 6 | name: common lint 7 | permissions: 8 | contents: read 9 | 10 | on: 11 | pull_request: 12 | branches: 13 | - master 14 | types: 15 | - opened 16 | - synchronize 17 | - reopened 18 | 19 | concurrency: 20 | group: "${{ github.workflow }}-${{ github.ref }}" 21 | cancel-in-progress: true 22 | 23 | jobs: 24 | lint: 25 | name: Common Lint 26 | uses: LizardByte/.github/.github/workflows/__call-common-lint.yml@master 27 | if: ${{ github.repository != 'LizardByte/.github' }} 28 | -------------------------------------------------------------------------------- /tests/fixtures/include/fixtures/json_converter_test.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // local includes 4 | #include "display_device/json.h" 5 | #include "fixtures.h" 6 | 7 | class JsonConverterTest: public BaseTest { 8 | public: 9 | template 10 | void executeTestCase(const T &input, const std::string &expected_string) { 11 | bool success {false}; 12 | const auto json_string {display_device::toJson(input, std::nullopt, &success)}; 13 | EXPECT_TRUE(success); 14 | EXPECT_EQ(json_string, expected_string); 15 | 16 | std::string error_message {}; 17 | T defaulted_input {}; 18 | if (!display_device::fromJson(json_string, defaulted_input, &error_message)) { 19 | GTEST_FAIL() << error_message; 20 | } 21 | EXPECT_EQ(input, defaulted_input); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /src/common/json.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file src/common/json.cpp 3 | * @brief Definitions for JSON conversion functions. 4 | */ 5 | // header include 6 | #include "display_device/json.h" 7 | 8 | // special ordered include of details 9 | #define DD_JSON_DETAIL 10 | // clang-format off 11 | #include "display_device/detail/json_serializer.h" 12 | #include "display_device/detail/json_converter.h" 13 | // clang-format on 14 | 15 | namespace display_device { 16 | DD_JSON_DEFINE_CONVERTER(EdidData) 17 | DD_JSON_DEFINE_CONVERTER(EnumeratedDevice) 18 | DD_JSON_DEFINE_CONVERTER(EnumeratedDeviceList) 19 | DD_JSON_DEFINE_CONVERTER(SingleDisplayConfiguration) 20 | DD_JSON_DEFINE_CONVERTER(std::set) 21 | DD_JSON_DEFINE_CONVERTER(std::string) 22 | DD_JSON_DEFINE_CONVERTER(bool) 23 | } // namespace display_device 24 | -------------------------------------------------------------------------------- /tests/unit/general/test_noop_settings_persistence.cpp: -------------------------------------------------------------------------------- 1 | // local includes 2 | #include "display_device/noop_settings_persistence.h" 3 | #include "fixtures/fixtures.h" 4 | 5 | namespace { 6 | // Test fixture(s) for this file 7 | class NoopSettingsPersistenceTest: public BaseTest { 8 | public: 9 | display_device::NoopSettingsPersistence m_impl; 10 | }; 11 | 12 | // Specialized TEST macro(s) for this test file 13 | #define TEST_F_S(...) DD_MAKE_TEST(TEST_F, NoopSettingsPersistenceTest, __VA_ARGS__) 14 | } // namespace 15 | 16 | TEST_F_S(Store) { 17 | EXPECT_TRUE(m_impl.store({})); 18 | EXPECT_TRUE(m_impl.store({0x01, 0x02, 0x03})); 19 | } 20 | 21 | TEST_F_S(Load) { 22 | EXPECT_EQ(m_impl.load(), std::vector {}); 23 | } 24 | 25 | TEST_F_S(Clear) { 26 | EXPECT_TRUE(m_impl.clear()); 27 | } 28 | -------------------------------------------------------------------------------- /tests/unit/windows/utils/guards.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // system includes 4 | #include 5 | 6 | // local includes 7 | #include "display_device/windows/win_display_device_interface.h" 8 | #include "helpers.h" 9 | 10 | // Helper functions to make guards for restoring previous state 11 | boost::scope::scope_exit makeTopologyGuard(display_device::WinDisplayDeviceInterface &win_dd); 12 | 13 | boost::scope::scope_exit makeModeGuard(display_device::WinDisplayDeviceInterface &win_dd); 14 | 15 | boost::scope::scope_exit makePrimaryGuard(display_device::WinDisplayDeviceInterface &win_dd); 16 | 17 | boost::scope::scope_exit makeHdrStateGuard(display_device::WinDisplayDeviceInterface &win_dd); 18 | -------------------------------------------------------------------------------- /tests/fixtures/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # A global identifier for the library 2 | set(MODULE libfixtures) 3 | 4 | # Globing headers (so that they appear in some IDEs) and sources 5 | file(GLOB HEADER_LIST CONFIGURE_DEPENDS "include/fixtures/*.h") 6 | file(GLOB SOURCE_LIST CONFIGURE_DEPENDS "*.cpp") 7 | 8 | # Automatic library - will be static or dynamic based on user setting 9 | add_library(${MODULE} ${HEADER_LIST} ${SOURCE_LIST}) 10 | 11 | # Provide the includes together with this library 12 | target_include_directories(${MODULE} PUBLIC include) 13 | 14 | # Additional external libraries 15 | include(Boost_DD) 16 | 17 | # Link the additional libraries 18 | target_link_libraries(${MODULE} 19 | PUBLIC 20 | Boost::preprocessor 21 | 22 | PRIVATE 23 | gtest 24 | libdisplaydevice::common 25 | ) 26 | -------------------------------------------------------------------------------- /src/windows/json.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file src/windows/json.cpp 3 | * @brief Definitions for JSON conversion functions (Windows-only). 4 | */ 5 | // header include 6 | #include "display_device/windows/json.h" 7 | 8 | // special ordered include of details 9 | #define DD_JSON_DETAIL 10 | // clang-format off 11 | #include "display_device/windows/detail/json_serializer.h" 12 | #include "display_device/detail/json_converter.h" 13 | // clang-format on 14 | 15 | namespace display_device { 16 | const std::optional JSON_COMPACT {std::nullopt}; 17 | 18 | DD_JSON_DEFINE_CONVERTER(ActiveTopology) 19 | DD_JSON_DEFINE_CONVERTER(DeviceDisplayModeMap) 20 | DD_JSON_DEFINE_CONVERTER(HdrStateMap) 21 | DD_JSON_DEFINE_CONVERTER(SingleDisplayConfigState) 22 | DD_JSON_DEFINE_CONVERTER(WinWorkarounds) 23 | } // namespace display_device 24 | -------------------------------------------------------------------------------- /.github/workflows/_codeql.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # This workflow is centrally managed in https://github.com/LizardByte/.github/ 3 | # Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in 4 | # the above-mentioned repo. 5 | 6 | name: CodeQL 7 | permissions: 8 | actions: read 9 | contents: read 10 | security-events: write 11 | 12 | on: 13 | push: 14 | branches: 15 | - master 16 | pull_request: 17 | branches: 18 | - master 19 | schedule: 20 | - cron: '00 12 * * 0' # every Sunday at 12:00 UTC 21 | 22 | concurrency: 23 | group: "${{ github.workflow }}-${{ github.ref }}" 24 | cancel-in-progress: true 25 | 26 | jobs: 27 | call-codeql: 28 | name: CodeQL 29 | uses: LizardByte/.github/.github/workflows/__call-codeql.yml@master 30 | if: ${{ github.repository != 'LizardByte/.github' }} 31 | -------------------------------------------------------------------------------- /src/common/include/display_device/noop_audio_context.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file src/common/include/display_device/noop_audio_context.h 3 | * @brief Declarations for the NoopAudioContext. 4 | */ 5 | #pragma once 6 | 7 | // local includes 8 | #include "audio_context_interface.h" 9 | 10 | namespace display_device { 11 | /** 12 | * @brief A no-operation implementation for AudioContextInterface. 13 | */ 14 | class NoopAudioContext: public AudioContextInterface { 15 | public: 16 | /** Always returns true and sets m_is_captured to true. */ 17 | [[nodiscard]] bool capture() override; 18 | 19 | /** Returns the m_is_captured value. */ 20 | [[nodiscard]] bool isCaptured() const override; 21 | 22 | /** Sets m_is_captured to false. */ 23 | void release() override; 24 | 25 | private: 26 | bool m_is_captured {}; 27 | }; 28 | } // namespace display_device 29 | -------------------------------------------------------------------------------- /src/common/include/display_device/noop_settings_persistence.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file src/common/include/display_device/noop_settings_persistence.h 3 | * @brief Declarations for NoopSettingsPersistence. 4 | */ 5 | #pragma once 6 | 7 | // local includes 8 | #include "settings_persistence_interface.h" 9 | 10 | namespace display_device { 11 | /** 12 | * @brief A no-operation implementation for SettingsPersistenceInterface. 13 | */ 14 | class NoopSettingsPersistence: public SettingsPersistenceInterface { 15 | public: 16 | /** Always returns true. */ 17 | [[nodiscard]] bool store(const std::vector &) override; 18 | 19 | /** Always returns empty vector. */ 20 | [[nodiscard]] std::optional> load() const override; 21 | 22 | /** Always returns true. */ 23 | [[nodiscard]] bool clear() override; 24 | }; 25 | } // namespace display_device 26 | -------------------------------------------------------------------------------- /scripts/update_clang_format.py: -------------------------------------------------------------------------------- 1 | # standard imports 2 | import os 3 | import subprocess 4 | 5 | # variables 6 | directories = [ 7 | 'src', 8 | 'tests', 9 | ] 10 | file_types = [ 11 | 'c', 12 | 'cpp', 13 | 'h', 14 | 'h', 15 | 'm', 16 | 'mm' 17 | ] 18 | 19 | 20 | def clang_format(file: str): 21 | print(f'Formatting {file} ...') 22 | subprocess.run(['clang-format', '-i', file]) 23 | 24 | 25 | def main(): 26 | """ 27 | Main entry point. 28 | """ 29 | # walk the directories 30 | for directory in directories: 31 | for root, dirs, files in os.walk(directory): 32 | for file in files: 33 | file_path = os.path.join(root, file) 34 | if os.path.isfile(file_path) and file.rsplit('.')[-1] in file_types: 35 | clang_format(file=file_path) 36 | 37 | 38 | if __name__ == '__main__': 39 | main() 40 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # .readthedocs.yaml 3 | # Read the Docs configuration file 4 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 5 | 6 | version: 2 7 | 8 | build: 9 | os: ubuntu-24.04 10 | tools: 11 | python: "miniconda-latest" 12 | commands: 13 | - | 14 | if [ -f readthedocs_build.sh ]; then 15 | doxyconfig_dir="." 16 | else 17 | doxyconfig_dir="./third-party/doxyconfig" 18 | fi 19 | chmod +x "${doxyconfig_dir}/readthedocs_build.sh" 20 | export DOXYCONFIG_DIR="${doxyconfig_dir}" 21 | "${doxyconfig_dir}/readthedocs_build.sh" 22 | 23 | # using conda, we can get newer doxygen and graphviz than ubuntu provide 24 | # https://github.com/readthedocs/readthedocs.org/issues/8151#issuecomment-890359661 25 | conda: 26 | environment: third-party/doxyconfig/environment.yml 27 | 28 | submodules: 29 | include: all 30 | recursive: true 31 | -------------------------------------------------------------------------------- /src/common/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # A global identifier for the library 2 | set(MODULE libdisplaydevice_common) 3 | set(MODULE_ALIAS libdisplaydevice::common) 4 | 5 | # Globing headers (so that they appear in some IDEs) and sources 6 | file(GLOB HEADER_LIST CONFIGURE_DEPENDS "include/display_device/*.h") 7 | file(GLOB HEADER_DETAIL_LIST CONFIGURE_DEPENDS "include/display_device/detail/*.h") 8 | file(GLOB SOURCE_LIST CONFIGURE_DEPENDS "*.cpp") 9 | 10 | # Automatic library - will be static or dynamic based on user setting 11 | add_library(${MODULE} ${HEADER_LIST} ${HEADER_DETAIL_LIST} ${SOURCE_LIST}) 12 | add_library(${MODULE_ALIAS} ALIAS ${MODULE}) 13 | 14 | # Provide the includes together with this library 15 | target_include_directories(${MODULE} PUBLIC include) 16 | 17 | # Additional external libraries 18 | include(Json_DD) 19 | 20 | # Link the additional libraries 21 | target_link_libraries(${MODULE} PRIVATE nlohmann_json::nlohmann_json) 22 | -------------------------------------------------------------------------------- /src/common/include/display_device/detail/json_serializer.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file src/common/include/display_device/detail/json_serializer.h 3 | * @brief Declarations for private JSON serialization helpers. 4 | */ 5 | #pragma once 6 | 7 | // local includes 8 | #include "json_serializer_details.h" 9 | 10 | #ifdef DD_JSON_DETAIL 11 | namespace display_device { 12 | // Enums 13 | DD_JSON_DECLARE_SERIALIZE_TYPE(HdrState) 14 | DD_JSON_DECLARE_SERIALIZE_TYPE(SingleDisplayConfiguration::DevicePreparation) 15 | 16 | // Structs 17 | DD_JSON_DECLARE_SERIALIZE_TYPE(Resolution) 18 | DD_JSON_DECLARE_SERIALIZE_TYPE(Rational) 19 | DD_JSON_DECLARE_SERIALIZE_TYPE(Point) 20 | DD_JSON_DECLARE_SERIALIZE_TYPE(EdidData) 21 | DD_JSON_DECLARE_SERIALIZE_TYPE(EnumeratedDevice::Info) 22 | DD_JSON_DECLARE_SERIALIZE_TYPE(EnumeratedDevice) 23 | DD_JSON_DECLARE_SERIALIZE_TYPE(SingleDisplayConfiguration) 24 | } // namespace display_device 25 | #endif 26 | -------------------------------------------------------------------------------- /src/windows/json_serializer.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file src/windows/json_serializer.cpp 3 | * @brief Definitions for private JSON serialization helpers (Windows-only). 4 | */ 5 | // special ordered include of details 6 | #define DD_JSON_DETAIL 7 | // clang-format off 8 | #include "display_device/windows/types.h" 9 | #include "display_device/windows/detail/json_serializer.h" 10 | // clang-format on 11 | 12 | namespace display_device { 13 | // Structs 14 | DD_JSON_DEFINE_SERIALIZE_STRUCT(DisplayMode, resolution, refresh_rate) 15 | DD_JSON_DEFINE_SERIALIZE_STRUCT(SingleDisplayConfigState::Initial, topology, primary_devices) 16 | DD_JSON_DEFINE_SERIALIZE_STRUCT(SingleDisplayConfigState::Modified, topology, original_modes, original_hdr_states, original_primary_device) 17 | DD_JSON_DEFINE_SERIALIZE_STRUCT(SingleDisplayConfigState, initial, modified) 18 | DD_JSON_DEFINE_SERIALIZE_STRUCT(WinWorkarounds, hdr_blank_delay) 19 | } // namespace display_device 20 | -------------------------------------------------------------------------------- /tests/unit/general/test_noop_audio_context.cpp: -------------------------------------------------------------------------------- 1 | // local includes 2 | #include "display_device/noop_audio_context.h" 3 | #include "fixtures/fixtures.h" 4 | 5 | namespace { 6 | // Test fixture(s) for this file 7 | class NoopAudioContextTest: public BaseTest { 8 | public: 9 | display_device::NoopAudioContext m_impl; 10 | }; 11 | 12 | // Specialized TEST macro(s) for this test file 13 | #define TEST_F_S(...) DD_MAKE_TEST(TEST_F, NoopAudioContextTest, __VA_ARGS__) 14 | } // namespace 15 | 16 | TEST_F_S(Capture) { 17 | EXPECT_FALSE(m_impl.isCaptured()); 18 | EXPECT_TRUE(m_impl.capture()); 19 | EXPECT_TRUE(m_impl.isCaptured()); 20 | EXPECT_TRUE(m_impl.capture()); 21 | EXPECT_TRUE(m_impl.isCaptured()); 22 | } 23 | 24 | TEST_F_S(Release) { 25 | EXPECT_FALSE(m_impl.isCaptured()); 26 | EXPECT_NO_THROW(m_impl.release()); 27 | EXPECT_FALSE(m_impl.isCaptured()); 28 | EXPECT_TRUE(m_impl.capture()); 29 | EXPECT_TRUE(m_impl.isCaptured()); 30 | EXPECT_NO_THROW(m_impl.release()); 31 | EXPECT_FALSE(m_impl.isCaptured()); 32 | } 33 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # This a shared common library for other libraries 2 | add_subdirectory(common) 3 | 4 | # This is a platform-specific library 5 | if(WIN32) 6 | add_subdirectory(windows) 7 | elseif(APPLE) 8 | add_library(libdisplaydevice_macos_dummy INTERFACE) 9 | add_library(libdisplaydevice::platform ALIAS libdisplaydevice_macos_dummy) 10 | message(WARNING "MacOS is not supported yet.") 11 | elseif(UNIX) 12 | add_library(libdisplaydevice_linux_dummy INTERFACE) 13 | add_library(libdisplaydevice::platform ALIAS libdisplaydevice_linux_dummy) 14 | message(WARNING "Linux is not supported yet.") 15 | else() 16 | message(FATAL_ERROR "Unsupported platform") 17 | endif() 18 | 19 | # Create a target that links to everything 20 | add_library(libdisplaydevice_display_device INTERFACE) 21 | target_link_libraries(libdisplaydevice_display_device INTERFACE 22 | libdisplaydevice::common 23 | libdisplaydevice::platform) 24 | 25 | # Create an alias for the main target 26 | add_library(libdisplaydevice::display_device ALIAS libdisplaydevice_display_device) 27 | -------------------------------------------------------------------------------- /.github/workflows/_update-docs.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # This workflow is centrally managed in https://github.com/LizardByte/.github/ 3 | # Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in 4 | # the above-mentioned repo. 5 | 6 | # To use, add the `rtd` repository label to identify repositories that should trigger this workflow. 7 | # If the project slug is not the repository name, add a repository variable named `READTHEDOCS_SLUG` with the value of 8 | # the ReadTheDocs project slug. 9 | 10 | # Update readthedocs on release events. 11 | 12 | name: Update docs 13 | permissions: {} 14 | 15 | on: 16 | release: 17 | types: 18 | - created 19 | - edited 20 | - deleted 21 | 22 | concurrency: 23 | group: "${{ github.workflow }}-${{ github.event.release.tag_name }}" 24 | cancel-in-progress: true 25 | 26 | jobs: 27 | update-docs: 28 | name: Update docs 29 | uses: LizardByte/.github/.github/workflows/__call-update-docs.yml@master 30 | if: github.repository_owner == 'LizardByte' 31 | with: 32 | readthedocs_slug: ${{ vars.READTHEDOCS_SLUG }} 33 | secrets: 34 | READTHEDOCS_TOKEN: ${{ secrets.READTHEDOCS_TOKEN }} 35 | -------------------------------------------------------------------------------- /tests/unit/windows/utils/guards.cpp: -------------------------------------------------------------------------------- 1 | // header include 2 | #include "guards.h" 3 | 4 | // local includes 5 | #include "display_device/windows/settings_utils.h" 6 | 7 | boost::scope::scope_exit makeTopologyGuard(display_device::WinDisplayDeviceInterface &win_dd) { 8 | return boost::scope::scope_exit(display_device::win_utils::topologyGuardFn(win_dd, win_dd.getCurrentTopology())); 9 | } 10 | 11 | boost::scope::scope_exit makeModeGuard(display_device::WinDisplayDeviceInterface &win_dd) { 12 | return boost::scope::scope_exit(display_device::win_utils::modeGuardFn(win_dd, win_dd.getCurrentTopology())); 13 | } 14 | 15 | boost::scope::scope_exit makePrimaryGuard(display_device::WinDisplayDeviceInterface &win_dd) { 16 | return boost::scope::scope_exit(display_device::win_utils::primaryGuardFn(win_dd, win_dd.getCurrentTopology())); 17 | } 18 | 19 | boost::scope::scope_exit makeHdrStateGuard(display_device::WinDisplayDeviceInterface &win_dd) { 20 | return boost::scope::scope_exit(display_device::win_utils::hdrStateGuardFn(win_dd, win_dd.getCurrentTopology())); 21 | } 22 | -------------------------------------------------------------------------------- /src/windows/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # A global identifier for the library 2 | set(MODULE libdisplaydevice_windows) 3 | set(MODULE_ALIAS libdisplaydevice::platform) 4 | 5 | # Globing headers (so that they appear in some IDEs) and sources 6 | file(GLOB HEADER_LIST CONFIGURE_DEPENDS "include/display_device/windows/*.h") 7 | file(GLOB HEADER_DETAIL_LIST CONFIGURE_DEPENDS "include/display_device/windows/detail/*.h") 8 | file(GLOB SOURCE_LIST CONFIGURE_DEPENDS "*.cpp") 9 | 10 | # Automatic library - will be static or dynamic based on user setting 11 | add_library(${MODULE} ${HEADER_LIST} ${HEADER_DETAIL_LIST} ${SOURCE_LIST}) 12 | add_library(${MODULE_ALIAS} ALIAS ${MODULE}) 13 | 14 | # Provide the includes together with this library 15 | target_include_directories(${MODULE} PUBLIC include) 16 | 17 | # Library requires newer WinAPI, therefore it is set to the Windows 10+ version 18 | target_compile_definitions(${MODULE} PRIVATE 19 | _WIN32_WINNT=0x0A00 20 | WINVER=0x0A00 21 | ) 22 | 23 | # Additional external libraries 24 | include(Boost_DD) 25 | include(Json_DD) 26 | 27 | # Link the additional libraries 28 | target_link_libraries(${MODULE} PRIVATE 29 | Boost::algorithm 30 | Boost::scope 31 | Boost::uuid 32 | libdisplaydevice::common 33 | nlohmann_json::nlohmann_json 34 | setupapi) 35 | -------------------------------------------------------------------------------- /tests/fixtures/include/fixtures/test_utils.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // system includes 4 | #include 5 | #include 6 | #include 7 | 8 | // local includes 9 | #include "display_device/types.h" 10 | 11 | /** 12 | * @brief Contains some useful predefined structures for UTs. 13 | * @note Data is to be extended with relevant information as needed. 14 | */ 15 | namespace ut_consts { 16 | extern const std::vector DEFAULT_EDID; 17 | extern const display_device::EdidData DEFAULT_EDID_DATA; 18 | } // namespace ut_consts 19 | 20 | /** 21 | * @brief Test regular expression against string. 22 | * @return True if string matches the regex, false otherwise. 23 | */ 24 | bool testRegex(const std::string &test_pattern, const std::string ®ex_pattern); 25 | 26 | /** 27 | * @brief Set an environment variable. 28 | * @param name Name of the environment variable. 29 | * @param value Value of the environment variable. 30 | * @return 0 on success, non-zero error code on failure. 31 | */ 32 | int setEnv(const std::string &name, const std::string &value); 33 | 34 | /** 35 | * @brief Get an environment variable. 36 | * @param name Name of the environment variable. 37 | * @return String value of the variable or an empty optional otherwise. 38 | */ 39 | std::optional getEnv(const std::string &name); 40 | -------------------------------------------------------------------------------- /src/common/include/display_device/json.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file src/common/include/display_device/json.h 3 | * @brief Declarations for JSON conversion functions. 4 | */ 5 | #pragma once 6 | 7 | // system includes 8 | #include 9 | 10 | // local includes 11 | #include "types.h" 12 | 13 | /** 14 | * @brief Helper MACRO to declare the toJson and fromJson converters for a type. 15 | * @examples 16 | * EnumeratedDeviceList devices; 17 | * DD_LOG(info) << "Got devices:\n" << toJson(devices); 18 | * @examples_end 19 | */ 20 | #define DD_JSON_DECLARE_CONVERTER(Type) \ 21 | [[nodiscard]] std::string toJson(const Type &obj, const std::optional &indent = 2u, bool *success = nullptr); \ 22 | [[nodiscard]] bool fromJson(const std::string &string, Type &obj, std::string *error_message = nullptr); // NOLINT(*-macro-parentheses) 23 | 24 | // Shared converters (add as needed) 25 | namespace display_device { 26 | extern const std::optional JSON_COMPACT; 27 | 28 | DD_JSON_DECLARE_CONVERTER(EdidData) 29 | DD_JSON_DECLARE_CONVERTER(EnumeratedDevice) 30 | DD_JSON_DECLARE_CONVERTER(EnumeratedDeviceList) 31 | DD_JSON_DECLARE_CONVERTER(SingleDisplayConfiguration) 32 | DD_JSON_DECLARE_CONVERTER(std::set) 33 | DD_JSON_DECLARE_CONVERTER(std::string) 34 | DD_JSON_DECLARE_CONVERTER(bool) 35 | } // namespace display_device 36 | -------------------------------------------------------------------------------- /tests/unit/windows/utils/mock_win_display_device.cpp: -------------------------------------------------------------------------------- 1 | // header include 2 | #include "mock_win_display_device.h" 3 | 4 | // local includes 5 | #include "helpers.h" 6 | 7 | namespace ut_consts { 8 | const std::optional SDCS_NULL {std::nullopt}; 9 | const std::optional SDCS_EMPTY {display_device::SingleDisplayConfigState {}}; 10 | const std::optional SDCS_FULL {[]() { 11 | const display_device::SingleDisplayConfigState state { 12 | {{{"DeviceId1"}}, 13 | {"DeviceId1"}}, 14 | {display_device::SingleDisplayConfigState::Modified { 15 | {{"DeviceId1"}, {"DeviceId3"}}, 16 | {{"DeviceId1", {{1920, 1080}, {120, 1}}}, 17 | {"DeviceId3", {{1920, 1080}, {60, 1}}}}, 18 | {{"DeviceId1", {display_device::HdrState::Disabled}}, 19 | {"DeviceId3", display_device::HdrState::Enabled}}, 20 | {"DeviceId1"}, 21 | }} 22 | }; 23 | 24 | return state; 25 | }()}; 26 | const std::optional SDCS_NO_MODIFICATIONS {[]() { 27 | const display_device::SingleDisplayConfigState state { 28 | {{{"DeviceId1"}}, 29 | {"DeviceId1"}}, 30 | {display_device::SingleDisplayConfigState::Modified { 31 | {{"DeviceId1"}, {"DeviceId3"}} 32 | }} 33 | }; 34 | 35 | return state; 36 | }()}; 37 | } // namespace ut_consts 38 | -------------------------------------------------------------------------------- /tests/unit/windows/utils/comparison.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // local includes 4 | #include "display_device/windows/types.h" 5 | 6 | // Helper comparison operators 7 | bool operator==(const LUID &lhs, const LUID &rhs); 8 | 9 | bool operator==(const POINTL &lhs, const POINTL &rhs); 10 | 11 | bool operator==(const RECTL &lhs, const RECTL &rhs); 12 | 13 | bool operator==(const DISPLAYCONFIG_RATIONAL &lhs, const DISPLAYCONFIG_RATIONAL &rhs); 14 | 15 | bool operator==(const DISPLAYCONFIG_2DREGION &lhs, const DISPLAYCONFIG_2DREGION &rhs); 16 | 17 | bool operator==(const DISPLAYCONFIG_PATH_SOURCE_INFO &lhs, const DISPLAYCONFIG_PATH_SOURCE_INFO &rhs); 18 | 19 | bool operator==(const DISPLAYCONFIG_PATH_TARGET_INFO &lhs, const DISPLAYCONFIG_PATH_TARGET_INFO &rhs); 20 | 21 | bool operator==(const DISPLAYCONFIG_PATH_INFO &lhs, const DISPLAYCONFIG_PATH_INFO &rhs); 22 | 23 | bool operator==(const DISPLAYCONFIG_SOURCE_MODE &lhs, const DISPLAYCONFIG_SOURCE_MODE &rhs); 24 | 25 | bool operator==(const DISPLAYCONFIG_VIDEO_SIGNAL_INFO &lhs, const DISPLAYCONFIG_VIDEO_SIGNAL_INFO &rhs); 26 | 27 | bool operator==(const DISPLAYCONFIG_TARGET_MODE &lhs, const DISPLAYCONFIG_TARGET_MODE &rhs); 28 | 29 | bool operator==(const DISPLAYCONFIG_DESKTOP_IMAGE_INFO &lhs, const DISPLAYCONFIG_DESKTOP_IMAGE_INFO &rhs); 30 | 31 | bool operator==(const DISPLAYCONFIG_MODE_INFO &lhs, const DISPLAYCONFIG_MODE_INFO &rhs); 32 | 33 | namespace display_device { 34 | bool operator==(const PathSourceIndexData &lhs, const PathSourceIndexData &rhs); 35 | } // namespace display_device 36 | -------------------------------------------------------------------------------- /src/windows/types.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file src/windows/types.cpp 3 | * @brief Definitions for Windows specific types. 4 | */ 5 | // header include 6 | #include "display_device/windows/types.h" 7 | 8 | namespace display_device { 9 | bool operator==(const DisplayMode &lhs, const DisplayMode &rhs) { 10 | return lhs.m_refresh_rate == rhs.m_refresh_rate && lhs.m_resolution == rhs.m_resolution; 11 | } 12 | 13 | bool SingleDisplayConfigState::Modified::hasModifications() const { 14 | return !m_original_modes.empty() || !m_original_hdr_states.empty() || !m_original_primary_device.empty(); 15 | } 16 | 17 | bool operator==(const SingleDisplayConfigState::Initial &lhs, const SingleDisplayConfigState::Initial &rhs) { 18 | return lhs.m_topology == rhs.m_topology && lhs.m_primary_devices == rhs.m_primary_devices; 19 | } 20 | 21 | bool operator==(const SingleDisplayConfigState::Modified &lhs, const SingleDisplayConfigState::Modified &rhs) { 22 | return lhs.m_topology == rhs.m_topology && lhs.m_original_modes == rhs.m_original_modes && lhs.m_original_hdr_states == rhs.m_original_hdr_states && lhs.m_original_primary_device == rhs.m_original_primary_device; 23 | } 24 | 25 | bool operator==(const SingleDisplayConfigState &lhs, const SingleDisplayConfigState &rhs) { 26 | return lhs.m_initial == rhs.m_initial && lhs.m_modified == rhs.m_modified; 27 | } 28 | 29 | bool operator==(const WinWorkarounds &lhs, const WinWorkarounds &rhs) { 30 | return lhs.m_hdr_blank_delay == rhs.m_hdr_blank_delay; 31 | } 32 | } // namespace display_device 33 | -------------------------------------------------------------------------------- /tests/unit/windows/utils/helpers.cpp: -------------------------------------------------------------------------------- 1 | // header include 2 | #include "helpers.h" 3 | 4 | // local includes 5 | #include "display_device/windows/json.h" 6 | 7 | std::optional> getAvailableDevices(display_device::WinApiLayer &layer, const bool only_valid_output) { 8 | const auto all_devices {layer.queryDisplayConfig(display_device::QueryType::All)}; 9 | if (!all_devices) { 10 | return std::nullopt; 11 | } 12 | 13 | std::set device_ids; 14 | for (const auto &path : all_devices->m_paths) { 15 | if (only_valid_output && path.targetInfo.outputTechnology == DISPLAYCONFIG_OUTPUT_TECHNOLOGY_OTHER) { 16 | continue; 17 | } 18 | 19 | const auto device_id {layer.getDeviceId(path)}; 20 | if (!device_id.empty()) { 21 | device_ids.insert(device_id); 22 | } 23 | } 24 | 25 | return std::vector {device_ids.begin(), device_ids.end()}; 26 | } 27 | 28 | std::optional> serializeState(const std::optional &state) { 29 | if (state) { 30 | if (state->m_initial.m_topology.empty() && state->m_initial.m_primary_devices.empty() && state->m_modified.m_topology.empty() && !state->m_modified.hasModifications()) { 31 | return std::vector {}; 32 | } 33 | 34 | bool is_ok {false}; 35 | const auto data_string {toJson(*state, 2, &is_ok)}; 36 | if (is_ok) { 37 | return std::vector {std::begin(data_string), std::end(data_string)}; 38 | } 39 | } 40 | 41 | return std::nullopt; 42 | } 43 | -------------------------------------------------------------------------------- /docs/Doxyfile: -------------------------------------------------------------------------------- 1 | # This file describes the settings to be used by the documentation system 2 | # doxygen (www.doxygen.org) for a project. 3 | # 4 | # All text after a double hash (##) is considered a comment and is placed in 5 | # front of the TAG it is preceding. 6 | # 7 | # All text after a single hash (#) is considered a comment and will be ignored. 8 | # The format is: 9 | # TAG = value [value, ...] 10 | # For lists, items can also be appended using: 11 | # TAG += value [value, ...] 12 | # Values that contain spaces should be placed between quotes (\" \"). 13 | # 14 | # Note: 15 | # 16 | # Use doxygen to compare the used configuration file with the template 17 | # configuration file: 18 | # doxygen -x [configFile] 19 | # Use doxygen to compare the used configuration file with the template 20 | # configuration file without replacing the environment variables or CMake type 21 | # replacement variables: 22 | # doxygen -x_noenv [configFile] 23 | 24 | # project metadata 25 | DOCSET_BUNDLE_ID = dev.lizardbyte.libdisplaydevice 26 | DOCSET_PUBLISHER_ID = dev.lizardbyte.libdisplaydevice.documentation 27 | PROJECT_BRIEF = "C++ library to modify display devices." 28 | PROJECT_NAME = libdisplaydevice 29 | 30 | # project specific settings 31 | DOT_GRAPH_MAX_NODES = 50 32 | # IMAGE_PATH = ../docs/images 33 | INCLUDE_PATH = 34 | 35 | # TODO: Enable this when we have complete documentation 36 | WARN_IF_UNDOCUMENTED = NO 37 | 38 | # files and directories to process 39 | USE_MDFILE_AS_MAINPAGE = ../README.md 40 | INPUT = ../README.md \ 41 | ../third-party/doxyconfig/docs/source_code.md \ 42 | ../src 43 | -------------------------------------------------------------------------------- /src/common/json_serializer.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file src/common/json_serializer.cpp 3 | * @brief Definitions for private JSON serialization helpers. 4 | */ 5 | // special ordered include of details 6 | #define DD_JSON_DETAIL 7 | // clang-format off 8 | #include "display_device/types.h" 9 | #include "display_device/detail/json_serializer.h" 10 | // clang-format on 11 | 12 | namespace display_device { 13 | // Enums 14 | DD_JSON_DEFINE_SERIALIZE_ENUM_GCOVR_EXCL_BR_LINE(HdrState, {{HdrState::Disabled, "Disabled"}, {HdrState::Enabled, "Enabled"}}) 15 | DD_JSON_DEFINE_SERIALIZE_ENUM_GCOVR_EXCL_BR_LINE(SingleDisplayConfiguration::DevicePreparation, {{SingleDisplayConfiguration::DevicePreparation::VerifyOnly, "VerifyOnly"}, {SingleDisplayConfiguration::DevicePreparation::EnsureActive, "EnsureActive"}, {SingleDisplayConfiguration::DevicePreparation::EnsurePrimary, "EnsurePrimary"}, {SingleDisplayConfiguration::DevicePreparation::EnsureOnlyDisplay, "EnsureOnlyDisplay"}}) 16 | 17 | // Structs 18 | DD_JSON_DEFINE_SERIALIZE_STRUCT(Resolution, width, height) 19 | DD_JSON_DEFINE_SERIALIZE_STRUCT(Rational, numerator, denominator) 20 | DD_JSON_DEFINE_SERIALIZE_STRUCT(Point, x, y) 21 | DD_JSON_DEFINE_SERIALIZE_STRUCT(EdidData, manufacturer_id, product_code, serial_number) 22 | DD_JSON_DEFINE_SERIALIZE_STRUCT(EnumeratedDevice::Info, resolution, resolution_scale, refresh_rate, primary, origin_point, hdr_state) 23 | DD_JSON_DEFINE_SERIALIZE_STRUCT(EnumeratedDevice, device_id, display_name, friendly_name, edid, info) 24 | DD_JSON_DEFINE_SERIALIZE_STRUCT(SingleDisplayConfiguration, device_id, device_prep, resolution, refresh_rate, hdr_state) 25 | } // namespace display_device 26 | -------------------------------------------------------------------------------- /tests/unit/general/test_edid_parsing.cpp: -------------------------------------------------------------------------------- 1 | // local includes 2 | #include "display_device/types.h" 3 | #include "fixtures/fixtures.h" 4 | 5 | namespace { 6 | // Specialized TEST macro(s) for this test file 7 | #define TEST_S(...) DD_MAKE_TEST(TEST, EdidParsing, __VA_ARGS__) 8 | } // namespace 9 | 10 | TEST_S(NoData) { 11 | EXPECT_EQ(display_device::EdidData::parse({}), std::nullopt); 12 | } 13 | 14 | TEST_S(TooLittleData) { 15 | EXPECT_EQ(display_device::EdidData::parse({std::byte {0x11}}), std::nullopt); 16 | } 17 | 18 | TEST_S(BadFixedHeader) { 19 | auto EDID_DATA {ut_consts::DEFAULT_EDID}; 20 | EDID_DATA[1] = std::byte {0xAA}; 21 | EXPECT_EQ(display_device::EdidData::parse(EDID_DATA), std::nullopt); 22 | } 23 | 24 | TEST_S(BadChecksum) { 25 | auto EDID_DATA {ut_consts::DEFAULT_EDID}; 26 | EDID_DATA[16] = std::byte {0x00}; 27 | EXPECT_EQ(display_device::EdidData::parse(EDID_DATA), std::nullopt); 28 | } 29 | 30 | TEST_S(InvalidManufacturerId, BelowLimit) { 31 | auto EDID_DATA {ut_consts::DEFAULT_EDID}; 32 | // The sum of 8th and 9th bytes should remain 109 33 | EDID_DATA[8] = std::byte {0x00}; 34 | EDID_DATA[9] = std::byte {0x6D}; 35 | EXPECT_EQ(display_device::EdidData::parse(EDID_DATA), std::nullopt); 36 | } 37 | 38 | TEST_S(InvalidManufacturerId, AboveLimit) { 39 | auto EDID_DATA {ut_consts::DEFAULT_EDID}; 40 | // The sum of 8th and 9th bytes should remain 109 41 | EDID_DATA[8] = std::byte {0x6D}; 42 | EDID_DATA[9] = std::byte {0x00}; 43 | EXPECT_EQ(display_device::EdidData::parse(EDID_DATA), std::nullopt); 44 | } 45 | 46 | TEST_S(ValidOutput) { 47 | EXPECT_EQ(display_device::EdidData::parse(ut_consts::DEFAULT_EDID), ut_consts::DEFAULT_EDID_DATA); 48 | } 49 | -------------------------------------------------------------------------------- /cmake/Boost_DD.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Loads the boost library giving the priority to the system package first, with a fallback 3 | # to the submodule. 4 | # 5 | include_guard(GLOBAL) 6 | 7 | # Limit boost to the required libraries only 8 | set(REQUIRED_HEADER_LIBRARIES 9 | algorithm 10 | scope 11 | preprocessor 12 | uuid 13 | ) 14 | 15 | find_package(Boost 1.85 CONFIG QUIET GLOBAL) 16 | if(NOT Boost_FOUND) 17 | message(STATUS "Boost v1.85.x package not found in the system. Falling back to FetchContent.") 18 | include(FetchContent) 19 | 20 | set(BOOST_INCLUDE_LIBRARIES ${REQUIRED_HEADER_LIBRARIES}) 21 | FetchContent_Declare( 22 | Boost 23 | URL https://github.com/boostorg/boost/releases/download/boost-1.85.0/boost-1.85.0-cmake.tar.xz 24 | URL_HASH MD5=BADEA970931766604D4D5F8F4090B176 25 | DOWNLOAD_EXTRACT_TIMESTAMP 26 | ) 27 | FetchContent_MakeAvailable(Boost) 28 | else() 29 | # For whatever reason the Boost::headers from find_package is not the same as the one from FetchContent 30 | # (differ in linked targets). 31 | # Also, FetchContent creates Boost:: targets, whereas find_package does not. Since we cannot extend 32 | # Boost::headers as it is an ALIAS target, this is the workaround: 33 | get_target_property(ORIGINAL_TARGET Boost::headers ALIASED_TARGET) 34 | if (ORIGINAL_TARGET STREQUAL "ORIGINAL_TARGET-NOTFOUND") 35 | set(ORIGINAL_TARGET Boost::headers) 36 | endif () 37 | foreach (lib ${REQUIRED_HEADER_LIBRARIES}) 38 | if (NOT TARGET Boost::${lib}) 39 | add_library(Boost::${lib} ALIAS ${ORIGINAL_TARGET}) 40 | endif () 41 | endforeach () 42 | endif() 43 | -------------------------------------------------------------------------------- /src/windows/include/display_device/windows/persistent_state.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file src/windows/include/display_device/windows/persistent_state.h 3 | * @brief Declarations for the PersistentState. 4 | */ 5 | #pragma once 6 | 7 | // system includes 8 | #include 9 | 10 | // local includes 11 | #include "display_device/settings_persistence_interface.h" 12 | #include "display_device/windows/win_display_device_interface.h" 13 | 14 | namespace display_device { 15 | /** 16 | * @brief A simple wrapper around the SettingsPersistenceInterface and cached local state to keep them in sync. 17 | */ 18 | class PersistentState { 19 | public: 20 | /** 21 | * Default constructor for the class. 22 | * @param settings_persistence_api [Optional] A pointer to the Settings Persistence interface. 23 | * @param throw_on_load_error Specify whether to throw exception in constructor in case settings fail to load. 24 | */ 25 | explicit PersistentState(std::shared_ptr settings_persistence_api, bool throw_on_load_error = false); 26 | 27 | /** 28 | * @brief Store the new state via the interface and cache it. 29 | * @param state New state to be set. 30 | * @return True if the state was succesfully updated, false otherwise. 31 | */ 32 | [[nodiscard]] bool persistState(const std::optional &state); 33 | 34 | /** 35 | * @brief Get cached state. 36 | * @return Cached state 37 | */ 38 | [[nodiscard]] const std::optional &getState() const; 39 | 40 | protected: 41 | std::shared_ptr m_settings_persistence_api; 42 | 43 | private: 44 | std::optional m_cached_state; 45 | }; 46 | } // namespace display_device 47 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # 2 | # Project configuration 3 | # 4 | cmake_minimum_required(VERSION 3.24) 5 | project(libdisplaydevice VERSION 0.0.0 6 | DESCRIPTION "Library to modify display devices." 7 | HOMEPAGE_URL "https://app.lizardbyte.dev" 8 | LANGUAGES CXX) 9 | 10 | set(PROJECT_LICENSE "GPL-3.0") 11 | set(CMAKE_CXX_STANDARD 20) 12 | 13 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) 14 | message(STATUS "Setting build type to 'Release' as none was specified.") 15 | set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Choose the type of build." FORCE) 16 | endif() 17 | 18 | # Add our custom CMake modules to the global path 19 | list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake") 20 | 21 | # 22 | # Project optional configuration 23 | # 24 | if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) 25 | option(BUILD_DOCS "Build documentation" ON) 26 | option(BUILD_TESTS "Build tests" ON) 27 | endif() 28 | 29 | # 30 | # Testing and documentation are only available if this is the main project 31 | # 32 | if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) 33 | if(BUILD_DOCS) 34 | add_subdirectory(third-party/doxyconfig docs) 35 | endif() 36 | 37 | if(BUILD_TESTS) 38 | # 39 | # Additional setup for coverage 40 | # https://gcovr.com/en/stable/guide/compiling.html#compiler-options 41 | # 42 | if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") 43 | set(CMAKE_CXX_FLAGS "-fprofile-arcs -ftest-coverage -ggdb -O0") 44 | set(CMAKE_C_FLAGS "-fprofile-arcs -ftest-coverage -ggdb -O0") 45 | endif() 46 | 47 | enable_testing() 48 | add_subdirectory(tests) 49 | endif() 50 | endif() 51 | 52 | # 53 | # Library code is located here 54 | # When building tests this must be after the coverage flags are set 55 | # 56 | add_subdirectory(src) 57 | -------------------------------------------------------------------------------- /src/common/include/display_device/audio_context_interface.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file src/common/include/display_device/audio_context_interface.h 3 | * @brief Declarations for the AudioContextInterface. 4 | */ 5 | #pragma once 6 | 7 | namespace display_device { 8 | /** 9 | * @brief A class for capturing associated audio context (settings, info or whatever). 10 | * 11 | * Some of the display devices have audio devices associated with them. 12 | * Turning off and on the devices will not necessarily restore them as the default 13 | * audio devices for the system. 14 | */ 15 | class AudioContextInterface { 16 | public: 17 | /** 18 | * @brief Default virtual destructor. 19 | */ 20 | virtual ~AudioContextInterface() = default; 21 | 22 | /** 23 | * @brief Capture audio context for currently active devices. 24 | * @returns True if the contexts could be captured, false otherwise. 25 | * @examples 26 | * AudioContextInterface* iface = getIface(...); 27 | * const auto result { iface->capture() }; 28 | * @examples_end 29 | */ 30 | [[nodiscard]] virtual bool capture() = 0; 31 | 32 | /** 33 | * @brief Check if the context is already captured. 34 | * @returns True if the context is captured, false otherwise. 35 | * @examples 36 | * AudioContextInterface* iface = getIface(...); 37 | * const auto result { iface->isCaptured() }; 38 | * @examples_end 39 | */ 40 | [[nodiscard]] virtual bool isCaptured() const = 0; 41 | 42 | /** 43 | * @brief Release captured audio context for the devices (if any). 44 | * @examples 45 | * AudioContextInterface* iface = getIface(...); 46 | * const auto result { iface->release() }; 47 | * @examples_end 48 | */ 49 | virtual void release() = 0; 50 | }; 51 | } // namespace display_device 52 | -------------------------------------------------------------------------------- /src/common/include/display_device/file_settings_persistence.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file src/common/include/display_device/file_settings_persistence.h 3 | * @brief Declarations for persistent file settings. 4 | */ 5 | #pragma once 6 | 7 | // system includes 8 | #include 9 | 10 | // local includes 11 | #include "settings_persistence_interface.h" 12 | 13 | namespace display_device { 14 | /** 15 | * @brief Implementation of the SettingsPersistenceInterface, 16 | * that saves/loads the persistent settings to/from the file. 17 | */ 18 | class FileSettingsPersistence: public SettingsPersistenceInterface { 19 | public: 20 | /** 21 | * Default constructor. Does not perform any operations on the file yet. 22 | * @param filepath A non-empty filepath. Throws on empty. 23 | */ 24 | explicit FileSettingsPersistence(std::filesystem::path filepath); 25 | 26 | /** 27 | * Store the data in the file specified in constructor. 28 | * @warning The method does not create missing directories! 29 | * @see SettingsPersistenceInterface::store for more details. 30 | */ 31 | [[nodiscard]] bool store(const std::vector &data) override; 32 | 33 | /** 34 | * Read the data from the file specified in constructor. 35 | * @note If file does not exist, an empty data list will be returned instead of null optional. 36 | * @see SettingsPersistenceInterface::load for more details. 37 | */ 38 | [[nodiscard]] std::optional> load() const override; 39 | 40 | /** 41 | * Remove the file specified in constructor (if it exists). 42 | * @see SettingsPersistenceInterface::clear for more details. 43 | */ 44 | [[nodiscard]] bool clear() override; 45 | 46 | private: 47 | std::filesystem::path m_filepath; 48 | }; 49 | } // namespace display_device 50 | -------------------------------------------------------------------------------- /src/common/include/display_device/settings_persistence_interface.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file src/common/include/display_device/settings_persistence_interface.h 3 | * @brief Declarations for the SettingsPersistenceInterface. 4 | */ 5 | #pragma once 6 | 7 | // system includes 8 | #include 9 | #include 10 | #include 11 | 12 | namespace display_device { 13 | /** 14 | * @brief A class for storing and loading settings data from a persistent medium. 15 | */ 16 | class SettingsPersistenceInterface { 17 | public: 18 | /** 19 | * @brief Default virtual destructor. 20 | */ 21 | virtual ~SettingsPersistenceInterface() = default; 22 | 23 | /** 24 | * @brief Store the provided data. 25 | * @param data Data array to store. 26 | * @returns True on success, false otherwise. 27 | * @examples 28 | * std::vector data; 29 | * SettingsPersistenceInterface* iface = getIface(...); 30 | * const auto result = iface->store(data); 31 | * @examples_end 32 | */ 33 | [[nodiscard]] virtual bool store(const std::vector &data) = 0; 34 | 35 | /** 36 | * @brief Load saved settings data. 37 | * @returns Null optional if failed to load data. 38 | * Empty array, if there is no data. 39 | * Non-empty array, if some data was loaded. 40 | * @examples 41 | * const SettingsPersistenceInterface* iface = getIface(...); 42 | * const auto opt_data = iface->load(); 43 | * @examples_end 44 | */ 45 | [[nodiscard]] virtual std::optional> load() const = 0; 46 | 47 | /** 48 | * @brief Clear the persistent settings data. 49 | * @returns True if data was cleared, false otherwise. 50 | * @examples 51 | * SettingsPersistenceInterface* iface = getIface(...); 52 | * const auto result = iface->clear(); 53 | * @examples_end 54 | */ 55 | [[nodiscard]] virtual bool clear() = 0; 56 | }; 57 | } // namespace display_device 58 | -------------------------------------------------------------------------------- /src/common/include/display_device/detail/json_converter.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file src/common/include/display_device/detail/json_converter.h 3 | * @brief Declarations for private JSON conversion helpers. 4 | */ 5 | #pragma once 6 | 7 | #ifdef DD_JSON_DETAIL 8 | // system includes 9 | #include 10 | 11 | namespace display_device { 12 | // A shared "toJson" implementation. Extracted here for UTs + coverage. 13 | template 14 | std::string toJsonHelper(const Type &obj, const std::optional &indent, bool *success) { 15 | try { 16 | if (success) { 17 | *success = true; 18 | } 19 | 20 | nlohmann::json json_obj = obj; 21 | return json_obj.dump(static_cast(indent.value_or(-1))); 22 | } catch (const std::exception &err) { // GCOVR_EXCL_BR_LINE for fallthrough branch 23 | if (success) { 24 | *success = false; 25 | } 26 | 27 | return err.what(); 28 | } 29 | } 30 | 31 | // A shared "fromJson" implementation. Extracted here for UTs + coverage. 32 | template 33 | bool fromJsonHelper(const std::string &string, Type &obj, std::string *error_message = nullptr) { 34 | try { 35 | if (error_message) { 36 | error_message->clear(); 37 | } 38 | 39 | Type parsed_obj = nlohmann::json::parse(string); 40 | obj = std::move(parsed_obj); 41 | return true; 42 | } catch (const std::exception &err) { 43 | if (error_message) { 44 | *error_message = err.what(); 45 | } 46 | 47 | return false; 48 | } 49 | } 50 | 51 | #define DD_JSON_DEFINE_CONVERTER(Type) \ 52 | std::string toJson(const Type &obj, const std::optional &indent, bool *success) { \ 53 | return toJsonHelper(obj, indent, success); \ 54 | } \ 55 | bool fromJson(const std::string &string, Type &obj, std::string *error_message) { \ 56 | return fromJsonHelper(string, obj, error_message); \ 57 | } 58 | } // namespace display_device 59 | #endif 60 | -------------------------------------------------------------------------------- /tests/unit/windows/test_comparison.cpp: -------------------------------------------------------------------------------- 1 | // local includes 2 | #include "display_device/windows/types.h" 3 | #include "fixtures/fixtures.h" 4 | 5 | namespace { 6 | // Specialized TEST macro(s) for this test file 7 | #define TEST_S(...) DD_MAKE_TEST(TEST, TypeComparison, __VA_ARGS__) 8 | } // namespace 9 | 10 | TEST_S(DisplayMode) { 11 | EXPECT_EQ(display_device::DisplayMode({1, 1}, {1, 1}), display_device::DisplayMode({1, 1}, {1, 1})); 12 | EXPECT_NE(display_device::DisplayMode({1, 1}, {1, 1}), display_device::DisplayMode({1, 0}, {1, 1})); 13 | EXPECT_NE(display_device::DisplayMode({1, 1}, {1, 1}), display_device::DisplayMode({1, 1}, {1, 0})); 14 | } 15 | 16 | TEST_S(SingleDisplayConfigState, Initial) { 17 | using Initial = display_device::SingleDisplayConfigState::Initial; 18 | EXPECT_EQ(Initial({{{"1"}}}, {"1"}), Initial({{{"1"}}}, {"1"})); 19 | EXPECT_NE(Initial({{{"1"}}}, {"1"}), Initial({{{"0"}}}, {"1"})); 20 | EXPECT_NE(Initial({{{"1"}}}, {"1"}), Initial({{{"1"}}}, {"0"})); 21 | } 22 | 23 | TEST_S(SingleDisplayConfigState, Modified) { 24 | using Modified = display_device::SingleDisplayConfigState::Modified; 25 | EXPECT_EQ(Modified({{{"1"}}}, {{"1", {}}}, {{"1", {}}}, "1"), Modified({{{"1"}}}, {{"1", {}}}, {{"1", {}}}, "1")); 26 | EXPECT_NE(Modified({{{"1"}}}, {{"1", {}}}, {{"1", {}}}, "1"), Modified({{{"0"}}}, {{"1", {}}}, {{"1", {}}}, "1")); 27 | EXPECT_NE(Modified({{{"1"}}}, {{"1", {}}}, {{"1", {}}}, "1"), Modified({{{"1"}}}, {{"0", {}}}, {{"1", {}}}, "1")); 28 | EXPECT_NE(Modified({{{"1"}}}, {{"1", {}}}, {{"1", {}}}, "1"), Modified({{{"1"}}}, {{"1", {}}}, {{"0", {}}}, "1")); 29 | EXPECT_NE(Modified({{{"1"}}}, {{"1", {}}}, {{"1", {}}}, "1"), Modified({{{"1"}}}, {{"1", {}}}, {{"1", {}}}, "0")); 30 | } 31 | 32 | TEST_S(SingleDisplayConfigState) { 33 | using SDSC = display_device::SingleDisplayConfigState; 34 | EXPECT_EQ(SDSC({{{"1"}}}, {{{"1"}}}), SDSC({{{"1"}}}, {{{"1"}}})); 35 | EXPECT_NE(SDSC({{{"1"}}}, {{{"1"}}}), SDSC({{{"0"}}}, {{{"1"}}})); 36 | EXPECT_NE(SDSC({{{"1"}}}, {{{"1"}}}), SDSC({{{"1"}}}, {{{"0"}}})); 37 | } 38 | -------------------------------------------------------------------------------- /tests/unit/windows/utils/mock_win_display_device.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // system includes 4 | #include 5 | 6 | // local includes 7 | #include "display_device/windows/win_display_device.h" 8 | 9 | namespace display_device { 10 | class MockWinDisplayDevice: public WinDisplayDeviceInterface { 11 | public: 12 | MOCK_METHOD(bool, isApiAccessAvailable, (), (const, override)); 13 | MOCK_METHOD(EnumeratedDeviceList, enumAvailableDevices, (), (const, override)); 14 | MOCK_METHOD(std::string, getDisplayName, (const std::string &), (const, override)); 15 | MOCK_METHOD(ActiveTopology, getCurrentTopology, (), (const, override)); 16 | MOCK_METHOD(bool, isTopologyValid, (const ActiveTopology &), (const, override)); 17 | MOCK_METHOD(bool, isTopologyTheSame, (const ActiveTopology &, const ActiveTopology &), (const, override)); 18 | MOCK_METHOD(bool, setTopology, (const ActiveTopology &), (override)); 19 | MOCK_METHOD(DeviceDisplayModeMap, getCurrentDisplayModes, (const std::set &), (const, override)); 20 | MOCK_METHOD(bool, setDisplayModes, (const DeviceDisplayModeMap &), (override)); 21 | MOCK_METHOD(bool, isPrimary, (const std::string &), (const, override)); 22 | MOCK_METHOD(bool, setAsPrimary, (const std::string &), (override)); 23 | MOCK_METHOD(HdrStateMap, getCurrentHdrStates, (const std::set &), (const, override)); 24 | MOCK_METHOD(bool, setHdrStates, (const HdrStateMap &), (override)); 25 | }; 26 | } // namespace display_device 27 | 28 | /** 29 | * @brief Contains some useful predefined structures for UTs. 30 | * @note Data is to be extended with relevant information as needed. 31 | */ 32 | namespace ut_consts { 33 | extern const std::optional SDCS_NULL; 34 | extern const std::optional SDCS_EMPTY; 35 | extern const std::optional SDCS_FULL; 36 | extern const std::optional SDCS_NO_MODIFICATIONS; 37 | } // namespace ut_consts 38 | -------------------------------------------------------------------------------- /tests/unit/windows/utils/mock_win_api_layer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // system includes 4 | #include 5 | 6 | // local includes 7 | #include "display_device/windows/win_api_layer_interface.h" 8 | 9 | namespace display_device { 10 | class MockWinApiLayer: public WinApiLayerInterface { 11 | public: 12 | MOCK_METHOD(std::string, getErrorString, (LONG), (const, override)); 13 | MOCK_METHOD(std::optional, queryDisplayConfig, (QueryType), (const, override)); 14 | MOCK_METHOD(std::string, getDeviceId, (const DISPLAYCONFIG_PATH_INFO &), (const, override)); 15 | MOCK_METHOD(std::vector, getEdid, (const DISPLAYCONFIG_PATH_INFO &), (const, override)); 16 | MOCK_METHOD(std::string, getMonitorDevicePath, (const DISPLAYCONFIG_PATH_INFO &), (const, override)); 17 | MOCK_METHOD(std::string, getFriendlyName, (const DISPLAYCONFIG_PATH_INFO &), (const, override)); 18 | MOCK_METHOD(std::string, getDisplayName, (const DISPLAYCONFIG_PATH_INFO &), (const, override)); 19 | MOCK_METHOD(LONG, setDisplayConfig, (std::vector, std::vector, UINT32), (override)); 20 | MOCK_METHOD(std::optional, getHdrState, (const DISPLAYCONFIG_PATH_INFO &), (const, override)); 21 | MOCK_METHOD(bool, setHdrState, (const DISPLAYCONFIG_PATH_INFO &, HdrState), (override)); 22 | MOCK_METHOD(std::optional, getDisplayScale, (const std::string &, const DISPLAYCONFIG_SOURCE_MODE &), (const, override)); 23 | }; 24 | } // namespace display_device 25 | 26 | /** 27 | * @brief Contains some useful predefined structures for UTs. 28 | * @note Data is to be extended with relevant information as needed. 29 | */ 30 | namespace ut_consts { 31 | extern const std::optional PAM_NULL; 32 | extern const std::optional PAM_EMPTY; 33 | extern const std::optional PAM_3_ACTIVE; 34 | extern const std::optional PAM_3_ACTIVE_WITH_INVALID_MODE_IDX; 35 | extern const std::optional PAM_4_ACTIVE_WITH_2_DUPLICATES; 36 | } // namespace ut_consts 37 | -------------------------------------------------------------------------------- /src/windows/settings_manager_general.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file src/windows/settings_manager_general.cpp 3 | * @brief Definitions for the leftover (general) methods in SettingsManager. 4 | */ 5 | // class header include 6 | #include "display_device/windows/settings_manager.h" 7 | 8 | // local includes 9 | #include "display_device/logging.h" 10 | #include "display_device/noop_audio_context.h" 11 | #include "display_device/windows/json.h" 12 | 13 | namespace display_device { 14 | SettingsManager::SettingsManager( 15 | std::shared_ptr dd_api, 16 | std::shared_ptr audio_context_api, 17 | std::unique_ptr persistent_state, 18 | WinWorkarounds workarounds 19 | ): 20 | m_dd_api {std::move(dd_api)}, 21 | m_audio_context_api {std::move(audio_context_api)}, 22 | m_persistence_state {std::move(persistent_state)}, 23 | m_workarounds {std::move(workarounds)} { 24 | if (!m_dd_api) { 25 | throw std::logic_error {"Nullptr provided for WinDisplayDeviceInterface in SettingsManager!"}; 26 | } 27 | 28 | if (!m_audio_context_api) { 29 | m_audio_context_api = std::make_shared(); 30 | } 31 | 32 | if (!m_persistence_state) { 33 | throw std::logic_error {"Nullptr provided for PersistentState in SettingsManager!"}; 34 | } 35 | 36 | DD_LOG(info) << "Provided workaround settings for SettingsManager:\n" 37 | << toJson(m_workarounds); 38 | } 39 | 40 | EnumeratedDeviceList SettingsManager::enumAvailableDevices() const { 41 | return m_dd_api->enumAvailableDevices(); 42 | } 43 | 44 | std::string SettingsManager::getDisplayName(const std::string &device_id) const { 45 | return m_dd_api->getDisplayName(device_id); 46 | } 47 | 48 | bool SettingsManager::resetPersistence() { 49 | DD_LOG(info) << "Trying to reset persistent display device settings."; 50 | if (const auto &cached_state {m_persistence_state->getState()}; !cached_state) { 51 | return true; 52 | } 53 | 54 | if (!m_persistence_state->persistState(std::nullopt)) { 55 | DD_LOG(error) << "Failed to clear persistence!"; 56 | return false; 57 | } 58 | 59 | if (m_audio_context_api->isCaptured()) { 60 | m_audio_context_api->release(); 61 | } 62 | return true; 63 | } 64 | } // namespace display_device 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | [![GitHub Workflow Status (CI)](https://img.shields.io/github/actions/workflow/status/lizardbyte/libdisplaydevice/ci.yml.svg?branch=master&label=CI%20build&logo=github&style=for-the-badge)](https://github.com/LizardByte/libdisplaydevice/actions/workflows/ci.yml?query=branch%3Amaster) 4 | [![Codecov](https://img.shields.io/codecov/c/gh/LizardByte/libdisplaydevice?token=goyvmDl6J5&style=for-the-badge&logo=codecov&label=codecov)](https://codecov.io/gh/LizardByte/libdisplaydevice) 5 | [![GitHub stars](https://img.shields.io/github/stars/lizardbyte/libdisplaydevice.svg?logo=github&style=for-the-badge)](https://github.com/LizardByte/libdisplaydevice) 6 | 7 | ## About 8 | 9 | LizardByte has the full documentation hosted on [Read the Docs](https://libdisplaydevice.readthedocs.io/). 10 | 11 | libdisplaydevice is a WIP library that provides a common interface for interacting with display devices. 12 | It is intended to be used by applications that need to interact with displays, such as screen capture software, 13 | remote desktop software, and video players. 14 | 15 | Initial support is planned for Windows, but could be expanded to other platforms in the future. 16 | 17 | ## Build 18 | 19 | ### Clone 20 | 21 | Ensure [git](https://git-scm.com/) is installed and run the following: 22 | 23 | ```bash 24 | git clone https://github.com/lizardbyte/libdisplaydevice.git --recurse-submodules 25 | cd libdisplaydevice 26 | mkdir -p build 27 | ``` 28 | 29 | ### Windows 30 | 31 | #### Requirements 32 | 33 | First you need to install [MSYS2](https://www.msys2.org), then startup "MSYS2 UCRT64" and execute the following 34 | commands. 35 | 36 | Update all packages: 37 | ```bash 38 | pacman -Syu 39 | ``` 40 | 41 | Install dependencies: 42 | ```bash 43 | pacman -S \ 44 | doxygen \ 45 | mingw-w64-ucrt-x86_64-binutils \ 46 | mingw-w64-ucrt-x86_64-cmake \ 47 | mingw-w64-ucrt-x86_64-graphviz \ 48 | mingw-w64-ucrt-x86_64-ninja \ 49 | mingw-w64-ucrt-x86_64-toolchain \ 50 | mingw-w64-ucrt-x86_64-boost \ 51 | mingw-w64-ucrt-x86_64-nlohmann-json 52 | ``` 53 | 54 | ### Build 55 | 56 | ```bash 57 | cmake -G Ninja -B build -S . 58 | ninja -C build 59 | ``` 60 | 61 | ### Test 62 | 63 | ```bash 64 | ./build/tests/test_libdisplaydevice 65 | ``` 66 | 67 | ## Support 68 | 69 | Our support methods are listed in our [LizardByte Docs](https://lizardbyte.readthedocs.io/latest/about/support.html). 70 | 71 |
72 | 73 | [TOC] 74 |
75 | -------------------------------------------------------------------------------- /tests/fixtures/test_utils.cpp: -------------------------------------------------------------------------------- 1 | // header include 2 | #include "fixtures/test_utils.h" 3 | 4 | // system includes 5 | #include 6 | 7 | // system includes 8 | #include 9 | #include 10 | #include 11 | 12 | namespace ut_consts { 13 | namespace { 14 | template 15 | std::vector makeBytes(Ts &&...args) { 16 | return {std::byte {static_cast(args)}...}; 17 | } 18 | } // namespace 19 | 20 | const std::vector DEFAULT_EDID {makeBytes( 21 | // clang-format off 22 | 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x04, 0x69, 23 | 0xEC, 0x27, 0xAA, 0x55, 0x00, 0x00, 0x13, 0x1D, 0x01, 0x04, 24 | 0xA5, 0x3C, 0x22, 0x78, 0x06, 0xEE, 0x91, 0xA3, 0x54, 0x4C, 25 | 0x99, 0x26, 0x0F, 0x50, 0x54, 0x21, 0x08, 0x00, 0x01, 0x01, 26 | 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 27 | 0x01, 0x01, 0x01, 0x01, 0x56, 0x5E, 0x00, 0xA0, 0xA0, 0xA0, 28 | 0x29, 0x50, 0x30, 0x20, 0x35, 0x00, 0x56, 0x50, 0x21, 0x00, 29 | 0x00, 0x1A, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x23, 0x41, 0x53, 30 | 0x4E, 0x39, 0x4A, 0x36, 0x6E, 0x4E, 0x49, 0x54, 0x62, 0x64, 31 | 0x00, 0x00, 0x00, 0xFD, 0x00, 0x1E, 0x90, 0x22, 0xDE, 0x3B, 32 | 0x01, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, 0x00, 33 | 0x00, 0xFC, 0x00, 0x52, 0x4F, 0x47, 0x20, 0x50, 0x47, 0x32, 34 | 0x37, 0x39, 0x51, 0x0A, 0x20, 0x20, 0x01, 0x8B 35 | // clang-format on 36 | )}; 37 | const display_device::EdidData DEFAULT_EDID_DATA { 38 | .m_manufacturer_id = "ACI", 39 | .m_product_code = "27EC", 40 | .m_serial_number = 21930 41 | }; 42 | } // namespace ut_consts 43 | 44 | bool testRegex(const std::string &input, const std::string &pattern) { 45 | std::regex regex(pattern); 46 | std::smatch match; 47 | if (!std::regex_match(input, match, regex)) { 48 | std::cout << "Regex test failed:\n" 49 | << " Input : " << input << "\n" 50 | << " Pattern: " << pattern << std::endl; 51 | return false; 52 | } 53 | return true; 54 | } 55 | 56 | int setEnv(const std::string &name, const std::string &value) { 57 | #ifdef _WIN32 58 | return _putenv_s(name.c_str(), value.c_str()); 59 | #else 60 | return setenv(name.c_str(), value.c_str(), 1); 61 | #endif 62 | } 63 | 64 | std::optional getEnv(const std::string &name) { 65 | if (const auto value {std::getenv(name.c_str())}; value) { 66 | return std::string {value}; 67 | } 68 | return std::nullopt; 69 | } 70 | -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # 2 | # Setup google test 3 | # 4 | set(INSTALL_GTEST OFF) 5 | set(INSTALL_GMOCK OFF) 6 | include(GoogleTest) 7 | add_subdirectory("${PROJECT_SOURCE_DIR}/third-party/googletest" "third-party/googletest") 8 | 9 | if (WIN32) 10 | # For Windows: Prevent overriding the parent project's compiler/linker settings 11 | set(gtest_force_shared_crt ON CACHE BOOL "Always use msvcrt.dll" FORCE) # cmake-lint: disable=C0103 12 | endif () 13 | 14 | # A helper function to setup the dependencies for the test executable 15 | function(add_dd_test_dir) 16 | set(options "") 17 | set(oneValueArgs "") 18 | set(multiValueArgs ADDITIONAL_LIBRARIES ADDITIONAL_SOURCES) 19 | cmake_parse_arguments(FN_VARS "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) 20 | 21 | # Get the current sources and libraries 22 | get_property(sources GLOBAL PROPERTY DD_TEST_SOURCES) 23 | get_property(libraries GLOBAL PROPERTY DD_TEST_LIBRARIES) 24 | 25 | # Gather new data 26 | file(GLOB test_files CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/test_*.cpp") 27 | 28 | list(APPEND sources ${test_files}) 29 | list(APPEND libraries ${FN_VARS_ADDITIONAL_LIBRARIES}) 30 | 31 | foreach (source_pattern ${FN_VARS_ADDITIONAL_SOURCES}) 32 | file(GLOB source_files CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/${source_pattern}") 33 | foreach (source_file ${source_files}) 34 | list(APPEND sources ${source_file}) 35 | endforeach () 36 | endforeach () 37 | 38 | # Update the global variables 39 | set_property(GLOBAL PROPERTY DD_TEST_SOURCES "${sources}") 40 | set_property(GLOBAL PROPERTY DD_TEST_LIBRARIES "${libraries}") 41 | endfunction() 42 | 43 | # 44 | # Add subdirectories 45 | # 46 | add_subdirectory(fixtures) 47 | add_subdirectory(unit) 48 | 49 | # 50 | # Setup the final test binary 51 | # 52 | set(TEST_BINARY test_libdisplaydevice) 53 | get_property(sources GLOBAL PROPERTY DD_TEST_SOURCES) 54 | get_property(libraries GLOBAL PROPERTY DD_TEST_LIBRARIES) 55 | 56 | add_executable(${TEST_BINARY} ${sources}) 57 | target_link_libraries(${TEST_BINARY} 58 | PUBLIC 59 | gmock_main # if we use this we don't need our own main function 60 | libdisplaydevice::display_device # this target includes common + platform specific targets 61 | libfixtures # these are our fixtures/helpers for the tests 62 | ${libraries} # additional libraries if needed 63 | ) 64 | 65 | # Add the test to CTest 66 | gtest_discover_tests(${TEST_BINARY}) 67 | -------------------------------------------------------------------------------- /src/windows/include/display_device/windows/win_api_layer.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file src/windows/include/display_device/windows/win_api_layer.h 3 | * @brief Declarations for the WinApiLayer. 4 | */ 5 | #pragma once 6 | 7 | // local includes 8 | #include "win_api_layer_interface.h" 9 | 10 | namespace display_device { 11 | /** 12 | * @brief Default implementation for the WinApiLayerInterface. 13 | */ 14 | class WinApiLayer: public WinApiLayerInterface { 15 | public: 16 | /** For details @see WinApiLayerInterface::getErrorString */ 17 | [[nodiscard]] std::string getErrorString(LONG error_code) const override; 18 | 19 | /** For details @see WinApiLayerInterface::queryDisplayConfig */ 20 | [[nodiscard]] std::optional queryDisplayConfig(QueryType type) const override; 21 | 22 | /** For details @see WinApiLayerInterface::getDeviceId */ 23 | [[nodiscard]] std::string getDeviceId(const DISPLAYCONFIG_PATH_INFO &path) const override; 24 | 25 | /** For details @see WinApiLayerInterface::getEdid */ 26 | [[nodiscard]] std::vector getEdid(const DISPLAYCONFIG_PATH_INFO &path) const override; 27 | 28 | /** For details @see WinApiLayerInterface::getMonitorDevicePath */ 29 | [[nodiscard]] std::string getMonitorDevicePath(const DISPLAYCONFIG_PATH_INFO &path) const override; 30 | 31 | /** For details @see WinApiLayerInterface::getFriendlyName */ 32 | [[nodiscard]] std::string getFriendlyName(const DISPLAYCONFIG_PATH_INFO &path) const override; 33 | 34 | /** For details @see WinApiLayerInterface::getDisplayName */ 35 | [[nodiscard]] std::string getDisplayName(const DISPLAYCONFIG_PATH_INFO &path) const override; 36 | 37 | /** For details @see WinApiLayerInterface::setDisplayConfig */ 38 | [[nodiscard]] LONG setDisplayConfig(std::vector paths, std::vector modes, UINT32 flags) override; 39 | 40 | /** For details @see WinApiLayerInterface::getHdrState */ 41 | [[nodiscard]] std::optional getHdrState(const DISPLAYCONFIG_PATH_INFO &path) const override; 42 | 43 | /** For details @see WinApiLayerInterface::setHdrState */ 44 | [[nodiscard]] bool setHdrState(const DISPLAYCONFIG_PATH_INFO &path, HdrState state) override; 45 | 46 | /** For details @see WinApiLayerInterface::getDisplayScale */ 47 | [[nodiscard]] std::optional getDisplayScale(const std::string &display_name, const DISPLAYCONFIG_SOURCE_MODE &source_mode) const override; 48 | }; 49 | } // namespace display_device 50 | -------------------------------------------------------------------------------- /src/windows/persistent_state.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file src/windows/persistent_state.cpp 3 | * @brief Definitions for the PersistentState. 4 | */ 5 | // class header include 6 | #include "display_device/windows/persistent_state.h" 7 | 8 | // local includes 9 | #include "display_device/logging.h" 10 | #include "display_device/noop_settings_persistence.h" 11 | #include "display_device/windows/json.h" 12 | 13 | namespace display_device { 14 | PersistentState::PersistentState(std::shared_ptr settings_persistence_api, const bool throw_on_load_error): 15 | m_settings_persistence_api {std::move(settings_persistence_api)} { 16 | if (!m_settings_persistence_api) { 17 | m_settings_persistence_api = std::make_shared(); 18 | } 19 | 20 | std::string error_message; 21 | if (const auto persistent_settings {m_settings_persistence_api->load()}) { 22 | if (!persistent_settings->empty()) { 23 | m_cached_state = SingleDisplayConfigState {}; 24 | if (!fromJson({std::begin(*persistent_settings), std::end(*persistent_settings)}, *m_cached_state, &error_message)) { 25 | error_message = "Failed to parse persistent settings! Error:\n" + error_message; 26 | } 27 | } 28 | } else { 29 | error_message = "Failed to load persistent settings!"; 30 | } 31 | 32 | if (!error_message.empty()) { 33 | if (throw_on_load_error) { 34 | throw std::runtime_error {error_message}; 35 | } 36 | 37 | DD_LOG(error) << error_message; 38 | m_cached_state = std::nullopt; 39 | } 40 | } 41 | 42 | bool PersistentState::persistState(const std::optional &state) { 43 | if (m_cached_state == state) { 44 | return true; 45 | } 46 | 47 | if (!state) { 48 | if (!m_settings_persistence_api->clear()) { 49 | return false; 50 | } 51 | 52 | m_cached_state = std::nullopt; 53 | return true; 54 | } 55 | 56 | bool success {false}; 57 | const auto json_string {toJson(*state, 2, &success)}; 58 | if (!success) { 59 | DD_LOG(error) << "Failed to serialize new persistent state! Error:\n" 60 | << json_string; 61 | return false; 62 | } 63 | 64 | if (!m_settings_persistence_api->store({std::begin(json_string), std::end(json_string)})) { 65 | return false; 66 | } 67 | 68 | m_cached_state = *state; 69 | return true; 70 | } 71 | 72 | const std::optional &PersistentState::getState() const { 73 | return m_cached_state; 74 | } 75 | } // namespace display_device 76 | -------------------------------------------------------------------------------- /tests/unit/windows/test_json_converter.cpp: -------------------------------------------------------------------------------- 1 | // local includes 2 | #include "display_device/windows/json.h" 3 | #include "fixtures/json_converter_test.h" 4 | #include "utils/comparison.h" 5 | 6 | namespace { 7 | // Specialized TEST macro(s) for this test file 8 | #define TEST_F_S(...) DD_MAKE_TEST(TEST_F, JsonConverterTest, __VA_ARGS__) 9 | } // namespace 10 | 11 | TEST_F_S(ActiveTopology) { 12 | executeTestCase(display_device::ActiveTopology {}, R"([])"); 13 | executeTestCase(display_device::ActiveTopology {{"DeviceId1"}, {"DeviceId2", "DeviceId3"}, {"DeviceId4"}}, R"([["DeviceId1"],["DeviceId2","DeviceId3"],["DeviceId4"]])"); 14 | } 15 | 16 | TEST_F_S(DeviceDisplayModeMap) { 17 | executeTestCase(display_device::DeviceDisplayModeMap {}, R"({})"); 18 | executeTestCase(display_device::DeviceDisplayModeMap {{"DeviceId1", {}}, {"DeviceId2", {{1920, 1080}, {120, 1}}}}, R"({"DeviceId1":{"refresh_rate":{"denominator":0,"numerator":0},"resolution":{"height":0,"width":0}},"DeviceId2":{"refresh_rate":{"denominator":1,"numerator":120},"resolution":{"height":1080,"width":1920}}})"); 19 | } 20 | 21 | TEST_F_S(HdrStateMap) { 22 | executeTestCase(display_device::HdrStateMap {}, R"({})"); 23 | executeTestCase(display_device::HdrStateMap {{"DeviceId1", std::nullopt}, {"DeviceId2", display_device::HdrState::Enabled}}, R"({"DeviceId1":null,"DeviceId2":"Enabled"})"); 24 | } 25 | 26 | TEST_F_S(SingleDisplayConfigState) { 27 | const display_device::SingleDisplayConfigState valid_input { 28 | {{{"DeviceId1"}}, 29 | {"DeviceId1"}}, 30 | {display_device::SingleDisplayConfigState::Modified { 31 | {{"DeviceId2"}}, 32 | {{"DeviceId2", {{1920, 1080}, {120, 1}}}}, 33 | {{"DeviceId2", {display_device::HdrState::Disabled}}}, 34 | {"DeviceId2"}, 35 | }} 36 | }; 37 | 38 | executeTestCase(display_device::SingleDisplayConfigState {}, R"({"initial":{"primary_devices":[],"topology":[]},"modified":{"original_hdr_states":{},"original_modes":{},"original_primary_device":"","topology":[]}})"); 39 | executeTestCase(valid_input, R"({"initial":{"primary_devices":["DeviceId1"],"topology":[["DeviceId1"]]},"modified":{"original_hdr_states":{"DeviceId2":"Disabled"},"original_modes":{"DeviceId2":{"refresh_rate":{"denominator":1,"numerator":120},"resolution":{"height":1080,"width":1920}}},"original_primary_device":"DeviceId2","topology":[["DeviceId2"]]}})"); 40 | } 41 | 42 | TEST_F_S(WinWorkarounds) { 43 | display_device::WinWorkarounds input { 44 | std::chrono::milliseconds {500} 45 | }; 46 | 47 | executeTestCase(display_device::WinWorkarounds {}, R"({"hdr_blank_delay":null})"); 48 | executeTestCase(input, R"({"hdr_blank_delay":500})"); 49 | } 50 | -------------------------------------------------------------------------------- /src/common/file_settings_persistence.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file src/common/file_settings_persistence.cpp 3 | * @brief Definitions for persistent file settings. 4 | */ 5 | // class header include 6 | #include "display_device/file_settings_persistence.h" 7 | 8 | // system includes 9 | #include 10 | #include 11 | #include 12 | 13 | // local includes 14 | #include "display_device/logging.h" 15 | 16 | namespace display_device { 17 | FileSettingsPersistence::FileSettingsPersistence(std::filesystem::path filepath): 18 | m_filepath {std::move(filepath)} { 19 | if (m_filepath.empty()) { 20 | throw std::runtime_error {"Empty filename provided for FileSettingsPersistence!"}; 21 | } 22 | } 23 | 24 | bool FileSettingsPersistence::store(const std::vector &data) { 25 | try { 26 | std::ofstream stream {m_filepath, std::ios::binary | std::ios::trunc}; 27 | if (!stream) { 28 | DD_LOG(error) << "Failed to open " << m_filepath << " for writing!"; 29 | return false; 30 | } 31 | 32 | std::copy(std::begin(data), std::end(data), std::ostreambuf_iterator {stream}); 33 | return true; 34 | } catch (const std::exception &error) { 35 | DD_LOG(error) << "Failed to write to " << m_filepath << "! Error:\n" 36 | << error.what(); 37 | return false; 38 | } 39 | } 40 | 41 | std::optional> FileSettingsPersistence::load() const { 42 | if (std::error_code error_code; !std::filesystem::exists(m_filepath, error_code)) { 43 | if (error_code) { 44 | DD_LOG(error) << "Failed to load " << m_filepath << "! Error:\n" 45 | << "[" << error_code.value() << "] " << error_code.message(); 46 | return std::nullopt; 47 | } 48 | 49 | return std::vector {}; 50 | } 51 | 52 | try { 53 | std::ifstream stream {m_filepath, std::ios::binary}; 54 | if (!stream) { 55 | DD_LOG(error) << "Failed to open " << m_filepath << " for reading!"; 56 | return std::nullopt; 57 | } 58 | 59 | return std::vector {std::istreambuf_iterator {stream}, std::istreambuf_iterator {}}; 60 | } catch (const std::exception &error) { 61 | DD_LOG(error) << "Failed to read " << m_filepath << "! Error:\n" 62 | << error.what(); 63 | return std::nullopt; 64 | } 65 | } 66 | 67 | bool FileSettingsPersistence::clear() { 68 | // Return valud does not matter since we check the error code in case the file could NOT be removed. 69 | std::error_code error_code; 70 | std::filesystem::remove(m_filepath, error_code); 71 | 72 | if (error_code) { 73 | DD_LOG(error) << "Failed to remove " << m_filepath << "! Error:\n" 74 | << "[" << error_code.value() << "] " << error_code.message(); 75 | return false; 76 | } 77 | 78 | return true; 79 | } 80 | } // namespace display_device 81 | -------------------------------------------------------------------------------- /src/windows/include/display_device/windows/win_display_device.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file src/windows/include/display_device/windows/win_display_device.h 3 | * @brief Declarations for the WinDisplayDevice. 4 | */ 5 | #pragma once 6 | 7 | // system includes 8 | #include 9 | 10 | // local includes 11 | #include "win_api_layer_interface.h" 12 | #include "win_display_device_interface.h" 13 | 14 | namespace display_device { 15 | /** 16 | * @brief Default implementation for the WinDisplayDeviceInterface. 17 | */ 18 | class WinDisplayDevice: public WinDisplayDeviceInterface { 19 | public: 20 | /** 21 | * Default constructor for the class. 22 | * @param w_api A pointer to the Windows API layer. Will throw on nullptr! 23 | */ 24 | explicit WinDisplayDevice(std::shared_ptr w_api); 25 | 26 | /** For details @see WinDisplayDeviceInterface::isApiAccessAvailable */ 27 | [[nodiscard]] bool isApiAccessAvailable() const override; 28 | 29 | /** For details @see WinDisplayDeviceInterface::enumAvailableDevices */ 30 | [[nodiscard]] EnumeratedDeviceList enumAvailableDevices() const override; 31 | 32 | /** For details @see WinDisplayDeviceInterface::getDisplayName */ 33 | [[nodiscard]] std::string getDisplayName(const std::string &device_id) const override; 34 | 35 | /** For details @see WinDisplayDeviceInterface::getCurrentTopology */ 36 | [[nodiscard]] ActiveTopology getCurrentTopology() const override; 37 | 38 | /** For details @see WinDisplayDeviceInterface::isTopologyValid */ 39 | [[nodiscard]] bool isTopologyValid(const ActiveTopology &topology) const override; 40 | 41 | /** For details @see WinDisplayDeviceInterface::getCurrentTopology */ 42 | [[nodiscard]] bool isTopologyTheSame(const ActiveTopology &lhs, const ActiveTopology &rhs) const override; 43 | 44 | /** For details @see WinDisplayDeviceInterface::setTopology */ 45 | [[nodiscard]] bool setTopology(const ActiveTopology &new_topology) override; 46 | 47 | /** For details @see WinDisplayDeviceInterface::getCurrentDisplayModes */ 48 | [[nodiscard]] DeviceDisplayModeMap getCurrentDisplayModes(const std::set &device_ids) const override; 49 | 50 | /** For details @see WinDisplayDeviceInterface::setDisplayModes */ 51 | [[nodiscard]] bool setDisplayModes(const DeviceDisplayModeMap &modes) override; 52 | 53 | /** For details @see WinDisplayDeviceInterface::isPrimary */ 54 | [[nodiscard]] bool isPrimary(const std::string &device_id) const override; 55 | 56 | /** For details @see WinDisplayDeviceInterface::setAsPrimary */ 57 | [[nodiscard]] bool setAsPrimary(const std::string &device_id) override; 58 | 59 | /** For details @see WinDisplayDeviceInterface::getCurrentHdrStates */ 60 | [[nodiscard]] HdrStateMap getCurrentHdrStates(const std::set &device_ids) const override; 61 | 62 | /** For details @see WinDisplayDeviceInterface::setHdrStates */ 63 | [[nodiscard]] bool setHdrStates(const HdrStateMap &states) override; 64 | 65 | private: 66 | std::shared_ptr m_w_api; 67 | }; 68 | } // namespace display_device 69 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # This file is centrally managed in https://github.com//.github/ 3 | # Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in 4 | # the above-mentioned repo. 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "cargo" 9 | directory: "/" 10 | rebase-strategy: disabled 11 | schedule: 12 | interval: "cron" 13 | cronjob: "0 1 * * *" 14 | timezone: "America/New_York" 15 | open-pull-requests-limit: 10 16 | 17 | - package-ecosystem: "docker" 18 | directory: "/" 19 | rebase-strategy: disabled 20 | schedule: 21 | interval: "cron" 22 | cronjob: "30 1 * * *" 23 | timezone: "America/New_York" 24 | open-pull-requests-limit: 10 25 | 26 | - package-ecosystem: "github-actions" 27 | directories: 28 | - "/" 29 | - "/.github/actions/*" 30 | - "/actions/*" 31 | rebase-strategy: disabled 32 | schedule: 33 | interval: "cron" 34 | cronjob: "0 2 * * *" 35 | timezone: "America/New_York" 36 | open-pull-requests-limit: 10 37 | groups: 38 | docker-actions: 39 | applies-to: version-updates 40 | patterns: 41 | - "docker/*" 42 | github-actions: 43 | applies-to: version-updates 44 | patterns: 45 | - "actions/*" 46 | - "github/*" 47 | lizardbyte-actions: 48 | applies-to: version-updates 49 | patterns: 50 | - "LizardByte/*" 51 | 52 | - package-ecosystem: "gitsubmodule" 53 | directory: "/" 54 | rebase-strategy: disabled 55 | schedule: 56 | interval: "cron" 57 | cronjob: "30 2 * * *" 58 | timezone: "America/New_York" 59 | open-pull-requests-limit: 10 60 | 61 | - package-ecosystem: "npm" 62 | directory: "/" 63 | rebase-strategy: disabled 64 | schedule: 65 | interval: "cron" 66 | cronjob: "0 3 * * *" 67 | timezone: "America/New_York" 68 | open-pull-requests-limit: 10 69 | groups: 70 | dev-dependencies: 71 | applies-to: version-updates 72 | dependency-type: "development" 73 | 74 | - package-ecosystem: "nuget" 75 | directory: "/" 76 | rebase-strategy: disabled 77 | schedule: 78 | interval: "cron" 79 | cronjob: "30 3 * * *" 80 | timezone: "America/New_York" 81 | open-pull-requests-limit: 10 82 | 83 | - package-ecosystem: "pip" 84 | directory: "/" 85 | rebase-strategy: disabled 86 | schedule: 87 | interval: "cron" 88 | cronjob: "0 4 * * *" 89 | timezone: "America/New_York" 90 | open-pull-requests-limit: 10 91 | groups: 92 | pytest-dependencies: 93 | applies-to: version-updates 94 | patterns: 95 | - "pytest*" 96 | 97 | - package-ecosystem: "rust-toolchain" 98 | directory: "/" 99 | rebase-strategy: disabled 100 | schedule: 101 | interval: "cron" 102 | cronjob: "30 4 * * *" 103 | timezone: "America/New_York" 104 | open-pull-requests-limit: 1 105 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | # This file is centrally managed in https://github.com//.github/ 3 | # Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in 4 | # the above-mentioned repo. 5 | 6 | # Generated from CLion C/C++ Code Style settings 7 | BasedOnStyle: LLVM 8 | AccessModifierOffset: -2 9 | AlignAfterOpenBracket: BlockIndent 10 | AlignConsecutiveAssignments: None 11 | AlignEscapedNewlines: DontAlign 12 | AlignOperands: Align 13 | AllowAllArgumentsOnNextLine: false 14 | AllowAllConstructorInitializersOnNextLine: false 15 | AllowAllParametersOfDeclarationOnNextLine: false 16 | AllowShortBlocksOnASingleLine: Empty 17 | AllowShortCaseLabelsOnASingleLine: false 18 | AllowShortEnumsOnASingleLine: false 19 | AllowShortFunctionsOnASingleLine: Empty 20 | AllowShortIfStatementsOnASingleLine: Never 21 | AllowShortLambdasOnASingleLine: None 22 | AllowShortLoopsOnASingleLine: true 23 | AlignTrailingComments: false 24 | AlwaysBreakAfterDefinitionReturnType: None 25 | AlwaysBreakAfterReturnType: None 26 | AlwaysBreakBeforeMultilineStrings: true 27 | AlwaysBreakTemplateDeclarations: MultiLine 28 | BinPackArguments: false 29 | BinPackParameters: false 30 | BracedInitializerIndentWidth: 2 31 | BraceWrapping: 32 | AfterCaseLabel: false 33 | AfterClass: false 34 | AfterControlStatement: Never 35 | AfterEnum: false 36 | AfterExternBlock: true 37 | AfterFunction: false 38 | AfterNamespace: false 39 | AfterObjCDeclaration: false 40 | AfterUnion: false 41 | BeforeCatch: true 42 | BeforeElse: true 43 | IndentBraces: false 44 | SplitEmptyFunction: false 45 | SplitEmptyRecord: true 46 | BreakArrays: true 47 | BreakBeforeBinaryOperators: None 48 | BreakBeforeBraces: Attach 49 | BreakBeforeTernaryOperators: false 50 | BreakConstructorInitializers: AfterColon 51 | BreakInheritanceList: AfterColon 52 | ColumnLimit: 0 53 | CompactNamespaces: false 54 | ContinuationIndentWidth: 2 55 | Cpp11BracedListStyle: true 56 | EmptyLineAfterAccessModifier: Never 57 | EmptyLineBeforeAccessModifier: Always 58 | ExperimentalAutoDetectBinPacking: true 59 | FixNamespaceComments: true 60 | IncludeBlocks: Regroup 61 | IndentAccessModifiers: false 62 | IndentCaseBlocks: true 63 | IndentCaseLabels: true 64 | IndentExternBlock: Indent 65 | IndentGotoLabels: true 66 | IndentPPDirectives: BeforeHash 67 | IndentWidth: 2 68 | IndentWrappedFunctionNames: true 69 | InsertBraces: true 70 | InsertNewlineAtEOF: true 71 | KeepEmptyLinesAtTheStartOfBlocks: false 72 | MaxEmptyLinesToKeep: 1 73 | NamespaceIndentation: All 74 | ObjCBinPackProtocolList: Never 75 | ObjCSpaceAfterProperty: true 76 | ObjCSpaceBeforeProtocolList: true 77 | PackConstructorInitializers: Never 78 | PenaltyBreakBeforeFirstCallParameter: 1 79 | PenaltyBreakComment: 1 80 | PenaltyBreakString: 1 81 | PenaltyBreakFirstLessLess: 0 82 | PenaltyExcessCharacter: 1000000 83 | PenaltyReturnTypeOnItsOwnLine: 100000000 84 | PointerAlignment: Right 85 | ReferenceAlignment: Pointer 86 | ReflowComments: true 87 | RemoveBracesLLVM: false 88 | RemoveSemicolon: false 89 | SeparateDefinitionBlocks: Always 90 | SortIncludes: CaseInsensitive 91 | SortUsingDeclarations: Lexicographic 92 | SpaceAfterCStyleCast: true 93 | SpaceAfterLogicalNot: false 94 | SpaceAfterTemplateKeyword: false 95 | SpaceBeforeAssignmentOperators: true 96 | SpaceBeforeCaseColon: false 97 | SpaceBeforeCpp11BracedList: true 98 | SpaceBeforeCtorInitializerColon: false 99 | SpaceBeforeInheritanceColon: false 100 | SpaceBeforeJsonColon: false 101 | SpaceBeforeParens: ControlStatements 102 | SpaceBeforeRangeBasedForLoopColon: true 103 | SpaceBeforeSquareBrackets: false 104 | SpaceInEmptyBlock: false 105 | SpaceInEmptyParentheses: false 106 | SpacesBeforeTrailingComments: 2 107 | SpacesInAngles: Never 108 | SpacesInCStyleCastParentheses: false 109 | SpacesInContainerLiterals: false 110 | SpacesInLineCommentPrefix: 111 | Maximum: 3 112 | Minimum: 1 113 | SpacesInParentheses: false 114 | SpacesInSquareBrackets: false 115 | TabWidth: 2 116 | UseTab: Never 117 | -------------------------------------------------------------------------------- /tests/unit/windows/test_win_playground.cpp: -------------------------------------------------------------------------------- 1 | // local includes 2 | #include "display_device/windows/json.h" 3 | #include "display_device/windows/settings_manager.h" 4 | #include "display_device/windows/win_api_layer.h" 5 | #include "display_device/windows/win_display_device.h" 6 | #include "fixtures/fixtures.h" 7 | #include "utils/guards.h" 8 | 9 | namespace { 10 | // Convenience stuff for GTest 11 | #define GTEST_DISABLED_CLASS_NAME(x) DISABLED_##x 12 | 13 | // Test fixture(s) for this file 14 | class GTEST_DISABLED_CLASS_NAME(WinPlayground): 15 | public BaseTest { 16 | public: 17 | bool isOutputSuppressed() const override { 18 | return false; 19 | } 20 | 21 | std::optional getDefaultLogLevel() const override { 22 | // Unless user explicitly has overriden the level via ENV, we don't want all 23 | // that noise from verbose logs... 24 | return BaseTest::getDefaultLogLevel().value_or(display_device::Logger::LogLevel::info); 25 | } 26 | 27 | display_device::SettingsManager &getImpl(const display_device::WinWorkarounds &workarounds = {}) { 28 | if (!m_impl) { 29 | m_impl = std::make_unique( 30 | std::make_shared(std::make_shared()), 31 | nullptr, 32 | std::make_unique(nullptr), 33 | workarounds 34 | ); 35 | } 36 | 37 | return *m_impl; 38 | } 39 | 40 | private: 41 | std::unique_ptr m_impl; 42 | }; 43 | 44 | // Specialized TEST macro(s) for this test file 45 | #define TEST_F_S(...) DD_MAKE_TEST(TEST_F, GTEST_DISABLED_CLASS_NAME(WinPlayground), __VA_ARGS__) 46 | } // namespace 47 | 48 | TEST_F_S(EnumAvailableDevices) { 49 | // Usage example: 50 | // test_libdisplaydevice.exe --gtest_color=yes --gtest_also_run_disabled_tests --gtest_filter=*WinPlayground.EnumAvailableDevices 51 | 52 | DD_LOG(info) << "enumerated devices:\n" 53 | << toJson(getImpl().enumAvailableDevices()); 54 | } 55 | 56 | TEST_F_S(ApplySettings) { 57 | // Usage example: 58 | // test_libdisplaydevice.exe --gtest_color=yes --gtest_also_run_disabled_tests --gtest_filter=*WinPlayground.ApplySettings config='{\"device_id\":\"{77f67f3e-754f-5d31-af64-ee037e18100a}\",\"device_prep\":\"EnsureActive\",\"hdr_state\":null,\"refresh_rate\":null,\"resolution\":null}' 59 | // 60 | // With workarounds (optional): 61 | // test_libdisplaydevice.exe --gtest_color=yes --gtest_also_run_disabled_tests --gtest_filter=*WinPlayground.ApplySettings config='...' workarounds='{\"hdr_blank_delay\":500}' 62 | 63 | const auto config_arg {getArgWithMatchingPattern(R"(^config=)", true)}; 64 | if (!config_arg) { 65 | GTEST_FAIL() << "\"config=\" argument not found!"; 66 | } 67 | 68 | std::string parse_error {}; 69 | display_device::SingleDisplayConfiguration config; 70 | if (!fromJson(*config_arg, config, &parse_error)) { 71 | GTEST_FAIL() << "Config argument could not be parsed!\nArgument:\n " << *config_arg << "\nError:\n " << parse_error; 72 | } 73 | 74 | if (const auto workarounds_arg {getArgWithMatchingPattern(R"(^workarounds=)", true)}) { 75 | display_device::WinWorkarounds workarounds; 76 | if (!fromJson(*workarounds_arg, workarounds, &parse_error)) { 77 | GTEST_FAIL() << "Workarounds argument could not be parsed!\nArgument:\n " << *workarounds_arg << "\nError:\n " << parse_error; 78 | } 79 | 80 | // Initialize the implementation 81 | getImpl(workarounds); 82 | } 83 | 84 | const boost::scope::scope_exit cleanup {[this]() { 85 | static_cast(getImpl().revertSettings()); 86 | }}; 87 | 88 | std::cout << "Applying settings. Press enter to continue..." << std::endl; 89 | std::cin.get(); 90 | if (getImpl().applySettings(config) != display_device::SettingsManagerInterface::ApplyResult::Ok) { 91 | GTEST_FAIL() << "Failed to apply configuration!"; 92 | } 93 | 94 | std::cout << "Reverting settings. Press enter to continue..." << std::endl; 95 | std::cin.get(); 96 | } 97 | -------------------------------------------------------------------------------- /src/common/logging.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file src/common/logging.cpp 3 | * @brief Definitions for the logging utility. 4 | */ 5 | #if !defined(_MSC_VER) && !defined(_POSIX_THREAD_SAFE_FUNCTIONS) 6 | #define _POSIX_THREAD_SAFE_FUNCTIONS // For localtime_r 7 | #endif 8 | 9 | // class header include 10 | #include "display_device/logging.h" 11 | 12 | // system includes 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | namespace display_device { 19 | namespace { 20 | std::tm threadSafeLocaltime(const std::time_t &time) { 21 | #if defined(_MSC_VER) // MSVCRT (2005+): std::localtime is threadsafe 22 | const auto tm_ptr {std::localtime(&time)}; 23 | #else // POSIX 24 | std::tm buffer; 25 | const auto tm_ptr {localtime_r(&time, &buffer)}; 26 | #endif // _MSC_VER 27 | if (tm_ptr) { 28 | return *tm_ptr; 29 | } 30 | return {}; 31 | } 32 | } // namespace 33 | 34 | Logger &Logger::get() { 35 | static Logger instance; // GCOVR_EXCL_BR_LINE for some reason... 36 | return instance; 37 | } 38 | 39 | void Logger::setLogLevel(const LogLevel log_level) { 40 | m_enabled_log_level = log_level; 41 | } 42 | 43 | bool Logger::isLogLevelEnabled(LogLevel log_level) const { 44 | const auto log_level_v {static_cast>(log_level)}; 45 | const auto enabled_log_level_v {static_cast>(m_enabled_log_level)}; 46 | return log_level_v >= enabled_log_level_v; 47 | } 48 | 49 | void Logger::setCustomCallback(Callback callback) { 50 | m_custom_callback = std::move(callback); 51 | } 52 | 53 | void Logger::write(const LogLevel log_level, std::string value) { 54 | if (!isLogLevelEnabled(log_level)) { 55 | return; 56 | } 57 | 58 | if (m_custom_callback) { 59 | m_custom_callback(log_level, std::move(value)); 60 | return; 61 | } 62 | 63 | std::stringstream stream; 64 | { 65 | // Time (limited by GCC 10, so it's not pretty...) 66 | { 67 | const auto now {std::chrono::system_clock::now()}; 68 | const auto now_ms {std::chrono::duration_cast(now.time_since_epoch())}; 69 | const auto now_s {std::chrono::duration_cast(now_ms)}; 70 | 71 | const std::time_t time {std::chrono::system_clock::to_time_t(now)}; 72 | const auto localtime {threadSafeLocaltime(time)}; 73 | const auto now_decimal_part {now_ms - now_s}; 74 | 75 | const auto old_flags {stream.flags()}; // Save formatting flags so that they can be restored... 76 | stream << std::put_time(&localtime, "[%Y-%m-%d %H:%M:%S.") << std::setfill('0') << std::setw(3) << now_decimal_part.count() << "] "; 77 | stream.flags(old_flags); 78 | } 79 | 80 | // Log level 81 | switch (log_level) { // GCOVR_EXCL_BR_LINE for when there is no case match... 82 | case LogLevel::verbose: 83 | stream << "VERBOSE: "; 84 | break; 85 | case LogLevel::debug: 86 | stream << "DEBUG: "; 87 | break; 88 | case LogLevel::info: 89 | stream << "INFO: "; 90 | break; 91 | case LogLevel::warning: 92 | stream << "WARNING: "; 93 | break; 94 | case LogLevel::error: 95 | stream << "ERROR: "; 96 | break; 97 | case LogLevel::fatal: 98 | stream << "FATAL: "; 99 | break; 100 | } 101 | 102 | // Value 103 | stream << value; 104 | } 105 | 106 | static std::mutex log_mutex; 107 | std::lock_guard lock {log_mutex}; 108 | std::cout << stream.rdbuf() << std::endl; 109 | } 110 | 111 | Logger::Logger(): 112 | m_enabled_log_level {LogLevel::info} { 113 | } 114 | 115 | LogWriter::LogWriter(const Logger::LogLevel log_level): 116 | m_log_level {log_level} {} 117 | 118 | LogWriter::~LogWriter() { 119 | Logger::get().write(m_log_level, m_buffer.str()); 120 | } 121 | } // namespace display_device 122 | -------------------------------------------------------------------------------- /tests/fixtures/fixtures.cpp: -------------------------------------------------------------------------------- 1 | // header include 2 | #include "fixtures/fixtures.h" 3 | 4 | // system includes 5 | #include 6 | 7 | void BaseTest::SetUp() { 8 | if (const auto skip_reason {skipTest()}; !skip_reason.empty()) { 9 | m_test_skipped_at_setup = true; 10 | GTEST_SKIP() << skip_reason; 11 | } 12 | 13 | if (isOutputSuppressed()) { 14 | // See https://stackoverflow.com/a/58369622/11214013 15 | m_sbuf = std::cout.rdbuf(); // save cout buffer (std::cout) 16 | std::cout.rdbuf(m_cout_buffer.rdbuf()); // redirect cout to buffer (std::cout) 17 | } 18 | 19 | // Set the default log level, before the test starts. Will default to verbose in case nothing was specified. 20 | display_device::Logger::get().setLogLevel(getDefaultLogLevel().value_or(display_device::Logger::LogLevel::verbose)); 21 | } 22 | 23 | void BaseTest::TearDown() { 24 | if (m_test_skipped_at_setup) { 25 | // We are not using the IsSkipped() state here. Here we are skipping 26 | // teardown, because we have skipped the setup entirely, but during normal 27 | // skips we still want to do teardown. 28 | return; 29 | } 30 | 31 | // reset the callback to avoid potential leaks 32 | display_device::Logger::get().setCustomCallback(nullptr); 33 | 34 | // Restore cout buffer and print the suppressed output out in case we have failed :/ 35 | if (isOutputSuppressed()) { 36 | std::cout.rdbuf(m_sbuf); 37 | m_sbuf = nullptr; 38 | 39 | const auto test_info = ::testing::UnitTest::GetInstance()->current_test_info(); 40 | if (test_info && test_info->result()->Failed()) { 41 | std::cout << std::endl 42 | << "Test failed: " << test_info->name() << std::endl 43 | << std::endl 44 | << "Captured cout:" << std::endl 45 | << m_cout_buffer.str() << std::endl; 46 | } 47 | } 48 | } 49 | 50 | const std::vector &BaseTest::getArgs() const { 51 | static const auto args {::testing::internal::GetArgvs()}; 52 | return args; 53 | } 54 | 55 | std::optional BaseTest::getArgWithMatchingPattern(const std::string &pattern, bool remove_match) const { 56 | const auto &args {getArgs()}; 57 | if (!args.empty()) { 58 | const std::regex re_pattern {pattern}; 59 | 60 | // We are skipping the first arg which is always binary name/path. 61 | for (auto it {std::next(std::begin(args))}; it != std::end(args); ++it) { 62 | if (std::smatch match; std::regex_search(*it, match, re_pattern)) { 63 | return remove_match ? std::regex_replace(*it, re_pattern, "") : *it; 64 | } 65 | } 66 | } 67 | 68 | return std::nullopt; 69 | } 70 | 71 | bool BaseTest::isOutputSuppressed() const { 72 | return true; 73 | } 74 | 75 | bool BaseTest::isSystemTest() const { 76 | return false; 77 | } 78 | 79 | std::string BaseTest::skipTest() const { 80 | if (isSystemTest()) { 81 | const static bool is_system_test_skippable { 82 | []() { 83 | const auto value {getEnv("SKIP_SYSTEM_TESTS")}; 84 | return value == "1"; 85 | }() 86 | }; 87 | 88 | if (is_system_test_skippable) { 89 | return "Skipping, this system test is disabled via SKIP_SYSTEM_TESTS=1 env."; 90 | } 91 | } 92 | return {}; 93 | } 94 | 95 | std::optional BaseTest::getDefaultLogLevel() const { 96 | const static auto default_log_level { 97 | []() -> std::optional { 98 | const auto value {getEnv("LOG_LEVEL")}; 99 | if (value == "verbose") { 100 | return display_device::Logger::LogLevel::verbose; 101 | } 102 | if (value == "debug") { 103 | return display_device::Logger::LogLevel::debug; 104 | } 105 | if (value == "info") { 106 | return display_device::Logger::LogLevel::info; 107 | } 108 | if (value == "warning") { 109 | return display_device::Logger::LogLevel::warning; 110 | } 111 | if (value == "error") { 112 | return display_device::Logger::LogLevel::error; 113 | } 114 | 115 | return std::nullopt; 116 | }() 117 | }; 118 | 119 | return default_log_level; 120 | } 121 | -------------------------------------------------------------------------------- /tests/fixtures/include/fixtures/fixtures.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // system includes 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | // local includes 12 | #include "display_device/logging.h" 13 | #include "test_utils.h" 14 | 15 | // Undefine the original TEST macro 16 | #undef TEST 17 | 18 | // Redefine TEST to use our BaseTest class, to automatically use our BaseTest fixture 19 | #define TEST(test_case_name, test_name) \ 20 | GTEST_TEST_(test_case_name, test_name, ::BaseTest, ::testing::internal::GetTypeId<::BaseTest>()) 21 | 22 | // Helper macros for concatenating macro variables with an underscore separator (https://stackoverflow.com/questions/74505380/how-to-concatenate-join-va-args-with-delimiters-separators) 23 | #define DD_CAT_OP_(index, state, elem) BOOST_PP_CAT(state, BOOST_PP_CAT(_, elem)) 24 | #define DD_CAT_SEQ_(seq) BOOST_PP_SEQ_FOLD_LEFT(DD_CAT_OP_, BOOST_PP_SEQ_HEAD(seq), BOOST_PP_SEQ_TAIL(seq)) 25 | #define DD_CAT_VA_MULTIPLE_(...) DD_CAT_SEQ_(BOOST_PP_VARIADIC_TO_SEQ(__VA_ARGS__)) 26 | #define DD_CAT_VA_SINGLE_(...) __VA_ARGS__ 27 | #define DD_CAT_VA_ARGS_(...) BOOST_PP_IF(BOOST_PP_EQUAL(BOOST_PP_VARIADIC_SIZE(__VA_ARGS__), 1), DD_CAT_VA_SINGLE_, DD_CAT_VA_MULTIPLE_)(__VA_ARGS__) 28 | 29 | // A macro for making the actual test macro. 30 | // Usage example: 31 | // For normal tests: 32 | // #define TEST_S(...) DD_MAKE_TEST(TEST, SomeTestSuite, __VA_ARGS__) 33 | // For tests with fixtures: 34 | // #define TEST_F_S(...) DD_MAKE_TEST(TEST_F, SomeTestFixture, __VA_ARGS__) 35 | #define DD_MAKE_TEST(test_macro, test_suite_name, ...) test_macro(test_suite_name, DD_CAT_VA_ARGS_(__VA_ARGS__)) 36 | 37 | /** 38 | * @brief Base class for tests. 39 | * 40 | * This class provides a base test fixture for all tests. 41 | */ 42 | class BaseTest: public ::testing::Test { 43 | protected: 44 | ~BaseTest() override = default; 45 | 46 | void SetUp() override; 47 | 48 | void TearDown() override; 49 | 50 | /** 51 | * @brief Get available command line arguments. 52 | * @return Command line args from GTest. 53 | */ 54 | [[nodiscard]] virtual const std::vector &getArgs() const; 55 | 56 | /** 57 | * @brief Get the command line argument that matches the pattern. 58 | * @param pattern Pattern to look for. 59 | * @param remove_match Specify if the matched pattern should be removed before returning argument. 60 | * @return Matching command line argument or null optional if nothing matched. 61 | */ 62 | [[nodiscard]] virtual std::optional getArgWithMatchingPattern(const std::string &pattern, bool remove_match) const; 63 | 64 | /** 65 | * @brief Check if the test output is to be redirected and printed out only if test fails. 66 | * @return True if output is to be suppressed, false otherwise. 67 | * @note It is useful for suppressing noise in automatic tests, but not so much in manual ones. 68 | */ 69 | [[nodiscard]] virtual bool isOutputSuppressed() const; 70 | 71 | /** 72 | * @brief Check if the test interacts/modifies with the system settings. 73 | * @returns True if it does, false otherwise. 74 | * @note By setting SKIP_SYSTEM_TESTS=1 env, these tests will be skipped (useful during development). 75 | */ 76 | [[nodiscard]] virtual bool isSystemTest() const; 77 | 78 | /** 79 | * @brief Skip the test by specifying the reason. 80 | * @returns A non-empty string (reason) if test needs to be skipped, empty string otherwise. 81 | */ 82 | [[nodiscard]] virtual std::string skipTest() const; 83 | 84 | /** 85 | * @brief Get the default log level for the test base. 86 | * @returns A log level set in the env OR null optional if fallback should be used (verbose). 87 | * @note By setting LOG_LEVEL= env you can change the level (e.g. LOG_LEVEL=error). 88 | */ 89 | [[nodiscard]] virtual std::optional getDefaultLogLevel() const; 90 | 91 | std::stringstream m_cout_buffer; /**< Stores the cout in case the output is suppressed. */ 92 | 93 | private: 94 | std::streambuf *m_sbuf {nullptr}; /**< Stores the handle to the original cout stream. */ 95 | bool m_test_skipped_at_setup {false}; /**< Indicates whether the SetUp method was skipped. */ 96 | }; 97 | -------------------------------------------------------------------------------- /tests/unit/windows/utils/comparison.cpp: -------------------------------------------------------------------------------- 1 | // local includes 2 | #include "comparison.h" 3 | 4 | bool operator==(const LUID &lhs, const LUID &rhs) { 5 | return lhs.HighPart == rhs.HighPart && lhs.LowPart == rhs.LowPart; 6 | } 7 | 8 | bool operator==(const POINTL &lhs, const POINTL &rhs) { 9 | return lhs.x == rhs.x && lhs.y == rhs.y; 10 | } 11 | 12 | bool operator==(const RECTL &lhs, const RECTL &rhs) { 13 | return lhs.bottom == rhs.bottom && lhs.left == rhs.left && lhs.right == rhs.right && lhs.top == rhs.top; 14 | } 15 | 16 | bool operator==(const DISPLAYCONFIG_RATIONAL &lhs, const DISPLAYCONFIG_RATIONAL &rhs) { 17 | return lhs.Denominator == rhs.Denominator && lhs.Numerator == rhs.Numerator; 18 | } 19 | 20 | bool operator==(const DISPLAYCONFIG_2DREGION &lhs, const DISPLAYCONFIG_2DREGION &rhs) { 21 | return lhs.cx == rhs.cx && lhs.cy == rhs.cy; 22 | } 23 | 24 | bool operator==(const DISPLAYCONFIG_PATH_SOURCE_INFO &lhs, const DISPLAYCONFIG_PATH_SOURCE_INFO &rhs) { 25 | // clang-format off 26 | return lhs.adapterId == rhs.adapterId && 27 | lhs.id == rhs.id && 28 | lhs.cloneGroupId == rhs.cloneGroupId && 29 | lhs.sourceModeInfoIdx == rhs.sourceModeInfoIdx && 30 | lhs.statusFlags == rhs.statusFlags; 31 | // clang-format on 32 | } 33 | 34 | bool operator==(const DISPLAYCONFIG_PATH_TARGET_INFO &lhs, const DISPLAYCONFIG_PATH_TARGET_INFO &rhs) { 35 | // clang-format off 36 | return lhs.adapterId == rhs.adapterId && 37 | lhs.id == rhs.id && 38 | lhs.desktopModeInfoIdx == rhs.desktopModeInfoIdx && 39 | lhs.targetModeInfoIdx == rhs.targetModeInfoIdx && 40 | lhs.outputTechnology == rhs.outputTechnology && 41 | lhs.rotation == rhs.rotation && 42 | lhs.scaling == rhs.scaling && 43 | lhs.refreshRate == rhs.refreshRate && 44 | lhs.scanLineOrdering == rhs.scanLineOrdering && 45 | lhs.targetAvailable == rhs.targetAvailable && 46 | lhs.statusFlags == rhs.statusFlags; 47 | // clang-format on 48 | } 49 | 50 | bool operator==(const DISPLAYCONFIG_PATH_INFO &lhs, const DISPLAYCONFIG_PATH_INFO &rhs) { 51 | return lhs.sourceInfo == rhs.sourceInfo && lhs.targetInfo == rhs.targetInfo && lhs.flags == rhs.flags; 52 | } 53 | 54 | bool operator==(const DISPLAYCONFIG_SOURCE_MODE &lhs, const DISPLAYCONFIG_SOURCE_MODE &rhs) { 55 | return lhs.width == rhs.width && lhs.height == rhs.height && lhs.pixelFormat == rhs.pixelFormat && lhs.position == rhs.position; 56 | } 57 | 58 | bool operator==(const DISPLAYCONFIG_VIDEO_SIGNAL_INFO &lhs, const DISPLAYCONFIG_VIDEO_SIGNAL_INFO &rhs) { 59 | // clang-format on 60 | return lhs.pixelRate == rhs.pixelRate && 61 | lhs.hSyncFreq == rhs.hSyncFreq && 62 | lhs.vSyncFreq == rhs.vSyncFreq && 63 | lhs.activeSize == rhs.activeSize && 64 | lhs.totalSize == rhs.totalSize && 65 | lhs.videoStandard == rhs.videoStandard && 66 | lhs.scanLineOrdering == rhs.scanLineOrdering; 67 | // clang-format oon 68 | } 69 | 70 | bool operator==(const DISPLAYCONFIG_TARGET_MODE &lhs, const DISPLAYCONFIG_TARGET_MODE &rhs) { 71 | return lhs.targetVideoSignalInfo == rhs.targetVideoSignalInfo; 72 | } 73 | 74 | bool operator==(const DISPLAYCONFIG_DESKTOP_IMAGE_INFO &lhs, const DISPLAYCONFIG_DESKTOP_IMAGE_INFO &rhs) { 75 | return lhs.PathSourceSize == rhs.PathSourceSize && lhs.DesktopImageRegion == rhs.DesktopImageRegion && lhs.DesktopImageClip == rhs.DesktopImageClip; 76 | } 77 | 78 | bool operator==(const DISPLAYCONFIG_MODE_INFO &lhs, const DISPLAYCONFIG_MODE_INFO &rhs) { 79 | if (lhs.infoType == rhs.infoType && lhs.id == rhs.id && lhs.adapterId == rhs.adapterId) { 80 | if (lhs.infoType == DISPLAYCONFIG_MODE_INFO_TYPE_SOURCE) { 81 | return lhs.sourceMode == rhs.sourceMode; 82 | } else if (lhs.infoType == DISPLAYCONFIG_MODE_INFO_TYPE_TARGET) { 83 | return lhs.targetMode == rhs.targetMode; 84 | } else if (lhs.infoType == DISPLAYCONFIG_MODE_INFO_TYPE_DESKTOP_IMAGE) { 85 | // TODO: fix once implemented 86 | return false; 87 | } else { 88 | return true; 89 | } 90 | } 91 | return false; 92 | } 93 | 94 | namespace display_device { 95 | bool operator==(const PathSourceIndexData &lhs, const PathSourceIndexData &rhs) { 96 | return lhs.m_source_id_to_path_index == rhs.m_source_id_to_path_index && lhs.m_adapter_id == rhs.m_adapter_id && lhs.m_active_source == rhs.m_active_source; 97 | } 98 | } // namespace display_device 99 | -------------------------------------------------------------------------------- /tests/unit/general/test_file_settings_persistence.cpp: -------------------------------------------------------------------------------- 1 | // system includes 2 | #include 3 | #include 4 | 5 | // local includes 6 | #include "display_device/file_settings_persistence.h" 7 | #include "fixtures/fixtures.h" 8 | 9 | namespace { 10 | // Convenience keywords for GMock 11 | using ::testing::HasSubstr; 12 | 13 | // Test fixture(s) for this file 14 | class FileSettingsPersistenceTest: public BaseTest { 15 | public: 16 | ~FileSettingsPersistenceTest() override { 17 | std::filesystem::remove(m_filepath); 18 | } 19 | 20 | display_device::FileSettingsPersistence &getImpl(const std::filesystem::path &filepath = "testfile.ext") { 21 | if (!m_impl) { 22 | m_filepath = filepath; 23 | m_impl = std::make_unique(m_filepath); 24 | } 25 | 26 | return *m_impl; 27 | } 28 | 29 | private: 30 | std::filesystem::path m_filepath; 31 | std::unique_ptr m_impl; 32 | }; 33 | 34 | // Specialized TEST macro(s) for this test file 35 | #define TEST_F_S(...) DD_MAKE_TEST(TEST_F, FileSettingsPersistenceTest, __VA_ARGS__) 36 | } // namespace 37 | 38 | TEST_F_S(EmptyFilenameProvided) { 39 | EXPECT_THAT([]() { 40 | const display_device::FileSettingsPersistence persistence {{}}; 41 | }, 42 | ThrowsMessage(HasSubstr("Empty filename provided for FileSettingsPersistence!"))); 43 | } 44 | 45 | TEST_F_S(Store, NewFileCreated) { 46 | const std::filesystem::path filepath {"myfile.ext"}; 47 | const std::vector data {0x00, 0x01, 0x02, 0x04, 'S', 'O', 'M', 'E', ' ', 'D', 'A', 'T', 'A'}; 48 | 49 | EXPECT_FALSE(std::filesystem::exists(filepath)); 50 | EXPECT_TRUE(getImpl(filepath).store(data)); 51 | EXPECT_TRUE(std::filesystem::exists(filepath)); 52 | 53 | std::ifstream stream {filepath, std::ios::binary}; 54 | std::vector file_data {std::istreambuf_iterator {stream}, std::istreambuf_iterator {}}; 55 | EXPECT_EQ(file_data, data); 56 | } 57 | 58 | TEST_F_S(Store, FileOverwritten) { 59 | const std::filesystem::path filepath {"myfile.ext"}; 60 | const std::vector data1 {0x00, 0x01, 0x02, 0x04, 'S', 'O', 'M', 'E', ' ', 'D', 'A', 'T', 'A', ' ', '1'}; 61 | const std::vector data2 {0x00, 0x01, 0x02, 0x04, 'S', 'O', 'M', 'E', ' ', 'D', 'A', 'T', 'A', ' ', '2'}; 62 | 63 | { 64 | std::ofstream file {filepath, std::ios_base::binary}; 65 | std::copy(std::begin(data1), std::end(data1), std::ostreambuf_iterator {file}); 66 | } 67 | 68 | EXPECT_TRUE(std::filesystem::exists(filepath)); 69 | EXPECT_TRUE(getImpl(filepath).store(data2)); 70 | EXPECT_TRUE(std::filesystem::exists(filepath)); 71 | 72 | std::ifstream stream {filepath, std::ios::binary}; 73 | std::vector file_data {std::istreambuf_iterator {stream}, std::istreambuf_iterator {}}; 74 | EXPECT_EQ(file_data, data2); 75 | } 76 | 77 | TEST_F_S(Store, FilepathWithDirectory) { 78 | const std::filesystem::path filepath {"somedir/myfile.ext"}; 79 | const std::vector data {0x00, 0x01, 0x02, 0x04, 'S', 'O', 'M', 'E', ' ', 'D', 'A', 'T', 'A'}; 80 | 81 | EXPECT_FALSE(std::filesystem::exists(filepath)); 82 | EXPECT_FALSE(getImpl(filepath).store(data)); 83 | EXPECT_FALSE(std::filesystem::exists(filepath)); 84 | } 85 | 86 | TEST_F_S(Load, NoFileAvailable) { 87 | EXPECT_EQ(getImpl().load(), std::vector {}); 88 | } 89 | 90 | TEST_F_S(Load, FileRead) { 91 | const std::filesystem::path filepath {"myfile.ext"}; 92 | const std::vector data {0x00, 0x01, 0x02, 0x04, 'S', 'O', 'M', 'E', ' ', 'D', 'A', 'T', 'A'}; 93 | 94 | { 95 | std::ofstream file {filepath, std::ios_base::binary}; 96 | std::copy(std::begin(data), std::end(data), std::ostreambuf_iterator {file}); 97 | } 98 | 99 | EXPECT_EQ(getImpl(filepath).load(), data); 100 | } 101 | 102 | TEST_F_S(Clear, NoFileAvailable) { 103 | EXPECT_TRUE(getImpl().clear()); 104 | } 105 | 106 | TEST_F_S(Clear, FileRemoved) { 107 | const std::filesystem::path filepath {"myfile.ext"}; 108 | { 109 | std::ofstream file {filepath}; 110 | file << "some data"; 111 | } 112 | 113 | EXPECT_TRUE(std::filesystem::exists(filepath)); 114 | EXPECT_TRUE(getImpl(filepath).clear()); 115 | EXPECT_FALSE(std::filesystem::exists(filepath)); 116 | } 117 | -------------------------------------------------------------------------------- /src/windows/win_display_device_primary.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file src/windows/win_display_device_primary.cpp 3 | * @brief Definitions for the "primary device" related methods in WinDisplayDevice. 4 | */ 5 | // class header include 6 | #include "display_device/windows/win_display_device.h" 7 | 8 | // local includes 9 | #include "display_device/logging.h" 10 | #include "display_device/windows/win_api_utils.h" 11 | 12 | namespace display_device { 13 | bool WinDisplayDevice::isPrimary(const std::string &device_id) const { 14 | if (device_id.empty()) { 15 | DD_LOG(error) << "Device id is empty!"; 16 | return false; 17 | } 18 | 19 | const auto display_data {m_w_api->queryDisplayConfig(QueryType::Active)}; 20 | if (!display_data) { 21 | // Error already logged 22 | return false; 23 | } 24 | 25 | const auto path {win_utils::getActivePath(*m_w_api, device_id, display_data->m_paths)}; 26 | if (!path) { 27 | DD_LOG(error) << "Failed to find active device for " << device_id << "!"; 28 | return false; 29 | } 30 | 31 | const auto source_mode {win_utils::getSourceMode(win_utils::getSourceIndex(*path, display_data->m_modes), display_data->m_modes)}; 32 | if (!source_mode) { 33 | DD_LOG(error) << "Active device does not have a source mode: " << device_id << "!"; 34 | return false; 35 | } 36 | 37 | return win_utils::isPrimary(*source_mode); 38 | } 39 | 40 | bool WinDisplayDevice::setAsPrimary(const std::string &device_id) { 41 | if (device_id.empty()) { 42 | DD_LOG(error) << "Device id is empty!"; 43 | return false; 44 | } 45 | 46 | auto display_data {m_w_api->queryDisplayConfig(QueryType::Active)}; 47 | if (!display_data) { 48 | // Error already logged 49 | return false; 50 | } 51 | 52 | // Get the current origin point of the device (the one that we want to make primary) 53 | POINTL origin; 54 | { 55 | const auto path {win_utils::getActivePath(*m_w_api, device_id, display_data->m_paths)}; 56 | if (!path) { 57 | DD_LOG(error) << "Failed to find device for " << device_id << "!"; 58 | return false; 59 | } 60 | 61 | const auto source_mode {win_utils::getSourceMode(win_utils::getSourceIndex(*path, display_data->m_modes), display_data->m_modes)}; 62 | if (!source_mode) { 63 | DD_LOG(error) << "Active device does not have a source mode: " << device_id << "!"; 64 | return false; 65 | } 66 | 67 | if (win_utils::isPrimary(*source_mode)) { 68 | DD_LOG(debug) << "Device " << device_id << " is already a primary device."; 69 | return true; 70 | } 71 | 72 | origin = source_mode->position; 73 | } 74 | 75 | // Shift the source mode origin points accordingly, so that the provided 76 | // device moves to (0, 0) position and others to their new positions. 77 | std::set modified_modes; 78 | for (auto &path : display_data->m_paths) { 79 | const auto current_id {m_w_api->getDeviceId(path)}; 80 | const auto source_index {win_utils::getSourceIndex(path, display_data->m_modes)}; 81 | auto source_mode {win_utils::getSourceMode(source_index, display_data->m_modes)}; 82 | 83 | if (!source_index || !source_mode) { 84 | DD_LOG(error) << "Active device does not have a source mode: " << current_id << "!"; 85 | return false; 86 | } 87 | 88 | if (modified_modes.find(*source_index) != std::end(modified_modes)) { 89 | // Happens when VIRTUAL_MODE_AWARE is not specified when querying paths, probably will never happen in our (since it's always set), but just to be safe... 90 | DD_LOG(debug) << "Device " << current_id << " shares the same mode index as a previous device. Device is duplicated. Skipping."; 91 | continue; 92 | } 93 | 94 | source_mode->position.x -= origin.x; 95 | source_mode->position.y -= origin.y; 96 | 97 | modified_modes.insert(*source_index); 98 | } 99 | 100 | const UINT32 flags {SDC_APPLY | SDC_USE_SUPPLIED_DISPLAY_CONFIG | SDC_SAVE_TO_DATABASE | SDC_VIRTUAL_MODE_AWARE}; 101 | const LONG result {m_w_api->setDisplayConfig(display_data->m_paths, display_data->m_modes, flags)}; 102 | if (result != ERROR_SUCCESS) { 103 | DD_LOG(error) << m_w_api->getErrorString(result) << " failed to set primary mode for " << device_id << "!"; 104 | return false; 105 | } 106 | 107 | return true; 108 | } 109 | } // namespace display_device 110 | -------------------------------------------------------------------------------- /tests/unit/windows/utils/mock_win_api_layer.cpp: -------------------------------------------------------------------------------- 1 | // local includes 2 | #include "mock_win_api_layer.h" 3 | 4 | namespace { 5 | display_device::PathAndModeData make3ActiveDeviceGroups(const bool include_duplicate) { 6 | display_device::PathAndModeData data; 7 | 8 | // 1st group (1 device) 9 | { 10 | data.m_paths.push_back({}); 11 | data.m_paths.back().flags = DISPLAYCONFIG_PATH_ACTIVE; 12 | data.m_paths.back().sourceInfo.sourceModeInfoIdx = data.m_modes.size(); 13 | data.m_paths.back().sourceInfo.adapterId = {1, 1}; 14 | data.m_paths.back().sourceInfo.id = 1; 15 | data.m_paths.back().targetInfo.targetAvailable = TRUE; 16 | data.m_paths.back().targetInfo.refreshRate = {120, 1}; 17 | 18 | data.m_modes.push_back({}); 19 | data.m_modes.back().infoType = DISPLAYCONFIG_MODE_INFO_TYPE_SOURCE; 20 | data.m_modes.back().sourceMode = {}; // Set the union 21 | data.m_modes.back().sourceMode.position = {0, 0}; 22 | data.m_modes.back().sourceMode.width = 1920; 23 | data.m_modes.back().sourceMode.height = 1080; 24 | } 25 | 26 | // 2nd group (1+ device) 27 | { 28 | data.m_paths.push_back({}); 29 | data.m_paths.back().flags = DISPLAYCONFIG_PATH_ACTIVE; 30 | data.m_paths.back().sourceInfo.sourceModeInfoIdx = data.m_modes.size(); 31 | data.m_paths.back().sourceInfo.adapterId = {2, 2}; 32 | data.m_paths.back().sourceInfo.id = 2; 33 | data.m_paths.back().targetInfo.targetAvailable = TRUE; 34 | data.m_paths.back().targetInfo.refreshRate = {119995, 1000}; 35 | 36 | data.m_modes.push_back({}); 37 | data.m_modes.back().infoType = DISPLAYCONFIG_MODE_INFO_TYPE_SOURCE; 38 | data.m_modes.back().sourceMode = {}; // Set the union 39 | data.m_modes.back().sourceMode.position = {1921, 0}; 40 | data.m_modes.back().sourceMode.width = 1920; 41 | data.m_modes.back().sourceMode.height = 2160; 42 | 43 | if (include_duplicate) { 44 | data.m_paths.push_back({}); 45 | data.m_paths.back().flags = DISPLAYCONFIG_PATH_ACTIVE; 46 | data.m_paths.back().sourceInfo.sourceModeInfoIdx = data.m_modes.size(); 47 | data.m_paths.back().sourceInfo.adapterId = {3, 3}; 48 | data.m_paths.back().sourceInfo.id = 3; 49 | data.m_paths.back().targetInfo.targetAvailable = TRUE; 50 | data.m_paths.back().targetInfo.refreshRate = {60, 1}; 51 | 52 | data.m_modes.push_back({}); 53 | data.m_modes.back().infoType = DISPLAYCONFIG_MODE_INFO_TYPE_SOURCE; 54 | data.m_modes.back().sourceMode = {}; // Set the union 55 | data.m_modes.back().sourceMode.position = {1921, 0}; 56 | data.m_modes.back().sourceMode.width = 1920; 57 | data.m_modes.back().sourceMode.height = 2160; 58 | } 59 | } 60 | 61 | // 3rd group (1 device) 62 | { 63 | data.m_paths.push_back({}); 64 | data.m_paths.back().flags = DISPLAYCONFIG_PATH_ACTIVE; 65 | data.m_paths.back().sourceInfo.sourceModeInfoIdx = data.m_modes.size(); 66 | data.m_paths.back().sourceInfo.adapterId = {4, 4}; 67 | data.m_paths.back().sourceInfo.id = 4; 68 | data.m_paths.back().targetInfo.targetAvailable = TRUE; 69 | data.m_paths.back().targetInfo.refreshRate = {90, 1}; 70 | 71 | data.m_modes.push_back({}); 72 | data.m_modes.back().infoType = DISPLAYCONFIG_MODE_INFO_TYPE_SOURCE; 73 | data.m_modes.back().sourceMode = {}; // Set the union 74 | data.m_modes.back().sourceMode.position = {0, 1081}; 75 | data.m_modes.back().sourceMode.width = 3840; 76 | data.m_modes.back().sourceMode.height = 2160; 77 | } 78 | 79 | return data; 80 | } 81 | } // namespace 82 | 83 | namespace ut_consts { 84 | const std::optional PAM_NULL {std::nullopt}; 85 | const std::optional PAM_EMPTY {display_device::PathAndModeData {}}; 86 | const std::optional PAM_3_ACTIVE {make3ActiveDeviceGroups(false)}; 87 | const std::optional PAM_3_ACTIVE_WITH_INVALID_MODE_IDX {[]() { 88 | auto data {make3ActiveDeviceGroups(false)}; 89 | 90 | // Scramble the indexes 91 | auto invalid_idx {DISPLAYCONFIG_PATH_SOURCE_MODE_IDX_INVALID}; 92 | for (auto &item : data.m_paths) { 93 | item.sourceInfo.sourceModeInfoIdx = invalid_idx--; 94 | } 95 | 96 | return data; 97 | }()}; 98 | const std::optional PAM_4_ACTIVE_WITH_2_DUPLICATES {make3ActiveDeviceGroups(true)}; 99 | } // namespace ut_consts 100 | -------------------------------------------------------------------------------- /src/windows/win_display_device_hdr.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file src/windows/win_display_device_hdr.cpp 3 | * @brief Definitions for the HDR related methods in WinDisplayDevice. 4 | */ 5 | // class header include 6 | #include "display_device/windows/win_display_device.h" 7 | 8 | // system includes 9 | #include 10 | 11 | // local includes 12 | #include "display_device/logging.h" 13 | #include "display_device/windows/win_api_utils.h" 14 | 15 | namespace display_device { 16 | namespace { 17 | /** @brief HDR state map without optional values. */ 18 | using HdrStateMapNoOpt = std::map; 19 | 20 | /** 21 | * @see setHdrStates for a description as this was split off to reduce cognitive complexity. 22 | */ 23 | bool doSetHdrStates(WinApiLayerInterface &w_api, const PathAndModeData &display_data, const HdrStateMapNoOpt &states, HdrStateMapNoOpt *changed_states) { 24 | const auto try_set_state { 25 | [&w_api, &display_data](const auto &device_id, const auto &state, auto ¤t_state) { 26 | const auto path {win_utils::getActivePath(w_api, device_id, display_data.m_paths)}; 27 | if (!path) { 28 | DD_LOG(error) << "Failed to find device for " << device_id << "!"; 29 | return false; 30 | } 31 | 32 | const auto current_state_int {w_api.getHdrState(*path)}; 33 | if (!current_state_int) { 34 | DD_LOG(error) << "HDR state cannot be changed for " << device_id << "!"; 35 | return false; 36 | } 37 | 38 | if (state != *current_state_int) { 39 | if (!w_api.setHdrState(*path, state)) { 40 | // Error already logged 41 | return false; 42 | } 43 | 44 | current_state = current_state_int; 45 | } 46 | 47 | return true; 48 | } 49 | }; 50 | 51 | for (const auto &[device_id, state] : states) { 52 | std::optional current_state; 53 | if (try_set_state(device_id, state, current_state)) { 54 | if (current_state && changed_states != nullptr) { 55 | (*changed_states)[device_id] = *current_state; 56 | } 57 | } 58 | // If we are undoing changes we don't want to return early and continue regardless of what error we get. 59 | else if (changed_states != nullptr) { 60 | return false; 61 | } 62 | } 63 | 64 | return true; 65 | } 66 | 67 | } // namespace 68 | 69 | HdrStateMap WinDisplayDevice::getCurrentHdrStates(const std::set &device_ids) const { 70 | if (device_ids.empty()) { 71 | DD_LOG(error) << "Device id set is empty!"; 72 | return {}; 73 | } 74 | 75 | const auto display_data {m_w_api->queryDisplayConfig(QueryType::Active)}; 76 | if (!display_data) { 77 | // Error already logged 78 | return {}; 79 | } 80 | 81 | HdrStateMap states; 82 | for (const auto &device_id : device_ids) { 83 | const auto path {win_utils::getActivePath(*m_w_api, device_id, display_data->m_paths)}; 84 | if (!path) { 85 | DD_LOG(error) << "Failed to find device for " << device_id << "!"; 86 | return {}; 87 | } 88 | 89 | states[device_id] = m_w_api->getHdrState(*path); 90 | } 91 | 92 | return states; 93 | } 94 | 95 | bool WinDisplayDevice::setHdrStates(const HdrStateMap &states) { 96 | if (states.empty()) { 97 | DD_LOG(error) << "States map is empty!"; 98 | return false; 99 | } 100 | 101 | HdrStateMapNoOpt states_without_opt; 102 | std::ranges::copy(states | std::ranges::views::filter([](const auto &entry) { 103 | return static_cast(entry.second); 104 | }) | 105 | std::views::transform([](const auto &entry) { 106 | return std::make_pair(entry.first, *entry.second); 107 | }), 108 | std::inserter(states_without_opt, std::begin(states_without_opt))); 109 | 110 | if (states_without_opt.empty()) { 111 | // Return early as there is nothing to do... 112 | return true; 113 | } 114 | 115 | const auto display_data {m_w_api->queryDisplayConfig(QueryType::Active)}; 116 | if (!display_data) { 117 | // Error already logged 118 | return {}; 119 | } 120 | 121 | HdrStateMapNoOpt changed_states; 122 | if (!doSetHdrStates(*m_w_api, *display_data, states_without_opt, &changed_states)) { 123 | if (!changed_states.empty()) { 124 | doSetHdrStates(*m_w_api, *display_data, changed_states, nullptr); // return value does not matter 125 | } 126 | return false; 127 | } 128 | 129 | return true; 130 | } 131 | } // namespace display_device 132 | -------------------------------------------------------------------------------- /src/windows/win_display_device_general.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file src/windows/win_display_device_general.cpp 3 | * @brief Definitions for the leftover (general) methods in WinDisplayDevice. 4 | */ 5 | // class header include 6 | #include "display_device/windows/win_display_device.h" 7 | 8 | // local includes 9 | #include "display_device/logging.h" 10 | #include "display_device/windows/win_api_utils.h" 11 | 12 | namespace display_device { 13 | WinDisplayDevice::WinDisplayDevice(std::shared_ptr w_api): 14 | m_w_api {std::move(w_api)} { 15 | if (!m_w_api) { 16 | throw std::logic_error {"Nullptr provided for WinApiLayerInterface in WinDisplayDevice!"}; 17 | } 18 | } 19 | 20 | bool WinDisplayDevice::isApiAccessAvailable() const { 21 | // Unless something is really broken on Windows, this call should never fail under normal circumstances - the configuration is 100% correct, since it was 22 | // provided by Windows. 23 | const UINT32 flags {SDC_VALIDATE | SDC_USE_DATABASE_CURRENT}; 24 | const LONG result {m_w_api->setDisplayConfig({}, {}, flags)}; 25 | 26 | DD_LOG(debug) << "WinDisplayDevice::isApiAccessAvailable result: " << m_w_api->getErrorString(result); 27 | return result == ERROR_SUCCESS; 28 | } 29 | 30 | EnumeratedDeviceList WinDisplayDevice::enumAvailableDevices() const { 31 | const auto display_data {m_w_api->queryDisplayConfig(QueryType::All)}; 32 | if (!display_data) { 33 | // Error already logged 34 | return {}; 35 | } 36 | 37 | EnumeratedDeviceList available_devices; 38 | const auto source_data {win_utils::collectSourceDataForMatchingPaths(*m_w_api, display_data->m_paths)}; 39 | if (source_data.empty()) { 40 | // Error already logged 41 | return {}; 42 | } 43 | 44 | for (const auto &[device_id, data] : source_data) { 45 | // In case we have no active source, we will take the first available source id 46 | const auto source_id_index {data.m_active_source.value_or(data.m_source_id_to_path_index.begin()->first)}; 47 | const auto &best_path {display_data->m_paths.at(data.m_source_id_to_path_index.at(source_id_index))}; 48 | const auto friendly_name {m_w_api->getFriendlyName(best_path)}; 49 | const bool is_active {win_utils::isActive(best_path)}; 50 | const auto source_mode {is_active ? win_utils::getSourceMode(win_utils::getSourceIndex(best_path, display_data->m_modes), display_data->m_modes) : nullptr}; 51 | const auto display_name {is_active ? m_w_api->getDisplayName(best_path) : std::string {}}; // Inactive devices can have multiple display names, so it's just meaningless use any 52 | const auto edid {EdidData::parse(m_w_api->getEdid(best_path))}; 53 | 54 | if (is_active && !source_mode) { 55 | DD_LOG(warning) << "Device " << device_id << " is missing source mode!"; 56 | } 57 | 58 | if (source_mode) { 59 | const Rational refresh_rate {best_path.targetInfo.refreshRate.Denominator > 0 ? Rational {best_path.targetInfo.refreshRate.Numerator, best_path.targetInfo.refreshRate.Denominator} : Rational {0, 1}}; 60 | const EnumeratedDevice::Info info { 61 | {source_mode->width, source_mode->height}, 62 | m_w_api->getDisplayScale(display_name, *source_mode).value_or(Rational {0, 1}), 63 | refresh_rate, 64 | win_utils::isPrimary(*source_mode), 65 | {static_cast(source_mode->position.x), static_cast(source_mode->position.y)}, 66 | m_w_api->getHdrState(best_path) 67 | }; 68 | 69 | available_devices.push_back( 70 | {device_id, 71 | display_name, 72 | friendly_name, 73 | edid, 74 | info} 75 | ); 76 | } else { 77 | available_devices.push_back( 78 | {device_id, 79 | display_name, 80 | friendly_name, 81 | edid, 82 | std::nullopt} 83 | ); 84 | } 85 | } 86 | 87 | return available_devices; 88 | } 89 | 90 | std::string WinDisplayDevice::getDisplayName(const std::string &device_id) const { 91 | if (device_id.empty()) { 92 | // Valid return, no error 93 | return {}; 94 | } 95 | 96 | const auto display_data {m_w_api->queryDisplayConfig(QueryType::Active)}; 97 | if (!display_data) { 98 | // Error already logged 99 | return {}; 100 | } 101 | 102 | const auto path {win_utils::getActivePath(*m_w_api, device_id, display_data->m_paths)}; 103 | if (!path) { 104 | // Debug level, because inactive device is valid case for this function 105 | DD_LOG(debug) << "Failed to find device for " << device_id << "!"; 106 | return {}; 107 | } 108 | 109 | const auto display_name {m_w_api->getDisplayName(*path)}; 110 | if (display_name.empty()) { 111 | // Theoretically possible due to some race condition in the OS... 112 | DD_LOG(error) << "Device " << device_id << " has no display name assigned."; 113 | } 114 | 115 | return display_name; 116 | } 117 | } // namespace display_device 118 | -------------------------------------------------------------------------------- /src/common/include/display_device/settings_manager_interface.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file src/common/include/display_device/settings_manager_interface.h 3 | * @brief Declarations for the SettingsManagerInterface. 4 | */ 5 | #pragma once 6 | 7 | // local includes 8 | #include "types.h" 9 | 10 | namespace display_device { 11 | /** 12 | * @brief A class for applying and reverting display device settings. 13 | */ 14 | class SettingsManagerInterface { 15 | public: 16 | /** 17 | * @brief Outcome values when trying to apply settings. 18 | */ 19 | enum class ApplyResult { 20 | Ok, ///< Settings were applied successfully 21 | ApiTemporarilyUnavailable, ///< API is temporarily unavailable 22 | DevicePrepFailed, ///< Device preparation failed 23 | PrimaryDevicePrepFailed, ///< Primary device preparation failed 24 | DisplayModePrepFailed, ///< Display mode preparation failed 25 | HdrStatePrepFailed, ///< HDR state preparation failed 26 | PersistenceSaveFailed, ///< Persistence save failed 27 | }; 28 | 29 | /** 30 | * @brief Outcome values when trying to revert settings. 31 | */ 32 | enum class RevertResult { 33 | Ok, ///< Settings were reverted successfully 34 | ApiTemporarilyUnavailable, ///< API is temporarily unavailable 35 | TopologyIsInvalid, ///< Topology is invalid 36 | SwitchingTopologyFailed, ///< Switching topology has failed 37 | RevertingPrimaryDeviceFailed, ///< Reverting primary device failed 38 | RevertingDisplayModesFailed, ///< Reverting display modes failed 39 | RevertingHdrStatesFailed, ///< Reverting HDR states failed 40 | PersistenceSaveFailed, ///< Persistence save failed 41 | }; 42 | 43 | /** 44 | * @brief Default virtual destructor. 45 | */ 46 | virtual ~SettingsManagerInterface() = default; 47 | 48 | /** 49 | * @brief Enumerate the available (active and inactive) devices. 50 | * @returns A list of available devices. 51 | * Empty list can also be returned if an error has occurred. 52 | * @examples 53 | * const SettingsManagerInterface* iface = getIface(...); 54 | * const auto devices { iface->enumAvailableDevices() }; 55 | * @examples_end 56 | */ 57 | [[nodiscard]] virtual EnumeratedDeviceList enumAvailableDevices() const = 0; 58 | 59 | /** 60 | * @brief Get display name associated with the device. 61 | * @param device_id A device to get display name for. 62 | * @returns A display name for the device, or an empty string if the device is inactive or not found. 63 | * Empty string can also be returned if an error has occurred. 64 | * @examples 65 | * const std::string device_id { "MY_DEVICE_ID" }; 66 | * const SettingsManagerInterface* iface = getIface(...); 67 | * const std::string display_name = iface->getDisplayName(device_id); 68 | * @examples_end 69 | */ 70 | [[nodiscard]] virtual std::string getDisplayName(const std::string &device_id) const = 0; 71 | 72 | /** 73 | * @brief Apply the provided configuration to the system. 74 | * @param config A desired configuration for the display device. 75 | * @returns The apply result. 76 | * @examples 77 | * const SingleDisplayConfiguration config; 78 | * 79 | * SettingsManagerInterface* iface = getIface(...); 80 | * const auto result = iface->applySettings(config); 81 | * @examples_end 82 | */ 83 | [[nodiscard]] virtual ApplyResult applySettings(const SingleDisplayConfiguration &config) = 0; 84 | 85 | /** 86 | * @brief Revert the applied configuration and restore the previous settings. 87 | * @returns True if settings were reverted or there was nothing to revert, false otherwise. 88 | * @examples 89 | * SettingsManagerInterface* iface = getIface(...); 90 | * const auto result = iface->revertSettings(); 91 | * @examples_end 92 | */ 93 | [[nodiscard]] virtual RevertResult revertSettings() = 0; 94 | 95 | /** 96 | * @brief Reset the persistence in case the settings cannot be reverted. 97 | * @returns True if persistence was reset, false otherwise. 98 | * 99 | * In case the settings cannot be reverted, because the display is turned or some other reason, 100 | * this allows to "accept" the current state and start from scratch, but only if the persistence was 101 | * cleared successfully. 102 | * @examples 103 | * SettingsManagerInterface* iface = getIface(...); 104 | * auto result = iface->applySettings(config); 105 | * if (result == ApplyResult::Ok) { 106 | * // Wait for some time 107 | * if (iface->revertSettings() != RevertResult::Ok) { 108 | * // Wait for user input 109 | * const bool user_wants_reset { true }; 110 | * if (user_wants_reset) { 111 | * iface->resetPersistence(); 112 | * } 113 | * } 114 | * } 115 | * @examples_end 116 | */ 117 | [[nodiscard]] virtual bool resetPersistence() = 0; 118 | }; 119 | } // namespace display_device 120 | -------------------------------------------------------------------------------- /src/common/include/display_device/types.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file src/common/include/display_device/types.h 3 | * @brief Declarations for common display device types. 4 | */ 5 | #pragma once 6 | 7 | // system includes 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | namespace display_device { 15 | /** 16 | * @brief The device's HDR state in the operating system. 17 | */ 18 | enum class HdrState { 19 | Disabled, ///< HDR is disabled 20 | Enabled ///< HDR is enabled 21 | }; 22 | 23 | /** 24 | * @brief Display's resolution. 25 | */ 26 | struct Resolution { 27 | unsigned int m_width {}; 28 | unsigned int m_height {}; 29 | 30 | /** 31 | * @brief Comparator for strict equality. 32 | */ 33 | friend bool operator==(const Resolution &lhs, const Resolution &rhs); 34 | }; 35 | 36 | /** 37 | * @brief An arbitrary point object. 38 | */ 39 | struct Point { 40 | int m_x {}; 41 | int m_y {}; 42 | 43 | /** 44 | * @brief Comparator for strict equality. 45 | */ 46 | friend bool operator==(const Point &lhs, const Point &rhs); 47 | }; 48 | 49 | /** 50 | * @brief Floating point stored in a "numerator/denominator" form. 51 | */ 52 | struct Rational { 53 | unsigned int m_numerator {}; 54 | unsigned int m_denominator {}; 55 | 56 | /** 57 | * @brief Comparator for strict equality. 58 | */ 59 | friend bool operator==(const Rational &lhs, const Rational &rhs); 60 | }; 61 | 62 | /** 63 | * @brief Floating point type. 64 | */ 65 | using FloatingPoint = std::variant; 66 | 67 | /** 68 | * @brief Parsed EDID data. 69 | */ 70 | struct EdidData { 71 | std::string m_manufacturer_id {}; 72 | std::string m_product_code {}; 73 | std::uint32_t m_serial_number {}; 74 | 75 | /** 76 | * @brief Parse EDID data. 77 | * @param data Data to parse. 78 | * @return Parsed data or empty optional if failed to parse it. 79 | */ 80 | static std::optional parse(const std::vector &data); 81 | 82 | /** 83 | * @brief Comparator for strict equality. 84 | */ 85 | friend bool operator==(const EdidData &lhs, const EdidData &rhs); 86 | }; 87 | 88 | /** 89 | * @brief Enumerated display device information. 90 | */ 91 | struct EnumeratedDevice { 92 | /** 93 | * @brief Available information for the active display only. 94 | */ 95 | struct Info { 96 | Resolution m_resolution {}; /**< Resolution of an active device. */ 97 | FloatingPoint m_resolution_scale {}; /**< Resolution scaling of an active device. */ 98 | FloatingPoint m_refresh_rate {}; /**< Refresh rate of an active device. */ 99 | bool m_primary {}; /**< Indicates whether the device is a primary display. */ 100 | Point m_origin_point {}; /**< A starting point of the display. */ 101 | std::optional m_hdr_state {}; /**< HDR of an active device. */ 102 | 103 | /** 104 | * @brief Comparator for strict equality. 105 | */ 106 | friend bool operator==(const Info &lhs, const Info &rhs); 107 | }; 108 | 109 | std::string m_device_id {}; /**< A unique device ID used by this API to identify the device. */ 110 | std::string m_display_name {}; /**< A logical name representing given by the OS for a display. */ 111 | std::string m_friendly_name {}; /**< A human-readable name for the device. */ 112 | std::optional m_edid {}; /**< Some basic parsed EDID data. */ 113 | std::optional m_info {}; /**< Additional information about an active display device. */ 114 | 115 | /** 116 | * @brief Comparator for strict equality. 117 | */ 118 | friend bool operator==(const EnumeratedDevice &lhs, const EnumeratedDevice &rhs); 119 | }; 120 | 121 | /** 122 | * @brief A list of EnumeratedDevice objects. 123 | */ 124 | using EnumeratedDeviceList = std::vector; 125 | 126 | /** 127 | * @brief Configuration centered around a single display. 128 | * 129 | * Allows to easily configure the display without providing a complete configuration 130 | * for all of the system display devices. 131 | */ 132 | struct SingleDisplayConfiguration { 133 | /** 134 | * @brief Enum detailing how to prepare the display device. 135 | */ 136 | enum class DevicePreparation { 137 | VerifyOnly, /**< User has to make sure the display device is active, we will only verify. */ 138 | EnsureActive, /**< Activate the device if needed. */ 139 | EnsurePrimary, /**< Activate the device if needed and make it a primary display. */ 140 | EnsureOnlyDisplay /**< Deactivate other displays and turn on the specified one only. */ 141 | }; 142 | 143 | std::string m_device_id {}; /**< Device to perform configuration for (can be empty if primary device should be used). */ 144 | DevicePreparation m_device_prep {}; /**< Instruction on how to prepare device. */ 145 | std::optional m_resolution {}; /**< Resolution to configure. */ 146 | std::optional m_refresh_rate {}; /**< Refresh rate to configure. */ 147 | std::optional m_hdr_state {}; /**< HDR state to configure (if supported by the display). */ 148 | 149 | /** 150 | * @brief Comparator for strict equality. 151 | */ 152 | friend bool operator==(const SingleDisplayConfiguration &lhs, const SingleDisplayConfiguration &rhs); 153 | }; 154 | } // namespace display_device 155 | -------------------------------------------------------------------------------- /src/common/include/display_device/logging.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file src/common/include/display_device/logging.h 3 | * @brief Declarations for the logging utility. 4 | */ 5 | #pragma once 6 | 7 | // system includes 8 | #include 9 | #include 10 | #include 11 | 12 | namespace display_device { 13 | /** 14 | * @brief A singleton class for logging or re-routing logs. 15 | * 16 | * This class is not meant to be used directly (only for configuration). 17 | * Instead, the MACRO below should be used throughout the code for logging. 18 | * 19 | * @note A lazy-evaluated, correctly-destroyed, thread-safe singleton pattern is used here (https://stackoverflow.com/a/1008289). 20 | */ 21 | class Logger { 22 | public: 23 | /** 24 | * @brief Defines the possible log levels. 25 | * @note Level implicitly includes all other levels below it. 26 | * @note All levels are in lower-case on purpose to fit the "BOOST_LOG(info)" style. 27 | */ 28 | enum class LogLevel { 29 | verbose = 0, ///< Verbose level 30 | debug, ///< Debug level 31 | info, ///< Info level 32 | warning, ///< Warning level 33 | error, ///< Error level 34 | fatal ///< Fatal level 35 | }; 36 | 37 | /** 38 | * @brief Defines the callback type for log data re-routing. 39 | */ 40 | using Callback = std::function; 41 | 42 | /** 43 | * @brief Get the singleton instance. 44 | * @returns Singleton instance for the class. 45 | * @examples 46 | * Logger& logger { Logger::get() }; 47 | * @examples_end 48 | */ 49 | static Logger &get(); 50 | 51 | /** 52 | * @brief Set the log level for the logger. 53 | * @param log_level New level to be used. 54 | * @examples 55 | * Logger::get().setLogLevel(Logger::LogLevel::Info); 56 | * @examples_end 57 | */ 58 | void setLogLevel(LogLevel log_level); 59 | 60 | /** 61 | * @brief Check if log level is currently enabled. 62 | * @param log_level Log level to check. 63 | * @returns True if log level is enabled. 64 | * @examples 65 | * const bool is_enabled { Logger::get().isLogLevelEnabled(Logger::LogLevel::Info) }; 66 | * @examples_end 67 | */ 68 | [[nodiscard]] bool isLogLevelEnabled(LogLevel log_level) const; 69 | 70 | /** 71 | * @brief Set custom callback for writing the logs. 72 | * @param callback New callback to be used or nullptr to reset to the default. 73 | * @examples 74 | * Logger::get().setCustomCallback([](const LogLevel level, std::string value){ 75 | * // write to file or something 76 | * }); 77 | * @examples_end 78 | */ 79 | void setCustomCallback(Callback callback); 80 | 81 | /** 82 | * @brief Write the string to the output (via callback) if the log level is enabled. 83 | * @param log_level Log level to be checked and (probably) written. 84 | * @param value A copy of the string to be written. 85 | * @examples 86 | * Logger::get().write(Logger::LogLevel::Info, "Hello World!"); 87 | * @examples_end 88 | */ 89 | void write(LogLevel log_level, std::string value); 90 | 91 | /** 92 | * @brief A deleted copy constructor for singleton pattern. 93 | * @note Public to ensure better compiler error message. 94 | */ 95 | Logger(Logger const &) = delete; 96 | 97 | /** 98 | * @brief A deleted assignment operator for singleton pattern. 99 | * @note Public to ensure better compiler error message. 100 | */ 101 | void operator=(Logger const &) = delete; 102 | 103 | private: 104 | /** 105 | * @brief A private constructor to ensure the singleton pattern. 106 | */ 107 | explicit Logger(); 108 | 109 | LogLevel m_enabled_log_level; /**< The currently enabled log level. */ 110 | Callback m_custom_callback; /**< Custom callback to pass log data to. */ 111 | }; 112 | 113 | /** 114 | * @brief A helper class for accumulating output via the stream operator and then writing it out at once. 115 | */ 116 | class LogWriter { 117 | public: 118 | /** 119 | * @brief Constructor scoped writer utility. 120 | * @param log_level Level to be used when writing out the output. 121 | */ 122 | explicit LogWriter(Logger::LogLevel log_level); 123 | 124 | /** 125 | * @brief Write out the accumulated output. 126 | */ 127 | virtual ~LogWriter(); 128 | 129 | /** 130 | * @brief Stream value to the buffer. 131 | * @param value Arbitrary value to be written to the buffer. 132 | * @returns Reference to the writer utility for chaining the operators. 133 | */ 134 | template 135 | LogWriter &operator<<(T &&value) { 136 | m_buffer << std::forward(value); 137 | return *this; 138 | } 139 | 140 | private: 141 | Logger::LogLevel m_log_level; /**< Log level to be used. */ 142 | std::ostringstream m_buffer; /**< Buffer to hold all the output. */ 143 | }; 144 | } // namespace display_device 145 | 146 | /** 147 | * @brief Helper MACRO that disables output string computation if log level is not enabled. 148 | * @examples 149 | * DD_LOG(info) << "Hello World!" << " " << 123; 150 | * DD_LOG(error) << "OH MY GAWD!"; 151 | * @examples_end 152 | */ 153 | #define DD_LOG(level) \ 154 | for (bool is_enabled {display_device::Logger::get().isLogLevelEnabled(display_device::Logger::LogLevel::level)}; is_enabled; is_enabled = false) \ 155 | display_device::LogWriter(display_device::Logger::LogLevel::level) 156 | -------------------------------------------------------------------------------- /tests/unit/windows/test_settings_manager_general.cpp: -------------------------------------------------------------------------------- 1 | // local includes 2 | #include "display_device/noop_audio_context.h" 3 | #include "display_device/noop_settings_persistence.h" 4 | #include "display_device/windows/settings_manager.h" 5 | #include "fixtures/fixtures.h" 6 | #include "fixtures/mock_audio_context.h" 7 | #include "fixtures/mock_settings_persistence.h" 8 | #include "utils/comparison.h" 9 | #include "utils/helpers.h" 10 | #include "utils/mock_win_display_device.h" 11 | 12 | namespace { 13 | // Convenience keywords for GMock 14 | using ::testing::_; 15 | using ::testing::HasSubstr; 16 | using ::testing::InSequence; 17 | using ::testing::Return; 18 | using ::testing::StrictMock; 19 | 20 | // Test fixture(s) for this file 21 | class SettingsManagerGeneralMocked: public BaseTest { 22 | public: 23 | display_device::SettingsManager &getImpl() { 24 | if (!m_impl) { 25 | m_impl = std::make_unique(m_dd_api, m_audio_context_api, std::make_unique(m_settings_persistence_api), display_device::WinWorkarounds {}); 26 | } 27 | 28 | return *m_impl; 29 | } 30 | 31 | std::shared_ptr> m_dd_api {std::make_shared>()}; 32 | std::shared_ptr> m_settings_persistence_api {std::make_shared>()}; 33 | std::shared_ptr> m_audio_context_api {std::make_shared>()}; 34 | 35 | private: 36 | std::unique_ptr m_impl; 37 | }; 38 | 39 | // Specialized TEST macro(s) for this test 40 | #define TEST_F_S_MOCKED(...) DD_MAKE_TEST(TEST_F, SettingsManagerGeneralMocked, __VA_ARGS__) 41 | } // namespace 42 | 43 | TEST_F_S_MOCKED(NullptrDisplayDeviceApiProvided) { 44 | EXPECT_THAT([]() { 45 | const display_device::SettingsManager settings_manager(nullptr, nullptr, nullptr, {}); 46 | }, 47 | ThrowsMessage(HasSubstr("Nullptr provided for WinDisplayDeviceInterface in SettingsManager!"))); 48 | } 49 | 50 | TEST_F_S_MOCKED(NoopAudioContext) { 51 | class NakedSettingsManager: public display_device::SettingsManager { 52 | public: 53 | using SettingsManager::m_audio_context_api; 54 | using SettingsManager::SettingsManager; 55 | }; 56 | 57 | const NakedSettingsManager settings_manager {m_dd_api, nullptr, std::make_unique(nullptr), {}}; 58 | EXPECT_TRUE(std::dynamic_pointer_cast(settings_manager.m_audio_context_api) != nullptr); 59 | } 60 | 61 | TEST_F_S_MOCKED(NullptrPersistentStateProvided) { 62 | EXPECT_THAT([this]() { 63 | const display_device::SettingsManager settings_manager(m_dd_api, nullptr, nullptr, {}); 64 | }, 65 | ThrowsMessage(HasSubstr("Nullptr provided for PersistentState in SettingsManager!"))); 66 | } 67 | 68 | TEST_F_S_MOCKED(EnumAvailableDevices) { 69 | const display_device::EnumeratedDeviceList test_list { 70 | {"DeviceId1", 71 | "", 72 | "FriendlyName1", 73 | std::nullopt} 74 | }; 75 | 76 | EXPECT_CALL(*m_settings_persistence_api, load()) 77 | .Times(1) 78 | .WillOnce(Return(serializeState(ut_consts::SDCS_EMPTY))); 79 | EXPECT_CALL(*m_dd_api, enumAvailableDevices()) 80 | .Times(1) 81 | .WillOnce(Return(test_list)); 82 | 83 | EXPECT_EQ(getImpl().enumAvailableDevices(), test_list); 84 | } 85 | 86 | TEST_F_S_MOCKED(GetDisplayName) { 87 | EXPECT_CALL(*m_settings_persistence_api, load()) 88 | .Times(1) 89 | .WillOnce(Return(serializeState(ut_consts::SDCS_EMPTY))); 90 | EXPECT_CALL(*m_dd_api, getDisplayName("DeviceId1")) 91 | .Times(1) 92 | .WillOnce(Return("DeviceName1")); 93 | 94 | EXPECT_EQ(getImpl().getDisplayName("DeviceId1"), "DeviceName1"); 95 | } 96 | 97 | TEST_F_S_MOCKED(ResetPersistence, NoPersistence) { 98 | EXPECT_CALL(*m_settings_persistence_api, load()) 99 | .Times(1) 100 | .WillOnce(Return(serializeState(ut_consts::SDCS_EMPTY))); 101 | 102 | EXPECT_TRUE(getImpl().resetPersistence()); 103 | } 104 | 105 | TEST_F_S_MOCKED(ResetPersistence, FailedToReset) { 106 | EXPECT_CALL(*m_settings_persistence_api, load()) 107 | .Times(1) 108 | .WillOnce(Return(serializeState(ut_consts::SDCS_FULL))); 109 | EXPECT_CALL(*m_settings_persistence_api, clear()) 110 | .Times(1) 111 | .WillOnce(Return(false)); 112 | 113 | EXPECT_FALSE(getImpl().resetPersistence()); 114 | } 115 | 116 | TEST_F_S_MOCKED(ResetPersistence, PersistenceReset, NoCapturedDevice) { 117 | EXPECT_CALL(*m_settings_persistence_api, load()) 118 | .Times(1) 119 | .WillOnce(Return(serializeState(ut_consts::SDCS_FULL))); 120 | EXPECT_CALL(*m_settings_persistence_api, clear()) 121 | .Times(1) 122 | .WillOnce(Return(true)); 123 | EXPECT_CALL(*m_audio_context_api, isCaptured()) 124 | .Times(1) 125 | .WillOnce(Return(false)) 126 | .RetiresOnSaturation(); 127 | 128 | EXPECT_TRUE(getImpl().resetPersistence()); 129 | } 130 | 131 | TEST_F_S_MOCKED(ResetPersistence, PersistenceReset, WithCapturedDevice) { 132 | EXPECT_CALL(*m_settings_persistence_api, load()) 133 | .Times(1) 134 | .WillOnce(Return(serializeState(ut_consts::SDCS_FULL))); 135 | EXPECT_CALL(*m_settings_persistence_api, clear()) 136 | .Times(1) 137 | .WillOnce(Return(true)); 138 | EXPECT_CALL(*m_audio_context_api, isCaptured()) 139 | .Times(1) 140 | .WillOnce(Return(true)); 141 | EXPECT_CALL(*m_audio_context_api, release()) 142 | .Times(1); 143 | 144 | EXPECT_TRUE(getImpl().resetPersistence()); 145 | } 146 | -------------------------------------------------------------------------------- /src/windows/include/display_device/windows/types.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file src/windows/include/display_device/windows/types.h 3 | * @brief Declarations for Windows specific types. 4 | */ 5 | #pragma once 6 | 7 | // the most stupid and smelly windows include 8 | #ifndef NOMINMAX 9 | #define NOMINMAX 10 | #endif 11 | #include 12 | 13 | // system includes 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | // local includes 20 | #include "display_device/types.h" 21 | 22 | namespace display_device { 23 | /** 24 | * @brief Type of query the OS should perform while searching for display devices. 25 | */ 26 | enum class QueryType { 27 | Active, /**< The device path must be active. */ 28 | All /**< The device path can be active or inactive. */ 29 | }; 30 | 31 | /** 32 | * @brief Contains currently available paths and associated modes. 33 | */ 34 | struct PathAndModeData { 35 | std::vector m_paths {}; /**< Available display paths. */ 36 | std::vector m_modes {}; /**< Display modes for ACTIVE displays. */ 37 | }; 38 | 39 | /** 40 | * @brief Specifies additional constraints for the validated device. 41 | */ 42 | enum class ValidatedPathType { 43 | Active, /**< The device path must be active. */ 44 | Any /**< The device path can be active or inactive. */ 45 | }; 46 | 47 | /** 48 | * @brief Contains the device path and the id for a VALID device. 49 | * @see win_utils::getDeviceInfoForValidPath for what is considered a valid device. 50 | * @see WinApiLayerInterface::getDeviceId for how we make the device id. 51 | */ 52 | struct ValidatedDeviceInfo { 53 | std::string m_device_path {}; /**< Unique device path string. */ 54 | std::string m_device_id {}; /**< A device id (made up by us) that identifies the device. */ 55 | }; 56 | 57 | /** 58 | * @brief Contains information about sources with identical adapter ids from matching paths. 59 | */ 60 | struct PathSourceIndexData { 61 | std::map m_source_id_to_path_index {}; /**< Maps source ids to its index in the path list. */ 62 | LUID m_adapter_id {}; /**< Adapter id shared by all source ids. */ 63 | std::optional m_active_source {}; /**< Currently active source id. */ 64 | }; 65 | 66 | /** 67 | * @brief Ordered map of [DEVICE_ID -> PathSourceIndexData]. 68 | * @see PathSourceIndexData 69 | */ 70 | using PathSourceIndexDataMap = std::map; 71 | 72 | /** 73 | * @brief A LIST[LIST[DEVICE_ID]] structure which represents an active topology. 74 | * 75 | * Single display: 76 | * [[DISPLAY_1]] 77 | * 2 extended displays: 78 | * [[DISPLAY_1], [DISPLAY_2]] 79 | * 2 duplicated displays: 80 | * [[DISPLAY_1, DISPLAY_2]] 81 | * Mixed displays: 82 | * [[EXTENDED_DISPLAY_1], [DUPLICATED_DISPLAY_1, DUPLICATED_DISPLAY_2], [EXTENDED_DISPLAY_2]] 83 | * 84 | * @note On Windows the order does not matter of both device ids or the inner lists. 85 | */ 86 | using ActiveTopology = std::vector>; 87 | 88 | /** 89 | * @brief Display's mode (resolution + refresh rate). 90 | */ 91 | struct DisplayMode { 92 | Resolution m_resolution {}; 93 | Rational m_refresh_rate {}; 94 | 95 | /** 96 | * @brief Comparator for strict equality. 97 | */ 98 | friend bool operator==(const DisplayMode &lhs, const DisplayMode &rhs); 99 | }; 100 | 101 | /** 102 | * @brief Ordered map of [DEVICE_ID -> DisplayMode]. 103 | */ 104 | using DeviceDisplayModeMap = std::map; 105 | 106 | /** 107 | * @brief Ordered map of [DEVICE_ID -> std::optional]. 108 | */ 109 | using HdrStateMap = std::map>; 110 | 111 | /** 112 | * @brief Arbitrary data for making and undoing changes. 113 | */ 114 | struct SingleDisplayConfigState { 115 | /** 116 | * @brief Data that represents the original system state and is used 117 | * as a base when trying to re-apply settings without reverting settings. 118 | */ 119 | struct Initial { 120 | ActiveTopology m_topology {}; 121 | std::set m_primary_devices {}; 122 | 123 | /** 124 | * @brief Comparator for strict equality. 125 | */ 126 | friend bool operator==(const Initial &lhs, const Initial &rhs); 127 | }; 128 | 129 | /** 130 | * @brief Data for tracking the modified changes. 131 | */ 132 | struct Modified { 133 | ActiveTopology m_topology {}; 134 | DeviceDisplayModeMap m_original_modes {}; 135 | HdrStateMap m_original_hdr_states {}; 136 | std::string m_original_primary_device {}; 137 | 138 | /** 139 | * @brief Check if the changed topology has any other modifications. 140 | * @return True if DisplayMode, HDR or primary device has been changed, false otherwise. 141 | * @examples 142 | * SingleDisplayConfigState state; 143 | * const no_modifications = state.hasModifications(); 144 | * 145 | * state.modified.original_primary_device = "DeviceId2"; 146 | * const has_modifications = state.hasModifications(); 147 | * @examples_end 148 | */ 149 | [[nodiscard]] bool hasModifications() const; 150 | 151 | /** 152 | * @brief Comparator for strict equality. 153 | */ 154 | friend bool operator==(const Modified &lhs, const Modified &rhs); 155 | }; 156 | 157 | Initial m_initial; 158 | Modified m_modified; 159 | 160 | /** 161 | * @brief Comparator for strict equality. 162 | */ 163 | friend bool operator==(const SingleDisplayConfigState &lhs, const SingleDisplayConfigState &rhs); 164 | }; 165 | 166 | /** 167 | * @brief Default function type used for cleanup/guard functions. 168 | */ 169 | using DdGuardFn = std::function; 170 | 171 | /** 172 | * @brief Settings for workarounds/hacks for Windows. 173 | */ 174 | struct WinWorkarounds { 175 | std::optional m_hdr_blank_delay {std::nullopt}; ///< @seealso{win_utils::blankHdrStates for more details.} 176 | 177 | /** 178 | * @brief Comparator for strict equality. 179 | */ 180 | friend bool operator==(const WinWorkarounds &lhs, const WinWorkarounds &rhs); 181 | }; 182 | } // namespace display_device 183 | -------------------------------------------------------------------------------- /src/common/types.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file src/common/types.cpp 3 | * @brief Definitions for common display device types. 4 | */ 5 | // header include 6 | #include "display_device/types.h" 7 | 8 | // system includes 9 | #include 10 | #include 11 | #include 12 | 13 | // local includes 14 | #include "display_device/logging.h" 15 | 16 | namespace { 17 | bool fuzzyCompare(const double lhs, const double rhs) { 18 | return std::abs(lhs - rhs) * 1000000000000. <= std::min(std::abs(lhs), std::abs(rhs)); 19 | } 20 | 21 | bool fuzzyCompare(const display_device::FloatingPoint &lhs, const display_device::FloatingPoint &rhs) { 22 | if (lhs.index() == rhs.index()) { 23 | if (std::holds_alternative(lhs)) { 24 | return fuzzyCompare(std::get(lhs), std::get(rhs)); 25 | } 26 | return lhs == rhs; 27 | } 28 | return false; 29 | } 30 | 31 | std::byte operator+(const std::byte lhs, const std::byte &rhs) { 32 | return std::byte {static_cast(static_cast(lhs) + static_cast(rhs))}; 33 | } 34 | 35 | // This madness should be removed once the minimum compiler version increases... 36 | std::byte logicalAnd(const std::byte &lhs, const std::byte &rhs) { 37 | return std::byte {static_cast(static_cast(lhs) & static_cast(rhs))}; 38 | } 39 | 40 | // This madness should be removed once the minimum compiler version increases... 41 | std::byte shiftLeft(const std::byte &lhs, const int rhs) { 42 | return std::byte {static_cast(static_cast(lhs) << rhs)}; 43 | } 44 | 45 | // This madness should be removed once the minimum compiler version increases... 46 | std::byte shiftRight(const std::byte &lhs, const int rhs) { 47 | return std::byte {static_cast(static_cast(lhs) >> rhs)}; 48 | } 49 | } // namespace 50 | 51 | namespace display_device { 52 | bool operator==(const Rational &lhs, const Rational &rhs) { 53 | return lhs.m_numerator == rhs.m_numerator && lhs.m_denominator == rhs.m_denominator; 54 | } 55 | 56 | bool operator==(const Point &lhs, const Point &rhs) { 57 | return lhs.m_x == rhs.m_x && lhs.m_y == rhs.m_y; 58 | } 59 | 60 | bool operator==(const Resolution &lhs, const Resolution &rhs) { 61 | return lhs.m_height == rhs.m_height && lhs.m_width == rhs.m_width; 62 | } 63 | 64 | std::optional EdidData::parse(const std::vector &data) { 65 | if (data.empty()) { 66 | return std::nullopt; 67 | } 68 | 69 | if (data.size() < 128) { 70 | DD_LOG(warning) << "EDID data size is too small: " << data.size(); 71 | return std::nullopt; 72 | } 73 | 74 | // ---- Verify fixed header 75 | static const std::vector fixed_header {std::byte {0x00}, std::byte {0xFF}, std::byte {0xFF}, std::byte {0xFF}, std::byte {0xFF}, std::byte {0xFF}, std::byte {0xFF}, std::byte {0x00}}; 76 | if (!std::equal(std::begin(fixed_header), std::end(fixed_header), std::begin(data))) { 77 | DD_LOG(warning) << "EDID data does not contain fixed header."; 78 | return std::nullopt; 79 | } 80 | 81 | // ---- Verify checksum 82 | { 83 | int sum = 0; 84 | for (std::size_t i = 0; i < 128; ++i) { 85 | sum += static_cast(data[i]); 86 | } 87 | 88 | if (sum % 256 != 0) { 89 | DD_LOG(warning) << "EDID checksum verification failed."; 90 | return std::nullopt; 91 | } 92 | } 93 | 94 | EdidData edid {}; 95 | 96 | // ---- Get manufacturer ID (ASCII code A-Z) 97 | { 98 | constexpr std::byte ascii_offset {'@'}; 99 | 100 | const auto byte_a {data[8]}; 101 | const auto byte_b {data[9]}; 102 | std::array man_id {}; 103 | 104 | man_id[0] = static_cast(ascii_offset + shiftRight(logicalAnd(byte_a, std::byte {0x7C}), 2)); 105 | man_id[1] = static_cast(ascii_offset + shiftLeft(logicalAnd(byte_a, std::byte {0x03}), 3) + shiftRight(logicalAnd(byte_b, std::byte {0xE0}), 5)); 106 | man_id[2] = static_cast(ascii_offset + logicalAnd(byte_b, std::byte {0x1F})); 107 | 108 | for (const char ch : man_id) { 109 | if (ch < 'A' || ch > 'Z') { 110 | DD_LOG(warning) << "EDID manufacturer id is out of range."; 111 | return std::nullopt; 112 | } 113 | } 114 | 115 | edid.m_manufacturer_id = {std::begin(man_id), std::end(man_id)}; 116 | } 117 | 118 | // ---- Product code (HEX representation) 119 | { 120 | std::uint16_t prod_num {0}; 121 | prod_num |= static_cast(data[10]) << 0; 122 | prod_num |= static_cast(data[11]) << 8; 123 | 124 | std::stringstream stream; 125 | stream << std::setfill('0') << std::setw(4) << std::hex << std::uppercase << prod_num; 126 | edid.m_product_code = stream.str(); 127 | } 128 | 129 | // ---- Serial number 130 | { 131 | std::uint32_t serial_num {0}; 132 | serial_num |= static_cast(data[12]) << 0; 133 | serial_num |= static_cast(data[13]) << 8; 134 | serial_num |= static_cast(data[14]) << 16; 135 | serial_num |= static_cast(data[15]) << 24; 136 | 137 | edid.m_serial_number = serial_num; 138 | } 139 | 140 | return edid; 141 | } 142 | 143 | bool operator==(const EdidData &lhs, const EdidData &rhs) { 144 | return lhs.m_manufacturer_id == rhs.m_manufacturer_id && lhs.m_product_code == rhs.m_product_code && lhs.m_serial_number == rhs.m_serial_number; 145 | } 146 | 147 | bool operator==(const EnumeratedDevice::Info &lhs, const EnumeratedDevice::Info &rhs) { 148 | return lhs.m_resolution == rhs.m_resolution && fuzzyCompare(lhs.m_resolution_scale, rhs.m_resolution_scale) && 149 | fuzzyCompare(lhs.m_refresh_rate, rhs.m_refresh_rate) && lhs.m_primary == rhs.m_primary && 150 | lhs.m_origin_point == rhs.m_origin_point && lhs.m_hdr_state == rhs.m_hdr_state; 151 | } 152 | 153 | bool operator==(const EnumeratedDevice &lhs, const EnumeratedDevice &rhs) { 154 | return lhs.m_device_id == rhs.m_device_id && lhs.m_display_name == rhs.m_display_name && lhs.m_friendly_name == rhs.m_friendly_name && lhs.m_edid == rhs.m_edid && lhs.m_info == rhs.m_info; 155 | } 156 | 157 | bool operator==(const SingleDisplayConfiguration &lhs, const SingleDisplayConfiguration &rhs) { 158 | return lhs.m_device_id == rhs.m_device_id && lhs.m_device_prep == rhs.m_device_prep && lhs.m_resolution == rhs.m_resolution && lhs.m_refresh_rate == rhs.m_refresh_rate && lhs.m_hdr_state == rhs.m_hdr_state; 159 | } 160 | } // namespace display_device 161 | -------------------------------------------------------------------------------- /tests/unit/general/test_json_converter.cpp: -------------------------------------------------------------------------------- 1 | // local includes 2 | #include "fixtures/json_converter_test.h" 3 | 4 | namespace { 5 | // Specialized TEST macro(s) for this test file 6 | #define TEST_F_S(...) DD_MAKE_TEST(TEST_F, JsonConverterTest, __VA_ARGS__) 7 | } // namespace 8 | 9 | TEST_F_S(EdidData) { 10 | display_device::EdidData item { 11 | .m_manufacturer_id = "LOL", 12 | .m_product_code = "ABCD", 13 | .m_serial_number = 777777 14 | }; 15 | 16 | executeTestCase(display_device::EdidData {}, R"({"manufacturer_id":"","product_code":"","serial_number":0})"); 17 | executeTestCase(item, R"({"manufacturer_id":"LOL","product_code":"ABCD","serial_number":777777})"); 18 | } 19 | 20 | TEST_F_S(EnumeratedDevice) { 21 | display_device::EnumeratedDevice item_1 { 22 | "ID_1", 23 | "NAME_2", 24 | "FU_NAME_3", 25 | std::nullopt, 26 | display_device::EnumeratedDevice::Info { 27 | {1920, 1080}, 28 | display_device::Rational {175, 100}, 29 | 119.9554, 30 | false, 31 | {1, 2}, 32 | display_device::HdrState::Enabled 33 | } 34 | }; 35 | display_device::EnumeratedDevice item_2 { 36 | "ID_2", 37 | "NAME_2", 38 | "FU_NAME_2", 39 | display_device::EdidData {}, 40 | display_device::EnumeratedDevice::Info { 41 | {1920, 1080}, 42 | 1.75, 43 | display_device::Rational {1199554, 10000}, 44 | true, 45 | {0, 0}, 46 | display_device::HdrState::Disabled 47 | } 48 | }; 49 | 50 | executeTestCase(display_device::EnumeratedDevice {}, R"({"device_id":"","display_name":"","edid":null,"friendly_name":"","info":null})"); 51 | executeTestCase(item_1, R"({"device_id":"ID_1","display_name":"NAME_2","edid":null,"friendly_name":"FU_NAME_3","info":{"hdr_state":"Enabled","origin_point":{"x":1,"y":2},"primary":false,"refresh_rate":{"type":"double","value":119.9554},"resolution":{"height":1080,"width":1920},"resolution_scale":{"type":"rational","value":{"denominator":100,"numerator":175}}}})"); 52 | executeTestCase(item_2, R"({"device_id":"ID_2","display_name":"NAME_2","edid":{"manufacturer_id":"","product_code":"","serial_number":0},"friendly_name":"FU_NAME_2","info":{"hdr_state":"Disabled","origin_point":{"x":0,"y":0},"primary":true,"refresh_rate":{"type":"rational","value":{"denominator":10000,"numerator":1199554}},"resolution":{"height":1080,"width":1920},"resolution_scale":{"type":"double","value":1.75}}})"); 53 | } 54 | 55 | TEST_F_S(EnumeratedDeviceList) { 56 | display_device::EnumeratedDevice item_1 { 57 | "ID_1", 58 | "NAME_2", 59 | "FU_NAME_3", 60 | std::nullopt, 61 | display_device::EnumeratedDevice::Info { 62 | {1920, 1080}, 63 | display_device::Rational {175, 100}, 64 | 119.9554, 65 | false, 66 | {1, 2}, 67 | display_device::HdrState::Enabled 68 | } 69 | }; 70 | display_device::EnumeratedDevice item_2 { 71 | "ID_2", 72 | "NAME_2", 73 | "FU_NAME_2", 74 | display_device::EdidData {}, 75 | display_device::EnumeratedDevice::Info { 76 | {1920, 1080}, 77 | 1.75, 78 | display_device::Rational {1199554, 10000}, 79 | true, 80 | {0, 0}, 81 | display_device::HdrState::Disabled 82 | } 83 | }; 84 | display_device::EnumeratedDevice item_3 {}; 85 | 86 | executeTestCase(display_device::EnumeratedDeviceList {}, R"([])"); 87 | executeTestCase(display_device::EnumeratedDeviceList {item_1, item_2, item_3}, R"([{"device_id":"ID_1","display_name":"NAME_2","edid":null,"friendly_name":"FU_NAME_3","info":{"hdr_state":"Enabled","origin_point":{"x":1,"y":2},"primary":false,"refresh_rate":{"type":"double","value":119.9554},"resolution":{"height":1080,"width":1920},"resolution_scale":{"type":"rational","value":{"denominator":100,"numerator":175}}}},)" 88 | R"({"device_id":"ID_2","display_name":"NAME_2","edid":{"manufacturer_id":"","product_code":"","serial_number":0},"friendly_name":"FU_NAME_2","info":{"hdr_state":"Disabled","origin_point":{"x":0,"y":0},"primary":true,"refresh_rate":{"type":"rational","value":{"denominator":10000,"numerator":1199554}},"resolution":{"height":1080,"width":1920},"resolution_scale":{"type":"double","value":1.75}}},)" 89 | R"({"device_id":"","display_name":"","edid":null,"friendly_name":"","info":null}])"); 90 | } 91 | 92 | TEST_F_S(SingleDisplayConfiguration) { 93 | display_device::SingleDisplayConfiguration config_1 {"ID_1", display_device::SingleDisplayConfiguration::DevicePreparation::VerifyOnly, {{156, 123}}, 85., display_device::HdrState::Enabled}; 94 | display_device::SingleDisplayConfiguration config_2 {"ID_2", display_device::SingleDisplayConfiguration::DevicePreparation::EnsureActive, std::nullopt, display_device::Rational {85, 1}, display_device::HdrState::Disabled}; 95 | display_device::SingleDisplayConfiguration config_3 {"ID_3", display_device::SingleDisplayConfiguration::DevicePreparation::EnsureOnlyDisplay, {{156, 123}}, std::nullopt, std::nullopt}; 96 | display_device::SingleDisplayConfiguration config_4 {"ID_4", display_device::SingleDisplayConfiguration::DevicePreparation::EnsurePrimary, std::nullopt, std::nullopt, std::nullopt}; 97 | 98 | executeTestCase(display_device::SingleDisplayConfiguration {}, R"({"device_id":"","device_prep":"VerifyOnly","hdr_state":null,"refresh_rate":null,"resolution":null})"); 99 | executeTestCase(config_1, R"({"device_id":"ID_1","device_prep":"VerifyOnly","hdr_state":"Enabled","refresh_rate":{"type":"double","value":85.0},"resolution":{"height":123,"width":156}})"); 100 | executeTestCase(config_2, R"({"device_id":"ID_2","device_prep":"EnsureActive","hdr_state":"Disabled","refresh_rate":{"type":"rational","value":{"denominator":1,"numerator":85}},"resolution":null})"); 101 | executeTestCase(config_3, R"({"device_id":"ID_3","device_prep":"EnsureOnlyDisplay","hdr_state":null,"refresh_rate":null,"resolution":{"height":123,"width":156}})"); 102 | executeTestCase(config_4, R"({"device_id":"ID_4","device_prep":"EnsurePrimary","hdr_state":null,"refresh_rate":null,"resolution":null})"); 103 | } 104 | 105 | TEST_F_S(StringSet) { 106 | executeTestCase(std::set {}, R"([])"); 107 | executeTestCase(std::set {"ABC", "DEF"}, R"(["ABC","DEF"])"); 108 | executeTestCase(std::set {"DEF", "ABC"}, R"(["ABC","DEF"])"); 109 | } 110 | 111 | TEST_F_S(String) { 112 | executeTestCase(std::string {}, R"("")"); 113 | executeTestCase(std::string {"ABC"}, R"("ABC")"); 114 | } 115 | 116 | TEST_F_S(Bool) { 117 | executeTestCase(true, R"(true)"); 118 | executeTestCase(false, R"(false)"); 119 | } 120 | -------------------------------------------------------------------------------- /src/common/include/display_device/detail/json_serializer_details.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file src/common/include/display_device/detail/json_serializer_details.h 3 | * @brief Declarations for private JSON serialization details. 4 | */ 5 | #pragma once 6 | 7 | #ifdef DD_JSON_DETAIL 8 | // system includes 9 | #include 10 | 11 | // Special versions of the NLOHMANN definitions to remove the "m_" prefix in string form ('cause I like it that way ;P) 12 | #define DD_JSON_TO(v1) nlohmann_json_j[#v1] = nlohmann_json_t.m_##v1; 13 | #define DD_JSON_FROM(v1) nlohmann_json_j.at(#v1).get_to(nlohmann_json_t.m_##v1); 14 | 15 | // Coverage has trouble with inlined functions when they are included in different units, 16 | // therefore the usual macro was split into declaration and definition 17 | #define DD_JSON_DECLARE_SERIALIZE_TYPE(Type) \ 18 | void to_json(nlohmann::json &nlohmann_json_j, const Type &nlohmann_json_t); \ 19 | void from_json(const nlohmann::json &nlohmann_json_j, Type &nlohmann_json_t); 20 | 21 | #define DD_JSON_DEFINE_SERIALIZE_STRUCT(Type, ...) \ 22 | void to_json(nlohmann::json &nlohmann_json_j, const Type &nlohmann_json_t) { \ 23 | NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(DD_JSON_TO, __VA_ARGS__)) \ 24 | } \ 25 | \ 26 | void from_json(const nlohmann::json &nlohmann_json_j, Type &nlohmann_json_t) { \ 27 | NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(DD_JSON_FROM, __VA_ARGS__)) \ 28 | } 29 | 30 | // Coverage has trouble with getEnumMap() function since it has a lot of "fallthrough" 31 | // branches when creating a map, therefore the macro has baked in pattern to disable branch coverage 32 | // in GCOVR 33 | #define DD_JSON_DEFINE_SERIALIZE_ENUM_GCOVR_EXCL_BR_LINE(Type, ...) \ 34 | const std::map & \ 35 | getEnumMap(const Type &) { \ 36 | static_assert(std::is_enum::value, #Type " must be an enum!"); \ 37 | static const std::map map = __VA_ARGS__; \ 38 | return map; \ 39 | } \ 40 | \ 41 | void to_json(nlohmann::json &nlohmann_json_j, const Type &nlohmann_json_t) { \ 42 | nlohmann_json_j = findInEnumMap(#Type " is missing enum mapping!", [nlohmann_json_t](const auto &pair) { \ 43 | return pair.first == nlohmann_json_t; \ 44 | })->second; \ 45 | } \ 46 | \ 47 | void from_json(const nlohmann::json &nlohmann_json_j, Type &nlohmann_json_t) { \ 48 | nlohmann_json_t = findInEnumMap(#Type " is missing enum mapping!", [&nlohmann_json_j](const auto &pair) { \ 49 | return pair.second == nlohmann_json_j; \ 50 | })->first; \ 51 | } 52 | 53 | namespace display_device { 54 | /** 55 | * @brief Holds information for serializing variants. 56 | */ 57 | namespace detail { 58 | template 59 | struct JsonTypeName; 60 | 61 | template<> 62 | struct JsonTypeName { 63 | static constexpr std::string_view m_name {"double"}; 64 | }; 65 | 66 | template<> 67 | struct JsonTypeName { 68 | static constexpr std::string_view m_name {"rational"}; 69 | }; 70 | 71 | template 72 | bool variantFromJson(const nlohmann::json &nlohmann_json_j, std::variant &value) { 73 | if (nlohmann_json_j.at("type").get() != JsonTypeName::m_name) { 74 | return false; 75 | } 76 | 77 | value = nlohmann_json_j.at("value").get(); 78 | return true; 79 | } 80 | } // namespace detail 81 | 82 | // A shared function for enums to find values in the map. Extracted here for UTs + coverage 83 | template 84 | typename std::map::const_iterator findInEnumMap(const char *error_msg, Predicate predicate) { 85 | const auto &map {getEnumMap(T {})}; 86 | auto it {std::find_if(std::begin(map), std::end(map), predicate)}; 87 | if (it == std::end(map)) { // GCOVR_EXCL_BR_LINE for fallthrough branch 88 | throw std::runtime_error(error_msg); // GCOVR_EXCL_BR_LINE for fallthrough branch 89 | } 90 | return it; 91 | } 92 | } // namespace display_device 93 | 94 | namespace nlohmann { 95 | // Specialization for optional types until they actually implement it. 96 | template 97 | struct adl_serializer> { 98 | static void to_json(json &nlohmann_json_j, const std::optional &nlohmann_json_t) { 99 | if (nlohmann_json_t == std::nullopt) { 100 | nlohmann_json_j = nullptr; 101 | } else { 102 | nlohmann_json_j = *nlohmann_json_t; 103 | } 104 | } 105 | 106 | static void from_json(const json &nlohmann_json_j, std::optional &nlohmann_json_t) { 107 | if (nlohmann_json_j.is_null()) { 108 | nlohmann_json_t = std::nullopt; 109 | } else { 110 | nlohmann_json_t = nlohmann_json_j.get(); 111 | } 112 | } 113 | }; 114 | 115 | // Specialization for variant type. 116 | // See https://github.com/nlohmann/json/issues/1261#issuecomment-2048770747 117 | template 118 | struct adl_serializer> { 119 | static void to_json(json &nlohmann_json_j, const std::variant &nlohmann_json_t) { 120 | std::visit( 121 | [&nlohmann_json_j](const T &value) { 122 | nlohmann_json_j["type"] = display_device::detail::JsonTypeName>::m_name; 123 | nlohmann_json_j["value"] = value; 124 | }, 125 | nlohmann_json_t 126 | ); 127 | } 128 | 129 | static void from_json(const json &nlohmann_json_j, std::variant &nlohmann_json_t) { 130 | // Call variant_from_json for all types, only one will succeed 131 | const bool found {(display_device::detail::variantFromJson(nlohmann_json_j, nlohmann_json_t) || ...)}; 132 | if (!found) { 133 | const std::string error {"Could not parse variant from type " + nlohmann_json_j.at("type").get() + "!"}; 134 | throw std::runtime_error(error); 135 | } 136 | } 137 | }; 138 | 139 | // Specialization for chrono duration. 140 | template 141 | struct adl_serializer> { 142 | using NanoRep = decltype(std::chrono::nanoseconds {}.count()); 143 | static_assert(std::numeric_limits::max() <= std::numeric_limits::max(), "Duration support above nanoseconds have not been tested/verified yet!"); 144 | 145 | static void to_json(json &nlohmann_json_j, const std::chrono::duration &nlohmann_json_t) { 146 | nlohmann_json_j = nlohmann_json_t.count(); 147 | } 148 | 149 | static void from_json(const json &nlohmann_json_j, std::chrono::duration &nlohmann_json_t) { 150 | nlohmann_json_t = std::chrono::duration {nlohmann_json_j.get()}; 151 | } 152 | }; 153 | } // namespace nlohmann 154 | #endif 155 | -------------------------------------------------------------------------------- /tests/unit/windows/test_persistent_state.cpp: -------------------------------------------------------------------------------- 1 | // local includes 2 | #include "display_device/noop_settings_persistence.h" 3 | #include "display_device/windows/settings_manager.h" 4 | #include "fixtures/fixtures.h" 5 | #include "fixtures/mock_settings_persistence.h" 6 | #include "utils/comparison.h" 7 | #include "utils/helpers.h" 8 | #include "utils/mock_win_display_device.h" 9 | 10 | namespace { 11 | // Convenience keywords for GMock 12 | using ::testing::HasSubstr; 13 | using ::testing::Return; 14 | using ::testing::StrictMock; 15 | 16 | // Test fixture(s) for this file 17 | class PersistentStateMocked: public BaseTest { 18 | public: 19 | display_device::PersistentState &getImpl(bool throw_on_load_error = false) { 20 | if (!m_impl) { 21 | m_impl = std::make_unique(m_settings_persistence_api, throw_on_load_error); 22 | } 23 | 24 | return *m_impl; 25 | } 26 | 27 | std::shared_ptr> m_settings_persistence_api {std::make_shared>()}; 28 | 29 | private: 30 | std::unique_ptr m_impl; 31 | }; 32 | 33 | // Specialized TEST macro(s) for this test 34 | #define TEST_F_S_MOCKED(...) DD_MAKE_TEST(TEST_F, PersistentStateMocked, __VA_ARGS__) 35 | } // namespace 36 | 37 | TEST_F_S_MOCKED(NoopSettingsPersistence) { 38 | class NakedPersistentState: public display_device::PersistentState { 39 | public: 40 | using PersistentState::m_settings_persistence_api; 41 | using PersistentState::PersistentState; 42 | }; 43 | 44 | const NakedPersistentState persistent_state {nullptr}; 45 | EXPECT_TRUE(std::dynamic_pointer_cast(persistent_state.m_settings_persistence_api) != nullptr); 46 | } 47 | 48 | TEST_F_S_MOCKED(FailedToLoadPersitence) { 49 | EXPECT_CALL(*m_settings_persistence_api, load()) 50 | .Times(1) 51 | .WillOnce(Return(serializeState(ut_consts::SDCS_NULL))); 52 | 53 | EXPECT_THAT([this]() { 54 | getImpl(true); 55 | }, 56 | ThrowsMessage(HasSubstr("Failed to load persistent settings!"))); 57 | } 58 | 59 | TEST_F_S_MOCKED(FailedToLoadPersitence, ThrowIsSuppressed) { 60 | EXPECT_CALL(*m_settings_persistence_api, load()) 61 | .Times(1) 62 | .WillOnce(Return(serializeState(ut_consts::SDCS_NULL))); 63 | 64 | EXPECT_EQ(getImpl(false).getState(), std::nullopt); 65 | } 66 | 67 | TEST_F_S_MOCKED(InvalidPersitenceData) { 68 | const std::string data_string {"SOMETHING"}; 69 | const std::vector data {std::begin(data_string), std::end(data_string)}; 70 | 71 | EXPECT_CALL(*m_settings_persistence_api, load()) 72 | .Times(1) 73 | .WillOnce(Return(data)); 74 | 75 | EXPECT_THAT([this]() { 76 | getImpl(true); 77 | }, 78 | ThrowsMessage(HasSubstr("Failed to parse persistent settings! Error:\n" 79 | "[json.exception.parse_error.101] parse error at line 1, column 1: syntax error while parsing value - invalid literal; last read: 'S'"))); 80 | } 81 | 82 | TEST_F_S_MOCKED(InvalidPersitenceData, ThrowIsSuppressed) { 83 | const std::string data_string {"SOMETHING"}; 84 | const std::vector data {std::begin(data_string), std::end(data_string)}; 85 | 86 | EXPECT_CALL(*m_settings_persistence_api, load()) 87 | .Times(1) 88 | .WillOnce(Return(data)); 89 | 90 | EXPECT_EQ(getImpl(false).getState(), std::nullopt); 91 | } 92 | 93 | TEST_F_S_MOCKED(NothingIsThrownOnSuccess) { 94 | EXPECT_CALL(*m_settings_persistence_api, load()) 95 | .Times(1) 96 | .WillOnce(Return(serializeState(ut_consts::SDCS_FULL))); 97 | 98 | EXPECT_EQ(getImpl(true).getState(), ut_consts::SDCS_FULL); 99 | } 100 | 101 | TEST_F_S_MOCKED(FailedToPersistState, ClearFailed) { 102 | EXPECT_CALL(*m_settings_persistence_api, load()) 103 | .Times(1) 104 | .WillOnce(Return(serializeState(ut_consts::SDCS_FULL))); 105 | EXPECT_CALL(*m_settings_persistence_api, clear()) 106 | .Times(1) 107 | .WillOnce(Return(false)); 108 | 109 | EXPECT_EQ(getImpl().getState(), ut_consts::SDCS_FULL); 110 | EXPECT_FALSE(getImpl().persistState(ut_consts::SDCS_NULL)); 111 | EXPECT_EQ(getImpl().getState(), ut_consts::SDCS_FULL); 112 | } 113 | 114 | TEST_F_S_MOCKED(FailedToPersistState, BadJsonEncoding) { 115 | display_device::SingleDisplayConfigState invalid_state; 116 | invalid_state.m_modified.m_original_primary_device = "InvalidDeviceName\xC2"; 117 | 118 | EXPECT_CALL(*m_settings_persistence_api, load()) 119 | .Times(1) 120 | .WillOnce(Return(serializeState(ut_consts::SDCS_NO_MODIFICATIONS))); 121 | 122 | EXPECT_EQ(getImpl().getState(), ut_consts::SDCS_NO_MODIFICATIONS); 123 | EXPECT_FALSE(getImpl().persistState(invalid_state)); 124 | EXPECT_EQ(getImpl().getState(), ut_consts::SDCS_NO_MODIFICATIONS); 125 | } 126 | 127 | TEST_F_S_MOCKED(FailedToPersistState, StoreFailed) { 128 | EXPECT_CALL(*m_settings_persistence_api, load()) 129 | .Times(1) 130 | .WillOnce(Return(serializeState(ut_consts::SDCS_NO_MODIFICATIONS))); 131 | EXPECT_CALL(*m_settings_persistence_api, store(*serializeState(ut_consts::SDCS_FULL))) 132 | .Times(1) 133 | .WillOnce(Return(false)); 134 | 135 | EXPECT_EQ(getImpl().getState(), ut_consts::SDCS_NO_MODIFICATIONS); 136 | EXPECT_FALSE(getImpl().persistState(ut_consts::SDCS_FULL)); 137 | EXPECT_EQ(getImpl().getState(), ut_consts::SDCS_NO_MODIFICATIONS); 138 | } 139 | 140 | TEST_F_S_MOCKED(ClearState) { 141 | EXPECT_CALL(*m_settings_persistence_api, load()) 142 | .Times(1) 143 | .WillOnce(Return(serializeState(ut_consts::SDCS_FULL))); 144 | EXPECT_CALL(*m_settings_persistence_api, clear()) 145 | .Times(1) 146 | .WillOnce(Return(true)); 147 | 148 | EXPECT_EQ(getImpl().getState(), ut_consts::SDCS_FULL); 149 | EXPECT_TRUE(getImpl().persistState(ut_consts::SDCS_NULL)); 150 | EXPECT_EQ(getImpl().getState(), ut_consts::SDCS_NULL); 151 | } 152 | 153 | TEST_F_S_MOCKED(StoreState) { 154 | EXPECT_CALL(*m_settings_persistence_api, load()) 155 | .Times(1) 156 | .WillOnce(Return(serializeState(ut_consts::SDCS_NO_MODIFICATIONS))); 157 | EXPECT_CALL(*m_settings_persistence_api, store(*serializeState(ut_consts::SDCS_FULL))) 158 | .Times(1) 159 | .WillOnce(Return(true)); 160 | 161 | EXPECT_EQ(getImpl().getState(), ut_consts::SDCS_NO_MODIFICATIONS); 162 | EXPECT_TRUE(getImpl().persistState(ut_consts::SDCS_FULL)); 163 | EXPECT_EQ(getImpl().getState(), ut_consts::SDCS_FULL); 164 | } 165 | 166 | TEST_F_S_MOCKED(PersistStateSkippedDueToEqValues) { 167 | EXPECT_CALL(*m_settings_persistence_api, load()) 168 | .Times(1) 169 | .WillOnce(Return(serializeState(ut_consts::SDCS_FULL))); 170 | 171 | EXPECT_EQ(getImpl().getState(), ut_consts::SDCS_FULL); 172 | EXPECT_TRUE(getImpl().persistState(ut_consts::SDCS_FULL)); 173 | EXPECT_EQ(getImpl().getState(), ut_consts::SDCS_FULL); 174 | } 175 | -------------------------------------------------------------------------------- /src/windows/include/display_device/windows/settings_manager.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file src/windows/include/display_device/windows/settings_manager.h 3 | * @brief Declarations for the SettingsManager. 4 | */ 5 | #pragma once 6 | 7 | // system includes 8 | #include 9 | 10 | // local includes 11 | #include "display_device/audio_context_interface.h" 12 | #include "display_device/settings_manager_interface.h" 13 | #include "display_device/windows/win_display_device_interface.h" 14 | #include "persistent_state.h" 15 | 16 | namespace display_device { 17 | /** 18 | * @brief Default implementation for the SettingsManagerInterface. 19 | */ 20 | class SettingsManager: public SettingsManagerInterface { 21 | public: 22 | /** 23 | * Default constructor for the class. 24 | * @param dd_api A pointer to the Windows Display Device interface. Will throw on nullptr! 25 | * @param audio_context_api [Optional] A pointer to the Audio Context interface. 26 | * @param persistent_state A pointer to a class for managing persistence. 27 | * @param workarounds Workaround settings for the APIs. 28 | */ 29 | explicit SettingsManager( 30 | std::shared_ptr dd_api, 31 | std::shared_ptr audio_context_api, 32 | std::unique_ptr persistent_state, 33 | WinWorkarounds workarounds 34 | ); 35 | 36 | /** For details @see SettingsManagerInterface::enumAvailableDevices */ 37 | [[nodiscard]] EnumeratedDeviceList enumAvailableDevices() const override; 38 | 39 | /** For details @see SettingsManagerInterface::getDisplayName */ 40 | [[nodiscard]] std::string getDisplayName(const std::string &device_id) const override; 41 | 42 | /** For details @see SettingsManagerInterface::applySettings */ 43 | [[nodiscard]] ApplyResult applySettings(const SingleDisplayConfiguration &config) override; 44 | 45 | /** For details @see SettingsManagerInterface::revertSettings */ 46 | [[nodiscard]] RevertResult revertSettings() override; 47 | 48 | /** For details @see SettingsManagerInterface::resetPersistence */ 49 | [[nodiscard]] bool resetPersistence() override; 50 | 51 | protected: 52 | /** 53 | * @brief Preps the topology so that the further settings could be applied. 54 | * @param config Configuration to be used for preparing topology. 55 | * @param topology_before_changes The current topology before any changes. 56 | * @param release_context Specifies whether the audio context should be released at the very end IF everything else has succeeded. 57 | * @param system_settings_touched Inticates whether a "write" operation could have been performed on the OS. 58 | * @return A tuple of (new_state that is to be updated/persisted, device_to_configure, additional_devices_to_configure). 59 | */ 60 | [[nodiscard]] std::optional>> prepareTopology(const SingleDisplayConfiguration &config, const ActiveTopology &topology_before_changes, bool &release_context, bool &system_settings_touched); 61 | 62 | /** 63 | * @brief Changes or restores the primary device based on the cached state, new state and configuration. 64 | * @param config Configuration to be used for preparing primary device. 65 | * @param device_to_configure The main device to be used for preparation. 66 | * @param guard_fn Reference to the guard function which will be set to restore original state (if needed) in case something else fails down the line. 67 | * @param new_state Reference to the new state which is to be updated accordingly. 68 | * @param system_settings_touched Inticates whether a "write" operation could have been performed on the OS. 69 | * @return True if no errors have occured, false otherwise. 70 | */ 71 | [[nodiscard]] bool preparePrimaryDevice(const SingleDisplayConfiguration &config, const std::string &device_to_configure, DdGuardFn &guard_fn, SingleDisplayConfigState &new_state, bool &system_settings_touched); 72 | 73 | /** 74 | * @brief Changes or restores the display modes based on the cached state, new state and configuration. 75 | * @param config Configuration to be used for preparing display modes. 76 | * @param device_to_configure The main device to be used for preparation. 77 | * @param additional_devices_to_configure Additional devices that should be configured. 78 | * @param guard_fn Reference to the guard function which will be set to restore original state (if needed) in case something else fails down the line. 79 | * @param new_state Reference to the new state which is to be updated accordingly. 80 | * @param system_settings_touched Inticates whether a "write" operation could have been performed on the OS. 81 | * @return True if no errors have occured, false otherwise. 82 | */ 83 | [[nodiscard]] bool prepareDisplayModes(const SingleDisplayConfiguration &config, const std::string &device_to_configure, const std::set &additional_devices_to_configure, DdGuardFn &guard_fn, SingleDisplayConfigState &new_state, bool &system_settings_touched); 84 | 85 | /** 86 | * @brief Changes or restores the HDR states based on the cached state, new state and configuration. 87 | * @param config Configuration to be used for preparing HDR states. 88 | * @param device_to_configure The main device to be used for preparation. 89 | * @param additional_devices_to_configure Additional devices that should be configured. 90 | * @param guard_fn Reference to the guard function which will be set to restore original state (if needed) in case something else fails down the line. 91 | * @param new_state Reference to the new state which is to be updated accordingly. 92 | * @param system_settings_touched Inticates whether a "write" operation could have been performed on the OS. 93 | * @return True if no errors have occured, false otherwise. 94 | */ 95 | [[nodiscard]] bool prepareHdrStates(const SingleDisplayConfiguration &config, const std::string &device_to_configure, const std::set &additional_devices_to_configure, DdGuardFn &guard_fn, SingleDisplayConfigState &new_state, bool &system_settings_touched); 96 | 97 | /** 98 | * @brief Try to revert the modified settings. 99 | * @param current_topology Topology before this method is called. 100 | * @param system_settings_touched Indicates whether a "write" operation could have been performed on the OS. 101 | * @param switched_topology [Optional] Indicates whether the current topology was switched to revert settings. 102 | * @returns Result enum indicating success or failure. 103 | * @warning The method assumes that the caller will ensure restoring the topology 104 | * in case of a failure! 105 | */ 106 | [[nodiscard]] RevertResult revertModifiedSettings(const ActiveTopology ¤t_topology, bool &system_settings_touched, bool *switched_topology = nullptr); 107 | 108 | std::shared_ptr m_dd_api; 109 | std::shared_ptr m_audio_context_api; 110 | std::unique_ptr m_persistence_state; 111 | WinWorkarounds m_workarounds; 112 | }; 113 | } // namespace display_device 114 | -------------------------------------------------------------------------------- /tests/unit/general/test_comparison.cpp: -------------------------------------------------------------------------------- 1 | // local includes 2 | #include "display_device/types.h" 3 | #include "fixtures/fixtures.h" 4 | 5 | namespace { 6 | // Specialized TEST macro(s) for this test file 7 | #define TEST_S(...) DD_MAKE_TEST(TEST, TypeComparison, __VA_ARGS__) 8 | } // namespace 9 | 10 | TEST_S(Point) { 11 | EXPECT_EQ(display_device::Point({1, 1}), display_device::Point({1, 1})); 12 | EXPECT_NE(display_device::Point({1, 1}), display_device::Point({0, 1})); 13 | EXPECT_NE(display_device::Point({1, 1}), display_device::Point({1, 0})); 14 | } 15 | 16 | TEST_S(Rational) { 17 | EXPECT_EQ(display_device::Rational({1, 1}), display_device::Rational({1, 1})); 18 | EXPECT_NE(display_device::Rational({1, 1}), display_device::Rational({0, 1})); 19 | EXPECT_NE(display_device::Rational({1, 1}), display_device::Rational({1, 0})); 20 | } 21 | 22 | TEST_S(Resolution) { 23 | EXPECT_EQ(display_device::Resolution({1, 1}), display_device::Resolution({1, 1})); 24 | EXPECT_NE(display_device::Resolution({1, 1}), display_device::Resolution({0, 1})); 25 | EXPECT_NE(display_device::Resolution({1, 1}), display_device::Resolution({1, 0})); 26 | } 27 | 28 | TEST_S(EdidData) { 29 | EXPECT_EQ(display_device::EdidData({"LOL", "1337", 1234}), display_device::EdidData({"LOL", "1337", 1234})); 30 | EXPECT_NE(display_device::EdidData({"LOL", "1337", 1234}), display_device::EdidData({"MEH", "1337", 1234})); 31 | EXPECT_NE(display_device::EdidData({"LOL", "1337", 1234}), display_device::EdidData({"LOL", "1338", 1234})); 32 | EXPECT_NE(display_device::EdidData({"LOL", "1337", 1234}), display_device::EdidData({"LOL", "1337", 1235})); 33 | } 34 | 35 | TEST_S(EnumeratedDevice, Info) { 36 | using Rat = display_device::Rational; 37 | EXPECT_EQ(display_device::EnumeratedDevice::Info({{1, 1}, 1., 1., true, {1, 1}, std::nullopt}), display_device::EnumeratedDevice::Info({{1, 1}, 1., 1., true, {1, 1}, std::nullopt})); 38 | EXPECT_EQ(display_device::EnumeratedDevice::Info({{1, 1}, Rat {1, 1}, Rat {1, 1}, true, {1, 1}, std::nullopt}), display_device::EnumeratedDevice::Info({{1, 1}, Rat {1, 1}, Rat {1, 1}, true, {1, 1}, std::nullopt})); 39 | EXPECT_EQ(display_device::EnumeratedDevice::Info({{1, 1}, 1., Rat {1, 1}, true, {1, 1}, std::nullopt}), display_device::EnumeratedDevice::Info({{1, 1}, 1., Rat {1, 1}, true, {1, 1}, std::nullopt})); 40 | EXPECT_EQ(display_device::EnumeratedDevice::Info({{1, 1}, Rat {1, 1}, 1., true, {1, 1}, std::nullopt}), display_device::EnumeratedDevice::Info({{1, 1}, Rat {1, 1}, 1., true, {1, 1}, std::nullopt})); 41 | EXPECT_NE(display_device::EnumeratedDevice::Info({{1, 1}, 1., Rat {1, 1}, true, {1, 1}, std::nullopt}), display_device::EnumeratedDevice::Info({{1, 1}, Rat {1, 1}, Rat {1, 1}, true, {1, 1}, std::nullopt})); 42 | EXPECT_NE(display_device::EnumeratedDevice::Info({{1, 1}, Rat {1, 1}, 1., true, {1, 1}, std::nullopt}), display_device::EnumeratedDevice::Info({{1, 1}, Rat {1, 1}, Rat {1, 1}, true, {1, 1}, std::nullopt})); 43 | EXPECT_NE(display_device::EnumeratedDevice::Info({{1, 1}, 1., 1., true, {1, 1}, std::nullopt}), display_device::EnumeratedDevice::Info({{1, 0}, 1., 1., true, {1, 1}, std::nullopt})); 44 | EXPECT_NE(display_device::EnumeratedDevice::Info({{1, 1}, 1., 1., true, {1, 1}, std::nullopt}), display_device::EnumeratedDevice::Info({{1, 1}, 1.1, 1., true, {1, 1}, std::nullopt})); 45 | EXPECT_NE(display_device::EnumeratedDevice::Info({{1, 1}, 1., 1., true, {1, 1}, std::nullopt}), display_device::EnumeratedDevice::Info({{1, 1}, 1., 1.1, true, {1, 1}, std::nullopt})); 46 | EXPECT_NE(display_device::EnumeratedDevice::Info({{1, 1}, 1., 1., true, {1, 1}, std::nullopt}), display_device::EnumeratedDevice::Info({{1, 1}, 1., 1., false, {1, 1}, std::nullopt})); 47 | EXPECT_NE(display_device::EnumeratedDevice::Info({{1, 1}, 1., 1., true, {1, 1}, std::nullopt}), display_device::EnumeratedDevice::Info({{1, 1}, 1., 1., true, {1, 0}, std::nullopt})); 48 | EXPECT_NE(display_device::EnumeratedDevice::Info({{1, 1}, 1., 1., true, {1, 1}, std::nullopt}), display_device::EnumeratedDevice::Info({{1, 1}, 1., 1., true, {1, 1}, display_device::HdrState::Disabled})); 49 | } 50 | 51 | TEST_S(EnumeratedDevice) { 52 | EXPECT_EQ(display_device::EnumeratedDevice({"1", "1", "1", display_device::EdidData {}, display_device::EnumeratedDevice::Info {}}), display_device::EnumeratedDevice({"1", "1", "1", display_device::EdidData {}, display_device::EnumeratedDevice::Info {}})); 53 | EXPECT_NE(display_device::EnumeratedDevice({"1", "1", "1", display_device::EdidData {}, display_device::EnumeratedDevice::Info {}}), display_device::EnumeratedDevice({"0", "1", "1", display_device::EdidData {}, display_device::EnumeratedDevice::Info {}})); 54 | EXPECT_NE(display_device::EnumeratedDevice({"1", "1", "1", display_device::EdidData {}, display_device::EnumeratedDevice::Info {}}), display_device::EnumeratedDevice({"1", "0", "1", display_device::EdidData {}, display_device::EnumeratedDevice::Info {}})); 55 | EXPECT_NE(display_device::EnumeratedDevice({"1", "1", "1", display_device::EdidData {}, display_device::EnumeratedDevice::Info {}}), display_device::EnumeratedDevice({"1", "1", "0", display_device::EdidData {}, display_device::EnumeratedDevice::Info {}})); 56 | EXPECT_NE(display_device::EnumeratedDevice({"1", "1", "1", display_device::EdidData {}, display_device::EnumeratedDevice::Info {}}), display_device::EnumeratedDevice({"1", "1", "1", std::nullopt, display_device::EnumeratedDevice::Info {}})); 57 | EXPECT_NE(display_device::EnumeratedDevice({"1", "1", "1", display_device::EdidData {}, display_device::EnumeratedDevice::Info {}}), display_device::EnumeratedDevice({"1", "1", "1", display_device::EdidData {}, std::nullopt})); 58 | } 59 | 60 | TEST_S(SingleDisplayConfiguration) { 61 | using DevicePrep = display_device::SingleDisplayConfiguration::DevicePreparation; 62 | using Rat = display_device::Rational; 63 | EXPECT_EQ(display_device::SingleDisplayConfiguration({"1", DevicePrep::EnsureActive, {{1, 1}}, 1., display_device::HdrState::Disabled}), display_device::SingleDisplayConfiguration({"1", DevicePrep::EnsureActive, {{1, 1}}, 1., display_device::HdrState::Disabled})); 64 | EXPECT_EQ(display_device::SingleDisplayConfiguration({"1", DevicePrep::EnsureActive, {{1, 1}}, Rat {1, 1}, display_device::HdrState::Disabled}), display_device::SingleDisplayConfiguration({"1", DevicePrep::EnsureActive, {{1, 1}}, Rat {1, 1}, display_device::HdrState::Disabled})); 65 | EXPECT_NE(display_device::SingleDisplayConfiguration({"1", DevicePrep::EnsureActive, {{1, 1}}, 1., display_device::HdrState::Disabled}), display_device::SingleDisplayConfiguration({"1", DevicePrep::EnsureActive, {{1, 1}}, Rat {1, 1}, display_device::HdrState::Disabled})); 66 | EXPECT_NE(display_device::SingleDisplayConfiguration({"1", DevicePrep::EnsureActive, {{1, 1}}, 1., display_device::HdrState::Disabled}), display_device::SingleDisplayConfiguration({"0", DevicePrep::EnsureActive, {{1, 1}}, 1., display_device::HdrState::Disabled})); 67 | EXPECT_NE(display_device::SingleDisplayConfiguration({"1", DevicePrep::EnsureActive, {{1, 1}}, 1., display_device::HdrState::Disabled}), display_device::SingleDisplayConfiguration({"1", DevicePrep::EnsurePrimary, {{1, 1}}, 1., display_device::HdrState::Disabled})); 68 | EXPECT_NE(display_device::SingleDisplayConfiguration({"1", DevicePrep::EnsureActive, {{1, 1}}, 1., display_device::HdrState::Disabled}), display_device::SingleDisplayConfiguration({"1", DevicePrep::EnsureActive, {{1, 0}}, 1., display_device::HdrState::Disabled})); 69 | EXPECT_NE(display_device::SingleDisplayConfiguration({"1", DevicePrep::EnsureActive, {{1, 1}}, 1., display_device::HdrState::Disabled}), display_device::SingleDisplayConfiguration({"1", DevicePrep::EnsureActive, {{1, 1}}, 1.1, display_device::HdrState::Disabled})); 70 | EXPECT_NE(display_device::SingleDisplayConfiguration({"1", DevicePrep::EnsureActive, {{1, 1}}, 1., display_device::HdrState::Disabled}), display_device::SingleDisplayConfiguration({"1", DevicePrep::EnsureActive, {{1, 1}}, 1., display_device::HdrState::Enabled})); 71 | } 72 | -------------------------------------------------------------------------------- /tests/unit/general/test_logging.cpp: -------------------------------------------------------------------------------- 1 | // local includes 2 | #include "display_device/logging.h" 3 | #include "fixtures/fixtures.h" 4 | 5 | namespace { 6 | // Specialized TEST macro(s) for this test file 7 | #define TEST_S(...) DD_MAKE_TEST(TEST, LoggingTest, __VA_ARGS__) 8 | } // namespace 9 | 10 | TEST_S(LogLevelVerbose) { 11 | using level = display_device::Logger::LogLevel; 12 | auto &logger {display_device::Logger::get()}; 13 | 14 | logger.setLogLevel(level::verbose); 15 | 16 | EXPECT_EQ(logger.isLogLevelEnabled(level::verbose), true); 17 | EXPECT_EQ(logger.isLogLevelEnabled(level::debug), true); 18 | EXPECT_EQ(logger.isLogLevelEnabled(level::info), true); 19 | EXPECT_EQ(logger.isLogLevelEnabled(level::warning), true); 20 | EXPECT_EQ(logger.isLogLevelEnabled(level::error), true); 21 | EXPECT_EQ(logger.isLogLevelEnabled(level::fatal), true); 22 | } 23 | 24 | TEST_S(LogLevelDebug) { 25 | using level = display_device::Logger::LogLevel; 26 | auto &logger {display_device::Logger::get()}; 27 | 28 | logger.setLogLevel(level::debug); 29 | 30 | EXPECT_EQ(logger.isLogLevelEnabled(level::verbose), false); 31 | EXPECT_EQ(logger.isLogLevelEnabled(level::debug), true); 32 | EXPECT_EQ(logger.isLogLevelEnabled(level::info), true); 33 | EXPECT_EQ(logger.isLogLevelEnabled(level::warning), true); 34 | EXPECT_EQ(logger.isLogLevelEnabled(level::error), true); 35 | EXPECT_EQ(logger.isLogLevelEnabled(level::fatal), true); 36 | } 37 | 38 | TEST_S(LogLevelInfo) { 39 | using level = display_device::Logger::LogLevel; 40 | auto &logger {display_device::Logger::get()}; 41 | 42 | logger.setLogLevel(level::info); 43 | 44 | EXPECT_EQ(logger.isLogLevelEnabled(level::verbose), false); 45 | EXPECT_EQ(logger.isLogLevelEnabled(level::debug), false); 46 | EXPECT_EQ(logger.isLogLevelEnabled(level::info), true); 47 | EXPECT_EQ(logger.isLogLevelEnabled(level::warning), true); 48 | EXPECT_EQ(logger.isLogLevelEnabled(level::error), true); 49 | EXPECT_EQ(logger.isLogLevelEnabled(level::fatal), true); 50 | } 51 | 52 | TEST_S(LogLevelWarning) { 53 | using level = display_device::Logger::LogLevel; 54 | auto &logger {display_device::Logger::get()}; 55 | 56 | logger.setLogLevel(level::warning); 57 | 58 | EXPECT_EQ(logger.isLogLevelEnabled(level::verbose), false); 59 | EXPECT_EQ(logger.isLogLevelEnabled(level::debug), false); 60 | EXPECT_EQ(logger.isLogLevelEnabled(level::info), false); 61 | EXPECT_EQ(logger.isLogLevelEnabled(level::warning), true); 62 | EXPECT_EQ(logger.isLogLevelEnabled(level::error), true); 63 | EXPECT_EQ(logger.isLogLevelEnabled(level::fatal), true); 64 | } 65 | 66 | TEST_S(LogLevelError) { 67 | using level = display_device::Logger::LogLevel; 68 | auto &logger {display_device::Logger::get()}; 69 | 70 | logger.setLogLevel(level::error); 71 | 72 | EXPECT_EQ(logger.isLogLevelEnabled(level::verbose), false); 73 | EXPECT_EQ(logger.isLogLevelEnabled(level::debug), false); 74 | EXPECT_EQ(logger.isLogLevelEnabled(level::info), false); 75 | EXPECT_EQ(logger.isLogLevelEnabled(level::warning), false); 76 | EXPECT_EQ(logger.isLogLevelEnabled(level::error), true); 77 | EXPECT_EQ(logger.isLogLevelEnabled(level::fatal), true); 78 | } 79 | 80 | TEST_S(LogLevelFatal) { 81 | using level = display_device::Logger::LogLevel; 82 | auto &logger {display_device::Logger::get()}; 83 | 84 | logger.setLogLevel(level::fatal); 85 | 86 | EXPECT_EQ(logger.isLogLevelEnabled(level::verbose), false); 87 | EXPECT_EQ(logger.isLogLevelEnabled(level::debug), false); 88 | EXPECT_EQ(logger.isLogLevelEnabled(level::info), false); 89 | EXPECT_EQ(logger.isLogLevelEnabled(level::warning), false); 90 | EXPECT_EQ(logger.isLogLevelEnabled(level::error), false); 91 | EXPECT_EQ(logger.isLogLevelEnabled(level::fatal), true); 92 | } 93 | 94 | TEST_S(DefaultLogger) { 95 | using level = display_device::Logger::LogLevel; 96 | auto &logger {display_device::Logger::get()}; 97 | 98 | const auto write_and_get_cout {[this, &logger](level level, std::string value) -> std::string { 99 | m_cout_buffer.str(std::string {}); // reset the buffer 100 | logger.write(level, std::move(value)); 101 | return m_cout_buffer.str(); 102 | }}; 103 | 104 | logger.setLogLevel(level::verbose); 105 | // clang-format off 106 | EXPECT_TRUE(testRegex(write_and_get_cout(level::verbose, "Hello World!"), R"(\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d{3}\] VERBOSE: Hello World!\n)")); 107 | EXPECT_TRUE(testRegex(write_and_get_cout(level::debug, "Hello World!"), R"(\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d{3}\] DEBUG: Hello World!\n)")); 108 | EXPECT_TRUE(testRegex(write_and_get_cout(level::info, "Hello World!"), R"(\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d{3}\] INFO: Hello World!\n)")); 109 | EXPECT_TRUE(testRegex(write_and_get_cout(level::warning, "Hello World!"), R"(\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d{3}\] WARNING: Hello World!\n)")); 110 | EXPECT_TRUE(testRegex(write_and_get_cout(level::error, "Hello World!"), R"(\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d{3}\] ERROR: Hello World!\n)")); 111 | EXPECT_TRUE(testRegex(write_and_get_cout(level::fatal, "Hello World!"), R"(\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d{3}\] FATAL: Hello World!\n)")); 112 | // clang-format on 113 | } 114 | 115 | TEST_S(CustomCallback) { 116 | using level = display_device::Logger::LogLevel; 117 | using level_t = std::underlying_type_t; 118 | auto &logger {display_device::Logger::get()}; 119 | 120 | std::string output; 121 | logger.setLogLevel(level::verbose); 122 | logger.setCustomCallback([&output](const level level, const std::string &value) { 123 | output = std::to_string(static_cast(level)) + " " + value; 124 | }); 125 | 126 | logger.write(level::verbose, "Hello World!"); 127 | EXPECT_EQ(output, "0 Hello World!"); 128 | EXPECT_TRUE(m_cout_buffer.str().empty()); 129 | 130 | logger.write(level::debug, "Hello World!"); 131 | EXPECT_EQ(output, "1 Hello World!"); 132 | EXPECT_TRUE(m_cout_buffer.str().empty()); 133 | 134 | logger.write(level::info, "Hello World!"); 135 | EXPECT_EQ(output, "2 Hello World!"); 136 | EXPECT_TRUE(m_cout_buffer.str().empty()); 137 | 138 | logger.write(level::warning, "Hello World!"); 139 | EXPECT_EQ(output, "3 Hello World!"); 140 | EXPECT_TRUE(m_cout_buffer.str().empty()); 141 | 142 | logger.write(level::error, "Hello World!"); 143 | EXPECT_EQ(output, "4 Hello World!"); 144 | EXPECT_TRUE(m_cout_buffer.str().empty()); 145 | 146 | logger.write(level::fatal, "Hello World!"); 147 | EXPECT_EQ(output, "5 Hello World!"); 148 | EXPECT_TRUE(m_cout_buffer.str().empty()); 149 | } 150 | 151 | TEST_S(WriteMethodRespectsLogLevel, DefaultLogger) { 152 | using level = display_device::Logger::LogLevel; 153 | auto &logger {display_device::Logger::get()}; 154 | 155 | EXPECT_TRUE(m_cout_buffer.str().empty()); 156 | 157 | logger.setLogLevel(level::error); 158 | logger.write(level::info, "Hello World!"); 159 | EXPECT_TRUE(m_cout_buffer.str().empty()); 160 | 161 | logger.setLogLevel(level::info); 162 | logger.write(level::info, "Hello World!"); 163 | EXPECT_FALSE(m_cout_buffer.str().empty()); 164 | } 165 | 166 | TEST_S(WriteMethodRespectsLogLevel, CustomCallback) { 167 | using level = display_device::Logger::LogLevel; 168 | auto &logger {display_device::Logger::get()}; 169 | 170 | bool callback_invoked {false}; 171 | logger.setCustomCallback([&callback_invoked](auto, auto) { 172 | callback_invoked = true; 173 | }); 174 | 175 | logger.setLogLevel(level::error); 176 | logger.write(level::info, "Hello World!"); 177 | EXPECT_EQ(callback_invoked, false); 178 | 179 | logger.setLogLevel(level::info); 180 | logger.write(level::info, "Hello World!"); 181 | EXPECT_EQ(callback_invoked, true); 182 | } 183 | 184 | TEST_S(LogMacroDisablesStreamChain) { 185 | using level = display_device::Logger::LogLevel; 186 | auto &logger {display_device::Logger::get()}; 187 | 188 | bool output_logged {false}; 189 | logger.setCustomCallback([&output_logged](auto, auto) { 190 | output_logged = true; 191 | }); 192 | 193 | bool some_function_invoked {false}; 194 | const auto some_function {[&some_function_invoked]() { 195 | some_function_invoked = true; 196 | return "some string"; 197 | }}; 198 | 199 | logger.setLogLevel(level::error); 200 | DD_LOG(info) << some_function(); 201 | EXPECT_EQ(output_logged, false); 202 | EXPECT_EQ(some_function_invoked, false); 203 | 204 | logger.setLogLevel(level::info); 205 | DD_LOG(info) << some_function(); 206 | EXPECT_EQ(output_logged, true); 207 | EXPECT_EQ(some_function_invoked, true); 208 | } 209 | -------------------------------------------------------------------------------- /src/windows/win_display_device_topology.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file src/windows/win_display_device_topology.cpp 3 | * @brief Definitions for the topology related methods in WinDisplayDevice. 4 | */ 5 | // class header include 6 | #include "display_device/windows/win_display_device.h" 7 | 8 | // system includes 9 | #include 10 | #include 11 | 12 | // local includes 13 | #include "display_device/logging.h" 14 | #include "display_device/windows/win_api_utils.h" 15 | 16 | namespace display_device { 17 | namespace { 18 | /** 19 | * @see set_topology for a description as this was split off to reduce cognitive complexity. 20 | */ 21 | bool doSetTopology(WinApiLayerInterface &w_api, const ActiveTopology &new_topology, const PathAndModeData &display_data) { 22 | const auto path_data {win_utils::collectSourceDataForMatchingPaths(w_api, display_data.m_paths)}; 23 | if (path_data.empty()) { 24 | // Error already logged 25 | return false; 26 | } 27 | 28 | auto paths {win_utils::makePathsForNewTopology(new_topology, path_data, display_data.m_paths)}; 29 | if (paths.empty()) { 30 | // Error already logged 31 | return false; 32 | } 33 | 34 | UINT32 flags {SDC_APPLY | SDC_TOPOLOGY_SUPPLIED | SDC_ALLOW_PATH_ORDER_CHANGES | SDC_VIRTUAL_MODE_AWARE}; 35 | LONG result {w_api.setDisplayConfig(paths, {}, flags)}; 36 | if (result == ERROR_GEN_FAILURE) { 37 | DD_LOG(warning) << w_api.getErrorString(result) << " failed to change topology using the topology from Windows DB! Asking Windows to create the topology."; 38 | 39 | flags = SDC_APPLY | SDC_USE_SUPPLIED_DISPLAY_CONFIG | SDC_ALLOW_CHANGES /* This flag is probably not needed, but who knows really... (not MSDOCS at least) */ | SDC_VIRTUAL_MODE_AWARE | SDC_SAVE_TO_DATABASE; 40 | result = w_api.setDisplayConfig(paths, {}, flags); 41 | if (result != ERROR_SUCCESS) { 42 | DD_LOG(error) << w_api.getErrorString(result) << " failed to create new topology configuration!"; 43 | return false; 44 | } 45 | } else if (result != ERROR_SUCCESS) { 46 | DD_LOG(error) << w_api.getErrorString(result) << " failed to change topology configuration!"; 47 | return false; 48 | } 49 | 50 | return true; 51 | } 52 | } // namespace 53 | 54 | ActiveTopology WinDisplayDevice::getCurrentTopology() const { 55 | const auto display_data {m_w_api->queryDisplayConfig(QueryType::Active)}; 56 | if (!display_data) { 57 | // Error already logged 58 | return {}; 59 | } 60 | 61 | // Duplicate displays can be identified by having the same x/y position. Here we have a 62 | // "position to index" map for a simple and lazy lookup in case we have to add a device to the 63 | // topology group. 64 | std::unordered_map position_to_topology_index; 65 | ActiveTopology topology; 66 | for (const auto &path : display_data->m_paths) { 67 | const auto device_info {win_utils::getDeviceInfoForValidPath(*m_w_api, path, display_device::ValidatedPathType::Active)}; 68 | if (!device_info) { 69 | continue; 70 | } 71 | 72 | const auto source_mode {win_utils::getSourceMode(win_utils::getSourceIndex(path, display_data->m_modes), display_data->m_modes)}; 73 | if (!source_mode) { 74 | DD_LOG(error) << "Active device does not have a source mode: " << device_info->m_device_id << "!"; 75 | return {}; 76 | } 77 | 78 | const std::string lazy_lookup {std::to_string(source_mode->position.x) + std::to_string(source_mode->position.y)}; 79 | auto index_it {position_to_topology_index.find(lazy_lookup)}; 80 | 81 | if (index_it == std::end(position_to_topology_index)) { 82 | position_to_topology_index[lazy_lookup] = topology.size(); 83 | topology.push_back({device_info->m_device_id}); 84 | } else { 85 | topology.at(index_it->second).push_back(device_info->m_device_id); 86 | } 87 | } 88 | 89 | return topology; 90 | } 91 | 92 | bool WinDisplayDevice::isTopologyValid(const ActiveTopology &topology) const { 93 | if (topology.empty()) { 94 | DD_LOG(warning) << "Topology input is empty!"; 95 | return false; 96 | } 97 | 98 | std::unordered_set device_ids; 99 | for (const auto &group : topology) { 100 | // Size 2 is a Windows' limitation. 101 | // You CAN set the group to be more than 2, but then 102 | // Windows' settings app breaks since it was not designed for this :/ 103 | if (group.empty() || group.size() > 2) { 104 | DD_LOG(warning) << "Topology group is invalid!"; 105 | return false; 106 | } 107 | 108 | for (const auto &device_id : group) { 109 | if (!device_ids.insert(device_id).second) { 110 | DD_LOG(warning) << "Duplicate device ids found in topology!"; 111 | return false; 112 | } 113 | } 114 | } 115 | 116 | return true; 117 | } 118 | 119 | bool WinDisplayDevice::isTopologyTheSame(const ActiveTopology &lhs, const ActiveTopology &rhs) const { 120 | const auto sort_topology = [](ActiveTopology &topology) { 121 | for (auto &group : topology) { 122 | std::sort(std::begin(group), std::end(group)); 123 | } 124 | 125 | std::sort(std::begin(topology), std::end(topology)); 126 | }; 127 | 128 | auto lhs_copy {lhs}; 129 | auto rhs_copy {rhs}; 130 | 131 | // On Windows order does not matter. 132 | sort_topology(lhs_copy); 133 | sort_topology(rhs_copy); 134 | 135 | return lhs_copy == rhs_copy; 136 | } 137 | 138 | bool WinDisplayDevice::setTopology(const ActiveTopology &new_topology) { 139 | if (!isTopologyValid(new_topology)) { 140 | DD_LOG(error) << "Topology input is invalid!"; 141 | return false; 142 | } 143 | 144 | const auto current_topology {getCurrentTopology()}; 145 | if (!isTopologyValid(current_topology)) { 146 | DD_LOG(error) << "Failed to get current topology!"; 147 | return false; 148 | } 149 | 150 | if (isTopologyTheSame(current_topology, new_topology)) { 151 | DD_LOG(debug) << "Same topology provided."; 152 | return true; 153 | } 154 | 155 | const auto &original_data {m_w_api->queryDisplayConfig(QueryType::All)}; 156 | if (!original_data) { 157 | // Error already logged 158 | return false; 159 | } 160 | 161 | if (doSetTopology(*m_w_api, new_topology, *original_data)) { 162 | const auto updated_topology {getCurrentTopology()}; 163 | if (isTopologyValid(updated_topology)) { 164 | if (isTopologyTheSame(new_topology, updated_topology)) { 165 | return true; 166 | } else { 167 | // There is an interesting bug in Windows when you have nearly 168 | // identical devices, drivers or something. For example, imagine you have: 169 | // AM - Actual Monitor 170 | // IDD1 - Virtual display 1 171 | // IDD2 - Virtual display 2 172 | // 173 | // You can have the following topology: 174 | // [[AM, IDD1]] 175 | // but not this: 176 | // [[AM, IDD2]] 177 | // 178 | // Windows API will just default to: 179 | // [[AM, IDD1]] 180 | // even if you provide the second variant. Windows API will think 181 | // it's OK and just return ERROR_SUCCESS in this case and there is 182 | // nothing you can do. Even the Windows' settings app will not 183 | // be able to set the desired topology. 184 | // 185 | // There seems to be a workaround - you need to make sure the IDD1 186 | // device is used somewhere else in the topology, like: 187 | // [[AM, IDD2], [IDD1]] 188 | // 189 | // However, since we have this bug an additional sanity check is needed 190 | // regardless of what Windows report back to us. 191 | DD_LOG(error) << "Failed to change topology due to Windows bug or because the display is in deep sleep!"; 192 | } 193 | } else { 194 | DD_LOG(error) << "Failed to get updated topology!"; 195 | } 196 | 197 | // Revert back to the original topology 198 | const UINT32 flags {SDC_APPLY | SDC_USE_SUPPLIED_DISPLAY_CONFIG | SDC_SAVE_TO_DATABASE | SDC_VIRTUAL_MODE_AWARE}; 199 | static_cast(m_w_api->setDisplayConfig(original_data->m_paths, original_data->m_modes, flags)); // Return value does not matter as we are trying out best to undo 200 | } 201 | 202 | return false; 203 | } 204 | } // namespace display_device 205 | --------------------------------------------------------------------------------