/** * @file src/display_device.cpp * @brief Definitions for display device handling. */ // header include #include "display_device.h" // lib includes #include #include #include #include #include #include #include #include // local includes #include "audio.h" #include "platform/common.h" #include "rtsp.h" // platform-specific includes #ifdef _WIN32 #include #include #include #endif namespace display_device { namespace { constexpr std::chrono::milliseconds DEFAULT_RETRY_INTERVAL {5000}; /** * @brief A global for the settings manager interface and other settings whose lifetime is managed by `display_device::init(...)`. */ struct { std::mutex mutex {}; std::chrono::milliseconds config_revert_delay {0}; std::unique_ptr> sm_instance {nullptr}; } DD_DATA; /** * @brief Helper class for capturing audio context when the API demands it. * * The capture is needed to be done in case some of the displays are going * to be deactivated before the stream starts. In this case the audio context * will be captured for this display and can be restored once it is turned back. */ class sunshine_audio_context_t: public AudioContextInterface { public: [[nodiscard]] bool capture() override { return context_scheduler.execute([](auto &audio_context) { // Explicitly releasing the context first in case it was not release yet so that it can be potentially cleaned up. audio_context = boost::none; audio_context = audio_context_t {}; // Always say that we have captured it successfully as otherwise the settings change procedure will be aborted. return true; }); } [[nodiscard]] bool isCaptured() const override { return context_scheduler.execute([](const auto &audio_context) { if (audio_context) { // In case we still have context we need to check whether it was released or not. // If it was released we can pretend that we no longer have it as it will be immediately cleaned up in `capture` method before we acquire new context. return !audio_context->released; } return false; }); } void release() override { context_scheduler.schedule([](auto &audio_context, auto &stop_token) { if (audio_context) { audio_context->released = true; const auto *audio_ctx_ptr = audio_context->audio_ctx_ref.get(); if (audio_ctx_ptr && !audio::is_audio_ctx_sink_available(*audio_ctx_ptr) && audio_context->retry_counter > 0) { // It is possible that the audio sink is not immediately available after the display is turned on. // Therefore, we will hold on to the audio context a little longer, until it is either available // or we time out. --audio_context->retry_counter; return; } } audio_context = boost::none; stop_token.requestStop(); }, SchedulerOptions {.m_sleep_durations = {2s}}); } private: struct audio_context_t { /** * @brief A reference to the audio context that will automatically extend the audio session. * @note It is auto-initialized here for convenience. */ decltype(audio::get_audio_ctx_ref()) audio_ctx_ref {audio::get_audio_ctx_ref()}; /** * @brief Will be set to true if the capture was released, but we still have to keep the context around, because the device is not available. */ bool released {false}; /** * @brief How many times to check if the audio sink is available before giving up. */ int retry_counter {15}; }; RetryScheduler> context_scheduler {std::make_unique>(boost::none)}; }; /** * @brief Convert string to unsigned int. * @note For random reason there is std::stoi, but not std::stou... * @param value String to be converted * @return Parsed unsigned integer. */ unsigned int stou(const std::string &value) { unsigned long result {std::stoul(value)}; if (result > std::numeric_limits::max()) { throw std::out_of_range("stou"); } return result; } /** * @brief Parse resolution value from the string. * @param input String to be parsed. * @param output Reference to output variable to fill in. * @returns True on successful parsing (empty string allowed), false otherwise. * * @examples * std::optional resolution; * if (parse_resolution_string("1920x1080", resolution)) { * if (resolution) { * BOOST_LOG(info) << "Value was specified"; * } * else { * BOOST_LOG(info) << "Value was empty"; * } * } * @examples_end */ bool parse_resolution_string(const std::string &input, std::optional &output) { const std::string trimmed_input {boost::algorithm::trim_copy(input)}; const std::regex resolution_regex {R"(^(\d+)x(\d+)$)"}; if (std::smatch match; std::regex_match(trimmed_input, match, resolution_regex)) { try { output = Resolution { stou(match[1].str()), stou(match[2].str()) }; return true; } catch (const std::out_of_range &) { BOOST_LOG(error) << "Failed to parse resolution string " << trimmed_input << " (number out of range)."; } catch (const std::exception &err) { BOOST_LOG(error) << "Failed to parse resolution string " << trimmed_input << ":\n" << err.what(); } } else { if (trimmed_input.empty()) { output = std::nullopt; return true; } BOOST_LOG(error) << "Failed to parse resolution string " << trimmed_input << R"(. It must match a "1920x1080" pattern!)"; } return false; } /** * @brief Parse refresh rate value from the string. * @param input String to be parsed. * @param output Reference to output variable to fill in. * @param allow_decimal_point Specify whether the decimal point is allowed or not. * @returns True on successful parsing (empty string allowed), false otherwise. * * @examples * std::optional refresh_rate; * if (parse_refresh_rate_string("59.95", refresh_rate)) { * if (refresh_rate) { * BOOST_LOG(info) << "Value was specified"; * } * else { * BOOST_LOG(info) << "Value was empty"; * } * } * @examples_end */ bool parse_refresh_rate_string(const std::string &input, std::optional &output, const bool allow_decimal_point = true) { static const auto is_zero {[](const auto &character) { return character == '0'; }}; const std::string trimmed_input {boost::algorithm::trim_copy(input)}; const std::regex refresh_rate_regex {allow_decimal_point ? R"(^(\d+)(?:\.(\d+))?$)" : R"(^(\d+)$)"}; if (std::smatch match; std::regex_match(trimmed_input, match, refresh_rate_regex)) { try { // Here we are trimming zeros from the string to possibly reduce out of bounds case std::string trimmed_match_1 {boost::algorithm::trim_left_copy_if(match[1].str(), is_zero)}; if (trimmed_match_1.empty()) { trimmed_match_1 = "0"s; // Just in case ALL the string is full of zeros, we want to leave one } std::string trimmed_match_2; if (allow_decimal_point && match[2].matched) { trimmed_match_2 = boost::algorithm::trim_right_copy_if(match[2].str(), is_zero); } if (!trimmed_match_2.empty()) { // We have a decimal point and will have to split it into numerator and denominator. // For example: // 59.995: // numerator = 59995 // denominator = 1000 // We are essentially removing the decimal point here: 59.995 -> 59995 const std::string numerator_str {trimmed_match_1 + trimmed_match_2}; const auto numerator {stou(numerator_str)}; // Here we are counting decimal places and calculating denominator: 10^decimal_places const auto denominator {static_cast(std::pow(10, trimmed_match_2.size()))}; output = Rational {numerator, denominator}; } else { // We do not have a decimal point, just a valid number. // For example: // 60: // numerator = 60 // denominator = 1 output = Rational {stou(trimmed_match_1), 1}; } return true; } catch (const std::out_of_range &) { BOOST_LOG(error) << "Failed to parse refresh rate string " << trimmed_input << " (number out of range)."; } catch (const std::exception &err) { BOOST_LOG(error) << "Failed to parse refresh rate string " << trimmed_input << ":\n" << err.what(); } } else { if (trimmed_input.empty()) { output = std::nullopt; return true; } BOOST_LOG(error) << "Failed to parse refresh rate string " << trimmed_input << ". Must have a pattern of " << (allow_decimal_point ? R"("123" or "123.456")" : R"("123")") << "!"; } return false; } /** * @brief Parse device preparation option from the user configuration and the session information. * @param video_config User's video related configuration. * @returns Parsed device preparation value we need to use. * Empty optional if no preparation nor configuration shall take place. * * @examples * const config::video_t &video_config { config::video }; * const auto device_prep_option = parse_device_prep_option(video_config); * @examples_end */ std::optional parse_device_prep_option(const config::video_t &video_config) { using enum config::video_t::dd_t::config_option_e; using enum SingleDisplayConfiguration::DevicePreparation; switch (video_config.dd.configuration_option) { case verify_only: return VerifyOnly; case ensure_active: return EnsureActive; case ensure_primary: return EnsurePrimary; case ensure_only_display: return EnsureOnlyDisplay; case disabled: break; } return std::nullopt; } /** * @brief Parse resolution option from the user configuration and the session information. * @param video_config User's video related configuration. * @param session Session information. * @param config A reference to a display config object that will be modified on success. * @returns True on successful parsing, false otherwise. * * @examples * const std::shared_ptr launch_session; * const config::video_t &video_config { config::video }; * * SingleDisplayConfiguration config; * const bool success = parse_resolution_option(video_config, *launch_session, config); * @examples_end */ bool parse_resolution_option(const config::video_t &video_config, const rtsp_stream::launch_session_t &session, SingleDisplayConfiguration &config) { using resolution_option_e = config::video_t::dd_t::resolution_option_e; switch (video_config.dd.resolution_option) { case resolution_option_e::automatic: { if (!session.enable_sops) { BOOST_LOG(warning) << R"(Sunshine is configured to change resolution automatically, but the "Optimize game settings" is not set in the client! Resolution will not be changed.)"; } else if (session.width >= 0 && session.height >= 0) { config.m_resolution = Resolution { static_cast(session.width), static_cast(session.height) }; } else { BOOST_LOG(error) << "Resolution provided by client session config is invalid: " << session.width << "x" << session.height; return false; } break; } case resolution_option_e::manual: { if (!session.enable_sops) { BOOST_LOG(warning) << R"(Sunshine is configured to change resolution manually, but the "Optimize game settings" is not set in the client! Resolution will not be changed.)"; } else { if (!parse_resolution_string(video_config.dd.manual_resolution, config.m_resolution)) { BOOST_LOG(error) << "Failed to parse manual resolution string!"; return false; } if (!config.m_resolution) { BOOST_LOG(error) << "Manual resolution must be specified!"; return false; } } break; } case resolution_option_e::disabled: break; } return true; } /** * @brief Parse refresh rate option from the user configuration and the session information. * @param video_config User's video related configuration. * @param session Session information. * @param config A reference to a config object that will be modified on success. * @returns True on successful parsing, false otherwise. * * @examples * const std::shared_ptr launch_session; * const config::video_t &video_config { config::video }; * * SingleDisplayConfiguration config; * const bool success = parse_refresh_rate_option(video_config, *launch_session, config); * @examples_end */ bool parse_refresh_rate_option(const config::video_t &video_config, const rtsp_stream::launch_session_t &session, SingleDisplayConfiguration &config) { using refresh_rate_option_e = config::video_t::dd_t::refresh_rate_option_e; switch (video_config.dd.refresh_rate_option) { case refresh_rate_option_e::automatic: { if (session.fps >= 0) { config.m_refresh_rate = Rational {static_cast(session.fps), 1}; } else { BOOST_LOG(error) << "FPS value provided by client session config is invalid: " << session.fps; return false; } break; } case refresh_rate_option_e::manual: { if (!parse_refresh_rate_string(video_config.dd.manual_refresh_rate, config.m_refresh_rate)) { BOOST_LOG(error) << "Failed to parse manual refresh rate string!"; return false; } if (!config.m_refresh_rate) { BOOST_LOG(error) << "Manual refresh rate must be specified!"; return false; } break; } case refresh_rate_option_e::disabled: break; } return true; } /** * @brief Parse HDR option from the user configuration and the session information. * @param video_config User's video related configuration. * @param session Session information. * @returns Parsed HDR state value we need to switch to. * Empty optional if no action is required. * * @examples * const std::shared_ptr launch_session; * const config::video_t &video_config { config::video }; * const auto hdr_option = parse_hdr_option(video_config, *launch_session); * @examples_end */ std::optional parse_hdr_option(const config::video_t &video_config, const rtsp_stream::launch_session_t &session) { using hdr_option_e = config::video_t::dd_t::hdr_option_e; switch (video_config.dd.hdr_option) { case hdr_option_e::automatic: return session.enable_hdr ? HdrState::Enabled : HdrState::Disabled; case hdr_option_e::disabled: break; } return std::nullopt; } /** * @brief Indicates which remapping fields and config structure shall be used. */ enum class remapping_type_e { mixed, ///! Both reseolution and refresh rate may be remapped resolution_only, ///! Only resolution will be remapped refresh_rate_only ///! Only refresh rate will be remapped }; /** * @brief Determine the ramapping type from the user config. * @param video_config User's video related configuration. * @returns Enum value if remapping can be performed, null optional if remapping shall be skipped. */ std::optional determine_remapping_type(const config::video_t &video_config) { using dd_t = config::video_t::dd_t; const bool auto_resolution {video_config.dd.resolution_option == dd_t::resolution_option_e::automatic}; const bool auto_refresh_rate {video_config.dd.refresh_rate_option == dd_t::refresh_rate_option_e::automatic}; if (auto_resolution && auto_refresh_rate) { return remapping_type_e::mixed; } if (auto_resolution) { return remapping_type_e::resolution_only; } if (auto_refresh_rate) { return remapping_type_e::refresh_rate_only; } return std::nullopt; } /** * @brief Contains remapping data parsed from the string values. */ struct parsed_remapping_entry_t { std::optional requested_resolution; std::optional requested_fps; std::optional final_resolution; std::optional final_refresh_rate; }; /** * @brief Check if resolution is to be mapped based on remmaping type. * @param type Remapping type to check. * @returns True if resolution is to be mapped, false otherwise. */ bool is_resolution_mapped(const remapping_type_e type) { return type == remapping_type_e::resolution_only || type == remapping_type_e::mixed; } /** * @brief Check if FPS is to be mapped based on remmaping type. * @param type Remapping type to check. * @returns True if FPS is to be mapped, false otherwise. */ bool is_fps_mapped(const remapping_type_e type) { return type == remapping_type_e::refresh_rate_only || type == remapping_type_e::mixed; } /** * @brief Parse the remapping entry from the config into an internal structure. * @param entry Entry to parse. * @param type Specify which entry fields should be parsed. * @returns Parsed structure or null optional if a necessary field could not be parsed. */ std::optional parse_remapping_entry(const config::video_t::dd_t::mode_remapping_entry_t &entry, const remapping_type_e type) { parsed_remapping_entry_t result {}; if (is_resolution_mapped(type) && (!parse_resolution_string(entry.requested_resolution, result.requested_resolution) || !parse_resolution_string(entry.final_resolution, result.final_resolution))) { return std::nullopt; } if (is_fps_mapped(type) && (!parse_refresh_rate_string(entry.requested_fps, result.requested_fps, false) || !parse_refresh_rate_string(entry.final_refresh_rate, result.final_refresh_rate))) { return std::nullopt; } return result; } /** * @brief Remap the the requested display mode based on the config. * @param video_config User's video related configuration. * @param session Session information. * @param config A reference to a config object that will be modified on success. * @returns True if the remapping was performed or skipped, false if remapping has failed due to invalid config. * * @examples * const std::shared_ptr launch_session; * const config::video_t &video_config { config::video }; * * SingleDisplayConfiguration config; * const bool success = remap_display_mode_if_needed(video_config, *launch_session, config); * @examples_end */ bool remap_display_mode_if_needed(const config::video_t &video_config, const rtsp_stream::launch_session_t &session, SingleDisplayConfiguration &config) { const auto remapping_type {determine_remapping_type(video_config)}; if (!remapping_type) { return true; } const auto &remapping_list {[&]() { using enum remapping_type_e; switch (*remapping_type) { case resolution_only: return video_config.dd.mode_remapping.resolution_only; case refresh_rate_only: return video_config.dd.mode_remapping.refresh_rate_only; case mixed: default: return video_config.dd.mode_remapping.mixed; } }()}; if (remapping_list.empty()) { BOOST_LOG(debug) << "No values are available for display mode remapping."; return true; } BOOST_LOG(debug) << "Trying to remap display modes..."; const auto entry_to_string {[type = *remapping_type](const config::video_t::dd_t::mode_remapping_entry_t &entry) { const bool mapping_resolution {is_resolution_mapped(type)}; const bool mapping_fps {is_fps_mapped(type)}; // clang-format off return (mapping_resolution ? " - requested resolution: "s + entry.requested_resolution + "\n" : "") + (mapping_fps ? " - requested FPS: "s + entry.requested_fps + "\n" : "") + (mapping_resolution ? " - final resolution: "s + entry.final_resolution + "\n" : "") + (mapping_fps ? " - final refresh rate: "s + entry.final_refresh_rate : ""); // clang-format on }}; for (const auto &entry : remapping_list) { const auto parsed_entry {parse_remapping_entry(entry, *remapping_type)}; if (!parsed_entry) { BOOST_LOG(error) << "Failed to parse remapping entry from:\n" << entry_to_string(entry); return false; } if (!parsed_entry->final_resolution && !parsed_entry->final_refresh_rate) { BOOST_LOG(error) << "At least one final value must be set for remapping display modes! Entry:\n" << entry_to_string(entry); return false; } if (!session.enable_sops && (parsed_entry->requested_resolution || parsed_entry->final_resolution)) { BOOST_LOG(warning) << R"(Skipping remapping entry, because the "Optimize game settings" is not set in the client! Entry:\n)" << entry_to_string(entry); continue; } // Note: at this point config should already have parsed resolution set. if (parsed_entry->requested_resolution && parsed_entry->requested_resolution != config.m_resolution) { BOOST_LOG(verbose) << "Skipping remapping because requested resolutions do not match! Entry:\n" << entry_to_string(entry); continue; } // Note: at this point config should already have parsed refresh rate set. if (parsed_entry->requested_fps && parsed_entry->requested_fps != config.m_refresh_rate) { BOOST_LOG(verbose) << "Skipping remapping because requested FPS do not match! Entry:\n" << entry_to_string(entry); continue; } BOOST_LOG(info) << "Remapping requested display mode. Entry:\n" << entry_to_string(entry); if (parsed_entry->final_resolution) { config.m_resolution = parsed_entry->final_resolution; } if (parsed_entry->final_refresh_rate) { config.m_refresh_rate = parsed_entry->final_refresh_rate; } break; } return true; } /** * @brief Construct a settings manager interface to manage display device settings. * @param persistence_filepath File location for saving persistent state. * @param video_config User's video related configuration. * @return An interface or nullptr if the OS does not support the interface. */ std::unique_ptr make_settings_manager([[maybe_unused]] const std::filesystem::path &persistence_filepath, [[maybe_unused]] const config::video_t &video_config) { #ifdef _WIN32 return std::make_unique( std::make_shared(std::make_shared()), std::make_shared(), std::make_unique( std::make_shared(persistence_filepath) ), WinWorkarounds { .m_hdr_blank_delay = video_config.dd.wa.hdr_toggle_delay != std::chrono::milliseconds::zero() ? std::make_optional(video_config.dd.wa.hdr_toggle_delay) : std::nullopt } ); #else return nullptr; #endif } /** * @brief Defines the "revert config" algorithms. */ enum class revert_option_e { try_once, ///< Try reverting once and then abort. try_indefinitely, ///< Keep trying to revert indefinitely. try_indefinitely_with_delay ///< Keep trying to revert indefinitely, but delay the first try by some amount of time. }; /** * @brief Reverts the configuration based on the provided option. * @note This is function does not lock mutex. */ void revert_configuration_unlocked(const revert_option_e option) { if (!DD_DATA.sm_instance) { // Platform is not supported, nothing to do. return; } // Note: by default the executor function is immediately executed in the calling thread. With delay, we want to avoid that. SchedulerOptions scheduler_option {.m_sleep_durations = {DEFAULT_RETRY_INTERVAL}}; if (option == revert_option_e::try_indefinitely_with_delay && DD_DATA.config_revert_delay > std::chrono::milliseconds::zero()) { scheduler_option.m_sleep_durations = {DD_DATA.config_revert_delay, DEFAULT_RETRY_INTERVAL}; scheduler_option.m_execution = SchedulerOptions::Execution::ScheduledOnly; } DD_DATA.sm_instance->schedule([try_once = (option == revert_option_e::try_once), tried_out_devices = std::set {}](auto &settings_iface, auto &stop_token) mutable { if (try_once) { std::ignore = settings_iface.revertSettings(); stop_token.requestStop(); return; } auto available_devices {[&settings_iface]() { const auto devices {settings_iface.enumAvailableDevices()}; std::set parsed_devices; std::transform( std::begin(devices), std::end(devices), std::inserter(parsed_devices, std::end(parsed_devices)), [](const auto &device) { return device.m_device_id + " - " + device.m_friendly_name; } ); return parsed_devices; }()}; if (available_devices == tried_out_devices) { BOOST_LOG(debug) << "Skipping reverting configuration, because no newly added/removed devices were detected since last check. Currently available devices:\n" << toJson(available_devices); return; } using enum SettingsManagerInterface::RevertResult; if (const auto result {settings_iface.revertSettings()}; result == Ok) { stop_token.requestStop(); return; } else if (result == ApiTemporarilyUnavailable) { // Do nothing and retry next time return; } // If we have failed to revert settings then we will try to do it next time only if a device was added/removed BOOST_LOG(warning) << "Failed to revert display device configuration (will retry once devices are added or removed). Enabling all of the available devices:\n" << toJson(available_devices); tried_out_devices.swap(available_devices); }, scheduler_option); } } // namespace std::unique_ptr init(const std::filesystem::path &persistence_filepath, const config::video_t &video_config) { std::lock_guard lock {DD_DATA.mutex}; // We can support re-init without any issues, however we should make sure to clean up first! revert_configuration_unlocked(revert_option_e::try_once); DD_DATA.config_revert_delay = video_config.dd.config_revert_delay; DD_DATA.sm_instance = nullptr; // If we fail to create settings manager, this means platform is not supported, and // we will need to provided error-free pass-trough in other methods if (auto settings_manager {make_settings_manager(persistence_filepath, video_config)}) { DD_DATA.sm_instance = std::make_unique>(std::move(settings_manager)); const auto available_devices {DD_DATA.sm_instance->execute([](auto &settings_iface) { return settings_iface.enumAvailableDevices(); })}; BOOST_LOG(info) << "Currently available display devices:\n" << toJson(available_devices); // In case we have failed to revert configuration before shutting down, we should // do it now. revert_configuration_unlocked(revert_option_e::try_indefinitely); } class deinit_t: public platf::deinit_t { public: ~deinit_t() override { std::lock_guard lock {DD_DATA.mutex}; try { // This may throw if used incorrectly. At the moment this will not happen, however // in case some unforeseen changes are made that could raise an exception, // we definitely don't want this to happen in destructor. Especially in the // deinit_t where the outcome does not really matter. revert_configuration_unlocked(revert_option_e::try_once); } catch (std::exception &err) { BOOST_LOG(fatal) << err.what(); } DD_DATA.sm_instance = nullptr; } }; return std::make_unique(); } std::string map_output_name(const std::string &output_name) { std::lock_guard lock {DD_DATA.mutex}; if (!DD_DATA.sm_instance) { // Fallback to giving back the output name if the platform is not supported. return output_name; } return DD_DATA.sm_instance->execute([&output_name](auto &settings_iface) { return settings_iface.getDisplayName(output_name); }); } void configure_display(const config::video_t &video_config, const rtsp_stream::launch_session_t &session) { const auto result {parse_configuration(video_config, session)}; if (const auto *parsed_config {std::get_if(&result)}; parsed_config) { configure_display(*parsed_config); return; } if (const auto *disabled {std::get_if(&result)}; disabled) { revert_configuration(); return; } // Error already logged for failed_to_parse_tag_t case, and we also don't // want to revert active configuration in case we have any } void configure_display(const SingleDisplayConfiguration &config) { std::lock_guard lock {DD_DATA.mutex}; if (!DD_DATA.sm_instance) { // Platform is not supported, nothing to do. return; } DD_DATA.sm_instance->schedule([config](auto &settings_iface, auto &stop_token) { // We only want to keep retrying in case of a transient errors. // In other cases, when we either fail or succeed we just want to stop... if (settings_iface.applySettings(config) != SettingsManagerInterface::ApplyResult::ApiTemporarilyUnavailable) { stop_token.requestStop(); } }, {.m_sleep_durations = {DEFAULT_RETRY_INTERVAL}}); } void revert_configuration() { std::lock_guard lock {DD_DATA.mutex}; revert_configuration_unlocked(revert_option_e::try_indefinitely_with_delay); } bool reset_persistence() { std::lock_guard lock {DD_DATA.mutex}; if (!DD_DATA.sm_instance) { // Platform is not supported, assume success. return true; } return DD_DATA.sm_instance->execute([](auto &settings_iface, auto &stop_token) { // Whatever the outcome is we want to stop interfering with the user, // so any schedulers need to be stopped. stop_token.requestStop(); return settings_iface.resetPersistence(); }); } EnumeratedDeviceList enumerate_devices() { std::lock_guard lock {DD_DATA.mutex}; if (!DD_DATA.sm_instance) { // Platform is not supported. return {}; } return DD_DATA.sm_instance->execute([](auto &settings_iface) { return settings_iface.enumAvailableDevices(); }); } std::variant parse_configuration(const config::video_t &video_config, const rtsp_stream::launch_session_t &session) { const auto device_prep {parse_device_prep_option(video_config)}; if (!device_prep) { return configuration_disabled_tag_t {}; } SingleDisplayConfiguration config; config.m_device_id = video_config.output_name; config.m_device_prep = *device_prep; config.m_hdr_state = parse_hdr_option(video_config, session); if (!parse_resolution_option(video_config, session, config)) { // Error already logged return failed_to_parse_tag_t {}; } if (!parse_refresh_rate_option(video_config, session, config)) { // Error already logged return failed_to_parse_tag_t {}; } if (!remap_display_mode_if_needed(video_config, session, config)) { // Error already logged return failed_to_parse_tag_t {}; } return config; } } // namespace display_device