From 75b14b1e3289565e21f91b8bb5ebc3ed23637077 Mon Sep 17 00:00:00 2001 From: derrod Date: Thu, 3 Oct 2024 19:32:01 +0200 Subject: [PATCH] UI: Show warning on startup if selected encoder is missing --- UI/data/locale/en-US.ini | 26 +++++ UI/window-basic-main-profiles.cpp | 155 ++++++++++++++++++++++++++++++ UI/window-basic-main.cpp | 1 + UI/window-basic-main.hpp | 1 + 4 files changed, 183 insertions(+) diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini index 2f15c5abdd0d92..3e73cc8a9c77e4 100644 --- a/UI/data/locale/en-US.ini +++ b/UI/data/locale/en-US.ini @@ -1600,3 +1600,29 @@ MultitrackVideo.IncompatibleSettings.UpdateAndStartStreaming="Update Settings an MultitrackVideo.IncompatibleSettings.AudioChannels="%1 is not currently compatible with [Audio → General → Channels] set to '%2', %3" MultitrackVideo.IncompatibleSettings.AudioChannelsSingle="[Audio → General → Channels] needs to be set to '%1'" MultitrackVideo.IncompatibleSettings.AudioChannelsMultiple="%1 requires multiple different settings for [Audio → General → Channels]" + +# Missing Encoder Warning +EncoderMissing.Title="Missing Encoders" +EncoderMissing.TextBegin="The following encoders are missing:\n\nSee the Knowledge Base Article for further information." +EncoderMissing.Unknown="Unknown" + +## Nvenc specific errors +EncoderMissing.NVENC.TestProgramFailedStartup="NVENC check program start failure" +EncoderMissing.NVENC.TestProgramExitWithError="NVENC check process failure, exit code: %1" +EncoderMissing.NVENC.TestProgramReadFailure="NVENC check process output read failure, exit code: %1" +EncoderMissing.NVENC.TestProgramError="NVENC check failed with reason: %1 (code: %2)" +EncoderMissing.NVENC.NoDevices="No NVIDIA GPUs found" +EncoderMissing.NVENC.Unsupported.Kepler="Your GPU's (%1) architecture (Kepler) is no longer supported. Please use OBS 30.2 or earlier instead." +EncoderMissing.NVENC.Reason.NvmlLoad="Failed loading NVML library" +EncoderMissing.NVENC.Reason.NvmlInit="Failed initializing NVML" +EncoderMissing.NVENC.Reason.NvencLoad="Failed loading NVENC library" +EncoderMissing.NVENC.Reason.NvencInit="Failed initializing NVENC" +EncoderMissing.NVENC.Reason.NvencVersion="NVENC version empty" +EncoderMissing.NVENC.Reason.CudaLoad="Failed loading CUDA library" +EncoderMissing.NVENC.Reason.CudaInit="Failed initializing CUDA" +EncoderMissing.NVENC.Reason.CudaVersion="Invalid/Missing CUDA version" +EncoderMissing.NVENC.Reason.DriverOutdated="Outdated driver" +EncoderMissing.NVENC.Reason.NoSupportedDevices="No NVIDIA devices with NVENC support found" +EncoderMissing.NVENC.Reason.SessionLimitExceeded="Encoder session limit exceeded" diff --git a/UI/window-basic-main-profiles.cpp b/UI/window-basic-main-profiles.cpp index 559170e5e47853..d6f280b0ea71f9 100644 --- a/UI/window-basic-main-profiles.cpp +++ b/UI/window-basic-main-profiles.cpp @@ -20,13 +20,16 @@ #include #include #include +#include #include +#include #include #include #include #include #include #include + #include "window-basic-main.hpp" #include "window-basic-auto-config.hpp" #include "window-namedialog.hpp" @@ -671,6 +674,7 @@ void OBSBasic::ActivateProfile(const OBSProfile &profile, bool reset) } CheckForSimpleModeX264Fallback(); + CheckForAdvancedModeEncoders(); RefreshProfiles(); @@ -863,3 +867,154 @@ void OBSBasic::CheckForSimpleModeX264Fallback() activeConfiguration.SaveSafe("tmp"); } } + +#ifndef __APPLE__ +/* These may also be useful for QSV/AMF tests so keep them here. */ +static auto args_deleter = [](os_process_args_t *args) { + os_process_args_destroy(args); +}; +using OsProcessArgs = std::unique_ptr; + +static std::optional> CheckNVENCVersion() +{ + const std::vector> nvencFailureReasons = { + {"nvml_lib", "EncoderMissing.NVENC.Reason.NvmlLoad"}, + // Some error messages are a prefix + numeric error code + {"nvml_init", "EncoderMissing.NVENC.Reason.NvmlInit"}, + {"nvenc_lib", "EncoderMissing.NVENC.Reason.NvencLoad"}, + {"nvenc_init", "EncoderMissing.NVENC.Reason.NvencInit"}, + {"cuda_lib", "EncoderMissing.NVENC.Reason.CudaLoad"}, + {"cuda_init", "EncoderMissing.NVENC.Reason.CudaInit"}, + {"no_cuda_version", "EncoderMissing.NVENC.Reason.CudaVersion"}, + {"no_devices", "EncoderMissing.NVENC.NoDevices"}, + {"no_nvenc_version", "EncoderMissing.NVENC.Reason.NvencVersion"}, + {"outdated_driver", "EncoderMissing.NVENC.Reason.DriverOutdated"}, + {"no_supported_devices", "EncoderMissing.NVENC.Reason.NoSupportedDevices"}, + {"session_limit", "EncoderMissing.NVENC.Reason.SessionLimitExceeded"}, + }; + +#ifdef _WIN32 + BPtr test_exe = os_get_executable_path_ptr("obs-nvenc-test.exe"); +#else + BPtr test_exe = os_get_executable_path_ptr("obs-nvenc-test"); +#endif + + OsProcessArgs args{os_process_args_create(test_exe), args_deleter}; + + os_process_pipe_t *pp = os_process_pipe_create2(args.get(), "r"); + if (!pp) + return std::make_pair(u"", QTStr("EncoderMissing.NVENC.TestProgramFailedStartup")); + + std::string caps_result; + for (;;) { + char data[2048]; + size_t len = os_process_pipe_read(pp, (uint8_t *)data, sizeof(data)); + if (!len) + break; + + caps_result.append(data, len); + } + + int exit_code = os_process_pipe_destroy(pp); + + if (caps_result.empty()) + return std::make_pair(u"", QTStr("EncoderMissing.NVENC.TestProgramExitWithError").arg(exit_code)); + + auto config = ConfigFile(); + if (config.OpenString(caps_result.c_str()) != CONFIG_SUCCESS) + return std::make_pair(u"", QTStr("EncoderMissing.NVENC.TestProgramReadFailure").arg(exit_code)); + + /* Read devices and attempt to find reason for failure */ + auto numDevices = config_get_uint(config, "general", "cuda_devices"); + if (!numDevices) + return std::make_pair(u"", QTStr("EncoderMissing.NVENC.NoDevices")); + + /* Check device generation (architecture) */ + for (uint64_t i = 0; i < numDevices; i++) { + std::string section = "device." + std::to_string(i); + + if (!config_has_user_value(config, section.c_str(), "name")) + continue; + + const QString name(config_get_string(config, section.c_str(), "name")); + const std::string_view arch = config_get_string(config, section.c_str(), "architecture_name"); + + if (arch == "Kepler") + return std::make_pair(u"kepler", QTStr("EncoderMissing.NVENC.Unsupported.Kepler").arg(name)); + } + + const std::string_view reason = config_get_string(config, "general", "reason"); + + /* All generic errors */ + if (!reason.empty()) { + QString ret, crumb; + + for (auto &[code, desc] : nvencFailureReasons) { + if (reason.find(code) == std::string_view::npos) + continue; + + ret = QTStr("EncoderMissing.NVENC.TestProgramError").arg(QTStr(desc.data())).arg(code.data()); + crumb = QString::fromUtf8(code.data()); + break; + } + + return std::make_pair(crumb, ret); + } + + return std::nullopt; +} +#endif + +void OBSBasic::CheckForAdvancedModeEncoders() +{ +#ifndef __APPLE__ + constexpr QStringView kbURL = u"https://obsproject.com/kb/encoder-missing#%1"; + const std::string_view curStreamEncoder = config_get_string(activeConfiguration, "AdvOut", "Encoder"); + const std::string_view curRecEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); + std::unordered_set available_encoders; + + size_t idx = 0; + const char *id; + while (obs_enum_encoder_types(idx++, &id)) + available_encoders.insert(id); + + auto is_encoder_missing = + [&](const std::string_view &encoder) -> std::optional> { + if (available_encoders.count(encoder)) + return std::nullopt; + + if (encoder.find("nvenc") != std::string_view::npos) { + if (auto res = CheckNVENCVersion()) + return res; + } + + return std::make_pair(u"", QTStr("EncoderMissing.Unknown")); + }; + + auto streamEncError = is_encoder_missing(curStreamEncoder); + auto recEncError = curStreamEncoder == curRecEncoder ? std::nullopt : is_encoder_missing(curRecEncoder); + + // Show warning message + if (streamEncError || recEncError) { + QString encoderMissingMessage = QTStr("EncoderMissing.TextBegin"); + QStringView crumb; + + if (streamEncError) { + encoderMissingMessage += QTStr("EncoderMissing.ListItem") + .arg(curStreamEncoder.data()) + .arg(streamEncError->second); + crumb = streamEncError->first; + } + if (recEncError) { + encoderMissingMessage += + QTStr("EncoderMissing.ListItem").arg(curRecEncoder.data()).arg(recEncError->second); + if (crumb.isEmpty()) + crumb = recEncError->first; + } + + encoderMissingMessage += QTStr("EncoderMissing.TextEnd").arg(kbURL.arg(crumb)); + + OBSMessageBox::warning(this, QTStr("EncoderMissing.Title"), encoderMissingMessage, true); + } +#endif +} diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp index 0616a5fa9be64d..b0ec09f1b2c83d 100644 --- a/UI/window-basic-main.cpp +++ b/UI/window-basic-main.cpp @@ -2122,6 +2122,7 @@ void OBSBasic::OBSInit() InitBasicConfigDefaults2(); CheckForSimpleModeX264Fallback(); + CheckForAdvancedModeEncoders(); LogEncoders(); diff --git a/UI/window-basic-main.hpp b/UI/window-basic-main.hpp index d04edfeff88b49..4a876a76623346 100644 --- a/UI/window-basic-main.hpp +++ b/UI/window-basic-main.hpp @@ -1304,6 +1304,7 @@ public slots: std::vector GetRestartRequirements(const ConfigFile &config) const; void ResetProfileData(); void CheckForSimpleModeX264Fallback(); + void CheckForAdvancedModeEncoders(); public: inline const OBSProfileCache &GetProfileCache() const noexcept { return profiles; };