From 56f15d451eec363b6980581c4806e68a81418548 Mon Sep 17 00:00:00 2001 From: fatebug <1339524041@qq.com> Date: Thu, 14 May 2026 17:49:35 +0800 Subject: [PATCH 01/10] feat(vdd): add virtual display management --- cmake/compile_definitions/common.cmake | 3 + docs/configuration.md | 71 ++ src/config.cpp | 14 + src/config.h | 8 + src/confighttp.cpp | 220 +++++++ src/main.cpp | 38 ++ src/system_tray.cpp | 41 +- src/vdd_control.cpp | 610 ++++++++++++++++++ src/vdd_control.h | 115 ++++ src_assets/common/assets/web/config.html | 12 +- .../assets/web/configs/tabs/AudioVideo.vue | 7 + .../web/configs/tabs/VirtualDisplay.vue | 305 +++++++++ .../assets/web/public/assets/locale/en.json | 41 +- .../assets/web/public/assets/locale/zh.json | 41 +- .../web/public/assets/locale/zh_TW.json | 41 +- third-party/parsec-vdd/parsec-vdd.h | 356 ++++++++++ 16 files changed, 1918 insertions(+), 5 deletions(-) create mode 100644 src/vdd_control.cpp create mode 100644 src/vdd_control.h create mode 100644 src_assets/common/assets/web/configs/tabs/VirtualDisplay.vue create mode 100644 third-party/parsec-vdd/parsec-vdd.h diff --git a/cmake/compile_definitions/common.cmake b/cmake/compile_definitions/common.cmake index 73cfdae755c..19d662c268c 100644 --- a/cmake/compile_definitions/common.cmake +++ b/cmake/compile_definitions/common.cmake @@ -108,6 +108,8 @@ set(SUNSHINE_TARGET_FILES "${CMAKE_SOURCE_DIR}/src/network.cpp" "${CMAKE_SOURCE_DIR}/src/network.h" "${CMAKE_SOURCE_DIR}/src/move_by_copy.h" + "${CMAKE_SOURCE_DIR}/src/vdd_control.h" + "${CMAKE_SOURCE_DIR}/src/vdd_control.cpp" "${CMAKE_SOURCE_DIR}/src/system_tray.cpp" "${CMAKE_SOURCE_DIR}/src/system_tray.h" "${CMAKE_SOURCE_DIR}/src/task_pool.h" @@ -139,6 +141,7 @@ include_directories( BEFORE SYSTEM "${CMAKE_SOURCE_DIR}/third-party" + "${CMAKE_SOURCE_DIR}/third-party/parsec-vdd" "${CMAKE_SOURCE_DIR}/third-party/moonlight-common-c/enet/include" "${CMAKE_SOURCE_DIR}/third-party/nanors" "${CMAKE_SOURCE_DIR}/third-party/nanors/deps/obl" diff --git a/docs/configuration.md b/docs/configuration.md index 5ef85dc92a3..ce368334fab 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1396,6 +1396,77 @@ editing the `conf` file in a text editor. Use the examples as reference. +
+ + {{ vddStatus.driver_ok ? $t('config.vdd_connected') : $t('config.vdd_not_connected') }} + + v{{ vddStatus.driver_version }} +
+ {{ vddStatus.display_count }} + + {{ $t('config.vdd_refresh') }} + +
{{ display.device_name }}
{{ $t('config.vdd_driver_status') }}
{{ vddStatus.driver_ok ? $t('config.vdd_connected') : $t('config.vdd_not_connected') }} @@ -185,7 +185,7 @@ onMounted(() => {
{{ $t('config.vdd_active_displays') }}
{{ vddStatus.display_count }} @@ -197,7 +197,7 @@ onMounted(() => { - {{ $t('config.vdd_presets') }} + {{ $t('config.vdd_presets') }} { - {{ $t('config.vdd_input_width') }} - {{ $t('config.vdd_input_width') }} + - {{ $t('config.vdd_input_height') }} - {{ $t('config.vdd_input_height') }} + - {{ $t('config.vdd_input_hz') }} - {{ $t('config.vdd_input_hz') }} + diff --git a/third-party/parsec-vdd/parsec-vdd.h b/third-party/parsec-vdd/parsec-vdd.h index 402b8e0cc54..041546996e2 100644 --- a/third-party/parsec-vdd/parsec-vdd.h +++ b/third-party/parsec-vdd/parsec-vdd.h @@ -38,13 +38,185 @@ #endif #ifdef __cplusplus + +#include + namespace parsec_vdd { -#endif // Device helper. ////////////////////////////////////////////////// +enum class DeviceStatus { + OK = 0, // Ready to use + INACCESSIBLE, // Inaccessible + UNKNOWN, // Unknown status + UNKNOWN_PROBLEM, // Unknown problem + DISABLED, // Device is disabled + DRIVER_ERROR, // Device encountered error + RESTART_REQUIRED, // Must restart PC to use (could ignore but would have issue) + DISABLED_SERVICE, // Service is disabled + NOT_INSTALLED // Driver is not installed +}; + +static DeviceStatus DetermineDeviceStatus(ULONG devStatus, ULONG devProblemNum) +{ + if ((devStatus & (DN_DRIVER_LOADED | DN_STARTED)) != 0) + return DeviceStatus::OK; + + if ((devStatus & DN_HAS_PROBLEM) == 0) + return DeviceStatus::UNKNOWN; + + switch (devProblemNum) + { + case CM_PROB_NEED_RESTART: + return DeviceStatus::RESTART_REQUIRED; + case CM_PROB_DISABLED: + case CM_PROB_HARDWARE_DISABLED: + return DeviceStatus::DISABLED; + case CM_PROB_DISABLED_SERVICE: + return DeviceStatus::DISABLED_SERVICE; + default: + return (devProblemNum == CM_PROB_FAILED_POST_START) ? DeviceStatus::DRIVER_ERROR : DeviceStatus::UNKNOWN_PROBLEM; + } +} + +static bool MatchHardwareId(LPCSTR propBuffer, DWORD requiredSize, const char *deviceId) +{ + for (LPCSTR cp = propBuffer; cp && *cp != 0 && cp < (LPCSTR)(propBuffer + requiredSize); cp += lstrlenA(cp) + 1) + { + if (lstrcmpA(deviceId, cp) == 0) + return true; + } + return false; +} + +/** +* Query the driver status. +* +* @param classGuid The GUID of the class. +* @param deviceId The device/hardware ID of the driver. +* @return DeviceStatus +*/ +static DeviceStatus QueryDeviceStatus(const GUID *classGuid, const char *deviceId) +{ + SP_DEVINFO_DATA devInfoData; + ZeroMemory(&devInfoData, sizeof(SP_DEVINFO_DATA)); + devInfoData.cbSize = sizeof(SP_DEVINFO_DATA); + + if (auto devInfo = SetupDiGetClassDevsA(classGuid, nullptr, nullptr, DIGCF_PRESENT); devInfo != INVALID_HANDLE_VALUE) + { + DeviceStatus status = DeviceStatus::NOT_INSTALLED; + BOOL foundProp = FALSE; + + for (UINT deviceIndex = 0; SetupDiEnumDeviceInfo(devInfo, deviceIndex, &devInfoData); ++deviceIndex) + { + DWORD requiredSize = 0; + SetupDiGetDeviceRegistryPropertyA(devInfo, &devInfoData, + SPDRP_HARDWAREID, nullptr, nullptr, 0, &requiredSize); + + if (requiredSize == 0) + continue; + + std::vector propBuffer(requiredSize); + DWORD regDataType = 0; + + if (!SetupDiGetDeviceRegistryPropertyA(devInfo, &devInfoData, + SPDRP_HARDWAREID, ®DataType, propBuffer.data(), + requiredSize, &requiredSize)) + continue; + + if (regDataType != REG_SZ && regDataType != REG_MULTI_SZ) + continue; + + if (!MatchHardwareId((LPCSTR)propBuffer.data(), requiredSize, deviceId)) + { + status = DeviceStatus::NOT_INSTALLED; + break; + } + + foundProp = TRUE; + ULONG devStatus = 0; + ULONG devProblemNum = 0; + + if (CM_Get_DevNode_Status(&devStatus, &devProblemNum, devInfoData.DevInst, 0) != CR_SUCCESS) + { + status = DeviceStatus::NOT_INSTALLED; + break; + } + + status = DetermineDeviceStatus(devStatus, devProblemNum); + break; + } + + if (!foundProp && GetLastError() != 0) + status = DeviceStatus::NOT_INSTALLED; + + SetupDiDestroyDeviceInfoList(devInfo); + return status; + } + + return DeviceStatus::INACCESSIBLE; +} + +/** +* Obtain the device handle. +* Returns nullptr or INVALID_HANDLE_VALUE if fails, otherwise a valid handle. +* Should call CloseDeviceHandle to close this handle after use. +* +* @param interfaceGuid The adapter/interface GUID of the target device. +* @return HANDLE +*/ +static HANDLE OpenDeviceHandle(const GUID *interfaceGuid) +{ + HANDLE handle = INVALID_HANDLE_VALUE; + + if (auto devInfo = SetupDiGetClassDevsA(interfaceGuid, + nullptr, nullptr, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE); devInfo != INVALID_HANDLE_VALUE) + { + SP_DEVICE_INTERFACE_DATA devInterface; + ZeroMemory(&devInterface, sizeof(SP_DEVICE_INTERFACE_DATA)); + devInterface.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA); + + for (DWORD i = 0; SetupDiEnumDeviceInterfaces(devInfo, nullptr, interfaceGuid, i, &devInterface); ++i) + { + DWORD detailSize = 0; + SetupDiGetDeviceInterfaceDetailA(devInfo, &devInterface, nullptr, 0, &detailSize, nullptr); + + std::vector detailBuffer(detailSize); + auto *detail = (SP_DEVICE_INTERFACE_DETAIL_DATA_A *)detailBuffer.data(); + detail->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA_A); + + if (SetupDiGetDeviceInterfaceDetailA(devInfo, &devInterface, detail, detailSize, &detailSize, nullptr)) + { + handle = CreateFileA(detail->DevicePath, + GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, + nullptr, + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL | FILE_FLAG_NO_BUFFERING | FILE_FLAG_OVERLAPPED | FILE_FLAG_WRITE_THROUGH, + nullptr); + + if (handle != nullptr && handle != INVALID_HANDLE_VALUE) + break; + } + } + + SetupDiDestroyDeviceInfoList(devInfo); + } + + return handle; +} + +/* Release the device handle */ +static void CloseDeviceHandle(HANDLE handle) +{ + if (handle != nullptr && handle != INVALID_HANDLE_VALUE) + CloseHandle(handle); +} + +#else // __cplusplus not defined — C fallback + typedef enum { DEVICE_OK = 0, // Ready to use DEVICE_INACCESSIBLE, // Inaccessible @@ -57,13 +229,6 @@ typedef enum { DEVICE_NOT_INSTALLED // Driver is not installed } DeviceStatus; -/** -* Query the driver status. -* -* @param classGuid The GUID of the class. -* @param deviceId The device/hardware ID of the driver. -* @return DeviceStatus -*/ static DeviceStatus QueryDeviceStatus(const GUID *classGuid, const char *deviceId) { DeviceStatus status = DEVICE_INACCESSIBLE; @@ -174,14 +339,6 @@ static DeviceStatus QueryDeviceStatus(const GUID *classGuid, const char *deviceI return status; } -/** -* Obtain the device handle. -* Returns NULL or INVALID_HANDLE_VALUE if fails, otherwise a valid handle. -* Should call CloseDeviceHandle to close this handle after use. -* -* @param interfaceGuid The adapter/interface GUID of the target device. -* @return HANDLE -*/ static HANDLE OpenDeviceHandle(const GUID *interfaceGuid) { HANDLE handle = INVALID_HANDLE_VALUE; @@ -227,46 +384,113 @@ static HANDLE OpenDeviceHandle(const GUID *interfaceGuid) return handle; } -/* Release the device handle */ static void CloseDeviceHandle(HANDLE handle) { if (handle != NULL && handle != INVALID_HANDLE_VALUE) CloseHandle(handle); } +typedef enum { + VDD_IOCTL_ADD = 0x0022e004, + VDD_IOCTL_REMOVE = 0x0022a008, + VDD_IOCTL_UPDATE = 0x0022a00c, + VDD_IOCTL_VERSION = 0x0022e010 +} VddCtlCode; + +static DWORD VddIoControl(HANDLE vdd, VddCtlCode code, const void *data, size_t size) +{ + if (vdd == NULL || vdd == INVALID_HANDLE_VALUE) + return -1; + + BYTE InBuffer[32]; + ZeroMemory(InBuffer, sizeof(InBuffer)); + + OVERLAPPED Overlapped; + ZeroMemory(&Overlapped, sizeof(OVERLAPPED)); + + DWORD OutBuffer = 0; + DWORD NumberOfBytesTransferred; + + if (data != NULL && size > 0) + memcpy(InBuffer, data, (size < sizeof(InBuffer)) ? size : sizeof(InBuffer)); + + Overlapped.hEvent = CreateEventA(NULL, TRUE, FALSE, NULL); + DeviceIoControl(vdd, (DWORD)code, InBuffer, sizeof(InBuffer), &OutBuffer, sizeof(DWORD), NULL, &Overlapped); + + if (!GetOverlappedResultEx(vdd, &Overlapped, &NumberOfBytesTransferred, 5000, FALSE)) + { + CloseHandle(Overlapped.hEvent); + return -1; + } + + if (Overlapped.hEvent != NULL) + CloseHandle(Overlapped.hEvent); + + return OutBuffer; +} + +static int VddVersion(HANDLE vdd) +{ + int minor = VddIoControl(vdd, VDD_IOCTL_VERSION, NULL, 0); + return minor; +} + +static void VddUpdate(HANDLE vdd) +{ + VddIoControl(vdd, VDD_IOCTL_UPDATE, NULL, 0); +} + +static int VddAddDisplay(HANDLE vdd) +{ + int idx = VddIoControl(vdd, VDD_IOCTL_ADD, NULL, 0); + VddUpdate(vdd); + return idx; +} + +static void VddRemoveDisplay(HANDLE vdd, int index) +{ + UINT16 indexData = ((index & 0xFF) << 8) | ((index >> 8) & 0xFF); + VddIoControl(vdd, VDD_IOCTL_REMOVE, &indexData, sizeof(indexData)); + VddUpdate(vdd); +} + +#endif // __cplusplus + // Parsec VDD core. ////////////////////////////////////////////////// // Display name info. -static const char *VDD_DISPLAY_ID = "PSCCDD0"; // You will see it in registry (HKLM\SYSTEM\CurrentControlSet\Enum\DISPLAY) -static const char *VDD_DISPLAY_NAME = "ParsecVDA"; // You will see it in the [Advanced display settings] tab. +static const char * const VDD_DISPLAY_ID = "PSCCDD0"; // You will see it in registry (HKLM\SYSTEM\CurrentControlSet\Enum\DISPLAY) +static const char * const VDD_DISPLAY_NAME = "ParsecVDA"; // You will see it in the [Advanced display settings] tab. // Apdater GUID to obtain the device handle. // {00b41627-04c4-429e-a26e-0265cf50c8fa} static const GUID VDD_ADAPTER_GUID = { 0x00b41627, 0x04c4, 0x429e, { 0xa2, 0x6e, 0x02, 0x65, 0xcf, 0x50, 0xc8, 0xfa } }; -static const char *VDD_ADAPTER_NAME = "Parsec Virtual Display Adapter"; +static const char * const VDD_ADAPTER_NAME = "Parsec Virtual Display Adapter"; // Class and hwid to query device status. // {4d36e968-e325-11ce-bfc1-08002be10318} static const GUID VDD_CLASS_GUID = { 0x4d36e968, 0xe325, 0x11ce, { 0xbf, 0xc1, 0x08, 0x00, 0x2b, 0xe1, 0x03, 0x18 } }; -static const char *VDD_HARDWARE_ID = "Root\\Parsec\\VDA"; +static const char * const VDD_HARDWARE_ID = "Root\\Parsec\\VDA"; // Actually up to 16 devices could be created per adapter // so just use a half to avoid plugging lag. static const int VDD_MAX_DISPLAYS = 8; +#ifdef __cplusplus + // Core IoControl codes, see usage below. -typedef enum { - VDD_IOCTL_ADD = 0x0022e004, // CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800 + 1, METHOD_BUFFERED, FILE_READ_ACCESS | FILE_WRITE_ACCESS) - VDD_IOCTL_REMOVE = 0x0022a008, // CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800 + 2, METHOD_BUFFERED, FILE_WRITE_ACCESS) - VDD_IOCTL_UPDATE = 0x0022a00c, // CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800 + 3, METHOD_BUFFERED, FILE_WRITE_ACCESS) - VDD_IOCTL_VERSION = 0x0022e010 // CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800 + 4, METHOD_BUFFERED, FILE_READ_ACCESS | FILE_WRITE_ACCESS) -} VddCtlCode; +enum class VddCtlCode : DWORD { + ADD = 0x0022e004, // CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800 + 1, METHOD_BUFFERED, FILE_READ_ACCESS | FILE_WRITE_ACCESS) + REMOVE = 0x0022a008, // CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800 + 2, METHOD_BUFFERED, FILE_WRITE_ACCESS) + UPDATE = 0x0022a00c, // CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800 + 3, METHOD_BUFFERED, FILE_WRITE_ACCESS) + VERSION = 0x0022e010 // CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800 + 4, METHOD_BUFFERED, FILE_READ_ACCESS | FILE_WRITE_ACCESS) +}; // Generic DeviceIoControl for all IoControl codes. -static DWORD VddIoControl(HANDLE vdd, VddCtlCode code, const void *data, size_t size) +static DWORD VddIoControl(HANDLE vdd, VddCtlCode code, const BYTE *data, size_t size) { - if (vdd == NULL || vdd == INVALID_HANDLE_VALUE) + if (vdd == nullptr || vdd == INVALID_HANDLE_VALUE) return -1; BYTE InBuffer[32]; @@ -276,13 +500,13 @@ static DWORD VddIoControl(HANDLE vdd, VddCtlCode code, const void *data, size_t ZeroMemory(&Overlapped, sizeof(OVERLAPPED)); DWORD OutBuffer = 0; - DWORD NumberOfBytesTransferred; + DWORD NumberOfBytesTransferred = 0; - if (data != NULL && size > 0) + if (data != nullptr && size > 0) memcpy(InBuffer, data, (size < sizeof(InBuffer)) ? size : sizeof(InBuffer)); - Overlapped.hEvent = CreateEventA(NULL, TRUE, FALSE, NULL); - DeviceIoControl(vdd, (DWORD)code, InBuffer, sizeof(InBuffer), &OutBuffer, sizeof(DWORD), NULL, &Overlapped); + Overlapped.hEvent = CreateEventA(nullptr, TRUE, FALSE, nullptr); + DeviceIoControl(vdd, (DWORD)code, InBuffer, sizeof(InBuffer), &OutBuffer, sizeof(DWORD), nullptr, &Overlapped); if (!GetOverlappedResultEx(vdd, &Overlapped, &NumberOfBytesTransferred, 5000, FALSE)) { @@ -290,7 +514,7 @@ static DWORD VddIoControl(HANDLE vdd, VddCtlCode code, const void *data, size_t return -1; } - if (Overlapped.hEvent != NULL) + if (Overlapped.hEvent != nullptr) CloseHandle(Overlapped.hEvent); return OutBuffer; @@ -304,7 +528,7 @@ static DWORD VddIoControl(HANDLE vdd, VddCtlCode code, const void *data, size_t */ static int VddVersion(HANDLE vdd) { - int minor = VddIoControl(vdd, VDD_IOCTL_VERSION, NULL, 0); + int minor = VddIoControl(vdd, VddCtlCode::VERSION, nullptr, 0); return minor; } @@ -317,7 +541,7 @@ static int VddVersion(HANDLE vdd) */ static void VddUpdate(HANDLE vdd) { - VddIoControl(vdd, VDD_IOCTL_UPDATE, NULL, 0); + VddIoControl(vdd, VddCtlCode::UPDATE, nullptr, 0); } /** @@ -328,7 +552,7 @@ static void VddUpdate(HANDLE vdd) */ static int VddAddDisplay(HANDLE vdd) { - int idx = VddIoControl(vdd, VDD_IOCTL_ADD, NULL, 0); + int idx = VddIoControl(vdd, VddCtlCode::ADD, nullptr, 0); VddUpdate(vdd); return idx; @@ -345,12 +569,12 @@ static void VddRemoveDisplay(HANDLE vdd, int index) // 16-bit BE index UINT16 indexData = ((index & 0xFF) << 8) | ((index >> 8) & 0xFF); - VddIoControl(vdd, VDD_IOCTL_REMOVE, &indexData, sizeof(indexData)); + VddIoControl(vdd, VddCtlCode::REMOVE, (const BYTE *)&indexData, sizeof(indexData)); VddUpdate(vdd); } -#ifdef __cplusplus -} -#endif +} // namespace parsec_vdd + +#endif // __cplusplus -#endif \ No newline at end of file +#endif // __PARSEC_VDD_H From 374c7d03d8781e6c1ca7dd341b84d93791b0c730 Mon Sep 17 00:00:00 2001 From: fatebug <1339524041@qq.com> Date: Thu, 14 May 2026 22:37:22 +0800 Subject: [PATCH 03/10] docs(vdd): add code comments for VDD functions --- src/main.cpp | 7 +++++++ third-party/parsec-vdd/parsec-vdd.h | 9 +++++++++ 2 files changed, 16 insertions(+) diff --git a/src/main.cpp b/src/main.cpp index 761f9c7d9ac..4620016d05a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -122,6 +122,13 @@ void mainThreadLoop(const std::shared_ptr> &shutdown_event) } #ifdef _WIN32 + /** + * @brief Restore virtual displays from the persisted count in config. + * + * Called during startup when a physical display already exists. Reads + * virtual_display_count from config and re-creates that many displays + * at the configured default resolution/refresh. + */ static void restore_persisted_virtual_displays() { int count = config::video.vdd.virtual_display_count; if (count > 0) { diff --git a/third-party/parsec-vdd/parsec-vdd.h b/third-party/parsec-vdd/parsec-vdd.h index 041546996e2..55351de826c 100644 --- a/third-party/parsec-vdd/parsec-vdd.h +++ b/third-party/parsec-vdd/parsec-vdd.h @@ -59,6 +59,11 @@ enum class DeviceStatus { NOT_INSTALLED // Driver is not installed }; +/** + * @brief Map Windows CM device-status flags to a DeviceStatus enum value. + * @param devStatus The DN_* status flags from CM_Get_DevNode_Status. + * @param devProblemNum The CM_PROB_* problem number. + */ static DeviceStatus DetermineDeviceStatus(ULONG devStatus, ULONG devProblemNum) { if ((devStatus & (DN_DRIVER_LOADED | DN_STARTED)) != 0) @@ -81,6 +86,10 @@ static DeviceStatus DetermineDeviceStatus(ULONG devStatus, ULONG devProblemNum) } } +/** + * @brief Walk a REG_MULTI_SZ buffer looking for a matching hardware ID. + * @return true if deviceId is found among the null-terminated strings. + */ static bool MatchHardwareId(LPCSTR propBuffer, DWORD requiredSize, const char *deviceId) { for (LPCSTR cp = propBuffer; cp && *cp != 0 && cp < (LPCSTR)(propBuffer + requiredSize); cp += lstrlenA(cp) + 1) From fe5e09ce5731d45f2ea4434a8926316c35ab932a Mon Sep 17 00:00:00 2001 From: fatebug <1339524041@qq.com> Date: Thu, 14 May 2026 22:51:35 +0800 Subject: [PATCH 04/10] fix(vdd): use std::to_underlying and add missing includes --- src/vdd_control.cpp | 3 +- third-party/parsec-vdd/parsec-vdd.h | 61 ++++++++++++++++------------- 2 files changed, 36 insertions(+), 28 deletions(-) diff --git a/src/vdd_control.cpp b/src/vdd_control.cpp index d9278b8d805..90907a40ed7 100644 --- a/src/vdd_control.cpp +++ b/src/vdd_control.cpp @@ -25,6 +25,7 @@ #include #include #include +#include // lib includes #include @@ -159,7 +160,7 @@ namespace vdd { // Query driver status — informational; non-OK is logged but not fatal. auto status = QueryDeviceStatus(&VDD_CLASS_GUID, VDD_HARDWARE_ID); if (status != DeviceStatus::OK) { - BOOST_LOG(warning) << "VDD: Driver not ready (status="sv << (int)status << ')' << std::endl; + BOOST_LOG(warning) << "VDD: Driver not ready (status="sv << std::to_underlying(status) << ')' << std::endl; } // The real gate: open the device handle. Driver might be usable despite diff --git a/third-party/parsec-vdd/parsec-vdd.h b/third-party/parsec-vdd/parsec-vdd.h index 55351de826c..6cf7b0318c2 100644 --- a/third-party/parsec-vdd/parsec-vdd.h +++ b/third-party/parsec-vdd/parsec-vdd.h @@ -32,15 +32,18 @@ #include #include +#ifdef __cplusplus +#include +#include +#include +#endif + #ifdef _MSC_VER #pragma comment(lib, "cfgmgr32.lib") #pragma comment(lib, "setupapi.lib") #endif #ifdef __cplusplus - -#include - namespace parsec_vdd { @@ -66,23 +69,25 @@ enum class DeviceStatus { */ static DeviceStatus DetermineDeviceStatus(ULONG devStatus, ULONG devProblemNum) { + using enum DeviceStatus; + if ((devStatus & (DN_DRIVER_LOADED | DN_STARTED)) != 0) - return DeviceStatus::OK; + return OK; if ((devStatus & DN_HAS_PROBLEM) == 0) - return DeviceStatus::UNKNOWN; + return UNKNOWN; switch (devProblemNum) { case CM_PROB_NEED_RESTART: - return DeviceStatus::RESTART_REQUIRED; + return RESTART_REQUIRED; case CM_PROB_DISABLED: case CM_PROB_HARDWARE_DISABLED: - return DeviceStatus::DISABLED; + return DISABLED; case CM_PROB_DISABLED_SERVICE: - return DeviceStatus::DISABLED_SERVICE; + return DISABLED_SERVICE; default: - return (devProblemNum == CM_PROB_FAILED_POST_START) ? DeviceStatus::DRIVER_ERROR : DeviceStatus::UNKNOWN_PROBLEM; + return (devProblemNum == CM_PROB_FAILED_POST_START) ? DRIVER_ERROR : UNKNOWN_PROBLEM; } } @@ -92,7 +97,7 @@ static DeviceStatus DetermineDeviceStatus(ULONG devStatus, ULONG devProblemNum) */ static bool MatchHardwareId(LPCSTR propBuffer, DWORD requiredSize, const char *deviceId) { - for (LPCSTR cp = propBuffer; cp && *cp != 0 && cp < (LPCSTR)(propBuffer + requiredSize); cp += lstrlenA(cp) + 1) + for (LPCSTR cp = propBuffer; cp && *cp != 0 && cp < propBuffer + requiredSize; cp += lstrlenA(cp) + 1) { if (lstrcmpA(deviceId, cp) == 0) return true; @@ -109,13 +114,15 @@ static bool MatchHardwareId(LPCSTR propBuffer, DWORD requiredSize, const char *d */ static DeviceStatus QueryDeviceStatus(const GUID *classGuid, const char *deviceId) { + using enum DeviceStatus; + SP_DEVINFO_DATA devInfoData; ZeroMemory(&devInfoData, sizeof(SP_DEVINFO_DATA)); devInfoData.cbSize = sizeof(SP_DEVINFO_DATA); if (auto devInfo = SetupDiGetClassDevsA(classGuid, nullptr, nullptr, DIGCF_PRESENT); devInfo != INVALID_HANDLE_VALUE) { - DeviceStatus status = DeviceStatus::NOT_INSTALLED; + DeviceStatus status = NOT_INSTALLED; BOOL foundProp = FALSE; for (UINT deviceIndex = 0; SetupDiEnumDeviceInfo(devInfo, deviceIndex, &devInfoData); ++deviceIndex) @@ -140,8 +147,8 @@ static DeviceStatus QueryDeviceStatus(const GUID *classGuid, const char *deviceI if (!MatchHardwareId((LPCSTR)propBuffer.data(), requiredSize, deviceId)) { - status = DeviceStatus::NOT_INSTALLED; - break; + status = NOT_INSTALLED; + continue; } foundProp = TRUE; @@ -150,22 +157,23 @@ static DeviceStatus QueryDeviceStatus(const GUID *classGuid, const char *deviceI if (CM_Get_DevNode_Status(&devStatus, &devProblemNum, devInfoData.DevInst, 0) != CR_SUCCESS) { - status = DeviceStatus::NOT_INSTALLED; - break; + status = NOT_INSTALLED; + } + else + { + status = DetermineDeviceStatus(devStatus, devProblemNum); } - - status = DetermineDeviceStatus(devStatus, devProblemNum); break; } if (!foundProp && GetLastError() != 0) - status = DeviceStatus::NOT_INSTALLED; + status = NOT_INSTALLED; SetupDiDestroyDeviceInfoList(devInfo); return status; } - return DeviceStatus::INACCESSIBLE; + return INACCESSIBLE; } /** @@ -178,7 +186,7 @@ static DeviceStatus QueryDeviceStatus(const GUID *classGuid, const char *deviceI */ static HANDLE OpenDeviceHandle(const GUID *interfaceGuid) { - HANDLE handle = INVALID_HANDLE_VALUE; + auto handle = INVALID_HANDLE_VALUE; if (auto devInfo = SetupDiGetClassDevsA(interfaceGuid, nullptr, nullptr, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE); devInfo != INVALID_HANDLE_VALUE) @@ -205,10 +213,10 @@ static HANDLE OpenDeviceHandle(const GUID *interfaceGuid) OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_NO_BUFFERING | FILE_FLAG_OVERLAPPED | FILE_FLAG_WRITE_THROUGH, nullptr); - - if (handle != nullptr && handle != INVALID_HANDLE_VALUE) - break; } + + if (handle != nullptr && handle != INVALID_HANDLE_VALUE) + break; } SetupDiDestroyDeviceInfoList(devInfo); @@ -502,8 +510,7 @@ static DWORD VddIoControl(HANDLE vdd, VddCtlCode code, const BYTE *data, size_t if (vdd == nullptr || vdd == INVALID_HANDLE_VALUE) return -1; - BYTE InBuffer[32]; - ZeroMemory(InBuffer, sizeof(InBuffer)); + std::array InBuffer{}; OVERLAPPED Overlapped; ZeroMemory(&Overlapped, sizeof(OVERLAPPED)); @@ -512,10 +519,10 @@ static DWORD VddIoControl(HANDLE vdd, VddCtlCode code, const BYTE *data, size_t DWORD NumberOfBytesTransferred = 0; if (data != nullptr && size > 0) - memcpy(InBuffer, data, (size < sizeof(InBuffer)) ? size : sizeof(InBuffer)); + memcpy(InBuffer.data(), data, (size < InBuffer.size()) ? size : InBuffer.size()); Overlapped.hEvent = CreateEventA(nullptr, TRUE, FALSE, nullptr); - DeviceIoControl(vdd, (DWORD)code, InBuffer, sizeof(InBuffer), &OutBuffer, sizeof(DWORD), nullptr, &Overlapped); + DeviceIoControl(vdd, std::to_underlying(code), InBuffer.data(), static_cast(InBuffer.size()), &OutBuffer, sizeof(DWORD), nullptr, &Overlapped); if (!GetOverlappedResultEx(vdd, &Overlapped, &NumberOfBytesTransferred, 5000, FALSE)) { From ae160850844e90fcd812abeb3cbb63be4a37d96d Mon Sep 17 00:00:00 2001 From: fatebug <1339524041@qq.com> Date: Fri, 15 May 2026 09:12:26 +0800 Subject: [PATCH 05/10] fix(vdd): use C++17 init-statement for devProblemNum --- third-party/parsec-vdd/parsec-vdd.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/third-party/parsec-vdd/parsec-vdd.h b/third-party/parsec-vdd/parsec-vdd.h index 6cf7b0318c2..0aaf4ab0c82 100644 --- a/third-party/parsec-vdd/parsec-vdd.h +++ b/third-party/parsec-vdd/parsec-vdd.h @@ -153,9 +153,8 @@ static DeviceStatus QueryDeviceStatus(const GUID *classGuid, const char *deviceI foundProp = TRUE; ULONG devStatus = 0; - ULONG devProblemNum = 0; - if (CM_Get_DevNode_Status(&devStatus, &devProblemNum, devInfoData.DevInst, 0) != CR_SUCCESS) + if (ULONG devProblemNum = 0; CM_Get_DevNode_Status(&devStatus, &devProblemNum, devInfoData.DevInst, 0) != CR_SUCCESS) { status = NOT_INSTALLED; } From a0092ccc51ec03c0661250f6d5d058c4ee7a9536 Mon Sep 17 00:00:00 2001 From: fatebug <1339524041@qq.com> Date: Fri, 15 May 2026 09:32:17 +0800 Subject: [PATCH 06/10] feat(vdd): persist per-display configs and add auto-start toggle - Store each virtual display's resolution/refresh as JSON in config - Restore per-display configs on startup, not just a flat count - Add vdd_enabled toggle in Web UI for auto-start control --- src/config.cpp | 2 + src/config.h | 1 + src/main.cpp | 30 +++++++++++-- src/vdd_control.cpp | 45 ++++++++++++++----- .../web/configs/tabs/VirtualDisplay.vue | 10 +++++ 5 files changed, 74 insertions(+), 14 deletions(-) diff --git a/src/config.cpp b/src/config.cpp index 1f998bafa5d..e0510d58745 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -530,6 +530,7 @@ namespace config { 1080, // virtual_display_height 144, // virtual_display_refresh_rate 0, // virtual_display_count + {}, // virtual_display_configs }, // vdd 0, // max_bitrate @@ -1204,6 +1205,7 @@ namespace config { int_f(vars, "vdd_height", video.vdd.virtual_display_height); int_f(vars, "vdd_refresh_rate", video.vdd.virtual_display_refresh_rate); int_f(vars, "vdd_display_count", video.vdd.virtual_display_count); + string_f(vars, "vdd_display_configs", video.vdd.virtual_display_configs); { int value = 0; int_between_f(vars, "dd_wa_hdr_toggle_delay", value, {0, 3000}); diff --git a/src/config.h b/src/config.h index f2bd3d1e7c7..12c7d62edf4 100644 --- a/src/config.h +++ b/src/config.h @@ -158,6 +158,7 @@ namespace config { int virtual_display_height; ///< Virtual display height int virtual_display_refresh_rate; ///< Virtual display refresh rate int virtual_display_count; ///< Number of persisted virtual displays to restore on startup + std::string virtual_display_configs; ///< Persisted virtual display configurations (JSON array) } vdd; int max_bitrate; // Maximum bitrate, sets ceiling in kbps for bitrate requested from client diff --git a/src/main.cpp b/src/main.cpp index 4620016d05a..0049aa958bd 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -9,6 +9,9 @@ #include #include +// lib includes +#include + #ifdef __APPLE__ #include #endif @@ -123,13 +126,34 @@ void mainThreadLoop(const std::shared_ptr> &shutdown_event) #ifdef _WIN32 /** - * @brief Restore virtual displays from the persisted count in config. + * @brief Restore virtual displays from persisted config. * * Called during startup when a physical display already exists. Reads - * virtual_display_count from config and re-creates that many displays - * at the configured default resolution/refresh. + * vdd_display_configs (JSON array) from config and re-creates each + * display at its previously saved resolution/refresh. Falls back to + * the count-based approach with default config values if JSON is empty. */ static void restore_persisted_virtual_displays() { + const auto &json_str = config::video.vdd.virtual_display_configs; + if (!json_str.empty()) { + try { + auto configs = nlohmann::json::parse(json_str); + if (configs.is_array() && !configs.empty()) { + BOOST_LOG(info) << "Restoring "sv << configs.size() << " persisted virtual display(s) with saved configs"sv; + for (const auto &cfg : configs) { + int w = cfg.value("w", config::video.vdd.virtual_display_width); + int h = cfg.value("h", config::video.vdd.virtual_display_height); + int hz = cfg.value("hz", config::video.vdd.virtual_display_refresh_rate); + vdd::add_display(w, h, hz); + } + return; + } + } catch (const std::exception &e) { + BOOST_LOG(warning) << "Failed to parse vdd_display_configs: "sv << e.what(); + } + } + + // Fallback: restore by count with default resolution int count = config::video.vdd.virtual_display_count; if (count > 0) { BOOST_LOG(info) << "Restoring "sv << count << " persisted virtual display(s)"sv; diff --git a/src/vdd_control.cpp b/src/vdd_control.cpp index 90907a40ed7..6bc997792dc 100644 --- a/src/vdd_control.cpp +++ b/src/vdd_control.cpp @@ -70,18 +70,30 @@ namespace vdd { std::vector g_vdd_configs; // NOSONAR -- runtime collection /** - * @brief Persist the current display count to the config file. + * @brief Persist display count and individual configurations to the config file. * - * Edits only the vdd_display_count line in-place; preserves user comments, - * blank lines, and option ordering. + * In-place edit of vdd_display_count and vdd_display_configs lines; + * preserves comments, blank lines, and option ordering. */ void persist_display_count() { auto content = file_handler::read_file(config::sunshine.config_file.c_str()); - auto new_value = std::to_string(g_vdd_indices.size()); + auto count_str = std::to_string(g_vdd_indices.size()); + + // Build JSON array of individual display configs + std::string configs_json = "["; + for (size_t i = 0; i < g_vdd_configs.size(); ++i) { + if (i > 0) configs_json += ','; + configs_json += std::format(R"({{"w":{},"h":{},"hz":{}}})", + g_vdd_configs[i].width, + g_vdd_configs[i].height, + g_vdd_configs[i].hz); + } + configs_json += ']'; std::string line; std::stringstream result; - bool found = false; + bool found_count = false; + bool found_configs = false; for (size_t i = 0; i <= content.size(); ++i) { bool at_end = (i == content.size()); @@ -89,9 +101,18 @@ namespace vdd { line += content[i]; continue; } - if (!line.empty() && line.rfind("vdd_display_count", 0) == 0) { - result << "vdd_display_count = "sv << new_value << '\n'; - found = true; + + // Trim leading whitespace for key matching + std::string_view sv(line); + auto start = sv.find_first_not_of(" \t"); + if (start != std::string_view::npos) sv = sv.substr(start); + + if (sv.starts_with("vdd_display_count =") || sv == "vdd_display_count") { + result << "vdd_display_count = "sv << count_str << '\n'; + found_count = true; + } else if (sv.starts_with("vdd_display_configs =") || sv == "vdd_display_configs") { + result << "vdd_display_configs = "sv << configs_json << '\n'; + found_configs = true; } else { result << line; if (!at_end) result << '\n'; @@ -99,9 +120,11 @@ namespace vdd { line.clear(); } - // Key not in config yet — append it. - if (!found) { - result << "vdd_display_count = "sv << new_value << '\n'; + if (!found_count) { + result << "vdd_display_count = "sv << count_str << '\n'; + } + if (!found_configs) { + result << "vdd_display_configs = "sv << configs_json << '\n'; } file_handler::write_file(config::sunshine.config_file.c_str(), result.str()); diff --git a/src_assets/common/assets/web/configs/tabs/VirtualDisplay.vue b/src_assets/common/assets/web/configs/tabs/VirtualDisplay.vue index dc162c63d6b..61844603c04 100644 --- a/src_assets/common/assets/web/configs/tabs/VirtualDisplay.vue +++ b/src_assets/common/assets/web/configs/tabs/VirtualDisplay.vue @@ -195,6 +195,16 @@ onMounted(() => { + + + + + {{ $t('config.vdd_enabled') }} + + + {{ $t('config.vdd_enabled_desc') }} + {{ $t('config.vdd_presets') }} From ae381759e0e3f058f7579f2ed588e8f8ec60a85a Mon Sep 17 00:00:00 2001 From: fatebug <1339524041@qq.com> Date: Fri, 15 May 2026 10:08:27 +0800 Subject: [PATCH 07/10] test: mark vdd_display_configs as internal config option Add vdd_display_configs to the internal options list in config consistency tests so the test doesn't fail on the new auto-managed VDD persistence config key. --- tests/integration/test_config_consistency.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/integration/test_config_consistency.cpp b/tests/integration/test_config_consistency.cpp index 821c5923df7..05b4d18743f 100644 --- a/tests/integration/test_config_consistency.cpp +++ b/tests/integration/test_config_consistency.cpp @@ -445,7 +445,8 @@ TEST_F(ConfigConsistencyTest, AllConfigOptionsExistInAllFiles) { // Options that are internal/special and shouldn't be in UI/docs const std::set> internalOptions = { - "flags" // Internal config flags, not user-configurable + "flags", // Internal config flags, not user-configurable + "vdd_display_configs" // Automatically managed by VDD persistence }; std::vector missingFromFiles; @@ -627,7 +628,8 @@ TEST_F(ConfigConsistencyTest, TestFrameworkDetectsMissingOptions) { // Options that are internal/special and shouldn't be in UI/docs std::set> internalOptions = { - "flags" // Internal config flags, not user-configurable + "flags", // Internal config flags, not user-configurable + "vdd_display_configs" // Automatically managed by VDD persistence }; std::vector missingFromFiles; From fff4efb437180abb3dbf5438f0fd3803601fce72 Mon Sep 17 00:00:00 2001 From: fatebug <1339524041@qq.com> Date: Fri, 15 May 2026 11:51:31 +0800 Subject: [PATCH 08/10] fix(confighttp): merge config on save instead of overwrite --- src/confighttp.cpp | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/confighttp.cpp b/src/confighttp.cpp index 06c5ff6057b..4b8023b06ec 100644 --- a/src/confighttp.cpp +++ b/src/confighttp.cpp @@ -1057,17 +1057,24 @@ namespace confighttp { ss << request->content.rdbuf(); try { // TODO: Input Validation - std::stringstream config_stream; nlohmann::json output_tree; nlohmann::json input_tree = nlohmann::json::parse(ss); + + // Merge into existing config: read current file, update with POST values, + // write back. This preserves keys (like vdd_display_configs) that are + // managed outside the web UI. + auto vars = config::parse_config(file_handler::read_file(config::sunshine.config_file.c_str())); for (const auto &[k, v] : input_tree.items()) { if (v.is_null() || (v.is_string() && v.get().empty())) { + vars.erase(k); continue; } + vars[k] = v.is_string() ? v.get() : v.dump(); + } - // v.dump() will dump valid json, which we do not want for strings in the config, right now - // we should migrate the config file to straight JSON and get rid of all this nonsense - config_stream << k << " = " << (v.is_string() ? v.get() : v.dump()) << std::endl; + std::stringstream config_stream; + for (const auto &[k, v] : vars) { + config_stream << k << " = " << v << '\n'; } file_handler::write_file(config::sunshine.config_file.c_str(), config_stream.str()); output_tree["status"] = true; From 4652fdbd2274d654f1b25d9c6b773fa4391ba8ef Mon Sep 17 00:00:00 2001 From: fatebug <1339524041@qq.com> Date: Fri, 15 May 2026 13:09:42 +0800 Subject: [PATCH 09/10] fix: narrow exception type and minor C++17 cleanup - Catch nlohmann::json::parse_error instead of generic std::exception - Use C++17 init-statement in vdd config parsing loop --- src/main.cpp | 2 +- src/vdd_control.cpp | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 0049aa958bd..1d8e6bcfe35 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -148,7 +148,7 @@ void mainThreadLoop(const std::shared_ptr> &shutdown_event) } return; } - } catch (const std::exception &e) { + } catch (const nlohmann::json::parse_error &e) { BOOST_LOG(warning) << "Failed to parse vdd_display_configs: "sv << e.what(); } } diff --git a/src/vdd_control.cpp b/src/vdd_control.cpp index 6bc997792dc..1f1b73b4b2c 100644 --- a/src/vdd_control.cpp +++ b/src/vdd_control.cpp @@ -104,8 +104,7 @@ namespace vdd { // Trim leading whitespace for key matching std::string_view sv(line); - auto start = sv.find_first_not_of(" \t"); - if (start != std::string_view::npos) sv = sv.substr(start); + if (auto start = sv.find_first_not_of(" \t"); start != std::string_view::npos) sv = sv.substr(start); if (sv.starts_with("vdd_display_count =") || sv == "vdd_display_count") { result << "vdd_display_count = "sv << count_str << '\n'; From 23fd2a5e8fd70ada67a6f04f883d9b1762358c54 Mon Sep 17 00:00:00 2001 From: fatebug <1339524041@qq.com> Date: Fri, 15 May 2026 15:47:59 +0800 Subject: [PATCH 10/10] refactor(vdd): move vdd_control to src/platform/windows/ Relocate Windows-only virtual display driver control files to the platform directory. Update include paths and CMake references. --- cmake/compile_definitions/common.cmake | 4 ++-- src/confighttp.cpp | 2 +- src/main.cpp | 2 +- src/{ => platform/windows}/vdd_control.cpp | 10 +++++----- src/{ => platform/windows}/vdd_control.h | 2 +- src/system_tray.cpp | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) rename src/{ => platform/windows}/vdd_control.cpp (99%) rename src/{ => platform/windows}/vdd_control.h (98%) diff --git a/cmake/compile_definitions/common.cmake b/cmake/compile_definitions/common.cmake index 19d662c268c..fd25d89fcd3 100644 --- a/cmake/compile_definitions/common.cmake +++ b/cmake/compile_definitions/common.cmake @@ -108,8 +108,8 @@ set(SUNSHINE_TARGET_FILES "${CMAKE_SOURCE_DIR}/src/network.cpp" "${CMAKE_SOURCE_DIR}/src/network.h" "${CMAKE_SOURCE_DIR}/src/move_by_copy.h" - "${CMAKE_SOURCE_DIR}/src/vdd_control.h" - "${CMAKE_SOURCE_DIR}/src/vdd_control.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/windows/vdd_control.h" + "${CMAKE_SOURCE_DIR}/src/platform/windows/vdd_control.cpp" "${CMAKE_SOURCE_DIR}/src/system_tray.cpp" "${CMAKE_SOURCE_DIR}/src/system_tray.h" "${CMAKE_SOURCE_DIR}/src/task_pool.h" diff --git a/src/confighttp.cpp b/src/confighttp.cpp index 4b8023b06ec..4645434cba7 100644 --- a/src/confighttp.cpp +++ b/src/confighttp.cpp @@ -40,7 +40,7 @@ #include "network.h" #ifdef _WIN32 - #include "vdd_control.h" + #include "platform/windows/vdd_control.h" #endif #include "nvhttp.h" #include "platform/common.h" diff --git a/src/main.cpp b/src/main.cpp index 1d8e6bcfe35..46cf0333050 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -28,7 +28,7 @@ #include "process.h" #include "system_tray.h" #include "upnp.h" -#include "vdd_control.h" +#include "platform/windows/vdd_control.h" #include "video.h" extern "C" { diff --git a/src/vdd_control.cpp b/src/platform/windows/vdd_control.cpp similarity index 99% rename from src/vdd_control.cpp rename to src/platform/windows/vdd_control.cpp index 1f1b73b4b2c..97b7ab571ff 100644 --- a/src/vdd_control.cpp +++ b/src/platform/windows/vdd_control.cpp @@ -1,5 +1,5 @@ /** - * @file src/vdd_control.cpp + * @file src/platform/windows/vdd_control.cpp * @brief Parsec Virtual Display Driver (VDD) control — wraps the parsec-vdd * kernel driver to create and manage virtual displays on Windows. * @@ -32,10 +32,10 @@ #include // local includes -#include "config.h" -#include "file_handler.h" -#include "logging.h" -#include "platform/common.h" +#include "src/config.h" +#include "src/file_handler.h" +#include "src/logging.h" +#include "src/platform/common.h" // parsec-vdd core header #include diff --git a/src/vdd_control.h b/src/platform/windows/vdd_control.h similarity index 98% rename from src/vdd_control.h rename to src/platform/windows/vdd_control.h index c51f12c4cce..6edf75b60fa 100644 --- a/src/vdd_control.h +++ b/src/platform/windows/vdd_control.h @@ -1,5 +1,5 @@ /** - * @file src/vdd_control.h + * @file src/platform/windows/vdd_control.h * @brief Declarations for Parsec Virtual Display Driver control. */ #pragma once diff --git a/src/system_tray.cpp b/src/system_tray.cpp index c12e65ae271..349c271aa5f 100644 --- a/src/system_tray.cpp +++ b/src/system_tray.cpp @@ -48,7 +48,7 @@ #include "platform/common.h" #include "process.h" #include "src/entry_handler.h" - #include "vdd_control.h" + #include "platform/windows/vdd_control.h" using namespace std::literals;
{{ $t('config.vdd_presets') }}
{{ $t('config.vdd_enabled_desc') }}