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 +}