diff --git a/app/Vdd/Controller.cs b/app/Vdd/Controller.cs
index 066d5de..3f73d86 100644
--- a/app/Vdd/Controller.cs
+++ b/app/Vdd/Controller.cs
@@ -1,5 +1,5 @@
-using System;
-using System.Diagnostics;
+using System;
+using System.Collections.Generic;
using System.Threading;
namespace ParsecVDisplay.Vdd
@@ -11,11 +11,26 @@ namespace ParsecVDisplay.Vdd
static CancellationTokenSource Cancellation;
static IntPtr VddHandle = IntPtr.Zero;
- static Device.Status LastStatus;
+ // Stored as int so Interlocked can be used safely; cast on read
+ static int LastStatusValue;
+ static Device.Status LastStatus => (Device.Status)Volatile.Read(ref LastStatusValue);
+
+ // Wakes StatusLoop on demand (e.g. after Resume) instead of waiting up to 2s
+ static ManualResetEventSlim StatusKick;
+ // Signals that the device handle is open and ready to receive IOCTLs
+ static ManualResetEventSlim HandleReady;
+ // True while the controller is suspended (sleep / hibernation)
+ static volatile bool Suspended;
+
+ public static bool IsSuspended => Suspended;
+ public static bool IsHandleReady => VddHandle.IsValidHandle();
public static void Start()
{
Cancellation = new CancellationTokenSource();
+ StatusKick = new ManualResetEventSlim(false);
+ HandleReady = new ManualResetEventSlim(false);
+ Suspended = false;
UpdateThread = new Thread(() => UpdateLoop(Cancellation.Token));
UpdateThread.IsBackground = true;
@@ -32,21 +47,90 @@ namespace ParsecVDisplay.Vdd
public static void Stop()
{
Cancellation?.Cancel();
+ StatusKick?.Set();
UpdateThread?.Join();
StatusThread?.Join();
- Device.CloseHandle(VddHandle);
+ var handle = Interlocked.Exchange(ref VddHandle, IntPtr.Zero);
+ Device.CloseHandle(handle);
+
+ StatusKick?.Dispose();
+ HandleReady?.Dispose();
+ }
+
+ ///
+ /// Block until the device handle is open or the timeout elapses.
+ /// Returns true if the handle is ready.
+ ///
+ public static bool WaitForReady(int timeoutMs)
+ {
+ return HandleReady != null && HandleReady.Wait(timeoutMs);
+ }
+
+ ///
+ /// Force StatusLoop to re-check the driver status now instead of
+ /// waiting up to 2 seconds for the next tick. Cheap, no allocation.
+ ///
+ public static void KickStatusCheck()
+ {
+ StatusKick?.Set();
+ }
+
+ ///
+ /// Suspend driver activity: snapshot current displays, unplug them in
+ /// reverse order (preserves Windows 10 Connectivity registry config),
+ /// then close the device handle so no IOCTLs are sent while the system
+ /// sleeps. The keep-alive thread becomes a no-op while suspended.
+ ///
+ public static List Suspend()
+ {
+ if (Suspended)
+ return new List();
+
+ var displays = Core.GetDisplays();
+ var snapshot = displays.ConvertAll(d => d.Snapshot());
+
+ Suspended = true;
+
+ // Unplug in reverse order to keep Windows 10 from inventing a
+ // new Connectivity registry entry for the remaining subset.
+ if (VddHandle.IsValidHandle())
+ {
+ for (int i = displays.Count - 1; i >= 0; i--)
+ {
+ try { Core.RemoveDisplay(VddHandle, displays[i].DisplayIndex); }
+ catch { /* best effort */ }
+ }
+ }
+
+ var handle = Interlocked.Exchange(ref VddHandle, IntPtr.Zero);
+ HandleReady.Reset();
+ Device.CloseHandle(handle);
+
+ return snapshot;
+ }
+
+ ///
+ /// Mark the controller as resumed and wake StatusLoop so the handle
+ /// is reopened as soon as the driver reports OK. Callers should then
+ /// WaitForReady before adding displays.
+ ///
+ public static void Resume()
+ {
+ Suspended = false;
+ StatusKick?.Set();
}
static void UpdateLoop(CancellationToken cancellation)
{
- while (true)
+ while (!cancellation.IsCancellationRequested)
{
- if (cancellation.IsCancellationRequested)
- break;
-
- if (VddHandle.IsValidHandle() && LastStatus == Device.Status.OK)
+ if (!Suspended
+ && VddHandle.IsValidHandle()
+ && LastStatus == Device.Status.OK)
+ {
Core.Update(VddHandle);
+ }
Thread.Sleep(100);
}
@@ -54,46 +138,44 @@ namespace ParsecVDisplay.Vdd
static void StatusLoop(CancellationToken cancellation)
{
- bool first = true;
- var sw = Stopwatch.StartNew();
-
- while (true)
+ while (!cancellation.IsCancellationRequested)
{
- if (cancellation.IsCancellationRequested)
- break;
-
- if (first || sw.ElapsedMilliseconds >= 2000)
+ if (Suspended)
{
- first = false;
-
- var status = QueryStatus(out var _);
- unsafe
- {
- fixed (Device.Status* s = &LastStatus)
- {
- Interlocked.Exchange(ref *(int*)s, (int)status);
- }
- }
-
- if (status == Device.Status.OK)
- {
- if (!VddHandle.IsValidHandle())
- {
- Device.OpenHandle(Core.ADAPTER_GUID, out var handle);
- Interlocked.Exchange(ref VddHandle, handle);
- }
- }
- else
- {
- var handle = VddHandle;
- Interlocked.Exchange(ref VddHandle, IntPtr.Zero);
- Device.CloseHandle(handle);
- }
-
- sw.Restart();
+ // Sleep until Resume() (or Stop) signals us
+ try { StatusKick.Wait(Timeout.Infinite, cancellation); }
+ catch (OperationCanceledException) { break; }
+ StatusKick.Reset();
+ continue;
}
- Thread.Sleep(50);
+ var status = QueryStatus(out var _);
+ Volatile.Write(ref LastStatusValue, (int)status);
+
+ if (status == Device.Status.OK)
+ {
+ if (!VddHandle.IsValidHandle())
+ {
+ Device.OpenHandle(Core.ADAPTER_GUID, out var handle);
+ Interlocked.Exchange(ref VddHandle, handle);
+ if (handle.IsValidHandle())
+ HandleReady.Set();
+ }
+ }
+ else
+ {
+ var handle = Interlocked.Exchange(ref VddHandle, IntPtr.Zero);
+ if (handle != IntPtr.Zero)
+ {
+ HandleReady.Reset();
+ Device.CloseHandle(handle);
+ }
+ }
+
+ // Wait up to 2s — KickStatusCheck() / Resume() shortcut this.
+ try { StatusKick.Wait(2000, cancellation); }
+ catch (OperationCanceledException) { break; }
+ StatusKick.Reset();
}
}
@@ -109,6 +191,13 @@ namespace ParsecVDisplay.Vdd
public static void AddDisplay()
{
+ AddDisplay(out var _);
+ }
+
+ public static void AddDisplay(out int driverIndex)
+ {
+ driverIndex = -1;
+
var status = QueryStatus();
if (status != Device.Status.OK)
throw new ErrorDriverStatus(status);
@@ -117,16 +206,16 @@ namespace ParsecVDisplay.Vdd
var displays = Core.GetDisplays();
if (displays.Count >= limit)
- {
throw new ErrorExceededLimit(limit);
- }
- else
- {
- if (!Core.AddDisplay(VddHandle, out var _))
- {
- throw new ErrorOperationFailed(ErrorOperationFailed.Operation.AddDisplay);
- }
- }
+
+ // Snapshot the handle ONCE so StatusLoop closing it after this
+ // check doesn't leave us calling DeviceIoControl on a stale value.
+ var handle = VddHandle;
+ if (!handle.IsValidHandle())
+ throw new ErrorDeviceHandle();
+
+ if (!Core.AddDisplay(handle, out driverIndex))
+ throw new ErrorOperationFailed(ErrorOperationFailed.Operation.AddDisplay);
}
public static void RemoveDisplay(int index)
@@ -135,13 +224,14 @@ namespace ParsecVDisplay.Vdd
if (status != Device.Status.OK)
throw new ErrorDriverStatus(status);
- if (index >= 0)
- {
- if (!Core.RemoveDisplay(VddHandle, index))
- {
- throw new ErrorOperationFailed(ErrorOperationFailed.Operation.RemoveDisplay);
- }
- }
+ if (index < 0)
+ return;
+
+ if (!VddHandle.IsValidHandle())
+ throw new ErrorDeviceHandle();
+
+ if (!Core.RemoveDisplay(VddHandle, index))
+ throw new ErrorOperationFailed(ErrorOperationFailed.Operation.RemoveDisplay);
}
public static void RemoveLastDisplay()
@@ -154,4 +244,4 @@ namespace ParsecVDisplay.Vdd
}
}
}
-}
\ No newline at end of file
+}