feat(display): add display mode remapping option (#3529)

Co-authored-by: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com>
This commit is contained in:
Lukas Senionis 2025-01-12 21:14:20 +02:00 committed by GitHub
commit 1b94e9339a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 701 additions and 35 deletions

View file

@ -1234,6 +1234,92 @@ editing the `conf` file in a text editor. Use the examples as reference.
</tr> </tr>
</table> </table>
### dd_mode_remapping
<table>
<tr>
<td>Description</td>
<td colspan="2">
Remap the requested resolution and FPS to another display mode.<br>
Depending on the [dd_resolution_option](#dd_resolution_option) and
[dd_refresh_rate_option](#dd_refresh_rate_option) values, the following mapping
groups are available:
<ul>
<li>`mixed` - both options are set to `auto`.</li>
<li>
`resolution_only` - only [dd_resolution_option](#dd_resolution_option) is set to `auto`.
</li>
<li>
`refresh_rate_only` - only [dd_refresh_rate_option](#dd_refresh_rate_option) is set to `auto`.
</li>
</ul>
For each of those groups, a list of fields can be configured to perform remapping:
<ul>
<li>
`requested_resolution` - resolution that needs to be matched in order to use this remapping entry.
</li>
<li>`requested_fps` - FPS that needs to be matched in order to use this remapping entry.</li>
<li>`final_resolution` - resolution value to be used if the entry was matched.</li>
<li>`final_refresh_rate` - refresh rate value to be used if the entry was matched.</li>
</ul>
If `requested_*` field is left empty, it will match <b>everything</b>.<br>
If `final_*` field is left empty, the original value will not be remapped and either a requested, manual
or current value is used. However, at least one `final_*` must be set, otherwise the entry is considered
invalid.<br>
@note{"Optimize game settings" must be enabled on client side for ANY entry with `resolution`
field to be considered.}
@note{First entry to be matched in the list is the one that will be used.}
@tip{`requested_resolution` and `final_resolution` can be omitted for `refresh_rate_only` group.}
@tip{`requested_fps` and `final_refresh_rate` can be omitted for `resolution_only` group.}
@note{Applies to Windows only.}
</td>
</tr>
<tr>
<td>Default</td>
<td colspan="2">@code{}
dd_mode_remapping = {
"mixed": [],
"resolution_only": [],
"refresh_rate_only": []
}
@endcode
</td>
</tr>
<tr>
<td>Example</td>
<td colspan="2">@code{}
dd_mode_remapping = {
"mixed": [
{
"requested_fps": "60",
"final_refresh_rate": "119.95",
"requested_resolution": "1920x1080",
"final_resolution": "2560x1440"
},
{
"requested_fps": "60",
"final_refresh_rate": "120",
"requested_resolution": "",
"final_resolution": ""
}
],
"resolution_only": [
{
"requested_resolution": "1920x1080",
"final_resolution": "2560x1440"
}
],
"refresh_rate_only": [
{
"requested_fps": "60",
"final_refresh_rate": "119.95"
}
]
}@endcode
</td>
</tr>
</table>
### min_fps_factor ### min_fps_factor
<table> <table>

View file

@ -379,6 +379,38 @@ namespace config {
#undef _CONVERT_2_ARG_ #undef _CONVERT_2_ARG_
return video_t::dd_t::hdr_option_e::disabled; // Default to this if value is invalid return video_t::dd_t::hdr_option_e::disabled; // Default to this if value is invalid
} }
video_t::dd_t::mode_remapping_t
mode_remapping_from_view(const std::string_view value) {
const auto parse_entry_list { [](const auto &entry_list, auto &output_field) {
for (auto &[_, entry] : entry_list) {
auto requested_resolution = entry.template get_optional<std::string>("requested_resolution"s);
auto requested_fps = entry.template get_optional<std::string>("requested_fps"s);
auto final_resolution = entry.template get_optional<std::string>("final_resolution"s);
auto final_refresh_rate = entry.template get_optional<std::string>("final_refresh_rate"s);
output_field.push_back(video_t::dd_t::mode_remapping_entry_t {
requested_resolution.value_or(""),
requested_fps.value_or(""),
final_resolution.value_or(""),
final_refresh_rate.value_or("") });
}
} };
// We need to add a wrapping object to make it valid JSON, otherwise ptree cannot parse it.
std::stringstream json_stream;
json_stream << "{\"dd_mode_remapping\":" << value << "}";
boost::property_tree::ptree json_tree;
boost::property_tree::read_json(json_stream, json_tree);
video_t::dd_t::mode_remapping_t output;
parse_entry_list(json_tree.get_child("dd_mode_remapping.mixed"), output.mixed);
parse_entry_list(json_tree.get_child("dd_mode_remapping.resolution_only"), output.resolution_only);
parse_entry_list(json_tree.get_child("dd_mode_remapping.refresh_rate_only"), output.refresh_rate_only);
return output;
}
} // namespace dd } // namespace dd
video_t video { video_t video {
@ -447,6 +479,7 @@ namespace config {
{}, // manual_refresh_rate {}, // manual_refresh_rate
video_t::dd_t::hdr_option_e::automatic, // hdr_option video_t::dd_t::hdr_option_e::automatic, // hdr_option
3s, // config_revert_delay 3s, // config_revert_delay
{}, // mode_remapping
{} // wa {} // wa
}, // display_device }, // display_device
@ -1105,6 +1138,7 @@ namespace config {
video.dd.config_revert_delay = std::chrono::milliseconds { value }; video.dd.config_revert_delay = std::chrono::milliseconds { value };
} }
} }
generic_f(vars, "dd_mode_remapping", video.dd.mode_remapping, dd::mode_remapping_from_view);
bool_f(vars, "dd_wa_hdr_toggle", video.dd.wa.hdr_toggle); bool_f(vars, "dd_wa_hdr_toggle", video.dd.wa.hdr_toggle);
int_between_f(vars, "min_fps_factor", video.min_fps_factor, { 1, 3 }); int_between_f(vars, "min_fps_factor", video.min_fps_factor, { 1, 3 });

View file

@ -110,6 +110,19 @@ namespace config {
automatic ///< Change HDR settings and use the state requested by Moonlight. automatic ///< Change HDR settings and use the state requested by Moonlight.
}; };
struct mode_remapping_entry_t {
std::string requested_resolution;
std::string requested_fps;
std::string final_resolution;
std::string final_refresh_rate;
};
struct mode_remapping_t {
std::vector<mode_remapping_entry_t> mixed; ///< To be used when `resolution_option` and `refresh_rate_option` is set to `automatic`.
std::vector<mode_remapping_entry_t> resolution_only; ///< To be use when only `resolution_option` is set to `automatic`.
std::vector<mode_remapping_entry_t> refresh_rate_only; ///< To be use when only `refresh_rate_option` is set to `automatic`.
};
config_option_e configuration_option; config_option_e configuration_option;
resolution_option_e resolution_option; resolution_option_e resolution_option;
std::string manual_resolution; ///< Manual resolution in case `resolution_option == resolution_option_e::manual`. std::string manual_resolution; ///< Manual resolution in case `resolution_option == resolution_option_e::manual`.
@ -117,6 +130,7 @@ namespace config {
std::string manual_refresh_rate; ///< Manual refresh rate in case `refresh_rate_option == refresh_rate_option_e::manual`. std::string manual_refresh_rate; ///< Manual refresh rate in case `refresh_rate_option == refresh_rate_option_e::manual`.
hdr_option_e hdr_option; hdr_option_e hdr_option;
std::chrono::milliseconds config_revert_delay; ///< Time to wait until settings are reverted (after stream ends/app exists). std::chrono::milliseconds config_revert_delay; ///< Time to wait until settings are reverted (after stream ends/app exists).
mode_remapping_t mode_remapping;
workarounds_t wa; workarounds_t wa;
} dd; } dd;

View file

@ -188,6 +188,7 @@ namespace display_device {
* @brief Parse refresh rate value from the string. * @brief Parse refresh rate value from the string.
* @param input String to be parsed. * @param input String to be parsed.
* @param output Reference to output variable to fill in. * @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. * @returns True on successful parsing (empty string allowed), false otherwise.
* *
* @examples * @examples
@ -203,10 +204,10 @@ namespace display_device {
* @examples_end * @examples_end
*/ */
bool bool
parse_refresh_rate_string(const std::string &input, std::optional<FloatingPoint> &output) { parse_refresh_rate_string(const std::string &input, std::optional<FloatingPoint> &output, const bool allow_decimal_point = true) {
static const auto is_zero { [](const auto &character) { return character == '0'; } }; static const auto is_zero { [](const auto &character) { return character == '0'; } };
const std::string trimmed_input { boost::algorithm::trim_copy(input) }; const std::string trimmed_input { boost::algorithm::trim_copy(input) };
const std::regex refresh_rate_regex { R"(^(\d+)(?:\.(\d+))?$)" }; 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)) { if (std::smatch match; std::regex_match(trimmed_input, match, refresh_rate_regex)) {
try { try {
@ -217,7 +218,7 @@ namespace display_device {
} }
std::string trimmed_match_2; std::string trimmed_match_2;
if (match[2].matched) { if (allow_decimal_point && match[2].matched) {
trimmed_match_2 = boost::algorithm::trim_right_copy_if(match[2].str(), is_zero); trimmed_match_2 = boost::algorithm::trim_right_copy_if(match[2].str(), is_zero);
} }
@ -261,7 +262,7 @@ namespace display_device {
return true; return true;
} }
BOOST_LOG(error) << "Failed to parse refresh rate string " << trimmed_input << R"(. Must have a pattern of "123" or "123.456"!)"; 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; return false;
@ -435,6 +436,196 @@ namespace display_device {
return std::nullopt; 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<remapping_type_e>
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<Resolution> requested_resolution;
std::optional<FloatingPoint> requested_fps;
std::optional<Resolution> final_resolution;
std::optional<FloatingPoint> 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<parsed_remapping_entry_t>
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<rtsp_stream::launch_session_t> 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. * @brief Construct a settings manager interface to manage display device settings.
* @param persistence_filepath File location for saving persistent state. * @param persistence_filepath File location for saving persistent state.
@ -626,6 +817,11 @@ namespace display_device {
return failed_to_parse_tag_t {}; 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; return config;
} }
} // namespace display_device } // namespace display_device

View file

@ -33,7 +33,6 @@
<general <general
v-if="currentTab === 'general'" v-if="currentTab === 'general'"
:config="config" :config="config"
:global-prep-cmd="global_prep_cmd"
:platform="platform"> :platform="platform">
</general> </general>
@ -127,7 +126,6 @@
restarted: false, restarted: false,
config: null, config: null,
currentTab: "general", currentTab: "general",
global_prep_cmd: [],
tabs: [ // TODO: Move the options to each Component instead, encapsulate. tabs: [ // TODO: Move the options to each Component instead, encapsulate.
{ {
id: "general", id: "general",
@ -136,7 +134,7 @@
"locale": "en", "locale": "en",
"sunshine_name": "", "sunshine_name": "",
"min_log_level": 2, "min_log_level": 2,
"global_prep_cmd": "[]", "global_prep_cmd": [],
"notify_pre_releases": "disabled", "notify_pre_releases": "disabled",
}, },
}, },
@ -178,6 +176,7 @@
"dd_manual_refresh_rate": "", "dd_manual_refresh_rate": "",
"dd_hdr_option": "auto", "dd_hdr_option": "auto",
"dd_config_revert_delay": 3000, "dd_config_revert_delay": 3000,
"dd_mode_remapping": {"mixed": [], "resolution_only": [], "refresh_rate_only": []},
"dd_wa_hdr_toggle": "disabled", "dd_wa_hdr_toggle": "disabled",
"min_fps_factor": 1, "min_fps_factor": 1,
}, },
@ -320,17 +319,23 @@
// TODO: let each tab's Component handle it's own data instead of doing it here // TODO: let each tab's Component handle it's own data instead of doing it here
// Parse the special options before population if available
const specialOptions = ["dd_mode_remapping", "global_prep_cmd"]
for (const optionKey of specialOptions) {
if (this.config.hasOwnProperty(optionKey)) {
this.config[optionKey] = JSON.parse(this.config[optionKey]);
}
}
// Populate default values from tabs options // Populate default values from tabs options
this.tabs.forEach(tab => { this.tabs.forEach(tab => {
Object.keys(tab.options).forEach(optionKey => { Object.keys(tab.options).forEach(optionKey => {
if (this.config[optionKey] === undefined) { if (this.config[optionKey] === undefined) {
this.config[optionKey] = tab.options[optionKey]; // Make sure to copy by value
this.config[optionKey] = JSON.parse(JSON.stringify(tab.options[optionKey]));
} }
}); });
}); });
this.config.global_prep_cmd = this.config.global_prep_cmd || [];
this.global_prep_cmd = JSON.parse(this.config.global_prep_cmd);
}); });
}, },
methods: { methods: {
@ -338,26 +343,26 @@
this.$forceUpdate() this.$forceUpdate()
}, },
serialize() { serialize() {
this.config.global_prep_cmd = JSON.stringify(this.global_prep_cmd); let config = JSON.parse(JSON.stringify(this.config));
config.global_prep_cmd = JSON.stringify(config.global_prep_cmd);
config.dd_mode_remapping = JSON.stringify(config.dd_mode_remapping);
return config;
}, },
save() { save() {
this.saved = false; this.saved = false;
this.restarted = false; this.restarted = false;
this.serialize();
// create a temp copy of this.config to use for the post request // create a temp copy of this.config to use for the post request
let config = JSON.parse(JSON.stringify(this.config)) let config = this.serialize();
// delete default values from this.config // delete default values from this.config
this.tabs.forEach(tab => { this.tabs.forEach(tab => {
Object.keys(tab.options).forEach(optionKey => { Object.keys(tab.options).forEach(optionKey => {
let delete_value = false let delete_value = false
if (["global_prep_cmd"].includes(optionKey)) { if (["global_prep_cmd", "dd_mode_remapping"].includes(optionKey)) {
let config_value, default_value const config_value = config[optionKey]
const default_value = JSON.stringify(tab.options[optionKey])
config_value = JSON.parse(config[optionKey])
default_value = JSON.parse(tab.options[optionKey])
if (config_value === default_value) { if (config_value === default_value) {
delete_value = true delete_value = true

View file

@ -11,7 +11,6 @@ import Checkbox from "../../Checkbox.vue";
const props = defineProps([ const props = defineProps([
'platform', 'platform',
'config', 'config',
'min_fps_factor',
]) ])
const config = ref(props.config) const config = ref(props.config)
@ -95,7 +94,6 @@ const config = ref(props.config)
<DisplayModesSettings <DisplayModesSettings
:platform="platform" :platform="platform"
:config="config" :config="config"
:min_fps_factor="min_fps_factor"
/> />
</div> </div>

View file

@ -4,12 +4,9 @@ import { ref } from 'vue'
const props = defineProps({ const props = defineProps({
platform: String, platform: String,
config: Object, config: Object
globalPrepCmd: Array
}) })
const config = ref(props.config) const config = ref(props.config)
const globalPrepCmd = ref(props.globalPrepCmd)
function addCmd() { function addCmd() {
let template = { let template = {
@ -20,11 +17,11 @@ function addCmd() {
if (props.platform === 'windows') { if (props.platform === 'windows') {
template = { ...template, elevated: false }; template = { ...template, elevated: false };
} }
globalPrepCmd.value.push(template); config.value.global_prep_cmd.push(template);
} }
function removeCmd(index) { function removeCmd(index) {
globalPrepCmd.value.splice(index,1) config.value.global_prep_cmd.splice(index,1)
} }
</script> </script>
@ -83,7 +80,7 @@ function removeCmd(index) {
<div id="global_prep_cmd" class="mb-3 d-flex flex-column"> <div id="global_prep_cmd" class="mb-3 d-flex flex-column">
<label class="form-label">{{ $t('config.global_prep_cmd') }}</label> <label class="form-label">{{ $t('config.global_prep_cmd') }}</label>
<div class="form-text">{{ $t('config.global_prep_cmd_desc') }}</div> <div class="form-text">{{ $t('config.global_prep_cmd_desc') }}</div>
<table class="table" v-if="globalPrepCmd.length > 0"> <table class="table" v-if="config.global_prep_cmd.length > 0">
<thead> <thead>
<tr> <tr>
<th scope="col"><i class="fas fa-play"></i> {{ $t('_common.do_cmd') }}</th> <th scope="col"><i class="fas fa-play"></i> {{ $t('_common.do_cmd') }}</th>
@ -95,7 +92,7 @@ function removeCmd(index) {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="(c, i) in globalPrepCmd"> <tr v-for="(c, i) in config.global_prep_cmd">
<td> <td>
<input type="text" class="form-control monospace" v-model="c.do" /> <input type="text" class="form-control monospace" v-model="c.do" />
</td> </td>

View file

@ -7,8 +7,44 @@ const props = defineProps({
platform: String, platform: String,
config: Object config: Object
}) })
const config = ref(props.config) const config = ref(props.config)
const REFRESH_RATE_ONLY = "refresh_rate_only"
const RESOLUTION_ONLY = "resolution_only"
const MIXED = "mixed"
function canBeRemapped() {
return (config.value.dd_resolution_option === "auto" || config.value.dd_refresh_rate_option === "auto")
&& config.value.dd_configuration_option !== "disabled";
}
function getRemappingType() {
// Assuming here that at least one setting is set to "auto" if other is not
if (config.value.dd_resolution_option !== "auto") {
return REFRESH_RATE_ONLY;
}
if (config.value.dd_refresh_rate_option !== "auto") {
return RESOLUTION_ONLY;
}
return MIXED;
}
function addRemappingEntry() {
const type = getRemappingType();
let template = {};
if (type !== RESOLUTION_ONLY) {
template["requested_fps"] = "";
template["final_refresh_rate"] = "";
}
if (type !== REFRESH_RATE_ONLY) {
template["requested_resolution"] = "";
template["final_resolution"] = "";
}
config.value.dd_mode_remapping[type].push(template);
}
</script> </script>
<template> <template>
@ -114,6 +150,76 @@ const config = ref(props.config)
{{ $t('config.dd_config_revert_delay_desc') }} {{ $t('config.dd_config_revert_delay_desc') }}
</div> </div>
</div> </div>
<!-- Display mode remapping -->
<div class="mb-3" v-if="canBeRemapped()">
<label for="dd_mode_remapping" class="form-label">
{{ $t('config.dd_mode_remapping') }}
</label>
<div id="dd_mode_remapping" class="d-flex flex-column">
<div class="form-text">
{{ $t('config.dd_mode_remapping_desc_1') }}<br>
{{ $t('config.dd_mode_remapping_desc_2') }}<br>
{{ $t('config.dd_mode_remapping_desc_3') }}<br>
{{ $t(getRemappingType() === MIXED ? 'config.dd_mode_remapping_desc_4_final_values_mixed' : 'config.dd_mode_remapping_desc_4_final_values_non_mixed') }}<br>
<template v-if="getRemappingType() === MIXED">
{{ $t('config.dd_mode_remapping_desc_5_sops_mixed_only') }}<br>
</template>
<template v-if="getRemappingType() === RESOLUTION_ONLY">
{{ $t('config.dd_mode_remapping_desc_5_sops_resolution_only') }}<br>
</template>
</div>
</div>
<table class="table" v-if="config.dd_mode_remapping[getRemappingType()].length > 0">
<thead>
<tr>
<th scope="col" v-if="getRemappingType() !== REFRESH_RATE_ONLY">
{{ $t('config.dd_mode_remapping_requested_resolution') }}
</th>
<th scope="col" v-if="getRemappingType() !== RESOLUTION_ONLY">
{{ $t('config.dd_mode_remapping_requested_fps') }}
</th>
<th scope="col" v-if="getRemappingType() !== REFRESH_RATE_ONLY">
{{ $t('config.dd_mode_remapping_final_resolution') }}
</th>
<th scope="col" v-if="getRemappingType() !== RESOLUTION_ONLY">
{{ $t('config.dd_mode_remapping_final_refresh_rate') }}
</th>
<!-- Additional columns for buttons-->
<th scope="col"></th>
</tr>
</thead>
<tbody>
<tr v-for="(value, idx) in config.dd_mode_remapping[getRemappingType()]">
<td v-if="getRemappingType() !== REFRESH_RATE_ONLY">
<input type="text" class="form-control monospace" v-model="value.requested_resolution"
:placeholder="'1920x1080'" />
</td>
<td v-if="getRemappingType() !== RESOLUTION_ONLY">
<input type="text" class="form-control monospace" v-model="value.requested_fps"
:placeholder="'60'" />
</td>
<td v-if="getRemappingType() !== REFRESH_RATE_ONLY">
<input type="text" class="form-control monospace" v-model="value.final_resolution"
:placeholder="'2560x1440'" />
</td>
<td v-if="getRemappingType() !== RESOLUTION_ONLY">
<input type="text" class="form-control monospace" v-model="value.final_refresh_rate"
:placeholder="'119.95'" />
</td>
<td>
<button class="btn btn-danger" @click="config.dd_mode_remapping[getRemappingType()].splice(idx, 1)">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
</tbody>
</table>
<button class="ms-0 mt-2 btn btn-success" style="margin: 0 auto" @click="addRemappingEntry()">
&plus; {{ $t('config.dd_mode_remapping_add') }}
</button>
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -6,13 +6,8 @@ import PlatformLayout from '../../../PlatformLayout.vue'
const props = defineProps([ const props = defineProps([
'platform', 'platform',
'config', 'config',
'min_fps_factor',
]) ])
const config = ref(props.config) const config = ref(props.config)
const resIn = ref("")
const fpsIn = ref("")
</script> </script>
<template> <template>

View file

@ -162,6 +162,19 @@
"dd_hdr_option": "HDR", "dd_hdr_option": "HDR",
"dd_hdr_option_auto": "Switch on/off the HDR mode as requested by the client (default)", "dd_hdr_option_auto": "Switch on/off the HDR mode as requested by the client (default)",
"dd_hdr_option_disabled": "Do not change HDR settings", "dd_hdr_option_disabled": "Do not change HDR settings",
"dd_mode_remapping": "Display mode remapping",
"dd_mode_remapping_add": "Add remapping entry",
"dd_mode_remapping_desc_1": "Specify remapping entries to change the requested resolution and/or refresh rate to other values.",
"dd_mode_remapping_desc_2": "The list is iterated from top to bottom and the first match is used.",
"dd_mode_remapping_desc_3": "\"Requested\" fields can be left empty to match any requested value.",
"dd_mode_remapping_desc_4_final_values_mixed": "At least one \"Final\" field must be specified. The unspecified resolution or refresh rate will not be changed.",
"dd_mode_remapping_desc_4_final_values_non_mixed": "\"Final\" field must be specified and cannot be empty.",
"dd_mode_remapping_desc_5_sops_mixed_only": "\"Optimize game settings\" option must be enabled in the Moonlight client, otherwise entries with any resolution fields specified are skipped.",
"dd_mode_remapping_desc_5_sops_resolution_only": "\"Optimize game settings\" option must be enabled in the Moonlight client, otherwise the mapping is skipped.",
"dd_mode_remapping_final_refresh_rate": "Final refresh rate",
"dd_mode_remapping_final_resolution": "Final resolution",
"dd_mode_remapping_requested_fps": "Requested FPS",
"dd_mode_remapping_requested_resolution": "Requested resolution",
"dd_options_header": "Advanced display device options", "dd_options_header": "Advanced display device options",
"dd_refresh_rate_option": "Refresh rate", "dd_refresh_rate_option": "Refresh rate",
"dd_refresh_rate_option_auto": "Use FPS value provided by the client (default)", "dd_refresh_rate_option_auto": "Use FPS value provided by the client (default)",

View file

@ -274,3 +274,225 @@ TEST_P(ParseRefreshRateOption, IntegrationTest) {
EXPECT_EQ(std::get<display_device::SingleDisplayConfiguration>(result).m_refresh_rate, expected_refresh_rate); EXPECT_EQ(std::get<display_device::SingleDisplayConfiguration>(result).m_refresh_rate, expected_refresh_rate);
} }
} }
namespace {
using res_t = resolution_t;
using fps_t = client_fps_t;
using remap_entries_t = config::video_t::dd_t::mode_remapping_t;
struct no_value_t {
};
template <class T>
struct auto_value_t {
T value;
};
template <class T>
struct manual_value_t {
T value;
};
using resolution_variant_t = std::variant<no_value_t, auto_value_t<res_t>, manual_value_t<res_t>>;
using rational_variant_t = std::variant<no_value_t, auto_value_t<fps_t>, manual_value_t<fps_t>>;
struct failed_to_remap_t {};
struct final_values_t {
std::optional<resolution_t> resolution;
std::optional<rational_t> refresh_rate;
};
const std::string INVALID_RES { "INVALID" };
const std::string INVALID_FPS { "1.23" };
const std::string INVALID_REFRESH_RATE { "INVALID" };
const remap_entries_t VALID_ENTRIES {
.mixed = {
{ "1920x1080", "11", "1024x720", "1.11" },
{ "1920x1080", "", "1024x720", "2" },
{ "", "33", "1024x720", "3" },
{ "1920x720", "44", "1024x720", "" },
{ "1920x720", "55", "", "5" },
{ "1920x720", "", "1024x720", "" },
{ "", "11", "", "7.77" } },
.resolution_only = { { "1920x1080", "", "720x720", "" }, { "1024x720", "", "1920x1920", "" } },
.refresh_rate_only = { { "", "11", "", "1.23" }, { "", "22", "", "2.34" } }
};
const remap_entries_t INVALID_REQ_RES {
.mixed = { { INVALID_RES, "11", "1024x720", "1.11" } },
.resolution_only = { { INVALID_RES, "", "720x720", "" } },
.refresh_rate_only = { { INVALID_RES, "11", "", "1.23" } }
};
const remap_entries_t INVALID_REQ_FPS {
.mixed = { { "1920x1080", INVALID_FPS, "1024x720", "1.11" } },
.resolution_only = { { "1920x1080", INVALID_FPS, "720x720", "" } },
.refresh_rate_only = { { "", INVALID_FPS, "", "1.23" } }
};
const remap_entries_t INVALID_FINAL_RES {
.mixed = { { "1920x1080", "11", INVALID_RES, "1.11" } },
.resolution_only = { { "1920x1080", "", INVALID_RES, "" } },
.refresh_rate_only = { { "", "11", INVALID_RES, "1.23" } }
};
const remap_entries_t INVALID_FINAL_REFRESH_RATE {
.mixed = { { "1920x1080", "11", "1024x720", INVALID_REFRESH_RATE } },
.resolution_only = { { "1920x1080", "", "720x720", INVALID_REFRESH_RATE } },
.refresh_rate_only = { { "", "11", "", INVALID_REFRESH_RATE } }
};
const remap_entries_t EMPTY_REQ_ENTRIES {
.mixed = { { "", "", "1024x720", "1.11" } },
.resolution_only = { { "", "", "720x720", "" } },
.refresh_rate_only = { { "", "", "", "1.23" } }
};
const remap_entries_t EMPTY_FINAL_ENTRIES {
.mixed = { { "1920x1080", "11", "", "" } },
.resolution_only = { { "1920x1080", "", "", "" } },
.refresh_rate_only = { { "", "11", "", "" } }
};
using DisplayModeRemapping = DisplayDeviceConfigTest<std::pair<std::tuple<resolution_variant_t, rational_variant_t, sops_enabled_t, remap_entries_t>, std::variant<failed_to_remap_t, final_values_t>>>;
INSTANTIATE_TEST_SUITE_P(
DisplayDeviceConfigTest,
DisplayModeRemapping,
testing::Values(
//---- Mixed (valid), SOPS enabled ----
std::make_pair(std::make_tuple(auto_value_t<res_t> { 1920, 1080 }, auto_value_t<fps_t> { 11 }, sops_enabled_t { true }, VALID_ENTRIES), final_values_t { { { 1024, 720 } }, { { 111, 100 } } }),
std::make_pair(std::make_tuple(auto_value_t<res_t> { 1920, 1080 }, auto_value_t<fps_t> { 120 }, sops_enabled_t { true }, VALID_ENTRIES), final_values_t { { { 1024, 720 } }, { { 2, 1 } } }),
std::make_pair(std::make_tuple(auto_value_t<res_t> { 1, 1 }, auto_value_t<fps_t> { 33 }, sops_enabled_t { true }, VALID_ENTRIES), final_values_t { { { 1024, 720 } }, { { 3, 1 } } }),
std::make_pair(std::make_tuple(auto_value_t<res_t> { 1920, 720 }, auto_value_t<fps_t> { 44 }, sops_enabled_t { true }, VALID_ENTRIES), final_values_t { { { 1024, 720 } }, { { 44, 1 } } }),
std::make_pair(std::make_tuple(auto_value_t<res_t> { 1920, 720 }, auto_value_t<fps_t> { 55 }, sops_enabled_t { true }, VALID_ENTRIES), final_values_t { { { 1920, 720 } }, { { 5, 1 } } }),
std::make_pair(std::make_tuple(auto_value_t<res_t> { 1920, 720 }, auto_value_t<fps_t> { 60 }, sops_enabled_t { true }, VALID_ENTRIES), final_values_t { { { 1024, 720 } }, { { 60, 1 } } }),
std::make_pair(std::make_tuple(auto_value_t<res_t> { 1, 1 }, auto_value_t<fps_t> { 123 }, sops_enabled_t { true }, VALID_ENTRIES), final_values_t { { { 1, 1 } }, { { 123, 1 } } }),
//---- Mixed (valid), SOPS disabled ----
std::make_pair(std::make_tuple(auto_value_t<res_t> { 1920, 1080 }, auto_value_t<fps_t> { 11 }, sops_enabled_t { false }, VALID_ENTRIES), final_values_t { std::nullopt, { { 777, 100 } } }),
std::make_pair(std::make_tuple(auto_value_t<res_t> { 1920, 1080 }, auto_value_t<fps_t> { 120 }, sops_enabled_t { false }, VALID_ENTRIES), final_values_t { std::nullopt, { { 120, 1 } } }),
std::make_pair(std::make_tuple(auto_value_t<res_t> { 1, 1 }, auto_value_t<fps_t> { 33 }, sops_enabled_t { false }, VALID_ENTRIES), final_values_t { std::nullopt, { { 33, 1 } } }),
std::make_pair(std::make_tuple(auto_value_t<res_t> { 1920, 720 }, auto_value_t<fps_t> { 44 }, sops_enabled_t { false }, VALID_ENTRIES), final_values_t { std::nullopt, { { 44, 1 } } }),
std::make_pair(std::make_tuple(auto_value_t<res_t> { 1920, 720 }, auto_value_t<fps_t> { 55 }, sops_enabled_t { false }, VALID_ENTRIES), final_values_t { std::nullopt, { { 55, 1 } } }),
std::make_pair(std::make_tuple(auto_value_t<res_t> { 1920, 720 }, auto_value_t<fps_t> { 60 }, sops_enabled_t { false }, VALID_ENTRIES), final_values_t { std::nullopt, { { 60, 1 } } }),
std::make_pair(std::make_tuple(auto_value_t<res_t> { 1, 1 }, auto_value_t<fps_t> { 123 }, sops_enabled_t { false }, VALID_ENTRIES), final_values_t { std::nullopt, { { 123, 1 } } }),
//---- Resolution only (valid), SOPS enabled ----
std::make_pair(std::make_tuple(auto_value_t<res_t> { 1920, 1080 }, manual_value_t<fps_t> { 11 }, sops_enabled_t { true }, VALID_ENTRIES), final_values_t { { { 720, 720 } }, { { 11, 1 } } }),
std::make_pair(std::make_tuple(auto_value_t<res_t> { 1024, 720 }, no_value_t {}, sops_enabled_t { true }, VALID_ENTRIES), final_values_t { { { 1920, 1920 } }, std::nullopt }),
std::make_pair(std::make_tuple(auto_value_t<res_t> { 11, 11 }, manual_value_t<fps_t> { 33 }, sops_enabled_t { true }, VALID_ENTRIES), final_values_t { { { 11, 11 } }, { { 33, 1 } } }),
//---- Resolution only (valid), SOPS disabled ----
std::make_pair(std::make_tuple(auto_value_t<res_t> { 1920, 1080 }, manual_value_t<fps_t> { 11 }, sops_enabled_t { false }, VALID_ENTRIES), final_values_t { std::nullopt, { { 11, 1 } } }),
std::make_pair(std::make_tuple(auto_value_t<res_t> { 1024, 720 }, no_value_t {}, sops_enabled_t { false }, VALID_ENTRIES), final_values_t { std::nullopt, std::nullopt }),
std::make_pair(std::make_tuple(auto_value_t<res_t> { 11, 11 }, manual_value_t<fps_t> { 33 }, sops_enabled_t { false }, VALID_ENTRIES), final_values_t { std::nullopt, { { 33, 1 } } }),
//---- Refresh rate only (valid), SOPS enabled ----
std::make_pair(std::make_tuple(manual_value_t<res_t> { 1920, 1080 }, auto_value_t<fps_t> { 11 }, sops_enabled_t { true }, VALID_ENTRIES), final_values_t { { { 1920, 1080 } }, { { 123, 100 } } }),
std::make_pair(std::make_tuple(no_value_t {}, auto_value_t<fps_t> { 22 }, sops_enabled_t { true }, VALID_ENTRIES), final_values_t { std::nullopt, { { 234, 100 } } }),
std::make_pair(std::make_tuple(manual_value_t<res_t> { 11, 11 }, auto_value_t<fps_t> { 33 }, sops_enabled_t { true }, VALID_ENTRIES), final_values_t { { { 11, 11 } }, { { 33, 1 } } }),
//---- Refresh rate only (valid), SOPS disabled ----
std::make_pair(std::make_tuple(manual_value_t<res_t> { 1920, 1080 }, auto_value_t<fps_t> { 11 }, sops_enabled_t { false }, VALID_ENTRIES), final_values_t { std::nullopt, { { 123, 100 } } }),
std::make_pair(std::make_tuple(no_value_t {}, auto_value_t<fps_t> { 22 }, sops_enabled_t { false }, VALID_ENTRIES), final_values_t { std::nullopt, { { 234, 100 } } }),
std::make_pair(std::make_tuple(manual_value_t<res_t> { 11, 11 }, auto_value_t<fps_t> { 33 }, sops_enabled_t { false }, VALID_ENTRIES), final_values_t { std::nullopt, { { 33, 1 } } }),
//---- No mapping (valid), SOPS enabled ----
std::make_pair(std::make_tuple(manual_value_t<res_t> { 1920, 1080 }, manual_value_t<fps_t> { 11 }, sops_enabled_t { true }, VALID_ENTRIES), final_values_t { { { 1920, 1080 } }, { { 11, 1 } } }),
std::make_pair(std::make_tuple(no_value_t {}, no_value_t {}, sops_enabled_t { true }, VALID_ENTRIES), final_values_t { std::nullopt, std::nullopt }),
//---- No mapping (valid), SOPS disabled ----
std::make_pair(std::make_tuple(manual_value_t<res_t> { 1920, 1080 }, manual_value_t<fps_t> { 11 }, sops_enabled_t { false }, VALID_ENTRIES), final_values_t { std::nullopt, { { 11, 1 } } }),
std::make_pair(std::make_tuple(no_value_t {}, no_value_t {}, sops_enabled_t { false }, VALID_ENTRIES), final_values_t { std::nullopt, std::nullopt }),
// ---- Invalid requested resolution, SOPS enabled ----
std::make_pair(std::make_tuple(auto_value_t<res_t> { 1920, 1080 }, auto_value_t<fps_t> { 11 }, sops_enabled_t { true }, INVALID_REQ_RES), failed_to_remap_t {}),
std::make_pair(std::make_tuple(auto_value_t<res_t> { 1920, 1080 }, manual_value_t<fps_t> { 11 }, sops_enabled_t { true }, INVALID_REQ_RES), failed_to_remap_t {}),
std::make_pair(std::make_tuple(manual_value_t<res_t> { 1920, 1080 }, auto_value_t<fps_t> { 11 }, sops_enabled_t { true }, INVALID_REQ_RES), final_values_t { { { 1920, 1080 } }, { { 123, 100 } } }),
// ---- Invalid requested resolution, SOPS disabled ----
std::make_pair(std::make_tuple(auto_value_t<res_t> { 1920, 1080 }, auto_value_t<fps_t> { 11 }, sops_enabled_t { false }, INVALID_REQ_RES), failed_to_remap_t {}),
std::make_pair(std::make_tuple(auto_value_t<res_t> { 1920, 1080 }, manual_value_t<fps_t> { 11 }, sops_enabled_t { false }, INVALID_REQ_RES), failed_to_remap_t {}),
std::make_pair(std::make_tuple(manual_value_t<res_t> { 1920, 1080 }, auto_value_t<fps_t> { 11 }, sops_enabled_t { false }, INVALID_REQ_RES), final_values_t { std::nullopt, { { 123, 100 } } }),
// ---- Invalid requested FPS, SOPS enabled ----
std::make_pair(std::make_tuple(auto_value_t<res_t> { 1920, 1080 }, auto_value_t<fps_t> { 11 }, sops_enabled_t { true }, INVALID_REQ_FPS), failed_to_remap_t {}),
std::make_pair(std::make_tuple(auto_value_t<res_t> { 1920, 1080 }, manual_value_t<fps_t> { 11 }, sops_enabled_t { true }, INVALID_REQ_FPS), final_values_t { { { 720, 720 } }, { { 11, 1 } } }),
std::make_pair(std::make_tuple(manual_value_t<res_t> { 1920, 1080 }, auto_value_t<fps_t> { 11 }, sops_enabled_t { true }, INVALID_REQ_FPS), failed_to_remap_t {}),
// ---- Invalid requested FPS, SOPS disabled ----
std::make_pair(std::make_tuple(auto_value_t<res_t> { 1920, 1080 }, auto_value_t<fps_t> { 11 }, sops_enabled_t { false }, INVALID_REQ_FPS), failed_to_remap_t {}),
std::make_pair(std::make_tuple(auto_value_t<res_t> { 1920, 1080 }, manual_value_t<fps_t> { 11 }, sops_enabled_t { false }, INVALID_REQ_FPS), final_values_t { std::nullopt, { { 11, 1 } } }),
std::make_pair(std::make_tuple(manual_value_t<res_t> { 1920, 1080 }, auto_value_t<fps_t> { 11 }, sops_enabled_t { false }, INVALID_REQ_FPS), failed_to_remap_t {}),
// ---- Invalid final resolution, SOPS enabled ----
std::make_pair(std::make_tuple(auto_value_t<res_t> { 1920, 1080 }, auto_value_t<fps_t> { 11 }, sops_enabled_t { true }, INVALID_FINAL_RES), failed_to_remap_t {}),
std::make_pair(std::make_tuple(auto_value_t<res_t> { 1920, 1080 }, manual_value_t<fps_t> { 11 }, sops_enabled_t { true }, INVALID_FINAL_RES), failed_to_remap_t {}),
std::make_pair(std::make_tuple(manual_value_t<res_t> { 1920, 1080 }, auto_value_t<fps_t> { 11 }, sops_enabled_t { true }, INVALID_FINAL_RES), final_values_t { { { 1920, 1080 } }, { { 123, 100 } } }),
// ---- Invalid final resolution, SOPS disabled ----
std::make_pair(std::make_tuple(auto_value_t<res_t> { 1920, 1080 }, auto_value_t<fps_t> { 11 }, sops_enabled_t { false }, INVALID_FINAL_RES), failed_to_remap_t {}),
std::make_pair(std::make_tuple(auto_value_t<res_t> { 1920, 1080 }, manual_value_t<fps_t> { 11 }, sops_enabled_t { false }, INVALID_FINAL_RES), failed_to_remap_t {}),
std::make_pair(std::make_tuple(manual_value_t<res_t> { 1920, 1080 }, auto_value_t<fps_t> { 11 }, sops_enabled_t { false }, INVALID_FINAL_RES), final_values_t { std::nullopt, { { 123, 100 } } }),
// ---- Invalid final refresh rate, SOPS enabled ----
std::make_pair(std::make_tuple(auto_value_t<res_t> { 1920, 1080 }, auto_value_t<fps_t> { 11 }, sops_enabled_t { true }, INVALID_FINAL_REFRESH_RATE), failed_to_remap_t {}),
std::make_pair(std::make_tuple(auto_value_t<res_t> { 1920, 1080 }, manual_value_t<fps_t> { 11 }, sops_enabled_t { true }, INVALID_FINAL_REFRESH_RATE), final_values_t { { { 720, 720 } }, { { 11, 1 } } }),
std::make_pair(std::make_tuple(manual_value_t<res_t> { 1920, 1080 }, auto_value_t<fps_t> { 11 }, sops_enabled_t { true }, INVALID_FINAL_REFRESH_RATE), failed_to_remap_t {}),
// ---- Invalid final refresh rate, SOPS disabled ----
std::make_pair(std::make_tuple(auto_value_t<res_t> { 1920, 1080 }, auto_value_t<fps_t> { 11 }, sops_enabled_t { false }, INVALID_FINAL_REFRESH_RATE), failed_to_remap_t {}),
std::make_pair(std::make_tuple(auto_value_t<res_t> { 1920, 1080 }, manual_value_t<fps_t> { 11 }, sops_enabled_t { false }, INVALID_FINAL_REFRESH_RATE), final_values_t { std::nullopt, { { 11, 1 } } }),
std::make_pair(std::make_tuple(manual_value_t<res_t> { 1920, 1080 }, auto_value_t<fps_t> { 11 }, sops_enabled_t { false }, INVALID_FINAL_REFRESH_RATE), failed_to_remap_t {}),
// ---- Empty req entries, SOPS enabled ----
std::make_pair(std::make_tuple(auto_value_t<res_t> { 1920, 1080 }, auto_value_t<fps_t> { 11 }, sops_enabled_t { true }, EMPTY_REQ_ENTRIES), final_values_t { { { 1024, 720 } }, { { 111, 100 } } }),
std::make_pair(std::make_tuple(auto_value_t<res_t> { 1920, 1080 }, manual_value_t<fps_t> { 11 }, sops_enabled_t { true }, EMPTY_REQ_ENTRIES), final_values_t { { { 720, 720 } }, { { 11, 1 } } }),
std::make_pair(std::make_tuple(manual_value_t<res_t> { 1920, 1080 }, auto_value_t<fps_t> { 11 }, sops_enabled_t { true }, EMPTY_REQ_ENTRIES), final_values_t { { { 1920, 1080 } }, { { 123, 100 } } }),
// ---- Empty req entries, SOPS disabled ----
std::make_pair(std::make_tuple(auto_value_t<res_t> { 1920, 1080 }, auto_value_t<fps_t> { 11 }, sops_enabled_t { false }, EMPTY_REQ_ENTRIES), final_values_t { std::nullopt, { { 11, 1 } } }),
std::make_pair(std::make_tuple(auto_value_t<res_t> { 1920, 1080 }, manual_value_t<fps_t> { 11 }, sops_enabled_t { false }, EMPTY_REQ_ENTRIES), final_values_t { std::nullopt, { { 11, 1 } } }),
std::make_pair(std::make_tuple(manual_value_t<res_t> { 1920, 1080 }, auto_value_t<fps_t> { 11 }, sops_enabled_t { false }, EMPTY_REQ_ENTRIES), final_values_t { std::nullopt, { { 123, 100 } } }),
// ---- Empty final entries, SOPS enabled ----
std::make_pair(std::make_tuple(auto_value_t<res_t> { 1920, 1080 }, auto_value_t<fps_t> { 11 }, sops_enabled_t { true }, EMPTY_FINAL_ENTRIES), failed_to_remap_t {}),
std::make_pair(std::make_tuple(auto_value_t<res_t> { 1920, 1080 }, manual_value_t<fps_t> { 11 }, sops_enabled_t { true }, EMPTY_FINAL_ENTRIES), failed_to_remap_t {}),
std::make_pair(std::make_tuple(manual_value_t<res_t> { 1920, 1080 }, auto_value_t<fps_t> { 11 }, sops_enabled_t { true }, EMPTY_FINAL_ENTRIES), failed_to_remap_t {}),
// ---- Empty final entries, SOPS disabled ----
std::make_pair(std::make_tuple(auto_value_t<res_t> { 1920, 1080 }, auto_value_t<fps_t> { 11 }, sops_enabled_t { false }, EMPTY_FINAL_ENTRIES), failed_to_remap_t {}),
std::make_pair(std::make_tuple(auto_value_t<res_t> { 1920, 1080 }, manual_value_t<fps_t> { 11 }, sops_enabled_t { false }, EMPTY_FINAL_ENTRIES), failed_to_remap_t {}),
std::make_pair(std::make_tuple(manual_value_t<res_t> { 1920, 1080 }, auto_value_t<fps_t> { 11 }, sops_enabled_t { false }, EMPTY_FINAL_ENTRIES), failed_to_remap_t {})));
TEST_P(DisplayModeRemapping, IntegrationTest) {
const auto &[input_value, expected_value] = GetParam();
const auto &[input_res, input_fps, input_enable_sops, input_entries] = input_value;
config::video_t video_config {};
rtsp_stream::launch_session_t session {};
{ // resolution
using enum resolution_option_e;
if (const auto *no_res { std::get_if<no_value_t>(&input_res) }; no_res) {
video_config.dd.resolution_option = disabled;
}
else if (const auto *auto_res { std::get_if<auto_value_t<res_t>>(&input_res) }; auto_res) {
video_config.dd.resolution_option = automatic;
session.width = static_cast<int>(auto_res->value.m_width);
session.height = static_cast<int>(auto_res->value.m_height);
}
else {
const auto [manual_res] = std::get<manual_value_t<res_t>>(input_res);
video_config.dd.resolution_option = manual;
video_config.dd.manual_resolution = std::to_string(manual_res.m_width) + "x"s + std::to_string(manual_res.m_height);
}
}
{ // fps
using enum refresh_rate_option_e;
if (const auto *no_fps { std::get_if<no_value_t>(&input_fps) }; no_fps) {
video_config.dd.refresh_rate_option = disabled;
}
else if (const auto *auto_fps { std::get_if<auto_value_t<fps_t>>(&input_fps) }; auto_fps) {
video_config.dd.refresh_rate_option = automatic;
session.fps = auto_fps->value;
}
else {
const auto [manual_fps] = std::get<manual_value_t<fps_t>>(input_fps);
video_config.dd.refresh_rate_option = manual;
video_config.dd.manual_refresh_rate = std::to_string(manual_fps);
}
}
video_config.dd.configuration_option = config_option_e::verify_only;
video_config.dd.mode_remapping = input_entries;
session.enable_sops = input_enable_sops;
const auto result { display_device::parse_configuration(video_config, session) };
if (const auto *failed_option { std::get_if<failed_to_remap_t>(&expected_value) }; failed_option) {
EXPECT_NO_THROW(std::get<display_device::failed_to_parse_tag_t>(result));
}
else {
const auto &[expected_resolution, expected_refresh_rate] = std::get<final_values_t>(expected_value);
const auto &parsed_config = std::get<display_device::SingleDisplayConfiguration>(result);
EXPECT_EQ(parsed_config.m_resolution, expected_resolution);
EXPECT_EQ(parsed_config.m_refresh_rate, expected_refresh_rate ? std::make_optional(display_device::FloatingPoint { *expected_refresh_rate }) : std::nullopt);
}
}
} // namespace