From 1c3556f19578bd659101b127ddb2d0008ab4912e Mon Sep 17 00:00:00 2001 From: ABeltramo Date: Mon, 5 Aug 2024 17:46:38 +0100 Subject: [PATCH] fix: better handling of multi GPU setups --- src/moonlight-server/state/configTOML.cpp | 91 +++++++++++++------ .../state/data-structures.hpp | 3 - tests/platforms/linux/nvidia.cpp | 33 ++++++- tests/testMoonlight.cpp | 4 - 4 files changed, 89 insertions(+), 42 deletions(-) diff --git a/src/moonlight-server/state/configTOML.cpp b/src/moonlight-server/state/configTOML.cpp index dca0a625..c10c61ce 100644 --- a/src/moonlight-server/state/configTOML.cpp +++ b/src/moonlight-server/state/configTOML.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -115,23 +116,8 @@ std::shared_ptr get_runner(const toml::value &item, const std::sh } } -static bool is_available(const GstEncoder &settings) { - if (auto plugin = gst_registry_find_plugin(gst_registry_get(), settings.plugin_name.c_str())) { - gst_object_unref(plugin); - return std::all_of(settings.check_elements.begin(), settings.check_elements.end(), [](const auto &el_name) { - if (auto el = gst_element_factory_make(el_name.c_str(), nullptr)) { - gst_object_unref(el); - return true; - } else { - return false; - } - }); - } - return false; -} - -static state::Encoder encoder_type(const std::string &gstreamer_plugin_name) { - switch (utils::hash(gstreamer_plugin_name)) { +static state::Encoder encoder_type(const GstEncoder &settings) { + switch (utils::hash(settings.plugin_name)) { case (utils::hash("nvcodec")): return NVIDIA; case (utils::hash("vaapi")): @@ -146,10 +132,43 @@ static state::Encoder encoder_type(const std::string &gstreamer_plugin_name) { case (utils::hash("aom")): return SOFTWARE; } - logs::log(logs::warning, "Unrecognised Gstreamer plugin name: {}", gstreamer_plugin_name); + logs::log(logs::warning, "Unrecognised Gstreamer plugin name: {}", settings.plugin_name); return UNKNOWN; } +static bool is_available(const GPU_VENDOR &gpu_vendor, const GstEncoder &settings) { + if (auto plugin = gst_registry_find_plugin(gst_registry_get(), settings.plugin_name.c_str())) { + gst_object_unref(plugin); + return std::all_of( + settings.check_elements.begin(), + settings.check_elements.end(), + [settings, gpu_vendor](const auto &el_name) { + // Can we instantiate the plugin? + if (auto el = gst_element_factory_make(el_name.c_str(), nullptr)) { + gst_object_unref(el); + // Is the selected GPU vendor compatible with the encoder? + // (Particularly useful when using multiple GPUs, e.g. nvcodec might be available but user + // wants to encode using the Intel GPU) + auto encoder_vendor = encoder_type(settings); + if (encoder_vendor == NVIDIA && gpu_vendor != GPU_VENDOR::NVIDIA) { + logs::log(logs::debug, "Skipping NVIDIA encoder, not a NVIDIA GPU ({})", (int)gpu_vendor); + return false; + } else if (encoder_vendor == VAAPI && (gpu_vendor != GPU_VENDOR::INTEL && gpu_vendor != GPU_VENDOR::AMD)) { + logs::log(logs::debug, "Skipping VAAPI encoder, not an Intel or AMD GPU ({})", (int)gpu_vendor); + return false; + } else if (encoder_vendor == QUICKSYNC && gpu_vendor != GPU_VENDOR::INTEL) { + logs::log(logs::debug, "Skipping QUICKSYNC encoder, not an Intel GPU ({})", (int)gpu_vendor); + return false; + } + return true; + } else { + return false; + } + }); + } + return false; +} + toml::value v1_to_v2(const toml::value &v1, const std::string &source) { create_default(source); auto v2 = toml::parse(source); @@ -218,35 +237,39 @@ Config load_or_default(const std::string &source, const std::shared_ptr(cfg, "gstreamer", "video"); GstAudioCfg default_gst_audio_settings = toml::find(cfg, "gstreamer", "audio"); + std::string default_app_render_node = utils::get_env("WOLF_RENDER_NODE", "/dev/dri/renderD128"); + auto vendor = get_vendor(default_app_render_node); + auto default_is_available = std::bind(is_available, vendor, std::placeholders::_1); + /* Automatic pick best H264 encoder */ auto h264_encoder = std::find_if(default_gst_video_settings.h264_encoders.begin(), default_gst_video_settings.h264_encoders.end(), - is_available); + default_is_available); if (h264_encoder == std::end(default_gst_video_settings.h264_encoders)) { throw std::runtime_error("Unable to find a compatible H264 encoder, please check [[gstreamer.video.h264_encoders]] " "in your config.toml or your Gstreamer installation"); } - logs::log(logs::info, "Selected H264 encoder: {}", h264_encoder->plugin_name); + logs::log(logs::info, "Default H264 encoder: {}", h264_encoder->plugin_name); /* Automatic pick best HEVC encoder */ auto hevc_encoder = std::find_if(default_gst_video_settings.hevc_encoders.begin(), default_gst_video_settings.hevc_encoders.end(), - is_available); + default_is_available); if (hevc_encoder == std::end(default_gst_video_settings.hevc_encoders)) { throw std::runtime_error("Unable to find a compatible HEVC encoder, please check [[gstreamer.video.hevc_encoders]] " "in your config.toml or your Gstreamer installation"); } - logs::log(logs::info, "Selected HEVC encoder: {}", hevc_encoder->plugin_name); + logs::log(logs::info, "Default HEVC encoder: {}", hevc_encoder->plugin_name); /* Automatic pick best AV1 encoder */ auto av1_encoder = std::find_if(default_gst_video_settings.av1_encoders.begin(), default_gst_video_settings.av1_encoders.end(), - is_available); + default_is_available); if (support_av1 && av1_encoder == std::end(default_gst_video_settings.av1_encoders)) { throw std::runtime_error("Unable to find a compatible AV1 encoder, please check [[gstreamer.video.av1_encoders]] " "in your config.toml or your Gstreamer installation"); } else if (support_av1) { - logs::log(logs::info, "Selected AV1 encoder: {}", av1_encoder->plugin_name); + logs::log(logs::info, "Default AV1 encoder: {}", av1_encoder->plugin_name); } /* Get paired clients */ @@ -256,7 +279,6 @@ Config load_or_default(const std::string &source, const std::shared_ptr{client}; }) // | ranges::to>>(); - std::string default_app_render_node = utils::get_env("WOLF_RENDER_NODE", "/dev/dri/renderD128"); /* Get apps, here we'll merge the default gstreamer settings with the app specific overrides */ auto cfg_apps = toml::find>(cfg, "apps"); auto apps = @@ -264,6 +286,18 @@ Config load_or_default(const std::string &source, const std::shared_ptr pair) { // auto [idx, item] = pair; + auto app_title = toml::find(item, "title"); + auto app_render_node = toml::find_or(item, "render_node", default_app_render_node); + auto app_gpu_vendor = get_vendor(app_render_node); + if (app_gpu_vendor != vendor) { + logs::log(logs::warning, + "App {} render node GPU vendor ({}) doesn't match the default GPU vendor ({})", + app_title, + (int)app_gpu_vendor, + (int)vendor); + // TODO: allow user to override the default GPU vendor for the gstreamer pipeline + } + auto h264_gst_pipeline = toml::find_or(item, "video", "source", default_gst_video_settings.default_source) + " ! " + toml::find_or(item, "video", "video_params", h264_encoder->video_params) + " ! " + toml::find_or(item, "video", "h264_encoder", h264_encoder->encoder_pipeline) + @@ -301,16 +335,13 @@ Config load_or_default(const std::string &source, const std::shared_ptr(item, "title"), + return state::App{.base = {.title = app_title, .id = std::to_string(idx + 1), .support_hdr = toml::find_or(item, "support_hdr", false)}, .h264_gst_pipeline = h264_gst_pipeline, - .h264_encoder = encoder_type(h264_encoder->plugin_name), .hevc_gst_pipeline = hevc_gst_pipeline, - .hevc_encoder = encoder_type(hevc_encoder->plugin_name), .av1_gst_pipeline = av1_gst_pipeline, - .av1_encoder = support_av1 ? encoder_type(av1_encoder->plugin_name) : UNKNOWN, - .render_node = toml::find_or(item, "render_node", default_app_render_node), + .render_node = app_render_node, .opus_gst_pipeline = opus_gst_pipeline, .start_virtual_compositor = toml::find_or(item, "start_virtual_compositor", true), diff --git a/src/moonlight-server/state/data-structures.hpp b/src/moonlight-server/state/data-structures.hpp index 170fa2aa..55b000fc 100644 --- a/src/moonlight-server/state/data-structures.hpp +++ b/src/moonlight-server/state/data-structures.hpp @@ -102,11 +102,8 @@ struct App { moonlight::App base; std::string h264_gst_pipeline; - Encoder h264_encoder; std::string hevc_gst_pipeline; - Encoder hevc_encoder; std::string av1_gst_pipeline; - Encoder av1_encoder; std::string render_node; diff --git a/tests/platforms/linux/nvidia.cpp b/tests/platforms/linux/nvidia.cpp index 6c1537de..a074cc51 100644 --- a/tests/platforms/linux/nvidia.cpp +++ b/tests/platforms/linux/nvidia.cpp @@ -1,19 +1,39 @@ #include -#include -#include #include #include +#include +#include +#include +#include #include using Catch::Matchers::Contains; +using Catch::Matchers::ContainsSubstring; using Catch::Matchers::Equals; using Catch::Matchers::SizeIs; +std::string get_nvidia_render_device() { + // iterate over /dev/dri/renderD12* devices + for (const auto &entry : std::filesystem::directory_iterator("/dev/dri")) { + if (entry.path().filename().string().find("renderD12") != std::string::npos) { + auto vendor = get_vendor(entry.path().string()); + logs::log(logs::info, "Found {} with vendor {}", entry.path().string(), (int)vendor); + if (vendor == NVIDIA) { + return entry.path().string(); + } + } + } + return ""; +} + TEST_CASE("libdrm find linked devices", "[NVIDIA]") { - auto devices = linked_devices("/dev/dri/renderD128"); + std::string nvidia_node = get_nvidia_render_device(); + REQUIRE(!nvidia_node.empty()); // If there's no nvidia node, we can't continue + + auto devices = linked_devices(nvidia_node); REQUIRE_THAT(devices, SizeIs(6)); - REQUIRE_THAT(devices, Contains("/dev/dri/card0")); + REQUIRE_THAT(devices, Contains(ContainsSubstring("/dev/dri/card"))); REQUIRE_THAT(devices, Contains("/dev/nvidia0")); REQUIRE_THAT(devices, Contains("/dev/nvidia-modeset")); REQUIRE_THAT(devices, Contains("/dev/nvidia-uvm")); @@ -25,7 +45,10 @@ TEST_CASE("libdrm find linked devices", "[NVIDIA]") { } TEST_CASE("libpci get vendor", "[NVIDIA]") { - REQUIRE(get_vendor("/dev/dri/renderD128") == NVIDIA); + std::string nvidia_node = get_nvidia_render_device(); + REQUIRE(!nvidia_node.empty()); // If there's no nvidia node, we can't continue + + REQUIRE(get_vendor(nvidia_node) == NVIDIA); REQUIRE(get_vendor("/dev/dri/a_non_existing_thing") == UNKNOWN); REQUIRE(get_vendor("software") == UNKNOWN); } \ No newline at end of file diff --git a/tests/testMoonlight.cpp b/tests/testMoonlight.cpp index e48f478e..79360d77 100644 --- a/tests/testMoonlight.cpp +++ b/tests/testMoonlight.cpp @@ -34,8 +34,6 @@ TEST_CASE("LocalState load TOML", "[LocalState]") { REQUIRE_THAT(first_app.hevc_gst_pipeline, Equals("video_source ! params ! hevc_pipeline ! video_sink")); REQUIRE(first_app.joypad_type == moonlight::control::pkts::CONTROLLER_TYPE::AUTO); REQUIRE(first_app.start_virtual_compositor); - REQUIRE(first_app.hevc_encoder == state::UNKNOWN); - REQUIRE(first_app.h264_encoder == state::UNKNOWN); REQUIRE(first_app.render_node == "/dev/dri/renderD128"); REQUIRE_THAT(toml::find(first_app.runner->serialise(), "type").as_string(), Equals("docker")); @@ -46,8 +44,6 @@ TEST_CASE("LocalState load TOML", "[LocalState]") { REQUIRE_THAT(second_app.hevc_gst_pipeline, Equals("override DEFAULT SOURCE ! params ! hevc_pipeline ! video_sink")); REQUIRE(!second_app.start_virtual_compositor); REQUIRE(second_app.joypad_type == moonlight::control::pkts::CONTROLLER_TYPE::XBOX); - REQUIRE(second_app.hevc_encoder == state::UNKNOWN); - REQUIRE(second_app.h264_encoder == state::UNKNOWN); REQUIRE(second_app.render_node == "/tmp/dead_beef"); REQUIRE_THAT(toml::find(second_app.runner->serialise(), "type").as_string(), Equals("process")); }