diff --git a/src/config.cpp b/src/config.cpp index c320ed6dccf..5ffc12faa55 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -1157,6 +1157,7 @@ namespace config { string_f(vars, "external_ip", nvhttp.external_ip); list_prep_cmd_f(vars, "global_prep_cmd", config::sunshine.prep_cmds); + list_prep_cmd_f(vars, "pre_probe_cmd", config::sunshine.pre_probe_cmds); string_f(vars, "audio_sink", audio.sink); string_f(vars, "virtual_sink", audio.virtual_sink); diff --git a/src/config.h b/src/config.h index e8d1594fba2..cd7718e80dd 100644 --- a/src/config.h +++ b/src/config.h @@ -259,6 +259,7 @@ namespace config { bool notify_pre_releases; bool system_tray; std::vector prep_cmds; + std::vector pre_probe_cmds; }; extern video_t video; diff --git a/src/nvhttp.cpp b/src/nvhttp.cpp index 4d8a87b4f88..b13f6ba0c36 100644 --- a/src/nvhttp.cpp +++ b/src/nvhttp.cpp @@ -810,6 +810,39 @@ namespace nvhttp { } } + /** + * @brief Execute pre-probe commands before encoder probing. + * @param launch_session The session to extract client parameters from (may be null for undo-only). + * @param execute_do If true, run "do" commands; if false, run "undo" commands. + */ + static void run_pre_probe_cmds( + const std::shared_ptr &launch_session, + bool execute_do) { + for (auto &cmd : config::sunshine.pre_probe_cmds) { + auto &cmd_str = execute_do ? cmd.do_cmd : cmd.undo_cmd; + if (cmd_str.empty()) continue; + + boost::process::v1::environment env = boost::this_process::environment(); + if (launch_session) { + env["SUNSHINE_CLIENT_WIDTH"] = std::to_string(launch_session->width); + env["SUNSHINE_CLIENT_HEIGHT"] = std::to_string(launch_session->height); + env["SUNSHINE_CLIENT_FPS"] = std::to_string(launch_session->fps); + env["SUNSHINE_CLIENT_HDR"] = launch_session->enable_hdr ? "true" : "false"; + } + + std::error_code ec; + boost::filesystem::path working_dir("."); + BOOST_LOG(info) << "Executing pre-probe cmd: ["sv << cmd_str << ']'; + auto child = platf::run_command(cmd.elevated, true, cmd_str, working_dir, env, nullptr, ec, nullptr); + if (ec) { + BOOST_LOG(warning) << "Pre-probe cmd failed: "sv << ec.message(); + } + else { + child.wait(); + } + } + } + void launch(bool &host_audio, resp_https_t response, req_https_t request) { print_req(request); @@ -863,6 +896,10 @@ namespace nvhttp { // The display should be restored in case something fails as there are no other sessions. revert_display_configuration = true; + // Run pre-probe commands (e.g. enable virtual display) before anything + // that depends on a connected monitor. + run_pre_probe_cmds(launch_session, true); + // We want to prepare display only if there are no active sessions at // the moment. This should be done before probing encoders as it could // change the active displays. @@ -877,6 +914,7 @@ namespace nvhttp { tree.put("root..status_message", "Failed to initialize video capture/encoding. Is a display connected and turned on?"); tree.put("root.gamesession", 0); + run_pre_probe_cmds(launch_session, false); return; } } @@ -968,6 +1006,10 @@ namespace nvhttp { const auto launch_session = make_launch_session(host_audio, args); if (no_active_sessions) { + // Run pre-probe commands (e.g. enable virtual display) before anything + // that depends on a connected monitor. + run_pre_probe_cmds(launch_session, true); + // We want to prepare display only if there are no active sessions at // the moment. This should be done before probing encoders as it could // change the active displays. @@ -982,6 +1024,7 @@ namespace nvhttp { tree.put("root..status_code", 503); tree.put("root..status_message", "Failed to initialize video capture/encoding. Is a display connected and turned on?"); + run_pre_probe_cmds(launch_session, false); return; } } @@ -1033,10 +1076,17 @@ namespace nvhttp { proc::proc.terminate(); } + // Undo pre-probe commands (e.g. disable virtual display). + run_pre_probe_cmds(nullptr, false); + // The config needs to be reverted regardless of whether "proc::proc.terminate()" was called or not. display_device::revert_configuration(); } + void undo_pre_probe_cmds() { + run_pre_probe_cmds(nullptr, false); + } + void appasset(resp_https_t response, req_https_t request) { print_req(request); diff --git a/src/nvhttp.h b/src/nvhttp.h index 636337071b9..796a406af4c 100644 --- a/src/nvhttp.h +++ b/src/nvhttp.h @@ -52,6 +52,11 @@ namespace nvhttp { */ void start(); + /** + * @brief Run the undo commands of all pre_probe_cmds. + */ + void undo_pre_probe_cmds(); + /** * @brief Setup the nvhttp server. * @param pkey diff --git a/src/platform/linux/kmsgrab.cpp b/src/platform/linux/kmsgrab.cpp index 7859bcd3bb1..e0bbe80fdb0 100644 --- a/src/platform/linux/kmsgrab.cpp +++ b/src/platform/linux/kmsgrab.cpp @@ -34,6 +34,9 @@ namespace fs = std::filesystem; namespace platf { + // Forward declaration: refresh card_descriptors for connector name resolution + std::vector kms_display_names(mem_type_e hwdevice_type); + namespace kms { class cap_sys_admin { @@ -594,6 +597,78 @@ namespace platf { BOOST_LOG(debug) << ss.str(); } + /** + * @brief Resolve a display name to a numeric monitor index. + * + * Accepts either a plain numeric index ("0", "1", ...) or a DRM connector + * name in the form "{type}-{index}" (e.g. "DP-2", "HDMI-A-1"). + * Connector names are matched against the card_descriptors populated by + * kms_display_names(), reusing the same parsing logic as correlate_to_wayland(). + * + * @param display_name The user-provided output_name value. + * @return The resolved monitor index, or -1 on failure. + */ + static int resolve_display_name(const std::string &display_name) { + // If the name is purely numeric, use it directly as a monitor index + bool all_digits = !display_name.empty(); + for (auto c : display_name) { + if (!std::isdigit(static_cast(c))) { + all_digits = false; + break; + } + } + + if (all_digits) { + return util::from_view(display_name); + } + + // Try to parse as connector name: "{type}-{index}" (e.g. "DP-2", "HDMI-A-1") + auto dash_pos = display_name.find_last_of('-'); + if (dash_pos == std::string::npos || dash_pos == 0 || dash_pos == display_name.size() - 1) { + return -1; + } + + auto type_str = std::string_view(display_name).substr(0, dash_pos); + auto index_str = std::string_view(display_name).substr(dash_pos + 1); + + // Verify the suffix is numeric before calling util::from_view() + for (auto c : index_str) { + if (!std::isdigit(static_cast(c))) { + return -1; + } + } + + auto type = kms::from_view(type_str); + auto index = std::max(1, util::from_view(index_str)); + + if (type == DRM_MODE_CONNECTOR_Unknown) { + return -1; + } + + // Search card_descriptors, refreshing once if the connector is not found. + // This handles the case where pre_probe_cmd just enabled a virtual display + // that wasn't present when card_descriptors was initially populated. + for (int attempt = 0; attempt < 2; ++attempt) { + for (auto &cd : card_descriptors) { + for (auto &[crtc_id, mon] : cd.crtc_to_monitor) { + if (mon.type == type && mon.index == index) { + BOOST_LOG(info) << "Resolved connector name '"sv << display_name + << "' to monitor index "sv << mon.monitor_index; + return mon.monitor_index; + } + } + } + + if (attempt == 0) { + BOOST_LOG(info) << "Connector '"sv << display_name + << "' not found in cached descriptors, refreshing display list..."sv; + kms_display_names(mem_type_e::unknown); + } + } + + return -1; + } + class display_t: public platf::display_t { public: display_t(mem_type_e mem_type): @@ -604,7 +679,12 @@ namespace platf { int init(const std::string &display_name, const ::video::config_t &config) { delay = std::chrono::nanoseconds {1s} / config.framerate; - int monitor_index = util::from_view(display_name); + int monitor_index = resolve_display_name(display_name); + if (monitor_index < 0) { + BOOST_LOG(error) << "Could not resolve output name '"sv << display_name + << "'. Use a numeric index (0, 1, 2, ...) or a connector name (DP-1, HDMI-A-1, ...)"sv; + return -1; + } int monitor = 0; fs::path card_dir {"/dev/dri"sv}; diff --git a/src/stream.cpp b/src/stream.cpp index b92e579e7cd..9937cd9c27e 100644 --- a/src/stream.cpp +++ b/src/stream.cpp @@ -26,6 +26,7 @@ extern "C" { #include "input.h" #include "logging.h" #include "network.h" +#include "nvhttp.h" #include "platform/common.h" #include "process.h" #include "stream.h" @@ -1951,6 +1952,9 @@ namespace stream { revert_display_config = true; } + // Undo pre-probe commands (e.g. disable virtual display). + nvhttp::undo_pre_probe_cmds(); + if (revert_display_config) { display_device::revert_configuration(); } diff --git a/src/video.cpp b/src/video.cpp index 7487e1278e6..c99486a0159 100644 --- a/src/video.cpp +++ b/src/video.cpp @@ -1171,6 +1171,14 @@ namespace video { return; } } + + // If output_name is a connector name (e.g. "DP-2") rather than a numeric + // index, it won't match the numeric display_names list. Add it directly + // so display_t::init() can resolve the connector name. + if (!output_name.empty()) { + display_names.emplace_back(output_name); + current_display_index = display_names.size() - 1; + } } }