diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index e6fd556..e67003d 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -77,12 +77,19 @@ jobs: - name: Package ${{ matrix.platform }} run: python .\utils\post-build.py .\bin\${{ matrix.platform }}\Release\output . ${{ env.VERSION }} + - name: Stage ${{ matrix.platform }} artifact folder + shell: pwsh + run: | + $artifactRoot = ".\bin\${{ matrix.platform }}\Release\artifact" + New-Item -ItemType Directory -Path $artifactRoot -Force | Out-Null + Copy-Item -Path ".\bin\${{ matrix.platform }}\Release\DS4Windows" -Destination $artifactRoot -Recurse -Force + - name: Upload ${{ matrix.platform }} artifact id: upload uses: actions/upload-artifact@v4 with: - name: DS4Windows-${{ matrix.platform }} - path: .\bin\${{ matrix.platform }}\Release\DS4Windows_${{ env.VERSION }}_${{ matrix.platform }}.zip + name: DS4Windows_${{ env.VERSION }}_${{ matrix.platform }} + path: .\bin\${{ matrix.platform }}\Release\artifact if-no-files-found: error - name: Publish artifact link to run summary @@ -92,5 +99,5 @@ jobs: echo "" >> "$GITHUB_STEP_SUMMARY" echo "Version: \`${{ env.VERSION }}\`" >> "$GITHUB_STEP_SUMMARY" echo "" >> "$GITHUB_STEP_SUMMARY" - echo "[Download DS4Windows-${{ matrix.platform }} (zip)](${{ steps.upload.outputs.artifact-url }})" >> "$GITHUB_STEP_SUMMARY" + echo "[Download DS4Windows_${{ env.VERSION }}_${{ matrix.platform }} (contains DS4Windows folder)](${{ steps.upload.outputs.artifact-url }})" >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e12759b..3891009 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,7 +35,7 @@ jobs: echo "SHORT_VERSION=${VERSION%-*}" >> $GITHUB_ENV shell: bash - name: Build X64 - run: dotnet publish .\DS4Windows\DS4WinWPF.csproj -c Release /p:platform=x64 /p:AssemblyVersion=${{ env.SHORT_VERSION }} /p:FileVersion=${{ env.SHORT_VERSION }} /p:Version=${{ env.VERSION }} -o .\bin\x64\Release\output + run: dotnet publish .\DS4Windows\DS4WinWPF.csproj -c Release /p:platform=x64 /p:AssemblyVersion=${{ env.SHORT_VERSION }} /p:FileVersion=${{ env.SHORT_VERSION }} /p:Version=${{ env.VERSION }} /p:InformationalVersion=${{ env.VERSION }} -o .\bin\x64\Release\output - name: Post-Build script X64 run: python .\utils\post-build.py .\bin\x64\Release\output . ${{env.VERSION}} - name: Publish release Build X64 @@ -43,10 +43,10 @@ jobs: env: GITHUB_TOKEN: ${{ github.TOKEN }} - name: Build X86 - run: dotnet publish .\DS4Windows\DS4WinWPF.csproj -c Release /p:platform=x86 /p:AssemblyVersion=${{ env.SHORT_VERSION }} /p:FileVersion=${{ env.SHORT_VERSION }} /p:Version=${{ env.VERSION }} -o .\bin\x86\Release\output + run: dotnet publish .\DS4Windows\DS4WinWPF.csproj -c Release /p:platform=x86 /p:AssemblyVersion=${{ env.SHORT_VERSION }} /p:FileVersion=${{ env.SHORT_VERSION }} /p:Version=${{ env.VERSION }} /p:InformationalVersion=${{ env.VERSION }} -o .\bin\x86\Release\output - name: Post-Build script X86 run: python .\utils\post-build.py .\bin\x86\Release\output . ${{env.VERSION}} - name: Publish release Build X86 run: gh release upload ${{github.event.release.tag_name}} .\bin\x86\Release\DS4Windows_${{env.VERSION}}_x86.zip env: - GITHUB_TOKEN: ${{ github.TOKEN }} \ No newline at end of file + GITHUB_TOKEN: ${{ github.TOKEN }} diff --git a/DS4Windows/App.xaml.cs b/DS4Windows/App.xaml.cs index 93bdbdd..1535c6c 100644 --- a/DS4Windows/App.xaml.cs +++ b/DS4Windows/App.xaml.cs @@ -25,6 +25,8 @@ You should have received a copy of the GNU General Public License using System.IO.MemoryMappedFiles; using System.Net.Http; using System.Runtime.InteropServices; +using System.Security.AccessControl; +using System.Security.Principal; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -89,10 +91,30 @@ private void Application_Startup(object sender, StartupEventArgs e) runShutdown = true; skipSave = true; + if (DS4Windows.GameBarIntegration.TryRunProbeCommand(e.Args)) + { + runShutdown = false; + Current.Shutdown(); + return; + } + ArgumentParser parser = new ArgumentParser(); parser.Parse(e.Args); CheckOptions(parser); + try + { + string exeDir = Path.GetDirectoryName(DS4Windows.Global.exelocation); + if (!string.IsNullOrEmpty(exeDir)) + { + Environment.CurrentDirectory = exeDir; + } + } + catch + { + // Keep startup going. A bad working directory should not block DS4Windows. + } + if (exitApp) { return; @@ -119,8 +141,8 @@ private void Application_Startup(object sender, StartupEventArgs e) try { if (EventWaitHandleAcl.TryOpenExisting(SingleAppComEventName, - System.Security.AccessControl.EventWaitHandleRights.Synchronize | - System.Security.AccessControl.EventWaitHandleRights.Modify, + EventWaitHandleRights.Synchronize | + EventWaitHandleRights.Modify, out EventWaitHandle tempComEvent)) { tempComEvent.Set(); // signal the other instance. @@ -133,7 +155,11 @@ private void Application_Startup(object sender, StartupEventArgs e) } catch (System.UnauthorizedAccessException) { - // Ignore exception + // An existing elevated instance can deny this process access if it was + // started by an older build. Do not continue into a second mapper. + runShutdown = false; + Current.Shutdown(); + return; } // Allow sleep time durations less than 16 ms @@ -143,7 +169,18 @@ private void Application_Startup(object sender, StartupEventArgs e) DS4Windows.Global.RefreshViGEmBusInfo(); // Create the Event handle - threadComEvent = new EventWaitHandle(false, EventResetMode.ManualReset, SingleAppComEventName); + try + { + threadComEvent = CreateSingleAppComEvent(); + } + catch (UnauthorizedAccessException) + { + // Another elevated instance can win the race with older event security. + runShutdown = false; + Current.Shutdown(); + return; + } + CreateTempWorkerThread(); CreateControlService(parser); @@ -181,7 +218,7 @@ private void Application_Startup(object sender, StartupEventArgs e) DispatcherUnhandledException += App_DispatcherUnhandledException; AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; Logger logger = logHolder.Logger; - string version = DS4Windows.Global.exeversion; + string version = DS4Windows.Global.exeDisplayVersion; logger.Info($"DS4Windows version {version}"); logger.Info($"DS4Windows exe file: {DS4Windows.Global.exeFileName}"); logger.Info($"DS4Windows Assembly Architecture: {(Environment.Is64BitProcess ? "x64" : "x86")}"); @@ -190,8 +227,12 @@ private void Application_Startup(object sender, StartupEventArgs e) logger.Info($"OS Release ID: {DS4Windows.Util.GetOSReleaseId()}"); logger.Info($"System Architecture: {(Environment.Is64BitOperatingSystem ? "x64" : "x86")}"); logger.Info("Logger created"); + StartupDiag(logger, $"App bootstrap pid={Environment.ProcessId} admin={DS4Windows.Global.IsAdministrator()} cwd=\"{Environment.CurrentDirectory}\" cmd=\"{Environment.CommandLine}\""); + StartupDiag(logger, $"Exe location=\"{DS4Windows.Global.exelocation}\" configPath=\"{DS4Windows.Global.appdatapath}\" firstRun={firstRun}"); + StartupDiag(logger, "Global.Load begin"); bool readAppConfig = DS4Windows.Global.Load(); + StartupDiag(logger, $"Global.Load end readAppConfig={readAppConfig}"); if (!firstRun && !readAppConfig) { logger.Info($@"Profiles.xml not read at location ${DS4Windows.Global.appdatapath}\Profiles.xml. Using default app settings"); @@ -224,9 +265,16 @@ private void Application_Startup(object sender, StartupEventArgs e) skipSave = false; + StartupDiag(logger, "Global.LoadActions begin"); if (!DS4Windows.Global.LoadActions()) { + StartupDiag(logger, "Global.LoadActions failed; CreateStdActions begin"); DS4Windows.Global.CreateStdActions(); + StartupDiag(logger, "CreateStdActions end"); + } + else + { + StartupDiag(logger, "Global.LoadActions end success"); } // Have app use selected culture @@ -234,16 +282,24 @@ private void Application_Startup(object sender, StartupEventArgs e) DS4Windows.AppThemeChoice themeChoice = DS4Windows.Global.UseCurrentTheme; ChangeTheme(DS4Windows.Global.UseCurrentTheme, false); + StartupDiag(logger, "LoadLinkedProfiles begin"); DS4Windows.Global.LoadLinkedProfiles(); + StartupDiag(logger, "LoadLinkedProfiles end"); + StartupDiag(logger, "MainWindow ctor begin"); DS4Forms.MainWindow window = new DS4Forms.MainWindow(parser); + StartupDiag(logger, "MainWindow ctor end"); MainWindow = window; window.IsInitialShow = true; + StartupDiag(logger, "MainWindow.Show begin"); window.Show(); + StartupDiag(logger, "MainWindow.Show end"); window.IsInitialShow = false; // Set up hooks for IPC command calls HwndSource source = PresentationSource.FromVisual(window) as HwndSource; + StartupDiag(logger, "CreateIPCClassNameMMF begin"); CreateIPCClassNameMMF(source.Handle); + StartupDiag(logger, "CreateIPCClassNameMMF end"); window.CheckMinStatus(); @@ -252,11 +308,28 @@ private void Application_Startup(object sender, StartupEventArgs e) if (DS4Windows.Global.hidHideInstalled) { + StartupDiag(logger, "CheckHidHidePresence begin"); rootHub.CheckHidHidePresence(); + StartupDiag(logger, "CheckHidHidePresence end"); } + StartupDiag(logger, "LoadPermanentSlotsConfig begin"); rootHub.LoadPermanentSlotsConfig(); + StartupDiag(logger, "LoadPermanentSlotsConfig end"); + StartupDiag(logger, "MainWindow.LateChecks begin"); window.LateChecks(parser); + StartupDiag(logger, "MainWindow.LateChecks returned"); + } + + private static void StartupDiag(Logger logger, string message) + { + if (!DS4Windows.Global.VerboseStartupLogging) + { + return; + } + + logger.Info($"[StartupDiag][T{Thread.CurrentThread.ManagedThreadId}] {message}"); + LogManager.Flush(TimeSpan.FromSeconds(1)); } private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) @@ -293,6 +366,29 @@ private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionE } } + private static EventWaitHandle CreateSingleAppComEvent() + { + EventWaitHandleSecurity security = new EventWaitHandleSecurity(); + EventWaitHandleRights appRights = EventWaitHandleRights.Synchronize | + EventWaitHandleRights.Modify | + EventWaitHandleRights.ReadPermissions; + + SecurityIdentifier authenticatedUsersSid = new SecurityIdentifier( + WellKnownSidType.AuthenticatedUserSid, null); + security.AddAccessRule(new EventWaitHandleAccessRule(authenticatedUsersSid, + appRights, AccessControlType.Allow)); + + SecurityIdentifier currentUserSid = WindowsIdentity.GetCurrent().User; + if (currentUserSid != null) + { + security.AddAccessRule(new EventWaitHandleAccessRule(currentUserSid, + EventWaitHandleRights.FullControl, AccessControlType.Allow)); + } + + return EventWaitHandleAcl.Create(false, EventResetMode.ManualReset, + SingleAppComEventName, out _, security); + } + private void App_DispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e) { //Debug.WriteLine("App Crashed"); @@ -746,16 +842,28 @@ private void CleanShutdown() { if (runShutdown) { + bool shutdownTimedOut = false; if (rootHub != null) { - Task.Run(() => + Task shutdownTask = Task.Run(() => { if (rootHub.running) { - rootHub.Stop(immediateUnplug: true); + rootHub.Stop(immediateUnplug: true, disposeViGEm: false); rootHub.ShutDown(); } - }).Wait(); + }); + + if (!shutdownTask.Wait(TimeSpan.FromSeconds(8))) + { + shutdownTimedOut = true; + try + { + logHolder?.Logger?.Warn("Timed out while stopping controller service during shutdown. Forcing process exit to avoid a stale single-instance lock."); + rootHub.PrepareAbort(); + } + catch { } + } } if (!skipSave) @@ -770,8 +878,11 @@ private void CleanShutdown() if (threadComEvent != null) { threadComEvent.Set(); // signal the other instance. - while (testThread.IsAlive) - Thread.SpinWait(500); + if (testThread != null && !testThread.Join(2000)) + { + shutdownTimedOut = true; + logHolder?.Logger?.Warn("Timed out waiting for single-instance worker thread to exit."); + } threadComEvent.Close(); } @@ -779,6 +890,11 @@ private void CleanShutdown() LogManager.Flush(); LogManager.Shutdown(); + + if (shutdownTimedOut) + { + Environment.Exit(0); + } } } } diff --git a/DS4Windows/AutoProfileChecker.cs b/DS4Windows/AutoProfileChecker.cs index 02fa51e..4d6454f 100644 --- a/DS4Windows/AutoProfileChecker.cs +++ b/DS4Windows/AutoProfileChecker.cs @@ -35,6 +35,8 @@ namespace DS4WinWPF [SuppressUnmanagedCodeSecurity] public class AutoProfileChecker { + private const uint PROCESS_QUERY_LIMITED_INFORMATION = 0x1000; + private AutoProfileHolder profileHolder; private IntPtr prevForegroundWnd = IntPtr.Zero; private uint prevForegroundProcessID; @@ -67,6 +69,12 @@ public void Process() if (GetTopWindowName(out topProcessName, out topWindowTitle)) { + if (Program.rootHub.IsAnyGameBarProfilePriorityActive() && + IsGameBarForegroundWindow(topProcessName, topWindowTitle)) + { + return; + } + // Find a profile match based on autoprofile program path and wnd title list. // The same program may set different profiles for each of the controllers, so we need an array of newProfileName[controllerIdx] values. for (int i = 0, pathsLen = profileHolder.AutoProfileColl.Count; i < pathsLen; i++) @@ -118,10 +126,14 @@ public void Process() continue; } - string tempname = controllerProfileEntity.ApplyToAllControllers ? - controllerProfileEntity.ProfileNames[0] : controllerProfileEntity.ProfileNames[j]; + string tempname = controllerProfileEntity.GetProfileNameForController(j); if (tempname != string.Empty && tempname != "(none)") { + if (Program.rootHub.TryDeferAutoProfileForGameBar(j, tempname)) + { + continue; + } + if ((Global.useTempProfile[j] && tempname != Global.tempprofilename[j]) || (!Global.useTempProfile[j] && tempname != Global.ProfilePath[j]) || forceLoadProfile) @@ -160,7 +172,7 @@ public void Process() } } - if (turnOffDS4WinApp) + if (turnOffDS4WinApp && !Program.rootHub.IsAnyGameBarProfilePriorityActive()) { turnOffTemp = true; if (App.rootHub.running) @@ -195,6 +207,11 @@ public void Process() { if (DS4Windows.Global.AutoProfileRevertDefaultProfile) { + if (Program.rootHub.TryDeferAutoProfileDefaultForGameBar(j)) + { + continue; + } + if (autoProfileDebugLogLevel > 0) DS4Windows.AppLogger.LogToGui($"DEBUG: Auto-Profile. Unknown process. Reverting to default profile. Controller {j + 1}={Global.ProfilePath[j]} (default)", false, true); @@ -250,6 +267,14 @@ private AutoProfileEntity SelectProfileEntityForController(List= 0 || + topProcessName.IndexOf("xboxgamingoverlay", StringComparison.OrdinalIgnoreCase) >= 0 || + topWndTitleName.IndexOf("game bar", StringComparison.OrdinalIgnoreCase) >= 0 || + topWndTitleName.IndexOf("xbox game bar", StringComparison.OrdinalIgnoreCase) >= 0; + } + private bool GetTopWindowName(out string topProcessName, out string topWndTitleName) { IntPtr hWnd = GetForegroundWindow(); @@ -288,7 +313,6 @@ private bool GetTopWindowName(out string topProcessName, out string topWndTitleN prevForegroundWnd = hWnd; - IntPtr hProcess = IntPtr.Zero; uint lpdwProcessId = 0; GetWindowThreadProcessId(hWnd, out lpdwProcessId); @@ -299,26 +323,59 @@ private bool GetTopWindowName(out string topProcessName, out string topWndTitleN else { prevForegroundProcessID = lpdwProcessId; - - hProcess = OpenProcess(0x0410, false, lpdwProcessId); - if (hProcess != IntPtr.Zero) GetModuleFileNameEx(hProcess, IntPtr.Zero, autoProfileCheckTextBuilder, autoProfileCheckTextBuilder.Capacity); - else autoProfileCheckTextBuilder.Clear(); - - prevForegroundProcessName = topProcessName = autoProfileCheckTextBuilder.Replace('/', '\\').ToString().ToLower(); + prevForegroundProcessName = topProcessName = GetProcessExecutablePath(lpdwProcessId) + .Replace('/', '\\') + .ToLower(); } GetWindowText(hWnd, autoProfileCheckTextBuilder, autoProfileCheckTextBuilder.Capacity); prevForegroundWndTitleName = topWndTitleName = autoProfileCheckTextBuilder.ToString().ToLower(); - if (hProcess != IntPtr.Zero) CloseHandle(hProcess); - if (autoProfileDebugLogLevel > 0) DS4Windows.AppLogger.LogToGui($"DEBUG: Auto-Profile. PID={lpdwProcessId} Path={topProcessName} | WND={hWnd} Title={topWndTitleName}", false, true); return true; } + private static string GetProcessExecutablePath(uint processId) + { + if (processId == 0) + { + return string.Empty; + } + + IntPtr hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, processId); + if (hProcess != IntPtr.Zero) + { + try + { + StringBuilder builder = new StringBuilder(1000); + int size = builder.Capacity; + if (QueryFullProcessImageName(hProcess, 0, builder, ref size) && size > 0) + { + return builder.ToString(); + } + } + finally + { + CloseHandle(hProcess); + } + } + + try + { + using (System.Diagnostics.Process process = System.Diagnostics.Process.GetProcessById((int)processId)) + { + return process.ProcessName + ".exe"; + } + } + catch + { + return string.Empty; + } + } + private static unsafe string GetWindowTitle(HWND handle) { var strLength = PInvoke.GetWindowTextLength(handle) + 1; @@ -390,8 +447,8 @@ private void DisplayProfileChange(int ind, string profile) [DllImport("kernel32.dll")] private static extern bool CloseHandle(IntPtr handle); - [DllImport("psapi.dll")] - private static extern uint GetModuleFileNameEx(IntPtr hWnd, IntPtr hModule, StringBuilder lpFileName, int nSize); + [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] + private static extern bool QueryFullProcessImageName(IntPtr hProcess, int dwFlags, StringBuilder lpExeName, ref int lpdwSize); [DllImport("user32.dll", CharSet = CharSet.Auto)] private static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nSize); diff --git a/DS4Windows/AutoProfileHolder.cs b/DS4Windows/AutoProfileHolder.cs index 5afc7cd..2951060 100644 --- a/DS4Windows/AutoProfileHolder.cs +++ b/DS4Windows/AutoProfileHolder.cs @@ -152,7 +152,15 @@ public class AutoProfileEntity public bool Turnoff { get => turnoff; set => turnoff = value; } public AutoProfileDeviceOption DeviceOption { get => deviceOption; set => deviceOption = value; } public bool ApplyToAllControllers { get => applyToAllControllers; set => applyToAllControllers = value; } - public string[] ProfileNames { get => profileNames; set => profileNames = value; } + public string[] ProfileNames + { + get + { + EnsureProfileNames(); + return profileNames; + } + set => profileNames = NormalizeProfileNames(value); + } public AutoProfileEntity(string pathStr, string titleStr) { @@ -197,6 +205,40 @@ public bool IsMatch(string searchPath, string searchTitle) return bPathMatched && bTitleMwatched; } + public string GetProfileNameForController(int controllerIndex) + { + EnsureProfileNames(); + int profileIndex = ApplyToAllControllers ? 0 : controllerIndex; + if (profileIndex < 0 || profileIndex >= profileNames.Length) + { + return NONE_STRING; + } + + return NormalizeProfileName(profileNames[profileIndex]); + } + + public void EnsureProfileNames() + { + profileNames = NormalizeProfileNames(profileNames); + } + + private static string[] NormalizeProfileNames(string[] source) + { + string[] result = new string[DS4Windows.Global.MAX_DS4_CONTROLLER_COUNT]; + for (int i = 0; i < result.Length; i++) + { + result[i] = i < (source?.Length ?? 0) ? + NormalizeProfileName(source[i]) : string.Empty; + } + + return result; + } + + private static string NormalizeProfileName(string profileName) + { + return profileName ?? string.Empty; + } + public bool IsDeviceMatch(DS4Windows.InputDevices.InputDeviceType deviceType) { switch (deviceOption) diff --git a/DS4Windows/DS4Control/ControlService.cs b/DS4Windows/DS4Control/ControlService.cs index 2d9c723..132b7f0 100644 --- a/DS4Windows/DS4Control/ControlService.cs +++ b/DS4Windows/DS4Control/ControlService.cs @@ -20,6 +20,7 @@ You should have received a copy of the GNU General Public License using DS4WinWPF.DS4Control; using Microsoft.Win32; using Nefarius.ViGEm.Client; +using NLog; using Sensorit.Base; using SharpOSC; using System; @@ -42,6 +43,10 @@ public class ControlService private readonly DualSenseAudioPassthrough dualSenseAudioPassthrough = new DualSenseAudioPassthrough(); private readonly DualSenseMicrophonePassthrough dualSenseMicrophonePassthrough = new DualSenseMicrophonePassthrough(); private readonly GameBarIntegration gameBarIntegration = new GameBarIntegration(); + private readonly object hidHideSessionLock = new object(); + private readonly HashSet hidHideSessionManagedInstanceIds = new HashSet(StringComparer.OrdinalIgnoreCase); + private readonly HashSet hidHidePersistentManagedInstanceIds = new HashSet(StringComparer.OrdinalIgnoreCase); + private bool? hidHideActiveStateBeforeManagedSession; // Might be useful for ScpVBus build public const int EXPANDED_CONTROLLER_COUNT = 8; public const int MAX_DS4_CONTROLLER_COUNT = Global.MAX_DS4_CONTROLLER_COUNT; @@ -68,6 +73,9 @@ public class ControlService bool[] buttonsdown = new bool[MAX_DS4_CONTROLLER_COUNT] { false, false, false, false, false, false, false, false }; bool[] held = new bool[MAX_DS4_CONTROLLER_COUNT]; int[] oldmouse = new int[MAX_DS4_CONTROLLER_COUNT] { -1, -1, -1, -1, -1, -1, -1, -1 }; + private int[] startupReportDiagCounts = new int[MAX_DS4_CONTROLLER_COUNT]; + private System.Threading.Timer gameBarProfileTimer; + private int gameBarProfileUpdateGate = 0; public OutputDevice[] outputDevices = new OutputDevice[MAX_DS4_CONTROLLER_COUNT] { null, null, null, null, null, null, null, null }; private OneEuroFilter3D[] udpEuroPairAccel = new OneEuroFilter3D[UdpServer.NUMBER_SLOTS] { @@ -80,7 +88,6 @@ public class ControlService new OneEuroFilter3D(), new OneEuroFilter3D(), }; Thread tempThread; - Thread tempBusThread; Thread eventDispatchThread; Dispatcher eventDispatcher; public bool suspending; @@ -92,11 +99,15 @@ public class ControlService private HashSet hidDeviceHidingExemptedDevs = new HashSet(); private bool hidDeviceHidingForced = false; private bool hidDeviceHidingEnabled = false; + private bool stickMouseFakerInputNoticeShown = false; + private bool stickMouseFakerInputMissingNoticeShown = false; + private readonly object outputKbmHandlerLock = new object(); private ControlServiceDeviceOptions deviceOptions; public ControlServiceDeviceOptions DeviceOptions { get => deviceOptions; } private DS4WinWPF.ArgumentParser cmdParser; + private static readonly Logger startupDiagLogger = LogManager.GetCurrentClassLogger(); public event EventHandler ServiceStarted; public event EventHandler PreServiceStop; @@ -461,35 +472,51 @@ private void CreateOSCCallback() public void RefreshOutputKBMHandler() { - if (Global.outputKBMHandler != null) + lock (outputKbmHandlerLock) { - Global.outputKBMHandler.Disconnect(); - Global.outputKBMHandler = null; - } + if (Global.outputKBMHandler != null) + { + Global.outputKBMHandler.Disconnect(); + Global.outputKBMHandler = null; + } - if (Global.outputKBMMapping != null) - { - Global.outputKBMMapping = null; - } + if (Global.outputKBMMapping != null) + { + Global.outputKBMMapping = null; + } - InitOutputKBMHandler(); + InitOutputKBMHandler(); + } } private void InitOutputKBMHandler() { string attemptVirtualkbmHandler = cmdParser.VirtualkbmHandler; + InitOutputKBMHandler(attemptVirtualkbmHandler); + } + + private void InitOutputKBMHandler(string attemptVirtualkbmHandler) + { + StartupDiag($"InitOutputKBMHandler begin requested={attemptVirtualkbmHandler}"); Global.InitOutputKBMHandler(attemptVirtualkbmHandler); + StartupDiag($"InitOutputKBMHandler created handler={Global.outputKBMHandler?.GetIdentifier()}"); bool handlerConnected = false; try { + StartupDiag($"OutputKBM.Connect begin handler={Global.outputKBMHandler?.GetIdentifier()}"); handlerConnected = Global.outputKBMHandler.Connect(); + StartupDiag($"OutputKBM.Connect end handler={Global.outputKBMHandler?.GetIdentifier()} connected={handlerConnected}"); + } + catch (Exception ex) + { + StartupDiag($"OutputKBM.Connect exception handler={Global.outputKBMHandler?.GetIdentifier()} {ex.GetType().Name}: {ex.Message}"); } - catch { } if (!handlerConnected && attemptVirtualkbmHandler != VirtualKBMFactory.GetFallbackHandlerIdentifier()) { + StartupDiag($"OutputKBM falling back to {VirtualKBMFactory.GetFallbackHandlerIdentifier()}"); Global.outputKBMHandler = VirtualKBMFactory.GetFallbackHandler(); } else @@ -504,6 +531,121 @@ private void InitOutputKBMHandler() Global.InitOutputKBMMapping(Global.outputKBMHandler.GetIdentifier()); Global.outputKBMMapping.PopulateConstants(); Global.outputKBMMapping.PopulateMappings(); + StartupDiag($"InitOutputKBMHandler end active={Global.outputKBMHandler?.GetFullDisplayName()} mapping={Global.outputKBMMapping?.GetType().Name}"); + } + + private bool SwitchOutputKBMHandler(string identifier) + { + lock (outputKbmHandlerLock) + { + if (Global.outputKBMHandler != null && + Global.outputKBMHandler.GetIdentifier() == identifier) + { + return true; + } + + VirtualKBMBase oldHandler = Global.outputKBMHandler; + VirtualKBMMapping oldMapping = Global.outputKBMMapping; + + try + { + InitOutputKBMHandler(identifier); + if (Global.outputKBMHandler?.GetIdentifier() == identifier) + { + RefreshLoadedActionAliases(); + oldHandler?.Disconnect(); + return true; + } + } + catch { } + + Global.outputKBMHandler?.Disconnect(); + Global.outputKBMHandler = oldHandler; + Global.outputKBMMapping = oldMapping; + return false; + } + } + + private void EnsureVirtualMouseForStickMouseProfile(int ind) + { + if (!ProfileUsesStickMouse(ind)) + { + return; + } + + if (Global.outputKBMHandler?.GetIdentifier() == FakerInputHandler.IDENTIFIER) + { + return; + } + + Global.RefreshFakerInputInfo(); + if (Global.fakerInputInstalled) + { + bool switched = SwitchOutputKBMHandler(FakerInputHandler.IDENTIFIER); + if (switched && !stickMouseFakerInputNoticeShown) + { + stickMouseFakerInputNoticeShown = true; + LogDebug("Stick mouse profile detected. Using FakerInput virtual mouse so Windows keeps a real pointer device available."); + } + else if (!switched && !stickMouseFakerInputMissingNoticeShown) + { + stickMouseFakerInputMissingNoticeShown = true; + LogDebug("Stick mouse profile detected, but DS4Windows could not connect to FakerInput. SendInput will remain active."); + } + + return; + } + + if (!stickMouseFakerInputMissingNoticeShown) + { + stickMouseFakerInputMissingNoticeShown = true; + string helpURL = "https://github.com/Ryochan7/FakerInput/"; + LogDebug($"Stick mouse profile detected, but FakerInput is not installed. Install FakerInput to expose a persistent virtual mouse and avoid hidden cursor behavior on couch/TV setups: {helpURL}"); + AppLogger.LogToTray("Stick mouse works best with FakerInput installed for a persistent virtual mouse."); + } + } + + private static bool ProfileUsesStickMouse(int ind) + { + return StickDirectionMapsToMouse(ind, DS4Controls.LXNeg) || + StickDirectionMapsToMouse(ind, DS4Controls.LXPos) || + StickDirectionMapsToMouse(ind, DS4Controls.LYNeg) || + StickDirectionMapsToMouse(ind, DS4Controls.LYPos) || + StickDirectionMapsToMouse(ind, DS4Controls.RXNeg) || + StickDirectionMapsToMouse(ind, DS4Controls.RXPos) || + StickDirectionMapsToMouse(ind, DS4Controls.RYNeg) || + StickDirectionMapsToMouse(ind, DS4Controls.RYPos); + } + + private static bool StickDirectionMapsToMouse(int ind, DS4Controls control) + { + DS4ControlSettings setting = GetDS4CSetting(ind, control); + return ActionMapsToMouse(setting.actionType, setting.action.actionBtn) || + ActionMapsToMouse(setting.shiftActionType, setting.shiftAction.actionBtn); + } + + private static bool ActionMapsToMouse(DS4ControlSettings.ActionType actionType, X360Controls outputControl) + { + if (actionType != DS4ControlSettings.ActionType.Button) + { + return false; + } + + return outputControl >= X360Controls.MouseUp && + outputControl <= X360Controls.AbsMouseRight; + } + + private static void RefreshLoadedActionAliases() + { + for (int device = 0; device < Global.MAX_DS4_CONTROLLER_COUNT; device++) + { + foreach (DS4Controls control in Enum.GetValues(typeof(DS4Controls))) + { + DS4ControlSettings setting = GetDS4CSetting(device, control); + Global.RefreshActionAlias(setting, false); + Global.RefreshActionAlias(setting, true); + } + } } private void OutputslotMan_ViGEmFailure(object sender, int errorCode) @@ -609,6 +751,7 @@ public void PrepareDS4DeviceInit(DS4Device device) public void ShutDown() { + ReleaseHidHideManagedDevices(); outputslotMan.ShutDown(); OutputSlotPersist.WriteConfig(outputslotMan); @@ -730,7 +873,7 @@ public void UpdateHidHideAttributes() hidDeviceHidingForced = false; // No known equivalent in HidHide hidDeviceHidingEnabled = false; - using (HidHideAPIDevice hidHideDevice = new HidHideAPIDevice()) + using (HidHideAPIDevice hidHideDevice = new HidHideAPIDevice(writeAccess: false)) { if (!hidHideDevice.IsOpen()) { @@ -817,6 +960,152 @@ private void ChangeExclusiveStatus(DS4Device dev) if (Global.hidHideInstalled) { dev.CurrentExclusiveStatus = DS4Device.ExclusiveStatus.HidHideAffected; + TryAddDeviceToSessionBlacklist(dev); + } + } + + /// + /// Adds the device to HidHide for this DS4Windows run and enables hiding. + /// Session entries stay active across Stop/Start inside this process and + /// are automatically removed by HidHide when DS4Windows exits. If session + /// blacklist writes fail, fall back to a normal blacklist entry and remove + /// only entries DS4Windows added on Stop/Shutdown. + /// + private bool TryAddDeviceToSessionBlacklist(DS4Device dev) + { + if (!Global.hidHideInstalled || dev == null) return false; + + string instanceId = Global.GetInstanceIdFromDevicePath(dev.HidDevice.DevicePath); + if (string.IsNullOrEmpty(instanceId)) return false; + + bool alreadyManaged; + lock (hidHideSessionLock) + { + alreadyManaged = hidHideSessionManagedInstanceIds.Contains(instanceId) || + hidHidePersistentManagedInstanceIds.Contains(instanceId); + } + + try + { + using (HidHideAPIDevice hidHideDevice = new HidHideAPIDevice()) + { + if (!hidHideDevice.IsOpen()) return false; + + bool active = hidHideDevice.GetActiveState(); + lock (hidHideSessionLock) + { + hidHideActiveStateBeforeManagedSession ??= active; + } + + if (!active) + { + hidHideDevice.SetActiveState(true); + } + + if (!alreadyManaged && hidHideDevice.AddSessionBlacklist(new List { instanceId })) + { + lock (hidHideSessionLock) + { + hidHideSessionManagedInstanceIds.Add(instanceId); + } + + LogDebug($"HidHide session hiding enabled for {dev.DisplayName} ({instanceId})", false); + } + else if (!alreadyManaged && !EnsurePersistentHidHideBlacklist(hidHideDevice, instanceId, dev)) + { + return false; + } + + UpdateHidHideAttributes(); + return true; + } + } + catch (Exception ex) + { + LogDebug($"HidHide session setup failed for {dev.DisplayName}: {ex.Message}", true); + return false; + } + } + + private bool EnsurePersistentHidHideBlacklist(HidHideAPIDevice hidHideDevice, string instanceId, DS4Device dev) + { + List instances = hidHideDevice.GetBlacklist() + .Where(item => !string.IsNullOrWhiteSpace(item)) + .ToList(); + + if (instances.Any(item => string.Equals(item, instanceId, StringComparison.OrdinalIgnoreCase))) + { + StartupDiag($"HidHide persistent blacklist already contains {instanceId}"); + return true; + } + + instances.Add(instanceId); + if (!hidHideDevice.SetBlacklist(instances)) + { + StartupDiag($"HidHide persistent blacklist fallback failed for {dev.DisplayName} ({instanceId})"); + return false; + } + + lock (hidHideSessionLock) + { + hidHidePersistentManagedInstanceIds.Add(instanceId); + } + + LogDebug($"HidHide persistent hiding enabled for {dev.DisplayName} ({instanceId})", false); + return true; + } + + private void ReleaseHidHideManagedDevices() + { + if (!Global.hidHideInstalled) return; + + List persistentIds; + bool? restoreActiveState; + lock (hidHideSessionLock) + { + persistentIds = hidHidePersistentManagedInstanceIds.ToList(); + restoreActiveState = hidHideActiveStateBeforeManagedSession; + hidHideSessionManagedInstanceIds.Clear(); + hidHidePersistentManagedInstanceIds.Clear(); + hidHideActiveStateBeforeManagedSession = null; + } + + if (persistentIds.Count == 0 && restoreActiveState is null) return; + + try + { + using (HidHideAPIDevice hidHideDevice = new HidHideAPIDevice()) + { + if (!hidHideDevice.IsOpen()) return; + + hidHideDevice.ClearSessionBlacklist(); + + if (persistentIds.Count > 0) + { + List instances = hidHideDevice.GetBlacklist() + .Where(item => !string.IsNullOrWhiteSpace(item)) + .ToList(); + + int removed = instances.RemoveAll(item => + persistentIds.Any(managed => string.Equals(managed, item, StringComparison.OrdinalIgnoreCase))); + + if (removed > 0 && hidHideDevice.SetBlacklist(instances)) + { + StartupDiag($"Released {removed} DS4Windows-managed HidHide blacklist entries"); + } + } + + if (restoreActiveState == false) + { + hidHideDevice.SetActiveState(false); + } + + UpdateHidHideAttributes(); + } + } + catch (Exception ex) + { + StartupDiag($"ReleaseHidHideManagedDevices exception {ex.GetType().Name}: {ex.Message}"); } } @@ -1010,17 +1299,24 @@ private void WarnExclusiveModeFailure(DS4Device device) private void StartViGEm() { + StartupDiag("StartViGEm begin"); // Refresh internal ViGEmBus info Global.RefreshViGEmBusInfo(); + StartupDiag($"StartViGEm info installed={Global.vigemInstalled} version={Global.vigembusVersion} supported={Global.IsRunningSupportedViGEmBus()}"); if (Global.IsRunningSupportedViGEmBus()) { tempThread = new Thread(() => { try { + StartupDiag("ViGEmClient ctor begin"); vigemTestClient = new ViGEmClient(); + StartupDiag("ViGEmClient ctor end success"); + } + catch (Exception ex) + { + StartupDiag($"ViGEmClient ctor exception {ex.GetType().Name}: {ex.Message}"); } - catch { } }); tempThread.Priority = ThreadPriority.AboveNormal; tempThread.IsBackground = true; @@ -1032,13 +1328,16 @@ private void StartViGEm() } tempThread = null; + StartupDiag($"StartViGEm end clientCreated={vigemTestClient != null}"); } private void StopViGEm() { if (vigemTestClient != null) { + StartupDiag("StopViGEm Dispose begin"); vigemTestClient.Dispose(); + StartupDiag("StopViGEm Dispose end"); vigemTestClient = null; } } @@ -1082,8 +1381,10 @@ public void AssignInitialDevices() private OutputDevice EstablishOutDevice(int index, OutContType contType) { + StartupDiag($"EstablishOutDevice begin index={index} contType={contType} client={vigemTestClient != null}"); OutputDevice temp = null; temp = outputslotMan.AllocateController(contType, vigemTestClient); + StartupDiag($"EstablishOutDevice end index={index} contType={contType} result={temp?.GetType().Name ?? "null"}"); return temp; } @@ -1370,11 +1671,13 @@ public void DetachUnboundOutDev(OutSlotDevice slotDevice) public void PluginOutDev(int index, DS4Device device) { OutContType contType = Global.OutContType[index]; + StartupDiag($"PluginOutDev enter index={index} contType={contType} useDInputOnly={useDInputOnly[index]} profileDInputOnly={getDInputOnly(index)}"); OutSlotDevice slotDevice = null; if (!getDInputOnly(index)) { slotDevice = outputslotMan.FindExistUnboundSlotType(contType); + StartupDiag($"PluginOutDev existingSlot index={index} found={slotDevice != null} slot={(slotDevice != null ? slotDevice.Index + 1 : 0)}"); } if (useDInputOnly[index]) @@ -1387,10 +1690,12 @@ public void PluginOutDev(int index, DS4Device device) if (slotDevice == null) { slotDevice = outputslotMan.FindOpenSlot(); + StartupDiag($"PluginOutDev X360 openSlot index={index} found={slotDevice != null} slot={(slotDevice != null ? slotDevice.Index + 1 : 0)}"); if (slotDevice != null) { Xbox360OutDevice tempXbox = EstablishOutDevice(index, OutContType.X360) as Xbox360OutDevice; + StartupDiag($"PluginOutDev X360 established index={index} null={tempXbox == null}"); //outputDevices[index] = tempXbox; // Enable ViGem feedback callback handler only if lightbar/rumble data output is enabled (if those are disabled then no point enabling ViGem callback handler call) @@ -1408,7 +1713,9 @@ public void PluginOutDev(int index, DS4Device device) } } + StartupDiag($"PluginOutDev X360 DeferredPlugin begin index={index}"); outputslotMan.DeferredPlugin(tempXbox, index, $"{device.DisplayName} [{device.MacAddress}]", outputDevices, contType); + StartupDiag($"PluginOutDev X360 DeferredPlugin end index={index}"); //slotDevice.CurrentInputBound = OutSlotDevice.InputBound.Bound; success = true; @@ -1420,6 +1727,7 @@ public void PluginOutDev(int index, DS4Device device) } else { + StartupDiag($"PluginOutDev X360 reusing slot index={index} slot={slotDevice.Index + 1}"); slotDevice.CurrentInputBound = OutSlotDevice.InputBound.Bound; Xbox360OutDevice tempXbox = slotDevice.OutputDevice as Xbox360OutDevice; @@ -1452,10 +1760,12 @@ public void PluginOutDev(int index, DS4Device device) if (slotDevice == null) { slotDevice = outputslotMan.FindOpenSlot(); + StartupDiag($"PluginOutDev DS4 openSlot index={index} found={slotDevice != null} slot={(slotDevice != null ? slotDevice.Index + 1 : 0)}"); if (slotDevice != null) { DS4OutDevice tempDS4 = EstablishOutDevice(index, OutContType.DS4) as DS4OutDevice; + StartupDiag($"PluginOutDev DS4 established index={index} null={tempDS4 == null}"); // Enable ViGem feedback callback handler only if DS4 lightbar/rumble data output is enabled (if those are disabled then no point enabling ViGem callback handler call) if (Global.EnableOutputDataToDS4[index]) @@ -1472,7 +1782,9 @@ public void PluginOutDev(int index, DS4Device device) } } + StartupDiag($"PluginOutDev DS4 DeferredPlugin begin index={index}"); outputslotMan.DeferredPlugin(tempDS4, index, $"{device.DisplayName} [{device.MacAddress}]", outputDevices, contType); + StartupDiag($"PluginOutDev DS4 DeferredPlugin end index={index}"); //slotDevice.CurrentInputBound = OutSlotDevice.InputBound.Bound; success = true; @@ -1484,6 +1796,7 @@ public void PluginOutDev(int index, DS4Device device) } else { + StartupDiag($"PluginOutDev DS4 reusing slot index={index} slot={slotDevice.Index + 1}"); slotDevice.CurrentInputBound = OutSlotDevice.InputBound.Bound; DS4OutDevice tempDS4 = slotDevice.OutputDevice as DS4OutDevice; @@ -1521,8 +1834,18 @@ public void PluginOutDev(int index, DS4Device device) { LogDebug($"Associated input controller #{index + 1} ({device.DisplayName}) to virtual {slotDevice.OutputDevice.GetDeviceType()} Controller in{(slotDevice.PermanentType != OutContType.None ? " permanent" : "")} output slot #{slotDevice.Index + 1}"); useDInputOnly[index] = false; + StartupDiag($"PluginOutDev success index={index} slot={slotDevice.Index + 1} output={slotDevice.OutputDevice.GetDeviceType()}"); + } + else + { + LogDebug("Failed. No output device was associated"); + StartupDiag($"PluginOutDev failed index={index} success={success} slotNull={slotDevice == null} slotOutputNull={slotDevice?.OutputDevice == null}"); } } + else + { + StartupDiag($"PluginOutDev skipped index={index} useDInputOnly=false"); + } } public void UnplugOutDev(int index, DS4Device device, bool immediate = false, bool force = false) @@ -1563,13 +1886,18 @@ public void UnplugOutDev(int index, DS4Device device, bool immediate = false, bo public bool Start(bool showlog = true) { + StartupDiag($"ControlService.Start enter showlog={showlog} running={running} inServiceTask={inServiceTask} admin={Global.IsAdministrator()}"); inServiceTask = true; + StartupDiag("ControlService.Start before StartViGEm"); StartViGEm(); + StartupDiag($"ControlService.Start after StartViGEm client={vigemTestClient != null}"); if (vigemTestClient != null) //if (x360Bus.Open() && x360Bus.Start()) { // Initialize output KBM handler at start of ControlService + StartupDiag("ControlService.Start before InitOutputKBMHandler"); InitOutputKBMHandler(); + StartupDiag($"ControlService.Start after InitOutputKBMHandler handler={Global.outputKBMHandler?.GetFullDisplayName()}"); if (showlog) LogDebug(DS4WinWPF.Properties.Resources.Starting); @@ -1588,7 +1916,20 @@ public bool Start(bool showlog = true) DS4Devices.isExclusiveMode = getUseExclusiveMode(); //Re-enable Exclusive Mode + StartupDiag($"UpdateHidHiddenAttributes begin exclusive={DS4Devices.isExclusiveMode}"); UpdateHidHiddenAttributes(); + StartupDiag("UpdateHidHiddenAttributes end"); + + if (Global.openRGBSyncEnabled) + { + StartupDiag($"OpenRGB start begin port={Global.openRGBServerPort}"); + bool openRGBStarted = OpenRGBServer.Instance.Start(Global.openRGBServerPort); + StartupDiag($"OpenRGB start end started={openRGBStarted}"); + if (showlog) + LogDebug(openRGBStarted + ? $"OpenRGB server listening on port {Global.openRGBServerPort}" + : $"OpenRGB server could not bind to port {Global.openRGBServerPort} - lightbar will use profile colour"); + } if (showlog) { @@ -1598,35 +1939,46 @@ public bool Start(bool showlog = true) if (isUsingOSCServer() && oscListener == null) { + StartupDiag("OSC listener start begin"); ChangeOSCListenerStatus(true); + StartupDiag("OSC listener start requested"); } if (isUsingOSCSender() && oscSender == null) { + StartupDiag("OSC sender start begin"); ChangeOSCSenderStatus(true); + StartupDiag("OSC sender start requested"); } if (isUsingUDPServer() && _udpServer == null) { + StartupDiag("UDP change-status start begin"); ChangeUDPStatus(true, false); while (udpChangeStatus == true) { Thread.SpinWait(500); } + StartupDiag("UDP change-status start end"); } try { loopControllers = true; + StartupDiag("AssignInitialDevices begin"); AssignInitialDevices(); + StartupDiag("AssignInitialDevices end"); + StartupDiag("DS4Devices.findControllers dispatch begin"); eventDispatcher.Invoke(() => { DS4Devices.findControllers(); }); + StartupDiag("DS4Devices.findControllers dispatch end"); IEnumerable devices = DS4Devices.getDS4Controllers(); int numControllers = devices.Count(); + StartupDiag($"DS4Devices.getDS4Controllers count={numControllers}"); activeControllers = numControllers; DS4LightBar.defaultLight = false; int i = 0; @@ -1635,8 +1987,11 @@ public bool Start(bool showlog = true) devEnum.MoveNext() && loopControllers; i++) { DS4Device device = devEnum.Current; + StartupDiag($"Prepare controller loop index={i} type={device.DeviceType} display={device.DisplayName} mac={device.MacAddress} conn={device.ConnectionType} synced={device.isSynced()} primary={device.PrimaryDevice}"); + StartupDiag($"BeginPrepareConnectedInputController begin index={i}"); BeginPrepareConnectedInputController(device, showlog: true); + StartupDiag($"BeginPrepareConnectedInputController end index={i}"); if (deviceOptions.JoyConDeviceOpts.LinkedMode == JoyConDeviceOptions.LinkMode.Joined) { @@ -1672,7 +2027,9 @@ public bool Start(bool showlog = true) DS4Controllers[i] = device; device.DeviceSlotNumber = i; + StartupDiag($"PrepareConnectedInputControllerSettingEvents begin index={i}"); PrepareConnectedInputControllerSettingEvents(numControllers, device, index: i); + StartupDiag($"PrepareConnectedInputControllerSettingEvents end index={i}"); if (i >= CURRENT_DS4_CONTROLLER_LIMIT) // out of Xinput devices! break; @@ -1680,11 +2037,14 @@ public bool Start(bool showlog = true) } catch (Exception e) { + StartupDiag($"ControlService.Start managed exception {e.GetType().Name}: {e.Message}"); LogDebug(e.Message, true); AppLogger.LogToTray(e.Message, true); } + StartupDiag("ControlService.Start setting running=true"); running = true; + StartGameBarProfileTimer(); if (_udpServer != null) { @@ -1694,11 +2054,14 @@ public bool Start(bool showlog = true) try { + StartupDiag($"UDP server Start begin address={UDP_SERVER_LISTEN_ADDRESS} port={UDP_SERVER_PORT}"); _udpServer.Start(UDP_SERVER_PORT, UDP_SERVER_LISTEN_ADDRESS); LogDebug($"UDP server listening on address {UDP_SERVER_LISTEN_ADDRESS} port {UDP_SERVER_PORT}"); + StartupDiag("UDP server Start end"); } catch (System.Net.Sockets.SocketException ex) { + StartupDiag($"UDP server Start exception {ex.SocketErrorCode}: {ex.Message}"); var errMsg = string.Format("Couldn't start UDP server on address {0}:{1}, outside applications won't be able to access pad data ({2})", UDP_SERVER_LISTEN_ADDRESS, UDP_SERVER_PORT, ex.SocketErrorCode); LogDebug(errMsg, true); @@ -1708,6 +2071,7 @@ public bool Start(bool showlog = true) } else { + StartupDiag("ControlService.Start no ViGEm client"); string logMessage = string.Empty; if (!vigemInstalled) { @@ -1728,10 +2092,13 @@ public bool Start(bool showlog = true) inServiceTask = false; runHotPlug = true; + StartupDiag("ControlService.Start before ServiceStarted events"); ServiceStarted?.Invoke(this, EventArgs.Empty); RunningChanged?.Invoke(this, EventArgs.Empty); + StartupDiag("ControlService.Start after RunningChanged"); using var process = Process.GetCurrentProcess(); process.PriorityClass = MainWindow.ProcessPriorityClasses[Global.ProcessPriority]; + StartupDiag($"ControlService.Start exit priority={process.PriorityClass}"); return true; } @@ -1796,14 +2163,25 @@ public void PrepareAbort() } } - public bool Stop(bool showlog = true, bool immediateUnplug = false) + public bool Stop(bool showlog = true, bool immediateUnplug = false, bool disposeViGEm = true) { + StartupDiag($"ControlService.Stop enter showlog={showlog} immediate={immediateUnplug} disposeViGEm={disposeViGEm} running={running}"); if (running) { + if (OpenRGBServer.Instance.IsRunning) + { + StartupDiag("ControlService.Stop OpenRGB stop begin"); + OpenRGBServer.Instance.Stop(); + StartupDiag("ControlService.Stop OpenRGB stop end"); + } + running = false; runHotPlug = false; inServiceTask = true; + StopGameBarProfileTimer(); + StartupDiag("ControlService.Stop PreServiceStop begin"); PreServiceStop?.Invoke(this, EventArgs.Empty); + StartupDiag("ControlService.Stop PreServiceStop end"); if (showlog) LogDebug(DS4WinWPF.Properties.Resources.StoppingX360); @@ -1816,6 +2194,7 @@ public bool Stop(bool showlog = true, bool immediateUnplug = false) DS4Device tempDevice = DS4Controllers[i]; if (tempDevice != null) { + StartupDiag($"ControlService.Stop controller loop index={i} display={tempDevice.DisplayName} mac={tempDevice.MacAddress} conn={tempDevice.ConnectionType} charging={tempDevice.isCharging()}"); if ((DCBTatStop && !tempDevice.isCharging()) || suspending) { if (tempDevice.getConnectionType() == ConnectionType.BT) @@ -1836,10 +2215,14 @@ public bool Stop(bool showlog = true, bool immediateUnplug = false) } else { - DS4LightBar.forcelight[i] = false; - DS4LightBar.forcedFlash[i] = 0; - DS4LightBar.defaultLight = true; - DS4LightBar.updateLightBar(DS4Controllers[i], i); + if (!immediateUnplug) + { + DS4LightBar.forcelight[i] = false; + DS4LightBar.forcedFlash[i] = 0; + DS4LightBar.defaultLight = true; + DS4LightBar.updateLightBar(DS4Controllers[i], i); + } + tempDevice.IsRemoved = true; tempDevice.StopUpdate(); DS4Devices.RemoveDevice(tempDevice); @@ -1850,7 +2233,9 @@ public bool Stop(bool showlog = true, bool immediateUnplug = false) OutputDevice tempout = outputDevices[i]; if (tempout != null) { + StartupDiag($"ControlService.Stop UnplugOutDev begin index={i} type={tempout.GetDeviceType()}"); UnplugOutDev(i, tempDevice, immediate: immediateUnplug, force: true); + StartupDiag($"ControlService.Stop UnplugOutDev end index={i}"); anyUnplugged = true; } @@ -1869,9 +2254,15 @@ public bool Stop(bool showlog = true, bool immediateUnplug = false) if (showlog) LogDebug(DS4WinWPF.Properties.Resources.StoppingDS4); + StartupDiag("ControlService.Stop DualSenseAudio dispose begin"); dualSenseAudioPassthrough.Dispose(); + StartupDiag("ControlService.Stop DualSenseAudio dispose end"); + StartupDiag("ControlService.Stop DualSenseMicrophone dispose begin"); dualSenseMicrophonePassthrough.Dispose(); + StartupDiag("ControlService.Stop DualSenseMicrophone dispose end"); + StartupDiag("ControlService.Stop DS4Devices.stopControllers begin"); DS4Devices.stopControllers(); + StartupDiag("ControlService.Stop DS4Devices.stopControllers end"); slotManager.ClearControllerList(); if (oscListener != null) @@ -1886,35 +2277,59 @@ public bool Stop(bool showlog = true, bool immediateUnplug = false) if (_udpServer != null) { + StartupDiag("ControlService.Stop UDP stop begin"); ChangeUDPStatus(false); + StartupDiag("ControlService.Stop UDP stop requested"); } if (showlog) LogDebug(DS4WinWPF.Properties.Resources.StoppedDS4Windows); - while (outputslotMan.RunningQueue) + Stopwatch outputQueueWait = Stopwatch.StartNew(); + while (outputslotMan.RunningQueue && outputQueueWait.ElapsedMilliseconds < 2000) { - Thread.SpinWait(500); + Thread.Sleep(1); + } + + if (outputslotMan.RunningQueue) + { + StartupDiag("ControlService.Stop timed out waiting for output slot queue"); } + + StartupDiag("ControlService.Stop outputslotMan.Stop begin"); outputslotMan.Stop(true); + StartupDiag("ControlService.Stop outputslotMan.Stop end"); if (anyUnplugged) { Thread.Sleep(OutputSlotManager.DELAY_TIME); } - StopViGEm(); + if (disposeViGEm) + { + StopViGEm(); + } + else + { + StartupDiag("ControlService.Stop skipping ViGEm Dispose during app exit"); + vigemTestClient = null; + } // Disconnect from KBM system when stopping ControlService + StartupDiag($"ControlService.Stop outputKBM Disconnect begin handler={outputKBMHandler?.GetFullDisplayName()}"); LogDebug($"Closing connection to output handler {outputKBMHandler.GetDisplayName()}"); outputKBMHandler.Disconnect(); + StartupDiag("ControlService.Stop outputKBM Disconnect end"); inServiceTask = false; activeControllers = 0; } runHotPlug = false; + ReleaseHidHideManagedDevices(); + StartupDiag("ControlService.Stop before stopped events"); ServiceStopped?.Invoke(this, EventArgs.Empty); RunningChanged?.Invoke(this, EventArgs.Empty); + StartupDiag("ControlService.Stop exit"); return true; } @@ -2050,10 +2465,19 @@ bool checkAlreadyExists() private void PrepareConnectedInputControllerSettingEvents(int numControllers, DS4Device device, int index) { + StartupDiag($"Controller prep begin index={index} numControllers={numControllers} display={device.DisplayName} mac={device.MacAddress} type={device.DeviceType}"); + StartupDiag($"RefreshExtrasButtons begin index={index}"); Global.RefreshExtrasButtons(index, GetKnownExtraButtons(device)); + StartupDiag($"RefreshExtrasButtons end index={index}"); + StartupDiag($"LoadControllerConfigs begin index={index}"); Global.LoadControllerConfigs(device); + StartupDiag($"LoadControllerConfigs end index={index}"); + StartupDiag($"device.LoadStoreSettings begin index={index}"); device.LoadStoreSettings(); + StartupDiag($"device.LoadStoreSettings end index={index}"); + StartupDiag($"CheckControllerNumDeviceSettings begin index={index}"); device.CheckControllerNumDeviceSettings(numControllers); + StartupDiag($"CheckControllerNumDeviceSettings end index={index}"); slotManager.AddController(device, index); if (isUsingOSCSender()) @@ -2067,7 +2491,9 @@ private void PrepareConnectedInputControllerSettingEvents(int numControllers, DS device.SerialChange += this.On_SerialChange; device.ChargingChanged += CheckQuickCharge; + StartupDiag($"TouchPad create begin index={index}"); touchPad[index] = new Mouse(index, device); + StartupDiag($"TouchPad create end index={index}"); bool profileLoaded = false; bool useAutoProfile = useTempProfile[index]; if (!useAutoProfile) @@ -2084,7 +2510,13 @@ private void PrepareConnectedInputControllerSettingEvents(int numControllers, DS } // Now attempt to load requested profile and settings + StartupDiag($"LoadProfile begin index={index} profile=\"{ProfilePath[index]}\" linked={Global.linkedProfileCheck[index]}"); profileLoaded = LoadProfile(index, false, this, false, false); + StartupDiag($"LoadProfile end index={index} loaded={profileLoaded} profile=\"{ProfilePath[index]}\" dinputOnly={getDInputOnly(index)} outType={Global.OutContType[index]}"); + } + else + { + StartupDiag($"LoadProfile skipped for auto/temp profile index={index} tempProfile=\"{tempprofilename[index]}\""); } if (profileLoaded || useAutoProfile) @@ -2095,7 +2527,9 @@ private void PrepareConnectedInputControllerSettingEvents(int numControllers, DS { if (device.PrimaryDevice) { + StartupDiag($"PluginOutDev begin index={index} outType={Global.OutContType[index]}"); PluginOutDev(index, device); + StartupDiag($"PluginOutDev end index={index} useDInputOnly={useDInputOnly[index]} activeOut={activeOutDevType[index]} outDev={outputDevices[index]?.GetDeviceType() ?? "null"}"); } else if (device.JointDeviceSlotNumber != DS4Device.DEFAULT_JOINT_SLOT_NUMBER) { @@ -2118,7 +2552,9 @@ private void PrepareConnectedInputControllerSettingEvents(int numControllers, DS if (device.PrimaryDevice && device.OutputMapGyro) { + StartupDiag($"TouchPadOn begin index={index}"); TouchPadOn(index, device); + StartupDiag($"TouchPadOn end index={index}"); } else if (device.JointDeviceSlotNumber != DS4Device.DEFAULT_JOINT_SLOT_NUMBER) { @@ -2135,8 +2571,16 @@ private void PrepareConnectedInputControllerSettingEvents(int numControllers, DS } } + StartupDiag($"CheckProfileOptions begin index={index}"); CheckProfileOptions(index, device); + StartupDiag($"CheckProfileOptions end index={index}"); + StartupDiag($"SetupInitialHookEvents begin index={index}"); SetupInitialHookEvents(index, device); + StartupDiag($"SetupInitialHookEvents end index={index}"); + } + else + { + StartupDiag($"Controller prep profile not loaded index={index} profile=\"{ProfilePath[index]}\""); } int tempIdx = index; @@ -2144,13 +2588,19 @@ private void PrepareConnectedInputControllerSettingEvents(int numControllers, DS { this.On_Report(sender, e, tempIdx); }; + StartupDiag($"Report hook added index={index}"); if (_udpServer != null && index < UdpServer.NUMBER_SLOTS) { + StartupDiag($"PrepareDevUDPMotion begin index={index}"); PrepareDevUDPMotion(device, tempIdx); + StartupDiag($"PrepareDevUDPMotion end index={index}"); } + StartupDiag($"device.StartUpdate begin index={index}"); device.StartUpdate(); + StartupDiag($"device.StartUpdate end index={index}"); + StartupDiag($"Controller prep end index={index}"); } private void BeginPrepareConnectedInputController(DS4Device device, bool showlog = false) @@ -2196,6 +2646,8 @@ private void ChangeUdpSmoothingAttrs(object sender, EventArgs e) public void CheckProfileOptions(int ind, DS4Device device, bool startUp = false) { + EnsureVirtualMouseForStickMouseProfile(ind); + device.ModifyFeatureSetFlag(VidPidFeatureSet.NoOutputData, !getEnableOutputDataToDS4(ind)); if (!getEnableOutputDataToDS4(ind)) LogDebug("Output data to DS4 disabled. Lightbar and rumble events are not written to DS4 gamepad. If the gamepad is connected over BT then IdleDisconnect option is recommended to let DS4Windows to close the connection after long period of idling."); @@ -2648,10 +3100,116 @@ protected void On_DS4Removal(object sender, EventArgs e) private DateTime gameBarLastVisibleUtc = DateTime.MinValue; private DateTime gameBarInvisibleSinceUtc = DateTime.MinValue; private DateTime gameBarLastVisibilityCheckUtc = DateTime.MinValue; - private DateTime gameBarLastDiagnosticLogUtc = DateTime.MinValue; - private bool gameBarLastDiagnosticVisible = false; - private bool gameBarHasDiagnosticVisibleState = false; - private bool gameBarAdminWarningLogged = false; + private bool gameBarVerboseDetectionLogInitialized = false; + private bool gameBarVerboseLastVisible = false; + private DateTime gameBarVerboseLastDetectionLogUtc = DateTime.MinValue; + + public bool IsGameBarProfilePriorityActive(int ind) + { + return ind >= 0 && ind < MAX_DS4_CONTROLLER_COUNT && + (gameBarProfileActive[ind] || gameBarProfilePending[ind]); + } + + public bool IsAnyGameBarProfilePriorityActive() + { + for (int i = 0; i < MAX_DS4_CONTROLLER_COUNT; i++) + { + if (IsGameBarProfilePriorityActive(i)) + { + return true; + } + } + + return false; + } + + public bool TryDeferAutoProfileForGameBar(int ind, string profileName) + { + if (!IsGameBarProfilePriorityActive(ind)) + { + return false; + } + + gameBarPreviousUseTempProfile[ind] = true; + gameBarPreviousTempProfileName[ind] = profileName; + if (!string.IsNullOrEmpty(Global.ProfilePath[ind])) + { + gameBarPreviousProfileName[ind] = Global.ProfilePath[ind]; + } + + return true; + } + + public bool TryDeferAutoProfileDefaultForGameBar(int ind) + { + if (!IsGameBarProfilePriorityActive(ind)) + { + return false; + } + + gameBarPreviousUseTempProfile[ind] = false; + gameBarPreviousTempProfileName[ind] = string.Empty; + if (!string.IsNullOrEmpty(Global.ProfilePath[ind])) + { + gameBarPreviousProfileName[ind] = Global.ProfilePath[ind]; + } + + return true; + } + + private bool HasAnyConfiguredGameBarProfile() + { + for (int i = 0; i < MAX_DS4_CONTROLLER_COUNT; i++) + { + if (DS4Controllers[i] != null && + Global.GameBarHomeButtonSupport[i] && + !string.IsNullOrEmpty(Global.GameBarProfileName[i])) + { + return true; + } + } + + return false; + } + + private bool TryGetConfiguredGameBarProfileName(int ind, out string profileName) + { + profileName = string.Empty; + if (!Global.GameBarHomeButtonSupport[ind]) + { + return false; + } + + profileName = Global.GameBarProfileName[ind]; + if (string.IsNullOrEmpty(profileName)) + { + return false; + } + + string profilePath = Path.Combine(appdatapath, "Profiles", $"{profileName}.xml"); + if (!File.Exists(profilePath)) + { + return false; + } + + return true; + } + + private void RequestGameBarProfilePriority(int ind, string profileName, DateTime now) + { + if (IsGameBarProfilePriorityActive(ind)) + { + return; + } + + gameBarPreviousUseTempProfile[ind] = Global.useTempProfile[ind]; + gameBarPreviousTempProfileName[ind] = Global.tempprofilename[ind]; + gameBarPreviousProfileName[ind] = Global.ProfilePath[ind]; + gameBarRequestedProfileName[ind] = profileName; + gameBarProfileRequestedUtc[ind] = now; + gameBarProfilePending[ind] = true; + StartupDiag($"GameBar profile requested controller={ind + 1} target='{profileName}' previousTemp={gameBarPreviousUseTempProfile[ind]} previousTempProfile='{gameBarPreviousTempProfileName[ind]}' previousProfile='{gameBarPreviousProfileName[ind]}'"); + } private void CheckGameBarHomeButton(int ind, DS4State cState, DS4State tempControlState, DS4State pState) { @@ -2677,117 +3235,180 @@ private void CheckGameBarHomeButton(int ind, DS4State cState, DS4State tempContr { cState.PS = false; tempControlState.PS = false; - LogDebug($"Game Bar open request: {gameBarIntegration.OpenGameBar()}"); + string openResult = gameBarIntegration.OpenGameBar(); + StartupDiag($"GameBar home button controller={ind + 1} existingPriority active={gameBarProfileActive[ind]} pending={gameBarProfilePending[ind]} {openResult}"); gameBarHomeButtonIgnoreUntilUtc[ind] = now + TimeSpan.FromSeconds(1); return; } - if (!Global.GameBarHomeButtonSupport[ind]) + if (!TryGetConfiguredGameBarProfileName(ind, out string profileName)) { return; } - string profileName = Global.GameBarProfileName[ind]; - if (string.IsNullOrEmpty(profileName)) + cState.PS = false; + tempControlState.PS = false; + gameBarHomeButtonIgnoreUntilUtc[ind] = now + TimeSpan.FromSeconds(1); + RequestGameBarProfilePriority(ind, profileName, now); + string result = gameBarIntegration.OpenGameBar(); + StartupDiag($"GameBar home button controller={ind + 1} requestedProfile='{profileName}' {result}"); + } + + public void UpdateGameBarProfileState() + { + if (!running) { - LogDebug($"Game Bar Home button support is enabled for controller {ind + 1}, but no Game Bar profile is selected.", true); return; } - string profilePath = Path.Combine(appdatapath, "Profiles", $"{profileName}.xml"); - if (!File.Exists(profilePath)) + if (Interlocked.Exchange(ref gameBarProfileUpdateGate, 1) == 1) { - LogDebug($"Game Bar profile '{profileName}' does not exist for controller {ind + 1}.", true); return; } - if (!gameBarIntegration.IsRunningElevated() && !gameBarAdminWarningLogged) + try { - LogDebug("Game Bar support needs DS4Windows to be run as administrator to reliably detect Game Bar overlay windows.", true); - gameBarAdminWarningLogged = true; - } + bool anyActiveOrPending = IsAnyGameBarProfilePriorityActive(); + bool anyConfigured = HasAnyConfiguredGameBarProfile(); - cState.PS = false; - tempControlState.PS = false; - gameBarHomeButtonIgnoreUntilUtc[ind] = now + TimeSpan.FromSeconds(1); - LogDebug($"Game Bar open request: {gameBarIntegration.OpenGameBar()}"); - gameBarPreviousUseTempProfile[ind] = Global.useTempProfile[ind]; - gameBarPreviousTempProfileName[ind] = Global.tempprofilename[ind]; - gameBarPreviousProfileName[ind] = Global.ProfilePath[ind]; - gameBarRequestedProfileName[ind] = profileName; - gameBarProfileRequestedUtc[ind] = now; - gameBarProfilePending[ind] = true; - LogDebug($"Controller {ind + 1} requested Game Bar profile '{profileName}'. Waiting for Game Bar to become visible."); - LogGameBarDiagnostics("Home button request"); - } + if (!anyActiveOrPending && !anyConfigured) + { + return; + } - private void UpdateGameBarProfileState() - { - bool anyActiveOrPending = false; - for (int i = 0; i < MAX_DS4_CONTROLLER_COUNT; i++) - { - if (gameBarProfileActive[i] || gameBarProfilePending[i]) + DateTime now = DateTime.UtcNow; + if (now - gameBarLastVisibilityCheckUtc < TimeSpan.FromMilliseconds(350)) { - anyActiveOrPending = true; - break; + return; } - } - if (!anyActiveOrPending) + gameBarLastVisibilityCheckUtc = now; + bool gameBarVisible = gameBarIntegration.IsGameBarVisible(); + LogGameBarDetectionIfVerbose(now, gameBarVisible, anyConfigured, anyActiveOrPending); + if (gameBarVisible) + { + gameBarLastVisibleUtc = now; + gameBarInvisibleSinceUtc = DateTime.MinValue; + RequestVisibleGameBarProfiles(now); + ActivatePendingGameBarProfiles(now); + return; + } + + if (gameBarInvisibleSinceUtc == DateTime.MinValue) + { + gameBarInvisibleSinceUtc = now; + } + + for (int i = 0; i < MAX_DS4_CONTROLLER_COUNT; i++) + { + if (gameBarProfilePending[i] && now - gameBarProfileRequestedUtc[i] > TimeSpan.FromSeconds(6)) + { + ClearPendingGameBarProfile(i); + } + + if (!gameBarProfileActive[i]) + { + continue; + } + + bool activationGraceElapsed = now - gameBarProfileActivatedUtc[i] > TimeSpan.FromSeconds(3); + bool visibilityGraceElapsed = gameBarLastVisibleUtc == DateTime.MinValue || + now - gameBarLastVisibleUtc > TimeSpan.FromMilliseconds(1500); + bool invisibleStable = gameBarInvisibleSinceUtc != DateTime.MinValue && + now - gameBarInvisibleSinceUtc > TimeSpan.FromMilliseconds(1500); + + if (activationGraceElapsed && visibilityGraceElapsed && invisibleStable && + RestorePreviousGameBarProfile(i)) + { + gameBarProfileActive[i] = false; + gameBarPreviousUseTempProfile[i] = false; + gameBarPreviousTempProfileName[i] = string.Empty; + gameBarPreviousProfileName[i] = string.Empty; + gameBarRequestedProfileName[i] = string.Empty; + } + } + } + catch (Exception ex) { - return; + StartupDiag($"UpdateGameBarProfileState exception {ex.GetType().Name}: {ex.Message}"); } - - DateTime now = DateTime.UtcNow; - if (now - gameBarLastVisibilityCheckUtc < TimeSpan.FromMilliseconds(350)) + finally { - return; + Interlocked.Exchange(ref gameBarProfileUpdateGate, 0); } + } - gameBarLastVisibilityCheckUtc = now; - bool gameBarVisible = gameBarIntegration.IsGameBarVisible(); - MaybeLogGameBarDiagnostics(now, gameBarVisible); - if (gameBarVisible) + private void LogGameBarDetectionIfVerbose(DateTime now, bool gameBarVisible, bool anyConfigured, bool anyActiveOrPending) + { + if (!Global.VerboseStartupLogging) { - gameBarLastVisibleUtc = now; - gameBarInvisibleSinceUtc = DateTime.MinValue; - ActivatePendingGameBarProfiles(now); return; } - if (gameBarInvisibleSinceUtc == DateTime.MinValue) + bool shouldLog = !gameBarVerboseDetectionLogInitialized || + gameBarVisible != gameBarVerboseLastVisible || + now - gameBarVerboseLastDetectionLogUtc > TimeSpan.FromSeconds(3); + + if (!shouldLog) { - gameBarInvisibleSinceUtc = now; + return; } + gameBarVerboseDetectionLogInitialized = true; + gameBarVerboseLastVisible = gameBarVisible; + gameBarVerboseLastDetectionLogUtc = now; + StartupDiag($"GameBar detection visible={gameBarVisible} anyConfigured={anyConfigured} anyActiveOrPending={anyActiveOrPending} {gameBarIntegration.LastDetectionSummary} controllers={BuildGameBarPriorityStateSummary()}"); + } + + private string BuildGameBarPriorityStateSummary() + { + StringBuilder builder = new StringBuilder(); for (int i = 0; i < MAX_DS4_CONTROLLER_COUNT; i++) { - if (gameBarProfilePending[i] && now - gameBarProfileRequestedUtc[i] > TimeSpan.FromSeconds(6)) + if (DS4Controllers[i] == null && + !gameBarProfileActive[i] && + !gameBarProfilePending[i]) { - ClearPendingGameBarProfile(i); - LogDebug($"Controller {i + 1} did not switch to Game Bar profile because Game Bar was not detected.", true); + continue; } - if (!gameBarProfileActive[i]) + if (builder.Length > 0) { - continue; + builder.Append(" "); } - bool activationGraceElapsed = now - gameBarProfileActivatedUtc[i] > TimeSpan.FromSeconds(3); - bool visibilityGraceElapsed = gameBarLastVisibleUtc == DateTime.MinValue || - now - gameBarLastVisibleUtc > TimeSpan.FromMilliseconds(1500); - bool invisibleStable = gameBarInvisibleSinceUtc != DateTime.MinValue && - now - gameBarInvisibleSinceUtc > TimeSpan.FromMilliseconds(1500); + builder.Append("C"); + builder.Append(i + 1); + builder.Append("[connected="); + builder.Append(DS4Controllers[i] != null); + builder.Append(",enabled="); + builder.Append(Global.GameBarHomeButtonSupport[i]); + builder.Append(",target='"); + builder.Append(Global.GameBarProfileName[i]); + builder.Append("',active="); + builder.Append(gameBarProfileActive[i]); + builder.Append(",pending="); + builder.Append(gameBarProfilePending[i]); + builder.Append(",requested='"); + builder.Append(gameBarRequestedProfileName[i]); + builder.Append("']"); + } + + return builder.Length == 0 ? "none" : builder.ToString(); + } + + private void RequestVisibleGameBarProfiles(DateTime now) + { + for (int i = 0; i < MAX_DS4_CONTROLLER_COUNT; i++) + { + if (DS4Controllers[i] == null || IsGameBarProfilePriorityActive(i)) + { + continue; + } - if (activationGraceElapsed && visibilityGraceElapsed && invisibleStable) + if (TryGetConfiguredGameBarProfileName(i, out string profileName)) { - RestorePreviousGameBarProfile(i); - gameBarProfileActive[i] = false; - gameBarPreviousUseTempProfile[i] = false; - gameBarPreviousTempProfileName[i] = string.Empty; - gameBarPreviousProfileName[i] = string.Empty; - gameBarRequestedProfileName[i] = string.Empty; - LogDebug($"Controller {i + 1} reverted from Game Bar profile."); + RequestGameBarProfilePriority(i, profileName, now); } } } @@ -2802,42 +3423,35 @@ private void ActivatePendingGameBarProfiles(DateTime now) } string profileName = gameBarRequestedProfileName[i]; - if (!string.IsNullOrEmpty(profileName) && Global.LoadTempProfile(i, profileName, false, this)) + bool actionRan = true; + bool profileLoaded = false; + if (!string.IsNullOrEmpty(profileName)) + { + actionRan = RunProfileActionWithReportingPaused(i, + () => profileLoaded = Global.LoadTempProfile(i, profileName, false, this)); + } + + if (!actionRan) + { + StartupDiag($"GameBar profile activation skipped controller={i + 1} target='{profileName}' reason=reporting-action-not-run"); + continue; + } + + if (profileLoaded) { gameBarProfileActive[i] = true; gameBarProfileActivatedUtc[i] = now; - LogDebug($"Controller {i + 1} switched to Game Bar profile '{profileName}'."); + StartupDiag($"GameBar profile activated controller={i + 1} target='{profileName}'"); } else { - LogDebug($"Controller {i + 1} could not load Game Bar profile '{profileName}'.", true); + StartupDiag($"GameBar profile activation failed controller={i + 1} target='{profileName}'"); } ClearPendingGameBarProfile(i); } } - private void MaybeLogGameBarDiagnostics(DateTime now, bool gameBarVisible) - { - bool changed = !gameBarHasDiagnosticVisibleState || gameBarLastDiagnosticVisible != gameBarVisible; - bool intervalElapsed = now - gameBarLastDiagnosticLogUtc > TimeSpan.FromSeconds(10); - - if (!changed && !intervalElapsed) - { - return; - } - - gameBarLastDiagnosticVisible = gameBarVisible; - gameBarHasDiagnosticVisibleState = true; - gameBarLastDiagnosticLogUtc = now; - LogGameBarDiagnostics(changed ? $"Visibility changed to {gameBarVisible}" : $"Visibility still {gameBarVisible}"); - } - - private void LogGameBarDiagnostics(string reason) - { - LogDebug($"Game Bar diagnostics ({reason}):\n{gameBarIntegration.GetGameBarStateDiagnostics()}"); - } - private void ClearPendingGameBarProfile(int ind) { gameBarProfilePending[ind] = false; @@ -2852,13 +3466,27 @@ private void ClearPendingGameBarProfile(int ind) } } - private void RestorePreviousGameBarProfile(int ind) + private bool RestorePreviousGameBarProfile(int ind) { + bool actionRan = true; + bool restored = false; + if (gameBarPreviousUseTempProfile[ind] && - !string.IsNullOrEmpty(gameBarPreviousTempProfileName[ind]) && - Global.LoadTempProfile(ind, gameBarPreviousTempProfileName[ind], false, this)) + !string.IsNullOrEmpty(gameBarPreviousTempProfileName[ind])) { - return; + actionRan = RunProfileActionWithReportingPaused(ind, + () => restored = Global.LoadTempProfile(ind, gameBarPreviousTempProfileName[ind], false, this)); + if (!actionRan) + { + StartupDiag($"GameBar profile restore skipped controller={ind + 1} targetTemp='{gameBarPreviousTempProfileName[ind]}' reason=reporting-action-not-run"); + return false; + } + + if (restored) + { + StartupDiag($"GameBar profile restored controller={ind + 1} tempProfile='{gameBarPreviousTempProfileName[ind]}'"); + return true; + } } string previousProfileName = gameBarPreviousProfileName[ind]; @@ -2868,7 +3496,46 @@ private void RestorePreviousGameBarProfile(int ind) Global.OlderProfilePath[ind] = previousProfileName; } - Global.LoadProfile(ind, false, this); + actionRan = RunProfileActionWithReportingPaused(ind, + () => restored = Global.LoadProfile(ind, false, this)); + StartupDiag($"GameBar profile restore controller={ind + 1} profile='{previousProfileName}' actionRan={actionRan} restored={restored}"); + return actionRan && restored; + } + + private bool RunProfileActionWithReportingPaused(int ind, Action action) + { + DS4Device device = ind >= 0 && ind < DS4Controllers.Length ? DS4Controllers[ind] : null; + if (device == null) + { + action?.Invoke(); + return true; + } + + bool actionRan = false; + device.HaltReportingRunAction(() => + { + actionRan = true; + action?.Invoke(); + }); + + return actionRan; + } + + private void StartGameBarProfileTimer() + { + if (gameBarProfileTimer != null) + { + return; + } + + gameBarProfileTimer = new System.Threading.Timer(_ => UpdateGameBarProfileState(), + null, TimeSpan.FromMilliseconds(350), TimeSpan.FromMilliseconds(350)); + } + + private void StopGameBarProfileTimer() + { + System.Threading.Timer timer = Interlocked.Exchange(ref gameBarProfileTimer, null); + timer?.Dispose(); } // Called every time a new input report has arrived @@ -2876,6 +3543,18 @@ protected void On_Report(DS4Device device, EventArgs e, int ind) { if (ind != -1) { + int startupReportCount = 0; + bool startupReportDiag = false; + if (Global.VerboseStartupLogging) + { + startupReportCount = ++startupReportDiagCounts[ind]; + startupReportDiag = startupReportCount <= 5 || startupReportCount == 50; + if (startupReportDiag) + { + StartupDiag($"On_Report enter index={ind} count={startupReportCount} synced={device.isSynced()} latency={device.Latency} useDInputOnly={useDInputOnly[ind]} activeOut={activeOutDevType[ind]} outDev={outputDevices[ind]?.GetDeviceType() ?? "null"}"); + } + } + string devError = tempStrings[ind] = device.error; if (!string.IsNullOrEmpty(devError)) { @@ -2924,8 +3603,6 @@ protected void On_Report(DS4Device device, EventArgs e, int ind) //device.getPreviousState(PreviousState[ind]); //DS4State pState = PreviousState[ind]; - UpdateGameBarProfileState(); - if (device.firstReport && device.isSynced()) { // Only send Log message when device is considered a primary device @@ -3000,6 +3677,11 @@ protected void On_Report(DS4Device device, EventArgs e, int ind) cState = device.Debouncer.ProcessInput(cState); + if (startupReportDiag) + { + StartupDiag($"On_Report pre-map index={ind} count={startupReportCount} buttons Cross={cState.Cross} Circle={cState.Circle} PS={cState.PS} LX={cState.LX} LY={cState.LY} RX={cState.RX} RY={cState.RY} L2={cState.L2} R2={cState.R2}"); + } + cState = Mapping.SetCurveAndDeadzone(ind, cState, TempState[ind]); if (!recordingMacro && (useTempProfile[ind] || @@ -3014,7 +3696,15 @@ protected void On_Report(DS4Device device, EventArgs e, int ind) OSCPreMappingStep(ind, cState, tempMapState, oscMapState); } + if (startupReportDiag) + { + StartupDiag($"On_Report MapCustom begin index={ind} count={startupReportCount}"); + } Mapping.MapCustom(ind, cState, tempMapState, ExposedState[ind], touchPad[ind], this); + if (startupReportDiag) + { + StartupDiag($"On_Report MapCustom end index={ind} count={startupReportCount}"); + } // Copy current Touchpad and Gyro data // Might change to use new DS4State.CopyExtrasTo method @@ -3055,7 +3745,15 @@ protected void On_Report(DS4Device device, EventArgs e, int ind) } } + if (startupReportDiag) + { + StartupDiag($"On_Report ConvertandSendReport begin index={ind} count={startupReportCount} outDev={outputDevices[ind]?.GetDeviceType() ?? "null"}"); + } outputDevices[ind]?.ConvertandSendReport(cState, ind); + if (startupReportDiag) + { + StartupDiag($"On_Report ConvertandSendReport end index={ind} count={startupReportCount}"); + } //testNewReport(ref x360reports[ind], cState, ind); //x360controls[ind]?.SendReport(x360reports[ind]); @@ -3103,10 +3801,26 @@ protected void On_Report(DS4Device device, EventArgs e, int ind) } // Output any synthetic events. + if (startupReportDiag) + { + StartupDiag($"On_Report Mapping.Commit begin index={ind} count={startupReportCount}"); + } Mapping.Commit(ind); + if (startupReportDiag) + { + StartupDiag($"On_Report Mapping.Commit end index={ind} count={startupReportCount}"); + } // Update the Lightbar color + if (startupReportDiag) + { + StartupDiag($"On_Report updateLightBar begin index={ind} count={startupReportCount}"); + } DS4LightBar.updateLightBar(device, ind); + if (startupReportDiag) + { + StartupDiag($"On_Report updateLightBar end index={ind} count={startupReportCount}"); + } if (device.PerformStateMerge) { @@ -3118,6 +3832,11 @@ protected void On_Report(DS4Device device, EventArgs e, int ind) // Copy for use in UDP tempControlState.Motion = device.GetRawCurrentStateRef().Motion; } + + if (startupReportDiag) + { + StartupDiag($"On_Report exit index={ind} count={startupReportCount}"); + } } } @@ -3429,6 +4148,15 @@ public void StartTPOff(int deviceID) } } + public void SetTouchpadMovementActive(int deviceID, bool active) + { + if (deviceID < CURRENT_DS4_CONTROLLER_LIMIT) + { + TouchActive[deviceID] = active; + touchreleased[deviceID] = true; + } + } + public string TouchpadSlide(int ind) { DS4State cState = CurrentState[ind]; @@ -3466,6 +4194,17 @@ public void LogDebug(String Data, bool warning = false) } } + public static void StartupDiag(string data) + { + if (!Global.VerboseStartupLogging) + { + return; + } + + startupDiagLogger.Info($"[StartupDiag][T{Thread.CurrentThread.ManagedThreadId}] {data}"); + LogManager.Flush(TimeSpan.FromSeconds(1)); + } + public void OnDebug(object sender, DebugEventArgs args) { if (Debug != null) diff --git a/DS4Windows/DS4Control/DS4LightBar.cs b/DS4Windows/DS4Control/DS4LightBar.cs index 03260d2..cd14c6b 100644 --- a/DS4Windows/DS4Control/DS4LightBar.cs +++ b/DS4Windows/DS4Control/DS4LightBar.cs @@ -21,6 +21,7 @@ You should have received a copy of the GNU General Public License using static System.Math; using static DS4Windows.Global; using System.Diagnostics; +using DS4WinWPF.DS4Control; namespace DS4Windows { @@ -324,6 +325,12 @@ public static void updateLightBar(DS4Device device, int deviceNum) LightBarColor = color }; */ + if (Global.openRGBSyncEnabled && + OpenRGBServer.Instance.TryGetColor(deviceNum, out DS4Color openRGBColor)) + { + color = openRGBColor; + } + DS4LightbarState lightState = new DS4LightbarState { LightBarColor = color, diff --git a/DS4Windows/DS4Control/DS4OutDevice.cs b/DS4Windows/DS4Control/DS4OutDevice.cs index ba6f475..28e088e 100644 --- a/DS4Windows/DS4Control/DS4OutDevice.cs +++ b/DS4Windows/DS4Control/DS4OutDevice.cs @@ -57,18 +57,41 @@ public override void Connect() } public override void Disconnect() { - // Remove feedback handlers before Disconnect - RemoveFeedbacks(); + try + { + // Remove feedback handlers before Disconnect + RemoveFeedbacks(); + } + catch (Exception ex) + { + ControlService.StartupDiag($"DS4OutDevice.RemoveFeedbacks exception {ex.GetType().Name}: {ex.Message}"); + } - connected = false; - cont.Disconnect(); - //cont.Dispose(); - cont = null; + try + { + cont?.Disconnect(); + } + catch (Exception ex) + { + ControlService.StartupDiag($"DS4OutDevice.Disconnect exception {ex.GetType().Name}: {ex.Message}"); + } + finally + { + connected = false; + //cont.Dispose(); + cont = null; + } } public override string GetDeviceType() => devtype; public override void RemoveFeedbacks() { + if (cont == null) + { + forceFeedbacksDict.Clear(); + return; + } + foreach (KeyValuePair pair in forceFeedbacksDict) { cont.FeedbackReceived -= pair.Value; @@ -79,7 +102,8 @@ public override void RemoveFeedbacks() public override void RemoveFeedback(int inIdx) { - if (forceFeedbacksDict.TryGetValue(inIdx, out DualShock4FeedbackReceivedEventHandler handler)) + if (cont != null && + forceFeedbacksDict.TryGetValue(inIdx, out DualShock4FeedbackReceivedEventHandler handler)) { cont.FeedbackReceived -= handler; forceFeedbacksDict.Remove(inIdx); diff --git a/DS4Windows/DS4Control/DS4OutDevices/DS4OutDeviceExt.cs b/DS4Windows/DS4Control/DS4OutDevices/DS4OutDeviceExt.cs index aa24b44..86dd2dd 100644 --- a/DS4Windows/DS4Control/DS4OutDevices/DS4OutDeviceExt.cs +++ b/DS4Windows/DS4Control/DS4OutDevices/DS4OutDeviceExt.cs @@ -333,14 +333,24 @@ public override void Disconnect() { if (awaitOutBuffThread != null) { - if (!awaitOutBuffThread.ThreadState.HasFlag(ThreadState.WaitSleepJoin)) + try { awaitOutBuffThread.Interrupt(); } + catch + { + } - awaitOutBuffThread.Join(); + if (!awaitOutBuffThread.Join(1000)) + { + ControlService.StartupDiag("DS4OutDeviceExt.Disconnect output-buffer thread did not exit before timeout"); + } } } + + tokenSource?.Dispose(); + tokenSource = null; + awaitOutBuffThread = null; } } } diff --git a/DS4Windows/DS4Control/DS4StateFieldMapping.cs b/DS4Windows/DS4Control/DS4StateFieldMapping.cs index 3da1ac7..2631d48 100644 --- a/DS4Windows/DS4Control/DS4StateFieldMapping.cs +++ b/DS4Windows/DS4Control/DS4StateFieldMapping.cs @@ -22,6 +22,7 @@ public class DS4StateFieldMapping { public enum ControlType : int { Unknown = 0, Button, AxisDir, Trigger, Touch, GyroDir, SwipeDir } public const byte LAST_DS4_ACTION = (byte)DS4Controls.TouchEnded; + public const byte TRIGGER_FULL_PULL_THRESHOLD = 250; public bool[] buttons = new bool[(int)LAST_DS4_ACTION + 1]; public byte[] axisdirs = new byte[(int)LAST_DS4_ACTION + 1]; @@ -121,10 +122,10 @@ public void PopulateFieldMapping(DS4State cState, DS4StateExposed exposeState, M triggers[(int)DS4Controls.R2] = cState.R2; buttons[(int)DS4Controls.L1] = cState.L1; - buttons[(int)DS4Controls.L2FullPull] = cState.L2Raw == 255; + buttons[(int)DS4Controls.L2FullPull] = IsTriggerFullPull(cState.L2Raw); buttons[(int)DS4Controls.L3] = cState.L3; buttons[(int)DS4Controls.R1] = cState.R1; - buttons[(int)DS4Controls.R2FullPull] = cState.R2Raw == 255; + buttons[(int)DS4Controls.R2FullPull] = IsTriggerFullPull(cState.R2Raw); buttons[(int)DS4Controls.R3] = cState.R3; buttons[(int)DS4Controls.Cross] = cState.Cross; @@ -183,6 +184,11 @@ public void PopulateFieldMapping(DS4State cState, DS4StateExposed exposeState, M } } + public static bool IsTriggerFullPull(byte rawTriggerValue) + { + return rawTriggerValue >= TRIGGER_FULL_PULL_THRESHOLD; + } + public void PopulateState(DS4State state) { unchecked diff --git a/DS4Windows/DS4Control/DTOXml/AppSettingsDTO.cs b/DS4Windows/DS4Control/DTOXml/AppSettingsDTO.cs index fd574ed..fad17aa 100644 --- a/DS4Windows/DS4Control/DTOXml/AppSettingsDTO.cs +++ b/DS4Windows/DS4Control/DTOXml/AppSettingsDTO.cs @@ -389,6 +389,13 @@ public bool UseAdvancedMoonlight set; } + [XmlElement("VerboseStartupLogging")] + public bool VerboseStartupLogging + { + get; + set; + } + [XmlIgnore] public bool CloseMinimizes { @@ -864,6 +871,7 @@ public void MapFrom(BackingStore source) QuickCharge = source.quickCharge; UseMoonlight = source.useMoonlight; UseAdvancedMoonlight = source.useAdvancedMoonlight; + VerboseStartupLogging = source.verboseStartupLogging; CloseMinimizes = source.closeMini; UseLang = source.useLang; DownloadLang = source.downloadLang; @@ -964,6 +972,7 @@ public void MapTo(BackingStore destination) destination.quickCharge = QuickCharge; destination.useMoonlight = UseMoonlight; destination.useAdvancedMoonlight = UseAdvancedMoonlight; + destination.verboseStartupLogging = VerboseStartupLogging; destination.closeMini = CloseMinimizes; destination.useLang = UseLang; destination.downloadLang = DownloadLang; diff --git a/DS4Windows/DS4Control/DTOXml/AutoProfilesDTO.cs b/DS4Windows/DS4Control/DTOXml/AutoProfilesDTO.cs index 34f54a7..191e4e0 100644 --- a/DS4Windows/DS4Control/DTOXml/AutoProfilesDTO.cs +++ b/DS4Windows/DS4Control/DTOXml/AutoProfilesDTO.cs @@ -39,6 +39,7 @@ public void MapFrom(AutoProfileHolder source) { foreach(AutoProfileEntity entity in source.AutoProfileColl) { + entity.EnsureProfileNames(); AutoProfileEntrySerializer temp = new AutoProfileEntrySerializer() { Path = entity.Path, diff --git a/DS4Windows/DS4Control/DTOXml/OutputSlotPersistDTO.cs b/DS4Windows/DS4Control/DTOXml/OutputSlotPersistDTO.cs index ca3d560..be13fba 100644 --- a/DS4Windows/DS4Control/DTOXml/OutputSlotPersistDTO.cs +++ b/DS4Windows/DS4Control/DTOXml/OutputSlotPersistDTO.cs @@ -18,8 +18,6 @@ You should have received a copy of the GNU General Public License using System; using System.Collections.Generic; -using System.Windows.Documents; -using System.Xml; using System.Xml.Serialization; using DS4Windows; @@ -76,11 +74,37 @@ public void MapTo(OutputSlotManager destination) if (tempDev != null) { + if (tempSlot.DeviceType == OutContType.None) + { + continue; + } + tempDev.CurrentReserveStatus = OutSlotDevice.ReserveStatus.Permanent; tempDev.PermanentType = tempSlot.DeviceType; } } } + + internal static OutContType ParseOutputDeviceType(string value, OutContType fallback) + { + if (Enum.TryParse(value, true, out OutContType parsed) && + Enum.IsDefined(typeof(OutContType), parsed)) + { + return parsed; + } + + return fallback; + } + + internal static string FormatOutputDeviceType(OutContType value) + { + return value switch + { + OutContType.DS4 => "DS4", + OutContType.None => "None", + _ => "X360", + }; + } } public class OutputSlotSerializer @@ -91,10 +115,17 @@ public int Index get; set; } = 0; - [XmlElement("DeviceType")] + [XmlIgnore] public OutContType DeviceType { get; set; } + + [XmlElement("DeviceType")] + public string DeviceTypeString + { + get => OutputSlotPersistDTO.FormatOutputDeviceType(DeviceType); + set => DeviceType = OutputSlotPersistDTO.ParseOutputDeviceType(value, OutContType.None); + } } } diff --git a/DS4Windows/DS4Control/DTOXml/ProfileDTO.cs b/DS4Windows/DS4Control/DTOXml/ProfileDTO.cs index be803d0..89f2a62 100644 --- a/DS4Windows/DS4Control/DTOXml/ProfileDTO.cs +++ b/DS4Windows/DS4Control/DTOXml/ProfileDTO.cs @@ -1436,12 +1436,19 @@ public AbsMouseRegionSettingsSerializer AbsMouseRegionSettings get; set; } - [XmlElement("OutputContDevice")] + [XmlIgnore] public OutContType OutputContDevice { get; set; } = BackingStore.DEFAULT_OUT_CONT_TYPE; + [XmlElement("OutputContDevice")] + public string OutputContDeviceString + { + get => OutputSlotPersistDTO.FormatOutputDeviceType(OutputContDevice); + set => OutputContDevice = OutputSlotPersistDTO.ParseOutputDeviceType(value, BackingStore.DEFAULT_OUT_CONT_TYPE); + } + [XmlElement("DS4OutputTriggerMode")] public DS4TriggerOutputMode OutputDS4TriggerMode { diff --git a/DS4Windows/DS4Control/GameBarIntegration.cs b/DS4Windows/DS4Control/GameBarIntegration.cs index 8183761..1520a96 100644 --- a/DS4Windows/DS4Control/GameBarIntegration.cs +++ b/DS4Windows/DS4Control/GameBarIntegration.cs @@ -19,10 +19,13 @@ You should have received a copy of the GNU General Public License using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Runtime.InteropServices; using System.Security.Principal; using System.Text; +using System.Threading; using System.Windows.Automation; +using Microsoft.Win32; using Windows.Foundation.Metadata; using Windows.Gaming.UI; using WinRect = System.Windows.Rect; @@ -35,11 +38,43 @@ public class GameBarIntegration private const byte VK_G = 0x47; private const int KEYEVENTF_KEYUP = 0x0002; private const int DWMWA_CLOAKED = 14; + private const int STARTF_USESHOWWINDOW = 0x00000001; + private const int STARTF_FORCEOFFFEEDBACK = 0x00000080; + private const int SW_HIDE = 0; + private const uint CREATE_NO_WINDOW = 0x08000000; + private const uint WAIT_OBJECT_0 = 0x00000000; + private const uint WAIT_TIMEOUT = 0x00000102; private const int MaxAutomationDiagnosticRows = 20; private const int MaxAutomationVisitCount = 500; private const int MaxAutomationDepth = 5; private const int MaxDiagnosticTextLength = 160; - private static bool? gameBarApiPresent; + private const int LiveGameBarApiPollMs = 1000; + private const int LiveGameBarApiCacheMs = 1500; + private const int LiveGameBarApiProbeTimeoutMs = 1500; + private const int LiveGameBarApiHangMs = 2500; + private const int LiveAutomationPollMs = 1000; + private const int LiveAutomationCacheMs = 3000; + public const string ProbeArgument = "--ds4windows-gamebar-probe"; + private static readonly object detectionStatusLock = new object(); + private static readonly object gameBarApiPollLock = new object(); + private static bool gameBarApiPollRunning; + private static bool gameBarApiPollCachedVisible; + private static DateTime gameBarApiPollLastStartedUtc = DateTime.MinValue; + private static DateTime gameBarApiPollLastCompletedUtc = DateTime.MinValue; + private static int gameBarApiPollGeneration; + private static bool gameBarApiPollLastSupported; + private static bool gameBarApiPollLastVisible; + private static bool gameBarApiPollLastInputRedirected; + private static long gameBarApiPollLastElapsedMs; + private static string gameBarApiPollLastStatus = "not started"; + private static string lastDetectionSummary = "not checked"; + private static readonly object automationPollLock = new object(); + private static bool automationPollRunning; + private static bool automationPollCachedVisible; + private static DateTime automationPollLastStartedUtc = DateTime.MinValue; + private static DateTime automationPollLastCompletedUtc = DateTime.MinValue; + private static bool? gameBarProtocolRegistered; + private static int gameBarMissingWarningLogged; private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); @@ -76,6 +111,30 @@ public class GameBarIntegration [DllImport("dwmapi.dll")] private static extern int DwmGetWindowAttribute(IntPtr hwnd, int dwAttribute, out int pvAttribute, int cbAttribute); + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern bool CreateProcess(string lpApplicationName, + StringBuilder lpCommandLine, + IntPtr lpProcessAttributes, + IntPtr lpThreadAttributes, + bool bInheritHandles, + uint dwCreationFlags, + IntPtr lpEnvironment, + string lpCurrentDirectory, + ref STARTUPINFO lpStartupInfo, + out PROCESS_INFORMATION lpProcessInformation); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool GetExitCodeProcess(IntPtr hProcess, out uint lpExitCode); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool TerminateProcess(IntPtr hProcess, uint uExitCode); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool CloseHandle(IntPtr hObject); + [StructLayout(LayoutKind.Sequential)] private struct RECT { @@ -85,6 +144,78 @@ private struct RECT public int Bottom; } + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + private struct STARTUPINFO + { + public int cb; + public string lpReserved; + public string lpDesktop; + public string lpTitle; + public int dwX; + public int dwY; + public int dwXSize; + public int dwYSize; + public int dwXCountChars; + public int dwYCountChars; + public int dwFillAttribute; + public int dwFlags; + public short wShowWindow; + public short cbReserved2; + public IntPtr lpReserved2; + public IntPtr hStdInput; + public IntPtr hStdOutput; + public IntPtr hStdError; + } + + [StructLayout(LayoutKind.Sequential)] + private struct PROCESS_INFORMATION + { + public IntPtr hProcess; + public IntPtr hThread; + public int dwProcessId; + public int dwThreadId; + } + + public static bool TryRunProbeCommand(string[] args) + { + if (args == null || + args.Length < 2 || + !args[0].Equals(ProbeArgument, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + RunProbeCommand(args[1]); + return true; + } + + private static void RunProbeCommand(string resultPath) + { + bool supported = false; + bool visible = false; + bool inputRedirected = false; + string status = "not started"; + + try + { + supported = TryGetGameBarApiStateCore(out visible, out inputRedirected, out status); + } + catch (Exception ex) + { + status = ex.GetType().Name + ": " + ex.Message; + } + + try + { + File.WriteAllText(resultPath, + $"{supported}|{visible}|{inputRedirected}|{SanitizeProbeStatus(status)}", + Encoding.UTF8); + } + catch + { + } + } + public bool IsRunningElevated() { using (WindowsIdentity identity = WindowsIdentity.GetCurrent()) @@ -96,6 +227,12 @@ public bool IsRunningElevated() public string OpenGameBar() { + if (!IsGameBarProtocolRegistered()) + { + LogMissingGameBarWarning(); + return "Game Bar not opened: ms-gamebar protocol is not registered"; + } + keybd_event(VK_LWIN, 0, 0, UIntPtr.Zero); keybd_event(VK_G, 0, 0, UIntPtr.Zero); keybd_event(VK_G, 0, KEYEVENTF_KEYUP, UIntPtr.Zero); @@ -103,32 +240,259 @@ public string OpenGameBar() return "keybd_event Win+G sent"; } + private static bool IsGameBarProtocolRegistered() + { + if (gameBarProtocolRegistered.HasValue) + { + return gameBarProtocolRegistered.Value; + } + + bool registered = RegistryProtocolExists(Registry.CurrentUser, @"Software\Classes\ms-gamebar") || + RegistryProtocolExists(Registry.ClassesRoot, "ms-gamebar"); + gameBarProtocolRegistered = registered; + return registered; + } + + private static bool RegistryProtocolExists(RegistryKey root, string subKeyName) + { + try + { + using RegistryKey key = root.OpenSubKey(subKeyName); + if (key == null) + { + return false; + } + + object urlProtocol = key.GetValue("URL Protocol"); + return urlProtocol != null || + !string.IsNullOrWhiteSpace(key.GetValue(null) as string); + } + catch + { + return false; + } + } + + private static void LogMissingGameBarWarning() + { + if (Interlocked.Exchange(ref gameBarMissingWarningLogged, 1) == 1) + { + return; + } + + const string message = "Xbox Game Bar is not installed or its ms-gamebar protocol handler is not registered. Install or repair Xbox Game Bar from the Microsoft Store, then restart DS4Windows to use Game Bar profile support."; + AppLogger.LogToGui(message, true); + AppLogger.LogToTray(message, true, true); + } + + public string LastDetectionSummary + { + get + { + lock (detectionStatusLock) + { + return lastDetectionSummary; + } + } + } + public bool IsGameBarVisible() { - if (TryGetGameBarApiState(out bool apiVisible, out bool apiInputRedirected, out _)) + bool apiVisible = IsGameBarVisibleByCachedGameBarApi(out string apiSummary); + bool windowVisible = IsGameBarVisibleByWindowEnumeration(out string windowSummary); + bool visible = apiVisible || windowVisible; + string source = apiVisible ? "api" : windowVisible ? "window" : "none"; + UpdateLastDetectionSummary(visible, source, + $"api={apiSummary}; window={windowSummary}; uia=diagnostic-only"); + return visible; + } + + private static void UpdateLastDetectionSummary(bool visible, string source, string details) + { + lock (detectionStatusLock) { - return apiVisible || apiInputRedirected; + lastDetectionSummary = $"source={source} visible={visible} {details}"; } + } + private static bool IsGameBarVisibleByWindowEnumeration() + { + return IsGameBarVisibleByWindowEnumeration(out _); + } + + private static bool IsGameBarVisibleByWindowEnumeration(out string summary) + { bool visible = false; + string matchSummary = string.Empty; - EnumWindows((hWnd, lParam) => + try { - if (!IsInspectableWindow(hWnd)) + EnumWindows((hWnd, lParam) => { + if (!IsInspectableWindow(hWnd)) + { + return true; + } + + if (LooksLikeGameBarWindow(hWnd, out string windowReason) || + HasGameBarChildWindow(hWnd, out windowReason)) + { + visible = true; + matchSummary = windowReason; + return false; + } + return true; - } + }, IntPtr.Zero); + } + catch (Exception ex) + { + summary = "error " + ex.GetType().Name + ": " + TruncateDiagnosticText(ex.Message); + return false; + } + + if (visible) + { + summary = matchSummary; + return true; + } - if (LooksLikeGameBarWindow(hWnd) || HasGameBarChildWindow(hWnd)) + summary = "no strict visible HWND match"; + return false; + } + + private static bool IsGameBarVisibleByCachedGameBarApi(out string summary) + { + DateTime now = DateTime.UtcNow; + lock (gameBarApiPollLock) + { + bool cachedResultIsFresh = gameBarApiPollCachedVisible && + now - gameBarApiPollLastCompletedUtc < TimeSpan.FromMilliseconds(LiveGameBarApiCacheMs); + + bool pollIsStale = gameBarApiPollRunning && + now - gameBarApiPollLastStartedUtc > TimeSpan.FromMilliseconds(LiveGameBarApiHangMs); + + if ((!gameBarApiPollRunning || pollIsStale) && + now - gameBarApiPollLastStartedUtc >= TimeSpan.FromMilliseconds(LiveGameBarApiPollMs)) { - visible = true; - return false; + gameBarApiPollRunning = true; + gameBarApiPollLastStartedUtc = now; + int pollGeneration = ++gameBarApiPollGeneration; + + Thread worker = new Thread(() => + { + bool visible = false; + bool supported = false; + bool apiVisible = false; + bool apiInputRedirected = false; + string apiStatus = "not started"; + Stopwatch stopwatch = Stopwatch.StartNew(); + try + { + supported = TryGetGameBarApiStateOutOfProcess(LiveGameBarApiProbeTimeoutMs, + out apiVisible, out apiInputRedirected, out apiStatus); + visible = supported && (apiVisible || apiInputRedirected); + } + catch (Exception ex) + { + apiStatus = ex.GetType().Name + ": " + ex.Message; + visible = false; + } + finally + { + stopwatch.Stop(); + lock (gameBarApiPollLock) + { + gameBarApiPollLastSupported = supported; + gameBarApiPollLastVisible = apiVisible; + gameBarApiPollLastInputRedirected = apiInputRedirected; + gameBarApiPollLastElapsedMs = stopwatch.ElapsedMilliseconds; + gameBarApiPollLastStatus = apiStatus; + + if (pollGeneration == gameBarApiPollGeneration || visible) + { + gameBarApiPollCachedVisible = visible; + gameBarApiPollLastCompletedUtc = DateTime.UtcNow; + } + + if (pollGeneration == gameBarApiPollGeneration) + { + gameBarApiPollRunning = false; + } + } + } + }); + + worker.IsBackground = true; + worker.Name = "DS4Windows Game Bar API Poll"; + worker.Start(); } - return true; - }, IntPtr.Zero); + summary = BuildCachedGameBarApiSummary(now, cachedResultIsFresh, pollIsStale); + return cachedResultIsFresh; + } + } + + private static string BuildCachedGameBarApiSummary(DateTime now, bool cachedResultIsFresh, bool pollIsStale) + { + string completedAge = gameBarApiPollLastCompletedUtc == DateTime.MinValue ? + "never" : + ((int)(now - gameBarApiPollLastCompletedUtc).TotalMilliseconds).ToString() + "ms"; + string startedAge = gameBarApiPollLastStartedUtc == DateTime.MinValue ? + "never" : + ((int)(now - gameBarApiPollLastStartedUtc).TotalMilliseconds).ToString() + "ms"; + + return $"cachedFresh={cachedResultIsFresh} cachedVisible={gameBarApiPollCachedVisible} " + + $"running={gameBarApiPollRunning} stale={pollIsStale} startedAge={startedAge} completedAge={completedAge} " + + $"lastSupported={gameBarApiPollLastSupported} lastVisible={gameBarApiPollLastVisible} " + + $"lastInputRedirected={gameBarApiPollLastInputRedirected} lastElapsedMs={gameBarApiPollLastElapsedMs} " + + $"lastStatus='{TruncateDiagnosticText(gameBarApiPollLastStatus)}'"; + } - return visible || IsGameBarVisibleByAutomation(); + private static bool IsGameBarVisibleByCachedAutomation() + { + DateTime now = DateTime.UtcNow; + lock (automationPollLock) + { + bool cachedResultIsFresh = automationPollCachedVisible && + now - automationPollLastCompletedUtc < TimeSpan.FromMilliseconds(LiveAutomationCacheMs); + + if (!automationPollRunning && + now - automationPollLastStartedUtc >= TimeSpan.FromMilliseconds(LiveAutomationPollMs)) + { + automationPollRunning = true; + automationPollLastStartedUtc = now; + + Thread worker = new Thread(() => + { + bool visible = false; + try + { + visible = IsGameBarVisibleByAutomation(); + } + catch + { + visible = false; + } + finally + { + lock (automationPollLock) + { + automationPollCachedVisible = visible; + automationPollLastCompletedUtc = DateTime.UtcNow; + automationPollRunning = false; + } + } + }); + + worker.IsBackground = true; + worker.Name = "DS4Windows Game Bar UIA Poll"; + worker.SetApartmentState(ApartmentState.STA); + worker.Start(); + } + + return cachedResultIsFresh; + } } public string GetGameBarWindowDiagnostics() @@ -172,15 +536,14 @@ public string GetGameBarStateDiagnostics() return $"GameBarVisible={IsGameBarVisible()} Elevated={IsRunningElevated()}\n{GetGameBarApiDiagnostics()}"; } - private static bool TryGetGameBarApiState(out bool visible, out bool inputRedirected, out string status) + private static bool TryGetGameBarApiStateCore(out bool visible, out bool inputRedirected, out string status) { visible = false; inputRedirected = false; try { - gameBarApiPresent ??= ApiInformation.IsTypePresent("Windows.Gaming.UI.GameBar"); - if (!gameBarApiPresent.Value) + if (!ApiInformation.IsTypePresent("Windows.Gaming.UI.GameBar")) { status = "not present"; return false; @@ -200,10 +563,239 @@ private static bool TryGetGameBarApiState(out bool visible, out bool inputRedire private static string GetGameBarApiDiagnostics() { - bool supported = TryGetGameBarApiState(out bool visible, out bool inputRedirected, out string status); + bool supported = TryGetGameBarApiStateOutOfProcess(2000, out bool visible, out bool inputRedirected, out string status); return $"GameBarApi supported={supported} visible={visible} inputRedirected={inputRedirected} status='{TruncateDiagnosticText(status)}'"; } + private static bool TryParseProbeResult(string result, out bool supported, out bool visible, out bool inputRedirected, out string status) + { + supported = false; + visible = false; + inputRedirected = false; + status = string.Empty; + + if (string.IsNullOrEmpty(result)) + { + return false; + } + + string[] parts = result.Split(new[] { '|' }, 4); + if (parts.Length < 4) + { + return false; + } + + if (!bool.TryParse(parts[0], out supported) || + !bool.TryParse(parts[1], out visible) || + !bool.TryParse(parts[2], out inputRedirected)) + { + supported = false; + visible = false; + inputRedirected = false; + return false; + } + + status = parts[3]; + return true; + } + + private static string SanitizeProbeStatus(string status) + { + if (string.IsNullOrEmpty(status)) + { + return string.Empty; + } + + return status.Replace('|', '/').Replace('\r', ' ').Replace('\n', ' '); + } + + private static bool TryRunProbeProcess(string exePath, string resultPath, int timeoutMs, out int exitCode, out string status) + { + exitCode = -1; + status = string.Empty; + + STARTUPINFO startupInfo = new STARTUPINFO + { + cb = Marshal.SizeOf(), + dwFlags = STARTF_USESHOWWINDOW | STARTF_FORCEOFFFEEDBACK, + wShowWindow = SW_HIDE, + }; + + StringBuilder commandLine = new StringBuilder(QuoteCommandLineArgument(exePath) + " " + + QuoteCommandLineArgument(ProbeArgument) + " " + + QuoteCommandLineArgument(resultPath)); + string workingDirectory = Path.GetDirectoryName(exePath); + + if (!CreateProcess(exePath, + commandLine, + IntPtr.Zero, + IntPtr.Zero, + false, + CREATE_NO_WINDOW, + IntPtr.Zero, + workingDirectory, + ref startupInfo, + out PROCESS_INFORMATION processInfo)) + { + status = "CreateProcess failed: " + Marshal.GetLastWin32Error(); + return false; + } + + try + { + uint waitResult = WaitForSingleObject(processInfo.hProcess, (uint)Math.Max(1, timeoutMs)); + if (waitResult == WAIT_TIMEOUT) + { + TerminateProcess(processInfo.hProcess, 1); + status = $"probe timeout after {timeoutMs}ms"; + return false; + } + + if (waitResult != WAIT_OBJECT_0) + { + status = "WaitForSingleObject failed: " + waitResult; + return false; + } + + if (GetExitCodeProcess(processInfo.hProcess, out uint nativeExitCode)) + { + exitCode = unchecked((int)nativeExitCode); + } + + return true; + } + finally + { + if (processInfo.hThread != IntPtr.Zero) + { + CloseHandle(processInfo.hThread); + } + + if (processInfo.hProcess != IntPtr.Zero) + { + CloseHandle(processInfo.hProcess); + } + } + } + + private static string QuoteCommandLineArgument(string value) + { + if (string.IsNullOrEmpty(value)) + { + return "\"\""; + } + + StringBuilder builder = new StringBuilder(); + builder.Append('"'); + int backslashCount = 0; + + foreach (char c in value) + { + if (c == '\\') + { + backslashCount++; + continue; + } + + if (c == '"') + { + builder.Append('\\', backslashCount * 2 + 1); + builder.Append('"'); + backslashCount = 0; + continue; + } + + if (backslashCount > 0) + { + builder.Append('\\', backslashCount); + backslashCount = 0; + } + + builder.Append(c); + } + + if (backslashCount > 0) + { + builder.Append('\\', backslashCount * 2); + } + + builder.Append('"'); + return builder.ToString(); + } + + private static void TryDeleteProbeFile(string resultPath) + { + try + { + if (!string.IsNullOrEmpty(resultPath) && File.Exists(resultPath)) + { + File.Delete(resultPath); + } + } + catch + { + } + } + + private static bool TryGetGameBarApiStateOutOfProcess(int timeoutMs, out bool visible, out bool inputRedirected, out string status) + { + visible = false; + inputRedirected = false; + + string exePath = Environment.ProcessPath; + if (string.IsNullOrEmpty(exePath)) + { + try + { + exePath = Process.GetCurrentProcess().MainModule?.FileName; + } + catch + { + exePath = string.Empty; + } + } + + if (string.IsNullOrEmpty(exePath) || !File.Exists(exePath)) + { + status = "probe exe not found"; + return false; + } + + string resultPath = Path.Combine(Path.GetTempPath(), "DS4Windows.GameBarProbe." + Guid.NewGuid().ToString("N") + ".txt"); + try + { + if (!TryRunProbeProcess(exePath, resultPath, timeoutMs, out int exitCode, out string launchStatus)) + { + status = launchStatus; + return false; + } + + if (!File.Exists(resultPath)) + { + status = $"probe exited {exitCode} without result"; + return false; + } + + string result = File.ReadAllText(resultPath, Encoding.UTF8); + if (!TryParseProbeResult(result, out bool supported, out visible, out inputRedirected, out status)) + { + status = "invalid probe result: " + TruncateDiagnosticText(result); + return false; + } + + return supported; + } + catch (Exception ex) + { + status = ex.GetType().Name + ": " + ex.Message; + return false; + } + finally + { + TryDeleteProbeFile(resultPath); + } + } + private static bool IsGameBarVisibleByAutomation() { try @@ -403,17 +995,23 @@ private static bool LooksLikeGameBarAutomationElement(AutomationElement element) return false; } - bool processLooksRight = IsGameBarRelatedProcessName(processName); - bool textLooksRight = name.IndexOf("Xbox Game Bar", StringComparison.OrdinalIgnoreCase) >= 0 || name.IndexOf("Game Bar", StringComparison.OrdinalIgnoreCase) >= 0 || automationId.IndexOf("GameBar", StringComparison.OrdinalIgnoreCase) >= 0 || - automationId.IndexOf("Xbox", StringComparison.OrdinalIgnoreCase) >= 0 || + automationId.IndexOf("XboxGameBar", StringComparison.OrdinalIgnoreCase) >= 0 || className.IndexOf("GameBar", StringComparison.OrdinalIgnoreCase) >= 0 || - className.IndexOf("Xbox", StringComparison.OrdinalIgnoreCase) >= 0; + className.IndexOf("XboxGameBar", StringComparison.OrdinalIgnoreCase) >= 0; - return processLooksRight || (textLooksRight && !IsKnownNoisyProcessName(processName)); + if (textLooksRight) + { + return true; + } + + return IsStrictGameBarProcessName(processName) && + (className.IndexOf("Xaml", StringComparison.OrdinalIgnoreCase) >= 0 || + className.IndexOf("CoreWindow", StringComparison.OrdinalIgnoreCase) >= 0 || + automationId.IndexOf("GameBar", StringComparison.OrdinalIgnoreCase) >= 0); } private static bool IsAutomationDiagnosticCandidate(string processName, string name, string className, string automationId) @@ -475,8 +1073,14 @@ private static bool SafeGetResponding(Process process) } private static bool HasGameBarChildWindow(IntPtr parentWindow) + { + return HasGameBarChildWindow(parentWindow, out _); + } + + private static bool HasGameBarChildWindow(IntPtr parentWindow, out string summary) { bool found = false; + string matchSummary = string.Empty; EnumChildWindows(parentWindow, (hWnd, lParam) => { @@ -485,42 +1089,71 @@ private static bool HasGameBarChildWindow(IntPtr parentWindow) return true; } - if (LooksLikeGameBarWindow(hWnd)) + if (LooksLikeGameBarWindow(hWnd, out string windowReason)) { found = true; + matchSummary = "child " + windowReason; return false; } return true; }, IntPtr.Zero); + summary = matchSummary; return found; } private static bool LooksLikeGameBarWindow(IntPtr hWnd) + { + return LooksLikeGameBarWindow(hWnd, out _); + } + + private static bool LooksLikeGameBarWindow(IntPtr hWnd, out string summary) { string title = GetWindowTitle(hWnd); string className = GetWindowClassName(hWnd); string processName = GetProcessName(hWnd); + summary = $"hwnd=0x{hWnd.ToInt64():X} proc='{TruncateDiagnosticText(processName)}' class='{TruncateDiagnosticText(className)}' title='{TruncateDiagnosticText(title)}'"; - bool processLooksRight = - processName.Equals("GameBar", StringComparison.OrdinalIgnoreCase) || - processName.Equals("XboxGameBar", StringComparison.OrdinalIgnoreCase) || - processName.Equals("GameBarFTServer", StringComparison.OrdinalIgnoreCase) || - processName.Equals("GameBarWidgets", StringComparison.OrdinalIgnoreCase) || - processName.Equals("GameBarElevatedFT_Alias", StringComparison.OrdinalIgnoreCase); + bool trustedProcess = IsStrictGameBarProcessName(processName) || + processName.Equals("ApplicationFrameHost", StringComparison.OrdinalIgnoreCase) || + processName.Equals("ShellExperienceHost", StringComparison.OrdinalIgnoreCase); - bool titleLooksRight = + if (!trustedProcess) + { + return false; + } + + bool titleExplicitlyGameBar = title.IndexOf("Xbox Game Bar", StringComparison.OrdinalIgnoreCase) >= 0 || - title.IndexOf("Game Bar", StringComparison.OrdinalIgnoreCase) >= 0; + title.Equals("Game Bar", StringComparison.OrdinalIgnoreCase); - bool classLooksRight = + bool classExplicitlyGameBar = className.IndexOf("GameBar", StringComparison.OrdinalIgnoreCase) >= 0 || - className.IndexOf("Xbox", StringComparison.OrdinalIgnoreCase) >= 0 || - className.IndexOf("Xaml", StringComparison.OrdinalIgnoreCase) >= 0; + className.IndexOf("XboxGameBar", StringComparison.OrdinalIgnoreCase) >= 0; + + if (titleExplicitlyGameBar || classExplicitlyGameBar) + { + return true; + } - return (processLooksRight && (titleLooksRight || classLooksRight)) || - titleLooksRight; + bool strictProcessGenericOverlayWindow = + IsStrictGameBarProcessName(processName) && + (className.IndexOf("Xaml", StringComparison.OrdinalIgnoreCase) >= 0 || + className.IndexOf("CoreWindow", StringComparison.OrdinalIgnoreCase) >= 0 || + className.IndexOf("ApplicationFrame", StringComparison.OrdinalIgnoreCase) >= 0); + + return strictProcessGenericOverlayWindow; + } + + private static bool IsStrictGameBarProcessName(string processName) + { + return processName.Equals("GameBar", StringComparison.OrdinalIgnoreCase) || + processName.Equals("XboxGameBar", StringComparison.OrdinalIgnoreCase) || + processName.Equals("GameBarFTServer", StringComparison.OrdinalIgnoreCase) || + processName.Equals("GameBarWidgets", StringComparison.OrdinalIgnoreCase) || + processName.Equals("XboxGameBarWidgets", StringComparison.OrdinalIgnoreCase) || + processName.Equals("GameBarElevatedFT_Alias", StringComparison.OrdinalIgnoreCase); } private static bool IsInspectableWindow(IntPtr hWnd) diff --git a/DS4Windows/DS4Control/HidHideAPIDevice.cs b/DS4Windows/DS4Control/HidHideAPIDevice.cs index 19bf67c..383643f 100644 --- a/DS4Windows/DS4Control/HidHideAPIDevice.cs +++ b/DS4Windows/DS4Control/HidHideAPIDevice.cs @@ -35,15 +35,23 @@ class HidHideAPIDevice : IDisposable private const uint IOCTL_SET_ACTIVE = 0x80016014; private const uint IOCTL_GET_WL_INVERT = 0x80016018; private const uint IOCTL_SET_WL_INVERT = 0x8001601C; + private const uint IOCTL_ADD_SESSION_BLACKLIST = 0x80016020; + private const uint IOCTL_CLR_SESSION_BLACKLIST = 0x80016024; private const string CONTROL_DEVICE_FILENAME = "\\\\.\\HidHide"; private SafeHandle hidHideHandle; - public HidHideAPIDevice() + public HidHideAPIDevice(bool writeAccess = true) { + uint desiredAccess = NativeMethods.GENERIC_READ; + if (writeAccess) + { + desiredAccess |= NativeMethods.GENERIC_WRITE; + } + hidHideHandle = NativeMethods.CreateFile(CONTROL_DEVICE_FILENAME, - NativeMethods.GENERIC_READ, + desiredAccess, NativeMethods.FILE_SHARE_READ | NativeMethods.FILE_SHARE_WRITE, IntPtr.Zero, NativeMethods.OpenExisting, @@ -159,6 +167,45 @@ public bool SetBlacklist(List instances) return result; } + /// + /// Adds device instance paths to a process-lifetime blacklist. + /// Entries are automatically removed by HidHide when this process exits, + /// regardless of whether the exit is clean or due to a crash. + /// Requires HidHide with session blacklist support (v1.5+). + /// + public bool AddSessionBlacklist(List instances) + { + if (instances == null || instances.Count == 0) return true; + + int bytesReturned = 0; + IntPtr inBuffer = StringListToMultiSzPointer(instances, out int inBufferLength); + + bool result = NativeMethods.DeviceIoControl(hidHideHandle.DangerousGetHandle(), + IOCTL_ADD_SESSION_BLACKLIST, + inBuffer, + inBufferLength, + IntPtr.Zero, + 0, + ref bytesReturned, + IntPtr.Zero); + + Marshal.FreeHGlobal(inBuffer); + return result; + } + + /// + /// Removes all session blacklist entries registered by this process. + /// Called automatically by HidHide on process exit; only needed for explicit early release. + /// + public bool ClearSessionBlacklist() + { + int bytesReturned = 0; + return NativeMethods.DeviceIoControl(hidHideHandle.DangerousGetHandle(), + IOCTL_CLR_SESSION_BLACKLIST, + IntPtr.Zero, 0, IntPtr.Zero, 0, + ref bytesReturned, IntPtr.Zero); + } + public List GetWhitelist() { List instances = new List(); diff --git a/DS4Windows/DS4Control/Mapping.cs b/DS4Windows/DS4Control/Mapping.cs index 1d86600..43a5621 100644 --- a/DS4Windows/DS4Control/Mapping.cs +++ b/DS4Windows/DS4Control/Mapping.cs @@ -106,6 +106,28 @@ public ControlToXInput(DS4Controls input, DS4Controls output) new Queue(), new Queue(), }; + private class ProfileSwitchRequest + { + public bool Pending; + public bool Running; + public bool TempProfile; + public bool LaunchProgram; + public string ProfileName = string.Empty; + public Action AfterLoad; + } + + private static readonly object[] profileSwitchRequestLocks = new object[Global.MAX_DS4_CONTROLLER_COUNT] + { + new object(), new object(), new object(), new object(), + new object(), new object(), new object(), new object(), + }; + + private static readonly ProfileSwitchRequest[] profileSwitchRequests = new ProfileSwitchRequest[Global.MAX_DS4_CONTROLLER_COUNT] + { + new ProfileSwitchRequest(), new ProfileSwitchRequest(), new ProfileSwitchRequest(), new ProfileSwitchRequest(), + new ProfileSwitchRequest(), new ProfileSwitchRequest(), new ProfileSwitchRequest(), new ProfileSwitchRequest(), + }; + struct DS4Vector2 { public double x; @@ -814,7 +836,6 @@ public class DeltaSettingsProcessorGroup 50, // DS4Controls.BLP 51, // DS4Controls.BRP }; - private static int macroEndIndex = DS4_CONTROL_MACRO_ARRAY_LEN - 1; // Special macros static bool altTabDone = true; @@ -827,9 +848,83 @@ public class DeltaSettingsProcessorGroup public static int prevmouseaccel = 0; private static double horizontalRemainder = 0.0, verticalRemainder = 0.0; public const int MOUSESPEEDFACTOR = 48; - private const double MOUSESTICKANTIOFFSET = 0.0128; - private const double MOUSESTICKMINVELOCITY = 67.5; - //private const double MOUSESTICKMINVELOCITY = 40.0; + + private static void RequestProfileSwitch(int device, string profileName, bool tempProfile, + bool launchProgram, ControlService ctrl, Action afterLoad = null) + { + if (device < 0 || device >= Global.MAX_DS4_CONTROLLER_COUNT) + { + return; + } + + lock (profileSwitchRequestLocks[device]) + { + ProfileSwitchRequest request = profileSwitchRequests[device]; + request.Pending = true; + request.TempProfile = tempProfile; + request.LaunchProgram = launchProgram; + request.ProfileName = profileName ?? string.Empty; + request.AfterLoad = afterLoad; + + if (request.Running) + { + return; + } + + request.Running = true; + } + + Task.Run(() => RunProfileSwitchRequests(device, ctrl)); + } + + private static void RunProfileSwitchRequests(int device, ControlService ctrl) + { + while (true) + { + bool tempProfile; + bool launchProgram; + string profileName; + Action afterLoad; + + lock (profileSwitchRequestLocks[device]) + { + ProfileSwitchRequest request = profileSwitchRequests[device]; + if (!request.Pending) + { + request.Running = false; + return; + } + + request.Pending = false; + tempProfile = request.TempProfile; + launchProgram = request.LaunchProgram; + profileName = request.ProfileName; + afterLoad = request.AfterLoad; + request.AfterLoad = null; + } + + bool loaded = false; + try + { + loaded = tempProfile ? + LoadTempProfile(device, profileName, launchProgram, ctrl) : + LoadProfile(device, launchProgram, ctrl); + } + catch (Exception ex) + { + AppLogger.LogToGui($"Profile switch action failed: {ex.Message}", false); + } + + try + { + afterLoad?.Invoke(loaded); + } + catch (Exception ex) + { + AppLogger.LogToGui($"Profile switch post-load action failed: {ex.Message}", false); + } + } + } public static void Commit(int device) { @@ -1087,12 +1182,6 @@ public static int DS4ControltoInt(DS4Controls ctrl) return result; } - static double TValue(double value1, double value2, double percent) - { - percent /= 100f; - return value1 * percent + value2 * (1 - percent); - } - private static int ClampInt(int min, int value, int max) { return (value < min) ? min : (value > max) ? max : value; @@ -3112,14 +3201,14 @@ private static void ProcessTwoStageTrigger(int device, DS4State cState, byte tri switch (outputSettings.twoStageMode) { case TwoStageTriggerMode.Normal: - if (triggerRawValue == 255) + if (DS4StateFieldMapping.IsTriggerFullPull(triggerRawValue)) { dcsFullPull = inputFullPull; } break; case TwoStageTriggerMode.ExclusiveButtons: - if (triggerRawValue == 255) + if (DS4StateFieldMapping.IsTriggerFullPull(triggerRawValue)) { dcsFullPull = inputFullPull; dcsTemp = null; @@ -3145,7 +3234,7 @@ private static void ProcessTwoStageTrigger(int device, DS4State cState, byte tri triggerData.actionStateMode = TwoStageTriggerMappingData.EngageButtonsMode.Both; - if (triggerRawValue == 255) + if (DS4StateFieldMapping.IsTriggerFullPull(triggerRawValue)) { // Full pull now activates both. Soft pull action // no longer engaged with threshold @@ -3189,7 +3278,7 @@ private static void ProcessTwoStageTrigger(int device, DS4State cState, byte tri { triggerData.outputActive = true; - if (triggerRawValue == 255) + if (DS4StateFieldMapping.IsTriggerFullPull(triggerRawValue)) { dcsFullPull = inputFullPull; triggerData.fullPullActActive = true; @@ -3206,7 +3295,7 @@ private static void ProcessTwoStageTrigger(int device, DS4State cState, byte tri else if (triggerData.outputActive) { //DS4State pState = d.getPreviousStateRef(); - if (triggerRawValue == 255) + if (DS4StateFieldMapping.IsTriggerFullPull(triggerRawValue)) { dcsFullPull = inputFullPull; triggerData.fullPullActActive = true; @@ -3249,7 +3338,7 @@ private static void ProcessTwoStageTrigger(int device, DS4State cState, byte tri { triggerData.outputActive = true; - if (triggerRawValue == 255) + if (DS4StateFieldMapping.IsTriggerFullPull(triggerRawValue)) { dcsFullPull = inputFullPull; triggerData.fullPullActActive = true; @@ -3268,7 +3357,7 @@ private static void ProcessTwoStageTrigger(int device, DS4State cState, byte tri else if (triggerData.outputActive) { //DS4State pState = d.getPreviousStateRef(); - if (triggerRawValue == 255 && + if (DS4StateFieldMapping.IsTriggerFullPull(triggerRawValue) && triggerData.actionStateMode == TwoStageTriggerMappingData.EngageButtonsMode.FullPullOnly) { dcsFullPull = inputFullPull; @@ -3945,11 +4034,6 @@ private static void RunLightbarMacro(ObservableCollection } } - private static bool IfAxisIsNotModified(int device, bool shift, DS4Controls dc) - { - return shift ? false : GetDS4CSetting(device, dc).actionType == DS4ControlSettings.ActionType.Default; - } - private static async void MapCustomAction(int device, DS4State cState, DS4State MappedState, DS4StateExposed eState, Mouse tp, ControlService ctrl, DS4StateFieldMapping fieldMapping, DS4StateFieldMapping outputfieldMapping) { @@ -4213,29 +4297,23 @@ private static async void MapCustomAction(int device, DS4State cState, DS4State AppLogger.LogToGui(prolog, false); if (Global.ProfileChangedNotification) AppLogger.LogToTray(prolog); - Task.Run(() => + RequestProfileSwitch(device, action.details, true, true, ctrl, loaded => { - d.HaltReportingRunAction(() => + if (loaded && action.uTrigger.Count == 0 && !action.automaticUntrigger) { - LoadTempProfile(device, action.details, true, ctrl); - - //LoadProfile(device, false, ctrl); - - if (action.uTrigger.Count == 0 && !action.automaticUntrigger) + // If the new profile has any actions with the same action key (controls) than this action + // then set status of those actions to wait for the release of the existing action key. + List profileActionsNext = getProfileActions(device); + for (int actionIndexNext = 0, profileListLenNext = profileActionsNext.Count; actionIndexNext < profileListLenNext; actionIndexNext++) { - // If the new profile has any actions with the same action key (controls) than this action (which doesn't have untrigger keys) then set status of those actions to wait for the release of the existing action key. - List profileActionsNext = getProfileActions(device); - for (int actionIndexNext = 0, profileListLenNext = profileActionsNext.Count; actionIndexNext < profileListLenNext; actionIndexNext++) - { - string actionnameNext = profileActionsNext[actionIndexNext]; - SpecialAction actionNext = GetProfileAction(device, actionnameNext); - int indexNext = GetProfileActionIndexOf(device, actionnameNext); + string actionnameNext = profileActionsNext[actionIndexNext]; + SpecialAction actionNext = GetProfileAction(device, actionnameNext); + int indexNext = GetProfileActionIndexOf(device, actionnameNext); - if (actionNext.controls == action.controls) - actionDone[indexNext].dev[device] = true; - } + if (indexNext >= 0 && actionNext.controls == action.controls) + actionDone[indexNext].dev[device] = true; } - }); + } }); return; @@ -4706,9 +4784,9 @@ private static async void MapCustomAction(int device, DS4State cState, DS4State untriggeraction[device] = null; if (profileName == string.Empty) - LoadProfile(device, false, ctrl); // Previous profile was a regular default profile of a controller + RequestProfileSwitch(device, string.Empty, false, false, ctrl); // Previous profile was a regular default profile of a controller else - LoadTempProfile(device, profileName, true, ctrl); // Previous profile was a temporary profile, so re-load it as a temp profile + RequestProfileSwitch(device, profileName, true, true, ctrl); // Previous profile was a temporary profile, so re-load it as a temp profile } } } @@ -4961,14 +5039,6 @@ private static bool PlayMacroCodeValue(int device, bool[] macrocontrol, DS4KeyTy return doDelayOnCaller; } - private static void EndMacro(int device, bool[] macrocontrol, string macro, DS4Controls control) - { - if ((macro.StartsWith("164/9/9/164") || macro.StartsWith("18/9/9/18")) && !altTabDone) - AltTabSwappingRelease(); - - if (control != DS4Controls.None) - macrodone[DS4ControltoInt(control)] = false; - } private static void EndMacro(int device, bool[] macrocontrol, List macro, DS4Controls control) { diff --git a/DS4Windows/DS4Control/Mouse.cs b/DS4Windows/DS4Control/Mouse.cs index 085676a..c94ba8d 100644 --- a/DS4Windows/DS4Control/Mouse.cs +++ b/DS4Windows/DS4Control/Mouse.cs @@ -1515,16 +1515,6 @@ private bool isRight(Touch t) return t.HwX >= 1920 * 2 / 5; } - private void AddEmptyTrackballEntry() - { - int iIndex = trackballBufferTail; - trackballXBuffer[iIndex] = 0; - trackballYBuffer[iIndex] = 0; - trackballBufferTail = (iIndex + 1) % TRACKBALL_BUFFER_LEN; - if (trackballBufferHead == trackballBufferTail) - trackballBufferHead = (trackballBufferHead + 1) % TRACKBALL_BUFFER_LEN; - } - private void ClearTouchMouseTrackballData() { Array.Clear(trackballXBuffer, 0, TRACKBALL_BUFFER_LEN); diff --git a/DS4Windows/DS4Control/OpenRGBServer.cs b/DS4Windows/DS4Control/OpenRGBServer.cs new file mode 100644 index 0000000..3824234 --- /dev/null +++ b/DS4Windows/DS4Control/OpenRGBServer.cs @@ -0,0 +1,326 @@ +/* +DS4Windows +Copyright (C) 2023 Travis Nickles + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +using System; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using DS4Windows; + +namespace DS4WinWPF.DS4Control +{ + // Lightweight OpenRGB SDK server (protocol v4). + // + // Add DS4Windows as a target in OpenRGB via Settings > SDK Client tab, + // entering host=localhost port=6743. DS4 controller slots appear as + // gamepad devices; OpenRGB can set lightbar colours which DS4LightBar + // reads via TryGetColor(slot, out color). + public sealed class OpenRGBServer : IDisposable + { + private static readonly Lazy _instance = + new Lazy(() => new OpenRGBServer()); + public static OpenRGBServer Instance => _instance.Value; + + private const int PROTOCOL_VERSION = 4; + private const int MAX_SLOTS = Global.MAX_DS4_CONTROLLER_COUNT; + + private readonly DS4Color[] pendingColors = new DS4Color[MAX_SLOTS]; + private readonly bool[] hasPendingColor = new bool[MAX_SLOTS]; + private readonly object colorLock = new object(); + + private TcpListener listener; + private volatile bool running; + + public bool IsRunning => running; + + private OpenRGBServer() { } + + public bool Start(int port = 6743) + { + if (running) Stop(); + try + { + listener = new TcpListener(IPAddress.Any, port); + listener.Start(); + running = true; + new Thread(AcceptLoop) { IsBackground = true, Name = "OpenRGBServerAccept" }.Start(); + return true; + } + catch + { + listener = null; + return false; + } + } + + public void Stop() + { + running = false; + try { listener?.Stop(); } catch { } + listener = null; + lock (colorLock) + { + for (int i = 0; i < MAX_SLOTS; i++) + hasPendingColor[i] = false; + } + } + + public bool TryGetColor(int slot, out DS4Color color) + { + if (slot < 0 || slot >= MAX_SLOTS || !running) + { + color = default; + return false; + } + lock (colorLock) + { + if (!hasPendingColor[slot]) + { + color = default; + return false; + } + color = pendingColors[slot]; + return true; + } + } + + private void AcceptLoop() + { + while (running) + { + try + { + TcpClient client = listener.AcceptTcpClient(); + new Thread(() => HandleClient(client)) + { + IsBackground = true, + Name = "OpenRGBServerClient" + }.Start(); + } + catch + { + if (!running) break; + Thread.Sleep(500); + } + } + } + + private void HandleClient(TcpClient client) + { + try + { + client.ReceiveTimeout = 0; + using NetworkStream stream = client.GetStream(); + + while (running && client.Connected) + { + byte[] header = ReadExact(stream, 16); + if (header == null) break; + + if (header[0] != 'O' || header[1] != 'R' || header[2] != 'G' || header[3] != 'B') + break; + + uint devIdx = BitConverter.ToUInt32(header, 4); + uint pktId = BitConverter.ToUInt32(header, 8); + uint dataSize = BitConverter.ToUInt32(header, 12); + + byte[] payload = dataSize > 0 ? ReadExact(stream, (int)dataSize) : Array.Empty(); + if (payload == null) break; + + ProcessPacket(stream, devIdx, pktId, payload); + } + } + catch { } + finally { client.Close(); } + } + + private void ProcessPacket(NetworkStream stream, uint devIdx, uint pktId, byte[] payload) + { + switch (pktId) + { + case 0: // REQUEST_CONTROLLER_COUNT + SendPacket(stream, 0, 0, BitConverter.GetBytes((uint)MAX_SLOTS)); + break; + + case 1: // REQUEST_CONTROLLER_DATA + if (devIdx < MAX_SLOTS) + { + DS4Color current; + lock (colorLock) + current = hasPendingColor[devIdx] + ? pendingColors[devIdx] + : new DS4Color(0, 0, 255); + SendPacket(stream, devIdx, 1, BuildDeviceData((int)devIdx, current)); + } + break; + + case 40: // REQUEST_PROTOCOL_VERSION + SendPacket(stream, 0, 40, BitConverter.GetBytes((uint)PROTOCOL_VERSION)); + break; + + case 50: // SET_CLIENT_NAME + break; + + case 1050: // UPDATELEDS + if (devIdx < MAX_SLOTS && payload.Length >= 10) + { + int numColors = BitConverter.ToUInt16(payload, 4); + if (numColors > 0 && payload.Length >= 6 + numColors * 4) + SetSlotColor((int)devIdx, payload[6], payload[7], payload[8]); + } + break; + + case 1051: // UPDATEZONELEDS + if (devIdx < MAX_SLOTS && payload.Length >= 14) + { + int numColors = BitConverter.ToUInt16(payload, 8); + if (numColors > 0 && payload.Length >= 10 + numColors * 4) + SetSlotColor((int)devIdx, payload[10], payload[11], payload[12]); + } + break; + + case 1052: // UPDATESINGLELED + if (devIdx < MAX_SLOTS && payload.Length >= 8) + SetSlotColor((int)devIdx, payload[4], payload[5], payload[6]); + break; + + case 1100: // SETCUSTOMMODE + break; + } + } + + private void SetSlotColor(int slot, byte r, byte g, byte b) + { + lock (colorLock) + { + pendingColors[slot] = new DS4Color(r, g, b); + hasPendingColor[slot] = true; + } + } + + private static void SendPacket(NetworkStream stream, uint devIdx, uint pktId, byte[] data) + { + byte[] header = new byte[16]; + header[0] = (byte)'O'; header[1] = (byte)'R'; header[2] = (byte)'G'; header[3] = (byte)'B'; + BitConverter.GetBytes(devIdx).CopyTo(header, 4); + BitConverter.GetBytes(pktId).CopyTo(header, 8); + BitConverter.GetBytes((uint)(data?.Length ?? 0)).CopyTo(header, 12); + stream.Write(header, 0, 16); + if (data != null && data.Length > 0) + stream.Write(data, 0, data.Length); + } + + private static byte[] ReadExact(NetworkStream stream, int count) + { + byte[] buf = new byte[count]; + int read = 0; + while (read < count) + { + int n = stream.Read(buf, read, count - read); + if (n == 0) return null; + read += n; + } + return buf; + } + + private static byte[] BuildDeviceData(int slot, DS4Color currentColor) + { + using MemoryStream ms = new MemoryStream(); + using BinaryWriter w = new BinaryWriter(ms, Encoding.UTF8, leaveOpen: true); + + w.Write((uint)0); // total data_size placeholder + w.Write((uint)14); // device type: gamepad + + WriteString(w, $"DS4 Slot {slot + 1}"); + WriteString(w, "Sony"); + WriteString(w, "DualShock 4 Lightbar"); + WriteString(w, "1.0"); + WriteString(w, ""); + WriteString(w, ""); + + w.Write((int)0); // active_mode index + w.Write((uint)1); // num_modes + WriteMode(w); + w.Write((uint)1); // num_zones + WriteZone(w); + w.Write((uint)1); // num_leds + WriteLed(w); + + w.Write((uint)1); // num_colors + w.Write(currentColor.red); + w.Write(currentColor.green); + w.Write(currentColor.blue); + w.Write((byte)0); // alpha + + byte[] result = ms.ToArray(); + BitConverter.GetBytes((uint)(result.Length - 4)).CopyTo(result, 0); + return result; + } + + private static void WriteMode(BinaryWriter w) + { + WriteString(w, "Static"); + w.Write((int)0); // value + w.Write((uint)0x20); // flags: MODE_FLAG_HAS_PER_LED_COLOR + w.Write((uint)0); // speed_min + w.Write((uint)0); // speed_max + w.Write((uint)0); // brightness_min + w.Write((uint)0); // brightness_max + w.Write((uint)0); // colors_min + w.Write((uint)0); // colors_max + w.Write((uint)0); // speed + w.Write((uint)0); // brightness + w.Write((uint)0); // direction + w.Write((uint)1); // color_mode: COLOR_MODE_PER_LED + w.Write((ushort)0); // num embedded mode colors + } + + private static void WriteZone(BinaryWriter w) + { + WriteString(w, "Lightbar"); + w.Write((uint)0); // type: ZONE_TYPE_SINGLE + w.Write((uint)1); // leds_min + w.Write((uint)1); // leds_max + w.Write((uint)1); // num_leds + w.Write((ushort)0); // matrix_len (no matrix) + } + + private static void WriteLed(BinaryWriter w) + { + WriteString(w, "Lightbar"); + w.Write((uint)0); // value + } + + private static void WriteString(BinaryWriter w, string s) + { + if (string.IsNullOrEmpty(s)) + { + w.Write((ushort)0); + return; + } + byte[] bytes = Encoding.UTF8.GetBytes(s); + w.Write((ushort)(bytes.Length + 1)); + w.Write(bytes); + w.Write((byte)0); + } + + public void Dispose() => Stop(); + } +} diff --git a/DS4Windows/DS4Control/OutputSlotManager.cs b/DS4Windows/DS4Control/OutputSlotManager.cs index f782952..8f8cb98 100644 --- a/DS4Windows/DS4Control/OutputSlotManager.cs +++ b/DS4Windows/DS4Control/OutputSlotManager.cs @@ -19,6 +19,7 @@ You should have received a copy of the GNU General Public License using System; using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics; using System.Linq; using System.Text; using System.Threading; @@ -101,9 +102,15 @@ public void ShutDown() public void Stop(bool immediate = false) { UnplugRemainingControllers(immediate); - while (RunningQueue) + Stopwatch queueWait = Stopwatch.StartNew(); + while (RunningQueue && queueWait.ElapsedMilliseconds < 2000) { - Thread.SpinWait(500); + Thread.Sleep(1); + } + + if (RunningQueue) + { + ControlService.StartupDiag("OutputSlotManager.Stop timed out waiting for queued output task"); } deviceDict.Clear(); @@ -156,12 +163,14 @@ private int FindEmptySlot() public void DeferredPlugin(OutputDevice outputDevice, int inIdx, string inDisplayString, OutputDevice[] outdevs, OutContType contType) { + ControlService.StartupDiag($"OutputSlotManager.DeferredPlugin enter inIdx={inIdx} contType={contType} outputNull={outputDevice == null}"); // releases ReaderWriterLockSlim when locker goes out of scope using WriteLocker locker = new WriteLocker(queueLocker); //queuedTasks++; //Action tempAction = new Action(() => { int slot = FindEmptySlot(); + ControlService.StartupDiag($"OutputSlotManager.DeferredPlugin emptySlot={slot + 1} inIdx={inIdx} contType={contType}"); if (slot != -1) { // Only relevant when Virtual Controller (Moonlight) support is on and @@ -171,23 +180,39 @@ public void DeferredPlugin(OutputDevice outputDevice, int inIdx, string inDispla if (contType == OutContType.DS4 && Global.UseMoonlight) { beforeVirtualDS4 = DS4Devices.SnapshotBeforeOwnVirtualDS4(); + DS4Devices.BeginOwnVirtualDS4Connect(); } try { + ControlService.StartupDiag($"OutputSlotManager.Connect begin slot={slot + 1} type={contType} output={outputDevice.GetType().Name}"); outputDevice.Connect(); + ControlService.StartupDiag($"OutputSlotManager.Connect end slot={slot + 1} type={contType}"); } catch (Win32Exception e) { + ControlService.StartupDiag($"OutputSlotManager.Connect Win32Exception slot={slot + 1} type={contType} error={e.ErrorCode} message={e.Message}"); // Leave task immediately if connect call failed //queuedTasks--; ViGEmFailure?.Invoke(this, e.ErrorCode); + if (beforeVirtualDS4 != null) + { + DS4Devices.EndOwnVirtualDS4Connect(); + } + return; } if (beforeVirtualDS4 != null) { - DS4Devices.RegisterOwnVirtualDS4(beforeVirtualDS4); + try + { + DS4Devices.RegisterOwnVirtualDS4(beforeVirtualDS4); + } + finally + { + DS4Devices.EndOwnVirtualDS4Connect(); + } } if (contType == OutContType.X360) @@ -210,6 +235,11 @@ public void DeferredPlugin(OutputDevice outputDevice, int inIdx, string inDispla outputSlots[slot].CurrentInputBound = OutSlotDevice.InputBound.Bound; } SlotAssigned?.Invoke(this, slot, outputSlots[slot]); + ControlService.StartupDiag($"OutputSlotManager.DeferredPlugin assigned slot={slot + 1} inIdx={inIdx} type={contType}"); + } + else + { + ControlService.StartupDiag($"OutputSlotManager.DeferredPlugin no empty slot inIdx={inIdx} type={contType}"); } }; @@ -220,6 +250,7 @@ public void DeferredRemoval(OutputDevice outputDevice, int inIdx, OutputDevice[] outdevs, bool immediate = false) { _ = immediate; + ControlService.StartupDiag($"OutputSlotManager.DeferredRemoval enter inIdx={inIdx} outputNull={outputDevice == null}"); // releases ReaderWriterLockSlim when locker goes out of scope using WriteLocker locker = new WriteLocker(queueLocker); @@ -228,13 +259,18 @@ public void DeferredRemoval(OutputDevice outputDevice, int inIdx, { if (revDeviceDict.TryGetValue(outputDevice, out int slot)) { + ControlService.StartupDiag($"OutputSlotManager.DeferredRemoval found slot={slot + 1} type={outputDevice.GetDeviceType()}"); //int slot = revDeviceDict[outputDevice]; outputDevices[slot] = null; deviceDict.Remove(slot); revDeviceDict.Remove(outputDevice); + ControlService.StartupDiag($"OutputSlotManager.RemoveFeedbacks begin slot={slot + 1}"); outputDevice.RemoveFeedbacks(); + ControlService.StartupDiag($"OutputSlotManager.RemoveFeedbacks end slot={slot + 1}"); + ControlService.StartupDiag($"OutputSlotManager.Disconnect begin slot={slot + 1}"); outputDevice.Disconnect(); + ControlService.StartupDiag($"OutputSlotManager.Disconnect end slot={slot + 1}"); if (inIdx != -1) { @@ -244,12 +280,17 @@ public void DeferredRemoval(OutputDevice outputDevice, int inIdx, outputSlots[slot].DetachDevice(); SlotUnassigned?.Invoke(this, slot, outputSlots[slot]); AppLogger.LogToGui($"Unplugging virtual {outputDevice.GetDeviceType()} Controller from output slot #{slot + 1}",false); + ControlService.StartupDiag($"OutputSlotManager.DeferredRemoval unassigned slot={slot + 1}"); //if (!immediate) //{ - // Task.Delay(DELAY_TIME).Wait(); + // Task.Delay(DELAY_TIME).Wait(); //} } + else + { + ControlService.StartupDiag("OutputSlotManager.DeferredRemoval output not found in reverse map"); + } }; //queuedTasks--; diff --git a/DS4Windows/DS4Control/ScpUtil.cs b/DS4Windows/DS4Control/ScpUtil.cs index 5f4fcca..575e7b9 100644 --- a/DS4Windows/DS4Control/ScpUtil.cs +++ b/DS4Windows/DS4Control/ScpUtil.cs @@ -34,6 +34,7 @@ You should have received a copy of the GNU General Public License using System.Runtime.InteropServices; using System.Security.Principal; using System.Text; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using System.Windows; @@ -558,6 +559,8 @@ public class Global public static string exeFileName = Path.GetFileName(exelocation); public static FileVersionInfo fileVersion = FileVersionInfo.GetVersionInfo(exelocation); public static string exeversion = fileVersion.FileVersion; + public static string exeDisplayVersion = string.IsNullOrWhiteSpace(fileVersion.ProductVersion) ? + exeversion : fileVersion.ProductVersion; public static ulong exeversionLong = (ulong)fileVersion.ProductMajorPart << 48 | (ulong)fileVersion.ProductMinorPart << 32 | (ulong)fileVersion.ProductBuildPart << 16; public static ulong fullExeVersionLong = exeversionLong | (ushort)fileVersion.ProductPrivatePart; @@ -626,6 +629,10 @@ public static bool IsWin10OrGreater() BLANK_VIGEMBUS_VERSION); public static Version minSupportedViGEmBusVersionInfo = new Version(MIN_SUPPORTED_VIGEMBUS_VERSION); public static bool hidHideInstalled = IsHidHideInstalled(); + + public static bool openRGBSyncEnabled = false; + public static int openRGBServerPort = 6743; + public static bool fakerInputInstalled = IsFakerInputInstalled(); public const string BLANK_FAKERINPUT_VERSION = "0.0.0.0"; public static string fakerInputVersion = FakerInputVersion(); @@ -1367,7 +1374,6 @@ public static bool CheckIfVirtualDevice(string devicePath) { bool result = false; bool excludeMatchFound = false; - var instanceId = GetInstanceIdFromDevicePath(devicePath); var testInstanceId = instanceId; while (!string.IsNullOrEmpty(testInstanceId)) @@ -1685,6 +1691,12 @@ public static bool UseAdvancedMoonlight set => m_Config.useAdvancedMoonlight = value; } + public static bool VerboseStartupLogging + { + get => m_Config.verboseStartupLogging; + set => m_Config.verboseStartupLogging = value; + } + public static bool getQuickCharge() { return m_Config.quickCharge; @@ -3126,27 +3138,6 @@ public static DS4Color getTransitionedColor(ref DS4Color c1, ref DS4Color c2, do return cs; } - private static Color applyRatio(Color c1, Color c2, uint r) - { - float ratio = r / 100f; - float hue1 = c1.GetHue(); - float hue2 = c2.GetHue(); - float bri1 = c1.GetBrightness(); - float bri2 = c2.GetBrightness(); - float sat1 = c1.GetSaturation(); - float sat2 = c2.GetSaturation(); - float hr = hue2 - hue1; - float br = bri2 - bri1; - float sr = sat2 - sat1; - Color csR; - if (bri1 == 0) - csR = HuetoRGB(hue2, sat2, bri2 - br * ratio); - else - csR = HuetoRGB(hue2 - hr * ratio, sat2 - sr * ratio, bri2 - br * ratio); - - return csR; - } - public static Color HuetoRGB(float hue, float sat, float bri) { float C = (1 - Math.Abs(2 * bri) - 1) * sat; @@ -3192,10 +3183,6 @@ public static double Clamp(double min, double value, double max) return (value < min) ? min : (value > max) ? max : value; } - private static int ClampInt(int min, int value, int max) - { - return (value < min) ? min : (value > max) ? max : value; - } public static void InitOutputKBMHandler(string identifier) { @@ -3366,12 +3353,29 @@ public static IEnumerable GrabCurrentMonitors() public class Changelog { - public const string GITHUB_RELEASES_API_URI = "https://api.github.com/repos/schmaldeo/DS4Windows/releases"; - public const string GITHUB_LATEST_RELEASE_API_URI = "https://api.github.com/repos/schmaldeo/DS4Windows/releases/latest"; + public const string GITHUB_RELEASES_API_URI = "https://api.github.com/repos/hbashton/DS4Windows/releases"; + public const string GITHUB_LATEST_RELEASE_API_URI = "https://api.github.com/repos/hbashton/DS4Windows/releases/latest"; private static bool? _newerVersionAvailable = null; private static Version _latestVersion; + public static bool TryParseReleaseVersion(string tagName, out Version version) + { + version = Version.Parse("0.0.0"); + if (string.IsNullOrWhiteSpace(tagName)) return false; + + Match versionMatch = Regex.Match(tagName, @"\d+(?:\.\d+){1,3}"); + return versionMatch.Success && Version.TryParse(versionMatch.Value, out version); + } + + private static bool IsStableRelease(GithubRelease release) + { + if (release is null || release.PreRelease) return false; + + string tagName = release.TagName ?? string.Empty; + return !Regex.IsMatch(tagName, @"(?i)(alpha|beta|preview|pre-release|prerelease|rc)"); + } + // Much more compact and elegant way of checking if there is a new update available than the // shenanigans with fetching newest.txt and using a .txt file as a DTO instead of simply // passing a string to the function that displays the updater window. @@ -3390,15 +3394,21 @@ public static bool CheckNewerVersionExists(out Version version, bool allowCached return (bool)_newerVersionAvailable; } - var request = App.requestClient.GetAsync(GITHUB_LATEST_RELEASE_API_URI); + var request = App.requestClient.GetAsync(GITHUB_RELEASES_API_URI); request.Wait(); if (request.Result.IsSuccessStatusCode) { - var task = request.Result.Content.ReadFromJsonAsync(); + var task = request.Result.Content.ReadFromJsonAsync(); task.Wait(); - // if can't parse the newest version - if (!Version.TryParse(task.Result.TagName[1..], out version)) return false; + foreach (var release in task.Result ?? Array.Empty()) + { + if (!IsStableRelease(release)) continue; + if (!TryParseReleaseVersion(release.TagName, out var parsedVersion)) continue; + if (parsedVersion > version) version = parsedVersion; + } + + if (version <= Version.Parse("0.0.0")) return false; // if there is a newer version available if (currentVersion < version) @@ -3423,15 +3433,19 @@ public static async Task> GetChangelog(bool allVersi if (!Version.TryParse(Global.exeversion, out var currentVersion)) return dict; var request = await App.requestClient.GetAsync(GITHUB_RELEASES_API_URI); + if (!request.IsSuccessStatusCode) return dict; + var releases = await request.Content.ReadFromJsonAsync(); + if (releases is null) return dict; foreach (var release in releases) { - if (release.PreRelease) continue; - - if (!Version.TryParse(release.TagName[1..], out var parsedVersion)) continue; + if (!IsStableRelease(release)) continue; + if (!TryParseReleaseVersion(release.TagName, out var parsedVersion)) continue; if (!allVersions && parsedVersion <= currentVersion) break; + if (dict.ContainsKey(parsedVersion)) continue; + dict.Add(parsedVersion, release.Body); } @@ -3445,10 +3459,19 @@ public static async Task GetChangelogMarkdown(bool allVersions = false) StringBuilder sb = new(); foreach (var version in versions) { + var parsedChangelog = ParseChangelogString(version.Value); + if (string.IsNullOrWhiteSpace(parsedChangelog)) continue; + + if (sb.Length > 0) + { + sb.AppendLine(); + sb.AppendLine(); + } + sb.Append("## Version "); sb.Append(version.Key); - sb.Append(Environment.NewLine); - var parsedChangelog = ParseChangelogString(version.Value); + sb.AppendLine(); + sb.AppendLine(); sb.Append(parsedChangelog); } @@ -3457,9 +3480,17 @@ public static async Task GetChangelogMarkdown(bool allVersions = false) private static string ParseChangelogString(string changelog) { - var split = changelog.Split("\n").ToList(); + if (string.IsNullOrWhiteSpace(changelog)) return string.Empty; + + var split = changelog + .Replace("\r\n", "\n") + .Replace("\r", "\n") + .Split("\n") + .Select(x => x.TrimEnd()) + .ToList(); + split.RemoveAll(x => x.StartsWith("**Full Changelog**")); - return string.Join(Environment.NewLine, split); + return string.Join(Environment.NewLine, split).Trim(); } } @@ -3919,6 +3950,7 @@ public void setSZOutCurveMode(int index, int value) public bool quickCharge = false; public bool useMoonlight = false; public bool useAdvancedMoonlight = false; + public bool verboseStartupLogging = false; public bool closeMini = false; public List actions = new List(); public List[] ds4settings = new List[Global.TEST_PROFILE_ITEM_COUNT] @@ -4347,54 +4379,6 @@ private void PortOldGyroSettings(int device) } } - private string GetGyroOutModeString(GyroOutMode mode) - { - string result = "None"; - switch (mode) - { - case GyroOutMode.Controls: - result = "Controls"; - break; - case GyroOutMode.Mouse: - result = "Mouse"; - break; - case GyroOutMode.MouseJoystick: - result = "MouseJoystick"; - break; - case GyroOutMode.Passthru: - result = "Passthru"; - break; - default: - break; - } - - return result; - } - - private GyroOutMode GetGyroOutModeType(string modeString) - { - GyroOutMode result = GyroOutMode.None; - switch (modeString) - { - case "Controls": - result = GyroOutMode.Controls; - break; - case "Mouse": - result = GyroOutMode.Mouse; - break; - case "MouseJoystick": - result = GyroOutMode.MouseJoystick; - break; - case "Passthru": - result = GyroOutMode.Passthru; - break; - default: - break; - } - - return result; - } - private string GetLightbarModeString(LightbarMode mode) { string result = "DS4Win"; @@ -5420,12 +5404,12 @@ public bool LoadProfileNew(int device, bool launchprogram, ControlService contro } catch (InvalidOperationException e) { - AppLogger.LogToGui($"Failed to load {profilepath}. {e.InnerException.Message}", false); + AppLogger.LogToGui($"Failed to load {profilepath}. {e.InnerException?.Message ?? e.Message}", false); loaded = false; } catch (XmlException e) { - AppLogger.LogToGui($"Failed to load {profilepath}. Invalid XML. {e.InnerException.Message}", false); + AppLogger.LogToGui($"Failed to load {profilepath}. Invalid XML. {e.InnerException?.Message ?? e.Message}", false); loaded = false; } @@ -5495,8 +5479,8 @@ public bool LoadProfileNew(int device, bool launchprogram, ControlService contro } } - // Check if Touchpad should be switched off - if (startTouchpadOff[device] == true) control.StartTPOff(device); + // Reset the runtime touchpad movement toggle from the loaded profile. + control.SetTouchpadMovementActive(device, !startTouchpadOff[device]); { bool tempToggle = gyroControlsInf[device].triggerToggle; @@ -6548,9 +6532,14 @@ public bool LoadProfile(int device, bool launchprogram, ControlService control, { Item = m_Xdoc.SelectSingleNode("/" + rootname + "/StartTouchpadOff"); bool.TryParse(Item.InnerText, out startTouchpadOff[device]); - if (startTouchpadOff[device] == true) control.StartTPOff(device); + control.SetTouchpadMovementActive(device, !startTouchpadOff[device]); + } + catch + { + startTouchpadOff[device] = false; + control.SetTouchpadMovementActive(device, true); + missingSetting = true; } - catch { startTouchpadOff[device] = false; missingSetting = true; } // Fallback lookup if TouchpadOutMode is not set bool tpForControlsPresent = false; @@ -10544,6 +10533,9 @@ private void PostLoadSnippet(int device, ControlService control, bool xinputStat //Program.rootHub.touchPad[device]?.ResetTrackAccel(trackballFriction[device]); } + else + { + } } } diff --git a/DS4Windows/DS4Control/Xbox360OutDevice.cs b/DS4Windows/DS4Control/Xbox360OutDevice.cs index 6d24968..bad9e3b 100644 --- a/DS4Windows/DS4Control/Xbox360OutDevice.cs +++ b/DS4Windows/DS4Control/Xbox360OutDevice.cs @@ -194,7 +194,9 @@ private short AxisScale(Int32 Value, Boolean Flip) public override void Connect() { + ControlService.StartupDiag("Xbox360OutDevice.Connect cont.Connect begin"); cont.Connect(); + ControlService.StartupDiag("Xbox360OutDevice.Connect cont.Connect end"); connected = true; if (_features.HasFlag(X360Features.XInputSlotNum)) @@ -203,10 +205,13 @@ public override void Connect() Thread.Sleep(USER_INDEX_WAIT); try { + ControlService.StartupDiag("Xbox360OutDevice.Connect UserIndex begin"); XinputSlotNum = cont.UserIndex; + ControlService.StartupDiag($"Xbox360OutDevice.Connect UserIndex end slot={XinputSlotNum}"); } catch (Exception) { + ControlService.StartupDiag("Xbox360OutDevice.Connect UserIndex exception; disabling feature"); // Failed to grab xinput slot number. Set default // slot number and remove feature flag _xInputSlotNum = XINPUT_SLOT_NUM_DEFAULT; @@ -216,16 +221,34 @@ public override void Connect() } public override void Disconnect() { - foreach (KeyValuePair pair in forceFeedbacksDict) + ControlService.StartupDiag("Xbox360OutDevice.Disconnect begin"); + if (cont != null) { - cont.FeedbackReceived -= pair.Value; + foreach (KeyValuePair pair in forceFeedbacksDict) + { + cont.FeedbackReceived -= pair.Value; + } } forceFeedbacksDict.Clear(); connected = false; - cont.Disconnect(); - cont = null; + ControlService.StartupDiag("Xbox360OutDevice.Disconnect cont.Disconnect begin"); + try + { + cont?.Disconnect(); + ControlService.StartupDiag("Xbox360OutDevice.Disconnect cont.Disconnect end"); + } + catch (Exception ex) + { + ControlService.StartupDiag($"Xbox360OutDevice.Disconnect exception {ex.GetType().Name}: {ex.Message}"); + } + finally + { + cont = null; + } + + ControlService.StartupDiag("Xbox360OutDevice.Disconnect end"); } public override string GetDeviceType() => devType; @@ -240,9 +263,12 @@ public override void ResetState(bool submit=true) public override void RemoveFeedbacks() { - foreach (KeyValuePair pair in forceFeedbacksDict) + if (cont != null) { - cont.FeedbackReceived -= pair.Value; + foreach (KeyValuePair pair in forceFeedbacksDict) + { + cont.FeedbackReceived -= pair.Value; + } } forceFeedbacksDict.Clear(); @@ -250,7 +276,8 @@ public override void RemoveFeedbacks() public override void RemoveFeedback(int inIdx) { - if (forceFeedbacksDict.TryGetValue(inIdx, out Xbox360FeedbackReceivedEventHandler handler)) + if (cont != null && + forceFeedbacksDict.TryGetValue(inIdx, out Xbox360FeedbackReceivedEventHandler handler)) { cont.FeedbackReceived -= handler; forceFeedbacksDict.Remove(inIdx); diff --git a/DS4Windows/DS4Forms/About.xaml b/DS4Windows/DS4Forms/About.xaml index ed6484a..acc5b7f 100644 --- a/DS4Windows/DS4Forms/About.xaml +++ b/DS4Windows/DS4Forms/About.xaml @@ -9,7 +9,7 @@ Title="{lex:LocExtension HotkeysAbout}" Height="450" Width="800" Style="{DynamicResource WindowStyle}"> -