├── .gitignore ├── LICENSE ├── README.md ├── parsec-vdd.cc ├── parsec-vdd.h └── regedit.png /.gitignore: -------------------------------------------------------------------------------- 1 | *.exe 2 | *.bat -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 HaliComing 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### ~~使命[已结束]~~ 2 | ~~等待[ParsecVDisplay App](https://github.com/nomi-san/parsec-vdd)的发布后,本程序预计也会随即停止更新。~~ 3 | 4 | 令人兴奋![ParsecVDisplay App](https://github.com/nomi-san/parsec-vdd)已发布!快去围观。在没有特殊需求的情况下,本仓库将暂停进一步的更新。 5 | 6 | # parsec-vdd-cli 命令行程序 7 | 8 | > 使命结束不意味着 parsec-vdd-cli 程序无法使用。 9 | 10 | > 使用前请安装 parsec-vdd 驱动。 11 | 12 | - [parsec-vdd-v0.38](https://builds.parsec.app/vdd/parsec-vdd-0.38.0.0.exe) 13 | - [parsec-vdd-v0.41](https://builds.parsec.app/vdd/parsec-vdd-0.41.0.0.exe) (recommended) 14 | 15 | ## 使用 16 | ``` 17 | parsec-vdd-cli.exe [-a] 18 | -a add a virtual display at startup. 19 | ``` 20 | 使用-a启动参数启动时将自动增加一个虚拟显示屏,当前代码虚拟显示屏上限8个。 21 | 22 | ### 可使用bat脚本启动 23 | parsec-vdd-cli.bat 24 | ``` 25 | @echo off 26 | parsec-vdd-cli.exe -a 27 | ``` 28 | 29 | ## 关于Easy-Virtual-Display 30 | 本软件可替代Easy-Virtual-Display(evd)使用,evd为托盘程序且有弹框,不是很方便,所以这才有了这个命令行程序。 31 | 32 | Easy-Virtual-Display程序自带的是旧版本的虚拟显示器驱动。parsec-vdd-cli兼容新旧版本驱动。无需卸载旧驱动。如需安装新驱动,下载parsec-vdd-0.41.0.0.exe,直接安装,安装会默认卸载旧版本驱动。 33 | 34 | ## 自定义分辨率 35 | 连接之前,虚拟显示器会在`HKEY_LOCAL_MACHINE\SOFTWARE\Parsec\vdd`注册表中查找其他预设分辨率。最多支持5个值。如果需要更多,需要自行修补驱动程序DLL。 36 | 37 | ``` 38 | SOFTWARE\Parsec\vdd 39 | key: 0 -> 5 | (width, height, hz) 40 | ``` 41 | 42 | 参考图 43 | ![自定义分辨率注册表参考图](regedit.png) 44 | 45 | ## 成品 46 | 请访问[Release](https://github.com/HaliComing/parsec-vdd-cli/releases)页面下载 47 | 48 | 国内下载加速,请访问GitHub 文件加速https://ghproxy.markxu.online/ 49 | ## 编译 50 | use gcc:`gcc -o parsec-vdd-cli.exe -static parsec-vdd.cc -lsetupapi -lstdc++` 51 | 52 | use g++:`g++ -o parsec-vdd-cli.exe -static parsec-vdd.cc -lsetupapi` 53 | 54 | ## 参考 55 | 56 | cli源码来自 https://github.com/nomi-san/parsec-vdd 57 | 58 | 本文中Easy-Virtual-Display指的是 https://github.com/KtzeAbyss/Easy-Virtual-Display/ 59 | -------------------------------------------------------------------------------- /parsec-vdd.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "parsec-vdd.h" 7 | 8 | using namespace std::chrono_literals; 9 | using namespace parsec_vdd; 10 | 11 | int main(int argc,char *argv[]) 12 | { 13 | // Check driver status. 14 | DeviceStatus status = QueryDeviceStatus(&VDD_CLASS_GUID, VDD_HARDWARE_ID); 15 | if (status != DEVICE_OK) 16 | { 17 | printf("Parsec VDD device is not OK, got status %d.\n", status); 18 | return 1; 19 | } 20 | 21 | // Obtain device handle. 22 | HANDLE vdd = OpenDeviceHandle(&VDD_ADAPTER_GUID); 23 | if (vdd == NULL || vdd == INVALID_HANDLE_VALUE) { 24 | printf("Failed to obtain the device handle.\n"); 25 | return 1; 26 | } 27 | 28 | bool running = true; 29 | std::vector displays; 30 | 31 | // Side thread for updating vdd. 32 | std::thread updater([&running, vdd] { 33 | while (running) { 34 | VddUpdate(vdd); 35 | std::this_thread::sleep_for(100ms); 36 | } 37 | }); 38 | 39 | updater.detach(); 40 | 41 | // Print out guide. 42 | printf("parsec-vdd-cli.exe [-a]\n"); 43 | printf(" -a add a virtual display at startup.\n"); 44 | printf("\n"); 45 | 46 | printf("Press A to add a virtual display.\n"); 47 | printf("Press R to remove the last added.\n"); 48 | printf("Press Q to quit (then unplug all).\n\n"); 49 | 50 | if (running) { 51 | if (argc == 2 && strcmp(argv[1], "-a") == 0 ) { 52 | if (displays.size() < VDD_MAX_DISPLAYS) { 53 | int index = VddAddDisplay(vdd); 54 | displays.push_back(index); 55 | printf("Added a new virtual display, index: %d.\n", index); 56 | } 57 | else { 58 | printf("Limit exceeded (%d), could not add more virtual displays.\n", VDD_MAX_DISPLAYS); 59 | } 60 | } 61 | } 62 | 63 | while (running) { 64 | switch (_getch()) { 65 | // quit 66 | case 'q': 67 | running = false; 68 | break; 69 | // add display 70 | case 'a': 71 | if (displays.size() < VDD_MAX_DISPLAYS) { 72 | int index = VddAddDisplay(vdd); 73 | displays.push_back(index); 74 | printf("Added a new virtual display, index: %d.\n", index); 75 | } 76 | else { 77 | printf("Limit exceeded (%d), could not add more virtual displays.\n", VDD_MAX_DISPLAYS); 78 | } 79 | break; 80 | // remove display 81 | case 'r': 82 | if (displays.size() > 0) { 83 | int index = displays.back(); 84 | VddRemoveDisplay(vdd, index); 85 | displays.pop_back(); 86 | printf("Removed the last virtual display, index: %d.\n", index); 87 | } 88 | else { 89 | printf("No added virtual displays.\n"); 90 | } 91 | break; 92 | } 93 | } 94 | 95 | // Remove all before exiting. 96 | for (int index : displays) { 97 | VddRemoveDisplay(vdd, index); 98 | } 99 | 100 | if (updater.joinable()) { 101 | updater.join(); 102 | } 103 | 104 | // Close the device handle. 105 | CloseDeviceHandle(vdd); 106 | 107 | return 0; 108 | } -------------------------------------------------------------------------------- /parsec-vdd.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, Nguyen Duy All rights reserved. 3 | * GitHub repo: https://github.com/nomi-san/parsec-vdd/ 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions are met: 7 | * 8 | * * Redistributions of source code must retain the above copyright notice, 9 | * this list of conditions and the following disclaimer. 10 | * * Redistributions in binary form must reproduce the above copyright 11 | * notice, this list of conditions and the following disclaimer in the 12 | * documentation and/or other materials provided with the distribution. 13 | * 14 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 18 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 19 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 20 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 21 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 22 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 23 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | * POSSIBILITY OF SUCH DAMAGE. 25 | * 26 | */ 27 | 28 | #ifndef __PARSEC_VDD_H 29 | #define __PARSEC_VDD_H 30 | 31 | #include 32 | #include 33 | #include 34 | 35 | #ifdef _MSC_VER 36 | #pragma comment(lib, "cfgmgr32.lib") 37 | #pragma comment(lib, "setupapi.lib") 38 | #endif 39 | 40 | #ifdef __cplusplus 41 | namespace parsec_vdd 42 | { 43 | #endif 44 | 45 | // Device helper. 46 | ////////////////////////////////////////////////// 47 | 48 | typedef enum { 49 | DEVICE_OK = 0, // Ready to use 50 | DEVICE_INACCESSIBLE, // Inaccessible 51 | DEVICE_UNKNOW, // Unknow status 52 | DEVICE_UNKNOW_PROBLEM, // Unknow problem 53 | DEVICE_DISABLED, // Device is disabled 54 | DEVICE_DRIVER_ERROR, // Device encountered error 55 | DEVICE_RESTART_REQUIRED, // Must restart PC to use (could ignore but would have issue) 56 | DEVICE_DISABLED_SERVICE, // Service is disabled 57 | DEVICE_NOT_INSTALLED // Driver is not installed 58 | } DeviceStatus; 59 | 60 | /** 61 | * Query the driver status. 62 | * 63 | * @param classGuid The GUID of the class. 64 | * @param deviceId The device/hardware ID of the driver. 65 | * @return DeviceStatus 66 | */ 67 | static DeviceStatus QueryDeviceStatus(const GUID *classGuid, const char *deviceId) 68 | { 69 | DeviceStatus status = DEVICE_INACCESSIBLE; 70 | 71 | SP_DEVINFO_DATA devInfoData; 72 | ZeroMemory(&devInfoData, sizeof(SP_DEVINFO_DATA)); 73 | devInfoData.cbSize = sizeof(SP_DEVINFO_DATA); 74 | 75 | HDEVINFO devInfo = SetupDiGetClassDevsA(classGuid, NULL, NULL, DIGCF_PRESENT); 76 | 77 | if (devInfo != INVALID_HANDLE_VALUE) 78 | { 79 | BOOL foundProp = FALSE; 80 | UINT deviceIndex = 0; 81 | 82 | do 83 | { 84 | if (!SetupDiEnumDeviceInfo(devInfo, deviceIndex, &devInfoData)) 85 | break; 86 | 87 | DWORD requiredSize = 0; 88 | SetupDiGetDeviceRegistryPropertyA(devInfo, &devInfoData, 89 | SPDRP_HARDWAREID, NULL, NULL, 0, &requiredSize); 90 | 91 | if (requiredSize > 0) 92 | { 93 | DWORD regDataType = 0; 94 | LPBYTE propBuffer = (LPBYTE)calloc(1, requiredSize); 95 | 96 | if (SetupDiGetDeviceRegistryPropertyA( 97 | devInfo, 98 | &devInfoData, 99 | SPDRP_HARDWAREID, 100 | ®DataType, 101 | propBuffer, 102 | requiredSize, 103 | &requiredSize)) 104 | { 105 | if (regDataType == REG_SZ || regDataType == REG_MULTI_SZ) 106 | { 107 | for (LPCSTR cp = (LPCSTR)propBuffer; ; cp += lstrlenA(cp) + 1) 108 | { 109 | if (!cp || *cp == 0 || cp >= (LPCSTR)(propBuffer + requiredSize)) 110 | { 111 | status = DEVICE_NOT_INSTALLED; 112 | goto except; 113 | } 114 | 115 | if (lstrcmpA(deviceId, cp) == 0) 116 | break; 117 | } 118 | 119 | foundProp = TRUE; 120 | ULONG devStatus, devProblemNum; 121 | 122 | if (CM_Get_DevNode_Status(&devStatus, &devProblemNum, devInfoData.DevInst, 0) != CR_SUCCESS) 123 | { 124 | status = DEVICE_NOT_INSTALLED; 125 | goto except; 126 | } 127 | 128 | if ((devStatus & (DN_DRIVER_LOADED | DN_STARTED)) != 0) 129 | { 130 | status = DEVICE_OK; 131 | } 132 | else if ((devStatus & DN_HAS_PROBLEM) != 0) 133 | { 134 | switch (devProblemNum) 135 | { 136 | case CM_PROB_NEED_RESTART: 137 | status = DEVICE_RESTART_REQUIRED; 138 | break; 139 | case CM_PROB_DISABLED: 140 | case CM_PROB_HARDWARE_DISABLED: 141 | status = DEVICE_DISABLED; 142 | break; 143 | case CM_PROB_DISABLED_SERVICE: 144 | status = DEVICE_DISABLED_SERVICE; 145 | break; 146 | default: 147 | if (devProblemNum == CM_PROB_FAILED_POST_START) 148 | status = DEVICE_DRIVER_ERROR; 149 | else 150 | status = DEVICE_UNKNOW_PROBLEM; 151 | break; 152 | } 153 | } 154 | else 155 | { 156 | status = DEVICE_UNKNOW; 157 | } 158 | } 159 | } 160 | 161 | except: 162 | free(propBuffer); 163 | } 164 | 165 | ++deviceIndex; 166 | } while (!foundProp); 167 | 168 | if (!foundProp && GetLastError() != 0) 169 | status = DEVICE_NOT_INSTALLED; 170 | 171 | SetupDiDestroyDeviceInfoList(devInfo); 172 | } 173 | 174 | return status; 175 | } 176 | 177 | /** 178 | * Obtain the device handle. 179 | * Returns NULL or INVALID_HANDLE_VALUE if fails, otherwise a valid handle. 180 | * Should call CloseDeviceHandle to close this handle after use. 181 | * 182 | * @param interfaceGuid The adapter/interface GUID of the target device. 183 | * @return HANDLE 184 | */ 185 | static HANDLE OpenDeviceHandle(const GUID *interfaceGuid) 186 | { 187 | HANDLE handle = INVALID_HANDLE_VALUE; 188 | HDEVINFO devInfo = SetupDiGetClassDevsA(interfaceGuid, 189 | NULL, NULL, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE); 190 | 191 | if (devInfo != INVALID_HANDLE_VALUE) 192 | { 193 | SP_DEVICE_INTERFACE_DATA devInterface; 194 | ZeroMemory(&devInterface, sizeof(SP_DEVICE_INTERFACE_DATA)); 195 | devInterface.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA); 196 | 197 | for (DWORD i = 0; SetupDiEnumDeviceInterfaces(devInfo, NULL, interfaceGuid, i, &devInterface); ++i) 198 | { 199 | DWORD detailSize = 0; 200 | SetupDiGetDeviceInterfaceDetailA(devInfo, &devInterface, NULL, 0, &detailSize, NULL); 201 | 202 | SP_DEVICE_INTERFACE_DETAIL_DATA_A *detail = (SP_DEVICE_INTERFACE_DETAIL_DATA_A *)calloc(1, detailSize); 203 | detail->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA_A); 204 | 205 | if (SetupDiGetDeviceInterfaceDetailA(devInfo, &devInterface, detail, detailSize, &detailSize, NULL)) 206 | { 207 | handle = CreateFileA(detail->DevicePath, 208 | GENERIC_READ | GENERIC_WRITE, 209 | FILE_SHARE_READ | FILE_SHARE_WRITE, 210 | NULL, 211 | OPEN_EXISTING, 212 | FILE_ATTRIBUTE_NORMAL | FILE_FLAG_NO_BUFFERING | FILE_FLAG_OVERLAPPED | FILE_FLAG_WRITE_THROUGH, 213 | NULL); 214 | 215 | if (handle != NULL && handle != INVALID_HANDLE_VALUE) 216 | break; 217 | } 218 | 219 | free(detail); 220 | } 221 | 222 | SetupDiDestroyDeviceInfoList(devInfo); 223 | } 224 | 225 | return handle; 226 | } 227 | 228 | /* Release the device handle */ 229 | static void CloseDeviceHandle(HANDLE handle) 230 | { 231 | if (handle != NULL && handle != INVALID_HANDLE_VALUE) 232 | CloseHandle(handle); 233 | } 234 | 235 | // Parsec VDD core. 236 | ////////////////////////////////////////////////// 237 | 238 | // Display name info. 239 | static const char *VDD_DISPLAY_ID = "PSCCDD0"; // You will see it in registry (HKLM\SYSTEM\CurrentControlSet\Enum\DISPLAY) 240 | static const char *VDD_DISPLAY_NAME = "ParsecVDA"; // You will see it in the [Advanced display settings] tab. 241 | 242 | // Apdater GUID to obtain the device handle. 243 | // {00b41627-04c4-429e-a26e-0265cf50c8fa} 244 | static const GUID VDD_ADAPTER_GUID = { 0x00b41627, 0x04c4, 0x429e, { 0xa2, 0x6e, 0x02, 0x65, 0xcf, 0x50, 0xc8, 0xfa } }; 245 | static const char *VDD_ADAPTER_NAME = "Parsec Virtual Display Adapter"; 246 | 247 | // Class and hwid to query device status. 248 | // {4d36e968-e325-11ce-bfc1-08002be10318} 249 | static const GUID VDD_CLASS_GUID = { 0x4d36e968, 0xe325, 0x11ce, { 0xbf, 0xc1, 0x08, 0x00, 0x2b, 0xe1, 0x03, 0x18 } }; 250 | static const char *VDD_HARDWARE_ID = "Root\\Parsec\\VDA"; 251 | 252 | // Actually up to 16 devices could be created per adapter 253 | // so just use a half to avoid plugging lag. 254 | static const int VDD_MAX_DISPLAYS = 8; 255 | 256 | // Core IoControl codes, see usage below. 257 | typedef enum { 258 | VDD_IOCTL_ADD = 0x0022e004, 259 | VDD_IOCTL_REMOVE = 0x0022a008, 260 | VDD_IOCTL_UPDATE = 0x0022a00c, 261 | VDD_IOCTL_VERSION = 0x0022e010, 262 | } VddCtlCode; 263 | 264 | // Generic DeviceIoControl for all IoControl codes. 265 | static DWORD VddIoControl(HANDLE vdd, VddCtlCode code, const void *data, size_t size) 266 | { 267 | if (vdd == NULL || vdd == INVALID_HANDLE_VALUE) 268 | return 0; 269 | 270 | BYTE InBuffer[32]; 271 | ZeroMemory(InBuffer, sizeof(InBuffer)); 272 | 273 | OVERLAPPED Overlapped; 274 | ZeroMemory(&Overlapped, sizeof(OVERLAPPED)); 275 | 276 | DWORD OutBuffer = 0; 277 | DWORD NumberOfBytesTransferred; 278 | 279 | if (data != NULL && size > 0) 280 | memcpy(InBuffer, data, (size < sizeof(InBuffer)) ? size : sizeof(InBuffer)); 281 | 282 | Overlapped.hEvent = CreateEventA(NULL, FALSE, FALSE, NULL); 283 | DeviceIoControl(vdd, (DWORD)code, InBuffer, sizeof(InBuffer), &OutBuffer, sizeof(DWORD), NULL, &Overlapped); 284 | 285 | GetOverlappedResult(vdd, &Overlapped, &NumberOfBytesTransferred, TRUE); 286 | 287 | if (Overlapped.hEvent != NULL) 288 | CloseHandle(Overlapped.hEvent); 289 | 290 | return OutBuffer; 291 | } 292 | 293 | /** 294 | * Query VDD minor version. 295 | * 296 | * @param vdd The device handle of VDD. 297 | * @return The number of minor version. 298 | */ 299 | static int VddVersion(HANDLE vdd) 300 | { 301 | int minor = VddIoControl(vdd, VDD_IOCTL_VERSION, NULL, 0); 302 | return minor; 303 | } 304 | 305 | /** 306 | * Update/ping to VDD. 307 | * Should call this function in a side thread for each 308 | * less than 100ms to keep all added virtual displays alive. 309 | * 310 | * @param vdd The device handle of VDD. 311 | */ 312 | static void VddUpdate(HANDLE vdd) 313 | { 314 | VddIoControl(vdd, VDD_IOCTL_UPDATE, NULL, 0); 315 | } 316 | 317 | /** 318 | * Add/plug a virtual display. 319 | * 320 | * @param vdd The device handle of VDD. 321 | * @return The index of the added display. 322 | */ 323 | static int VddAddDisplay(HANDLE vdd) 324 | { 325 | int idx = VddIoControl(vdd, VDD_IOCTL_ADD, NULL, 0); 326 | VddUpdate(vdd); 327 | 328 | return idx; 329 | } 330 | 331 | /** 332 | * Remove/unplug a virtual display. 333 | * 334 | * @param vdd The device handle of VDD. 335 | * @param index The index of the display will be removed. 336 | */ 337 | static void VddRemoveDisplay(HANDLE vdd, int index) 338 | { 339 | // 16-bit BE index 340 | UINT16 indexData = ((index & 0xFF) << 8) | ((index >> 8) & 0xFF); 341 | 342 | VddIoControl(vdd, VDD_IOCTL_REMOVE, &indexData, sizeof(indexData)); 343 | VddUpdate(vdd); 344 | } 345 | 346 | #ifdef __cplusplus 347 | } 348 | #endif 349 | 350 | #endif -------------------------------------------------------------------------------- /regedit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HaliComing/parsec-vdd-cli/8860efa524d5aaf323905c5b8de539643ea4bb87/regedit.png --------------------------------------------------------------------------------