diff --git a/CHANGELOG.md b/CHANGELOG.md index e7018e61..a3043aa0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ Notable changes to Wisp are recorded here. +## 2.6.12 - 2026-10-16 + +- Add Prioritize HUD updates on the GPU, a HUD setting that is off by default so the game keeps the GPU; turn it on to give HUD updates priority when the GPU is under heavy load. +- Keep tune performance results with the current tune after leaving Forza's Tune screen, as long as the car, parts and settings are unchanged. +- Report feature pauses from Wisp Tools when they apply and when a policy is rejected or cannot be cached. +- Let Chat with Views messages carry a subject. +- Complete application exit promptly even when a renderer thread is busy. +- Report clip action problems on the Clips page and allow a retry. + ## 2.6.11 - 2026-10-15 - Chat with Views in a compact popup with a chronological message history, replies and earlier messages. diff --git a/LICENSES/library-sources-distribution.json b/LICENSES/library-sources-distribution.json index 480091ed..04e609d1 100644 --- a/LICENSES/library-sources-distribution.json +++ b/LICENSES/library-sources-distribution.json @@ -1,8 +1,8 @@ { "schemaVersion": 1, - "version": "2.6.11", + "version": "2.6.12", "archive": { - "filename": "Wisp-2.6.11-library-sources.zip", + "filename": "Wisp-2.6.12-library-sources.zip", "sourceVersion": "2.6.8", "url": "https://github.com/Views2k/Wisp/releases/download/library-sources-2.6.8/Wisp-2.6.8-library-sources.zip", "bytes": 879043478, diff --git a/README.md b/README.md index 75371fb2..6473e82c 100644 --- a/README.md +++ b/README.md @@ -13,11 +13,11 @@ Buy Me a Coffee

-## Wisp 2.6.11 +## Wisp 2.6.12 -Wisp 2.6.11 brings conversation with the developer into a compact chat popup, with message history, replies and optional diagnostic attachments. Available updates stay visible on the dashboard with a working Update action. +Wisp 2.6.12 adds a HUD setting that decides whether HUD updates take priority over the game on the GPU, off by default, keeps tune performance results with your current tune after you leave Forza's Tune screen, and lets you give a message to the developer a subject. Closing Wisp completes promptly and clip actions report problems on the Clips page. -[Clips](#clips) · [Tune](#tune) · [Lap review](#lap-review) · [Live map and lap delta](#live-map-and-lap-delta) · [Install](#install) · [2.6.11 release notes](docs/releases/Wisp-2.6.11-release-notes.md) +[Clips](#clips) · [Tune](#tune) · [Lap review](#lap-review) · [Live map and lap delta](#live-map-and-lap-delta) · [Install](#install) · [2.6.12 release notes](docs/releases/Wisp-2.6.12-release-notes.md) ### Clips diff --git a/docs/releases/Wisp-2.6.12-release-notes.md b/docs/releases/Wisp-2.6.12-release-notes.md new file mode 100644 index 00000000..b3429210 --- /dev/null +++ b/docs/releases/Wisp-2.6.12-release-notes.md @@ -0,0 +1,22 @@ +# Wisp 2.6.12 + +[Download the latest Wisp release for Windows](https://github.com/Views2k/Wisp/releases/latest) + +## HUD GPU priority + +- Prioritize HUD updates on the GPU is a new HUD setting, off by default, so the game keeps the GPU to itself. +- Turn it on next to CPU rendering to give HUD updates priority when the GPU is under heavy load. It applies immediately. + +## Tune performance results + +- Performance results read from Forza's Tune screen stay with your current tune after you leave the screen. +- Refresh the current tune at any time and keep comparing braking, grip, acceleration and top speed. + +## Chat with Views + +- Add a subject to your messages so they are easy to find later. + +## Reliability + +- Closing Wisp completes promptly even when a renderer thread is busy. +- Clip actions report problems on the Clips page and can be retried. diff --git a/installer/Wisp.iss b/installer/Wisp.iss index 4daf56df..8345d526 100644 --- a/installer/Wisp.iss +++ b/installer/Wisp.iss @@ -1,5 +1,5 @@ #define MyAppName "Wisp" -#define MyAppVersion "2.6.11" +#define MyAppVersion "2.6.12" #define MyAppDisplayVersion MyAppVersion #define MyAppOutputVersion MyAppVersion #define MyAppPublisher "Wisp" diff --git a/src/Wisp.App/App.xaml.cs b/src/Wisp.App/App.xaml.cs index 57b2b5ba..b7aeb6ab 100644 --- a/src/Wisp.App/App.xaml.cs +++ b/src/Wisp.App/App.xaml.cs @@ -292,11 +292,14 @@ protected override void OnExit(ExitEventArgs e) { _activationCancellation.Cancel(); _activationEvent?.Set(); - _activationListener?.GetAwaiter().GetResult(); + _activationListener?.Wait(TimeSpan.FromSeconds(2)); } - _controller?.DisposeAsync().AsTask().GetAwaiter().GetResult(); - shutdownCompleted = true; + // Bounded: a worker stuck in a driver or file call must not keep the process + // alive after the user closed Wisp. An expired wait leaves the exit marker + // incomplete, which is what happened. + var disposal = _controller?.DisposeAsync().AsTask(); + shutdownCompleted = disposal is null || disposal.Wait(TimeSpan.FromSeconds(10)); } finally { diff --git a/src/Wisp.App/AppController.FeatureControls.cs b/src/Wisp.App/AppController.FeatureControls.cs index 912bf737..34e0f3f9 100644 --- a/src/Wisp.App/AppController.FeatureControls.cs +++ b/src/Wisp.App/AppController.FeatureControls.cs @@ -18,6 +18,8 @@ private bool TryUseHudProfiles(out string error) internal Task ApplyFeatureControlsAsync(FeatureControlState state) { if (_disposed || !FeatureControls.Apply(state)) return Task.CompletedTask; + // Tools sees that this installation applied the revision, so a pause can be verified end to end. + SupplementaryObservations.Record("feature", "app", state.Disabled.Count > 0 ? "detected" : "none", "apply"); var recording = FeatureAllowed("run-recording"); var clipping = FeatureAllowed("clip-recording"); _tuneCapture?.SetFeatureAvailable(FeatureAllowed("tune-read")); @@ -72,6 +74,7 @@ internal Task ApplyFeatureControlsAsync(FeatureControlState state) _ = ConfigureClipShortcut(false, settings.ToggleShortcutEnabled, settings.ToggleShortcut); _ = ConfigureClipShortcut(true, settings.SaveShortcutEnabled, settings.SaveShortcut); } + ApplyHudGpuPriority(); FeatureControlsChanged?.Invoke(this, EventArgs.Empty); UpdateShiftCueObservation(); Overlay?.RefreshFeatureControls(); diff --git a/src/Wisp.App/AppController.cs b/src/Wisp.App/AppController.cs index 515ad1e7..3adba74c 100644 --- a/src/Wisp.App/AppController.cs +++ b/src/Wisp.App/AppController.cs @@ -280,6 +280,28 @@ public void SetCpuRenderingEnabled(bool enabled) ViewModel.UpdateCpuRendering(Settings.CpuRenderingEnabled, saved); } + public void SetHudGpuPriorityEnabled(bool enabled) + { + if (_disposed) return; + var previous = Settings.HudGpuPriority; + Settings.HudGpuPriority = enabled; + var saved = SaveSettings(); + if (!saved) Settings.HudGpuPriority = previous; + PublishSupplementarySettings(); + ApplyHudGpuPriority(saved); + } + + // Effective only when the user turned it on and Tools has not paused it for this version. + internal void ApplyHudGpuPriority(bool saved = true) + { + var active = Settings.HudGpuPriority && FeatureAllowed("gpu-priority"); + ViewModel.UpdateHudGpuPriority(Settings.HudGpuPriority, active, + !saved ? "Could not save the renderer option. Try again." + : Settings.HudGpuPriority && !active + ? FeatureControls.Reason("gpu-priority") ?? "GPU priority is temporarily paused." + : ""); + } + public async Task SetDebugLoggingEnabledAsync(bool enabled) { if (_disposed) diff --git a/src/Wisp.App/AppSettings.cs b/src/Wisp.App/AppSettings.cs index e8122e8a..5f23600b 100644 --- a/src/Wisp.App/AppSettings.cs +++ b/src/Wisp.App/AppSettings.cs @@ -127,6 +127,8 @@ public double TorqueGaugeScale public bool AutomaticApplicationUpdateChecks { get; set; } = true; public DateTimeOffset? LastApplicationUpdateCheckUtc { get; set; } public bool CpuRenderingEnabled { get; set; } + // Off by default: the game keeps the GPU unless the user chooses otherwise. + public bool HudGpuPriority { get; set; } public bool DebugLoggingEnabled { get; set; } public DateTimeOffset? DebugLoggingExpiresAtUtc { get; set; } public string ColorTheme { get; set; } = AppColorThemes.DefaultName; diff --git a/src/Wisp.App/Clips/ClipsViewModel.cs b/src/Wisp.App/Clips/ClipsViewModel.cs index de8a0947..26caad85 100644 --- a/src/Wisp.App/Clips/ClipsViewModel.cs +++ b/src/Wisp.App/Clips/ClipsViewModel.cs @@ -1167,7 +1167,14 @@ private void NotifyDashboardNotice() private void OnChanged([CallerMemberName] string? name = null) => PropertyChanged?.Invoke(this, new(name)); private ICommand Command(Func execute, Func canExecute) { - var command = new ClipCommand(execute, canExecute); _commands.Add(command); return command; + var command = new ClipCommand(execute, canExecute, CommandFailed); _commands.Add(command); return command; + } + private void CommandFailed(Exception error) + { + if (_disposed) return; + ErrorText(error is IOException or UnauthorizedAccessException + ? "That clip action could not reach the clip folder. Check the storage status, then try again." + : "That clip action did not complete. Try again."); } public void Dispose() { @@ -1178,11 +1185,21 @@ public void Dispose() if (_thumbnails is IDisposable ownedThumbnails) ownedThumbnails.Dispose(); // Operations retain the captured token, never access a disposed token source. } - private sealed class ClipCommand(Func action, Func canExecute) : ICommand + // A failing action reports on the page instead of ending the process, and a running + // action cannot be started twice (the Runs and Tunes commands behave the same way). + private sealed class ClipCommand(Func action, Func canExecute, Action failed) : ICommand { + private bool _running; public event EventHandler? CanExecuteChanged; - public bool CanExecute(object? parameter) => canExecute(); - public async void Execute(object? parameter) { if (CanExecute(parameter)) await action(); } + public bool CanExecute(object? parameter) => !_running && canExecute(); + public async void Execute(object? parameter) + { + if (!CanExecute(parameter)) return; + _running = true; Raise(); + try { await action(); } + catch (Exception error) when (error is not OutOfMemoryException) { failed(error); } + finally { _running = false; Raise(); } + } public void Raise() => CanExecuteChanged?.Invoke(this, EventArgs.Empty); } } diff --git a/src/Wisp.App/ControlPanelWindow.SupportInbox.cs b/src/Wisp.App/ControlPanelWindow.SupportInbox.cs index 33ac576a..099b1c02 100644 --- a/src/Wisp.App/ControlPanelWindow.SupportInbox.cs +++ b/src/Wisp.App/ControlPanelWindow.SupportInbox.cs @@ -60,7 +60,7 @@ private void OpenSupportChat() if (_chatWindow is null) { _chatView = new SupportChatView( - (message, diagnostics) => Task.Run(() => app.CreateSupplementarySupportPreview("feedback", "Chat with Views", message, diagnostics)), + (title, message, diagnostics) => Task.Run(() => app.CreateSupplementarySupportPreview("feedback", title, message, diagnostics)), (report, cancellation) => app.SubmitSupplementarySupportAsync(report, cancellation, chat: true), (older, ids) => app.RefreshSupportChatAsync(older, ids)); var frame = _chatView.CreatePopupSurface(() => _chatWindow?.Hide(), () => _chatWindow?.DragMove()); diff --git a/src/Wisp.App/ControlPanelWindow.cs b/src/Wisp.App/ControlPanelWindow.cs index eeec37d9..7597f3cf 100644 --- a/src/Wisp.App/ControlPanelWindow.cs +++ b/src/Wisp.App/ControlPanelWindow.cs @@ -73,6 +73,7 @@ public abstract partial class ControlPanelWindow : Window private Button OverlayHotkeyCaptureButton => FindControl