diff --git a/.github/workflows/build_performance_overlay.yaml b/.github/workflows/build_performance_overlay.yaml index 4ee391f..65a467a 100644 --- a/.github/workflows/build_performance_overlay.yaml +++ b/.github/workflows/build_performance_overlay.yaml @@ -1,3 +1,5 @@ +name: build-performance-overlay + on: workflow_dispatch: push: @@ -6,6 +8,8 @@ on: - "PerformanceOverlay/**" - "CommonHelpers/**" - "ExternalHelpers/**" + - "XboxGameBarWidget/**" + - "scripts/install_gamebar_widget.ps1" - "VERSION" pull_request: paths: @@ -13,14 +17,17 @@ on: - "PerformanceOverlay/**" - "CommonHelpers/**" - "ExternalHelpers/**" + - "XboxGameBarWidget/**" + - "scripts/install_gamebar_widget.ps1" - "VERSION" env: - DOTNET_VERSION: "6.0.x" + DOTNET_VERSION: "8.0.x" APP_NAME: PerformanceOverlay + WIDGET_NAME: SteamDeckToolsGameBarWidget jobs: - build-performance-overlay: + build: runs-on: windows-latest steps: - uses: actions/checkout@v4 @@ -30,27 +37,80 @@ jobs: with: dotnet-version: ${{ env.DOTNET_VERSION }} - - name: Set RELEASE_VERSION + - name: Setup MSBuild + uses: microsoft/setup-msbuild@v2 + + - name: Set versions shell: pwsh run: | $majorVersion = (Get-Content VERSION -Raw).Trim() $releaseVersion = "$majorVersion.${{ github.run_number }}" "RELEASE_VERSION=$releaseVersion" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + $segments = @($majorVersion.Split('.') | Where-Object { $_ -ne '' }) + switch ($segments.Count) { + 0 { $widgetVersion = "1.0.${{ github.run_number }}.0" } + 1 { $widgetVersion = "$($segments[0]).0.${{ github.run_number }}.0" } + 2 { $widgetVersion = "$($segments[0]).$($segments[1]).${{ github.run_number }}.0" } + default { $widgetVersion = "$($segments[0]).$($segments[1]).$($segments[2]).${{ github.run_number }}" } + } + "WIDGET_PACKAGE_VERSION=$widgetVersion" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + - name: Restore PerformanceOverlay dependencies run: dotnet restore PerformanceOverlay/PerformanceOverlay.csproj - name: Build PerformanceOverlay run: dotnet build PerformanceOverlay/PerformanceOverlay.csproj --configuration Release --output "${{ env.APP_NAME }}-${{ env.RELEASE_VERSION }}/" "/property:Version=${{ env.RELEASE_VERSION }}" "/property:ExtraDefineConstants=PRODUCTION_BUILD" - - name: Create zip artifact + - name: Create PerformanceOverlay zip artifact shell: pwsh run: | Compress-Archive -Path "${{ env.APP_NAME }}-${{ env.RELEASE_VERSION }}\*" -DestinationPath "${{ env.APP_NAME }}-${{ env.RELEASE_VERSION }}.zip" + - name: Build Game Bar widget package + shell: pwsh + run: | + $manifestPath = "XboxGameBarWidget/Package.appxmanifest" + $manifestContent = Get-Content $manifestPath -Raw + $manifestContent = [regex]::Replace( + $manifestContent, + '( - net6.0-windows + net8.0-windows enable true true @@ -9,7 +9,7 @@ $(DefineConstants);$(ExtraDefineConstants) - + diff --git a/CommonHelpers/Vlv0100.cs b/CommonHelpers/Vlv0100.cs index f39eee7..25bf9cc 100644 --- a/CommonHelpers/Vlv0100.cs +++ b/CommonHelpers/Vlv0100.cs @@ -208,7 +208,7 @@ } private void SetRampRate(byte rampRate) { - byte[] data = BitConverter.GetBytes(rampRate); + byte[] data = new byte[] { rampRate }; inpOut?.WriteMemory(FRPR, data); } } diff --git a/ExternalHelpers/ExternalHelpers.csproj b/ExternalHelpers/ExternalHelpers.csproj index 247d3ee..2da42be 100644 --- a/ExternalHelpers/ExternalHelpers.csproj +++ b/ExternalHelpers/ExternalHelpers.csproj @@ -1,7 +1,7 @@  - net6.0-windows + net8.0-windows enable true enable @@ -9,7 +9,7 @@ - + diff --git a/PerformanceOverlay/OverlayCommandLine.cs b/PerformanceOverlay/OverlayCommandLine.cs new file mode 100644 index 0000000..06770e3 --- /dev/null +++ b/PerformanceOverlay/OverlayCommandLine.cs @@ -0,0 +1,332 @@ +using CommonHelpers; + +namespace PerformanceOverlay +{ + internal enum OverlayControlResult + { + NotACommand, + HandledAndExit, + ContinueStartup + } + + internal enum OverlayControlCommandType + { + SetMode, + CycleMode, + ToggleVisibility, + Show, + Hide + } + + internal readonly struct OverlayControlCommand + { + public OverlayControlCommandType CommandType { get; } + public OverlayMode Mode { get; } + + private OverlayControlCommand(OverlayControlCommandType commandType, OverlayMode mode = OverlayMode.FPS) + { + CommandType = commandType; + Mode = mode; + } + + public static OverlayControlCommand SetMode(OverlayMode mode) + { + return new OverlayControlCommand(OverlayControlCommandType.SetMode, mode); + } + + public static OverlayControlCommand CycleMode() + { + return new OverlayControlCommand(OverlayControlCommandType.CycleMode); + } + + public static OverlayControlCommand ToggleVisibility() + { + return new OverlayControlCommand(OverlayControlCommandType.ToggleVisibility); + } + + public static OverlayControlCommand Show() + { + return new OverlayControlCommand(OverlayControlCommandType.Show); + } + + public static OverlayControlCommand Hide() + { + return new OverlayControlCommand(OverlayControlCommandType.Hide); + } + } + + internal static class OverlayCommandLine + { + internal const string ProtocolScheme = "steamdecktools-performanceoverlay"; + private const string RunOnceMutexName = "Global\\PerformanceOverlay"; + + public static OverlayControlResult HandleArgs(string[] args) + { + if (!TryParseCommand(args, out var command)) + return OverlayControlResult.NotACommand; + + if (TryDispatchToRunningInstance(command)) + return OverlayControlResult.HandledAndExit; + + // Avoid run-once fatal dialog if an existing instance is present but IPC failed. + if (IsPerformanceOverlayInstanceRunning()) + return OverlayControlResult.HandledAndExit; + + ApplyLocally(command); + return OverlayControlResult.ContinueStartup; + } + + private static bool TryParseCommand(string[] args, out OverlayControlCommand command) + { + command = default; + + foreach (var arg in args) + { + if (TryParseUriCommand(arg, out command)) + return true; + } + + return TryParseSwitchCommand(args, out command); + } + + private static bool TryParseUriCommand(string arg, out OverlayControlCommand command) + { + command = default; + + if (!Uri.TryCreate(arg, UriKind.Absolute, out var uri)) + return false; + + if (!uri.Scheme.Equals(ProtocolScheme, StringComparison.OrdinalIgnoreCase)) + return false; + + var segments = new List(); + if (!String.IsNullOrWhiteSpace(uri.Host)) + segments.Add(uri.Host); + + segments.AddRange( + uri.AbsolutePath + .Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + ); + + if (segments.Count == 0) + return false; + + var head = segments[0]; + if (head.Equals("cycle", StringComparison.OrdinalIgnoreCase)) + { + command = OverlayControlCommand.CycleMode(); + return true; + } + + if (head.Equals("toggle", StringComparison.OrdinalIgnoreCase)) + { + command = OverlayControlCommand.ToggleVisibility(); + return true; + } + + if (head.Equals("show", StringComparison.OrdinalIgnoreCase)) + { + command = OverlayControlCommand.Show(); + return true; + } + + if (head.Equals("hide", StringComparison.OrdinalIgnoreCase)) + { + command = OverlayControlCommand.Hide(); + return true; + } + + if ((head.Equals("mode", StringComparison.OrdinalIgnoreCase) || head.Equals("set", StringComparison.OrdinalIgnoreCase)) + && segments.Count > 1 + && TryParseMode(segments[1], out var uriMode)) + { + command = OverlayControlCommand.SetMode(uriMode); + return true; + } + + return false; + } + + private static bool TryParseSwitchCommand(string[] args, out OverlayControlCommand command) + { + command = default; + for (int i = 0; i < args.Length; i++) + { + var arg = args[i]; + if (arg.Equals("--cycle", StringComparison.OrdinalIgnoreCase)) + { + command = OverlayControlCommand.CycleMode(); + return true; + } + + if (arg.Equals("--toggle", StringComparison.OrdinalIgnoreCase)) + { + command = OverlayControlCommand.ToggleVisibility(); + return true; + } + + if (arg.Equals("--show", StringComparison.OrdinalIgnoreCase)) + { + command = OverlayControlCommand.Show(); + return true; + } + + if (arg.Equals("--hide", StringComparison.OrdinalIgnoreCase)) + { + command = OverlayControlCommand.Hide(); + return true; + } + + if (arg.StartsWith("--mode=", StringComparison.OrdinalIgnoreCase)) + { + var modeArg = arg["--mode=".Length..]; + if (TryParseMode(modeArg, out var inlineMode)) + { + command = OverlayControlCommand.SetMode(inlineMode); + return true; + } + } + + if (arg.Equals("--mode", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length && TryParseMode(args[i + 1], out var nextMode)) + { + command = OverlayControlCommand.SetMode(nextMode); + return true; + } + } + + return false; + } + + private static bool TryParseMode(string value, out OverlayMode mode) + { + if (Enum.TryParse(value, true, out mode)) + return true; + + var normalized = value.Trim().Replace("-", "").Replace("_", "").ToLowerInvariant(); + switch (normalized) + { + case "fpsbattery": + case "fpswithbattery": + mode = OverlayMode.FPSWithBattery; + return true; + default: + return false; + } + } + + private static bool TryDispatchToRunningInstance(OverlayControlCommand command) + { + try + { + using var sharedData = SharedData.OpenExisting(); + if (!sharedData.GetValue(out var value)) + value = sharedData.NewValue(); + + ApplyToSharedState(ref value, command); + return sharedData.SetValue(value); + } + catch (Exception ex) + { + Log.TraceLine("OverlayCommandLine: IPC dispatch failed: {0}", ex.Message); + return false; + } + } + + private static void ApplyToSharedState(ref OverlayModeSetting value, OverlayControlCommand command) + { + switch (command.CommandType) + { + case OverlayControlCommandType.SetMode: + value.Desired = command.Mode; + value.DesiredEnabled = OverlayEnabled.Yes; + return; + case OverlayControlCommandType.CycleMode: + value.Desired = NextMode(CurrentMode(value)); + value.DesiredEnabled = OverlayEnabled.Yes; + return; + case OverlayControlCommandType.ToggleVisibility: + value.DesiredEnabled = CurrentEnabled(value) == OverlayEnabled.Yes ? OverlayEnabled.No : OverlayEnabled.Yes; + return; + case OverlayControlCommandType.Show: + value.DesiredEnabled = OverlayEnabled.Yes; + return; + case OverlayControlCommandType.Hide: + value.DesiredEnabled = OverlayEnabled.No; + return; + default: + return; + } + } + + private static OverlayMode CurrentMode(OverlayModeSetting value) + { + if (Enum.IsDefined(value.Current)) + return value.Current; + if (Enum.IsDefined(value.Desired)) + return value.Desired; + return Settings.Default.OSDMode; + } + + private static OverlayEnabled CurrentEnabled(OverlayModeSetting value) + { + if (Enum.IsDefined(value.CurrentEnabled)) + return value.CurrentEnabled; + if (Enum.IsDefined(value.DesiredEnabled)) + return value.DesiredEnabled; + return Settings.Default.ShowOSD ? OverlayEnabled.Yes : OverlayEnabled.No; + } + + private static void ApplyLocally(OverlayControlCommand command) + { + switch (command.CommandType) + { + case OverlayControlCommandType.SetMode: + Settings.Default.OSDMode = command.Mode; + Settings.Default.ShowOSD = true; + return; + case OverlayControlCommandType.CycleMode: + Settings.Default.OSDMode = NextMode(Settings.Default.OSDMode); + Settings.Default.ShowOSD = true; + return; + case OverlayControlCommandType.ToggleVisibility: + Settings.Default.ShowOSD = !Settings.Default.ShowOSD; + return; + case OverlayControlCommandType.Show: + Settings.Default.ShowOSD = true; + return; + case OverlayControlCommandType.Hide: + Settings.Default.ShowOSD = false; + return; + default: + return; + } + } + + private static OverlayMode NextMode(OverlayMode current) + { + var values = Enum.GetValues(); + var currentIndex = Array.IndexOf(values, current); + if (currentIndex < 0) + currentIndex = 0; + + return values[(currentIndex + 1) % values.Length]; + } + + private static bool IsPerformanceOverlayInstanceRunning() + { + try + { + using var mutex = Mutex.OpenExisting(RunOnceMutexName); + return true; + } + catch (WaitHandleCannotBeOpenedException) + { + return false; + } + catch (UnauthorizedAccessException) + { + return true; + } + } + } +} diff --git a/PerformanceOverlay/PerformanceOverlay.csproj b/PerformanceOverlay/PerformanceOverlay.csproj index 8286234..4ffe322 100644 --- a/PerformanceOverlay/PerformanceOverlay.csproj +++ b/PerformanceOverlay/PerformanceOverlay.csproj @@ -2,7 +2,7 @@ WinExe - net6.0-windows + net8.0-windows enable true enable diff --git a/PerformanceOverlay/Program.cs b/PerformanceOverlay/Program.cs index 40eb031..b0b65f1 100644 --- a/PerformanceOverlay/Program.cs +++ b/PerformanceOverlay/Program.cs @@ -1,6 +1,5 @@ using CommonHelpers; -using RTSSSharedMemoryNET; -using System.Diagnostics; +using System.Linq; namespace PerformanceOverlay { @@ -11,6 +10,12 @@ namespace PerformanceOverlay { Instance.WithSentry(() => { + UriProtocolRegistration.EnsureRegistered(); + + var commandResult = OverlayCommandLine.HandleArgs(Environment.GetCommandLineArgs().Skip(1).ToArray()); + if (commandResult == OverlayControlResult.HandledAndExit) + return; + ApplicationConfiguration.Initialize(); using (var controller = new Controller()) @@ -20,4 +25,4 @@ namespace PerformanceOverlay }); } } -} \ No newline at end of file +} diff --git a/PerformanceOverlay/UriProtocolRegistration.cs b/PerformanceOverlay/UriProtocolRegistration.cs new file mode 100644 index 0000000..2943c2c --- /dev/null +++ b/PerformanceOverlay/UriProtocolRegistration.cs @@ -0,0 +1,37 @@ +using CommonHelpers; +using Microsoft.Win32; + +namespace PerformanceOverlay +{ + internal static class UriProtocolRegistration + { + private const string ProtocolKey = @"Software\Classes\steamdecktools-performanceoverlay"; + + public static void EnsureRegistered() + { + try + { + var exePath = Environment.ProcessPath; + if (String.IsNullOrWhiteSpace(exePath)) + return; + + using var protocol = Registry.CurrentUser.CreateSubKey(ProtocolKey); + if (protocol is null) + return; + + protocol.SetValue("", "URL:Steam Deck Tools Performance Overlay"); + protocol.SetValue("URL Protocol", ""); + + using var icon = protocol.CreateSubKey("DefaultIcon"); + icon?.SetValue("", "\"" + exePath + "\",0"); + + using var command = protocol.CreateSubKey(@"shell\open\command"); + command?.SetValue("", "\"" + exePath + "\" \"%1\""); + } + catch (Exception ex) + { + Log.TraceLine("UriProtocolRegistration: {0}", ex.Message); + } + } + } +} diff --git a/README.md b/README.md index b6061b8..6552c81 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,14 @@ This repository is a personal fork of Steam Deck Tools, customized for my ROG Al ## Scope -This fork is focused on `PerformanceOverlay` only. +This fork is focused on `PerformanceOverlay` and companion tooling around it (including a Game Bar widget). Included projects: - `PerformanceOverlay` - `CommonHelpers` - `ExternalHelpers` +- `XboxGameBarWidget` The other original applications are intentionally not part of active development in this fork. @@ -21,9 +22,44 @@ dotnet restore PerformanceOverlay/PerformanceOverlay.csproj dotnet build PerformanceOverlay/PerformanceOverlay.csproj --configuration Release ``` +This fork now targets `.NET 8` for desktop projects. + ## CI -GitHub Actions is configured to build only `PerformanceOverlay` and publish a ZIP artifact. +GitHub Actions builds and publishes: + +- `PerformanceOverlay-.zip` +- `SteamDeckToolsGameBarWidget-.zip` (includes MSIX package + install script) + +## Xbox Game Bar widget (Windows 11) + +The widget appears in Xbox Game Bar (`Win+G`) as **Performance Overlay Control** and lets you: + +- Switch directly to `FPS`, `FPSWithBattery`, `Battery`, `Minimal`, `Detail`, `Full` +- `Cycle`, `Show`, `Hide`, or `Toggle` the overlay + +The widget sends URI commands to `PerformanceOverlay` via: + +- `steamdecktools-performanceoverlay://mode/` +- `steamdecktools-performanceoverlay://cycle` +- `steamdecktools-performanceoverlay://show` +- `steamdecktools-performanceoverlay://hide` +- `steamdecktools-performanceoverlay://toggle` + +Run `PerformanceOverlay` once after extracting it so it can register the URI protocol in `HKCU`. + +## Install widget package (PowerShell) + +After downloading the `SteamDeckToolsGameBarWidget-.zip` artifact: + +```powershell +powershell -ExecutionPolicy Bypass -File scripts/install_gamebar_widget.ps1 -PackagePath "C:\path\to\SteamDeckToolsGameBarWidget-.zip" +``` + +Use `-ForceReinstall` to remove an existing widget package before reinstalling. + +If Windows blocks unsigned package installation, enable Developer Mode: +`Settings > Privacy & security > For developers > Developer Mode`. ## Install latest GitHub build artifact (PowerShell) @@ -114,4 +150,4 @@ Steam Deck Tools is not affiliated with Valve, Steam, or any of their partners. [Creative Commons Attribution-NonCommercial-ShareAlike (CC-BY-NC-SA)](http://creativecommons.org/licenses/by-nc-sa/4.0/). -Free for personal use. Contact me in other cases (`ayufan@ayufan.eu`). \ No newline at end of file +Free for personal use. Contact me in other cases (`ayufan@ayufan.eu`). diff --git a/SteamDeckTools.sln b/SteamDeckTools.sln index a24df3a..e72f5ba 100644 --- a/SteamDeckTools.sln +++ b/SteamDeckTools.sln @@ -9,6 +9,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommonHelpers", "CommonHelp EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ExternalHelpers", "ExternalHelpers\ExternalHelpers.csproj", "{A3FD29A4-844F-42B1-80C5-0301BD053AC0}" EndProject +Project("{A5A43C5B-DE2A-4C0C-9213-0A381AF9435A}") = "SteamDeckToolsGameBarWidget", "XboxGameBarWidget\SteamDeckToolsGameBarWidget.csproj", "{4E1DE235-5D8B-4ED9-B4AE-2D6B3E5C4E6A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -55,6 +57,18 @@ Global {A3FD29A4-844F-42B1-80C5-0301BD053AC0}.Release|x64.Build.0 = Release|Any CPU {A3FD29A4-844F-42B1-80C5-0301BD053AC0}.Release|x86.ActiveCfg = Release|Any CPU {A3FD29A4-844F-42B1-80C5-0301BD053AC0}.Release|x86.Build.0 = Release|Any CPU + {4E1DE235-5D8B-4ED9-B4AE-2D6B3E5C4E6A}.Debug|Any CPU.ActiveCfg = Debug|x64 + {4E1DE235-5D8B-4ED9-B4AE-2D6B3E5C4E6A}.Debug|Any CPU.Build.0 = Debug|x64 + {4E1DE235-5D8B-4ED9-B4AE-2D6B3E5C4E6A}.Debug|x64.ActiveCfg = Debug|x64 + {4E1DE235-5D8B-4ED9-B4AE-2D6B3E5C4E6A}.Debug|x64.Build.0 = Debug|x64 + {4E1DE235-5D8B-4ED9-B4AE-2D6B3E5C4E6A}.Debug|x86.ActiveCfg = Debug|x86 + {4E1DE235-5D8B-4ED9-B4AE-2D6B3E5C4E6A}.Debug|x86.Build.0 = Debug|x86 + {4E1DE235-5D8B-4ED9-B4AE-2D6B3E5C4E6A}.Release|Any CPU.ActiveCfg = Release|x64 + {4E1DE235-5D8B-4ED9-B4AE-2D6B3E5C4E6A}.Release|Any CPU.Build.0 = Release|x64 + {4E1DE235-5D8B-4ED9-B4AE-2D6B3E5C4E6A}.Release|x64.ActiveCfg = Release|x64 + {4E1DE235-5D8B-4ED9-B4AE-2D6B3E5C4E6A}.Release|x64.Build.0 = Release|x64 + {4E1DE235-5D8B-4ED9-B4AE-2D6B3E5C4E6A}.Release|x86.ActiveCfg = Release|x86 + {4E1DE235-5D8B-4ED9-B4AE-2D6B3E5C4E6A}.Release|x86.Build.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/XboxGameBarWidget/App.xaml b/XboxGameBarWidget/App.xaml new file mode 100644 index 0000000..c6b65fe --- /dev/null +++ b/XboxGameBarWidget/App.xaml @@ -0,0 +1,5 @@ + diff --git a/XboxGameBarWidget/App.xaml.cs b/XboxGameBarWidget/App.xaml.cs new file mode 100644 index 0000000..f3f6497 --- /dev/null +++ b/XboxGameBarWidget/App.xaml.cs @@ -0,0 +1,93 @@ +using Microsoft.Gaming.XboxGameBar; +using System; +using Windows.ApplicationModel; +using Windows.ApplicationModel.Activation; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Navigation; + +namespace SteamDeckToolsGameBarWidget +{ + sealed partial class App : Application + { + private XboxGameBarWidget widget = null; + + public App() + { + InitializeComponent(); + Suspending += OnSuspending; + } + + protected override void OnActivated(IActivatedEventArgs args) + { + XboxGameBarWidgetActivatedEventArgs widgetArgs = null; + + if (args.Kind == ActivationKind.Protocol) + { + var protocolArgs = args as IProtocolActivatedEventArgs; + if (protocolArgs != null && protocolArgs.Uri.Scheme.Equals("ms-gamebarwidget", StringComparison.OrdinalIgnoreCase)) + { + widgetArgs = args as XboxGameBarWidgetActivatedEventArgs; + } + } + + if (widgetArgs == null) + return; + + if (widgetArgs.IsLaunchActivation) + { + var rootFrame = new Frame(); + rootFrame.NavigationFailed += OnNavigationFailed; + Window.Current.Content = rootFrame; + + widget = new XboxGameBarWidget( + widgetArgs, + Window.Current.CoreWindow, + rootFrame + ); + + rootFrame.Navigate(typeof(WidgetPage)); + Window.Current.Closed += WidgetWindow_Closed; + Window.Current.Activate(); + } + } + + protected override void OnLaunched(LaunchActivatedEventArgs e) + { + var rootFrame = Window.Current.Content as Frame; + + if (rootFrame == null) + { + rootFrame = new Frame(); + rootFrame.NavigationFailed += OnNavigationFailed; + Window.Current.Content = rootFrame; + } + + if (!e.PrelaunchActivated) + { + if (rootFrame.Content == null) + rootFrame.Navigate(typeof(MainPage), e.Arguments); + + Window.Current.Activate(); + } + } + + private void WidgetWindow_Closed(object sender, Windows.UI.Core.CoreWindowEventArgs e) + { + widget = null; + Window.Current.Closed -= WidgetWindow_Closed; + } + + private void OnNavigationFailed(object sender, NavigationFailedEventArgs e) + { + throw new Exception("Failed to load page " + e.SourcePageType.FullName); + } + + private void OnSuspending(object sender, SuspendingEventArgs e) + { + var deferral = e.SuspendingOperation.GetDeferral(); + widget = null; + deferral.Complete(); + } + } +} diff --git a/XboxGameBarWidget/Assets/LargeTile.scale-100.png b/XboxGameBarWidget/Assets/LargeTile.scale-100.png new file mode 100644 index 0000000..78b17c0 Binary files /dev/null and b/XboxGameBarWidget/Assets/LargeTile.scale-100.png differ diff --git a/XboxGameBarWidget/Assets/SmallTile.scale-100.png b/XboxGameBarWidget/Assets/SmallTile.scale-100.png new file mode 100644 index 0000000..d9052da Binary files /dev/null and b/XboxGameBarWidget/Assets/SmallTile.scale-100.png differ diff --git a/XboxGameBarWidget/Assets/SplashScreen.scale-100.png b/XboxGameBarWidget/Assets/SplashScreen.scale-100.png new file mode 100644 index 0000000..6d2a4a1 Binary files /dev/null and b/XboxGameBarWidget/Assets/SplashScreen.scale-100.png differ diff --git a/XboxGameBarWidget/Assets/Square150x150Logo.scale-100.png b/XboxGameBarWidget/Assets/Square150x150Logo.scale-100.png new file mode 100644 index 0000000..3e1885b Binary files /dev/null and b/XboxGameBarWidget/Assets/Square150x150Logo.scale-100.png differ diff --git a/XboxGameBarWidget/Assets/Square44x44Logo.scale-100.png b/XboxGameBarWidget/Assets/Square44x44Logo.scale-100.png new file mode 100644 index 0000000..aaa17c7 Binary files /dev/null and b/XboxGameBarWidget/Assets/Square44x44Logo.scale-100.png differ diff --git a/XboxGameBarWidget/Assets/Square44x44Logo.targetsize-24_altform-unplated.png b/XboxGameBarWidget/Assets/Square44x44Logo.targetsize-24_altform-unplated.png new file mode 100644 index 0000000..fa054c5 Binary files /dev/null and b/XboxGameBarWidget/Assets/Square44x44Logo.targetsize-24_altform-unplated.png differ diff --git a/XboxGameBarWidget/Assets/StoreLogo.scale-100.png b/XboxGameBarWidget/Assets/StoreLogo.scale-100.png new file mode 100644 index 0000000..af17249 Binary files /dev/null and b/XboxGameBarWidget/Assets/StoreLogo.scale-100.png differ diff --git a/XboxGameBarWidget/Assets/Wide310x150Logo.scale-100.png b/XboxGameBarWidget/Assets/Wide310x150Logo.scale-100.png new file mode 100644 index 0000000..117e225 Binary files /dev/null and b/XboxGameBarWidget/Assets/Wide310x150Logo.scale-100.png differ diff --git a/XboxGameBarWidget/MainPage.xaml b/XboxGameBarWidget/MainPage.xaml new file mode 100644 index 0000000..e6d3ba4 --- /dev/null +++ b/XboxGameBarWidget/MainPage.xaml @@ -0,0 +1,16 @@ + + + + + + diff --git a/XboxGameBarWidget/MainPage.xaml.cs b/XboxGameBarWidget/MainPage.xaml.cs new file mode 100644 index 0000000..dc3dda2 --- /dev/null +++ b/XboxGameBarWidget/MainPage.xaml.cs @@ -0,0 +1,12 @@ +using Windows.UI.Xaml.Controls; + +namespace SteamDeckToolsGameBarWidget +{ + public sealed partial class MainPage : Page + { + public MainPage() + { + InitializeComponent(); + } + } +} diff --git a/XboxGameBarWidget/Package.appxmanifest b/XboxGameBarWidget/Package.appxmanifest new file mode 100644 index 0000000..c17b301 --- /dev/null +++ b/XboxGameBarWidget/Package.appxmanifest @@ -0,0 +1,112 @@ + + + + + + + + + Steam Deck Tools Game Bar Widget + SteamDeckTools + Assets\StoreLogo.png + + + + + + + + + + + + + + + + + + + + + + true + true + + + 330 + 420 + 240 + 320 + 500 + 700 + + + true + true + + + + + + + + + + + + + + + Microsoft.Gaming.XboxGameBar.winmd + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/XboxGameBarWidget/Properties/AssemblyInfo.cs b/XboxGameBarWidget/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..2f23c4b --- /dev/null +++ b/XboxGameBarWidget/Properties/AssemblyInfo.cs @@ -0,0 +1,14 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("SteamDeckToolsGameBarWidget")] +[assembly: AssemblyDescription("Xbox Game Bar widget controls for Steam Deck Tools PerformanceOverlay.")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("SteamDeckToolsGameBarWidget")] +[assembly: AssemblyCopyright("Copyright © 2026")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: ComVisible(false)] diff --git a/XboxGameBarWidget/Properties/Default.rd.xml b/XboxGameBarWidget/Properties/Default.rd.xml new file mode 100644 index 0000000..fa46763 --- /dev/null +++ b/XboxGameBarWidget/Properties/Default.rd.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/XboxGameBarWidget/SteamDeckToolsGameBarWidget.csproj b/XboxGameBarWidget/SteamDeckToolsGameBarWidget.csproj new file mode 100644 index 0000000..c84c060 --- /dev/null +++ b/XboxGameBarWidget/SteamDeckToolsGameBarWidget.csproj @@ -0,0 +1,159 @@ + + + + + Debug + x86 + {4E1DE235-5D8B-4ED9-B4AE-2D6B3E5C4E6A} + AppContainerExe + Properties + SteamDeckToolsGameBarWidget + SteamDeckToolsGameBarWidget + en-US + UAP + 10.0.22621.0 + 10.0.18362.0 + 14 + 512 + {A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + true + False + False + False + False + True + Never + x64|arm64 + 0 + + + true + bin\x86\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + x86 + false + prompt + true + + + bin\x86\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + x86 + false + prompt + true + true + + + true + bin\ARM64\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + ARM64 + false + prompt + true + + + bin\ARM64\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + ARM64 + false + prompt + true + true + + + true + bin\x64\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + x64 + false + prompt + true + + + bin\x64\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + x64 + false + prompt + true + true + + + PackageReference + + + + App.xaml + + + MainPage.xaml + + + WidgetPage.xaml + + + + + + Designer + + + + + + + + + + + + + + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + + + 7.2.240903001 + + + 6.2.9 + + + + win32 + $(Platform) + + + 14.0 + + + diff --git a/XboxGameBarWidget/WidgetPage.xaml b/XboxGameBarWidget/WidgetPage.xaml new file mode 100644 index 0000000..673e668 --- /dev/null +++ b/XboxGameBarWidget/WidgetPage.xaml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/XboxGameBarWidget/WidgetPage.xaml.cs b/XboxGameBarWidget/WidgetPage.xaml.cs new file mode 100644 index 0000000..36f1401 --- /dev/null +++ b/XboxGameBarWidget/WidgetPage.xaml.cs @@ -0,0 +1,42 @@ +using System; +using Windows.System; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; + +namespace SteamDeckToolsGameBarWidget +{ + public sealed partial class WidgetPage : Page + { + public WidgetPage() + { + InitializeComponent(); + } + + private async void CommandButton_Click(object sender, RoutedEventArgs e) + { + var button = sender as Button; + if (button == null || !(button.Tag is string)) + return; + + try + { + var targetUri = (string)button.Tag; + Uri uri; + if (!Uri.TryCreate(targetUri, UriKind.Absolute, out uri)) + { + StatusText.Text = "Invalid command URI."; + return; + } + + bool launched = await Launcher.LaunchUriAsync(uri); + StatusText.Text = launched + ? "Sent command: " + button.Content + : "Unable to launch command URI. Start PerformanceOverlay first."; + } + catch (Exception ex) + { + StatusText.Text = "Failed to send command: " + ex.Message; + } + } + } +} diff --git a/docs/performance-overlay.md b/docs/performance-overlay.md index 94f69bc..eca9755 100644 --- a/docs/performance-overlay.md +++ b/docs/performance-overlay.md @@ -10,6 +10,27 @@ It currently registers two global hotkeys: - **Shift+F11** - enable performance overlay - **Alt+Shift+F11** - cycle to next performance overlay (and enable it) +It also supports command URIs (used by the Xbox Game Bar widget in this fork): + +- `steamdecktools-performanceoverlay://mode/fps` +- `steamdecktools-performanceoverlay://mode/fpswithbattery` +- `steamdecktools-performanceoverlay://mode/battery` +- `steamdecktools-performanceoverlay://mode/minimal` +- `steamdecktools-performanceoverlay://mode/detail` +- `steamdecktools-performanceoverlay://mode/full` +- `steamdecktools-performanceoverlay://cycle` +- `steamdecktools-performanceoverlay://show` +- `steamdecktools-performanceoverlay://hide` +- `steamdecktools-performanceoverlay://toggle` + +Equivalent command-line switches are available: + +- `--mode ` +- `--cycle` +- `--show` +- `--hide` +- `--toggle` + There are 5 modes of presentation: ## 1. FPS diff --git a/scripts/build.ps1 b/scripts/build.ps1 index f71f76e..fc0b16b 100644 --- a/scripts/build.ps1 +++ b/scripts/build.ps1 @@ -30,5 +30,5 @@ if ($lastVer) { } echo "nextVer: $nextVer" -dotnet --list-sdks -or winget install Microsoft.DotNet.SDK.6 +dotnet --list-sdks -or winget install Microsoft.DotNet.SDK.8 dotnet build PerformanceOverlay/PerformanceOverlay.csproj --configuration "$configuration" "/property:Version=$nextVer" --output "$path" diff --git a/scripts/install_gamebar_widget.ps1 b/scripts/install_gamebar_widget.ps1 new file mode 100644 index 0000000..eac2f03 --- /dev/null +++ b/scripts/install_gamebar_widget.ps1 @@ -0,0 +1,66 @@ +param( + [Parameter(Mandatory = $true)] + [string]$PackagePath, + + [switch]$ForceReinstall +) + +$ErrorActionPreference = "Stop" + +if (-not (Test-Path -LiteralPath $PackagePath)) { + throw "PackagePath not found: $PackagePath" +} + +$tempExtractDir = $null + +try { + $packageItem = Get-Item -LiteralPath $PackagePath + if (-not $packageItem.PSIsContainer -and $packageItem.Extension -eq ".zip") { + $tempExtractDir = Join-Path ([System.IO.Path]::GetTempPath()) ("sdt-widget-install-" + [System.Guid]::NewGuid().ToString("N")) + New-Item -ItemType Directory -Path $tempExtractDir -Force | Out-Null + Expand-Archive -Path $packageItem.FullName -DestinationPath $tempExtractDir -Force + $packageItem = Get-Item -LiteralPath $tempExtractDir + } + + if ($packageItem.PSIsContainer) { + $packageItem = Get-ChildItem -LiteralPath $packageItem.FullName -Recurse -File | + Where-Object { $_.Extension -in ".msix", ".appx", ".msixbundle", ".appxbundle" } | + Sort-Object LastWriteTime -Descending | + Select-Object -First 1 + } + + if (-not $packageItem) { + throw "No .msix/.appx package was found under '$PackagePath'." + } + + $identityName = "SteamDeckToolsGameBarWidget" + $installedPackage = Get-AppxPackage -Name $identityName -ErrorAction SilentlyContinue + + if ($ForceReinstall -and $installedPackage) { + Write-Host "Removing existing package: $($installedPackage.PackageFullName)" + Remove-AppxPackage -Package $installedPackage.PackageFullName + } + + Write-Host "Installing package: $($packageItem.FullName)" + + try { + Add-AppxPackage ` + -Path $packageItem.FullName ` + -ForceUpdateFromAnyVersion ` + -ForceApplicationShutdown ` + -AllowUnsigned + } + catch { + Write-Host "" + Write-Host "Install failed. If this is an unsigned package, enable Developer Mode in Windows:" + Write-Host "Settings > Privacy & security > For developers > Developer Mode" + throw + } + + Write-Host "Installed. Open Xbox Game Bar (Win+G), click Widget Menu, then add 'Performance Overlay Control'." +} +finally { + if ($tempExtractDir -and (Test-Path -LiteralPath $tempExtractDir)) { + Remove-Item -LiteralPath $tempExtractDir -Recurse -Force + } +} diff --git a/scripts/run_loop.bat b/scripts/run_loop.bat index d041ff5..4e5590d 100644 --- a/scripts/run_loop.bat +++ b/scripts/run_loop.bat @@ -6,10 +6,10 @@ cd "%~dp0\.." :retry taskkill /F /IM "%2.exe" -del %2\bin\Debug\net6.0-windows\%2.exe +del %2\bin\Debug\net8.0-windows\%2.exe dotnet build %2\%2.csproj -%2\bin\Debug\net6.0-windows\%2.exe +%2\bin\Debug\net8.0-windows\%2.exe timeout /t 3 goto retry