From 82e91880a0f5b9ec45e0868502c3fdf6c12564a8 Mon Sep 17 00:00:00 2001 From: CryoTechnic Date: Mon, 10 Aug 2026 17:11:18 -0400 Subject: [PATCH 01/20] Update submodules --- ColorThief | 2 +- H.NotifyIcon | 2 +- Hi3Helper.EncTool | 2 +- Hi3Helper.Http | 2 +- Hi3Helper.Plugin.Core | 2 +- Hi3Helper.SharpDiscordRPC | 2 +- Hi3Helper.SimpleZipArchiveReader | 2 +- Hi3Helper.Sophon | 2 +- Hi3Helper.Win32 | 2 +- ImageEx | 2 +- SevenZipExtractor | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/ColorThief b/ColorThief index 5a04db74a..b568f2973 160000 --- a/ColorThief +++ b/ColorThief @@ -1 +1 @@ -Subproject commit 5a04db74a50573469e48d8ffae4ece35f8707308 +Subproject commit b568f2973aa050c4a5665362da665c71945da2bb diff --git a/H.NotifyIcon b/H.NotifyIcon index 7422382b4..8cadbd2a7 160000 --- a/H.NotifyIcon +++ b/H.NotifyIcon @@ -1 +1 @@ -Subproject commit 7422382b4da5a738f84b400b31d23df1c1c0c319 +Subproject commit 8cadbd2a7a5d06473083ec59174de0ca04427c15 diff --git a/Hi3Helper.EncTool b/Hi3Helper.EncTool index 98d8b5786..47eeba101 160000 --- a/Hi3Helper.EncTool +++ b/Hi3Helper.EncTool @@ -1 +1 @@ -Subproject commit 98d8b57866ea969fbaf029c4491864af9b7332d8 +Subproject commit 47eeba101eb7a34d8eb94251ad022ad7215f1d04 diff --git a/Hi3Helper.Http b/Hi3Helper.Http index 887674ee6..2d4c209d4 160000 --- a/Hi3Helper.Http +++ b/Hi3Helper.Http @@ -1 +1 @@ -Subproject commit 887674ee6365c4907d4b45061b26857726d9df74 +Subproject commit 2d4c209d4d462ee94a5e6a13e3c45abcfbca848e diff --git a/Hi3Helper.Plugin.Core b/Hi3Helper.Plugin.Core index 55d05702c..14293ba31 160000 --- a/Hi3Helper.Plugin.Core +++ b/Hi3Helper.Plugin.Core @@ -1 +1 @@ -Subproject commit 55d05702ccc96eedb041c8d66f0bb3940b1ce08f +Subproject commit 14293ba31be02b513d3088d6866727504fa78da0 diff --git a/Hi3Helper.SharpDiscordRPC b/Hi3Helper.SharpDiscordRPC index a2d1cb7c6..e80e57c3e 160000 --- a/Hi3Helper.SharpDiscordRPC +++ b/Hi3Helper.SharpDiscordRPC @@ -1 +1 @@ -Subproject commit a2d1cb7c6d4d39660bc557bec7296aafb6d48ad3 +Subproject commit e80e57c3e758bc7a28f9c69fb266019e977d1a1a diff --git a/Hi3Helper.SimpleZipArchiveReader b/Hi3Helper.SimpleZipArchiveReader index 345d80576..2abcc93d8 160000 --- a/Hi3Helper.SimpleZipArchiveReader +++ b/Hi3Helper.SimpleZipArchiveReader @@ -1 +1 @@ -Subproject commit 345d8057652890010e7e3600849d91c0acddfcf0 +Subproject commit 2abcc93d837fd53b8893988e3eab85b89356f74c diff --git a/Hi3Helper.Sophon b/Hi3Helper.Sophon index 9189e990e..ea21b1883 160000 --- a/Hi3Helper.Sophon +++ b/Hi3Helper.Sophon @@ -1 +1 @@ -Subproject commit 9189e990e2d8ef6a9ee5b3dfd77b41e1874f9cac +Subproject commit ea21b18838579add755fe1edd709b2f3daf0fb7c diff --git a/Hi3Helper.Win32 b/Hi3Helper.Win32 index c061d517c..ff564bec9 160000 --- a/Hi3Helper.Win32 +++ b/Hi3Helper.Win32 @@ -1 +1 @@ -Subproject commit c061d517ca757ebaf8ceeca955aac1b73ef41087 +Subproject commit ff564bec95894ef8da5e4dfb4f08e3057080cdfc diff --git a/ImageEx b/ImageEx index f129c5ef9..4027757a4 160000 --- a/ImageEx +++ b/ImageEx @@ -1 +1 @@ -Subproject commit f129c5ef97209c9f343291b09d06639f692e97eb +Subproject commit 4027757a4890e68fe1d32306cee95310a19d231e diff --git a/SevenZipExtractor b/SevenZipExtractor index eb15009af..5e0f4de32 160000 --- a/SevenZipExtractor +++ b/SevenZipExtractor @@ -1 +1 @@ -Subproject commit eb15009af23925d56453415d75e51e3192160a44 +Subproject commit 5e0f4de32a5da3ae441fdd5c95d3951ef703480e From 7c1a411faf6cd43d2ee0339f0c31b446433e7297 Mon Sep 17 00:00:00 2001 From: CryoTechnic Date: Mon, 10 Aug 2026 17:51:53 -0400 Subject: [PATCH 02/20] feat(settings): render plugin game settings pages --- .../Plugins/PluginPresetConfigWrapper.cs | 2 + .../XAMLs/MainApp/MainPage.Navigation.cs | 4 +- .../PluginGameSettingsPage.cs | 283 ++++++++++++++++++ 3 files changed, 288 insertions(+), 1 deletion(-) create mode 100644 CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.cs diff --git a/CollapseLauncher/Classes/Plugins/PluginPresetConfigWrapper.cs b/CollapseLauncher/Classes/Plugins/PluginPresetConfigWrapper.cs index bd0578c38..6c8a77d52 100644 --- a/CollapseLauncher/Classes/Plugins/PluginPresetConfigWrapper.cs +++ b/CollapseLauncher/Classes/Plugins/PluginPresetConfigWrapper.cs @@ -25,6 +25,7 @@ public partial class PluginPresetConfigWrapper : PresetConfig, IDisposable { public DiscordPresenceExtension.DiscordPresenceContext DiscordPresenceContext { get; } public GameManagerExtension.RunGameFromGameManagerContext RunGameContext { get; } + public GameSettingsExtension.GameSettingsContext GameSettingsContext { get; } public readonly PluginInfo PluginInfo; public readonly IPlugin Plugin; private readonly IPluginPresetConfig _config; @@ -50,6 +51,7 @@ private unsafe PluginPresetConfigWrapper(PluginInfo pluginInfo, IPluginPresetCon }; DiscordPresenceContext = new DiscordPresenceExtension.DiscordPresenceContext(pluginInfo.Handle, config); + GameSettingsContext = new GameSettingsExtension.GameSettingsContext(pluginInfo.Handle, config); } public unsafe GameManagerExtension.RunGameFromGameManagerContext UseToggledGameLaunchContext() diff --git a/CollapseLauncher/XAMLs/MainApp/MainPage.Navigation.cs b/CollapseLauncher/XAMLs/MainApp/MainPage.Navigation.cs index fb8913904..ad9a7a1cf 100644 --- a/CollapseLauncher/XAMLs/MainApp/MainPage.Navigation.cs +++ b/CollapseLauncher/XAMLs/MainApp/MainPage.Navigation.cs @@ -203,11 +203,13 @@ void Impl() GameNameType.StarRail => typeof(StarRailGameSettingsPage), GameNameType.Genshin => typeof(GenshinGameSettingsPage), GameNameType.Zenless => typeof(ZenlessGameSettingsPage), + GameNameType.Plugin when presetConfig is PluginPresetConfigWrapper + { GameSettingsContext.HasPage: true } => typeof(PluginGameSettingsPage), _ => null }; NavigationViewItemsContext.GameSettingsPage.Item.Tag = gspPageType; - NavigationViewItemsContext.GameSettingsPage.Item.Visibility = isPluginGame ? Visibility.Collapsed : Visibility.Visible; + NavigationViewItemsContext.GameSettingsPage.Item.Visibility = gspPageType == null ? Visibility.Collapsed : Visibility.Visible; NavigationViewItemsContext.FileCleanupPage.Item.Visibility = isPluginGame ? Visibility.Collapsed : Visibility.Visible; } } diff --git a/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.cs b/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.cs new file mode 100644 index 000000000..31d9f03a3 --- /dev/null +++ b/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.cs @@ -0,0 +1,283 @@ +using CollapseLauncher.Plugins; +using CollapseLauncher.Helper; +using Hi3Helper.Plugin.Core.UI.Settings; +using Hi3Helper.Plugin.Core.Utility; +using Microsoft.UI; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media; +using System; +using System.Globalization; +using static CollapseLauncher.Statics.GamePropertyVault; + +#nullable enable +namespace CollapseLauncher.Pages; + +/// +/// Renders the declarative game settings page exposed by a v0.1.6 plugin. +/// +public sealed class PluginGameSettingsPage : Page +{ + private readonly GameSettingsExtension.GameSettingsContext _context; + private readonly TextBlock _statusText = new() + { + Margin = new Thickness(16, 0, 0, 0), + VerticalAlignment = VerticalAlignment.Center, + TextWrapping = TextWrapping.Wrap + }; + + public PluginGameSettingsPage() + { + NavigationCacheMode = Microsoft.UI.Xaml.Navigation.NavigationCacheMode.Disabled; + + if (GetCurrentGameProperty().GameVersion.GamePreset is not PluginPresetConfigWrapper preset) + { + throw new InvalidOperationException("The current game preset is not provided by a plugin"); + } + + _context = preset.GameSettingsContext; + Content = CreateContent(); + } + + private UIElement CreateContent() + { + if (!_context.TryGetPage(out GameSettingsPage? page, out Exception? error) || page == null) + { + return new TextBlock + { + Margin = new Thickness(32, 40, 32, 32), + Text = error?.Message ?? "This plugin did not provide a game settings page.", + TextWrapping = TextWrapping.Wrap + }; + } + + Grid root = new(); + root.RowDefinitions.Add(new RowDefinition()); + root.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); + + StackPanel sectionsPanel = new() { Margin = new Thickness(32, 40, 32, 32), Spacing = 24 }; + if (!string.IsNullOrWhiteSpace(page.Title)) + { + sectionsPanel.Children.Add(new TextBlock + { + Text = page.Title, + Style = Application.Current.Resources["TitleLargeTextBlockStyle"] as Style, + TextWrapping = TextWrapping.Wrap + }); + } + + foreach (GameSettingsSection section in page.Sections) + { + sectionsPanel.Children.Add(CreateSection(section)); + } + + ScrollViewer scrollViewer = new() + { + Content = sectionsPanel, + VerticalScrollBarVisibility = ScrollBarVisibility.Auto + }; + root.Children.Add(scrollViewer); + + Grid applyPanel = new() + { + Padding = new Thickness(32, 16, 32, 16), + Background = Application.Current.Resources["GameSettingsApplyGridBrush"] as Brush + }; + applyPanel.ColumnDefinitions.Add(new ColumnDefinition()); + applyPanel.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + applyPanel.Children.Add(_statusText); + + Button applyButton = new() + { + Content = Locale.Current.Lang?._GameSettingsPage?.ApplyBtn ?? "Apply settings", + MinWidth = 144, + CornerRadius = new CornerRadius(16), + Style = Application.Current.Resources["AccentButtonStyle"] as Style + }; + applyButton.Click += OnApply; + Grid.SetColumn(applyButton, 1); + applyPanel.Children.Add(applyButton); + Grid.SetRow(applyPanel, 1); + root.Children.Add(applyPanel); + + return root; + } + + private FrameworkElement CreateSection(GameSettingsSection section) + { + StackPanel panel = new() { Spacing = 8 }; + panel.Children.Add(new TextBlock + { + Text = section.Title, + Style = Application.Current.Resources["SubtitleTextBlockStyle"] as Style, + TextWrapping = TextWrapping.Wrap + }); + + if (!string.IsNullOrWhiteSpace(section.Description)) + { + panel.Children.Add(new TextBlock + { + Text = section.Description, + Opacity = 0.72, + TextWrapping = TextWrapping.Wrap + }); + } + + foreach (GameSettingEntry entry in section.Entries) + { + panel.Children.Add(CreateEntry(entry)); + } + + return panel; + } + + private FrameworkElement CreateEntry(GameSettingEntry entry) + { + Grid card = new() + { + Padding = new Thickness(16, 12, 16, 12), + ColumnSpacing = 24, + Background = Application.Current.Resources["CardBackgroundFillColorDefaultBrush"] as Brush, + CornerRadius = new CornerRadius(8) + }; + card.ColumnDefinitions.Add(new ColumnDefinition()); + card.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + + StackPanel text = new() { VerticalAlignment = VerticalAlignment.Center, Spacing = 2 }; + text.Children.Add(new TextBlock { Text = entry.Title, TextWrapping = TextWrapping.Wrap }); + if (!string.IsNullOrWhiteSpace(entry.Description)) + { + text.Children.Add(new TextBlock + { + Text = entry.Description, + Opacity = 0.72, + TextWrapping = TextWrapping.Wrap + }); + } + card.Children.Add(text); + + FrameworkElement editor = CreateEditor(entry); + editor.MinWidth = entry.Kind is GameSettingKind.Toggle ? 0 : 180; + editor.VerticalAlignment = VerticalAlignment.Center; + Grid.SetColumn(editor, 1); + card.Children.Add(editor); + return card; + } + + private FrameworkElement CreateEditor(GameSettingEntry entry) => entry.Kind switch + { + GameSettingKind.Toggle => CreateToggle(entry), + GameSettingKind.Text => CreateText(entry), + GameSettingKind.Number => CreateNumber(entry), + GameSettingKind.Slider => CreateSlider(entry), + GameSettingKind.Choice => CreateChoice(entry), + _ => throw new ArgumentOutOfRangeException(nameof(entry.Kind)) + }; + + private ToggleSwitch CreateToggle(GameSettingEntry entry) + { + ToggleSwitch control = new() { IsOn = bool.TryParse(entry.Value, out bool value) && value }; + control.Toggled += (_, _) => SetValue(entry.Key, control.IsOn ? bool.TrueString : bool.FalseString); + return control; + } + + private TextBox CreateText(GameSettingEntry entry) + { + TextBox control = new() { Text = entry.Value, PlaceholderText = entry.Placeholder }; + control.TextChanged += (_, _) => SetValue(entry.Key, control.Text); + return control; + } + + private NumberBox CreateNumber(GameSettingEntry entry) + { + NumberBox control = new() + { + Minimum = entry.Minimum, + Maximum = entry.Maximum, + SmallChange = entry.Step, + SpinButtonPlacementMode = NumberBoxSpinButtonPlacementMode.Compact, + Value = ParseNumber(entry.Value, entry.Minimum) + }; + control.ValueChanged += (_, args) => + { + if (!double.IsNaN(args.NewValue)) + { + SetValue(entry.Key, args.NewValue.ToString(CultureInfo.InvariantCulture)); + } + }; + return control; + } + + private Slider CreateSlider(GameSettingEntry entry) + { + Slider control = new() + { + Minimum = entry.Minimum, + Maximum = entry.Maximum, + StepFrequency = entry.Step, + Value = ParseNumber(entry.Value, entry.Minimum), + Width = 220 + }; + control.ValueChanged += (_, args) => + SetValue(entry.Key, args.NewValue.ToString(CultureInfo.InvariantCulture)); + return control; + } + + private ComboBox CreateChoice(GameSettingEntry entry) + { + ComboBox control = new(); + foreach (GameSettingChoice choice in entry.Choices ?? []) + { + ComboBoxItem item = new() { Content = choice.Title, Tag = choice.Value }; + control.Items.Add(item); + if (string.Equals(choice.Value, entry.Value, StringComparison.Ordinal)) + { + control.SelectedItem = item; + } + } + + control.SelectionChanged += (_, _) => + { + if (control.SelectedItem is ComboBoxItem { Tag: string value }) + { + SetValue(entry.Key, value); + } + }; + return control; + } + + private static double ParseNumber(string value, double fallback) => + double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out double result) ? result : fallback; + + private void SetValue(string key, string value) + { + try + { + _context.SetValue(key, value); + SetStatus(null); + } + catch (Exception ex) + { + SetStatus(ex.Message, true); + } + } + + private void OnApply(object sender, RoutedEventArgs args) + { + try + { + _context.Apply(); + SetStatus(Locale.Current.Lang?._GameSettingsPage?.SettingsApplied ?? "Settings applied."); + } + catch (Exception ex) + { + SetStatus(ex.Message, true); + } + } + + private void SetStatus(string? text, bool isError = false) + { + _statusText.Text = text ?? string.Empty; + _statusText.Foreground = isError ? new SolidColorBrush(Colors.IndianRed) : null; + } +} From 847a75f715ac8fb45c094056fa6e25147df354ca Mon Sep 17 00:00:00 2001 From: CryoTechnic Date: Tue, 18 Aug 2026 02:39:05 -0400 Subject: [PATCH 03/20] Hydrate background activity notification state Expose `Progress` and `Status` on `IBackgroundActivity` and use them when wiring notification handlers in `BackgroundActivityManager`. This immediately initializes notification text/progress for already-running activities instead of waiting for the next event, preventing placeholder content from lingering. --- .../Classes/EventsManagement/BackgroundActivityManager.cs | 6 ++++++ CollapseLauncher/Classes/Interfaces/IBackgroundActivity.cs | 2 ++ 2 files changed, 8 insertions(+) diff --git a/CollapseLauncher/Classes/EventsManagement/BackgroundActivityManager.cs b/CollapseLauncher/Classes/EventsManagement/BackgroundActivityManager.cs index 2ec01077c..d1d83fc86 100644 --- a/CollapseLauncher/Classes/EventsManagement/BackgroundActivityManager.cs +++ b/CollapseLauncher/Classes/EventsManagement/BackgroundActivityManager.cs @@ -259,6 +259,12 @@ private static void AttachEventToNotification(PresetConfig presetConfig, IBackgr activity!.ProgressChanged += progressChangedEventHandler; activity!.StatusChanged += statusChangedEventHandler; + // The notification can be attached after the operation has already + // started. Hydrate it immediately instead of waiting for another + // progress/status event and leaving the placeholder text visible. + progressChangedEventHandler(activity, activity.Progress); + statusChangedEventHandler(activity, activity.Status); + activity.FlushingTrigger += (_, _) => { activity.ProgressChanged -= progressChangedEventHandler; diff --git a/CollapseLauncher/Classes/Interfaces/IBackgroundActivity.cs b/CollapseLauncher/Classes/Interfaces/IBackgroundActivity.cs index 3eeda615b..ba378bf23 100644 --- a/CollapseLauncher/Classes/Interfaces/IBackgroundActivity.cs +++ b/CollapseLauncher/Classes/Interfaces/IBackgroundActivity.cs @@ -11,6 +11,8 @@ internal interface IBackgroundActivity event EventHandler StatusChanged; event EventHandler FlushingTrigger; + TotalPerFileProgress Progress { get; } + TotalPerFileStatus Status { get; } bool IsRunning { get; } UIElement ParentUI { get; } void CancelRoutine(); From 6ddfd39fa12a7358558f501ba0c0fdc399088d43 Mon Sep 17 00:00:00 2001 From: CryoTechnic Date: Wed, 19 Aug 2026 16:05:37 -0400 Subject: [PATCH 04/20] Reset install speed baseline on state changes Add a reusable speed-calculator reset helper in `ProgressBase` and use it in plugin install progress updates to avoid bogus speed deltas after state transitions. The wrapper now resets its download baseline when install state changes, computes per-tick bytes only from monotonic progress, and clears speed/time-left values appropriately. It also ensures completed installs report zero time left and clamp per-file progress to 100%. --- .../Classes/Interfaces/Class/ProgressBase.cs | 7 ++++ .../Plugins/PluginGameInstallWrapper.cs | 37 +++++++++++++++++-- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/CollapseLauncher/Classes/Interfaces/Class/ProgressBase.cs b/CollapseLauncher/Classes/Interfaces/Class/ProgressBase.cs index 9ca682bfa..88f3d842a 100644 --- a/CollapseLauncher/Classes/Interfaces/Class/ProgressBase.cs +++ b/CollapseLauncher/Classes/Interfaces/Class/ProgressBase.cs @@ -467,6 +467,13 @@ protected virtual void UpdateProgressCopyStream(long currentPosition, int read, protected double CalculateSpeed(long receivedBytes) => CalculateSpeed(receivedBytes, ref _scLastSpeed, ref _scLastReceivedBytes, ref _scLastTick); + protected void ResetSpeedCalculator() + { + Interlocked.Exchange(ref _scLastReceivedBytes, 0); + Interlocked.Exchange(ref _scLastTick, Environment.TickCount64); + _scLastSpeed = 0; + } + protected static double CalculateSpeed(long receivedBytes, ref double lastSpeedToUse, ref long lastReceivedBytesToUse, ref long lastTickToUse) { long currentTick = Environment.TickCount64 - lastTickToUse + 1; diff --git a/CollapseLauncher/Classes/Plugins/PluginGameInstallWrapper.cs b/CollapseLauncher/Classes/Plugins/PluginGameInstallWrapper.cs index f6fc94a65..c060d5371 100644 --- a/CollapseLauncher/Classes/Plugins/PluginGameInstallWrapper.cs +++ b/CollapseLauncher/Classes/Plugins/PluginGameInstallWrapper.cs @@ -73,6 +73,7 @@ public override string GamePath private InstallProgressState _currentInstallState = InstallProgressState.Idle; private string _lastActivityStatus = string.Empty; private float _lastLoggedPercentage = -1f; + private bool _resetSpeedBaseline = true; private PerFileProgressCallbackNative? _perFileProgressDelegate; private GCHandle _perFileProgressGcHandle; @@ -558,7 +559,18 @@ private void UpdateProgressCallback(in InstallProgress delegateProgress) long downloadedBytes = delegateProgress.DownloadedBytes; long downloadedBytesTotal = delegateProgress.TotalBytesToDownload; - long readDownload = delegateProgress.DownloadedBytes - _updateProgressProperty.LastDownloaded; + long readDownload = 0; + if (_resetSpeedBaseline) + { + _resetSpeedBaseline = false; + _updateProgressProperty.LastDownloaded = downloadedBytes; + } + else if (downloadedBytes >= _updateProgressProperty.LastDownloaded) + { + readDownload = downloadedBytes - _updateProgressProperty.LastDownloaded; + _updateProgressProperty.LastDownloaded = downloadedBytes; + } + double currentSpeed = CalculateSpeed(readDownload); Progress.ProgressAllSizeCurrent = downloadedBytes; @@ -605,8 +617,6 @@ or InstallProgressState.Verify : 0; } - _updateProgressProperty.LastDownloaded = downloadedBytes; - PublishProgressUi(updateProgressBar: true); } } @@ -623,7 +633,26 @@ private void UpdateStatusCallback(InstallProgressState delegateState) { using (_updateStatusLock.EnterScope()) { - _currentInstallState = delegateState; + if (_currentInstallState != delegateState) + { + _currentInstallState = delegateState; + _resetSpeedBaseline = true; + ResetSpeedCalculator(); + Progress.ProgressAllSpeed = 0; + } + + if (delegateState == InstallProgressState.Completed) + { + Progress.ProgressAllTimeLeft = TimeSpan.Zero; + if (Progress.ProgressPerFileSizeTotal > 0) + { + Progress.ProgressPerFilePercentage = Math.Min(100d, + ConverterTool.ToPercentage( + Progress.ProgressPerFileSizeTotal, + Progress.ProgressPerFileSizeCurrent)); + } + } + ApplyActivityStatusFromProperty(); PublishProgressUi(updateProgressBar: true); } From 3945b9dc2be5294cd8a5cb308fdee688f5d9c854 Mon Sep 17 00:00:00 2001 From: CryoTechnic Date: Wed, 19 Aug 2026 16:46:00 -0400 Subject: [PATCH 05/20] Bump submodules --- Hi3Helper.TaskScheduler/packages.lock.json | 101 ++++++++++++++++++++- 1 file changed, 98 insertions(+), 3 deletions(-) diff --git a/Hi3Helper.TaskScheduler/packages.lock.json b/Hi3Helper.TaskScheduler/packages.lock.json index 0849386bc..9fed7d551 100644 --- a/Hi3Helper.TaskScheduler/packages.lock.json +++ b/Hi3Helper.TaskScheduler/packages.lock.json @@ -15,10 +15,105 @@ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12" } }, - "hi3helper.win32": { - "type": "Project", + "Fody": { + "type": "Direct", + "requested": "[6.9.3, )", + "resolved": "6.9.3", + "contentHash": "1CUGgFdyECDKgi5HaUBhdv6k+VG9Iy4OCforGfHyar3xQXAJypZkzymgKtWj/4SPd6nSG0Qi7NH71qHrDSZLaA==" + }, + "Microsoft.NETFramework.ReferenceAssemblies": { + "type": "Direct", + "requested": "[1.0.3, )", + "resolved": "1.0.3", + "contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==", + "dependencies": { + "Microsoft.NETFramework.ReferenceAssemblies.net462": "1.0.3" + } + }, + "System.Net.Http": { + "type": "Direct", + "requested": "[4.3.4, )", + "resolved": "4.3.4", + "contentHash": "aOa2d51SEbmM+H+Csw7yJOuNZoHkrP2XnAurye5HWYgGVVU54YZDvsLUYRv6h18X3sPnjNCANmN7ZhIPiqMcjA==", + "dependencies": { + "System.Security.Cryptography.X509Certificates": "4.3.0" + } + }, + "System.Text.RegularExpressions": { + "type": "Direct", + "requested": "[4.3.1, )", + "resolved": "4.3.1", + "contentHash": "N0kNRrWe4+nXOWlpLT4LAY5brb8caNFlUuIRpraCVMDLYutKkol1aV079rQjLuSxKMJT2SpBQsYX9xbcTMmzwg==" + }, + "TaskScheduler": { + "type": "Direct", + "requested": "[2.12.2, )", + "resolved": "2.12.2", + "contentHash": "glpAb3VrwfdAofp6PIyAzL0ZeTV7XUJ8muu0oZoTeyU5jtk2sMJ6QAMRRuFbovcaj+SBJiEUGklxIWOqQoxshA==" + }, + "Microsoft.NETFramework.ReferenceAssemblies.net462": { + "type": "Transitive", + "resolved": "1.0.3", + "contentHash": "IzAV30z22ESCeQfxP29oVf4qEo8fBGXLXSU6oacv/9Iqe6PzgHDKCaWfwMBak7bSJQM0F5boXWoZS+kChztRIQ==" + }, + "System.Security.Cryptography.Algorithms": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "W1kd2Y8mYSCgc3ULTAZ0hOP2dSdG5YauTb1089T0/kRcN2MpSAW1izOFROrJgxSlMn3ArsgHXagigyi+ibhevg==", + "dependencies": { + "System.Security.Cryptography.Primitives": "4.3.0" + } + }, + "System.Security.Cryptography.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "1DEWjZZly9ae9C79vFwqaO5kaOlI5q+3/55ohmq/7dpDyDfc8lYe7YVxJUZ5MF/NtbkRjwFRo14yM4OEo9EmDw==" + }, + "System.Security.Cryptography.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "7bDIyVFNL/xKeFHjhobUAQqSpJq9YTOpbEs6mR233Et01STBMXNAc/V+BM6dwYGc95gVh/Zf+iVXWzj3mE8DWg==" + }, + "System.Security.Cryptography.X509Certificates": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "t2Tmu6Y2NtJ2um0RtcuhP7ZdNNxXEgUm2JeoA/0NvlMjAhKCnM1NX07TDl3244mVp3QU6LPEhT3HTtH1uF7IYw==", + "dependencies": { + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0" + } + } + }, + ".NETFramework,Version=v4.6.2/win-x64": { + "System.Net.Http": { + "type": "Direct", + "requested": "[4.3.4, )", + "resolved": "4.3.4", + "contentHash": "aOa2d51SEbmM+H+Csw7yJOuNZoHkrP2XnAurye5HWYgGVVU54YZDvsLUYRv6h18X3sPnjNCANmN7ZhIPiqMcjA==", + "dependencies": { + "System.Security.Cryptography.X509Certificates": "4.3.0" + } + }, + "System.Security.Cryptography.Algorithms": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "W1kd2Y8mYSCgc3ULTAZ0hOP2dSdG5YauTb1089T0/kRcN2MpSAW1izOFROrJgxSlMn3ArsgHXagigyi+ibhevg==", + "dependencies": { + "System.Security.Cryptography.Primitives": "4.3.0" + } + }, + "System.Security.Cryptography.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "1DEWjZZly9ae9C79vFwqaO5kaOlI5q+3/55ohmq/7dpDyDfc8lYe7YVxJUZ5MF/NtbkRjwFRo14yM4OEo9EmDw==" + }, + "System.Security.Cryptography.X509Certificates": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "t2Tmu6Y2NtJ2um0RtcuhP7ZdNNxXEgUm2JeoA/0NvlMjAhKCnM1NX07TDl3244mVp3QU6LPEhT3HTtH1uF7IYw==", "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "[10.0.12, )" + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0" } } } From 90c9c1a7f2feff63a9df045ac16b6b1115fd71d7 Mon Sep 17 00:00:00 2001 From: CryoTechnic Date: Wed, 19 Aug 2026 18:29:16 -0400 Subject: [PATCH 06/20] Fix opacity causing elements not to render correctly --- .../Pages/GameSettingsPages/PluginGameSettingsPage.cs | 9 ++++++++- .../Pages/GameSettingsPages/PluginGameSettingsPage.xaml | 5 +++++ 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.xaml diff --git a/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.cs b/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.cs index 31d9f03a3..8778c0e9c 100644 --- a/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.cs +++ b/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.cs @@ -1,5 +1,6 @@ using CollapseLauncher.Plugins; using CollapseLauncher.Helper; +using CollapseLauncher.GameManagement.ImageBackground; using Hi3Helper.Plugin.Core.UI.Settings; using Hi3Helper.Plugin.Core.Utility; using Microsoft.UI; @@ -16,7 +17,7 @@ namespace CollapseLauncher.Pages; /// /// Renders the declarative game settings page exposed by a v0.1.6 plugin. /// -public sealed class PluginGameSettingsPage : Page +public sealed partial class PluginGameSettingsPage : Page { private readonly GameSettingsExtension.GameSettingsContext _context; private readonly TextBlock _statusText = new() @@ -28,6 +29,12 @@ public sealed class PluginGameSettingsPage : Page public PluginGameSettingsPage() { + InitializeComponent(); + + ImageBackgroundManager.Shared.IsBackgroundElevated = true; + ImageBackgroundManager.Shared.ForegroundOpacity = 0d; + ImageBackgroundManager.Shared.SmokeOpacity = 1d; + NavigationCacheMode = Microsoft.UI.Xaml.Navigation.NavigationCacheMode.Disabled; if (GetCurrentGameProperty().GameVersion.GamePreset is not PluginPresetConfigWrapper preset) diff --git a/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.xaml b/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.xaml new file mode 100644 index 000000000..9b59cc711 --- /dev/null +++ b/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.xaml @@ -0,0 +1,5 @@ + + From e46deea69446eb1e9e901be2a5ec2a2179e8c6cf Mon Sep 17 00:00:00 2001 From: CryoTechnic Date: Sun, 6 Sep 2026 23:16:45 -0400 Subject: [PATCH 07/20] Improve plugin settings layout Refactor GSP into a responsive multi-column layout while keeping informational sections full width --- .../PluginGameSettingsPage.cs | 108 ++++++++++++------ 1 file changed, 76 insertions(+), 32 deletions(-) diff --git a/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.cs b/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.cs index 8778c0e9c..0c8dc08fc 100644 --- a/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.cs +++ b/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.cs @@ -6,9 +6,11 @@ using Microsoft.UI; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Automation; using Microsoft.UI.Xaml.Media; using System; using System.Globalization; +using Microsoft.UI.Text; using static CollapseLauncher.Statics.GamePropertyVault; #nullable enable @@ -73,11 +75,20 @@ private UIElement CreateContent() }); } + Grid sectionGrid = new() { ColumnSpacing = 32, RowSpacing = 28 }; foreach (GameSettingsSection section in page.Sections) { - sectionsPanel.Children.Add(CreateSection(section)); + // Informational sections, including plugin warnings, remain full width. + if (section.Entries.Count == 0) + sectionsPanel.Children.Add(CreateSection(section)); + else + sectionGrid.Children.Add(CreateSection(section)); } + sectionsPanel.Children.Add(sectionGrid); + sectionGrid.SizeChanged += (_, args) => ArrangeGrid(sectionGrid, args.NewSize.Width >= 900 ? 2 : 1); + ArrangeGrid(sectionGrid, 1); + ScrollViewer scrollViewer = new() { Content = sectionsPanel, @@ -90,8 +101,9 @@ private UIElement CreateContent() Padding = new Thickness(32, 16, 32, 16), Background = Application.Current.Resources["GameSettingsApplyGridBrush"] as Brush }; - applyPanel.ColumnDefinitions.Add(new ColumnDefinition()); applyPanel.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + applyPanel.ColumnDefinitions.Add(new ColumnDefinition()); + Grid.SetColumn(_statusText, 1); applyPanel.Children.Add(_statusText); Button applyButton = new() @@ -102,7 +114,7 @@ private UIElement CreateContent() Style = Application.Current.Resources["AccentButtonStyle"] as Style }; applyButton.Click += OnApply; - Grid.SetColumn(applyButton, 1); + Grid.SetColumn(applyButton, 0); applyPanel.Children.Add(applyButton); Grid.SetRow(applyPanel, 1); root.Children.Add(applyPanel); @@ -110,9 +122,29 @@ private UIElement CreateContent() return root; } + private static void ArrangeGrid(Grid grid, int columns) + { + int rows = (grid.Children.Count + columns - 1) / columns; + if (grid.ColumnDefinitions.Count == columns && grid.RowDefinitions.Count == rows) + return; + + grid.ColumnDefinitions.Clear(); + grid.RowDefinitions.Clear(); + for (int column = 0; column < columns; column++) + grid.ColumnDefinitions.Add(new ColumnDefinition()); + for (int row = 0; row < rows; row++) + grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); + for (int index = 0; index < grid.Children.Count; index++) + { + FrameworkElement child = (FrameworkElement)grid.Children[index]; + Grid.SetColumn(child, index % columns); + Grid.SetRow(child, index / columns); + } + } + private FrameworkElement CreateSection(GameSettingsSection section) { - StackPanel panel = new() { Spacing = 8 }; + StackPanel panel = new() { Spacing = 12, VerticalAlignment = VerticalAlignment.Top }; panel.Children.Add(new TextBlock { Text = section.Title, @@ -130,45 +162,50 @@ private FrameworkElement CreateSection(GameSettingsSection section) }); } + Grid entries = new() { ColumnSpacing = 16, RowSpacing = 16 }; foreach (GameSettingEntry entry in section.Entries) + entries.Children.Add(CreateEntry(entry)); + + if (entries.Children.Count > 0) { - panel.Children.Add(CreateEntry(entry)); + entries.SizeChanged += (_, args) => + ArrangeGrid(entries, args.NewSize.Width >= 520 ? 3 : args.NewSize.Width >= 340 ? 2 : 1); + ArrangeGrid(entries, 1); + panel.Children.Add(entries); } - return panel; } private FrameworkElement CreateEntry(GameSettingEntry entry) { - Grid card = new() - { - Padding = new Thickness(16, 12, 16, 12), - ColumnSpacing = 24, - Background = Application.Current.Resources["CardBackgroundFillColorDefaultBrush"] as Brush, - CornerRadius = new CornerRadius(8) - }; - card.ColumnDefinitions.Add(new ColumnDefinition()); - card.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + FrameworkElement editor = CreateEditor(entry); + editor.HorizontalAlignment = HorizontalAlignment.Stretch; + editor.VerticalAlignment = VerticalAlignment.Top; + AutomationProperties.SetName(editor, entry.Title); - StackPanel text = new() { VerticalAlignment = VerticalAlignment.Center, Spacing = 2 }; - text.Children.Add(new TextBlock { Text = entry.Title, TextWrapping = TextWrapping.Wrap }); + StackPanel panel = new() { Spacing = 6 }; + if (entry.Kind != GameSettingKind.Toggle) + { + panel.Children.Add(new TextBlock + { + Text = entry.Title, + FontWeight = FontWeights.SemiBold, + TextWrapping = TextWrapping.Wrap + }); + } + panel.Children.Add(editor); if (!string.IsNullOrWhiteSpace(entry.Description)) { - text.Children.Add(new TextBlock + panel.Children.Add(new TextBlock { Text = entry.Description, + FontSize = 12, Opacity = 0.72, TextWrapping = TextWrapping.Wrap }); + AutomationProperties.SetHelpText(editor, entry.Description); } - card.Children.Add(text); - - FrameworkElement editor = CreateEditor(entry); - editor.MinWidth = entry.Kind is GameSettingKind.Toggle ? 0 : 180; - editor.VerticalAlignment = VerticalAlignment.Center; - Grid.SetColumn(editor, 1); - card.Children.Add(editor); - return card; + return panel; } private FrameworkElement CreateEditor(GameSettingEntry entry) => entry.Kind switch @@ -181,10 +218,15 @@ private FrameworkElement CreateEntry(GameSettingEntry entry) _ => throw new ArgumentOutOfRangeException(nameof(entry.Kind)) }; - private ToggleSwitch CreateToggle(GameSettingEntry entry) + private CheckBox CreateToggle(GameSettingEntry entry) { - ToggleSwitch control = new() { IsOn = bool.TryParse(entry.Value, out bool value) && value }; - control.Toggled += (_, _) => SetValue(entry.Key, control.IsOn ? bool.TrueString : bool.FalseString); + CheckBox control = new() + { + Content = new TextBlock { Text = entry.Title, TextWrapping = TextWrapping.Wrap }, + IsChecked = bool.TryParse(entry.Value, out bool value) && value + }; + control.Checked += (_, _) => SetValue(entry.Key, bool.TrueString); + control.Unchecked += (_, _) => SetValue(entry.Key, bool.FalseString); return control; } @@ -222,8 +264,10 @@ private Slider CreateSlider(GameSettingEntry entry) Minimum = entry.Minimum, Maximum = entry.Maximum, StepFrequency = entry.Step, - Value = ParseNumber(entry.Value, entry.Minimum), - Width = 220 + Style = Application.Current.Resources["FatSliderStyle"] as Style, + TickFrequency = Math.Max(entry.Step, (entry.Maximum - entry.Minimum) / 10), + TickPlacement = Microsoft.UI.Xaml.Controls.Primitives.TickPlacement.Outside, + Value = ParseNumber(entry.Value, entry.Minimum) }; control.ValueChanged += (_, args) => SetValue(entry.Key, args.NewValue.ToString(CultureInfo.InvariantCulture)); @@ -232,7 +276,7 @@ private Slider CreateSlider(GameSettingEntry entry) private ComboBox CreateChoice(GameSettingEntry entry) { - ComboBox control = new(); + ComboBox control = new() { CornerRadius = new CornerRadius(14) }; foreach (GameSettingChoice choice in entry.Choices ?? []) { ComboBoxItem item = new() { Content = choice.Title, Tag = choice.Value }; From db9f9b1d693f684722b41842c2e0b2b11a65faa4 Mon Sep 17 00:00:00 2001 From: CryoTechnic Date: Tue, 29 Sep 2026 19:42:03 -0400 Subject: [PATCH 08/20] Use FFmpeg for YUV420 10-bit video streams - Extract FFmpeg decoder checks into `RequiresFFmpegDecoder` and reuse it across codec detection and loader paths. - Treat 10-bit YUV420 (`AV_PIX_FMT_YUV420P10LE/BE`) as requiring FFmpeg, improving handling for High 10 H.264 backgrounds that Windows decoders may not support. - Tighten background reload behavior by comparing decoder mode before skipping reloads. - Layer setup now starts hidden, restores opacity after attach, and only sets a foreground overlay for video backgrounds. --- .../ImageBackgroundManager.CodecDetect.cs | 11 +++++++++-- .../ImageBackgroundManager.Loaders.cs | 13 ++++++++----- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.CodecDetect.cs b/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.CodecDetect.cs index ed4a97354..1156e2df3 100644 --- a/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.CodecDetect.cs +++ b/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.CodecDetect.cs @@ -49,9 +49,8 @@ public partial class ImageBackgroundManager // -- Check using FFmpeg if available MediaSupport codecInfo = FFmpegPInvoke.GetMediaSupport(filePath); - PixelColorModel pixelFormat = codecInfo.VideoPixelFormatInfo.ColorModel; if ((GlobalIsUseFFmpeg && GlobalIsFFmpegAvailable) || - (codecInfo.Video.DecoderAvailable && pixelFormat.HasFlag(PixelColorModel.Rgb))) + RequiresFFmpegDecoder(codecInfo)) { return (true, true, true); } @@ -77,6 +76,14 @@ public partial class ImageBackgroundManager #endregion + private static bool RequiresFFmpegDecoder(MediaSupport codecInfo) + { + // Windows' H.264 decoder does not support the High 10 profile used by some backgrounds. + return codecInfo.Video.DecoderAvailable && + (codecInfo.VideoPixelFormatInfo.ColorModel.HasFlag(PixelColorModel.Rgb) || + codecInfo.VideoPixelFormatInfo.Format is AVPixelFormat.AV_PIX_FMT_YUV420P10LE or AVPixelFormat.AV_PIX_FMT_YUV420P10BE); + } + private static bool IsMediaFileExtensionSupported(ReadOnlySpan filePath) { return IsImageMediaFileExtensionSupported(filePath) || diff --git a/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.Loaders.cs b/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.Loaders.cs index 01fa62784..003848bca 100644 --- a/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.Loaders.cs +++ b/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.Loaders.cs @@ -1,4 +1,4 @@ -using CollapseLauncher.Extension; +using CollapseLauncher.Extension; using CollapseLauncher.Helper; using CollapseLauncher.Helper.Background; using CollapseLauncher.Helper.Image; @@ -154,8 +154,7 @@ public void PreloadBackground(PresetConfig? preset, Grid? presenterGrid) bool isVideo = IsVideoMediaFileExtensionSupported(primaryBg ?? staticBgUrl!); MediaSupport codecInfo = isVideo ? FFmpegPInvoke.GetMediaSupport(localBackground) : default; - PixelColorModel pixelFormat = codecInfo.VideoPixelFormatInfo.ColorModel; - if (isUseFFmpeg || (codecInfo.Video.DecoderAvailable && pixelFormat.HasFlag(PixelColorModel.Rgb))) + if (isUseFFmpeg || RequiresFFmpegDecoder(codecInfo)) { isUseFFmpeg = true; } @@ -367,6 +366,7 @@ private void SpawnImageLayer(Uri? overlayFilePath, if (!forceReload && CurrentBackgroundElement != null && + CurrentBackgroundElement.UseFfmpegDecoder == (forceFFmpeg || (GlobalIsUseFFmpeg && GlobalIsFFmpegAvailable)) && _displayedContext != null) { if (context.Equals(_displayedContext)) @@ -448,9 +448,10 @@ private LayeredBackgroundImage CreateLayerElement(Uri? { LayeredBackgroundImage layerElement = new() { + Opacity = 0, BackgroundSource = backgroundFilePath, BackgroundStaticSource = backgroundStaticFilePath, - ForegroundSource = overlayFilePath, + ForegroundSource = isVideo ? overlayFilePath : null, UseFfmpegDecoder = forceFFmpeg || (GlobalIsUseFFmpeg && GlobalIsFFmpegAvailable), Tag = context, ParallaxResetOnUnfocused = false, @@ -568,6 +569,8 @@ private void LayerElementOnLoaded(LayeredBackgroundImage layerElement) PresenterGrid.Background = null; } + layerElement.Opacity = 1; + if (CurrentIsEnableBackgroundAutoPlay && WindowUtility.CurrentWindowIsVisible) { Play(false); @@ -676,4 +679,4 @@ private async Task GetMediaAccentColor(object? context) SentryHelper.ExceptionHandler(ex); } } -} \ No newline at end of file +} From bebdbc8f774711ab638054f099e99e371eb1bac4 Mon Sep 17 00:00:00 2001 From: CryoTechnic Date: Tue, 29 Sep 2026 19:42:46 -0400 Subject: [PATCH 09/20] Harden video frame init and native cleanup - Improves `LayeredBackgroundImage` video initialization robustness by logging `MediaFailed` events, handling stale/invalid playback session size reads, and making render-target setup fail-safe with explicit success/failure returns. - Add proper native interface acquisition for DrawImageToRect, ensures acquired COM references are released correctly, detaches NaturalVideoSizeChanged on teardown, and clears frame player state during disposal to prevent leaks and stale callbacks. --- ...BackgroundImage.Events.FrameInitializer.cs | 72 +++++++++++++------ 1 file changed, 52 insertions(+), 20 deletions(-) diff --git a/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameInitializer.cs b/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameInitializer.cs index ccbe668d6..f6fd16654 100644 --- a/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameInitializer.cs +++ b/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameInitializer.cs @@ -18,6 +18,8 @@ namespace CollapseLauncher.XAMLs.Theme.CustomControls; public partial class LayeredBackgroundImage { + private MediaPlayer? _videoFramePlayer; + #region Initializers private void InitializeVideoPlayer() @@ -40,6 +42,7 @@ private void InitializeVideoPlayer() Interlocked.Exchange(ref _videoPlayer, player); player.MediaOpened += InitializeVideoFrameOnMediaOpened; + player.MediaFailed += VideoPlayer_OnMediaFailed; if (!_useSafeFrameRenderer) { @@ -54,37 +57,56 @@ private void InitializeVideoPlayer() } } - private void InitializeRenderTargetSize(MediaPlaybackSession playbackSession) + private void VideoPlayer_OnMediaFailed(MediaPlayer sender, MediaPlayerFailedEventArgs args) + { + Logger.LogWriteLine($"[LayeredBackgroundImage::MediaFailed] Decoder: {(UseFfmpegDecoder ? "FFmpeg" : "Windows")}; {args.Error}: {args.ErrorMessage}\r\n{args.ExtendedErrorCode}", + LogType.Error, + true); + } + + private bool InitializeRenderTargetSize(MediaPlaybackSession playbackSession) { try { - double currentCanvasWidth = playbackSession.NaturalVideoWidth; - double currentCanvasHeight = playbackSession.NaturalVideoHeight; - - _canvasWidth = (int)currentCanvasWidth; - _canvasHeight = (int)currentCanvasHeight; - _canvasRenderSize = new Rect(0, 0, _canvasWidth, _canvasHeight); + int width = (int)playbackSession.NaturalVideoWidth; + int height = (int)playbackSession.NaturalVideoHeight; // In some occasion, MediaPlayer reportedly 0x0px size which causes E_INVALIDARG while rendering frame // if FFmpeg source is used. So, use size reported by FFmpeg instead. - if (_canvasRenderSize != default || _videoFfmpegMediaSource == null) return; + if ((width <= 0 || height <= 0) && _videoFfmpegMediaSource?.CurrentVideoStream is { } videoStream) + { + width = videoStream.PixelWidth; + height = videoStream.PixelHeight; + } - _canvasWidth = _videoFfmpegMediaSource.CurrentVideoStream.PixelWidth; - _canvasHeight = _videoFfmpegMediaSource.CurrentVideoStream.PixelHeight; + if (width <= 0 || height <= 0) return false; + + _canvasWidth = width; + _canvasHeight = height; _canvasRenderSize = new Rect(0, 0, _canvasWidth, _canvasHeight); + return true; + } + catch (COMException ex) when ((uint)ex.HResult == 0xC00D3E85u) + { + // A queued MediaOpened callback can outlive the playback session. + return false; } catch (Exception ex) { - Logger.LogWriteLine($"[LayeredBackgroundImage::DisposeVideoPlayer] {ex}", + Logger.LogWriteLine($"[LayeredBackgroundImage::InitializeRenderTargetSize] {ex}", LogType.Error, true); + return false; } } - private unsafe void InitializeRenderTarget() + private unsafe bool InitializeRenderTarget() { try { + // Play() can arrive before MediaOpened. Read this player's size before creating any surfaces. + if (_videoPlayer == null || !InitializeRenderTargetSize(_videoPlayer.PlaybackSession)) return false; + Interlocked.Exchange(ref _isBlockVideoFrameDraw, 1); // Block frame drawing routine DisposeRenderTarget(_canvasImageSource == null); // Always ensure the previous render target has been disposed @@ -102,16 +124,18 @@ private unsafe void InitializeRenderTarget() Interlocked.Exchange(ref _useSafeFrameRenderer, false); if (_useSafeFrameRenderer) { - return; + return true; } - _canvasRenderTargetNativePtr = ((IWinRTObject)_canvasRenderTarget).NativeObject.ThisPtr; _canvasImageSourceNativePtr = ((IWinRTObject)_canvasImageSource).NativeObject.ThisPtr; - - ((IWinRTObject)_canvasRenderTarget).NativeObject.TryAs(typeof(IDirect3DSurface).GUID, out _canvasRenderTargetAsSurfacePtr); try { + // DrawImageToRect requires ICanvasBitmap, not the render target's default ICanvasRenderTarget interface. + Guid canvasBitmapIid = new("C57532ED-709E-4AC2-86BE-A1EC3A7FA8FE"); + Marshal.ThrowExceptionForHR(((IWinRTObject)_canvasRenderTarget).NativeObject.TryAs(canvasBitmapIid, out _canvasRenderTargetNativePtr)); + Marshal.ThrowExceptionForHR(((IWinRTObject)_canvasRenderTarget).NativeObject.TryAs(typeof(IDirect3DSurface).GUID, out _canvasRenderTargetAsSurfacePtr)); + if (_functionTableBeginDraw == null! || _functionTableDrawImage == null! || _functionTableCopyFrameToVideoSurface == null! || @@ -129,6 +153,7 @@ private unsafe void InitializeRenderTarget() } catch (Exception e) { + NullifyRenderTargetNativePointers(); Interlocked.Exchange(ref _useSafeFrameRenderer, true); // Fallback _functionTableBeginDraw = null; @@ -139,12 +164,14 @@ private unsafe void InitializeRenderTarget() LogType.Error, true); } + return true; } catch (Exception e) { Logger.LogWriteLine($"[LayeredBackgroundImage::InitializeRenderTarget] FATAL: {e}", LogType.Error, true); + return false; } finally { @@ -163,6 +190,8 @@ private unsafe void InitializeRenderTarget() private void DetachVideoPlayerEvents(MediaPlayer player) { player.MediaOpened -= InitializeVideoFrameOnMediaOpened; + player.MediaFailed -= VideoPlayer_OnMediaFailed; + player.PlaybackSession.NaturalVideoSizeChanged -= InitializeVideoFrameOnSizeChanged; player.VideoFrameAvailable -= NotifyVideoLoaded; player.VideoFrameAvailable -= VideoPlayer_VideoFrameAvailableUnsafe; player.VideoFrameAvailable -= VideoPlayer_VideoFrameAvailableSafe; @@ -206,6 +235,7 @@ private void DisposeVideoPlayer(bool disposeRenderImageSource = true) } finally { + _videoFramePlayer = null; Interlocked.Exchange(ref _isVideoInitialized, 0); } } @@ -270,13 +300,15 @@ private void NullifyRenderTargetNativePointers() { // -- Note to myself @neon-nyan: // Release IDirect3DSurface reference first, then dispose the whole CanvasRenderTarget. - // This is necessary as we just cast/QueryInterface the _canvasRenderTargetNativePtr (which - // is obtained from IWinRTObject's direct pointer) into IDirect3DSurface. If not released, + // This is necessary as we queried the render target's native object for + // IDirect3DSurface. If not released, // the reference on the IWinRTObject will not be zeroed, causing leak. if (_canvasRenderTargetAsSurfacePtr != nint.Zero) Marshal.Release(Interlocked.Exchange(ref _canvasRenderTargetAsSurfacePtr, nint.Zero)); - // -- Nullify IWinRTObject direct pointers. - Interlocked.Exchange(ref _canvasRenderTargetNativePtr, nint.Zero); + // -- Release the ICanvasBitmap reference acquired for DrawImageToRect. + if (_canvasRenderTargetNativePtr != nint.Zero) Marshal.Release(Interlocked.Exchange(ref _canvasRenderTargetNativePtr, nint.Zero)); + + // -- Nullify the borrowed IWinRTObject pointer. Interlocked.Exchange(ref _canvasImageSourceNativePtr, nint.Zero); } From cc870b7c8b52af13c44223d93a720be20363be90 Mon Sep 17 00:00:00 2001 From: CryoTechnic Date: Tue, 29 Sep 2026 19:43:27 -0400 Subject: [PATCH 10/20] Harden layered video frame initialization - Improve LayeredBackgroundImage media handling to avoid race conditions and invalid state during video setup. - Render target initialization now fails safely and resets init state, FFmpeg source opening validates the active player before wiring it, and media-open flow now responds to natural size changes before creating the frame image. - Fix UI layering by hiding the foreground grid for static backgrounds (and restoring it for normal mode), and allows non-autoplay video to start when play was explicitly requested. --- ...redBackgroundImage.Events.FrameRenderer.cs | 6 ++- .../LayeredBackgroundImage.Events.Loaders.cs | 40 ++++++++++++++++--- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameRenderer.cs b/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameRenderer.cs index 6159befcd..1807e1854 100644 --- a/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameRenderer.cs +++ b/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameRenderer.cs @@ -367,7 +367,11 @@ public void InitializeAndPlayVideoView(Action? actionOnPlay // Only initialize once. if (Interlocked.Exchange(ref _isVideoInitialized, 1) == 0) { - InitializeRenderTarget(); + if (!InitializeRenderTarget()) + { + Interlocked.Exchange(ref _isVideoInitialized, 0); + return; + } // Seek to last position if source was the same if (_videoPlayer.CanSeek && diff --git a/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.Loaders.cs b/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.Loaders.cs index 9d67ba424..5df0bc4ad 100644 --- a/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.Loaders.cs +++ b/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.Loaders.cs @@ -151,6 +151,8 @@ private static void BackgroundSource_OnChange(DependencyObject d, DependencyProp private static void BackgroundSource_UseStatic(LayeredBackgroundImage element) { Grid grid = element._backgroundGrid; + // Static artwork already includes the foreground used by the video. + element._foregroundGrid.Visibility = Visibility.Collapsed; element.LoadFromSourceAsyncDetached(BackgroundStaticSourceProperty, nameof(BackgroundStretch), @@ -166,6 +168,7 @@ private static void BackgroundSource_UseStatic(LayeredBackgroundImage element) private static void BackgroundSource_UseNormal(LayeredBackgroundImage element) { Grid grid = element._backgroundGrid; + element._foregroundGrid.Visibility = Visibility.Visible; element.LoadFromSourceAsyncDetached(BackgroundSourceProperty, nameof(BackgroundStretch), nameof(BackgroundHorizontalAlignment), @@ -435,8 +438,15 @@ private static async Task LoadVideoFromSourceAsync( return false; } - await ffmpegMediaSource.OpenWithMediaPlayerAsync(player); + if (player == null || !ReferenceEquals(player, instance._videoPlayer) || player.IsObjectDisposed()) + { + ffmpegMediaSource.Dispose(); + isError = true; + return false; + } + Interlocked.Exchange(ref instance._videoFfmpegMediaSource, ffmpegMediaSource); + await ffmpegMediaSource.OpenWithMediaPlayerAsync(player); // HACK: // Sometimes the media source isn't ready when the window just get restored from minimized state, @@ -533,11 +543,19 @@ private void InitializeVideoFrameOnMediaOpened(MediaPlayer sender, object args) void Impl() { - if (!IsLoaded) + if (!IsLoaded || !ReferenceEquals(sender, _videoPlayer) || sender.IsObjectDisposed()) { return; } + if (ReferenceEquals(_videoFramePlayer, sender)) return; + + MediaPlaybackSession session = sender.PlaybackSession; + session.NaturalVideoSizeChanged -= InitializeVideoFrameOnSizeChanged; + session.NaturalVideoSizeChanged += InitializeVideoFrameOnSizeChanged; + if (!InitializeRenderTargetSize(session)) return; + session.NaturalVideoSizeChanged -= InitializeVideoFrameOnSizeChanged; + // Update Media Duration and Update Binding to it. SetValue(MediaDurationProperty, sender.NaturalDuration); SetValue(IsCurrentMediaSeekableProperty, sender.CanSeek); @@ -563,18 +581,29 @@ void Impl() VerticalAlignmentProperty, BindingMode.OneWay); - InitializeRenderTargetSize(sender.PlaybackSession); - // Register events image.Loaded += Image_VideoFrameOnLoaded; image.Unloaded += Image_VideoFrameOnUnloaded; // Add to children image.Transitions.Add(new ContentThemeTransition()); + _videoFramePlayer = sender; _backgroundGrid.Children.Add(image); } } + private void InitializeVideoFrameOnSizeChanged(MediaPlaybackSession sender, object args) + { + DispatcherQueue.TryEnqueue(() => + { + if (_videoPlayer is { } player && !player.IsObjectDisposed() && + ReferenceEquals(sender, player.PlaybackSession)) + { + InitializeVideoFrameOnMediaOpened(player, args); + } + }); + } + private static void Image_VideoFrameOnLoaded(object sender, RoutedEventArgs e) { if (sender is not Image { Tag: ValueTuple parentGrid }) @@ -582,7 +611,8 @@ private static void Image_VideoFrameOnLoaded(object sender, RoutedEventArgs e) return; } - if (!parentGrid.Item2.IsVideoAutoplay) return; + if (!parentGrid.Item2.IsVideoAutoplay && + parentGrid.Item2._videoState is not (VideoState.RequestedPlay or VideoState.Playing)) return; Interlocked.Exchange(ref parentGrid.Item2._videoState, VideoState.Playing); parentGrid.Item2.InitializeAndPlayVideoView(); From bdaaf91feb1640fa00bafb4e04e60c9ea4a67d14 Mon Sep 17 00:00:00 2001 From: CryoTechnic Date: Tue, 29 Sep 2026 19:45:28 -0400 Subject: [PATCH 11/20] bump submodules --- Hi3Helper.EncTool | 2 +- Hi3Helper.Win32 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Hi3Helper.EncTool b/Hi3Helper.EncTool index 47eeba101..d4e9e4c31 160000 --- a/Hi3Helper.EncTool +++ b/Hi3Helper.EncTool @@ -1 +1 @@ -Subproject commit 47eeba101eb7a34d8eb94251ad022ad7215f1d04 +Subproject commit d4e9e4c31235699125ad11332652f978dffe443b diff --git a/Hi3Helper.Win32 b/Hi3Helper.Win32 index ff564bec9..a296dc19d 160000 --- a/Hi3Helper.Win32 +++ b/Hi3Helper.Win32 @@ -1 +1 @@ -Subproject commit ff564bec95894ef8da5e4dfb4f08e3057080cdfc +Subproject commit a296dc19d7ba72c8b92f2d6a7b71ef5aeac5fd07 From 961f6d2640e04cad9b78f5c33fa279860bee7c9f Mon Sep 17 00:00:00 2001 From: CryoTechnic Date: Wed, 30 Sep 2026 12:51:38 -0400 Subject: [PATCH 12/20] Queue `UseFfmpegDecoder` property via `DispatcherQueue` --- .../LayeredBackgroundImage.Events.FrameInitializer.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameInitializer.cs b/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameInitializer.cs index f6fd16654..1a35aa3c6 100644 --- a/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameInitializer.cs +++ b/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameInitializer.cs @@ -59,9 +59,12 @@ private void InitializeVideoPlayer() private void VideoPlayer_OnMediaFailed(MediaPlayer sender, MediaPlayerFailedEventArgs args) { - Logger.LogWriteLine($"[LayeredBackgroundImage::MediaFailed] Decoder: {(UseFfmpegDecoder ? "FFmpeg" : "Windows")}; {args.Error}: {args.ErrorMessage}\r\n{args.ExtendedErrorCode}", - LogType.Error, - true); + DispatcherQueue.TryEnqueue(() => + { + Logger.LogWriteLine($"[LayeredBackgroundImage::MediaFailed] Decoder: {(UseFfmpegDecoder ? "FFmpeg" : "Windows")}; {args.Error}: {args.ErrorMessage}\r\n{args.ExtendedErrorCode}", + LogType.Error, + true); + }); } private bool InitializeRenderTargetSize(MediaPlaybackSession playbackSession) From 07fc91bd6278f4145b810fc273c5fa03d2a2e949 Mon Sep 17 00:00:00 2001 From: CryoTechnic Date: Wed, 30 Sep 2026 13:01:49 -0400 Subject: [PATCH 13/20] Fix HRESULT handling in unsafe video frame copying --- .../LayeredBackgroundImage.Events.FrameRenderer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameRenderer.cs b/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameRenderer.cs index 1807e1854..69a178221 100644 --- a/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameRenderer.cs +++ b/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameRenderer.cs @@ -109,7 +109,7 @@ private unsafe void VideoPlayer_VideoFrameAvailableUnsafe(MediaPlayer sender, ob return; } - _functionTableCopyFrameToVideoSurface(_videoPlayerPtr, _canvasRenderTargetAsSurfacePtr); + SwapChainPanelHelper.MediaPlayerCopyFrameUnsafe(_videoPlayerPtr, _canvasRenderTargetAsSurfacePtr); drawingSessionPpv = SwapChainPanelHelper .CanvasSessionDrawUnsafe(_canvasImageSourceNativePtr, _canvasRenderTargetNativePtr, From e6d253a723ca612a19c6dca286229e09e04a6fd3 Mon Sep 17 00:00:00 2001 From: CryoTechnic Date: Wed, 30 Sep 2026 13:13:55 -0400 Subject: [PATCH 14/20] Guard game settings navigation init state --- .../XAMLs/MainApp/MainPage.Navigation.cs | 13 ++++++++++--- .../GameSettingsPages/PluginGameSettingsPage.cs | 10 +++++----- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/CollapseLauncher/XAMLs/MainApp/MainPage.Navigation.cs b/CollapseLauncher/XAMLs/MainApp/MainPage.Navigation.cs index ad9a7a1cf..3cfcba9d9 100644 --- a/CollapseLauncher/XAMLs/MainApp/MainPage.Navigation.cs +++ b/CollapseLauncher/XAMLs/MainApp/MainPage.Navigation.cs @@ -380,10 +380,17 @@ typeOfPageObj is Type currentPageType && { case Type pageType: { - if (pageType.Name.EndsWith("GameSettingsPage") && - !IsGameInstalled()) + if (pageType.Name.EndsWith("GameSettingsPage")) { - pageType = typeof(NotInstalledPage); + if (!IsLoadRegionComplete) + { + return false; + } + + if (!IsGameInstalled()) + { + pageType = typeof(NotInstalledPage); + } } LauncherFrame.Navigate(pageType, null, transitionInfo ?? new DrillInNavigationTransitionInfo()); diff --git a/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.cs b/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.cs index 0c8dc08fc..846c6254b 100644 --- a/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.cs +++ b/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.cs @@ -33,19 +33,19 @@ public PluginGameSettingsPage() { InitializeComponent(); - ImageBackgroundManager.Shared.IsBackgroundElevated = true; - ImageBackgroundManager.Shared.ForegroundOpacity = 0d; - ImageBackgroundManager.Shared.SmokeOpacity = 1d; - NavigationCacheMode = Microsoft.UI.Xaml.Navigation.NavigationCacheMode.Disabled; - if (GetCurrentGameProperty().GameVersion.GamePreset is not PluginPresetConfigWrapper preset) + if (GetCurrentGameProperty().GamePreset is not PluginPresetConfigWrapper preset) { throw new InvalidOperationException("The current game preset is not provided by a plugin"); } _context = preset.GameSettingsContext; Content = CreateContent(); + + ImageBackgroundManager.Shared.IsBackgroundElevated = true; + ImageBackgroundManager.Shared.ForegroundOpacity = 0d; + ImageBackgroundManager.Shared.SmokeOpacity = 1d; } private UIElement CreateContent() From 7950349723bb5b0535b514b0f362b59c13064e69 Mon Sep 17 00:00:00 2001 From: CryoTechnic Date: Wed, 30 Sep 2026 13:36:55 -0400 Subject: [PATCH 15/20] Fix layered video render locking --- ...BackgroundImage.Events.FrameInitializer.cs | 25 ++++++----- ...redBackgroundImage.Events.FrameRenderer.cs | 41 +++++++++++-------- 2 files changed, 40 insertions(+), 26 deletions(-) diff --git a/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameInitializer.cs b/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameInitializer.cs index 1a35aa3c6..2b1c88f7f 100644 --- a/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameInitializer.cs +++ b/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameInitializer.cs @@ -105,12 +105,13 @@ private bool InitializeRenderTargetSize(MediaPlaybackSession playbackSession) private unsafe bool InitializeRenderTarget() { + bool initialized = false; + Interlocked.Exchange(ref _isBlockVideoFrameDraw, 1); try { // Play() can arrive before MediaOpened. Read this player's size before creating any surfaces. if (_videoPlayer == null || !InitializeRenderTargetSize(_videoPlayer.PlaybackSession)) return false; - Interlocked.Exchange(ref _isBlockVideoFrameDraw, 1); // Block frame drawing routine DisposeRenderTarget(_canvasImageSource == null); // Always ensure the previous render target has been disposed _canvasDevice ??= CanvasDevice.GetSharedDevice(); @@ -127,7 +128,7 @@ private unsafe bool InitializeRenderTarget() Interlocked.Exchange(ref _useSafeFrameRenderer, false); if (_useSafeFrameRenderer) { - return true; + return initialized = true; } _canvasImageSourceNativePtr = ((IWinRTObject)_canvasImageSource).NativeObject.ThisPtr; @@ -167,7 +168,7 @@ private unsafe bool InitializeRenderTarget() LogType.Error, true); } - return true; + return initialized = true; } catch (Exception e) { @@ -178,11 +179,11 @@ private unsafe bool InitializeRenderTarget() } finally { - if (_canvasImageSource != null) + if (initialized && _canvasImageSource != null) { SetRenderImageSource(_canvasImageSource.Source); } - Interlocked.Exchange(ref _isBlockVideoFrameDraw, 0); // Unblock frame drawing routine + Interlocked.Exchange(ref _isBlockVideoFrameDraw, initialized ? 0 : 1); } } @@ -277,12 +278,16 @@ private void CanvasDevice_OnDeviceLost() LogType.Warning, true); - // -- Nullify _canvasImageSource so CanvasDevice and other dependencies are reinitialized too - Interlocked.Exchange(ref _canvasImageSource, null!); - InitializeRenderTarget(); + lock (_videoFrameRenderLock) + { + Interlocked.Exchange(ref _isBlockVideoFrameDraw, 1); + NullifyRenderTargetNativePointers(); + // Drop the image source only after invalidating its borrowed native pointer. + Interlocked.Exchange(ref _canvasImageSource, null!); + InitializeRenderTarget(); - // Try to unlock video draw progress (if a throw happened inside frame drawing routine) - Interlocked.Exchange(ref _isVideoFrameDrawInProgress, 0); + Interlocked.Exchange(ref _isVideoFrameDrawInProgress, 0); + } } #endregion diff --git a/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameRenderer.cs b/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameRenderer.cs index 69a178221..57de58569 100644 --- a/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameRenderer.cs +++ b/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameRenderer.cs @@ -1,4 +1,4 @@ -using CollapseLauncher.Extension; +using CollapseLauncher.Extension; using FFmpegInteropX; using Hi3Helper; using Hi3Helper.Data; @@ -65,6 +65,7 @@ private static ref readonly Guid IMediaPlayer5_IID private nint _canvasRenderTargetAsSurfacePtr; private int _isBlockVideoFrameDraw = 1; + private readonly object _videoFrameRenderLock = new(); private int _isVideoFrameDrawInProgress; private int _isVideoInitialized; @@ -91,6 +92,7 @@ private static ref readonly Guid IMediaPlayer5_IID private unsafe void VideoPlayer_VideoFrameAvailableUnsafe(MediaPlayer sender, object args) { nint drawingSessionPpv = nint.Zero; + var functionTableDispose = _functionTableDispose; try { if (Interlocked.Decrement(ref _videoToSkipFrames) > 0) @@ -98,24 +100,28 @@ private unsafe void VideoPlayer_VideoFrameAvailableUnsafe(MediaPlayer sender, ob return; } - if (_isBlockVideoFrameDraw == 1 || - _canvasImageSourceNativePtr == nint.Zero || - _canvasRenderTargetNativePtr == nint.Zero || - Interlocked.Exchange(ref _isVideoFrameDrawInProgress, 1) == 1) + lock (_videoFrameRenderLock) { + if (_isBlockVideoFrameDraw == 1 || + _canvasImageSourceNativePtr == nint.Zero || + _canvasRenderTargetNativePtr == nint.Zero || + Interlocked.Exchange(ref _isVideoFrameDrawInProgress, 1) == 1) + { #if DEBUG - Logger.LogWriteLine($@"Skipping frame at: {sender.Position:hh\:mm\:ss\.ffffff}"); + Logger.LogWriteLine($@"Skipping frame at: {sender.Position:hh\:mm\:ss\.ffffff}"); #endif - return; - } + return; + } - SwapChainPanelHelper.MediaPlayerCopyFrameUnsafe(_videoPlayerPtr, _canvasRenderTargetAsSurfacePtr); - drawingSessionPpv = SwapChainPanelHelper - .CanvasSessionDrawUnsafe(_canvasImageSourceNativePtr, - _canvasRenderTargetNativePtr, - _functionTableBeginDraw, - _functionTableDrawImage, - in _canvasRenderSize); + functionTableDispose = _functionTableDispose; + SwapChainPanelHelper.MediaPlayerCopyFrameUnsafe(_videoPlayerPtr, _canvasRenderTargetAsSurfacePtr); + drawingSessionPpv = SwapChainPanelHelper + .CanvasSessionDrawUnsafe(_canvasImageSourceNativePtr, + _canvasRenderTargetNativePtr, + _functionTableBeginDraw, + _functionTableDrawImage, + in _canvasRenderSize); + } } // Device lost error. If happened, reinitialize render target catch (COMException comEx) when ((uint)comEx.HResult is 0x887A0005u or 0x802B0020u or 0x8899000Cu) @@ -143,7 +149,10 @@ void DrawFrame() { try { - SwapChainPanelHelper.DrawingDisposeUnsafe(drawingSessionPpv, _functionTableDispose); + lock (_videoFrameRenderLock) + { + SwapChainPanelHelper.DrawingDisposeUnsafe(drawingSessionPpv, functionTableDispose); + } } // Device lost error. If happened, reinitialize render target catch (COMException comEx) when ((uint)comEx.HResult is 0x887A0005u or 0x802B0020u or 0x8899000Cu) From a6828bb368d2013e6817fcea46e03e420c73ce1e Mon Sep 17 00:00:00 2001 From: Kemal Setya Adhi Date: Thu, 1 Oct 2026 02:03:28 +0700 Subject: [PATCH 16/20] Unify plugin game settings design Use GameSettingsPageBase --- .../PluginGameSettingsPage.cs | 145 ++++++++++++------ .../PluginGameSettingsPage.xaml | 78 +++++++++- 2 files changed, 171 insertions(+), 52 deletions(-) diff --git a/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.cs b/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.cs index 846c6254b..839393a49 100644 --- a/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.cs +++ b/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.cs @@ -1,16 +1,22 @@ -using CollapseLauncher.Plugins; -using CollapseLauncher.Helper; +using CollapseLauncher.DiscordPresence; +using CollapseLauncher.Extension; using CollapseLauncher.GameManagement.ImageBackground; +using CollapseLauncher.Helper; +using CollapseLauncher.Helper.Animation; +using CollapseLauncher.Plugins; +using Hi3Helper; using Hi3Helper.Plugin.Core.UI.Settings; using Hi3Helper.Plugin.Core.Utility; +using Hi3Helper.Shared.ClassStruct; using Microsoft.UI; +using Microsoft.UI.Text; using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Automation; +using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Media; using System; using System.Globalization; -using Microsoft.UI.Text; +using System.Numerics; using static CollapseLauncher.Statics.GamePropertyVault; #nullable enable @@ -19,7 +25,7 @@ namespace CollapseLauncher.Pages; /// /// Renders the declarative game settings page exposed by a v0.1.6 plugin. /// -public sealed partial class PluginGameSettingsPage : Page +public sealed partial class PluginGameSettingsPage { private readonly GameSettingsExtension.GameSettingsContext _context; private readonly TextBlock _statusText = new() @@ -29,11 +35,14 @@ public sealed partial class PluginGameSettingsPage : Page TextWrapping = TextWrapping.Wrap }; - public PluginGameSettingsPage() + public PluginGameSettingsPage() : base(null!, null!) { InitializeComponent(); - NavigationCacheMode = Microsoft.UI.Xaml.Navigation.NavigationCacheMode.Disabled; + ApplyButton.Translation = new Vector3(0, 0, 32); + GameSettingsApplyGrid.Translation = new Vector3(0, 0, 64); + + SetApplyTextContainer(GameSettingsApplyGrid, gridColumn: 1); if (GetCurrentGameProperty().GamePreset is not PluginPresetConfigWrapper preset) { @@ -41,23 +50,13 @@ public PluginGameSettingsPage() } _context = preset.GameSettingsContext; - Content = CreateContent(); - - ImageBackgroundManager.Shared.IsBackgroundElevated = true; - ImageBackgroundManager.Shared.ForegroundOpacity = 0d; - ImageBackgroundManager.Shared.SmokeOpacity = 1d; } - private UIElement CreateContent() + private FrameworkElement? CreateContent(out Exception? error) { - if (!_context.TryGetPage(out GameSettingsPage? page, out Exception? error) || page == null) + if (!_context.TryGetPage(out GameSettingsPage? page, out error) || page == null) { - return new TextBlock - { - Margin = new Thickness(32, 40, 32, 32), - Text = error?.Message ?? "This plugin did not provide a game settings page.", - TextWrapping = TextWrapping.Wrap - }; + return null; } Grid root = new(); @@ -96,29 +95,6 @@ private UIElement CreateContent() }; root.Children.Add(scrollViewer); - Grid applyPanel = new() - { - Padding = new Thickness(32, 16, 32, 16), - Background = Application.Current.Resources["GameSettingsApplyGridBrush"] as Brush - }; - applyPanel.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); - applyPanel.ColumnDefinitions.Add(new ColumnDefinition()); - Grid.SetColumn(_statusText, 1); - applyPanel.Children.Add(_statusText); - - Button applyButton = new() - { - Content = Locale.Current.Lang?._GameSettingsPage?.ApplyBtn ?? "Apply settings", - MinWidth = 144, - CornerRadius = new CornerRadius(16), - Style = Application.Current.Resources["AccentButtonStyle"] as Style - }; - applyButton.Click += OnApply; - Grid.SetColumn(applyButton, 0); - applyPanel.Children.Add(applyButton); - Grid.SetRow(applyPanel, 1); - root.Children.Add(applyPanel); - return root; } @@ -313,22 +289,91 @@ private void SetValue(string key, string value) } } - private void OnApply(object sender, RoutedEventArgs args) + private void SetStatus(string? text, bool isError = false) + { + _statusText.Text = text ?? string.Empty; + _statusText.Foreground = isError ? new SolidColorBrush(Colors.IndianRed) : null; + } + + #region Overriden Methods from Game Settings Base Page + protected override async void OnLoaded(object? sender, RoutedEventArgs args) { try { - _context.Apply(); - SetStatus(Locale.Current.Lang?._GameSettingsPage?.SettingsApplied ?? "Settings applied."); + GameInstallStateEnum gameStatus = await CurrentGameProperty.GameVersion!.GetGameState(); + + ImageBackgroundManager.Shared.IsBackgroundElevated = true; + ImageBackgroundManager.Shared.ForegroundOpacity = 0d; + ImageBackgroundManager.Shared.SmokeOpacity = 1d; + + if (CurrentGameProperty.IsGameRunning) + { +#if !GSPBYPASSGAMERUNNING + Overlay.Visibility = Visibility.Visible; + PageContent.Visibility = Visibility.Collapsed; + OverlayTitle.Text = Locale.Current.Lang?._GameSettingsPage?.OverlayGameRunningTitle; + OverlaySubtitle.Text = Locale.Current.Lang?._GameSettingsPage?.OverlayGameRunningSubtitle; +#endif + } + else if (gameStatus + is GameInstallStateEnum.NotInstalled + or GameInstallStateEnum.NeedsUpdate + or GameInstallStateEnum.InstalledHavePlugin + or GameInstallStateEnum.GameBroken) + { + Overlay.Visibility = Visibility.Visible; + PageContent.Visibility = Visibility.Collapsed; + OverlayTitle.Text = Locale.Current.Lang?._GameSettingsPage?.OverlayNotInstalledTitle; + OverlaySubtitle.Text = Locale.Current.Lang?._GameSettingsPage?.OverlayNotInstalledSubtitle; + } + else + { +#if !DISABLEDISCORD + InnerLauncherConfig.AppDiscordPresence.SetActivity(DiscordActivityType.GameSettings); +#endif + + FrameworkElement? elementContent = CreateContent(out Exception? createPageError); + if (elementContent != null) + { + ElementContent.AddElementToGridRowColumn(elementContent); + SettingsScrollViewer.EnableImplicitAnimation(true); + return; + } + + Overlay.Visibility = Visibility.Visible; + PageContent.Visibility = Visibility.Collapsed; + OverlayTitle.Text = "Game Settings feature is not available."; + OverlaySubtitle.Text = createPageError?.Message ?? "This plugin did not provide a game settings page."; + + if (createPageError != null) + { + Logger.LogWriteLine($"An error has occurred while trying to create plugin-based game settings page.\r\n{createPageError}", LogType.Error, true); + ErrorSender.SendException(createPageError); + } + } } catch (Exception ex) { - SetStatus(ex.Message, true); + Logger.LogWriteLine($"FATAL ERROR!!!\r\n{ex}", LogType.Error, true); + ErrorSender.SendException(ex); } } - private void SetStatus(string? text, bool isError = false) + protected override void OnUnloaded(object? sender, RoutedEventArgs args) { - _statusText.Text = text ?? string.Empty; - _statusText.Foreground = isError ? new SolidColorBrush(Colors.IndianRed) : null; } + + protected override void OnApplyButtonClick(object sender, RoutedEventArgs args) + { + try + { + _context.Apply(); + SetApplyTextStatus("Lang._StarRailGameSettingsPage.SettingsApplied"); + } + catch (Exception ex) + { + SetApplyTextStatus(ex.Message, true); + } + } + #endregion } diff --git a/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.xaml b/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.xaml index 9b59cc711..774b7a303 100644 --- a/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.xaml +++ b/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.xaml @@ -1,5 +1,79 @@ - + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:conv="using:CollapseLauncher.Pages" + xmlns:helper="using:CollapseLauncher.Helper" + xmlns:extension="using:CollapseLauncher.Extension" + NavigationCacheMode="Disabled"> + + + + + + + + + + + + + + + + + + + + + + + From b550fb42d7fcc9a3aa27542557241cffa5186727 Mon Sep 17 00:00:00 2001 From: CryoTechnic Date: Wed, 30 Sep 2026 17:24:47 -0400 Subject: [PATCH 17/20] Add custom launch args section to plugin settings --- .../PluginGameSettingsPage.cs | 39 +++++++++++++++++++ .../PluginGameSettingsPage.xaml | 1 + 2 files changed, 40 insertions(+) diff --git a/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.cs b/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.cs index 839393a49..459db06e2 100644 --- a/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.cs +++ b/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.cs @@ -85,6 +85,7 @@ public PluginGameSettingsPage() : base(null!, null!) } sectionsPanel.Children.Add(sectionGrid); + sectionsPanel.Children.Add(CreateCustomArgumentsSection()); sectionGrid.SizeChanged += (_, args) => ArrangeGrid(sectionGrid, args.NewSize.Width >= 900 ? 2 : 1); ArrangeGrid(sectionGrid, 1); @@ -98,6 +99,44 @@ public PluginGameSettingsPage() : base(null!, null!) return root; } + private FrameworkElement CreateCustomArgumentsSection() + { + var settings = GetCurrentGameProperty().GameSettings; + StackPanel panel = new() { Spacing = 12 }; + panel.Children.Add(new TextBlock + { + Text = Locale.Current.Lang?._GameSettingsPage?.CustomArgs_Title ?? "Custom arguments", + Style = Application.Current.Resources["SubtitleTextBlockStyle"] as Style, + TextWrapping = TextWrapping.Wrap + }); + ToggleSwitch toggle = new() + { + Header = Locale.Current.Lang?._GameSettingsPage?.CustomArgs_Subtitle ?? "Use custom launch arguments", + IsOn = settings?.SettingsCollapseMisc.UseCustomArguments ?? false, + IsEnabled = settings != null + }; + TextBox arguments = new() + { + Text = settings?.SettingsCustomArgument.CustomArgumentValue ?? string.Empty, + IsEnabled = toggle.IsOn, + HorizontalAlignment = HorizontalAlignment.Stretch + }; + AutomationProperties.SetName(arguments, "Custom launch arguments"); + toggle.Toggled += (_, _) => + { + if (settings == null) return; + settings.SettingsCollapseMisc.UseCustomArguments = toggle.IsOn; + arguments.IsEnabled = toggle.IsOn; + }; + arguments.TextChanged += (_, _) => + { + if (settings != null) + settings.SettingsCustomArgument.CustomArgumentValue = arguments.Text; + }; + panel.Children.Add(toggle); + panel.Children.Add(arguments); + return panel; + } private static void ArrangeGrid(Grid grid, int columns) { int rows = (grid.Children.Count + columns - 1) / columns; diff --git a/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.xaml b/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.xaml index 774b7a303..bb529ba3e 100644 --- a/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.xaml +++ b/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/PluginGameSettingsPage.xaml @@ -10,6 +10,7 @@ From ff37d4d6dd33f8b351514a91645b9b3a7cc6852b Mon Sep 17 00:00:00 2001 From: Kemal Setya Adhi Date: Fri, 2 Oct 2026 02:03:01 +0700 Subject: [PATCH 18/20] [BG] Revert changes (Fix UI freeze due to lock and other changes) + Change CanvasVirtualImageSource -> CanvasImageSource + Simplify atomic check on the renderer state + Handle device lost and context lost catch correctly + Remove lock-based spin --- ...BackgroundImage.Events.FrameInitializer.cs | 67 +++--- ...redBackgroundImage.Events.FrameRenderer.cs | 221 ++++++++---------- .../LayeredBackgroundImage.Events.Loaders.cs | 9 +- Hi3Helper.Win32 | 2 +- 4 files changed, 124 insertions(+), 175 deletions(-) diff --git a/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameInitializer.cs b/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameInitializer.cs index 2b1c88f7f..183be758e 100644 --- a/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameInitializer.cs +++ b/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameInitializer.cs @@ -42,7 +42,6 @@ private void InitializeVideoPlayer() Interlocked.Exchange(ref _videoPlayer, player); player.MediaOpened += InitializeVideoFrameOnMediaOpened; - player.MediaFailed += VideoPlayer_OnMediaFailed; if (!_useSafeFrameRenderer) { @@ -57,16 +56,6 @@ private void InitializeVideoPlayer() } } - private void VideoPlayer_OnMediaFailed(MediaPlayer sender, MediaPlayerFailedEventArgs args) - { - DispatcherQueue.TryEnqueue(() => - { - Logger.LogWriteLine($"[LayeredBackgroundImage::MediaFailed] Decoder: {(UseFfmpegDecoder ? "FFmpeg" : "Windows")}; {args.Error}: {args.ErrorMessage}\r\n{args.ExtendedErrorCode}", - LogType.Error, - true); - }); - } - private bool InitializeRenderTargetSize(MediaPlaybackSession playbackSession) { try @@ -115,10 +104,10 @@ private unsafe bool InitializeRenderTarget() DisposeRenderTarget(_canvasImageSource == null); // Always ensure the previous render target has been disposed _canvasDevice ??= CanvasDevice.GetSharedDevice(); - _canvasImageSource ??= new CanvasVirtualImageSource(_canvasDevice, - _canvasWidth, - _canvasHeight, - 96f); + _canvasImageSource ??= new CanvasImageSource(_canvasDevice, + _canvasWidth, + _canvasHeight, + 96f); _canvasRenderTarget ??= new CanvasRenderTarget(_canvasDevice, _canvasWidth, @@ -145,14 +134,14 @@ private unsafe bool InitializeRenderTarget() _functionTableCopyFrameToVideoSurface == null! || _functionTableDispose == null!) { - SwapChainPanelHelper.GetDirectNativeDelegateForDrawRoutine(_canvasImageSourceNativePtr, - _canvasRenderTargetNativePtr, - _videoPlayerPtr, - out _functionTableBeginDraw, - out _functionTableDrawImage, - out _functionTableCopyFrameToVideoSurface, - out _functionTableDispose, - in _canvasRenderSize); + SwapChainPanelHelper.GetFuncForCanvasImageSource(_canvasImageSourceNativePtr, + _canvasRenderTargetNativePtr, + _videoPlayerPtr, + out _functionTableBeginDraw, + out _functionTableDrawImage, + out _functionTableCopyFrameToVideoSurface, + out _functionTableDispose, + in _canvasRenderSize); } } catch (Exception e) @@ -181,7 +170,7 @@ private unsafe bool InitializeRenderTarget() { if (initialized && _canvasImageSource != null) { - SetRenderImageSource(_canvasImageSource.Source); + SetRenderImageSource(_canvasImageSource); } Interlocked.Exchange(ref _isBlockVideoFrameDraw, initialized ? 0 : 1); } @@ -193,13 +182,12 @@ private unsafe bool InitializeRenderTarget() private void DetachVideoPlayerEvents(MediaPlayer player) { - player.MediaOpened -= InitializeVideoFrameOnMediaOpened; - player.MediaFailed -= VideoPlayer_OnMediaFailed; + player.MediaOpened -= InitializeVideoFrameOnMediaOpened; player.PlaybackSession.NaturalVideoSizeChanged -= InitializeVideoFrameOnSizeChanged; - player.VideoFrameAvailable -= NotifyVideoLoaded; - player.VideoFrameAvailable -= VideoPlayer_VideoFrameAvailableUnsafe; - player.VideoFrameAvailable -= VideoPlayer_VideoFrameAvailableSafe; - player.PlaybackSession.PositionChanged -= MediaDurationPosition_OnChangedBridge; + player.VideoFrameAvailable -= NotifyVideoLoaded; + player.VideoFrameAvailable -= VideoPlayerUnsafe_OnVideoFrameAvailable; + player.VideoFrameAvailable -= VideoPlayerSafe_OnVideoFrameAvailable; + player.PlaybackSession.PositionChanged -= MediaDurationPosition_OnChangedBridge; } private void DisposeVideoPlayer(bool disposeRenderImageSource = true) @@ -278,16 +266,17 @@ private void CanvasDevice_OnDeviceLost() LogType.Warning, true); - lock (_videoFrameRenderLock) - { - Interlocked.Exchange(ref _isBlockVideoFrameDraw, 1); - NullifyRenderTargetNativePointers(); - // Drop the image source only after invalidating its borrowed native pointer. - Interlocked.Exchange(ref _canvasImageSource, null!); - InitializeRenderTarget(); + // Note from: @neon-nyan. + // -- Use atomic exchange instead of locking :) + if (Interlocked.Exchange(ref _isBlockVideoFrameDraw, 1) == 1) + return; - Interlocked.Exchange(ref _isVideoFrameDrawInProgress, 0); - } + NullifyRenderTargetNativePointers(); + // Drop the image source only after invalidating its borrowed native pointer. + Interlocked.Exchange(ref _canvasImageSource, null!); + InitializeRenderTarget(); + + Interlocked.Exchange(ref _isVideoFrameDrawInProgress, 0); } #endregion diff --git a/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameRenderer.cs b/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameRenderer.cs index 57de58569..c78c7768d 100644 --- a/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameRenderer.cs +++ b/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameRenderer.cs @@ -65,13 +65,12 @@ private static ref readonly Guid IMediaPlayer5_IID private nint _canvasRenderTargetAsSurfacePtr; private int _isBlockVideoFrameDraw = 1; - private readonly object _videoFrameRenderLock = new(); private int _isVideoFrameDrawInProgress; private int _isVideoInitialized; - private CanvasDevice? _canvasDevice; - private CanvasVirtualImageSource? _canvasImageSource; - private nint _canvasImageSourceNativePtr = nint.Zero; + private CanvasDevice? _canvasDevice; + private CanvasImageSource? _canvasImageSource; + private nint _canvasImageSourceNativePtr = nint.Zero; private int _canvasWidth; private int _canvasHeight; @@ -89,163 +88,125 @@ private static ref readonly Guid IMediaPlayer5_IID #region Video Frame Drawing - private unsafe void VideoPlayer_VideoFrameAvailableUnsafe(MediaPlayer sender, object args) + private unsafe void VideoPlayerUnsafe_OnVideoFrameAvailable(MediaPlayer sender, object args) { - nint drawingSessionPpv = nint.Zero; - var functionTableDispose = _functionTableDispose; - try - { - if (Interlocked.Decrement(ref _videoToSkipFrames) > 0) - { - return; - } + if (_isBlockVideoFrameDraw == 1) + return; - lock (_videoFrameRenderLock) - { - if (_isBlockVideoFrameDraw == 1 || - _canvasImageSourceNativePtr == nint.Zero || - _canvasRenderTargetNativePtr == nint.Zero || - Interlocked.Exchange(ref _isVideoFrameDrawInProgress, 1) == 1) - { + if (Interlocked.Exchange(ref _isVideoFrameDrawInProgress, 1) > 0) + { #if DEBUG - Logger.LogWriteLine($@"Skipping frame at: {sender.Position:hh\:mm\:ss\.ffffff}"); + Logger.LogWriteLine($@"Skipping frame at: {sender.Position:hh\:mm\:ss\.ffffff}"); #endif - return; - } + return; + } - functionTableDispose = _functionTableDispose; - SwapChainPanelHelper.MediaPlayerCopyFrameUnsafe(_videoPlayerPtr, _canvasRenderTargetAsSurfacePtr); - drawingSessionPpv = SwapChainPanelHelper + _functionTableCopyFrameToVideoSurface(_videoPlayerPtr, _canvasRenderTargetAsSurfacePtr); + DispatcherQueue.TryEnqueue(DispatcherQueuePriority.High, () => + { + StartDraw: + try + { + nint drawingSessionPpv = SwapChainPanelHelper .CanvasSessionDrawUnsafe(_canvasImageSourceNativePtr, _canvasRenderTargetNativePtr, _functionTableBeginDraw, _functionTableDrawImage, in _canvasRenderSize); + if (drawingSessionPpv != nint.Zero) + SwapChainPanelHelper.DrawingDisposeUnsafe(drawingSessionPpv, _functionTableDispose); } - } - // Device lost error. If happened, reinitialize render target - catch (COMException comEx) when ((uint)comEx.HResult is 0x887A0005u or 0x802B0020u or 0x8899000Cu) - { - DispatcherQueue.TryEnqueue(CanvasDevice_OnDeviceLost); - } - catch (COMException comEx) when ((uint)comEx.HResult is 0x88980801u) - { - // Try to unlock if any error caused by DCOMPOSITION_ERROR_SURFACE_NOT_BEING_RENDERED - Interlocked.Exchange(ref _isVideoFrameDrawInProgress, 0); - } - catch (Exception ex) - { - Logger.LogWriteLine($"[LayeredBackgroundImage::VideoPlayer_VideoFrameAvailableUnsafe|OtherThread] {ex}", - LogType.Error, - true); - } - finally - { - if (drawingSessionPpv != nint.Zero) + // Ignore DCOMPOSITION_ERROR_SURFACE_BEING_RENDERED + catch (COMException comEx) when (unchecked((uint)comEx.HResult) == 0x88980801u) { - DispatcherQueue?.TryEnqueue(DispatcherQueuePriority.High, DrawFrame); + // ignored } - void DrawFrame() + // Trying to recreate the context instead of re-creating the entire canvas + catch (COMException comEx) when (unchecked((uint)comEx.HResult) == 0x802B0020u) { - try - { - lock (_videoFrameRenderLock) - { - SwapChainPanelHelper.DrawingDisposeUnsafe(drawingSessionPpv, functionTableDispose); - } - } - // Device lost error. If happened, reinitialize render target - catch (COMException comEx) when ((uint)comEx.HResult is 0x887A0005u or 0x802B0020u or 0x8899000Cu) - { - DispatcherQueue.TryEnqueue(CanvasDevice_OnDeviceLost); - } - catch (COMException comEx) when ((uint)comEx.HResult is 0x88980801u) - { - // Try to unlock if any error caused by DCOMPOSITION_ERROR_SURFACE_NOT_BEING_RENDERED - Interlocked.Exchange(ref _isVideoFrameDrawInProgress, 0); - } - catch (Exception ex) - { - Logger.LogWriteLine($"[LayeredBackgroundImage::VideoPlayer_VideoFrameAvailableUnsafe|UIThread] {ex}", - LogType.Error, - true); - } - finally - { - Interlocked.Exchange(ref _isVideoFrameDrawInProgress, 0); - } + Logger.LogWriteLine($"[LayeredBackgroundImage::UnsafeOnVideoFrameAvailable] CanvasImageSource has lost its context. Recreating the context...\r\n{comEx}", + LogType.Error, + true); + + // Re-create the context and start redrawing. + _canvasImageSource?.Recreate(_canvasDevice); + goto StartDraw; } - } + // Device lost error. If happened, reinitialize render target + catch (COMException comEx) when (unchecked((uint)comEx.HResult) is 0x887A0005u or 0x8899000Cu) + { + Logger.LogWriteLine($"[LayeredBackgroundImage::UnsafeOnVideoFrameAvailable] Direct3D device is lost! Trying to reinitialize it...\r\n{comEx}", + LogType.Error, + true); + CanvasDevice_OnDeviceLost(); + } + catch (Exception ex) + { + Logger.LogWriteLine($"[LayeredBackgroundImage::UnsafeOnVideoFrameAvailable]" + + $"\r\n_videoPlayerPtr: 0x{_videoPlayerPtr:x8}" + + $"\r\n_canvasRenderTargetAsSurfacePtr: 0x{_canvasRenderTargetAsSurfacePtr:x8}" + + $"\r\n_canvasImageSourceNativePtr: 0x{_canvasImageSourceNativePtr:x8}" + + $"\r\n_canvasRenderTargetNativePtr: 0x{_canvasRenderTargetNativePtr:x8}" + + $"\r\n{ex}", + LogType.Error, + true); + } + finally + { + Interlocked.Exchange(ref _isVideoFrameDrawInProgress, 0); + } + }); } - private void VideoPlayer_VideoFrameAvailableSafe(MediaPlayer sender, object args) + private void VideoPlayerSafe_OnVideoFrameAvailable(MediaPlayer sender, object args) { - Unsafe.SkipInit(out CanvasDrawingSession? ds); + if (_isBlockVideoFrameDraw == 1) + return; - try + if (Interlocked.Exchange(ref _isVideoFrameDrawInProgress, 1) == 1) { - if (Interlocked.Decrement(ref _videoToSkipFrames) > 0) - { - return; - } - - if (_isBlockVideoFrameDraw == 1 || - _canvasImageSource == null! || - _canvasRenderTarget == null! || - Interlocked.Exchange(ref _isVideoFrameDrawInProgress, 1) == 1) - { #if DEBUG - Logger.LogWriteLine($@"Skipping frame at: {sender.Position:hh\:mm\:ss\.ffffff}"); + Logger.LogWriteLine($@"Skipping frame at: {sender.Position:hh\:mm\:ss\.ffffff}"); #endif - return; - } - - _videoPlayer?.CopyFrameToVideoSurface(_canvasRenderTarget); - ds = _canvasImageSource?.CreateDrawingSession(default, _canvasRenderSize); - ds?.DrawImage(_canvasRenderTarget, _canvasRenderSize); - } - // Device lost error. If happened, reinitialize render target - catch (COMException comEx) when ((uint)comEx.HResult is 0x887A0005u or 0x802B0020u or 0x8899000Cu) - { - DispatcherQueue.TryEnqueue(CanvasDevice_OnDeviceLost); - } - catch (COMException comEx) when ((uint)comEx.HResult is 0x88980801u) - { - // Try to unlock if any error caused by DCOMPOSITION_ERROR_SURFACE_NOT_BEING_RENDERED - Interlocked.Exchange(ref _isVideoFrameDrawInProgress, 0); - } - catch (Exception ex) - { - Logger.LogWriteLine($"[LayeredBackgroundImage::VideoPlayer_VideoFrameAvailableSafe|OtherThread] {ex}", - LogType.Error, - true); - } - finally - { - DispatcherQueue?.TryEnqueue(DispatcherQueuePriority.High, DrawFrame); + return; } - return; - - void DrawFrame() + _videoPlayer?.CopyFrameToVideoSurface(_canvasRenderTarget); + DispatcherQueue.TryEnqueue(DispatcherQueuePriority.High, () => { + StartDraw: try { - ds?.Dispose(); + using CanvasDrawingSession? ds = _canvasImageSource?.CreateDrawingSession(default, _canvasRenderSize); + ds?.DrawImage(_canvasRenderTarget, _canvasRenderSize); } - // Device lost error. If happened, reinitialize render target - catch (COMException comEx) when ((uint)comEx.HResult is 0x887A0005u or 0x802B0020u or 0x8899000Cu) + // Ignore DCOMPOSITION_ERROR_SURFACE_BEING_RENDERED + catch (COMException comEx) when (unchecked((uint)comEx.HResult) == 0x88980801u) { - CanvasDevice_OnDeviceLost(); + // ignored } - catch (COMException comEx) when ((uint)comEx.HResult is 0x88980801u) + // Trying to recreate the context instead of re-creating the entire canvas + catch (COMException comEx) when (unchecked((uint)comEx.HResult) == 0x802B0020u) { - // Try to unlock if any error caused by DCOMPOSITION_ERROR_SURFACE_NOT_BEING_RENDERED - Interlocked.Exchange(ref _isVideoFrameDrawInProgress, 0); + Logger.LogWriteLine($"[LayeredBackgroundImage::SafeOnVideoFrameAvailable] CanvasImageSource has lost its context. Recreating the context...\r\n{comEx}", + LogType.Error, + true); + + // Re-create the context and start redrawing. + _canvasImageSource?.Recreate(_canvasDevice); + goto StartDraw; + } + // Device lost error. If happened, reinitialize render target + catch (COMException comEx) when (unchecked((uint)comEx.HResult) is 0x887A0005u or 0x8899000Cu) + { + Logger.LogWriteLine($"[LayeredBackgroundImage::SafeOnVideoFrameAvailable] Direct3D device is lost! Trying to reinitialize it...\r\n{comEx}", + LogType.Error, + true); + CanvasDevice_OnDeviceLost(); } catch (Exception ex) { - Logger.LogWriteLine($"[LayeredBackgroundImage::VideoPlayer_VideoFrameAvailableSafe|UIThread] {ex}", + Logger.LogWriteLine($"[LayeredBackgroundImage::SafeOnVideoFrameAvailable] {ex}", LogType.Error, true); } @@ -253,7 +214,7 @@ void DrawFrame() { Interlocked.Exchange(ref _isVideoFrameDrawInProgress, 0); } - } + }); } #endregion @@ -432,8 +393,8 @@ public void InitializeAndPlayVideoView(Action? actionOnPlay void ActionVideoAfterPlay() { _videoPlayer.VideoFrameAvailable += !_useSafeFrameRenderer - ? VideoPlayer_VideoFrameAvailableUnsafe - : VideoPlayer_VideoFrameAvailableSafe; + ? VideoPlayerUnsafe_OnVideoFrameAvailable + : VideoPlayerSafe_OnVideoFrameAvailable; } } else if (BackgroundSource != null) diff --git a/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.Loaders.cs b/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.Loaders.cs index 5df0bc4ad..bd16915d4 100644 --- a/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.Loaders.cs +++ b/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.Loaders.cs @@ -19,11 +19,10 @@ using Windows.Media.Playback; using Windows.Storage.Streams; // ReSharper disable IdentifierTypo -#pragma warning disable IDE0051 -#pragma warning disable CS8321 // Local function is declared but never used - // ReSharper disable StringLiteralTypo // ReSharper disable CommentTypo +#pragma warning disable IDE0051 +#pragma warning disable CS8321 // Local function is declared but never used #pragma warning disable CsWinRT1032 #nullable enable @@ -459,8 +458,8 @@ private static async Task LoadVideoFromSourceAsync( // Unsubscribe frame renderer event to avoid double call, and then mark deinitialization. Interlocked.Exchange(ref instance._isVideoInitialized, 0); player?.VideoFrameAvailable -= !instance._useSafeFrameRenderer - ? instance.VideoPlayer_VideoFrameAvailableUnsafe - : instance.VideoPlayer_VideoFrameAvailableSafe; + ? instance.VideoPlayerUnsafe_OnVideoFrameAvailable + : instance.VideoPlayerSafe_OnVideoFrameAvailable; ffmpegMediaSource.Dispose(); sourceStreamRandom?.Dispose(); diff --git a/Hi3Helper.Win32 b/Hi3Helper.Win32 index a296dc19d..a44bcf38e 160000 --- a/Hi3Helper.Win32 +++ b/Hi3Helper.Win32 @@ -1 +1 @@ -Subproject commit a296dc19d7ba72c8b92f2d6a7b71ef5aeac5fd07 +Subproject commit a44bcf38e78839e0ad6a9b3e4fbec316a7c9be4b From f988e48b5aaaa40d7202383ae9c30251859070f6 Mon Sep 17 00:00:00 2001 From: Kemal Setya Adhi Date: Fri, 2 Oct 2026 02:54:31 +0700 Subject: [PATCH 19/20] [BG] Revert using CanvasVirtualImageSource Still had issues with hard crash while trying to re-create the device context for the CanvasImageSource --- ...BackgroundImage.Events.FrameInitializer.cs | 10 ++-- ...redBackgroundImage.Events.FrameRenderer.cs | 54 +++++++++++++------ 2 files changed, 44 insertions(+), 20 deletions(-) diff --git a/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameInitializer.cs b/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameInitializer.cs index 183be758e..2cc3d8172 100644 --- a/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameInitializer.cs +++ b/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameInitializer.cs @@ -104,10 +104,10 @@ private unsafe bool InitializeRenderTarget() DisposeRenderTarget(_canvasImageSource == null); // Always ensure the previous render target has been disposed _canvasDevice ??= CanvasDevice.GetSharedDevice(); - _canvasImageSource ??= new CanvasImageSource(_canvasDevice, - _canvasWidth, - _canvasHeight, - 96f); + _canvasImageSource ??= new CanvasVirtualImageSource(_canvasDevice, + _canvasWidth, + _canvasHeight, + 96f); _canvasRenderTarget ??= new CanvasRenderTarget(_canvasDevice, _canvasWidth, @@ -170,7 +170,7 @@ private unsafe bool InitializeRenderTarget() { if (initialized && _canvasImageSource != null) { - SetRenderImageSource(_canvasImageSource); + SetRenderImageSource(_canvasImageSource.Source); } Interlocked.Exchange(ref _isBlockVideoFrameDraw, initialized ? 0 : 1); } diff --git a/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameRenderer.cs b/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameRenderer.cs index c78c7768d..61a9e5ebf 100644 --- a/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameRenderer.cs +++ b/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameRenderer.cs @@ -68,9 +68,9 @@ private static ref readonly Guid IMediaPlayer5_IID private int _isVideoFrameDrawInProgress; private int _isVideoInitialized; - private CanvasDevice? _canvasDevice; - private CanvasImageSource? _canvasImageSource; - private nint _canvasImageSourceNativePtr = nint.Zero; + private CanvasDevice? _canvasDevice; + private CanvasVirtualImageSource? _canvasImageSource; + private nint _canvasImageSourceNativePtr = nint.Zero; private int _canvasWidth; private int _canvasHeight; @@ -102,9 +102,13 @@ private unsafe void VideoPlayerUnsafe_OnVideoFrameAvailable(MediaPlayer sender, } _functionTableCopyFrameToVideoSurface(_videoPlayerPtr, _canvasRenderTargetAsSurfacePtr); - DispatcherQueue.TryEnqueue(DispatcherQueuePriority.High, () => + DispatcherQueue.TryEnqueue(DispatcherQueuePriority.High, Draw); + + return; + + void Draw() { - StartDraw: + // StartDraw: try { nint drawingSessionPpv = SwapChainPanelHelper @@ -124,13 +128,21 @@ private unsafe void VideoPlayerUnsafe_OnVideoFrameAvailable(MediaPlayer sender, // Trying to recreate the context instead of re-creating the entire canvas catch (COMException comEx) when (unchecked((uint)comEx.HResult) == 0x802B0020u) { - Logger.LogWriteLine($"[LayeredBackgroundImage::UnsafeOnVideoFrameAvailable] CanvasImageSource has lost its context. Recreating the context...\r\n{comEx}", - LogType.Error, + Logger.LogWriteLine($"[LayeredBackgroundImage::UnsafeOnVideoFrameAvailable] CanvasImageSource has lost its context. Re-creating the context...\r\n{comEx}", + LogType.Warning, true); - // Re-create the context and start redrawing. + /* TODO: Re-create the context and start redrawing. + * Use it for CanvasImageSource later. Needs some fixes with how I can re-create the RenderTarget and the ImageSource without + * dealing with the lock and stuffs. + using CanvasLock? canvasLock = _canvasDevice?.Lock(); _canvasImageSource?.Recreate(_canvasDevice); + Logger.LogWriteLine("[LayeredBackgroundImage::UnsafeOnVideoFrameAvailable] Trying to re-draw after re-creating the context...", + LogType.Warning, + true); goto StartDraw; + */ + CanvasDevice_OnDeviceLost(); } // Device lost error. If happened, reinitialize render target catch (COMException comEx) when (unchecked((uint)comEx.HResult) is 0x887A0005u or 0x8899000Cu) @@ -155,7 +167,7 @@ private unsafe void VideoPlayerUnsafe_OnVideoFrameAvailable(MediaPlayer sender, { Interlocked.Exchange(ref _isVideoFrameDrawInProgress, 0); } - }); + } } private void VideoPlayerSafe_OnVideoFrameAvailable(MediaPlayer sender, object args) @@ -172,9 +184,13 @@ private void VideoPlayerSafe_OnVideoFrameAvailable(MediaPlayer sender, object ar } _videoPlayer?.CopyFrameToVideoSurface(_canvasRenderTarget); - DispatcherQueue.TryEnqueue(DispatcherQueuePriority.High, () => + DispatcherQueue.TryEnqueue(DispatcherQueuePriority.High, Draw); + + return; + + void Draw() { - StartDraw: + // StartDraw: try { using CanvasDrawingSession? ds = _canvasImageSource?.CreateDrawingSession(default, _canvasRenderSize); @@ -188,13 +204,21 @@ private void VideoPlayerSafe_OnVideoFrameAvailable(MediaPlayer sender, object ar // Trying to recreate the context instead of re-creating the entire canvas catch (COMException comEx) when (unchecked((uint)comEx.HResult) == 0x802B0020u) { - Logger.LogWriteLine($"[LayeredBackgroundImage::SafeOnVideoFrameAvailable] CanvasImageSource has lost its context. Recreating the context...\r\n{comEx}", - LogType.Error, + Logger.LogWriteLine($"[LayeredBackgroundImage::SafeOnVideoFrameAvailable] CanvasImageSource has lost its context. Re-creating the context...\r\n{comEx}", + LogType.Warning, true); - // Re-create the context and start redrawing. + /* TODO: Re-create the context and start redrawing. + * Use it for CanvasImageSource later. Needs some fixes with how I can re-create the RenderTarget and the ImageSource without + * dealing with the lock and stuffs. + using CanvasLock? canvasLock = _canvasDevice?.Lock(); _canvasImageSource?.Recreate(_canvasDevice); + Logger.LogWriteLine("[LayeredBackgroundImage::SafeOnVideoFrameAvailable] Trying to re-draw after re-creating the context...", + LogType.Warning, + true); goto StartDraw; + */ + CanvasDevice_OnDeviceLost(); } // Device lost error. If happened, reinitialize render target catch (COMException comEx) when (unchecked((uint)comEx.HResult) is 0x887A0005u or 0x8899000Cu) @@ -214,7 +238,7 @@ private void VideoPlayerSafe_OnVideoFrameAvailable(MediaPlayer sender, object ar { Interlocked.Exchange(ref _isVideoFrameDrawInProgress, 0); } - }); + } } #endregion From feb26ce0d43b7718a38e1062c4587e6b1421e2f2 Mon Sep 17 00:00:00 2001 From: Kemal Setya Adhi Date: Fri, 2 Oct 2026 03:02:23 +0700 Subject: [PATCH 20/20] [BG] Off-load CanvasDrawingSession outside of UI Thread The .Dispose() still require to be called from the UI thread --- ...redBackgroundImage.Events.FrameRenderer.cs | 215 +++++++++--------- 1 file changed, 104 insertions(+), 111 deletions(-) diff --git a/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameRenderer.cs b/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameRenderer.cs index 61a9e5ebf..fa38a953e 100644 --- a/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameRenderer.cs +++ b/CollapseLauncher/XAMLs/Theme/CustomControls/LayeredBackgroundImage.Events.FrameRenderer.cs @@ -101,72 +101,69 @@ private unsafe void VideoPlayerUnsafe_OnVideoFrameAvailable(MediaPlayer sender, return; } - _functionTableCopyFrameToVideoSurface(_videoPlayerPtr, _canvasRenderTargetAsSurfacePtr); - DispatcherQueue.TryEnqueue(DispatcherQueuePriority.High, Draw); - - return; + nint drawingSessionPpv = nint.Zero; + try + { + _functionTableCopyFrameToVideoSurface(_videoPlayerPtr, _canvasRenderTargetAsSurfacePtr); + drawingSessionPpv = SwapChainPanelHelper + .CanvasSessionDrawUnsafe(_canvasImageSourceNativePtr, + _canvasRenderTargetNativePtr, + _functionTableBeginDraw, + _functionTableDrawImage, + in _canvasRenderSize); + } + // Ignore DCOMPOSITION_ERROR_SURFACE_BEING_RENDERED + catch (COMException comEx) when (unchecked((uint)comEx.HResult) == 0x88980801u) + { + // ignored + } + // Trying to recreate the context instead of re-creating the entire canvas + catch (COMException comEx) when (unchecked((uint)comEx.HResult) == 0x802B0020u) + { + Logger.LogWriteLine($"[LayeredBackgroundImage::UnsafeOnVideoFrameAvailable] CanvasImageSource has lost its context. Re-creating the context...\r\n{comEx}", + LogType.Warning, + true); - void Draw() + /* TODO: Re-create the context and start redrawing. + * Use it for CanvasImageSource later. Needs some fixes with how I can re-create the RenderTarget and the ImageSource without + * dealing with the lock and stuffs. + using CanvasLock? canvasLock = _canvasDevice?.Lock(); + _canvasImageSource?.Recreate(_canvasDevice); + Logger.LogWriteLine("[LayeredBackgroundImage::UnsafeOnVideoFrameAvailable] Trying to re-draw after re-creating the context...", + LogType.Warning, + true); + goto StartDraw; + */ + CanvasDevice_OnDeviceLost(); + } + // Device lost error. If happened, reinitialize render target + catch (COMException comEx) when (unchecked((uint)comEx.HResult) is 0x887A0005u or 0x8899000Cu) + { + Logger.LogWriteLine($"[LayeredBackgroundImage::UnsafeOnVideoFrameAvailable] Direct3D device is lost! Trying to reinitialize it...\r\n{comEx}", + LogType.Warning, + true); + CanvasDevice_OnDeviceLost(); + } + catch (Exception ex) + { + Logger.LogWriteLine($"[LayeredBackgroundImage::UnsafeOnVideoFrameAvailable]" + + $"\r\n_videoPlayerPtr: 0x{_videoPlayerPtr:x8}" + + $"\r\n_canvasRenderTargetAsSurfacePtr: 0x{_canvasRenderTargetAsSurfacePtr:x8}" + + $"\r\n_canvasImageSourceNativePtr: 0x{_canvasImageSourceNativePtr:x8}" + + $"\r\n_canvasRenderTargetNativePtr: 0x{_canvasRenderTargetNativePtr:x8}" + + $"\r\n{ex}", + LogType.Error, + true); + } + finally { - // StartDraw: - try + DispatcherQueue.TryEnqueue(DispatcherQueuePriority.High, () => { - nint drawingSessionPpv = SwapChainPanelHelper - .CanvasSessionDrawUnsafe(_canvasImageSourceNativePtr, - _canvasRenderTargetNativePtr, - _functionTableBeginDraw, - _functionTableDrawImage, - in _canvasRenderSize); if (drawingSessionPpv != nint.Zero) SwapChainPanelHelper.DrawingDisposeUnsafe(drawingSessionPpv, _functionTableDispose); - } - // Ignore DCOMPOSITION_ERROR_SURFACE_BEING_RENDERED - catch (COMException comEx) when (unchecked((uint)comEx.HResult) == 0x88980801u) - { - // ignored - } - // Trying to recreate the context instead of re-creating the entire canvas - catch (COMException comEx) when (unchecked((uint)comEx.HResult) == 0x802B0020u) - { - Logger.LogWriteLine($"[LayeredBackgroundImage::UnsafeOnVideoFrameAvailable] CanvasImageSource has lost its context. Re-creating the context...\r\n{comEx}", - LogType.Warning, - true); - - /* TODO: Re-create the context and start redrawing. - * Use it for CanvasImageSource later. Needs some fixes with how I can re-create the RenderTarget and the ImageSource without - * dealing with the lock and stuffs. - using CanvasLock? canvasLock = _canvasDevice?.Lock(); - _canvasImageSource?.Recreate(_canvasDevice); - Logger.LogWriteLine("[LayeredBackgroundImage::UnsafeOnVideoFrameAvailable] Trying to re-draw after re-creating the context...", - LogType.Warning, - true); - goto StartDraw; - */ - CanvasDevice_OnDeviceLost(); - } - // Device lost error. If happened, reinitialize render target - catch (COMException comEx) when (unchecked((uint)comEx.HResult) is 0x887A0005u or 0x8899000Cu) - { - Logger.LogWriteLine($"[LayeredBackgroundImage::UnsafeOnVideoFrameAvailable] Direct3D device is lost! Trying to reinitialize it...\r\n{comEx}", - LogType.Error, - true); - CanvasDevice_OnDeviceLost(); - } - catch (Exception ex) - { - Logger.LogWriteLine($"[LayeredBackgroundImage::UnsafeOnVideoFrameAvailable]" + - $"\r\n_videoPlayerPtr: 0x{_videoPlayerPtr:x8}" + - $"\r\n_canvasRenderTargetAsSurfacePtr: 0x{_canvasRenderTargetAsSurfacePtr:x8}" + - $"\r\n_canvasImageSourceNativePtr: 0x{_canvasImageSourceNativePtr:x8}" + - $"\r\n_canvasRenderTargetNativePtr: 0x{_canvasRenderTargetNativePtr:x8}" + - $"\r\n{ex}", - LogType.Error, - true); - } - finally - { + Interlocked.Exchange(ref _isVideoFrameDrawInProgress, 0); - } + }); } } @@ -183,61 +180,57 @@ private void VideoPlayerSafe_OnVideoFrameAvailable(MediaPlayer sender, object ar return; } - _videoPlayer?.CopyFrameToVideoSurface(_canvasRenderTarget); - DispatcherQueue.TryEnqueue(DispatcherQueuePriority.High, Draw); - - return; + Unsafe.SkipInit(out CanvasDrawingSession? drawingSession); + try + { + _videoPlayer?.CopyFrameToVideoSurface(_canvasRenderTarget); + drawingSession = _canvasImageSource?.CreateDrawingSession(default, _canvasRenderSize); + } + // Ignore DCOMPOSITION_ERROR_SURFACE_BEING_RENDERED + catch (COMException comEx) when (unchecked((uint)comEx.HResult) == 0x88980801u) + { + // ignored + } + // Trying to recreate the context instead of re-creating the entire canvas + catch (COMException comEx) when (unchecked((uint)comEx.HResult) == 0x802B0020u) + { + Logger.LogWriteLine($"[LayeredBackgroundImage::SafeOnVideoFrameAvailable] CanvasImageSource has lost its context. Re-creating the context...\r\n{comEx}", + LogType.Warning, + true); - void Draw() + /* TODO: Re-create the context and start redrawing. + * Use it for CanvasImageSource later. Needs some fixes with how I can re-create the RenderTarget and the ImageSource without + * dealing with the lock and stuffs. + using CanvasLock? canvasLock = _canvasDevice?.Lock(); + _canvasImageSource?.Recreate(_canvasDevice); + Logger.LogWriteLine("[LayeredBackgroundImage::SafeOnVideoFrameAvailable] Trying to re-draw after re-creating the context...", + LogType.Warning, + true); + goto StartDraw; + */ + CanvasDevice_OnDeviceLost(); + } + // Device lost error. If happened, reinitialize render target + catch (COMException comEx) when (unchecked((uint)comEx.HResult) is 0x887A0005u or 0x8899000Cu) { - // StartDraw: - try - { - using CanvasDrawingSession? ds = _canvasImageSource?.CreateDrawingSession(default, _canvasRenderSize); - ds?.DrawImage(_canvasRenderTarget, _canvasRenderSize); - } - // Ignore DCOMPOSITION_ERROR_SURFACE_BEING_RENDERED - catch (COMException comEx) when (unchecked((uint)comEx.HResult) == 0x88980801u) - { - // ignored - } - // Trying to recreate the context instead of re-creating the entire canvas - catch (COMException comEx) when (unchecked((uint)comEx.HResult) == 0x802B0020u) - { - Logger.LogWriteLine($"[LayeredBackgroundImage::SafeOnVideoFrameAvailable] CanvasImageSource has lost its context. Re-creating the context...\r\n{comEx}", - LogType.Warning, - true); - - /* TODO: Re-create the context and start redrawing. - * Use it for CanvasImageSource later. Needs some fixes with how I can re-create the RenderTarget and the ImageSource without - * dealing with the lock and stuffs. - using CanvasLock? canvasLock = _canvasDevice?.Lock(); - _canvasImageSource?.Recreate(_canvasDevice); - Logger.LogWriteLine("[LayeredBackgroundImage::SafeOnVideoFrameAvailable] Trying to re-draw after re-creating the context...", - LogType.Warning, - true); - goto StartDraw; - */ - CanvasDevice_OnDeviceLost(); - } - // Device lost error. If happened, reinitialize render target - catch (COMException comEx) when (unchecked((uint)comEx.HResult) is 0x887A0005u or 0x8899000Cu) - { - Logger.LogWriteLine($"[LayeredBackgroundImage::SafeOnVideoFrameAvailable] Direct3D device is lost! Trying to reinitialize it...\r\n{comEx}", - LogType.Error, - true); - CanvasDevice_OnDeviceLost(); - } - catch (Exception ex) - { - Logger.LogWriteLine($"[LayeredBackgroundImage::SafeOnVideoFrameAvailable] {ex}", - LogType.Error, - true); - } - finally + Logger.LogWriteLine($"[LayeredBackgroundImage::SafeOnVideoFrameAvailable] Direct3D device is lost! Trying to reinitialize it...\r\n{comEx}", + LogType.Warning, + true); + CanvasDevice_OnDeviceLost(); + } + catch (Exception ex) + { + Logger.LogWriteLine($"[LayeredBackgroundImage::SafeOnVideoFrameAvailable] {ex}", + LogType.Error, + true); + } + finally + { + DispatcherQueue.TryEnqueue(DispatcherQueuePriority.High, () => { + drawingSession?.Dispose(); Interlocked.Exchange(ref _isVideoFrameDrawInProgress, 0); - } + }); } }