From e1d2cf705dd9e8e36f1d2360c443e6bb49dbbe0e Mon Sep 17 00:00:00 2001 From: Alex Luccisano Date: Wed, 20 Nov 2024 14:17:58 -0500 Subject: [PATCH] frontend: Support enhanced broadcasting on Linux Enhanced broadcasting requires system information to be gathered on the client and submitted to the GetClientConfiguration request in order to obtain a valid response from the server. This commit adds support for gathering the required information on Linux-based systems. --- frontend/cmake/os-linux.cmake | 3 + frontend/utility/system-info-posix.cpp | 668 ++++++++++++++++++++++++- 2 files changed, 670 insertions(+), 1 deletion(-) diff --git a/frontend/cmake/os-linux.cmake b/frontend/cmake/os-linux.cmake index a32a3312e..91e8bbad2 100644 --- a/frontend/cmake/os-linux.cmake +++ b/frontend/cmake/os-linux.cmake @@ -5,6 +5,9 @@ target_compile_definitions( ) target_link_libraries(obs-studio PRIVATE Qt::GuiPrivate Qt::DBus) +find_package(Libpci REQUIRED) +target_link_libraries(obs-studio PRIVATE Libpci::pci) + if(TARGET OBS::python) find_package(Python REQUIRED COMPONENTS Interpreter Development) target_link_libraries(obs-studio PRIVATE Python::Python) diff --git a/frontend/utility/system-info-posix.cpp b/frontend/utility/system-info-posix.cpp index 00affc68e..474830cfc 100644 --- a/frontend/utility/system-info-posix.cpp +++ b/frontend/utility/system-info-posix.cpp @@ -1,6 +1,672 @@ #include "system-info.hpp" +#include "util/dstr.hpp" +#include "util/platform.h" +#include +#include +#include +#include +#include + +using namespace std; + +extern "C" { +#include "pci/pci.h" +} + +// Anonymous namespace ensures internal linkage +namespace { +struct drm_card_info { + uint16_t vendor_id; + uint16_t device_id; + uint16_t subsystem_vendor; + uint16_t subsystem_device; + + /* match_count is the number of matches between + * the tokenized GPU identification string and + * the device_name and vendor_name supplied by + * libpci. It is used to associate the GPU that + * OBS is using and the cards found in a bus scan + * using libpci. + */ + uint16_t match_count; + + /* The following 2 fields are easily + * obtained for AMD GPUs. Reporting + * for NVIDIA GPUs is not working at + * the moment. + */ + uint64_t dedicated_vram_total; + uint64_t shared_system_memory_total; + + // PCI domain:bus:slot:function + std::string dbsf; + std::string device_name; + std::string vendor_name; +}; + +constexpr std::string_view WHITE_SPACE = " \f\n\r\t\v"; + +// trim_ws() will remove leading and trailing +// white space from the string. +void trim_ws(std::string &s) +{ + // Trim leading whitespace + size_t pos = s.find_first_not_of(WHITE_SPACE); + if (pos != std::string::npos) + s = s.substr(pos); + + // Trim trailing whitespace + pos = s.find_last_not_of(WHITE_SPACE); + if (pos != std::string::npos) + s = s.substr(0, pos + 1); +} + +bool compare_match_strength(const drm_card_info &a, const drm_card_info &b) +{ + return a.match_count > b.match_count; +} + +void adjust_gpu_model(std::string &model) +{ + /* Use the sub-string between the [] brackets. For example, + * the NVIDIA Quadro P4000 model string from PCI ID database + * is "GP104GL [Quadro P4000]", and we only want the "Quadro + * P4000" sub-string. + */ + size_t first = model.find('['); + size_t last = model.find_last_of(']'); + if ((last - first) > 1) { + std::string adjusted_model = model.substr(first + 1, last - first - 1); + model.assign(adjusted_model); + } +} + +bool get_distribution_info(std::string &distro, std::string &version) +{ + ifstream file; + std::string line; + const std::string flatpak_file = "/.flatpak-info"; + const std::string systemd_file = "/etc/os-release"; + + distro = ""; + version = ""; + + if (std::filesystem::exists(flatpak_file)) { + /* The .flatpak-info file has a line of the form: + * + * runtime=runtime/org.kde.Platform/x86_64/6.6 + * + * Parse the line into tokens to identify the name and + * version, which are "org.kde.Platform" and "6.6" respectively, + * in the example above. + */ + file.open(flatpak_file); + if (file.is_open()) { + while (getline(file, line)) { + if (line.compare(0, 16, "runtime=runtime/") == 0) { + size_t pos = line.find('/'); + if (pos != string::npos && line.at(pos + 1) != '\0') { + line.erase(0, pos + 1); + + /* Split the string into tokens with a regex + * of one or more '/' characters'. + */ + std::regex fp_reg("[/]+"); + vector fp_tokens( + sregex_token_iterator(line.begin(), line.end(), fp_reg, -1), + sregex_token_iterator()); + if (fp_tokens.size() >= 2) { + auto token = begin(fp_tokens); + distro = "Flatpak " + *token; + token = next(fp_tokens.end(), -1); + version = *token; + } else { + distro = "Flatpak unknown"; + version = "0"; + blog(LOG_DEBUG, + "%s: Format of 'runtime' entry unrecognized in file %s", + __FUNCTION__, flatpak_file.c_str()); + } + break; + } + } + } + file.close(); + } + } else if (std::filesystem::exists(systemd_file)) { + /* systemd-based distributions use /etc/os-release to identify + * the OS. For example, the Ubuntu 24.04.1 variant looks like: + * + * PRETTY_NAME="Ubuntu 24.04.1 LTS" + * NAME="Ubuntu" + * VERSION_ID="24.04" + * VERSION="24.04.1 LTS (Noble Numbat)" + * VERSION_CODENAME=noble + * ID=ubuntu + * ID_LIKE=debian + * HOME_URL="https://www.ubuntu.com/" + * SUPPORT_URL="https://help.ubuntu.com/" + * BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" + * PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy" + * UBUNTU_CODENAME=noble + * LOGO=ubuntu-logo + * + * Parse the file looking for the NAME and VERSION_ID fields. + * Note that some distributions wrap the entry in '"' characters + * while others do not, so we need to remove those characters. + */ + file.open(systemd_file); + if (file.is_open()) { + while (getline(file, line)) { + if (line.compare(0, 4, "NAME") == 0) { + size_t pos = line.find('='); + if (pos != std::string::npos && line.at(pos + 1) != '\0') { + distro = line.substr(pos + 1); + + // Remove the '"' characters from the string, if any. + distro.erase(std::remove(distro.begin(), distro.end(), '"'), + distro.end()); + + trim_ws(distro); + continue; + } + } + + if (line.compare(0, 10, "VERSION_ID") == 0) { + size_t pos = line.find('='); + if (pos != std::string::npos && line.at(pos + 1) != '\0') { + version = line.substr(pos + 1); + + // Remove the '"' characters from the string, if any. + version.erase(std::remove(version.begin(), version.end(), '"'), + version.end()); + + trim_ws(version); + continue; + } + } + } + file.close(); + } + } else { + blog(LOG_DEBUG, "%s: Failed to find host OS or flatpak info", __FUNCTION__); + return false; + } + + return true; +} + +bool get_cpu_name(optional &proc_name) +{ + ifstream file("/proc/cpuinfo"); + std::string line; + int physical_id = -1; + bool found_name = false; + + /* Initialize processor name. Some ARM-based hosts do + * not output the "model name" field in /proc/cpuinfo. + */ + proc_name = "Unavailable"; + + if (file.is_open()) { + while (getline(file, line)) { + if (line.compare(0, 10, "model name") == 0) { + size_t pos = line.find(':'); + if (pos != std::string::npos && line.at(pos + 1) != '\0') { + proc_name = line.substr(pos + 1); + trim_ws((std::string &)proc_name); + found_name = true; + continue; + } + } + + if (line.compare(0, 11, "physical id") == 0) { + size_t pos = line.find(':'); + if (pos != std::string::npos && line.at(pos + 1) != '\0') { + physical_id = atoi(&line[pos + 1]); + if (physical_id == 0 && found_name) + break; + } + } + } + file.close(); + } + return true; +} + +bool get_cpu_freq(uint32_t &cpu_freq) +{ + ifstream freq_file; + std::string line; + + /* Look for the sysfs tree "base_frequency" first. + * Intel exports "base_frequency, AMD does not. + * If not found, use "cpuinfo_max_freq". + */ + freq_file.open("/sys/devices/system/cpu/cpu0/cpufreq/base_frequency"); + if (!freq_file.is_open()) { + freq_file.open("/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq"); + if (!freq_file.is_open()) + return false; + } + + if (getline(freq_file, line)) { + trim_ws(line); + // Convert the CPU frequency string to an integer in MHz + cpu_freq = atoi(line.c_str()) / 1000; + } else { + cpu_freq = 0; + } + freq_file.close(); + + return true; +} + +/* get_drm_cards() will find all render-capable cards + * in /sys/class/drm and populate a vector of drm_card_info. + */ +void get_drm_cards(std::vector &cards) +{ + struct drm_card_info dci; + char *file_str = NULL; + char buf[256]; + int len = 0; + uint val = 0; + + std::string base_path = "/sys/class/drm/"; + std::string drm_path = base_path; + size_t base_len = base_path.length(); + size_t node_len = 0; + + os_dir_t *dir = os_opendir(base_path.c_str()); + struct os_dirent *ent; + + while ((ent = os_readdir(dir)) != NULL) { + std::string entry_name = ent->d_name; + if (ent->directory && (entry_name.find("renderD") != std::string::npos)) { + dci = {0}; + drm_path.resize(base_len); + drm_path += ent->d_name; + drm_path += "/device"; + + /* Get the PCI D:B:S:F (domain:bus:slot:function) in string form + * by reading the device symlink. + */ + if ((len = readlink(drm_path.c_str(), buf, sizeof(buf) - 1)) != -1) { + // readlink() doesn't null terminate strings, so do it explicitly + buf[len] = '\0'; + dci.dbsf = buf; + + /* The DBSF string is of the form: "../../../0000:65:00.0/". + * Remove all the '/' characters, and + * remove all the leading '.' characters. + */ + dci.dbsf.erase(std::remove(dci.dbsf.begin(), dci.dbsf.end(), '/'), dci.dbsf.end()); + dci.dbsf.erase(0, dci.dbsf.find_first_not_of(".")); + + node_len = drm_path.length(); + + // Get the device_id + drm_path += "/device"; + file_str = os_quick_read_utf8_file(drm_path.c_str()); + if (!file_str) { + blog(LOG_ERROR, "Could not read from '%s'", drm_path.c_str()); + dci.device_id = 0; + } else { + // Skip over the "0x" and convert + sscanf(file_str + 2, "%x", &val); + dci.device_id = val; + bfree(file_str); + } + + // Get the vendor_id + drm_path.resize(node_len); + drm_path += "/vendor"; + file_str = os_quick_read_utf8_file(drm_path.c_str()); + if (!file_str) { + blog(LOG_ERROR, "Could not read from '%s'", drm_path.c_str()); + dci.vendor_id = 0; + } else { + // Skip over the "0x" and convert + sscanf(file_str + 2, "%x", &val); + dci.vendor_id = val; + bfree(file_str); + } + + // Get the subsystem_vendor + drm_path.resize(node_len); + drm_path += "/subsystem_vendor"; + file_str = os_quick_read_utf8_file(drm_path.c_str()); + if (!file_str) { + dci.subsystem_vendor = 0; + } else { + // Skip over the "0x" and convert + sscanf(file_str + 2, "%x", &val); + dci.subsystem_vendor = val; + bfree(file_str); + } + + // Get the subsystem_device + drm_path.resize(node_len); + drm_path += "/subsystem_device"; + file_str = os_quick_read_utf8_file(drm_path.c_str()); + if (!file_str) { + dci.subsystem_device = 0; + } else { + // Skip over the "0x" and convert + sscanf(file_str + 2, "%x", &val); + dci.subsystem_device = val; + bfree(file_str); + } + + /* The amdgpu driver exports the GPU memory information + * via sysfs nodes. Sadly NVIDIA doesn't have the same + * information via sysfs. Read the amdgpu-based nodes + * if present and get the required fields. + * + * Get the GPU total dedicated VRAM, if available + */ + drm_path.resize(node_len); + drm_path += "/mem_info_vram_total"; + file_str = os_quick_read_utf8_file(drm_path.c_str()); + if (!file_str) { + dci.dedicated_vram_total = 0; + } else { + sscanf(file_str, "%lu", &dci.dedicated_vram_total); + bfree(file_str); + } + + // Get the GPU total shared system memory, if available + drm_path.resize(node_len); + drm_path += "/mem_info_gtt_total"; + file_str = os_quick_read_utf8_file(drm_path.c_str()); + if (!file_str) { + dci.shared_system_memory_total = 0; + } else { + sscanf(file_str, "%lu", &dci.shared_system_memory_total); + bfree(file_str); + } + + cards.push_back(dci); + blog(LOG_DEBUG, + "%s: drm_adapter_info: PCI B:D:S:F: %s, device_id:0x%x," + "vendor_id:0x%x, sub_device:0x%x, sub_vendor:0x%x," + "vram_total: %lu, sys_memory: %lu", + __FUNCTION__, dci.dbsf.c_str(), dci.device_id, dci.vendor_id, dci.subsystem_device, + dci.subsystem_vendor, dci.dedicated_vram_total, dci.shared_system_memory_total); + } else { + blog(LOG_DEBUG, "%s: Failed to read symlink for %s", __FUNCTION__, drm_path.c_str()); + } + } + } + os_closedir(dir); +} + +/* system_gpu_data() returns a sorted vector of GoLiveApi::Gpu + * objects needed to build the multitrack video request. + * + * When a single GPU is installed the case is trivial. In systems + * with multiple GPUs (including the CPU iGPU), the GPU that + * OBS is currently using must be placed first in the list, so + * that composition_gpu_index=0 is valid. composition_gpu_index + * is set to to ovi.adapter which is always 0 apparently. + * + * system_gpu_data() does the following: + * 1. Gather information about the GPU being used by OBS + * 2. Scan the PCIe bus to identify all GPUs + * 3. Find a best match of the GPU in-use in the list found in 2 + * 4. Generate the sorted list of GoLiveAPI::Gpu entries + * + * Note that the PCIe device_id and vendor_id are not available + * via OpenGL calls, hence the need to match the in-use GPU with + * the libpci scanned results and extract the PCIe information. + */ +std::optional> system_gpu_data() +{ + std::vector adapter_info; + GoLiveApi::Gpu gpu; + std::string gs_driver_version; + std::string gs_device_renderer; + uint64_t dedicated_video_memory = 0; + uint64_t shared_system_memory = 0; + + std::vector drm_cards; + struct pci_access *pacc; + pacc = pci_alloc(); + pci_init(pacc); + char slot[256]; + + // Obtain GPU information by querying graphics API + obs_enter_graphics(); + gs_driver_version = gs_get_driver_version(); + gs_device_renderer = gs_get_renderer(); + dedicated_video_memory = gs_get_gpu_dmem() * 1024; + shared_system_memory = gs_get_gpu_smem() * 1024; + obs_leave_graphics(); + + /* Split the GPU renderer string into tokens with + * a regex of one or more white-space characters. + */ + std::regex ws_reg("[\\s]+"); + vector gpu_tokens(sregex_token_iterator(gs_device_renderer.begin(), gs_device_renderer.end(), + ws_reg, -1), + sregex_token_iterator()); + + // Remove extraneous characters from the tokens + constexpr std::string_view EXTRA_CHARS = ",()[]{}"; + for (auto token = begin(gpu_tokens); token != end(gpu_tokens); ++token) { + for (unsigned int i = 0; i < EXTRA_CHARS.size(); ++i) { + token->erase(std::remove(token->begin(), token->end(), EXTRA_CHARS[i]), token->end()); + } + // Convert tokens to lower-case + std::transform(token->begin(), token->end(), token->begin(), ::tolower); + blog(LOG_DEBUG, "%s: gpu_token: '%s'", __FUNCTION__, token->c_str()); + } + + // Scan the PCI bus once + pci_scan_bus(pacc); + + // Discover the set of DRM render-capable GPUs + get_drm_cards(drm_cards); + blog(LOG_DEBUG, "Number of GPUs detected: %lu", drm_cards.size()); + + // Iterate through drm_cards to get the device and vendor names via libpci + for (auto card = begin(drm_cards); card != end(drm_cards); ++card) { + struct pci_dev *pdev; + struct pci_filter pfilter; + char namebuf[1024]; + + /* Get around the 'const char*' vs 'char*' + * type issue with pci_filter_parse_slot(). + */ + strncpy(slot, card->dbsf.c_str(), sizeof(slot)); + + // Validate the "slot" string according to libpci + pci_filter_init(pacc, &pfilter); + if (pci_filter_parse_slot(&pfilter, slot)) { + blog(LOG_DEBUG, "%s: pci_filter_parse_slot() failed", __FUNCTION__); + continue; + } + + // Get the device name and vendor name from libpci + for (pdev = pacc->devices; pdev; pdev = pdev->next) { + if (pci_filter_match(&pfilter, pdev)) { + pci_fill_info(pdev, PCI_FILL_IDENT); + card->device_name = + pci_lookup_name(pacc, namebuf, sizeof(namebuf), + PCI_LOOKUP_DEVICE | PCI_LOOKUP_CACHE | PCI_LOOKUP_NETWORK, + pdev->vendor_id, pdev->device_id); + card->vendor_name = + pci_lookup_name(pacc, namebuf, sizeof(namebuf), + PCI_LOOKUP_VENDOR | PCI_LOOKUP_CACHE | PCI_LOOKUP_NETWORK, + pdev->vendor_id, pdev->device_id); + blog(LOG_DEBUG, "libpci lookup: device_name: %s, vendor_name: %s", + card->device_name.c_str(), card->vendor_name.c_str()); + break; + } + } + } + pci_cleanup(pacc); + + /* Iterate through drm_cards to determine a string match count + * against the GPU string tokens from the OpenGL identification. + */ + for (auto card = begin(drm_cards); card != end(drm_cards); ++card) { + card->match_count = 0; + for (auto token = begin(gpu_tokens); token != end(gpu_tokens); ++token) { + std::string card_device_name = card->device_name; + std::string card_gpu_vendor = card->vendor_name; + std::transform(card_device_name.begin(), card_device_name.end(), card_device_name.begin(), + ::tolower); + std::transform(card_gpu_vendor.begin(), card_gpu_vendor.end(), card_gpu_vendor.begin(), + ::tolower); + + // Compare GPU string tokens to PCI device name + std::size_t found = card_device_name.find(*token); + if (found != std::string::npos) { + card->match_count++; + blog(LOG_DEBUG, "Found %s in PCI device name", (*token).c_str()); + } + // Compare GPU string tokens to PCI vendor name + found = card_gpu_vendor.find(*token); + if (found != std::string::npos) { + card->match_count++; + blog(LOG_DEBUG, "Found %s in PCI vendor name", (*token).c_str()); + } + } + } + + /* Sort the cards based on the highest match strength. + * In the case of multiple cards and the first one is not a higher + * match, there is ambiguity and all we can do is log a warning. + * The chance of this happening is low, but not impossible. + */ + std::sort(drm_cards.begin(), drm_cards.end(), compare_match_strength); + if ((drm_cards.size() > 1) && (std::next(begin(drm_cards))->match_count) >= begin(drm_cards)->match_count) { + blog(LOG_WARNING, "%s: Ambiguous GPU association. Possible incorrect sort order.", __FUNCTION__); + for (auto card = begin(drm_cards); card != end(drm_cards); ++card) { + blog(LOG_DEBUG, "Total matches for card %s: %u", card->device_name.c_str(), card->match_count); + } + } + + /* Iterate through the sorted list of cards and generate + * the GoLiveApi GPU list. + */ + for (auto card = begin(drm_cards); card != end(drm_cards); ++card) { + gpu.device_id = card->device_id; + gpu.vendor_id = card->vendor_id; + gpu.model = card->device_name; + adjust_gpu_model(gpu.model); + + if (card == begin(drm_cards)) { + /* The first card in the list corresponds to the + * driver version and GPU memory information obtained + * previously by OpenGL calls into the GPU. + */ + gpu.driver_version = gs_driver_version; + gpu.dedicated_video_memory = dedicated_video_memory; + gpu.shared_system_memory = shared_system_memory; + } else { + /* The driver version for the other device(s) + * is not accessible easily in a common location. + */ + gpu.driver_version = "Unknown"; + + /* Use the GPU memory info discovered with get_drm_card_info() + * stored in drm_cards. amdgpu driver exposes the GPU memory + * info via sysfs, NVIDIA does not. + */ + gpu.dedicated_video_memory = card->dedicated_vram_total; + gpu.shared_system_memory = card->shared_system_memory_total; + } + adapter_info.push_back(gpu); + } + + return adapter_info; +} +} // namespace void system_info(GoLiveApi::Capabilities &capabilities) { - UNUSED_PARAMETER(capabilities); + // Determine the GPU capabilities + capabilities.gpu = system_gpu_data(); + + // Determine the CPU capabilities + { + auto &cpu_data = capabilities.cpu; + cpu_data.physical_cores = os_get_physical_cores(); + cpu_data.logical_cores = os_get_logical_cores(); + + if (!get_cpu_name(cpu_data.name)) { + cpu_data.name = "Unknown"; + } + + uint32_t cpu_freq; + if (get_cpu_freq(cpu_freq)) { + cpu_data.speed = cpu_freq; + } else { + cpu_data.speed = 0; + } + } + + // Determine the memory capabilities + { + auto &memory_data = capabilities.memory; + memory_data.total = os_get_sys_total_size(); + memory_data.free = os_get_sys_free_size(); + } + + // Reporting of gaming features not supported on Linux + UNUSED_PARAMETER(capabilities.gaming_features); + + // Determine the system capabilities + { + auto &system_data = capabilities.system; + + if (!get_distribution_info(system_data.name, system_data.release)) { + system_data.name = "Linux-based distribution"; + system_data.release = "unknown"; + } + + struct utsname utsinfo; + static const uint16_t max_sys_data_version_sz = 128; + if (uname(&utsinfo) == 0) { + /* To determine if the host is 64-bit, check if the machine + * name contains "64", as in "x86_64" or "aarch64". + */ + system_data.bits = strstr(utsinfo.machine, "64") ? 64 : 32; + + /* To determine if the host CPU is ARM based, check if the + * machine name contains "aarch". + */ + system_data.arm = strstr(utsinfo.machine, "aarch") ? true : false; + + /* Send the sysname (usually "Linux"), kernel version and + * release reported by utsname as the version string. + * The code below will produce something like: + * + * "Linux 6.5.0-41-generic #41~22.04.2-Ubuntu SMP PREEMPT_DYNAMIC + * Mon Jun 3 11:32:55 UTC 2" + */ + system_data.version = utsinfo.sysname; + system_data.version.append(" "); + system_data.version.append(utsinfo.release); + system_data.version.append(" "); + system_data.version.append(utsinfo.version); + // Ensure system_data.version string is within the maximum size + if (system_data.version.size() > max_sys_data_version_sz) { + system_data.version.resize(max_sys_data_version_sz); + } + } else { + UNUSED_PARAMETER(system_data.bits); + UNUSED_PARAMETER(system_data.arm); + system_data.version = "unknown"; + } + + // On Linux-based distros, there's no build or revision info + UNUSED_PARAMETER(system_data.build); + UNUSED_PARAMETER(system_data.revision); + + system_data.armEmulation = os_get_emulation_status(); + } }