GetDisplays()
53 | {
54 | return GetDisplays(out var _);
55 | }
56 |
57 | ///
58 | /// Query the driver device status.
59 | ///
60 | public static Device.Status QueryStatus(out Version version)
61 | {
62 | return Device.QueryStatus(CLASS_GUID, HARDWARE_ID, out version);
63 | }
64 |
65 | ///
66 | /// Get driver version from the device handle.
67 | ///
68 | public static bool GetVersion(IntPtr vdd, out string version)
69 | {
70 | if (IoControl(vdd, IoCtlCode.IOCTL_VERSION, null, out int vernum, 100))
71 | {
72 | int major = (vernum >> 16) & 0xFFFF;
73 | int minor = vernum & 0xFFFF;
74 | version = $"{major}.{minor}";
75 | return true;
76 | }
77 | else
78 | {
79 | version = "(unknown)";
80 | return false;
81 | }
82 | }
83 |
84 | ///
85 | /// Add a virtual display and retrieve the index.
86 | ///
87 | public static bool AddDisplay(IntPtr vdd, out int index)
88 | {
89 | if (IoControl(vdd, IoCtlCode.IOCTL_ADD, null, out index, 5000))
90 | {
91 | Update(vdd);
92 | return true;
93 | }
94 |
95 | return false;
96 | }
97 |
98 | ///
99 | /// Remove an added display by index.
100 | ///
101 | public static bool RemoveDisplay(IntPtr vdd, int index)
102 | {
103 | var input = new byte[2];
104 | input[1] = (byte)(index & 0xFF);
105 |
106 | if (IoControl(vdd, IoCtlCode.IOCTL_REMOVE, input, 1000))
107 | {
108 | Update(vdd);
109 | return true;
110 | }
111 |
112 | return false;
113 | }
114 |
115 | ///
116 | /// Update driver session to keep added displays alive.
117 | ///
118 | public static void Update(IntPtr vdd)
119 | {
120 | IoControl(vdd, IoCtlCode.IOCTL_UPDATE, null, 1000);
121 | }
122 |
123 | private enum IoCtlCode
124 | {
125 | IOCTL_ADD = 0x22E004,
126 | IOCTL_REMOVE = 0x22A008,
127 | IOCTL_UPDATE = 0x22A00C,
128 | IOCTL_VERSION = 0x22E010,
129 |
130 | // new code in driver v0.45
131 | // relates to IOCTL_UPDATE and per display state
132 | // but unused in Parsec app
133 | IOCTL_UNKNOWN1 = 0x22A014,
134 | }
135 |
136 | ///
137 | /// Send IO control code to the driver device handle.
138 | ///
139 | private static bool IoControl(IntPtr handle, IoCtlCode code, byte[] input, int* result, int timeout)
140 | {
141 | var InBuffer = new byte[32];
142 | var Overlapped = new Native.OVERLAPPED();
143 |
144 | if (input != null && input.Length > 0)
145 | {
146 | Array.Copy(input, InBuffer, Math.Min(input.Length, InBuffer.Length));
147 | }
148 |
149 | fixed (byte* buffer = InBuffer)
150 | {
151 | int outputLength = result != null ? sizeof(int) : 0;
152 | Overlapped.hEvent = Native.CreateEvent(null, false, false, null);
153 |
154 | bool sent = Native.DeviceIoControl(handle, (uint)code,
155 | buffer, InBuffer.Length,
156 | result, outputLength,
157 | null, ref Overlapped);
158 |
159 | #if DEBUG
160 | if (code != IoCtlCode.IOCTL_UPDATE)
161 | Console.WriteLine("[D] IoControl: {0}\n Sent: {1}, error: {2}", code, sent, DumpErrorCode(Marshal.GetLastWin32Error()));
162 | #endif
163 | if (!sent && Marshal.GetLastWin32Error() == 0x6)
164 | return false;
165 |
166 | bool success = Native.GetOverlappedResultEx(handle, ref Overlapped,
167 | out var NumberOfBytesTransferred, timeout, false);
168 |
169 | #if DEBUG
170 | if (code != IoCtlCode.IOCTL_UPDATE)
171 | Console.WriteLine(" OverlappedResult: {0}, error: {1}", success, DumpErrorCode(Marshal.GetLastWin32Error()));
172 | #endif
173 |
174 | if (Overlapped.hEvent != IntPtr.Zero)
175 | Native.CloseHandle(Overlapped.hEvent);
176 |
177 | return success;
178 | }
179 | }
180 |
181 | private static bool IoControl(IntPtr handle, IoCtlCode code, byte[] input, int timeout)
182 | {
183 | return IoControl(handle, code, input, null, timeout);
184 | }
185 |
186 | private static bool IoControl(IntPtr handle, IoCtlCode code, byte[] input, out int result, int timeout)
187 | {
188 | int output;
189 | bool success = IoControl(handle, code, input, &output, timeout);
190 | result = output;
191 | return success;
192 | }
193 |
194 | private static string DumpErrorCode(int code)
195 | {
196 | string ret = code.ToString("X");
197 |
198 | if (code == 0)
199 | ret += " (SUCCESS)";
200 | else if (code == 0x6)
201 | ret += " (ERROR_INVALID_HANDLE)";
202 | else if (code == 0x3E5)
203 | ret += " (ERROR_IO_PENDING)";
204 |
205 | return ret;
206 | }
207 |
208 | private static class Native
209 | {
210 | [DllImport("kernel32.dll", SetLastError = true)]
211 | [return: MarshalAs(UnmanagedType.Bool)]
212 | public static extern bool DeviceIoControl(
213 | IntPtr device, uint code,
214 | void* lpInBuffer, int nInBufferSize,
215 | void* lpOutBuffer, int nOutBufferSize,
216 | void* lpBytesReturned,
217 | ref OVERLAPPED lpOverlapped
218 | );
219 |
220 | [DllImport("kernel32.dll", SetLastError = true)]
221 | [return: MarshalAs(UnmanagedType.Bool)]
222 | public static extern bool GetOverlappedResultEx(
223 | IntPtr handle,
224 | ref OVERLAPPED lpOverlapped,
225 | out uint lpNumberOfBytesTransferred,
226 | int dwMilliseconds,
227 | [MarshalAs(UnmanagedType.Bool)] bool bAlertable
228 | );
229 |
230 | [StructLayout(LayoutKind.Sequential)]
231 | public struct OVERLAPPED
232 | {
233 | public IntPtr Internal;
234 | public IntPtr InternalHigh;
235 | public IntPtr Pointer;
236 | public IntPtr hEvent;
237 | }
238 |
239 | [DllImport("kernel32.dll", EntryPoint = "CreateEventW", CharSet = CharSet.Unicode)]
240 | public static extern IntPtr CreateEvent(
241 | void* lpEventAttributes,
242 | [MarshalAs(UnmanagedType.Bool)] bool bManualReset,
243 | [MarshalAs(UnmanagedType.Bool)] bool bInitialState,
244 | string lpName
245 | );
246 |
247 | [DllImport("kernel32.dll")]
248 | [return: MarshalAs(UnmanagedType.Bool)]
249 | public static extern bool CloseHandle(IntPtr handle);
250 | }
251 | }
252 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | ✨ Perfect virtual display for game streaming
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | ## ℹ About
27 |
28 | This project provides a **standalone solution for creating virtual displays** on
29 | a Windows host using the **Parsec Virtual Display Driver** (VDD), independent of
30 | the **Parsec app**.
31 |
32 | The Parsec VDD enables virtual displays on Windows 10+ systems, a feature
33 | available to Parsec Teams and Warp customers. With VDD, users can add up to
34 | three virtual displays to a host machine they connect to, ideal for setups where
35 | physical monitors may be unavailable or when additional displays are beneficial.
36 |
37 | Built by Parsec, the VDD leverages the IddCx API (Indirect Display Driver) to
38 | generate virtual displays with support for high resolutions and refresh rates,
39 | including up to 4K and 240 Hz. This capability makes it a versatile tool for
40 | gaming, streaming, or remote work, allowing users to simulate multiple screens
41 | for an enhanced, flexible visual experience.
42 |
43 | ## 📺 ParsecVDisplay App
44 |
45 | ParsecVDisplay is a comprehensive virtual display manager for Parsec VDD, built
46 | with C# and WPF. The app provides an intuitive interface to manage virtual
47 | displays, showing the number of active displays and allowing users to add or
48 | remove specific virtual displays. It also supports features like changing
49 | display resolution, capturing screenshots, and more, making it a versatile tool
50 | for flexible display management.
51 |
52 | 👉 Check out [Releases](https://github.com/nomi-san/parsec-vdd/releases) to
53 | download it.
54 |
55 |
56 |
57 |
58 |
59 | ## 🚀 Using Core API
60 |
61 | ### Design notes
62 |
63 | Parsec VDD is designed to work with Parsec client-connection sessions. When the
64 | user connects to the host, the app will start controlling the driver, it sends
65 | IO control codes and gets results. When adding a virtual display, you will get
66 | its index to be used for unplugging, the maximum number of displays could be
67 | added up to 16 per adapter. You have to ping the driver periodically to keep
68 | added displays alive, otherwise all of them will be unplugged after a second.
69 | There's no direct way to manipulate added displays, you should call Win32
70 | Display API to change their display mode (see the ParsecVDisplay source).
71 |
72 | ```mermaid
73 | flowchart LR
74 | A(app)
75 | B(vdd)
76 |
77 | A <--->|ioctl| B
78 | A ..->|ping| B
79 |
80 | B --- X(display1)
81 | B --- Y(display2)
82 | B --- Z(display3)
83 |
84 | winapi -->|manipulate| X
85 | ```
86 |
87 | ### Using the code
88 |
89 | For detailed instructions and usage examples, refer to the [VDD_LIBRARY_USAGE](./docs/VDD_LIBRARY_USAGE.md).
90 |
91 | - The core API is designed as single C/C++ header that can be added to any
92 | project, 👉 [core/parsec-vdd.h](./core/parsec-vdd.h)
93 | - There is also a simple demo program, 👉 [core/vdd-demo.cc](./core/vdd-demo.cc)
94 |
95 | ### Picking a driver
96 |
97 | You have to install the driver to make them work.
98 |
99 | | Version | Minimum OS | IddCx | Notes |
100 | | :---------------- | :-------------- | :---: | :-------------------------------------------------------- |
101 | | [parsec-vdd-0.38] | Windows 10 1607 | 1.0 | Obsolete, may crash randomly. |
102 | | [parsec-vdd-0.41] | Windows 10 19H2 | 1.4 | Stable. |
103 | | [parsec-vdd-0.45] | Windows 10 21H2 | 1.5 | Better streaming color, but may not work on some Windows. |
104 |
105 | [parsec-vdd-0.38]: https://builds.parsec.app/vdd/parsec-vdd-0.38.0.0.exe
106 | [parsec-vdd-0.41]: https://builds.parsec.app/vdd/parsec-vdd-0.41.0.0.exe
107 | [parsec-vdd-0.45]: https://builds.parsec.app/vdd/parsec-vdd-0.45.0.0.exe
108 |
109 | > All of them also work on Windows Server 2019 or higher.
110 |
111 | You can unzip (using 7z) the driver setup above to obtain the driver files and
112 | `nefconw` CLI.
113 |
114 | ```
115 | vdd-0.45/
116 | |__ nefconw.exe
117 | |__ driver/
118 | |__ mm.cat
119 | |__ mm.dll
120 | |__ mm.inf
121 | ```
122 |
123 | Command line method to install the driver using `nefconw` (admin required):
124 |
125 | ```
126 | start /wait .\nefconw.exe --remove-device-node --hardware-id Root\Parsec\VDA --class-guid "4D36E968-E325-11CE-BFC1-08002BE10318"
127 | start /wait .\nefconw.exe --create-device-node --class-name Display --class-guid "4D36E968-E325-11CE-BFC1-08002BE10318" --hardware-id Root\Parsec\VDA
128 | start /wait .\nefconw.exe --install-driver --inf-path ".\driver\mm.inf"
129 | ```
130 |
131 | In addition, you can run the driver setup in silent mode to install it quickly.
132 |
133 | ```
134 | .\parsec-vdd-0.45.0.0.exe /S
135 | ```
136 |
137 | ## 😥 Known Limitations
138 |
139 | > This list shows the known limitations of Parsec VDD.
140 |
141 | ### 1. HDR support
142 |
143 | Parsec VDD does not support HDR on its displays (see the EDID below).
144 | Theoretically, you can unlock support by editing the EDID, then adding HDR
145 | metadata and setting 10-bit+ color depth. Unfortunately, you cannot flash its
146 | firmware like a physical device, or modify the registry value.
147 |
148 | All IDDs have their own fixed EDID block inside the driver binary to initialize
149 | the monitor specs. So the solution is to modify this block in the driver DLL
150 | (mm.dll), then reinstall it with `nefconw` CLI (see above).
151 |
152 | ### 2. Custom resolutions
153 |
154 | Before connecting, the virtual display looks in the `HKLM\SOFTWARE\Parsec\vdd`
155 | registry for additional preset resolutions. Currently this supports a maximum of
156 | 5 values.
157 |
158 | ```yaml
159 | HKLM\SOFTWARE\Parsec\vdd:
160 | - key: [0 -> 5]
161 | value: { width, height, hz }
162 | ```
163 |
164 | To unlock this limit, you need to patch the driver DLL the same way as above,
165 | but **5 is enough** for personal use.
166 |
167 | ## 😑 Known Bugs
168 |
169 | > This is a list of known issues when working with standalone Parsec VDD.
170 |
171 | ### 1. Incompatible with Parsec Privacy Mode
172 |
173 | 
174 |
175 | If you have enabled "Privacy Mode" in Parsec Host settings, please disable it
176 | and clear the connected display configurations in the following Registry path.
177 |
178 | ```
179 | HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\GraphicsDrivers\Connectivity
180 | ```
181 |
182 | This option causes your main display to turn off when virtual displays are
183 | added, making it difficult to turn the display on and disrupting the remote
184 | desktop session.
185 |
186 | ### 2. // todo
187 |
188 | ## 🤔 Comparison with other IDDs
189 |
190 | The table below shows a comparison with other popular Indirect Display Driver
191 | projects.
192 |
193 | | Project | Iddcx version | Signed | Gaming | HDR | H-Cursor | Tweakable | Controller |
194 | | :----------------------------- | :-----------: | :----: | :----: | :-: | :------------------------------------------------------------------: | :-------: | :--------: |
195 | | [usbmmidd_v2] | | ✅ | ❌ | ❌ | ❌ | | |
196 | | [IddSampleDriver] | 1.2 | ❌ | ❌ | ❌ | ❌ | | |
197 | | [RustDeskIddDriver] | 1.2 | ❌ | ❌ | ❌ | ❌ | | |
198 | | [Virtual-Display-Driver (HDR)] | 1.10 | ❌ | | ✅ | ❌ | | |
199 | | [virtual-display-rs] | 1.5 | ❌ | | ❌ | [#81](https://github.com/MolotovCherry/virtual-display-rs/issues/81) | ✅ | ✅ |
200 | | parsec-vdd | 1.5 | ✅ | ✅ | ❌ | ✅ | 🆗 | ✅ |
201 |
202 | ✅ - full support, 🆗 - limited support
203 |
204 | [usbmmidd_v2]: https://www.amyuni.com/forum/viewtopic.php?t=3030
205 | [IddSampleDriver]: https://github.com/roshkins/IddSampleDriver
206 | [RustDeskIddDriver]: https://github.com/fufesou/RustDeskIddDriver
207 | [virtual-display-rs]: https://github.com/MolotovCherry/virtual-display-rs
208 | [Virtual-Display-Driver (HDR)]: https://github.com/itsmikethetech/Virtual-Display-Driver
209 |
210 | **Signed** means that the driver files have a valid digital signature.
211 | **H-Cursor** means hardware cursor support, without it, you will get a double
212 | cursor on some remote desktop apps. **Tweakable** is the ability to customize
213 | display modes. Visit
214 | [MSDN IddCx versions](https://learn.microsoft.com/en-us/windows-hardware/drivers/display/iddcx-versions)
215 | to check the minimum supported Windows version.
216 |
217 | ## 📘 Parsec VDD Specs
218 |
219 | Common preset display modes:
220 |
221 | | Resolution | Common Name | Aspect Ratio | Refresh Rates (Hz) |
222 | | ----------- | ----------- | ------------ | ------------------ |
223 | | 3840 x 2160 | 4K UHD | 16:9 | 24/30/60/144/240 |
224 | | 3440 x 1440 | UltraWide | 21.5:9 | 24/30/60/144/240 |
225 | | 2560 x 1440 | 2K | 16:9 | 24/30/60/144/240 |
226 | | 2560 x 1080 | UltraWide | 21:9 | 24/30/60/144/240 |
227 | | 1920 x 1080 | FHD | 16:9 | 24/30/60/144/240 |
228 | | 1600 x 900 | HD+ | 16:9 | 60/144/240 |
229 | | 1280 x 720 | HD | 16:9 | 60/144/240 |
230 |
231 | Check out [docs/PARSEC_VDD_SPECS](./docs/PARSEC_VDD_SPECS.md) to see full of
232 | preset display modes the driver specs.
233 |
234 | ## 🤝 Sponsors
235 |
236 |
242 |
243 | ## 🍻 Credits
244 |
245 | - Thanks to Parsec for the driver
246 | - The app's background was from old parsecgaming.com
247 |
--------------------------------------------------------------------------------
/app/CLI.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 | using System.Runtime.InteropServices;
4 | using System.Text.RegularExpressions;
5 | using System.Threading;
6 |
7 | namespace ParsecVDisplay
8 | {
9 | internal static class CLI
10 | {
11 | static IntPtr VddHandle = IntPtr.Zero;
12 |
13 | static void ShowHelp()
14 | {
15 | Console.WriteLine("vdd command [args...]");
16 | Console.WriteLine(" -a|add - Add a virtual display");
17 | Console.WriteLine(" -r|remove - Remove the last added virtual display");
18 | Console.WriteLine(" X - Remove the virtual display at index X (number)");
19 | Console.WriteLine(" all - Remove all the added virtual displays");
20 | Console.WriteLine(" -l|list - Show all the added virtual displays and specs");
21 | Console.WriteLine(" -s|set X WxH - Set resolution for a virtual display");
22 | Console.WriteLine(" where X is index number, WxH is size, e.g 1920x1080");
23 | Console.WriteLine(" X @R - Set only the refresh rate R, e.g @60, @120 (hz)");
24 | Console.WriteLine(" on Powershell, you should replace '@' with 'r'");
25 | Console.WriteLine(" X WxH@R - Set full display mode as above, e.g 1920x1080@144");
26 | Console.WriteLine(" -v|version - Query driver version and status");
27 | Console.WriteLine(" -h|help - Show this help");
28 | }
29 |
30 | public static int Execute(string[] args)
31 | {
32 | AttachConsole(-1);
33 |
34 | if (args.Length > 0)
35 | {
36 | try
37 | {
38 | switch (args[0])
39 | {
40 | case "-a":
41 | case "add":
42 | return AddDisplay();
43 |
44 | case "-r":
45 | case "remove":
46 | return RemoveDisplay(args);
47 |
48 | case "-l":
49 | case "list":
50 | return ListDisplay();
51 |
52 | case "-s":
53 | case "set":
54 | return SetDisplayMode(args);
55 |
56 | case "-v":
57 | case "version":
58 | return QueryVersion();
59 |
60 | case "-h":
61 | case "help":
62 | ShowHelp();
63 | return 0;
64 |
65 | default:
66 | Console.WriteLine("Invalid command '{0}'", args[0]);
67 | ShowHelp();
68 | return 0;
69 | }
70 | }
71 | catch (Exception ex)
72 | {
73 | Console.WriteLine("Error: {0}", ex.Message);
74 | #if DEBUG
75 | Console.Error.WriteLine(ex.StackTrace);
76 | #endif
77 | return -1;
78 | }
79 | finally
80 | {
81 | Vdd.Core.CloseHandle(VddHandle);
82 | }
83 | }
84 | else
85 | {
86 | ShowHelp();
87 | return 0;
88 | }
89 | }
90 |
91 | static Device.Status PrepareVdd()
92 | {
93 | var status = Vdd.Core.QueryStatus(out var _);
94 |
95 | if (status == Device.Status.NOT_INSTALLED)
96 | {
97 | throw new Exception("The driver is not found, please install it first");
98 | }
99 | else if (status != Device.Status.OK)
100 | {
101 | throw new Exception($"The driver is not OK, got status {status}");
102 | }
103 |
104 | if (!Vdd.Core.OpenHandle(out VddHandle))
105 | {
106 | throw new Exception("Failed to obtain the driver device handle");
107 | }
108 |
109 | return status;
110 | }
111 |
112 | static void CheckAppRunning()
113 | {
114 | if (!EventWaitHandle.TryOpenExisting(Program.AppId, out var _))
115 | {
116 | throw new Exception($"{Program.AppName} app is not running");
117 | }
118 | }
119 |
120 | static int AddDisplay()
121 | {
122 | var displays = Vdd.Core.GetDisplays();
123 | int maxCount = Vdd.Core.MAX_DISPLAYS;
124 |
125 | if (displays.Count >= maxCount)
126 | {
127 | throw new Exception(string.Format("Exceeded limit ({0}), could not add more displays", maxCount));
128 | }
129 |
130 | PrepareVdd();
131 | CheckAppRunning();
132 |
133 | if (Vdd.Core.AddDisplay(VddHandle, out int index))
134 | {
135 | Console.WriteLine($"Added a virtual display with index {0}.", index);
136 | return index;
137 | }
138 | else
139 | {
140 | throw new Exception("Failed to add a virtual display.");
141 | }
142 | }
143 |
144 | static int RemoveDisplay(string[] args)
145 | {
146 | var arg1 = args.Length >= 2 ? args[1] : "";
147 | bool removeAll = arg1 == "all" || arg1 == "*";
148 | int index = -1;
149 |
150 | if (args.Length == 1 || removeAll || int.TryParse(arg1, out index))
151 | {
152 | var displays = Vdd.Core.GetDisplays();
153 |
154 | if (displays.Count == 0)
155 | {
156 | Console.WriteLine("No Parsec Display available.");
157 | return 0;
158 | }
159 | else if (removeAll)
160 | {
161 | PrepareVdd();
162 | foreach (var di in displays)
163 | {
164 | if (!Vdd.Core.RemoveDisplay(VddHandle, di.DisplayIndex))
165 | throw new Exception(string.Format("Failed to remove the display at index {0}.", index));
166 | }
167 |
168 | Console.WriteLine("Removed all added displays.");
169 | return 0;
170 | }
171 | else
172 | {
173 | var display = index < 0 ? displays.LastOrDefault()
174 | : displays.FirstOrDefault(di => di.DisplayIndex == index);
175 |
176 | if (display != null)
177 | {
178 | PrepareVdd();
179 | if (!Vdd.Core.RemoveDisplay(VddHandle, display.DisplayIndex))
180 | throw new Exception(string.Format("Failed to remove the display at index {0}.", display.DisplayIndex));
181 |
182 | Console.WriteLine("Removed display at index {0}.", display.DisplayIndex);
183 | return 0;
184 | }
185 | else
186 | {
187 | throw new Exception(string.Format("Display index {0} is not found.", index));
188 | }
189 | }
190 | }
191 | else
192 | {
193 | throw new Exception(string.Format("Invalid display index '{0}'.", arg1));
194 | }
195 | }
196 |
197 | static int ListDisplay()
198 | {
199 | var displays = Vdd.Core.GetDisplays();
200 |
201 | if (displays.Count > 0)
202 | {
203 | foreach (var di in displays)
204 | {
205 | Console.WriteLine("Index: {0}", di.DisplayIndex);
206 | Console.WriteLine(" - Device: {0}", di.DeviceName);
207 | Console.WriteLine(" - Number: {0}", di.Identifier);
208 | Console.WriteLine(" - Name: {0}", di.DisplayName);
209 | Console.WriteLine(" - Mode: {0}", di.CurrentMode);
210 | Console.WriteLine(" - Orientation: {0} ({1}°)", di.CurrentOrientation, (int)di.CurrentOrientation * 90);
211 | }
212 | }
213 | else
214 | {
215 | Console.WriteLine("No virtual displays present.");
216 | }
217 |
218 | return displays.Count;
219 | }
220 |
221 | static int SetDisplayMode(string[] args)
222 | {
223 | if (args.Length < 2)
224 | throw new Exception("Missing display index.");
225 | if (args.Length < 3)
226 | throw new Exception("Missing resolution and/or refresh rate.");
227 |
228 | var argIndex = args[1];
229 | var argDMode = string.Join(" ", args.Skip(2));
230 |
231 | if (int.TryParse(argIndex, out int index))
232 | {
233 | var displays = Vdd.Core.GetDisplays();
234 |
235 | if (displays.Count == 0)
236 | {
237 | Console.WriteLine("No Parsec Display available.");
238 | return 0;
239 | }
240 | else
241 | {
242 | var display = displays.Find(di => di.DisplayIndex == index);
243 |
244 | if (display == null)
245 | throw new Exception(string.Format("Display index {0} is not found.", index));
246 |
247 | int? width, height, hz;
248 | ParseDisplayModeArg(argDMode, out width, out height, out hz);
249 |
250 | if ((width != null && height != null) || hz != null)
251 | {
252 | if (display.ChangeMode(width, height, hz, null))
253 | {
254 | Console.WriteLine($"Display index {index} is set to '{argDMode}'.");
255 | return 0;
256 | }
257 | else
258 | {
259 | throw new Exception($"Failed to set, display mode '{argDMode}' is not supported.");
260 | }
261 | }
262 | else
263 | {
264 | throw new Exception("Nothing to do, recheck your syntax.");
265 | }
266 | }
267 | }
268 | else
269 | {
270 | throw new Exception(string.Format("Invalid display index '{0}'.", argIndex));
271 | }
272 | }
273 |
274 | static int QueryVersion()
275 | {
276 | var status = Vdd.Core.QueryStatus(out var version);
277 |
278 | Console.WriteLine(Vdd.Core.ADAPTER);
279 | Console.WriteLine("- Status: {0}", status);
280 | Console.WriteLine("- Version: {0}.{1}", version.Major, version.Minor);
281 |
282 | return (int)status;
283 | }
284 |
285 | static void ParseDisplayModeArg(string arg, out int? width, out int? height, out int? hz)
286 | {
287 | Match match;
288 | arg = arg.Trim();
289 |
290 | width = null;
291 | height = null;
292 | hz = null;
293 |
294 | const string regexSize = @"^(\d+)\s*[xX]\s*(\d+)$";
295 | if (Regex.IsMatch(arg, regexSize))
296 | {
297 | match = Regex.Match(arg, regexSize);
298 | width = int.Parse(match.Groups[1].Value);
299 | height = int.Parse(match.Groups[2].Value);
300 | return;
301 | }
302 |
303 | const string regexHz = @"^[r@](\d+)$";
304 | if (Regex.IsMatch(arg, regexHz))
305 | {
306 | match = Regex.Match(arg, regexHz);
307 | hz = int.Parse(match.Groups[1].Value);
308 | return;
309 | }
310 |
311 | const string regexAll = @"^(\d+)\s*[xX]\s*(\d+)\s*[r@](\d+)$";
312 | if (Regex.IsMatch(arg, regexAll))
313 | {
314 | match = Regex.Match(arg, regexAll);
315 | width = int.Parse(match.Groups[1].Value);
316 | height = int.Parse(match.Groups[2].Value);
317 | hz = int.Parse(match.Groups[3].Value);
318 | return;
319 | }
320 | }
321 |
322 | [DllImport("kernel32.dll")]
323 | [return: MarshalAs(UnmanagedType.Bool)]
324 | static extern bool AttachConsole(int dwProcessId);
325 | }
326 | }
--------------------------------------------------------------------------------
/core/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_UNKNOWN, // Unknown status
52 | DEVICE_UNKNOWN_PROBLEM, // Unknown 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_UNKNOWN_PROBLEM;
151 | break;
152 | }
153 | }
154 | else
155 | {
156 | status = DEVICE_UNKNOWN;
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, // CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800 + 1, METHOD_BUFFERED, FILE_READ_ACCESS | FILE_WRITE_ACCESS)
259 | VDD_IOCTL_REMOVE = 0x0022a008, // CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800 + 2, METHOD_BUFFERED, FILE_WRITE_ACCESS)
260 | VDD_IOCTL_UPDATE = 0x0022a00c, // CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800 + 3, METHOD_BUFFERED, FILE_WRITE_ACCESS)
261 | VDD_IOCTL_VERSION = 0x0022e010, // CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800 + 4, METHOD_BUFFERED, FILE_READ_ACCESS | FILE_WRITE_ACCESS)
262 |
263 | // new code in driver v0.45
264 | // relates to IOCTL_UPDATE and per display state
265 | // but unused in Parsec app
266 | VDD_IOCTL_UNKONWN = 0x0022a00c, // CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800 + 5, METHOD_BUFFERED, FILE_WRITE_ACCESS)
267 | } VddCtlCode;
268 |
269 | // Generic DeviceIoControl for all IoControl codes.
270 | static DWORD VddIoControl(HANDLE vdd, VddCtlCode code, const void *data, size_t size)
271 | {
272 | if (vdd == NULL || vdd == INVALID_HANDLE_VALUE)
273 | return -1;
274 |
275 | BYTE InBuffer[32];
276 | ZeroMemory(InBuffer, sizeof(InBuffer));
277 |
278 | OVERLAPPED Overlapped;
279 | ZeroMemory(&Overlapped, sizeof(OVERLAPPED));
280 |
281 | DWORD OutBuffer = 0;
282 | DWORD NumberOfBytesTransferred;
283 |
284 | if (data != NULL && size > 0)
285 | memcpy(InBuffer, data, (size < sizeof(InBuffer)) ? size : sizeof(InBuffer));
286 |
287 | Overlapped.hEvent = CreateEventA(NULL, TRUE, FALSE, NULL);
288 | DeviceIoControl(vdd, (DWORD)code, InBuffer, sizeof(InBuffer), &OutBuffer, sizeof(DWORD), NULL, &Overlapped);
289 |
290 | if (!GetOverlappedResultEx(vdd, &Overlapped, &NumberOfBytesTransferred, 5000, FALSE))
291 | {
292 | CloseHandle(Overlapped.hEvent);
293 | return -1;
294 | }
295 |
296 | if (Overlapped.hEvent != NULL)
297 | CloseHandle(Overlapped.hEvent);
298 |
299 | return OutBuffer;
300 | }
301 |
302 | /**
303 | * Query VDD minor version.
304 | *
305 | * @param vdd The device handle of VDD.
306 | * @return The number of minor version.
307 | */
308 | static int VddVersion(HANDLE vdd)
309 | {
310 | int minor = VddIoControl(vdd, VDD_IOCTL_VERSION, NULL, 0);
311 | return minor;
312 | }
313 |
314 | /**
315 | * Update/ping to VDD.
316 | * Should call this function in a side thread for each
317 | * less than 100ms to keep all added virtual displays alive.
318 | *
319 | * @param vdd The device handle of VDD.
320 | */
321 | static void VddUpdate(HANDLE vdd)
322 | {
323 | VddIoControl(vdd, VDD_IOCTL_UPDATE, NULL, 0);
324 | }
325 |
326 | /**
327 | * Add/plug a virtual display.
328 | *
329 | * @param vdd The device handle of VDD.
330 | * @return The index of the added display.
331 | */
332 | static int VddAddDisplay(HANDLE vdd)
333 | {
334 | int idx = VddIoControl(vdd, VDD_IOCTL_ADD, NULL, 0);
335 | VddUpdate(vdd);
336 |
337 | return idx;
338 | }
339 |
340 | /**
341 | * Remove/unplug a virtual display.
342 | *
343 | * @param vdd The device handle of VDD.
344 | * @param index The index of the display will be removed.
345 | */
346 | static void VddRemoveDisplay(HANDLE vdd, int index)
347 | {
348 | // 16-bit BE index
349 | UINT16 indexData = ((index & 0xFF) << 8) | ((index >> 8) & 0xFF);
350 |
351 | VddIoControl(vdd, VDD_IOCTL_REMOVE, &indexData, sizeof(indexData));
352 | VddUpdate(vdd);
353 | }
354 |
355 | #ifdef __cplusplus
356 | }
357 | #endif
358 |
359 | #endif
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ### C++ ###
2 | # Prerequisites
3 | *.d
4 |
5 | # Compiled Object files
6 | *.slo
7 | *.lo
8 | *.o
9 | *.obj
10 |
11 | # Precompiled Headers
12 | *.gch
13 | *.pch
14 |
15 | # Compiled Dynamic libraries
16 | *.so
17 | *.dylib
18 | *.dll
19 |
20 | # Fortran module files
21 | *.mod
22 | *.smod
23 |
24 | # Compiled Static libraries
25 | *.lai
26 | *.la
27 | *.a
28 | *.lib
29 |
30 | # Executables
31 | *.exe
32 | *.out
33 | *.app
34 |
35 | ### Csharp ###
36 | ## Ignore Visual Studio temporary files, build results, and
37 | ## files generated by popular Visual Studio add-ons.
38 | ##
39 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
40 |
41 | # User-specific files
42 | *.rsuser
43 | *.suo
44 | *.user
45 | *.userosscache
46 | *.sln.docstates
47 |
48 | # User-specific files (MonoDevelop/Xamarin Studio)
49 | *.userprefs
50 |
51 | # Mono auto generated files
52 | mono_crash.*
53 |
54 | # Build results
55 | [Dd]ebug/
56 | [Dd]ebugPublic/
57 | [Rr]elease/
58 | [Rr]eleases/
59 | x64/
60 | x86/
61 | [Ww][Ii][Nn]32/
62 | [Aa][Rr][Mm]/
63 | [Aa][Rr][Mm]64/
64 | bld/
65 | [Bb]in/
66 | [Oo]bj/
67 | [Ll]og/
68 | [Ll]ogs/
69 |
70 | # Visual Studio 2015/2017 cache/options directory
71 | .vs/
72 | # Uncomment if you have tasks that create the project's static files in wwwroot
73 | #wwwroot/
74 |
75 | # Visual Studio 2017 auto generated files
76 | Generated\ Files/
77 |
78 | # MSTest test Results
79 | [Tt]est[Rr]esult*/
80 | [Bb]uild[Ll]og.*
81 |
82 | # NUnit
83 | *.VisualState.xml
84 | TestResult.xml
85 | nunit-*.xml
86 |
87 | # Build Results of an ATL Project
88 | [Dd]ebugPS/
89 | [Rr]eleasePS/
90 | dlldata.c
91 |
92 | # Benchmark Results
93 | BenchmarkDotNet.Artifacts/
94 |
95 | # .NET Core
96 | project.lock.json
97 | project.fragment.lock.json
98 | artifacts/
99 |
100 | # ASP.NET Scaffolding
101 | ScaffoldingReadMe.txt
102 |
103 | # StyleCop
104 | StyleCopReport.xml
105 |
106 | # Files built by Visual Studio
107 | *_i.c
108 | *_p.c
109 | *_h.h
110 | *.ilk
111 | *.meta
112 | *.iobj
113 | *.pdb
114 | *.ipdb
115 | *.pgc
116 | *.pgd
117 | *.rsp
118 | *.sbr
119 | *.tlb
120 | *.tli
121 | *.tlh
122 | *.tmp
123 | *.tmp_proj
124 | *_wpftmp.csproj
125 | *.log
126 | *.tlog
127 | *.vspscc
128 | *.vssscc
129 | .builds
130 | *.pidb
131 | *.svclog
132 | *.scc
133 |
134 | # Chutzpah Test files
135 | _Chutzpah*
136 |
137 | # Visual C++ cache files
138 | ipch/
139 | *.aps
140 | *.ncb
141 | *.opendb
142 | *.opensdf
143 | *.sdf
144 | *.cachefile
145 | *.VC.db
146 | *.VC.VC.opendb
147 |
148 | # Visual Studio profiler
149 | *.psess
150 | *.vsp
151 | *.vspx
152 | *.sap
153 |
154 | # Visual Studio Trace Files
155 | *.e2e
156 |
157 | # TFS 2012 Local Workspace
158 | $tf/
159 |
160 | # Guidance Automation Toolkit
161 | *.gpState
162 |
163 | # ReSharper is a .NET coding add-in
164 | _ReSharper*/
165 | *.[Rr]e[Ss]harper
166 | *.DotSettings.user
167 |
168 | # TeamCity is a build add-in
169 | _TeamCity*
170 |
171 | # DotCover is a Code Coverage Tool
172 | *.dotCover
173 |
174 | # AxoCover is a Code Coverage Tool
175 | .axoCover/*
176 | !.axoCover/settings.json
177 |
178 | # Coverlet is a free, cross platform Code Coverage Tool
179 | coverage*.json
180 | coverage*.xml
181 | coverage*.info
182 |
183 | # Visual Studio code coverage results
184 | *.coverage
185 | *.coveragexml
186 |
187 | # NCrunch
188 | _NCrunch_*
189 | .*crunch*.local.xml
190 | nCrunchTemp_*
191 |
192 | # MightyMoose
193 | *.mm.*
194 | AutoTest.Net/
195 |
196 | # Web workbench (sass)
197 | .sass-cache/
198 |
199 | # Installshield output folder
200 | [Ee]xpress/
201 |
202 | # DocProject is a documentation generator add-in
203 | DocProject/buildhelp/
204 | DocProject/Help/*.HxT
205 | DocProject/Help/*.HxC
206 | DocProject/Help/*.hhc
207 | DocProject/Help/*.hhk
208 | DocProject/Help/*.hhp
209 | DocProject/Help/Html2
210 | DocProject/Help/html
211 |
212 | # Click-Once directory
213 | publish/
214 |
215 | # Publish Web Output
216 | *.[Pp]ublish.xml
217 | *.azurePubxml
218 | # Note: Comment the next line if you want to checkin your web deploy settings,
219 | # but database connection strings (with potential passwords) will be unencrypted
220 | *.pubxml
221 | *.publishproj
222 |
223 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
224 | # checkin your Azure Web App publish settings, but sensitive information contained
225 | # in these scripts will be unencrypted
226 | PublishScripts/
227 |
228 | # NuGet Packages
229 | *.nupkg
230 | # NuGet Symbol Packages
231 | *.snupkg
232 | # The packages folder can be ignored because of Package Restore
233 | **/[Pp]ackages/*
234 | # except build/, which is used as an MSBuild target.
235 | !**/[Pp]ackages/build/
236 | # Uncomment if necessary however generally it will be regenerated when needed
237 | #!**/[Pp]ackages/repositories.config
238 | # NuGet v3's project.json files produces more ignorable files
239 | *.nuget.props
240 | *.nuget.targets
241 |
242 | # Microsoft Azure Build Output
243 | csx/
244 | *.build.csdef
245 |
246 | # Microsoft Azure Emulator
247 | ecf/
248 | rcf/
249 |
250 | # Windows Store app package directories and files
251 | AppPackages/
252 | BundleArtifacts/
253 | Package.StoreAssociation.xml
254 | _pkginfo.txt
255 | *.appx
256 | *.appxbundle
257 | *.appxupload
258 |
259 | # Visual Studio cache files
260 | # files ending in .cache can be ignored
261 | *.[Cc]ache
262 | # but keep track of directories ending in .cache
263 | !?*.[Cc]ache/
264 |
265 | # Others
266 | ClientBin/
267 | ~$*
268 | *~
269 | *.dbmdl
270 | *.dbproj.schemaview
271 | *.jfm
272 | *.pfx
273 | *.publishsettings
274 | orleans.codegen.cs
275 |
276 | # Including strong name files can present a security risk
277 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
278 | #*.snk
279 |
280 | # Since there are multiple workflows, uncomment next line to ignore bower_components
281 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
282 | #bower_components/
283 |
284 | # RIA/Silverlight projects
285 | Generated_Code/
286 |
287 | # Backup & report files from converting an old project file
288 | # to a newer Visual Studio version. Backup files are not needed,
289 | # because we have git ;-)
290 | _UpgradeReport_Files/
291 | Backup*/
292 | UpgradeLog*.XML
293 | UpgradeLog*.htm
294 | ServiceFabricBackup/
295 | *.rptproj.bak
296 |
297 | # SQL Server files
298 | *.mdf
299 | *.ldf
300 | *.ndf
301 |
302 | # Business Intelligence projects
303 | *.rdl.data
304 | *.bim.layout
305 | *.bim_*.settings
306 | *.rptproj.rsuser
307 | *- [Bb]ackup.rdl
308 | *- [Bb]ackup ([0-9]).rdl
309 | *- [Bb]ackup ([0-9][0-9]).rdl
310 |
311 | # Microsoft Fakes
312 | FakesAssemblies/
313 |
314 | # GhostDoc plugin setting file
315 | *.GhostDoc.xml
316 |
317 | # Node.js Tools for Visual Studio
318 | .ntvs_analysis.dat
319 | node_modules/
320 |
321 | # Visual Studio 6 build log
322 | *.plg
323 |
324 | # Visual Studio 6 workspace options file
325 | *.opt
326 |
327 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
328 | *.vbw
329 |
330 | # Visual Studio 6 auto-generated project file (contains which files were open etc.)
331 | *.vbp
332 |
333 | # Visual Studio 6 workspace and project file (working project files containing files to include in project)
334 | *.dsw
335 | *.dsp
336 |
337 | # Visual Studio 6 technical files
338 |
339 | # Visual Studio LightSwitch build output
340 | **/*.HTMLClient/GeneratedArtifacts
341 | **/*.DesktopClient/GeneratedArtifacts
342 | **/*.DesktopClient/ModelManifest.xml
343 | **/*.Server/GeneratedArtifacts
344 | **/*.Server/ModelManifest.xml
345 | _Pvt_Extensions
346 |
347 | # Paket dependency manager
348 | .paket/paket.exe
349 | paket-files/
350 |
351 | # FAKE - F# Make
352 | .fake/
353 |
354 | # CodeRush personal settings
355 | .cr/personal
356 |
357 | # Python Tools for Visual Studio (PTVS)
358 | __pycache__/
359 | *.pyc
360 |
361 | # Cake - Uncomment if you are using it
362 | # tools/**
363 | # !tools/packages.config
364 |
365 | # Tabs Studio
366 | *.tss
367 |
368 | # Telerik's JustMock configuration file
369 | *.jmconfig
370 |
371 | # BizTalk build output
372 | *.btp.cs
373 | *.btm.cs
374 | *.odx.cs
375 | *.xsd.cs
376 |
377 | # OpenCover UI analysis results
378 | OpenCover/
379 |
380 | # Azure Stream Analytics local run output
381 | ASALocalRun/
382 |
383 | # MSBuild Binary and Structured Log
384 | *.binlog
385 |
386 | # NVidia Nsight GPU debugger configuration file
387 | *.nvuser
388 |
389 | # MFractors (Xamarin productivity tool) working folder
390 | .mfractor/
391 |
392 | # Local History for Visual Studio
393 | .localhistory/
394 |
395 | # Visual Studio History (VSHistory) files
396 | .vshistory/
397 |
398 | # BeatPulse healthcheck temp database
399 | healthchecksdb
400 |
401 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
402 | MigrationBackup/
403 |
404 | # Ionide (cross platform F# VS Code tools) working folder
405 | .ionide/
406 |
407 | # Fody - auto-generated XML schema
408 | FodyWeavers.xsd
409 |
410 | # VS Code files for those working on multiple tools
411 | .vscode/*
412 | !.vscode/settings.json
413 | !.vscode/tasks.json
414 | !.vscode/launch.json
415 | !.vscode/extensions.json
416 | *.code-workspace
417 |
418 | # Local History for Visual Studio Code
419 | .history/
420 |
421 | # Windows Installer files from build outputs
422 | *.cab
423 | *.msi
424 | *.msix
425 | *.msm
426 | *.msp
427 |
428 | # JetBrains Rider
429 | *.sln.iml
430 |
431 | ### VisualStudio ###
432 |
433 | # User-specific files
434 |
435 | # User-specific files (MonoDevelop/Xamarin Studio)
436 |
437 | # Mono auto generated files
438 |
439 | # Build results
440 |
441 | # Visual Studio 2015/2017 cache/options directory
442 | # Uncomment if you have tasks that create the project's static files in wwwroot
443 |
444 | # Visual Studio 2017 auto generated files
445 |
446 | # MSTest test Results
447 |
448 | # NUnit
449 |
450 | # Build Results of an ATL Project
451 |
452 | # Benchmark Results
453 |
454 | # .NET Core
455 |
456 | # ASP.NET Scaffolding
457 |
458 | # StyleCop
459 |
460 | # Files built by Visual Studio
461 |
462 | # Chutzpah Test files
463 |
464 | # Visual C++ cache files
465 |
466 | # Visual Studio profiler
467 |
468 | # Visual Studio Trace Files
469 |
470 | # TFS 2012 Local Workspace
471 |
472 | # Guidance Automation Toolkit
473 |
474 | # ReSharper is a .NET coding add-in
475 |
476 | # TeamCity is a build add-in
477 |
478 | # DotCover is a Code Coverage Tool
479 |
480 | # AxoCover is a Code Coverage Tool
481 |
482 | # Coverlet is a free, cross platform Code Coverage Tool
483 |
484 | # Visual Studio code coverage results
485 |
486 | # NCrunch
487 |
488 | # MightyMoose
489 |
490 | # Web workbench (sass)
491 |
492 | # Installshield output folder
493 |
494 | # DocProject is a documentation generator add-in
495 |
496 | # Click-Once directory
497 |
498 | # Publish Web Output
499 | # Note: Comment the next line if you want to checkin your web deploy settings,
500 | # but database connection strings (with potential passwords) will be unencrypted
501 |
502 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
503 | # checkin your Azure Web App publish settings, but sensitive information contained
504 | # in these scripts will be unencrypted
505 |
506 | # NuGet Packages
507 | # NuGet Symbol Packages
508 | # The packages folder can be ignored because of Package Restore
509 | # except build/, which is used as an MSBuild target.
510 | # Uncomment if necessary however generally it will be regenerated when needed
511 | # NuGet v3's project.json files produces more ignorable files
512 |
513 | # Microsoft Azure Build Output
514 |
515 | # Microsoft Azure Emulator
516 |
517 | # Windows Store app package directories and files
518 |
519 | # Visual Studio cache files
520 | # files ending in .cache can be ignored
521 | # but keep track of directories ending in .cache
522 |
523 | # Others
524 |
525 | # Including strong name files can present a security risk
526 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
527 |
528 | # Since there are multiple workflows, uncomment next line to ignore bower_components
529 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
530 |
531 | # RIA/Silverlight projects
532 |
533 | # Backup & report files from converting an old project file
534 | # to a newer Visual Studio version. Backup files are not needed,
535 | # because we have git ;-)
536 |
537 | # SQL Server files
538 |
539 | # Business Intelligence projects
540 |
541 | # Microsoft Fakes
542 |
543 | # GhostDoc plugin setting file
544 |
545 | # Node.js Tools for Visual Studio
546 |
547 | # Visual Studio 6 build log
548 |
549 | # Visual Studio 6 workspace options file
550 |
551 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
552 |
553 | # Visual Studio 6 auto-generated project file (contains which files were open etc.)
554 |
555 | # Visual Studio 6 workspace and project file (working project files containing files to include in project)
556 |
557 | # Visual Studio 6 technical files
558 |
559 | # Visual Studio LightSwitch build output
560 |
561 | # Paket dependency manager
562 |
563 | # FAKE - F# Make
564 |
565 | # CodeRush personal settings
566 |
567 | # Python Tools for Visual Studio (PTVS)
568 |
569 | # Cake - Uncomment if you are using it
570 | # tools/**
571 | # !tools/packages.config
572 |
573 | # Tabs Studio
574 |
575 | # Telerik's JustMock configuration file
576 |
577 | # BizTalk build output
578 |
579 | # OpenCover UI analysis results
580 |
581 | # Azure Stream Analytics local run output
582 |
583 | # MSBuild Binary and Structured Log
584 |
585 | # NVidia Nsight GPU debugger configuration file
586 |
587 | # MFractors (Xamarin productivity tool) working folder
588 |
589 | # Local History for Visual Studio
590 |
591 | # Visual Studio History (VSHistory) files
592 |
593 | # BeatPulse healthcheck temp database
594 |
595 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
596 |
597 | # Ionide (cross platform F# VS Code tools) working folder
598 |
599 | # Fody - auto-generated XML schema
600 |
601 | # VS Code files for those working on multiple tools
602 |
603 | # Local History for Visual Studio Code
604 |
605 | # Windows Installer files from build outputs
606 |
607 | # JetBrains Rider
608 |
609 | ### VisualStudio Patch ###
610 | # Additional files built by Visual Studio
611 |
612 | # End of https://www.toptal.com/developers/gitignore/api/visualstudio,csharp,c++
613 |
614 | #########################
615 |
616 | *.zip
--------------------------------------------------------------------------------
/app/PowerEvents.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Runtime.InteropServices;
3 |
4 | namespace ParsecVDisplay
5 | {
6 | internal static class PowerEvents
7 | {
8 | static EventHandler _powerModeChanged;
9 | public static event EventHandler PowerModeChanged
10 | {
11 | add
12 | {
13 | _powerModeChanged += value;
14 | if (_powerEventHandler == IntPtr.Zero)
15 | {
16 | var result = Native.PowerRegisterSuspendResumeNotification(2, _dnsp, out _powerEventHandler);
17 | if (result != 0)
18 | throw new Exception("Failed To Register PowerSuspendResumeNotification");
19 | }
20 |
21 | }
22 | remove
23 | {
24 | _powerModeChanged -= value;
25 | if (_powerModeChanged == null)
26 | {
27 | if (Native.PowerUnregisterSuspendResumeNotification(_powerEventHandler) != 0)
28 | throw new Exception("Failed To Unregister PowerSuspendResumeNotification");
29 | _powerEventHandler = IntPtr.Zero;
30 | }
31 | }
32 | }
33 |
34 | static IntPtr _powerEventHandler;
35 | static Native.DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS _dnsp = new Native.DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS
36 | {
37 | Callback = OnDeviceNotify,
38 | Context = IntPtr.Zero
39 | };
40 |
41 | static uint OnDeviceNotify(IntPtr context, uint type, IntPtr setting)
42 | {
43 | _powerModeChanged?.Invoke(null, (PowerBroadcastType)type);
44 | return 0;
45 | }
46 |
47 | public enum PowerBroadcastType
48 | {
49 | PBT_APMQUERYSUSPEND = 0,
50 | //
51 | // Summary:
52 | // The PBT_APMQUERYSUSPEND message is sent to request permission to suspend the
53 | // computer. An application that grants permission should carry out preparations
54 | // for the suspension before returning. Return TRUE to grant the request to suspend.
55 | // To deny the request, return BROADCAST_QUERY_DENY.
56 | PBT_APMQUERYSTANDBY = 1,
57 | //
58 | // Summary:
59 | // [PBT_APMQUERYSUSPENDFAILED is available for use in the operating systems specified
60 | // in the Requirements section. Support for this event was removed in Windows Vista.
61 | // Use SetThreadExecutionState instead.]
62 | // Notifies applications that permission to suspend the computer was denied. This
63 | // event is broadcast if any application or driver returned BROADCAST_QUERY_DENY
64 | // to a previous PBT_APMQUERYSUSPEND event.
65 | // A window receives this event through the WM_POWERBROADCAST message. The wParam
66 | // and lParam parameters are set as described following.
67 | //
68 | // Remarks:
69 | // lParam: Reserved; must be zero.
70 | // No return value.
71 | // Applications typically respond to this event by resuming normal operation.
72 | PBT_APMQUERYSUSPENDFAILED = 2,
73 | //
74 | // Summary:
75 | // The PBT_APMQUERYSUSPENDFAILED message is sent to notify the application that
76 | // suspension was denied by some other application. However, this message is only
77 | // sent when we receive PBT_APMQUERY* before.
78 | PBT_APMQUERYSTANDBYFAILED = 3,
79 | //
80 | // Summary:
81 | // Notifies applications that the computer is about to enter a suspended state.
82 | // This event is typically broadcast when all applications and installable drivers
83 | // have returned TRUE to a previous PBT_APMQUERYSUSPEND event.
84 | // A window receives this event through the WM_POWERBROADCAST message. The wParam
85 | // and lParam parameters are set as described following.
86 | //
87 | // Remarks:
88 | // lParam: Reserved; must be zero.
89 | // No return value.
90 | // An application should process this event by completing all tasks necessary to
91 | // save data.
92 | // The system allows approximately two seconds for an application to handle this
93 | // notification. If an application is still performing operations after its time
94 | // allotment has expired, the system may interrupt the application.
95 | PBT_APMSUSPEND = 4,
96 | //
97 | // Summary:
98 | // Undocumented.
99 | PBT_APMSTANDBY = 5,
100 | //
101 | // Summary:
102 | // [PBT_APMRESUMECRITICAL is available for use in the operating systems specified
103 | // in the Requirements section. Support for this event was removed in Windows Vista.
104 | // Use PBT_APMRESUMEAUTOMATIC instead.]
105 | // Notifies applications that the system has resumed operation. This event can indicate
106 | // that some or all applications did not receive a PBT_APMSUSPEND event. For example,
107 | // this event can be broadcast after a critical suspension caused by a failing battery.
108 | // A window receives this event through the WM_POWERBROADCAST message. The wParam
109 | // and lParam parameters are set as described following.
110 | //
111 | // Remarks:
112 | // lParam: Reserved; must be zero.
113 | // No return value.
114 | // Because a critical suspension occurs without prior notification, resources and
115 | // data previously available may not be present when the application receives this
116 | // event. The application should attempt to restore its state to the best of its
117 | // ability. While in a critical suspension, the system maintains the state of the
118 | // DRAM and local hard disks, but may not maintain net connections. An application
119 | // may need to take action with respect to files that were open on the network before
120 | // critical suspension.
121 | PBT_APMRESUMECRITICAL = 6,
122 | //
123 | // Summary:
124 | // Notifies applications that the system has resumed operation after being suspended.
125 | // A window receives this event through the WM_POWERBROADCAST message. The wParam
126 | // and lParam parameters are set as described following.
127 | //
128 | // Remarks:
129 | // lParam: Reserved; must be zero.
130 | // No return value.
131 | // An application can receive this event only if it received the PBT_APMSUSPEND
132 | // event before the computer was suspended. Otherwise, the application will receive
133 | // a PBT_APMRESUMECRITICAL event.
134 | // If the system wakes due to user activity (such as pressing the power button)
135 | // or if the system detects user interaction at the physical console (such as mouse
136 | // or keyboard input) after waking unattended, the system first broadcasts the PBT_APMRESUMEAUTOMATIC
137 | // event, then it broadcasts the PBT_APMRESUMESUSPEND event. In addition, the system
138 | // turns on the display. Your application should reopen files that it closed when
139 | // the system entered sleep and prepare for user input.
140 | // If the system wakes due to an external wake signal (remote wake), the system
141 | // broadcasts only the PBT_APMRESUMEAUTOMATIC event. The PBT_APMRESUMESUSPEND event
142 | // is not sent.
143 | PBT_APMRESUMESUSPEND = 7,
144 | //
145 | // Summary:
146 | // The PBT_APMRESUMESTANDBY event is broadcast as a notification that the system
147 | // has resumed operation after being standby.
148 | PBT_APMRESUMESTANDBY = 8,
149 | //
150 | // Summary:
151 | // [PBT_APMBATTERYLOW is available for use in the operating systems specified in
152 | // the Requirements section. Support for this event was removed in Windows Vista.
153 | // Use PBT_APMPOWERSTATUSCHANGE instead.]
154 | // Notifies applications that the battery power is low.
155 | // A window receives this event through the WM_POWERBROADCAST message. The wParam
156 | // and lParam parameters are set as described following.
157 | //
158 | // Remarks:
159 | // lParam: Reserved, must be zero.
160 | // No return value.
161 | // This event is broadcast when a system's APM BIOS signals an APM battery low notification.
162 | // Because some APM BIOS implementations do not provide notifications when batteries
163 | // are low, this event may never be broadcast on some computers.
164 | PBT_APMBATTERYLOW = 9,
165 | //
166 | // Summary:
167 | // Notifies applications of a change in the power status of the computer, such as
168 | // a switch from battery power to A/C. The system also broadcasts this event when
169 | // remaining battery power slips below the threshold specified by the user or if
170 | // the battery power changes by a specified percentage.
171 | // A window receives this event through the WM_POWERBROADCAST message. The wParam
172 | // and lParam parameters are set as described following.
173 | //
174 | // Remarks:
175 | // lParam: Reserved; must be zero.
176 | // No return value.
177 | // An application should process this event by calling the GetSystemPowerStatus
178 | // function to retrieve the current power status of the computer. In particular,
179 | // the application should check the ACLineStatus, BatteryFlag, BatteryLifeTime,
180 | // and BatteryLifePercent members of the SYSTEM_POWER_STATUS structure for any changes.
181 | // This event can occur when battery life drops to less than 5 minutes, or when
182 | // the percentage of battery life drops below 10 percent, or if the battery life
183 | // changes by 3 percent.
184 | PBT_APMPOWERSTATUSCHANGE = 10,
185 | //
186 | // Summary:
187 | // [PBT_APMOEMEVENT is available for use in the operating systems specified in the
188 | // Requirements section. Support for this event was removed in Windows Vista.]
189 | // Notifies applications that the APM BIOS has signaled an APM OEM event.
190 | // A window receives this event through the WM_POWERBROADCAST message. The wParam
191 | // and lParam parameters are set as described following.
192 | //
193 | // Remarks:
194 | // lParam: The OEM-defined event code that was signaled by the system's APM BIOS.
195 | // OEM event codes are in the range 0200h - 02FFh.
196 | // No return value.
197 | // Because not all APM BIOS implementations provide OEM event notifications, this
198 | // event may never be broadcast on some computers.
199 | PBT_APMOEMEVENT = 11,
200 | //
201 | // Summary:
202 | // Notifies applications that the system is resuming from sleep or hibernation.
203 | // This event is delivered every time the system resumes and does not indicate whether
204 | // a user is present.
205 | // A window receives this event through the WM_POWERBROADCAST message. The wParam
206 | // and lParam parameters are set as described following.
207 | //
208 | // Remarks:
209 | // lParam: Reserved; must be zero.
210 | // No return value.
211 | // If the system detects any user activity after broadcasting PBT_APMRESUMEAUTOMATIC,
212 | // it will broadcast a PBT_APMRESUMESUSPEND event to let applications know they
213 | // can resume full interaction with the user.
214 | PBT_APMRESUMEAUTOMATIC = 18,
215 | //
216 | // Summary:
217 | // Power setting change event sent with a WM_POWERBROADCAST window message or in
218 | // a HandlerEx notification callback for services.
219 | //
220 | // Remarks:
221 | // lParam: Pointer to a POWERBROADCAST_SETTING structure.
222 | // No return value.
223 | PBT_POWERSETTINGCHANGE = 32787,
224 | ERROR_ERROR = 10101
225 | }
226 |
227 | static class Native
228 | {
229 | [UnmanagedFunctionPointer(CallingConvention.Winapi)]
230 | public delegate uint DeviceNotifyCallbackRoutine(IntPtr Context, uint Type, IntPtr Setting);
231 |
232 | [StructLayout(LayoutKind.Sequential)]
233 | public struct DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS
234 | {
235 | [MarshalAs(UnmanagedType.FunctionPtr)]
236 | public DeviceNotifyCallbackRoutine Callback;
237 | public IntPtr Context;
238 | }
239 |
240 | [DllImport("powrprof.dll", SetLastError = false, ExactSpelling = true)]
241 | public static extern uint PowerRegisterSuspendResumeNotification(int Flags, in DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS Recipient, out IntPtr RegistrationHandle);
242 |
243 | [DllImport("powrprof.dll", SetLastError = false, ExactSpelling = true)]
244 | public static extern uint PowerUnregisterSuspendResumeNotification(IntPtr RegistrationHandle);
245 | }
246 | }
247 | }
--------------------------------------------------------------------------------
/app/MirrorWindow.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.ComponentModel;
3 | using System.Diagnostics;
4 | using System.Drawing;
5 | using System.Runtime.InteropServices;
6 | using System.Threading;
7 | using System.Threading.Tasks;
8 | using System.Windows.Forms;
9 |
10 | namespace ParsecVDisplay
11 | {
12 | public class MirrorWindow : Form
13 | {
14 | private bool IsMirroring;
15 | private Thread MirrorThread;
16 | private TaskCompletionSource WhenHwnd;
17 |
18 | public MirrorWindow()
19 | {
20 | IsMirroring = false;
21 | WhenHwnd = new TaskCompletionSource();
22 |
23 | ClientSize = new Size(960, 540);
24 | Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath);
25 | }
26 |
27 | protected override void OnClosing(CancelEventArgs e)
28 | {
29 | IsMirroring = false;
30 | MirrorThread?.Join();
31 |
32 | base.OnClosing(e);
33 | }
34 |
35 | protected override void OnPaint(PaintEventArgs e)
36 | {
37 | }
38 |
39 | protected override void OnPaintBackground(PaintEventArgs e)
40 | {
41 | }
42 |
43 | protected override void OnHandleCreated(EventArgs e)
44 | {
45 | base.OnHandleCreated(e);
46 | WhenHwnd.SetResult(Handle);
47 | }
48 |
49 | public void MirrorScreen(string displayDevice)
50 | {
51 | if (!IsMirroring)
52 | {
53 | IsMirroring = true;
54 | Text = $"Mirror - {displayDevice}";
55 |
56 | int fps = Config.MirroringFPS;
57 |
58 | MirrorThread = new Thread(() => MirrorWorker(displayDevice, fps));
59 | MirrorThread.IsBackground = true;
60 | MirrorThread.Start();
61 | }
62 | }
63 |
64 | private void MirrorWorker(string displayDevice, int fps)
65 | {
66 | var hwnd = WhenHwnd.Task.Result;
67 |
68 | var dcDest = Native.GetDC(hwnd);
69 | var bgBrush = Native.GetStockObject(/*BLACK_BRUSH*/ 4);
70 |
71 | var devmode = default(Native.DEVMODE);
72 | short devmodeSize = (short)Marshal.SizeOf();
73 |
74 | try
75 | {
76 | var stopwatch = Stopwatch.StartNew();
77 | double previousTime = stopwatch.Elapsed.TotalMilliseconds;
78 |
79 | double frameTime = 1000.0 / fps;
80 |
81 | while (IsMirroring)
82 | {
83 | double currentTime = stopwatch.Elapsed.TotalMilliseconds;
84 | double elapsedTime = currentTime - previousTime;
85 |
86 | if (elapsedTime >= frameTime)
87 | {
88 | devmode.dmSize = devmodeSize;
89 |
90 | if (Native.EnumDisplaySettings(displayDevice, -1, ref devmode))
91 | {
92 | var dcScreens = Native.GetDC(IntPtr.Zero);
93 |
94 | var client = GetClientSize(hwnd);
95 | var screen = new Rectangle(devmode.dmPositionX, devmode.dmPositionY, devmode.dmPelsWidth, devmode.dmPelsHeight);
96 | var vp = GetViewport(client.Width, client.Height, screen.Width, screen.Height);
97 |
98 | DrawBackground(dcDest, bgBrush, ref client, ref vp);
99 | DrawScreen(dcDest, dcScreens, ref vp, ref screen);
100 | DrawCursor(dcDest, ref vp, ref screen);
101 |
102 | Native.ReleaseDC(IntPtr.Zero, dcScreens);
103 | }
104 |
105 | previousTime = currentTime;
106 | }
107 | else
108 | {
109 | int sleepTime = (int)(frameTime - elapsedTime);
110 | if (sleepTime > 0)
111 | Thread.Sleep(sleepTime);
112 | }
113 | }
114 | }
115 | finally
116 | {
117 | Native.ReleaseDC(hwnd, dcDest);
118 | Native.DeleteObject(bgBrush);
119 | }
120 | }
121 |
122 | private struct Viewport
123 | {
124 | public int X;
125 | public int Y;
126 | public int Width;
127 | public int Height;
128 | }
129 |
130 | private static void DrawBackground(IntPtr dc, IntPtr brush, ref Size client, ref Viewport vp)
131 | {
132 | var rect = default(Rectangle);
133 |
134 | // fill the excluded rectangles (areas outside the viewport)
135 | // this is the simplest way to avoid flickering without WM_PAINT
136 |
137 | // top excluded rect
138 | if (vp.Y > 0)
139 | {
140 | rect.X = 0;
141 | rect.Y = 0;
142 | rect.Width = client.Width;
143 | rect.Height = vp.Y;
144 |
145 | Native.FillRect(dc, ref rect, brush);
146 | }
147 |
148 | // bottom excluded rect
149 | if (vp.Y + vp.Height < client.Height)
150 | {
151 | rect.X = 0;
152 | rect.Y = vp.Y + vp.Height;
153 | rect.Width = client.Width;
154 | rect.Height = client.Height;
155 |
156 | Native.FillRect(dc, ref rect, brush);
157 | }
158 |
159 | // left excluded rect
160 | if (vp.X > 0)
161 | {
162 | rect.X = 0;
163 | rect.Y = vp.Y;
164 | rect.Width = vp.X;
165 | rect.Height = vp.Height + vp.Y;
166 |
167 | Native.FillRect(dc, ref rect, brush);
168 | }
169 |
170 | // right excluded rect
171 | if (vp.X + vp.Width < client.Width)
172 | {
173 | rect.X = vp.X + vp.Width;
174 | rect.Y = vp.Y;
175 | rect.Width = client.Width;
176 | rect.Height = vp.Height + vp.Y;
177 |
178 | Native.FillRect(dc, ref rect, brush);
179 | }
180 | }
181 |
182 | private static void DrawScreen(IntPtr dc, IntPtr dcSrc, ref Viewport vp, ref Rectangle screen)
183 | {
184 | // set scaling mode
185 | Native.SetStretchBltMode(dc, /*HALFTONE*/ 4);
186 |
187 | // draw the screen
188 | Native.StretchBlt(
189 | dc,
190 | vp.X, vp.Y, vp.Width, vp.Height,
191 | dcSrc,
192 | screen.X, screen.Y, screen.Width, screen.Height,
193 | Native.SRCCOPY
194 | );
195 | }
196 |
197 | private static void DrawCursor(IntPtr dc, ref Viewport vp, ref Rectangle screen)
198 | {
199 | var cursor = default(Native.CURSORINFO);
200 | cursor.cbSize = Marshal.SizeOf();
201 |
202 | if (Native.GetCursorInfo(ref cursor)
203 | // cursor must be inside the screen
204 | && screen.Contains(cursor.screenPosX, cursor.screenPosY)
205 | // and visible
206 | && cursor.flags == /*CURSOR_SHOWING*/ 0x1)
207 | {
208 | var iconInfo = default(Native.ICONINFO);
209 | Native.GetIconInfo(cursor.hCursor, ref iconInfo);
210 |
211 | var bmpCursor = default(Native.BITMAP);
212 | Native.GetObject(iconInfo.hbmColor, Marshal.SizeOf(), ref bmpCursor);
213 |
214 | int x = cursor.screenPosX - iconInfo.xHotspot - screen.X;
215 | int y = cursor.screenPosY - iconInfo.yHotspot - screen.Y;
216 | int width = bmpCursor.bmWidth;
217 | int height = bmpCursor.bmHeight;
218 | ScaleCursor(ref vp, screen.Size, ref x, ref y, ref width, ref height);
219 |
220 | Native.DrawIconEx(dc, x, y, cursor.hCursor, width, height, 0, IntPtr.Zero, /*DI_NORMAL*/ 0x3);
221 | }
222 | }
223 |
224 | private static Size GetClientSize(IntPtr hwnd)
225 | {
226 | var rect = new Rectangle();
227 | Native.GetClientRect(hwnd, ref rect);
228 | return new Size(rect.Width, rect.Height);
229 | }
230 |
231 | private static Viewport GetViewport(int clientWidth, int clientHeight, int rectWidth, int rectHeight)
232 | {
233 | float clientAspect = (float)clientWidth / clientHeight;
234 | float rectAspect = (float)rectWidth / rectHeight;
235 |
236 | int viewportX, viewportY;
237 | int viewportWidth, viewportHeight;
238 |
239 | // compare aspect ratios to determine scaling
240 | if (clientAspect > rectAspect)
241 | {
242 | // client is wider than rect, so scale to fit height
243 | viewportHeight = clientHeight;
244 | viewportWidth = (int)(rectAspect * viewportHeight);
245 | }
246 | else
247 | {
248 | // client is taller than rect, so scale to fit width
249 | viewportWidth = clientWidth;
250 | viewportHeight = (int)(viewportWidth / rectAspect);
251 | }
252 |
253 | // center the viewport
254 | viewportX = (clientWidth - viewportWidth) / 2;
255 | viewportY = (clientHeight - viewportHeight) / 2;
256 |
257 | return new Viewport
258 | {
259 | X = viewportX,
260 | Y = viewportY,
261 | Width = viewportWidth,
262 | Height = viewportHeight,
263 | };
264 | }
265 |
266 | private static void ScaleCursor(ref Viewport viewport, Size screen, ref int cursorX, ref int cursorY, ref int cursorWidth, ref int cursorHeight)
267 | {
268 | float scaleX = (float)viewport.Width / screen.Width;
269 | float scaleY = (float)viewport.Height / screen.Height;
270 |
271 | cursorWidth = (int)(cursorWidth * scaleX);
272 | cursorHeight = (int)(cursorHeight * scaleY);
273 |
274 | cursorX = viewport.X + (int)(cursorX * scaleX);
275 | cursorY = viewport.Y + (int)(cursorY * scaleY);
276 | }
277 |
278 | private static class Native
279 | {
280 | public const int ENUM_CURRENT_SETTINGS = -1;
281 | public const int SRCCOPY = 0x00CC0020;
282 |
283 | [StructLayout(LayoutKind.Sequential)]
284 | public struct DEVMODE
285 | {
286 | [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
287 | public string dmDeviceName;
288 | public short dmSpecVersion;
289 | public short dmDriverVersion;
290 | public short dmSize;
291 | public short dmDriverExtra;
292 | public int dmFields;
293 | public int dmPositionX;
294 | public int dmPositionY;
295 | public int dmDisplayOrientation;
296 | public int dmDisplayFixedOutput;
297 | public short dmColor;
298 | public short dmDuplex;
299 | public short dmYResolution;
300 | public short dmTTOption;
301 | public short dmCollate;
302 | [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
303 | public string dmFormName;
304 | public short dmLogPixels;
305 | public int dmBitsPerPel;
306 | public int dmPelsWidth;
307 | public int dmPelsHeight;
308 | public int dmDisplayFlags;
309 | public int dmDisplayFrequency;
310 | public int dmICMMethod;
311 | public int dmICMIntent;
312 | public int dmMediaType;
313 | public int dmDitherType;
314 | public int dmReserved1;
315 | public int dmReserved2;
316 | public int dmPanningWidth;
317 | public int dmPanningHeight;
318 | }
319 |
320 | [DllImport("user32.dll")]
321 | [return: MarshalAs(UnmanagedType.Bool)]
322 | public static extern bool EnumDisplaySettings(string deviceName, int modeNum, ref DEVMODE devMode);
323 |
324 | [DllImport("user32.dll")]
325 | [return: MarshalAs(UnmanagedType.Bool)]
326 | public static extern bool GetClientRect(IntPtr hwnd, ref Rectangle rect);
327 |
328 | [DllImport("user32.dll")]
329 | public static extern IntPtr GetDC(IntPtr hwnd);
330 |
331 | [DllImport("user32.dll")]
332 | public static extern int ReleaseDC(IntPtr hwnd, IntPtr hdc);
333 |
334 | [DllImport("gdi32.dll", SetLastError = true)]
335 | public static extern IntPtr CreateDC(string lpszDriver, string lpszDevice, string lpszOutput, IntPtr lpInitData);
336 |
337 | [DllImport("gdi32.dll", SetLastError = true)]
338 | public static extern bool DeleteDC(IntPtr hdc);
339 |
340 | [DllImport("gdi32.dll", SetLastError = true)]
341 | public static extern bool DeleteObject(IntPtr hObject);
342 |
343 | [DllImport("gdi32.dll")]
344 | public static extern int SetStretchBltMode(IntPtr hdc, int mode);
345 |
346 | [DllImport("gdi32.dll")]
347 | public static extern bool StretchBlt(IntPtr hdcDest, int nXOriginDest, int nYOriginDest, int nWidthDest, int nHeightDest,
348 | IntPtr hdcSrc, int nXOriginSrc, int nYOriginSrc, int nWidthSrc, int nHeightSrc, uint dwRop);
349 |
350 | [StructLayout(LayoutKind.Sequential)]
351 | public struct BITMAP
352 | {
353 | public uint bmType;
354 | public int bmWidth;
355 | public int bmHeight;
356 | public int bmWidthBytes;
357 | public short bmPlanes;
358 | public short bmBitsPixel;
359 | public IntPtr bmBits;
360 | }
361 |
362 | [StructLayout(LayoutKind.Sequential)]
363 | public struct ICONINFO
364 | {
365 | public int fIcon;
366 | public int xHotspot;
367 | public int yHotspot;
368 | public IntPtr hbmMask;
369 | public IntPtr hbmColor;
370 | }
371 |
372 | [StructLayout(LayoutKind.Sequential)]
373 | public struct CURSORINFO
374 | {
375 | public int cbSize;
376 | public uint flags;
377 | public IntPtr hCursor;
378 | public int screenPosX;
379 | public int screenPosY;
380 | }
381 |
382 | [DllImport("user32.dll")]
383 | [return: MarshalAs(UnmanagedType.Bool)]
384 | public static extern bool GetIconInfo(IntPtr hIcon, ref ICONINFO piconinfo);
385 |
386 | [DllImport("user32.dll")]
387 | [return: MarshalAs(UnmanagedType.Bool)]
388 | public static extern bool GetCursorInfo(ref CURSORINFO pci);
389 |
390 | [DllImport("gdi32.dll")]
391 | public static extern int GetObject(IntPtr h, int c, ref BITMAP pv);
392 |
393 | [DllImport("user32.dll")]
394 | [return: MarshalAs(UnmanagedType.Bool)]
395 | public static extern bool DrawIconEx(IntPtr hdc,
396 | int xLeft, int yTop, IntPtr hIcon, int cxWidth, int cyWidth,
397 | uint istepIfAniCur, IntPtr hbrFlickerFreeDraw, uint diFlags);
398 |
399 | [DllImport("user32.dll")]
400 | public static extern int FillRect(IntPtr hDC, ref Rectangle lprc, IntPtr hbr);
401 |
402 | [DllImport("gdi32.dll")]
403 | public static extern IntPtr GetStockObject(int i);
404 | }
405 | }
406 | }
--------------------------------------------------------------------------------