Skip to content

Commit

Permalink
fix: better handling of multi GPU setups
Browse files Browse the repository at this point in the history
  • Loading branch information
ABeltramo committed Aug 5, 2024
1 parent adbfe7d commit 1c3556f
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 42 deletions.
91 changes: 61 additions & 30 deletions src/moonlight-server/state/configTOML.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include <fstream>
#include <gst/gstelementfactory.h>
#include <gst/gstregistry.h>
#include <platforms/hw.hpp>
#include <range/v3/view.hpp>
#include <runners/docker.hpp>
#include <runners/process.hpp>
Expand Down Expand Up @@ -115,23 +116,8 @@ std::shared_ptr<state::Runner> 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")):
Expand All @@ -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<toml::preserve_comments>(source);
Expand Down Expand Up @@ -218,35 +237,39 @@ Config load_or_default(const std::string &source, const std::shared_ptr<dp::even
GstVideoCfg default_gst_video_settings = toml::find<GstVideoCfg>(cfg, "gstreamer", "video");
GstAudioCfg default_gst_audio_settings = toml::find<GstAudioCfg>(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 */
Expand All @@ -256,14 +279,25 @@ Config load_or_default(const std::string &source, const std::shared_ptr<dp::even
| ranges::views::transform([](const PairedClient &client) { return immer::box<PairedClient>{client}; }) //
| ranges::to<immer::vector<immer::box<PairedClient>>>();

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<std::vector<toml::value>>(cfg, "apps");
auto apps =
cfg_apps | //
ranges::views::enumerate | //
ranges::views::transform([&](std::pair<int, const toml::value &> pair) { //
auto [idx, item] = pair;
auto app_title = toml::find<std::string>(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) +
Expand Down Expand Up @@ -301,16 +335,13 @@ Config load_or_default(const std::string &source, const std::shared_ptr<dp::even
logs::log(logs::warning, "Unknown joypad type: {}", joypad_type);
}

return state::App{.base = {.title = toml::find<std::string>(item, "title"),
return state::App{.base = {.title = app_title,
.id = std::to_string(idx + 1),
.support_hdr = toml::find_or<bool>(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<bool>(item, "start_virtual_compositor", true),
Expand Down
3 changes: 0 additions & 3 deletions src/moonlight-server/state/data-structures.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
33 changes: 28 additions & 5 deletions tests/platforms/linux/nvidia.cpp
Original file line number Diff line number Diff line change
@@ -1,19 +1,39 @@
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_string.hpp>
#include <catch2/matchers/catch_matchers_vector.hpp>
#include <catch2/matchers/catch_matchers_container_properties.hpp>
#include <catch2/matchers/catch_matchers_contains.hpp>
#include <catch2/matchers/catch_matchers_string.hpp>
#include <catch2/matchers/catch_matchers_vector.hpp>
#include <filesystem>
#include <helpers/logger.hpp>
#include <platforms/hw.hpp>

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"));
Expand All @@ -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);
}
4 changes: 0 additions & 4 deletions tests/testMoonlight.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"));

Expand All @@ -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"));
}
Expand Down

0 comments on commit 1c3556f

Please sign in to comment.