From dda9c6b7995cdb2a12fe44cbcfc2fa801dff1aab Mon Sep 17 00:00:00 2001 From: Nguyen Duy Date: Thu, 14 May 2026 15:30:49 +0700 Subject: [PATCH] feat: update fallback display, add timeout - Count correct physical displays - Add timeout before adding virtual display - Remove virtual display after physical display is back --- app/MainWindow.xaml.cs | 7 +--- app/Tray.cs | 73 ++++++++++++++++++++++++++++++++++++++++++ app/Vdd/Core.cs | 15 +++++++++ 3 files changed, 89 insertions(+), 6 deletions(-) diff --git a/app/MainWindow.xaml.cs b/app/MainWindow.xaml.cs index 6b2bbf1..19e3183 100644 --- a/app/MainWindow.xaml.cs +++ b/app/MainWindow.xaml.cs @@ -90,7 +90,7 @@ namespace ParsecVDisplay { Dispatcher.Invoke(() => { - var displays = Vdd.Core.GetDisplays(out bool noMonitors); + var displays = Vdd.Core.GetDisplays(); xDisplays.Children.Clear(); xNoDisplay.Visibility = displays.Count > 0 @@ -103,11 +103,6 @@ namespace ParsecVDisplay } xAdd.IsEnabled = true; - - if (noMonitors && Config.FallbackDisplay) - { - AddDisplay(null, EventArgs.Empty); - } }); } diff --git a/app/Tray.cs b/app/Tray.cs index 0a49d46..12f4ba2 100644 --- a/app/Tray.cs +++ b/app/Tray.cs @@ -37,6 +37,16 @@ namespace ParsecVDisplay // re-trigger us while we're still showing the error MessageBox). int InAddDisplay; + // Driver index of the fallback display we auto-added because the host + // had no physical display. -1 = not active. Reset on suspend. + int FallbackDriverIndex = -1; + + // 1-second debounce — Windows fires multiple DisplaySettingsChanged + // events in a storm during display changes; we only act on the trailing + // edge so we don't flap displays during transitions. + System.Threading.Timer FallbackTimer; + const int FallbackEvaluateDelayMs = 1000; + // ParsecVDisplay v{version} // ______________ // Add display @@ -119,9 +129,13 @@ namespace ParsecVDisplay TrayIcon.DoubleClick += delegate { ShowApp(); }; TrayIcon.Visible = true; + FallbackTimer = new System.Threading.Timer(EvaluateFallback, + null, Timeout.Infinite, Timeout.Infinite); + SystemEvents.SessionEnding += SaveDisplayState; SystemEvents.SessionSwitch += SaveDisplayState; SystemEvents.DisplaySettingsChanged += SaveDisplayState; + SystemEvents.DisplaySettingsChanged += ScheduleFallbackEvaluation; PowerEvents.PowerModeChanged += OnPowerModeChanged; // Restore previously-saved displays as soon as the driver handle is ready. @@ -293,6 +307,46 @@ namespace ParsecVDisplay Display.CommitChanges(); } + void ScheduleFallbackEvaluation(object sender, EventArgs e) + { + FallbackTimer?.Change(FallbackEvaluateDelayMs, Timeout.Infinite); + } + + void EvaluateFallback(object state) + { + if (!Config.FallbackDisplay) return; + if (Vdd.Controller.IsSuspended) return; + + int physical = Vdd.Core.CountPhysicalDisplays(); + + // Drop a stale tracked index if the user removed the display manually + if (FallbackDriverIndex >= 0) + { + var parsecs = Vdd.Core.GetDisplays(); + bool stillThere = false; + foreach (var d in parsecs) + if (d.DisplayIndex == FallbackDriverIndex) { stillThere = true; break; } + if (!stillThere) + FallbackDriverIndex = -1; + } + + if (physical == 0 && FallbackDriverIndex < 0) + { + try + { + Vdd.Controller.AddDisplay(out int idx); + FallbackDriverIndex = idx; + } + catch { } + } + else if (physical > 0 && FallbackDriverIndex >= 0) + { + try { Vdd.Controller.RemoveDisplay(FallbackDriverIndex); } + catch { } + FallbackDriverIndex = -1; + } + } + void OnPowerModeChanged(object sender, PowerEvents.PowerBroadcastType type) { switch (type) @@ -300,6 +354,9 @@ namespace ParsecVDisplay case PowerEvents.PowerBroadcastType.PBT_APMSUSPEND: case PowerEvents.PowerBroadcastType.PBT_APMSTANDBY: Interlocked.Exchange(ref ResumeHandled, 0); + // The display we tracked is about to be unplugged; the + // resume path will re-evaluate fallback from scratch. + FallbackDriverIndex = -1; try { SuspendSnapshot = Vdd.Controller.Suspend(); } catch { } break; @@ -445,7 +502,21 @@ namespace ParsecVDisplay } } else if (sender == MI_FallbackDisplay) + { Config.FallbackDisplay = MI_FallbackDisplay.Checked; + if (MI_FallbackDisplay.Checked) + { + // Evaluate now in case the host is currently headless + FallbackTimer?.Change(0, Timeout.Infinite); + } + else if (FallbackDriverIndex >= 0) + { + // Disabling — drop our auto-added display, leave user displays alone + try { Vdd.Controller.RemoveDisplay(FallbackDriverIndex); } + catch { } + FallbackDriverIndex = -1; + } + } else if (sender == MI_KeepScreenOn) Config.KeepScreenOn = MI_KeepScreenOn.Checked; } @@ -525,7 +596,9 @@ namespace ParsecVDisplay SystemEvents.SessionEnding -= SaveDisplayState; SystemEvents.SessionSwitch -= SaveDisplayState; SystemEvents.DisplaySettingsChanged -= SaveDisplayState; + SystemEvents.DisplaySettingsChanged -= ScheduleFallbackEvaluation; PowerEvents.PowerModeChanged -= OnPowerModeChanged; + FallbackTimer?.Dispose(); if (Config.DisplayCount >= 0) Config.DisplayCount = displays.Count; diff --git a/app/Vdd/Core.cs b/app/Vdd/Core.cs index 149e5af..f74f6b6 100644 --- a/app/Vdd/Core.cs +++ b/app/Vdd/Core.cs @@ -54,6 +54,21 @@ namespace ParsecVDisplay.Vdd return GetDisplays(out var _); } + /// + /// Count active non-Parsec displays. Used by the fallback-display + /// feature to decide whether the host has any "real" output. + /// + public static int CountPhysicalDisplays() + { + int count = 0; + foreach (var d in Display.GetAllDisplays()) + { + if (d.Active && !d.DisplayName.Equals(DISPLAY_ID, StringComparison.OrdinalIgnoreCase)) + count++; + } + return count; + } + /// /// Query the driver device status. ///