fix(audio-info): crash when device name contains special characters (#4095)

This commit is contained in:
David Lane 2026-01-16 22:41:23 -05:00 committed by GitHub
commit ab52e27e0e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 476 additions and 163 deletions

View file

@ -59,6 +59,8 @@ set(PLATFORM_TARGET_FILES
"${CMAKE_SOURCE_DIR}/src/platform/windows/display_ram.cpp" "${CMAKE_SOURCE_DIR}/src/platform/windows/display_ram.cpp"
"${CMAKE_SOURCE_DIR}/src/platform/windows/display_wgc.cpp" "${CMAKE_SOURCE_DIR}/src/platform/windows/display_wgc.cpp"
"${CMAKE_SOURCE_DIR}/src/platform/windows/audio.cpp" "${CMAKE_SOURCE_DIR}/src/platform/windows/audio.cpp"
"${CMAKE_SOURCE_DIR}/src/platform/windows/utf_utils.cpp"
"${CMAKE_SOURCE_DIR}/src/platform/windows/utf_utils.h"
"${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/src/ViGEmClient.cpp" "${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/src/ViGEmClient.cpp"
"${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/include/ViGEm/Client.h" "${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/include/ViGEm/Client.h"
"${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/include/ViGEm/Common.h" "${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/include/ViGEm/Common.h"

View file

@ -16,10 +16,10 @@
#include <synchapi.h> #include <synchapi.h>
// local includes // local includes
#include "misc.h"
#include "src/config.h" #include "src/config.h"
#include "src/logging.h" #include "src/logging.h"
#include "src/platform/common.h" #include "src/platform/common.h"
#include "utf_utils.h"
// Must be the last included file // Must be the last included file
// clang-format off // clang-format off
@ -703,7 +703,7 @@ namespace platf::audio {
audio::wstring_t id; audio::wstring_t id;
device->GetId(&id); device->GetId(&id);
sink.host = to_utf8(id.get()); sink.host = utf_utils::to_utf8(id.get());
} }
// Prepare to search for the device_id of the virtual audio sink device, // Prepare to search for the device_id of the virtual audio sink device,
@ -713,14 +713,14 @@ namespace platf::audio {
if (config::audio.virtual_sink.empty()) { if (config::audio.virtual_sink.empty()) {
match_list = match_steam_speakers(); match_list = match_steam_speakers();
} else { } else {
match_list = match_all_fields(from_utf8(config::audio.virtual_sink)); match_list = match_all_fields(utf_utils::from_utf8(config::audio.virtual_sink));
} }
// Search for the virtual audio sink device currently present in the system. // Search for the virtual audio sink device currently present in the system.
auto matched = find_device_id(match_list); auto matched = find_device_id(match_list);
if (matched) { if (matched) {
// Prepare to fill virtual audio sink names with device_id. // Prepare to fill virtual audio sink names with device_id.
auto device_id = to_utf8(matched->second); auto device_id = utf_utils::to_utf8(matched->second);
// Also prepend format name (basically channel layout at the moment) // Also prepend format name (basically channel layout at the moment)
// because we don't want to extend the platform interface. // because we don't want to extend the platform interface.
sink.null = std::make_optional(sink_t::null_t { sink.null = std::make_optional(sink_t::null_t {
@ -736,7 +736,7 @@ namespace platf::audio {
} }
bool is_sink_available(const std::string &sink) override { bool is_sink_available(const std::string &sink) override {
const auto match_list = match_all_fields(from_utf8(sink)); const auto match_list = match_all_fields(utf_utils::from_utf8(sink));
const auto matched = find_device_id(match_list); const auto matched = find_device_id(match_list);
return static_cast<bool>(matched); return static_cast<bool>(matched);
} }
@ -758,7 +758,7 @@ namespace platf::audio {
for (const auto &format : formats) { for (const auto &format : formats) {
auto &name = format.name; auto &name = format.name;
if (current.find(name) == 0) { if (current.find(name) == 0) {
auto device_id = from_utf8(current.substr(name.size(), current.size() - name.size())); auto device_id = utf_utils::from_utf8(current.substr(name.size(), current.size() - name.size()));
return std::make_pair(device_id, std::reference_wrapper(format)); return std::make_pair(device_id, std::reference_wrapper(format));
} }
} }
@ -805,7 +805,7 @@ namespace platf::audio {
// Sink name does not begin with virtual-(format name), hence it's not a virtual sink // Sink name does not begin with virtual-(format name), hence it's not a virtual sink
// and we don't want to change playback format of the corresponding device. // and we don't want to change playback format of the corresponding device.
// Also need to perform matching, sink name is not necessarily device_id in this case. // Also need to perform matching, sink name is not necessarily device_id in this case.
auto matched = find_device_id(match_all_fields(from_utf8(sink))); auto matched = find_device_id(match_all_fields(utf_utils::from_utf8(sink)));
if (matched) { if (matched) {
return matched->second; return matched->second;
} else { } else {

View file

@ -14,6 +14,9 @@
#include <boost/process/v1.hpp> #include <boost/process/v1.hpp>
#include <MinHook.h> #include <MinHook.h>
// local includes
#include "utf_utils.h"
// We have to include boost/process/v1.hpp before display.h due to WinSock.h, // We have to include boost/process/v1.hpp before display.h due to WinSock.h,
// but that prevents the definition of NTSTATUS so we must define it ourself. // but that prevents the definition of NTSTATUS so we must define it ourself.
typedef long NTSTATUS; typedef long NTSTATUS;
@ -474,8 +477,8 @@ namespace platf::dxgi {
return -1; return -1;
} }
auto adapter_name = from_utf8(config::video.adapter_name); auto adapter_name = utf_utils::from_utf8(config::video.adapter_name);
auto output_name = from_utf8(display_name); auto output_name = utf_utils::from_utf8(display_name);
adapter_t::pointer adapter_p; adapter_t::pointer adapter_p;
for (int tries = 0; tries < 2; ++tries) { for (int tries = 0; tries < 2; ++tries) {
@ -589,7 +592,7 @@ namespace platf::dxgi {
DXGI_ADAPTER_DESC adapter_desc; DXGI_ADAPTER_DESC adapter_desc;
adapter->GetDesc(&adapter_desc); adapter->GetDesc(&adapter_desc);
auto description = to_utf8(adapter_desc.Description); auto description = utf_utils::to_utf8(adapter_desc.Description);
BOOST_LOG(info) BOOST_LOG(info)
<< std::endl << std::endl
<< "Device Description : " << description << std::endl << "Device Description : " << description << std::endl
@ -1068,7 +1071,7 @@ namespace platf {
BOOST_LOG(debug) BOOST_LOG(debug)
<< std::endl << std::endl
<< "====== ADAPTER ====="sv << std::endl << "====== ADAPTER ====="sv << std::endl
<< "Device Name : "sv << to_utf8(adapter_desc.Description) << std::endl << "Device Name : "sv << utf_utils::to_utf8(adapter_desc.Description) << std::endl
<< "Device Vendor ID : 0x"sv << util::hex(adapter_desc.VendorId).to_string_view() << std::endl << "Device Vendor ID : 0x"sv << util::hex(adapter_desc.VendorId).to_string_view() << std::endl
<< "Device Device ID : 0x"sv << util::hex(adapter_desc.DeviceId).to_string_view() << std::endl << "Device Device ID : 0x"sv << util::hex(adapter_desc.DeviceId).to_string_view() << std::endl
<< "Device Video Mem : "sv << adapter_desc.DedicatedVideoMemory / 1048576 << " MiB"sv << std::endl << "Device Video Mem : "sv << adapter_desc.DedicatedVideoMemory / 1048576 << " MiB"sv << std::endl
@ -1084,7 +1087,7 @@ namespace platf {
DXGI_OUTPUT_DESC desc; DXGI_OUTPUT_DESC desc;
output->GetDesc(&desc); output->GetDesc(&desc);
auto device_name = to_utf8(desc.DeviceName); auto device_name = utf_utils::to_utf8(desc.DeviceName);
auto width = desc.DesktopCoordinates.right - desc.DesktopCoordinates.left; auto width = desc.DesktopCoordinates.right - desc.DesktopCoordinates.left;
auto height = desc.DesktopCoordinates.bottom - desc.DesktopCoordinates.top; auto height = desc.DesktopCoordinates.bottom - desc.DesktopCoordinates.top;

View file

@ -28,6 +28,7 @@ extern "C" {
#include "src/nvenc/nvenc_d3d11_on_cuda.h" #include "src/nvenc/nvenc_d3d11_on_cuda.h"
#include "src/nvenc/nvenc_utils.h" #include "src/nvenc/nvenc_utils.h"
#include "src/video.h" #include "src/video.h"
#include "utf_utils.h"
#if !defined(SUNSHINE_SHADERS_DIR) // for testing this needs to be defined in cmake as we don't do an install #if !defined(SUNSHINE_SHADERS_DIR) // for testing this needs to be defined in cmake as we don't do an install
#define SUNSHINE_SHADERS_DIR SUNSHINE_ASSETS_DIR "/shaders/directx" #define SUNSHINE_SHADERS_DIR SUNSHINE_ASSETS_DIR "/shaders/directx"
@ -359,7 +360,7 @@ namespace platf::dxgi {
flags |= D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION; flags |= D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;
#endif #endif
auto wFile = from_utf8(file); auto wFile = utf_utils::from_utf8(file);
auto status = D3DCompileFromFile(wFile.c_str(), nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE, entrypoint, shader_model, flags, 0, &compiled_p, &msg_p); auto status = D3DCompileFromFile(wFile.c_str(), nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE, entrypoint, shader_model, flags, 0, &compiled_p, &msg_p);
if (msg_p) { if (msg_p) {

View file

@ -45,6 +45,7 @@
#include "src/logging.h" #include "src/logging.h"
#include "src/platform/common.h" #include "src/platform/common.h"
#include "src/utility.h" #include "src/utility.h"
#include "utf_utils.h"
// UDP_SEND_MSG_SIZE was added in the Windows 10 20H1 SDK // UDP_SEND_MSG_SIZE was added in the Windows 10 20H1 SDK
#ifndef UDP_SEND_MSG_SIZE #ifndef UDP_SEND_MSG_SIZE
@ -314,15 +315,15 @@ namespace platf {
// Parse the environment block and populate env // Parse the environment block and populate env
for (auto c = (PWCHAR) env_block; *c != UNICODE_NULL; c += wcslen(c) + 1) { for (auto c = (PWCHAR) env_block; *c != UNICODE_NULL; c += wcslen(c) + 1) {
// Environment variable entries end with a null-terminator, so std::wstring() will get an entire entry. // Environment variable entries end with a null-terminator, so std::wstring() will get an entire entry.
std::string env_tuple = to_utf8(std::wstring {c}); std::string env_tuple = utf_utils::to_utf8(std::wstring {c});
std::string env_name = env_tuple.substr(0, env_tuple.find('=')); std::string env_name = env_tuple.substr(0, env_tuple.find('='));
std::string env_val = env_tuple.substr(env_tuple.find('=') + 1); std::string env_val = env_tuple.substr(env_tuple.find('=') + 1);
// Perform a case-insensitive search to see if this variable name already exists // Perform a case-insensitive search to see if this variable name already exists
auto itr = std::find_if(env.cbegin(), env.cend(), [&](const auto &e) { if (auto itr = std::find_if(env.begin(), env.end(), [&](const auto &e) {
return boost::iequals(e.get_name(), env_name); return boost::iequals(e.get_name(), env_name);
}); });
if (itr != env.cend()) { itr != env.end()) {
// Use this existing name if it is already present to ensure we merge properly // Use this existing name if it is already present to ensure we merge properly
env_name = itr->get_name(); env_name = itr->get_name();
} }
@ -379,33 +380,36 @@ namespace platf {
offset += wstr.length(); offset += wstr.length();
} }
std::wstring create_environment_block(bp::environment &env) { std::wstring create_environment_block(const bp::environment &env) {
int size = 0; int size = 0;
for (const auto &entry : env) { for (const auto &entry : env) {
auto name = entry.get_name(); auto name = entry.get_name();
auto value = entry.to_string(); auto value = entry.to_string();
size += from_utf8(name).length() + 1 /* L'=' */ + from_utf8(value).length() + 1 /* L'\0' */; size += utf_utils::from_utf8(name).length() + 1 /* L'=' */ + utf_utils::from_utf8(value).length() + 1 /* L'\0' */;
} }
size += 1 /* L'\0' */; size += 1 /* L'\0' */;
wchar_t env_block[size]; std::vector<wchar_t> env_block(size);
int offset = 0; int offset = 0;
for (const auto &entry : env) { for (const auto &entry : env) {
auto name = entry.get_name(); auto name = entry.get_name();
auto value = entry.to_string(); auto value = entry.to_string();
// Construct the NAME=VAL\0 string // Construct the NAME=VAL\0 string
append_string_to_environment_block(env_block, offset, from_utf8(name)); append_string_to_environment_block(env_block.data(), offset, utf_utils::from_utf8(name));
env_block[offset++] = L'='; env_block[offset] = L'=';
append_string_to_environment_block(env_block, offset, from_utf8(value)); offset++;
env_block[offset++] = L'\0'; append_string_to_environment_block(env_block.data(), offset, utf_utils::from_utf8(value));
env_block[offset] = L'\0';
offset++;
} }
// Append a final null terminator // Append a final null terminator
env_block[offset++] = L'\0'; env_block[offset] = L'\0';
offset++;
return std::wstring(env_block, offset); return std::wstring(env_block.data(), offset);
} }
LPPROC_THREAD_ATTRIBUTE_LIST allocate_proc_thread_attr_list(DWORD attribute_count) { LPPROC_THREAD_ATTRIBUTE_LIST allocate_proc_thread_attr_list(DWORD attribute_count) {
@ -676,14 +680,14 @@ namespace platf {
* @return A command string suitable for use by CreateProcess(). * @return A command string suitable for use by CreateProcess().
*/ */
std::wstring resolve_command_string(const std::string &raw_cmd, const std::wstring &working_dir, HANDLE token, DWORD &creation_flags) { std::wstring resolve_command_string(const std::string &raw_cmd, const std::wstring &working_dir, HANDLE token, DWORD &creation_flags) {
std::wstring raw_cmd_w = from_utf8(raw_cmd); std::wstring raw_cmd_w = utf_utils::from_utf8(raw_cmd);
// First, convert the given command into parts so we can get the executable/file/URL without parameters // First, convert the given command into parts so we can get the executable/file/URL without parameters
auto raw_cmd_parts = boost::program_options::split_winmain(raw_cmd_w); auto raw_cmd_parts = boost::program_options::split_winmain(raw_cmd_w);
if (raw_cmd_parts.empty()) { if (raw_cmd_parts.empty()) {
// This is highly unexpected, but we'll just return the raw string and hope for the best. // This is highly unexpected, but we'll just return the raw string and hope for the best.
BOOST_LOG(warning) << "Failed to split command string: "sv << raw_cmd; BOOST_LOG(warning) << "Failed to split command string: "sv << raw_cmd;
return from_utf8(raw_cmd); return utf_utils::from_utf8(raw_cmd);
} }
auto raw_target = raw_cmd_parts.at(0); auto raw_target = raw_cmd_parts.at(0);
@ -697,7 +701,7 @@ namespace platf {
res = UrlGetPartW(raw_target.c_str(), scheme.data(), &out_len, URL_PART_SCHEME, 0); res = UrlGetPartW(raw_target.c_str(), scheme.data(), &out_len, URL_PART_SCHEME, 0);
if (res != S_OK) { if (res != S_OK) {
BOOST_LOG(warning) << "Failed to extract URL scheme from URL: "sv << raw_target << " ["sv << util::hex(res).to_string_view() << ']'; BOOST_LOG(warning) << "Failed to extract URL scheme from URL: "sv << raw_target << " ["sv << util::hex(res).to_string_view() << ']';
return from_utf8(raw_cmd); return utf_utils::from_utf8(raw_cmd);
} }
// If the target is a URL, the class is found using the URL scheme (prior to and not including the ':') // If the target is a URL, the class is found using the URL scheme (prior to and not including the ':')
@ -708,13 +712,13 @@ namespace platf {
if (extension == nullptr || *extension == 0) { if (extension == nullptr || *extension == 0) {
// If the file has no extension, assume it's a command and allow CreateProcess() // If the file has no extension, assume it's a command and allow CreateProcess()
// to try to find it via PATH // to try to find it via PATH
return from_utf8(raw_cmd); return utf_utils::from_utf8(raw_cmd);
} else if (boost::iequals(extension, L".exe")) { } else if (boost::iequals(extension, L".exe")) {
// If the file has an .exe extension, we will bypass the resolution here and // If the file has an .exe extension, we will bypass the resolution here and
// directly pass the unmodified command string to CreateProcess(). The argument // directly pass the unmodified command string to CreateProcess(). The argument
// escaping rules are subtly different between CreateProcess() and ShellExecute(), // escaping rules are subtly different between CreateProcess() and ShellExecute(),
// and we want to preserve backwards compatibility with older configs. // and we want to preserve backwards compatibility with older configs.
return from_utf8(raw_cmd); return utf_utils::from_utf8(raw_cmd);
} }
// For regular files, the class is found using the file extension (including the dot) // For regular files, the class is found using the file extension (including the dot)
@ -731,7 +735,7 @@ namespace platf {
// Override HKEY_CLASSES_ROOT and HKEY_CURRENT_USER to ensure we query the correct class info // Override HKEY_CLASSES_ROOT and HKEY_CURRENT_USER to ensure we query the correct class info
if (!override_per_user_predefined_keys(token)) { if (!override_per_user_predefined_keys(token)) {
return from_utf8(raw_cmd); return utf_utils::from_utf8(raw_cmd);
} }
// Find the command string for the specified class // Find the command string for the specified class
@ -762,7 +766,7 @@ namespace platf {
if (res != S_OK) { if (res != S_OK) {
BOOST_LOG(warning) << "Failed to query command string for raw command: "sv << raw_cmd << " ["sv << util::hex(res).to_string_view() << ']'; BOOST_LOG(warning) << "Failed to query command string for raw command: "sv << raw_cmd << " ["sv << util::hex(res).to_string_view() << ']';
return from_utf8(raw_cmd); return utf_utils::from_utf8(raw_cmd);
} }
// Finally, construct the real command string that will be passed into CreateProcess(). // Finally, construct the real command string that will be passed into CreateProcess().
@ -896,7 +900,7 @@ namespace platf {
* @return A `bp::child` object representing the new process, or an empty `bp::child` object if the launch fails. * @return A `bp::child` object representing the new process, or an empty `bp::child` object if the launch fails.
*/ */
bp::child run_command(bool elevated, bool interactive, const std::string &cmd, boost::filesystem::path &working_dir, const bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) { bp::child run_command(bool elevated, bool interactive, const std::string &cmd, boost::filesystem::path &working_dir, const bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) {
std::wstring start_dir = from_utf8(working_dir.string()); std::wstring start_dir = utf_utils::from_utf8(working_dir.string());
HANDLE job = group ? group->native_handle() : nullptr; HANDLE job = group ? group->native_handle() : nullptr;
STARTUPINFOEXW startup_info = create_startup_info(file, job ? &job : nullptr, ec); STARTUPINFOEXW startup_info = create_startup_info(file, job ? &job : nullptr, ec);
PROCESS_INFORMATION process_info; PROCESS_INFORMATION process_info;
@ -1690,65 +1694,13 @@ namespace platf {
return {}; return {};
} }
std::wstring from_utf8(const std::string &string) {
// No conversion needed if the string is empty
if (string.empty()) {
return {};
}
// Get the output size required to store the string
auto output_size = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, string.data(), string.size(), nullptr, 0);
if (output_size == 0) {
auto winerr = GetLastError();
BOOST_LOG(error) << "Failed to get UTF-16 buffer size: "sv << winerr;
return {};
}
// Perform the conversion
std::wstring output(output_size, L'\0');
output_size = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, string.data(), string.size(), output.data(), output.size());
if (output_size == 0) {
auto winerr = GetLastError();
BOOST_LOG(error) << "Failed to convert string to UTF-16: "sv << winerr;
return {};
}
return output;
}
std::string to_utf8(const std::wstring &string) {
// No conversion needed if the string is empty
if (string.empty()) {
return {};
}
// Get the output size required to store the string
auto output_size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, string.data(), string.size(), nullptr, 0, nullptr, nullptr);
if (output_size == 0) {
auto winerr = GetLastError();
BOOST_LOG(error) << "Failed to get UTF-8 buffer size: "sv << winerr;
return {};
}
// Perform the conversion
std::string output(output_size, '\0');
output_size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, string.data(), string.size(), output.data(), output.size(), nullptr, nullptr);
if (output_size == 0) {
auto winerr = GetLastError();
BOOST_LOG(error) << "Failed to convert string to UTF-8: "sv << winerr;
return {};
}
return output;
}
std::string get_host_name() { std::string get_host_name() {
WCHAR hostname[256]; WCHAR hostname[256];
if (GetHostNameW(hostname, ARRAYSIZE(hostname)) == SOCKET_ERROR) { if (GetHostNameW(hostname, ARRAYSIZE(hostname)) == SOCKET_ERROR) {
BOOST_LOG(error) << "GetHostNameW() failed: "sv << WSAGetLastError(); BOOST_LOG(error) << "GetHostNameW() failed: "sv << WSAGetLastError();
return "Sunshine"s; return "Sunshine"s;
} }
return to_utf8(hostname); return utf_utils::to_utf8(hostname);
} }
class win32_high_precision_timer: public high_precision_timer { class win32_high_precision_timer: public high_precision_timer {

View file

@ -19,18 +19,4 @@ namespace platf {
int64_t qpc_counter(); int64_t qpc_counter();
std::chrono::nanoseconds qpc_time_difference(int64_t performance_counter1, int64_t performance_counter2); std::chrono::nanoseconds qpc_time_difference(int64_t performance_counter1, int64_t performance_counter2);
/**
* @brief Convert a UTF-8 string into a UTF-16 wide string.
* @param string The UTF-8 string.
* @return The converted UTF-16 wide string.
*/
std::wstring from_utf8(const std::string &string);
/**
* @brief Convert a UTF-16 wide string into a UTF-8 string.
* @param string The UTF-16 wide string.
* @return The converted UTF-8 string.
*/
std::string to_utf8(const std::wstring &string);
} // namespace platf } // namespace platf

View file

@ -19,6 +19,7 @@
#include "src/nvhttp.h" #include "src/nvhttp.h"
#include "src/platform/common.h" #include "src/platform/common.h"
#include "src/thread_safe.h" #include "src/thread_safe.h"
#include "utf_utils.h"
#define _FN(x, ret, args) \ #define _FN(x, ret, args) \
typedef ret(*x##_fn) args; \ typedef ret(*x##_fn) args; \
@ -109,8 +110,8 @@ namespace platf::publish {
std::wstring domain {SERVICE_TYPE_DOMAIN.data(), SERVICE_TYPE_DOMAIN.size()}; std::wstring domain {SERVICE_TYPE_DOMAIN.data(), SERVICE_TYPE_DOMAIN.size()};
auto hostname = platf::get_host_name(); auto hostname = platf::get_host_name();
auto name = from_utf8(net::mdns_instance_name(hostname) + '.') + domain; auto name = utf_utils::from_utf8(net::mdns_instance_name(hostname) + '.') + domain;
auto host = from_utf8(hostname + ".local"); auto host = utf_utils::from_utf8(hostname + ".local");
DNS_SERVICE_INSTANCE instance {}; DNS_SERVICE_INSTANCE instance {};
instance.pszInstanceName = name.data(); instance.pszInstanceName = name.data();

View file

@ -0,0 +1,66 @@
/**
* @file src/platform/windows/utf_utils.cpp
* @brief Minimal UTF conversion utilities for Windows tools
*/
#include "utf_utils.h"
#include "src/logging.h"
#include <string>
#include <Windows.h>
using namespace std::literals;
namespace utf_utils {
std::wstring from_utf8(const std::string &string) {
// No conversion needed if the string is empty
if (string.empty()) {
return {};
}
// Get the output size required to store the string
auto output_size = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, string.data(), string.size(), nullptr, 0);
if (output_size == 0) {
auto winerr = GetLastError();
BOOST_LOG(error) << "Failed to get UTF-16 buffer size: "sv << winerr;
return {};
}
// Perform the conversion
std::wstring output(output_size, L'\0');
output_size = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, string.data(), string.size(), output.data(), output.size());
if (output_size == 0) {
auto winerr = GetLastError();
BOOST_LOG(error) << "Failed to convert string to UTF-16: "sv << winerr;
return {};
}
return output;
}
std::string to_utf8(const std::wstring &string) {
// No conversion needed if the string is empty
if (string.empty()) {
return {};
}
// Get the output size required to store the string
auto output_size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, string.data(), string.size(), nullptr, 0, nullptr, nullptr);
if (output_size == 0) {
auto winerr = GetLastError();
BOOST_LOG(error) << "Failed to get UTF-8 buffer size: "sv << winerr;
return {};
}
// Perform the conversion
std::string output(output_size, '\0');
output_size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, string.data(), string.size(), output.data(), output.size(), nullptr, nullptr);
if (output_size == 0) {
auto winerr = GetLastError();
BOOST_LOG(error) << "Failed to convert string to UTF-8: "sv << winerr;
return {};
}
return output;
}
} // namespace utf_utils

View file

@ -0,0 +1,23 @@
/**
* @file src/platform/windows/utf_utils.h
* @brief Minimal UTF conversion utilities for Windows tools
*/
#pragma once
#include <string>
namespace utf_utils {
/**
* @brief Convert a UTF-8 string into a UTF-16 wide string.
* @param string The UTF-8 string.
* @return The converted UTF-16 wide string.
*/
std::wstring from_utf8(const std::string &string);
/**
* @brief Convert a UTF-16 wide string into a UTF-8 string.
* @param string The UTF-16 wide string.
* @return The converted UTF-8 string.
*/
std::string to_utf8(const std::wstring &string);
} // namespace utf_utils

View file

@ -33,7 +33,7 @@
#ifdef _WIN32 #ifdef _WIN32
// from_utf8() string conversion function // from_utf8() string conversion function
#include "platform/windows/misc.h" #include "platform/windows/utf_utils.h"
// _SH constants for _wfsopen() // _SH constants for _wfsopen()
#include <share.h> #include <share.h>
@ -183,7 +183,7 @@ namespace proc {
#ifdef _WIN32 #ifdef _WIN32
// fopen() interprets the filename as an ANSI string on Windows, so we must convert it // fopen() interprets the filename as an ANSI string on Windows, so we must convert it
// to UTF-16 and use the wchar_t variants for proper Unicode log file path support. // to UTF-16 and use the wchar_t variants for proper Unicode log file path support.
auto woutput = platf::from_utf8(_app.output); auto woutput = utf_utils::from_utf8(_app.output);
// Use _SH_DENYNO to allow us to open this log file again for writing even if it is // Use _SH_DENYNO to allow us to open this log file again for writing even if it is
// still open from a previous execution. This is required to handle the case of a // still open from a previous execution. This is required to handle the case of a

View file

@ -0,0 +1,262 @@
/**
* @file tests/unit/platform/windows/test_utf_utils.cpp
* @brief Test src/platform/windows/utf_utils.cpp UTF conversion functions.
*/
#include "../../../tests_common.h"
#include <iostream>
#include <string>
#ifdef _WIN32
#include <src/platform/windows/utf_utils.h>
#include <Windows.h>
#endif
#ifdef _WIN32
/**
* @brief Test fixture for utf_utils namespace functions
*/
class UtfUtilsTest: public testing::Test {};
TEST_F(UtfUtilsTest, FromUtf8WithEmptyString) {
const std::string empty_string = "";
const std::wstring result = utf_utils::from_utf8(empty_string);
EXPECT_TRUE(result.empty()) << "Empty UTF-8 string should produce empty wide string";
}
TEST_F(UtfUtilsTest, ToUtf8WithEmptyWideString) {
const std::wstring empty_wstring = L"";
const std::string result = utf_utils::to_utf8(empty_wstring);
EXPECT_TRUE(result.empty()) << "Empty wide string should produce empty UTF-8 string";
}
TEST_F(UtfUtilsTest, FromUtf8WithBasicString) {
const std::string test_string = "Hello World";
const std::wstring result = utf_utils::from_utf8(test_string);
EXPECT_EQ(result, L"Hello World") << "Basic ASCII string should convert correctly";
}
TEST_F(UtfUtilsTest, ToUtf8WithBasicWideString) {
const std::wstring test_wstring = L"Hello World";
const std::string result = utf_utils::to_utf8(test_wstring);
EXPECT_EQ(result, "Hello World") << "Basic wide string should convert correctly";
}
TEST_F(UtfUtilsTest, RoundTripConversionBasic) {
const std::string original = "Test String";
const std::wstring wide = utf_utils::from_utf8(original);
const std::string converted_back = utf_utils::to_utf8(wide);
EXPECT_EQ(original, converted_back) << "Round trip conversion should preserve basic string";
}
TEST_F(UtfUtilsTest, FromUtf8WithQuotationMarks) {
// Test various quotation marks that might appear in device names
const std::string single_quote = "Device 'Audio' Output";
const std::string double_quote = "Device \"Audio\" Output";
const std::string left_quote = "Device \u{2018}Audio\u{2019} Output"; // Unicode left/right single quotes
const std::string right_quote = "Device \u{2019}Audio\u{2018} Output"; // Unicode right/left single quotes
const std::string left_double = "Device \u{201C}Audio\u{201D} Output"; // Unicode left/right double quotes
const std::string right_double = "Device \u{201D}Audio\u{201C} Output"; // Unicode right/left double quotes
const std::wstring result1 = utf_utils::from_utf8(single_quote);
const std::wstring result2 = utf_utils::from_utf8(double_quote);
const std::wstring result3 = utf_utils::from_utf8(left_quote);
const std::wstring result4 = utf_utils::from_utf8(right_quote);
const std::wstring result5 = utf_utils::from_utf8(left_double);
const std::wstring result6 = utf_utils::from_utf8(right_double);
EXPECT_EQ(result1, L"Device 'Audio' Output") << "Single quote conversion failed";
EXPECT_EQ(result2, L"Device \"Audio\" Output") << "Double quote conversion failed";
EXPECT_EQ(result3, L"Device \u{2018}Audio\u{2019} Output") << "Left quote conversion failed";
EXPECT_EQ(result4, L"Device \u{2019}Audio\u{2018} Output") << "Right quote conversion failed";
EXPECT_EQ(result5, L"Device \u{201C}Audio\u{201D} Output") << "Left double quote conversion failed";
EXPECT_EQ(result6, L"Device \u{201D}Audio\u{201C} Output") << "Right double quote conversion failed";
}
TEST_F(UtfUtilsTest, FromUtf8WithTrademarkSymbols) {
// Test trademark and copyright symbols
const std::string trademark = "Audio Device™";
const std::string registered = "Audio Device®";
const std::string copyright = "Audio Device©";
const std::string combined = "Realtek® Audio™";
const std::wstring result1 = utf_utils::from_utf8(trademark);
const std::wstring result2 = utf_utils::from_utf8(registered);
const std::wstring result3 = utf_utils::from_utf8(copyright);
const std::wstring result4 = utf_utils::from_utf8(combined);
EXPECT_EQ(result1, L"Audio Device™") << "Trademark symbol conversion failed";
EXPECT_EQ(result2, L"Audio Device®") << "Registered symbol conversion failed";
EXPECT_EQ(result3, L"Audio Device©") << "Copyright symbol conversion failed";
EXPECT_EQ(result4, L"Realtek® Audio™") << "Combined symbols conversion failed";
}
TEST_F(UtfUtilsTest, FromUtf8WithAccentedCharacters) {
// Test accented characters that might appear in international device names
const std::string french = "Haut-parleur à haute qualité";
const std::string spanish = "Altavoz ñáéíóú";
const std::string german = "Lautsprecher äöü";
const std::string mixed = "àáâãäåæçèéêë";
const std::wstring result1 = utf_utils::from_utf8(french);
const std::wstring result2 = utf_utils::from_utf8(spanish);
const std::wstring result3 = utf_utils::from_utf8(german);
const std::wstring result4 = utf_utils::from_utf8(mixed);
EXPECT_EQ(result1, L"Haut-parleur à haute qualité") << "French accents conversion failed";
EXPECT_EQ(result2, L"Altavoz ñáéíóú") << "Spanish accents conversion failed";
EXPECT_EQ(result3, L"Lautsprecher äöü") << "German accents conversion failed";
EXPECT_EQ(result4, L"àáâãäåæçèéêë") << "Mixed accents conversion failed";
}
TEST_F(UtfUtilsTest, FromUtf8WithSpecialSymbols) {
// Test various special symbols
const std::string math_symbols = "Audio @ 44.1kHz ± 0.1%";
const std::string punctuation = "Audio Device #1 & #2";
const std::string programming = "Device $%^&*()";
const std::string mixed_symbols = "Audio™ @#$%^&*()";
const std::wstring result1 = utf_utils::from_utf8(math_symbols);
const std::wstring result2 = utf_utils::from_utf8(punctuation);
const std::wstring result3 = utf_utils::from_utf8(programming);
const std::wstring result4 = utf_utils::from_utf8(mixed_symbols);
EXPECT_EQ(result1, L"Audio @ 44.1kHz ± 0.1%") << "Math symbols conversion failed";
EXPECT_EQ(result2, L"Audio Device #1 & #2") << "Punctuation conversion failed";
EXPECT_EQ(result3, L"Device $%^&*()") << "Programming symbols conversion failed";
EXPECT_EQ(result4, L"Audio™ @#$%^&*()") << "Mixed symbols conversion failed";
}
TEST_F(UtfUtilsTest, ToUtf8WithQuotationMarks) {
// Test various quotation marks conversion from wide to UTF-8
const std::wstring single_quote = L"Device 'Audio' Output";
const std::wstring double_quote = L"Device \"Audio\" Output";
const std::wstring left_quote = L"Device \u{2018}Audio\u{2019} Output"; // Unicode left/right single quotes
const std::wstring right_quote = L"Device \u{2019}Audio\u{2018} Output"; // Unicode right/left single quotes
const std::wstring left_double = L"Device \u{201C}Audio\u{201D} Output"; // Unicode left/right double quotes
const std::wstring right_double = L"Device \u{201D}Audio\u{201C} Output"; // Unicode right/left double quotes
const std::string result1 = utf_utils::to_utf8(single_quote);
const std::string result2 = utf_utils::to_utf8(double_quote);
const std::string result3 = utf_utils::to_utf8(left_quote);
const std::string result4 = utf_utils::to_utf8(right_quote);
const std::string result5 = utf_utils::to_utf8(left_double);
const std::string result6 = utf_utils::to_utf8(right_double);
EXPECT_EQ(result1, "Device 'Audio' Output") << "Single quote to UTF-8 conversion failed";
EXPECT_EQ(result2, "Device \"Audio\" Output") << "Double quote to UTF-8 conversion failed";
EXPECT_EQ(result3, "Device \u{2018}Audio\u{2019} Output") << "Left quote to UTF-8 conversion failed";
EXPECT_EQ(result4, "Device \u{2019}Audio\u{2018} Output") << "Right quote to UTF-8 conversion failed";
EXPECT_EQ(result5, "Device \u{201C}Audio\u{201D} Output") << "Left double quote to UTF-8 conversion failed";
EXPECT_EQ(result6, "Device \u{201D}Audio\u{201C} Output") << "Right double quote to UTF-8 conversion failed";
}
TEST_F(UtfUtilsTest, ToUtf8WithTrademarkSymbols) {
// Test trademark and copyright symbols conversion from wide to UTF-8
const std::wstring trademark = L"Audio Device™";
const std::wstring registered = L"Audio Device®";
const std::wstring copyright = L"Audio Device©";
const std::wstring combined = L"Realtek® Audio™";
const std::string result1 = utf_utils::to_utf8(trademark);
const std::string result2 = utf_utils::to_utf8(registered);
const std::string result3 = utf_utils::to_utf8(copyright);
const std::string result4 = utf_utils::to_utf8(combined);
EXPECT_EQ(result1, "Audio Device™") << "Trademark symbol to UTF-8 conversion failed";
EXPECT_EQ(result2, "Audio Device®") << "Registered symbol to UTF-8 conversion failed";
EXPECT_EQ(result3, "Audio Device©") << "Copyright symbol to UTF-8 conversion failed";
EXPECT_EQ(result4, "Realtek® Audio™") << "Combined symbols to UTF-8 conversion failed";
}
TEST_F(UtfUtilsTest, RoundTripConversionWithSpecialCharacters) {
// Test round trip conversion with various special characters
const std::string quotes = "Device 'Audio' with \u{201C}Special\u{201D} Characters";
const std::string symbols = "Realtek® Audio™ @ 44.1kHz ± 0.1%";
const std::string accents = "Haut-parleur àáâãäåæçèéêë";
const std::string mixed = "Audio™ 'Device' @#$%^&*() ñáéíóú";
// Convert to wide and back
const std::wstring wide1 = utf_utils::from_utf8(quotes);
const std::wstring wide2 = utf_utils::from_utf8(symbols);
const std::wstring wide3 = utf_utils::from_utf8(accents);
const std::wstring wide4 = utf_utils::from_utf8(mixed);
const std::string back1 = utf_utils::to_utf8(wide1);
const std::string back2 = utf_utils::to_utf8(wide2);
const std::string back3 = utf_utils::to_utf8(wide3);
const std::string back4 = utf_utils::to_utf8(wide4);
EXPECT_EQ(quotes, back1) << "Round trip failed for quotes";
EXPECT_EQ(symbols, back2) << "Round trip failed for symbols";
EXPECT_EQ(accents, back3) << "Round trip failed for accents";
EXPECT_EQ(mixed, back4) << "Round trip failed for mixed characters";
}
TEST_F(UtfUtilsTest, RealAudioDeviceNames) {
// Test with realistic audio device names that contain special characters
const std::string realtek = "Realtek® High Definition Audio";
const std::string creative = "Creative Sound Blaster™ X-Fi";
const std::string logitech = "Logitech G533 Gaming Headset";
const std::string bluetooth = "Sony WH-1000XM4 'Wireless' Headphones";
const std::string usb = "USB Audio Device @ 48kHz";
// Test conversion to wide
const std::wstring wide_realtek = utf_utils::from_utf8(realtek);
const std::wstring wide_creative = utf_utils::from_utf8(creative);
const std::wstring wide_logitech = utf_utils::from_utf8(logitech);
const std::wstring wide_bluetooth = utf_utils::from_utf8(bluetooth);
const std::wstring wide_usb = utf_utils::from_utf8(usb);
EXPECT_FALSE(wide_realtek.empty()) << "Realtek device name conversion failed";
EXPECT_FALSE(wide_creative.empty()) << "Creative device name conversion failed";
EXPECT_FALSE(wide_logitech.empty()) << "Logitech device name conversion failed";
EXPECT_FALSE(wide_bluetooth.empty()) << "Bluetooth device name conversion failed";
EXPECT_FALSE(wide_usb.empty()) << "USB device name conversion failed";
// Test round trip
EXPECT_EQ(realtek, utf_utils::to_utf8(wide_realtek)) << "Realtek round trip failed";
EXPECT_EQ(creative, utf_utils::to_utf8(wide_creative)) << "Creative round trip failed";
EXPECT_EQ(logitech, utf_utils::to_utf8(wide_logitech)) << "Logitech round trip failed";
EXPECT_EQ(bluetooth, utf_utils::to_utf8(wide_bluetooth)) << "Bluetooth round trip failed";
EXPECT_EQ(usb, utf_utils::to_utf8(wide_usb)) << "USB round trip failed";
}
TEST_F(UtfUtilsTest, InvalidUtf8Sequences) {
// Test with invalid UTF-8 sequences - should return empty string
const std::string invalid1 = "Test\x{FF}\x{FE}\x{FD}"; // Invalid UTF-8 bytes
const std::string invalid2 = "Test\x{80}\x{81}\x{82}"; // Invalid continuation bytes
const std::wstring result1 = utf_utils::from_utf8(invalid1);
const std::wstring result2 = utf_utils::from_utf8(invalid2);
// The function should return empty string for invalid UTF-8 sequences
EXPECT_TRUE(result1.empty()) << "Invalid UTF-8 sequence should return empty string";
EXPECT_TRUE(result2.empty()) << "Invalid UTF-8 sequence should return empty string";
}
TEST_F(UtfUtilsTest, LongStringsWithSpecialCharacters) {
// Test with longer strings containing many special characters
std::string long_special = "Device™ with 'special' characters: àáâãäåæçèéêë ñáéíóú äöü ";
for (int i = 0; i < 10; ++i) {
long_special += "Audio® Device™ @#$%^&*() ";
}
const std::wstring wide_result = utf_utils::from_utf8(long_special);
const std::string back_result = utf_utils::to_utf8(wide_result);
EXPECT_FALSE(wide_result.empty()) << "Long string conversion should not be empty";
EXPECT_EQ(long_special, back_result) << "Long string round trip should preserve content";
}
#else
// For non-Windows platforms, the utf_utils namespace doesn't exist
TEST(UtfUtilsTest, UtfUtilsNotAvailableOnNonWindows) {
GTEST_SKIP() << "utf_utils namespace is Windows-specific";
}
#endif

View file

@ -2,23 +2,38 @@ cmake_minimum_required(VERSION 3.20)
project(sunshine_tools) project(sunshine_tools)
include_directories("${CMAKE_SOURCE_DIR}") include_directories(
"${CMAKE_SOURCE_DIR}"
"${FFMPEG_INCLUDE_DIRS}" # this is included only for logging
)
add_executable(dxgi-info dxgi.cpp) set(TOOL_SOURCES
"${CMAKE_SOURCE_DIR}/src/logging.cpp"
"${CMAKE_SOURCE_DIR}/src/platform/windows/utf_utils.cpp"
)
add_executable(dxgi-info dxgi.cpp ${TOOL_SOURCES})
set_target_properties(dxgi-info PROPERTIES CXX_STANDARD 23) set_target_properties(dxgi-info PROPERTIES CXX_STANDARD 23)
target_link_libraries(dxgi-info target_link_libraries(dxgi-info
${Boost_LIBRARIES}
${CMAKE_THREAD_LIBS_INIT} ${CMAKE_THREAD_LIBS_INIT}
${FFMPEG_LIBRARIES} # this is included only for logging
dxgi dxgi
${PLATFORM_LIBRARIES}) libdisplaydevice::display_device # this is included only for logging
${PLATFORM_LIBRARIES}
)
target_compile_options(dxgi-info PRIVATE ${SUNSHINE_COMPILE_OPTIONS}) target_compile_options(dxgi-info PRIVATE ${SUNSHINE_COMPILE_OPTIONS})
add_executable(audio-info audio.cpp) add_executable(audio-info audio.cpp ${TOOL_SOURCES})
set_target_properties(audio-info PROPERTIES CXX_STANDARD 23) set_target_properties(audio-info PROPERTIES CXX_STANDARD 23)
target_link_libraries(audio-info target_link_libraries(audio-info
${Boost_LIBRARIES} ${Boost_LIBRARIES}
${CMAKE_THREAD_LIBS_INIT} ${CMAKE_THREAD_LIBS_INIT}
${FFMPEG_LIBRARIES} # this is included only for logging
libdisplaydevice::display_device # this is included only for logging
ksuser ksuser
${PLATFORM_LIBRARIES}) ${PLATFORM_LIBRARIES}
)
target_compile_options(audio-info PRIVATE ${SUNSHINE_COMPILE_OPTIONS}) target_compile_options(audio-info PRIVATE ${SUNSHINE_COMPILE_OPTIONS})
add_executable(sunshinesvc sunshinesvc.cpp) add_executable(sunshinesvc sunshinesvc.cpp)

View file

@ -6,17 +6,13 @@
// platform includes // platform includes
#include <Audioclient.h> #include <Audioclient.h>
#include <codecvt>
#include <iostream> #include <iostream>
#include <locale> #include <locale>
#include <mmdeviceapi.h> #include <mmdeviceapi.h>
#include <roapi.h> #include <roapi.h>
#include <synchapi.h>
// lib includes
#include <boost/locale.hpp>
// local includes // local includes
#include "src/platform/windows/utf_utils.h"
#include "src/utility.h" #include "src/utility.h"
DEFINE_PROPERTYKEY(PKEY_Device_DeviceDesc, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 2); // DEVPROP_TYPE_STRING DEFINE_PROPERTYKEY(PKEY_Device_DeviceDesc, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 2); // DEVPROP_TYPE_STRING
@ -35,7 +31,7 @@ namespace audio {
template<class T> template<class T>
void co_task_free(T *p) { void co_task_free(T *p) {
CoTaskMemFree((LPVOID) p); CoTaskMemFree(static_cast<LPVOID>(p));
} }
using device_enum_t = util::safe_ptr<IMMDeviceEnumerator, Release<IMMDeviceEnumerator>>; using device_enum_t = util::safe_ptr<IMMDeviceEnumerator, Release<IMMDeviceEnumerator>>;
@ -63,10 +59,6 @@ namespace audio {
PROPVARIANT prop; PROPVARIANT prop;
}; };
const wchar_t *no_null(const wchar_t *str) {
return str ? str : L"Unknown";
}
struct format_t { struct format_t {
std::string_view name; std::string_view name;
int channels; int channels;
@ -118,7 +110,11 @@ namespace audio {
wave_format->nAvgBytesPerSec = wave_format->nSamplesPerSec * wave_format->nBlockAlign; wave_format->nAvgBytesPerSec = wave_format->nSamplesPerSec * wave_format->nBlockAlign;
if (wave_format->wFormatTag == WAVE_FORMAT_EXTENSIBLE) { if (wave_format->wFormatTag == WAVE_FORMAT_EXTENSIBLE) {
((PWAVEFORMATEXTENSIBLE) wave_format.get())->dwChannelMask = format.channel_mask; // Access the extended format through proper offsetting
// WAVEFORMATEXTENSIBLE has WAVEFORMATEX as first member, so this is safe
const auto ext_format =
static_cast<PWAVEFORMATEXTENSIBLE>(static_cast<void *>(wave_format.get()));
ext_format->dwChannelMask = format.channel_mask;
} }
} }
@ -128,7 +124,7 @@ namespace audio {
IID_IAudioClient, IID_IAudioClient,
CLSCTX_ALL, CLSCTX_ALL,
nullptr, nullptr,
(void **) &audio_client static_cast<void **>(static_cast<void *>(&audio_client))
); );
if (FAILED(status)) { if (FAILED(status)) {
@ -186,7 +182,7 @@ namespace audio {
return; return;
} }
std::wstring device_state_string = L"Unknown"s; std::wstring device_state_string;
switch (device_state) { switch (device_state) {
case DEVICE_STATE_ACTIVE: case DEVICE_STATE_ACTIVE:
device_state_string = L"Active"s; device_state_string = L"Active"s;
@ -200,28 +196,36 @@ namespace audio {
case DEVICE_STATE_NOTPRESENT: case DEVICE_STATE_NOTPRESENT:
device_state_string = L"Not present"s; device_state_string = L"Not present"s;
break; break;
default:
device_state_string = L"Unknown"s;
break;
} }
std::wstring current_format = L"Unknown"s; std::string current_format = "Unknown";
for (const auto &format : formats) { for (const auto &format : formats) {
// This will fail for any format that's not the mix format for this device, // This will fail for any format that's not the mix format for this device,
// so we can take the first match as the current format to display. // so we can take the first match as the current format to display.
auto audio_client = make_audio_client(device, format); if (auto audio_client = make_audio_client(device, format)) {
if (audio_client) { current_format = std::string(format.name);
current_format = boost::locale::conv::utf_to_utf<wchar_t>(format.name.data());
break; break;
} }
} }
std::wcout auto safe_wstring_output = [](const wchar_t *wstr) -> std::string {
<< L"===== Device ====="sv << std::endl if (!wstr) {
<< L"Device ID : "sv << wstring.get() << std::endl return "Unknown";
<< L"Device name : "sv << no_null((LPWSTR) device_friendly_name.prop.pszVal) << std::endl }
<< L"Adapter name : "sv << no_null((LPWSTR) adapter_friendly_name.prop.pszVal) << std::endl return utf_utils::to_utf8(std::wstring(wstr));
<< L"Device description : "sv << no_null((LPWSTR) device_desc.prop.pszVal) << std::endl };
<< L"Device state : "sv << device_state_string << std::endl
<< L"Current format : "sv << current_format << std::endl std::cout << "===== Device =====" << std::endl;
<< std::endl; std::cout << "Device ID : " << utf_utils::to_utf8(std::wstring(wstring.get())) << std::endl;
std::cout << "Device name : " << safe_wstring_output(device_friendly_name.prop.pwszVal) << std::endl;
std::cout << "Adapter name : " << safe_wstring_output(adapter_friendly_name.prop.pwszVal) << std::endl;
std::cout << "Device description : " << safe_wstring_output(device_desc.prop.pwszVal) << std::endl;
std::cout << "Device state : " << utf_utils::to_utf8(device_state_string) << std::endl;
std::cout << "Current format : " << current_format << std::endl;
std::cout << std::endl;
} }
} // namespace audio } // namespace audio
@ -268,15 +272,13 @@ int main(int argc, char *argv[]) {
} }
} }
HRESULT status;
audio::device_enum_t device_enum; audio::device_enum_t device_enum;
status = CoCreateInstance( HRESULT status = CoCreateInstance(
CLSID_MMDeviceEnumerator, CLSID_MMDeviceEnumerator,
nullptr, nullptr,
CLSCTX_ALL, CLSCTX_ALL,
IID_IMMDeviceEnumerator, IID_IMMDeviceEnumerator,
(void **) &device_enum static_cast<void **>(static_cast<void *>(&device_enum))
); );
if (FAILED(status)) { if (FAILED(status)) {

View file

@ -3,10 +3,12 @@
* @brief Displays information about connected displays and GPUs * @brief Displays information about connected displays and GPUs
*/ */
#define WINVER 0x0A00 #define WINVER 0x0A00
#include "src/platform/windows/utf_utils.h"
#include "src/utility.h" #include "src/utility.h"
#include <d3dcommon.h> #include <d3dcommon.h>
#include <dxgi.h> #include <dxgi.h>
#include <format>
#include <iostream> #include <iostream>
using namespace std::literals; using namespace std::literals;
@ -20,17 +22,14 @@ namespace dxgi {
using factory1_t = util::safe_ptr<IDXGIFactory1, Release<IDXGIFactory1>>; using factory1_t = util::safe_ptr<IDXGIFactory1, Release<IDXGIFactory1>>;
using adapter_t = util::safe_ptr<IDXGIAdapter1, Release<IDXGIAdapter1>>; using adapter_t = util::safe_ptr<IDXGIAdapter1, Release<IDXGIAdapter1>>;
using output_t = util::safe_ptr<IDXGIOutput, Release<IDXGIOutput>>; using output_t = util::safe_ptr<IDXGIOutput, Release<IDXGIOutput>>;
} // namespace dxgi } // namespace dxgi
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {
HRESULT status;
// Set ourselves as per-monitor DPI aware for accurate resolution values on High DPI systems // Set ourselves as per-monitor DPI aware for accurate resolution values on High DPI systems
SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
dxgi::factory1_t::pointer factory_p {}; dxgi::factory1_t::pointer factory_p {};
status = CreateDXGIFactory1(IID_IDXGIFactory1, (void **) &factory_p); const HRESULT status = CreateDXGIFactory1(IID_IDXGIFactory1, static_cast<void **>(static_cast<void *>(&factory_p)));
dxgi::factory1_t factory {factory_p}; dxgi::factory1_t factory {factory_p};
if (FAILED(status)) { if (FAILED(status)) {
std::cout << "Failed to create DXGIFactory1 [0x"sv << util::hex(status).to_string_view() << ']' << std::endl; std::cout << "Failed to create DXGIFactory1 [0x"sv << util::hex(status).to_string_view() << ']' << std::endl;
@ -44,21 +43,24 @@ int main(int argc, char *argv[]) {
DXGI_ADAPTER_DESC1 adapter_desc; DXGI_ADAPTER_DESC1 adapter_desc;
adapter->GetDesc1(&adapter_desc); adapter->GetDesc1(&adapter_desc);
std::cout std::cout << "====== ADAPTER =====" << std::endl;
<< "====== ADAPTER ====="sv << std::endl; std::cout << "Device Name : " << utf_utils::to_utf8(std::wstring(adapter_desc.Description)) << std::endl;
std::wcout std::cout << "Device Vendor ID : " << "0x" << util::hex(adapter_desc.VendorId).to_string() << std::endl;
<< L"Device Name : "sv << adapter_desc.Description << std::endl; std::cout << "Device Device ID : " << "0x" << util::hex(adapter_desc.DeviceId).to_string() << std::endl;
std::cout std::cout << "Device Video Mem : " << std::format("{} MiB", adapter_desc.DedicatedVideoMemory / 1048576) << std::endl;
<< "Device Vendor ID : 0x"sv << util::hex(adapter_desc.VendorId).to_string_view() << std::endl std::cout << "Device Sys Mem : " << std::format("{} MiB", adapter_desc.DedicatedSystemMemory / 1048576) << std::endl;
<< "Device Device ID : 0x"sv << util::hex(adapter_desc.DeviceId).to_string_view() << std::endl std::cout << "Share Sys Mem : " << std::format("{} MiB", adapter_desc.SharedSystemMemory / 1048576) << std::endl;
<< "Device Video Mem : "sv << adapter_desc.DedicatedVideoMemory / 1048576 << " MiB"sv << std::endl
<< "Device Sys Mem : "sv << adapter_desc.DedicatedSystemMemory / 1048576 << " MiB"sv << std::endl
<< "Share Sys Mem : "sv << adapter_desc.SharedSystemMemory / 1048576 << " MiB"sv << std::endl
<< std::endl
<< " ====== OUTPUT ======"sv << std::endl;
dxgi::output_t::pointer output_p {}; dxgi::output_t::pointer output_p {};
bool has_outputs = false;
for (int y = 0; adapter->EnumOutputs(y, &output_p) != DXGI_ERROR_NOT_FOUND; ++y) { for (int y = 0; adapter->EnumOutputs(y, &output_p) != DXGI_ERROR_NOT_FOUND; ++y) {
// Print the header only when we find the first output
if (!has_outputs) {
std::cout << std::endl
<< " ====== OUTPUT ======" << std::endl;
has_outputs = true;
}
dxgi::output_t output {output_p}; dxgi::output_t output {output_p};
DXGI_OUTPUT_DESC desc; DXGI_OUTPUT_DESC desc;
@ -67,13 +69,11 @@ int main(int argc, char *argv[]) {
auto width = desc.DesktopCoordinates.right - desc.DesktopCoordinates.left; auto width = desc.DesktopCoordinates.right - desc.DesktopCoordinates.left;
auto height = desc.DesktopCoordinates.bottom - desc.DesktopCoordinates.top; auto height = desc.DesktopCoordinates.bottom - desc.DesktopCoordinates.top;
std::wcout std::cout << " Output Name : " << utf_utils::to_utf8(std::wstring(desc.DeviceName)) << std::endl;
<< L" Output Name : "sv << desc.DeviceName << std::endl; std::cout << " AttachedToDesktop : " << (desc.AttachedToDesktop ? "yes" : "no") << std::endl;
std::cout std::cout << " Resolution : " << std::format("{}x{}", width, height) << std::endl;
<< " AttachedToDesktop : "sv << (desc.AttachedToDesktop ? "yes"sv : "no"sv) << std::endl
<< " Resolution : "sv << width << 'x' << height << std::endl
<< std::endl;
} }
std::cout << std::endl;
} }
return 0; return 0;