From a25e234bf6df6234bd3cb53e3cf6b9a812dc4bd9 Mon Sep 17 00:00:00 2001 From: Cheerpipe Date: Mon, 15 Mar 2021 00:27:54 -0300 Subject: [PATCH 1/7] Fix null object exception on Gradient Editor DEL Hotkey --- .../Screens/GradientEditor/GradientEditorViewModel.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Artemis.UI.Shared/Screens/GradientEditor/GradientEditorViewModel.cs b/src/Artemis.UI.Shared/Screens/GradientEditor/GradientEditorViewModel.cs index 8b905c56a..f53a099af 100644 --- a/src/Artemis.UI.Shared/Screens/GradientEditor/GradientEditorViewModel.cs +++ b/src/Artemis.UI.Shared/Screens/GradientEditor/GradientEditorViewModel.cs @@ -65,6 +65,9 @@ namespace Artemis.UI.Shared.Screens.GradientEditor public void RemoveColorStop(ColorStopViewModel colorStopViewModel) { + if (colorStopViewModel == null) + return; + ColorStopViewModels.Remove(colorStopViewModel); ColorGradient.Stops.Remove(colorStopViewModel.ColorStop); ColorGradient.OnColorValuesUpdated(); From eaa1f15ac3e0bc80fb0bfdbeca0641f77341e647 Mon Sep 17 00:00:00 2001 From: Robert Date: Mon, 15 Mar 2021 19:24:35 +0100 Subject: [PATCH 2/7] Data bindings - Fix crash when removing color gradient stops --- .../Profile/DataBindings/DataBindingRegistration.cs | 10 ++++++++++ .../Profile/DataBindings/IDataBindingRegistration.cs | 5 +++++ .../Models/Profile/LayerProperties/LayerProperty.cs | 11 +++++++++++ src/Artemis.UI.Shared/Controls/DeviceVisualizer.cs | 2 +- src/Artemis.UI.Shared/Controls/DeviceVisualizerLed.cs | 1 - 5 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/Artemis.Core/Models/Profile/DataBindings/DataBindingRegistration.cs b/src/Artemis.Core/Models/Profile/DataBindings/DataBindingRegistration.cs index 78e233fe1..ab50c44c9 100644 --- a/src/Artemis.Core/Models/Profile/DataBindings/DataBindingRegistration.cs +++ b/src/Artemis.Core/Models/Profile/DataBindings/DataBindingRegistration.cs @@ -64,5 +64,15 @@ namespace Artemis.Core DataBinding = new DataBinding(LayerProperty, dataBinding); return DataBinding; } + + /// + public void ClearDataBinding() + { + if (DataBinding == null) + return; + + // The related entity is left behind, just in case the data binding is added back later + LayerProperty.DisableDataBinding(DataBinding); + } } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/IDataBindingRegistration.cs b/src/Artemis.Core/Models/Profile/DataBindings/IDataBindingRegistration.cs index 8e1de1f0a..c1321d4c3 100644 --- a/src/Artemis.Core/Models/Profile/DataBindings/IDataBindingRegistration.cs +++ b/src/Artemis.Core/Models/Profile/DataBindings/IDataBindingRegistration.cs @@ -20,5 +20,10 @@ /// /// IDataBinding? CreateDataBinding(); + + /// + /// If present, removes the current data binding + /// + void ClearDataBinding(); } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs index 61f38c428..c35bc401d 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs @@ -420,6 +420,14 @@ namespace Artemis.Core DataBindingRegistration registration = new(this, converter, getter, setter, displayName); _dataBindingRegistrations.Add(registration); + // If not yet initialized, load the data binding related to the registration if available + if (_isInitialized) + { + IDataBinding? dataBinding = registration.CreateDataBinding(); + if (dataBinding != null) + _dataBindings.Add(dataBinding); + } + OnDataBindingPropertyRegistered(); return registration; } @@ -432,7 +440,10 @@ namespace Artemis.Core if (_disposed) throw new ObjectDisposedException("LayerProperty"); + foreach (IDataBindingRegistration dataBindingRegistration in _dataBindingRegistrations) + dataBindingRegistration.ClearDataBinding(); _dataBindingRegistrations.Clear(); + OnDataBindingPropertiesCleared(); } diff --git a/src/Artemis.UI.Shared/Controls/DeviceVisualizer.cs b/src/Artemis.UI.Shared/Controls/DeviceVisualizer.cs index 21e82e6b1..ffb154dac 100644 --- a/src/Artemis.UI.Shared/Controls/DeviceVisualizer.cs +++ b/src/Artemis.UI.Shared/Controls/DeviceVisualizer.cs @@ -295,7 +295,7 @@ namespace Artemis.UI.Shared { Execute.PostToUIThread(SetupForDevice); } - + private void Render() { DrawingContext drawingContext = _backingStore.Open(); diff --git a/src/Artemis.UI.Shared/Controls/DeviceVisualizerLed.cs b/src/Artemis.UI.Shared/Controls/DeviceVisualizerLed.cs index 640812016..a4bf80753 100644 --- a/src/Artemis.UI.Shared/Controls/DeviceVisualizerLed.cs +++ b/src/Artemis.UI.Shared/Controls/DeviceVisualizerLed.cs @@ -51,7 +51,6 @@ namespace Artemis.UI.Shared byte g = Led.RgbLed.Color.GetG(); byte b = Led.RgbLed.Color.GetB(); - _renderColor.A = (byte) (isDimmed ? 100 : 255); _renderColor.A = isDimmed ? Dimmed : NonDimmed; _renderColor.R = r; _renderColor.G = g; From c51016f8f23886c26443505e8284397bb02677c6 Mon Sep 17 00:00:00 2001 From: Robert Date: Mon, 15 Mar 2021 19:50:21 +0100 Subject: [PATCH 3/7] Surface editor - Added option to only light the top-left LED of a device --- .../SurfaceEditor/SurfaceEditorView.xaml | 7 +++++ .../SurfaceEditor/SurfaceEditorViewModel.cs | 26 ++++++++++++++++--- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorView.xaml b/src/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorView.xaml index 0101d01e4..f3e5f181c 100644 --- a/src/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorView.xaml +++ b/src/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorView.xaml @@ -209,6 +209,13 @@ ToolTip="If selected, each device is completely lid up with a random color"> Show random device colors + + Light up first LED only + diff --git a/src/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorViewModel.cs b/src/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorViewModel.cs index 306928fee..831640a64 100644 --- a/src/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorViewModel.cs +++ b/src/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorViewModel.cs @@ -92,7 +92,18 @@ namespace Artemis.UI.Screens.SurfaceEditor public bool ColorDevices { get => _colorDevices; - set => SetAndNotify(ref _colorDevices, value); + set + { + SetAndNotify(ref _colorDevices, value); + if (!value) + ColorFirstLedOnly = false; + } + } + + public bool ColorFirstLedOnly + { + get => _colorFirstLedOnly; + set => SetAndNotify(ref _colorFirstLedOnly, value); } public void OpenHyperlink(object sender, RequestNavigateEventArgs e) @@ -127,8 +138,16 @@ namespace Artemis.UI.Screens.SurfaceEditor foreach (ListDeviceViewModel listDeviceViewModel in ListDeviceViewModels) { - foreach (ArtemisLed artemisLed in listDeviceViewModel.Device.Leds) - e.Canvas.DrawRect(artemisLed.AbsoluteRectangle, new SKPaint {Color = listDeviceViewModel.Color}); + // Order by position to accurately get the first LED + List leds = listDeviceViewModel.Device.Leds.OrderBy(l => l.Rectangle.Left).ThenBy(l => l.Rectangle.Top).ToList(); + for (int index = 0; index < leds.Count; index++) + { + ArtemisLed artemisLed = leds[index]; + if (ColorFirstLedOnly && index > 0) + e.Canvas.DrawRect(artemisLed.AbsoluteRectangle, new SKPaint {Color = new SKColor(0, 0, 0)}); + else + e.Canvas.DrawRect(artemisLed.AbsoluteRectangle, new SKPaint {Color = listDeviceViewModel.Color}); + } } } @@ -258,6 +277,7 @@ namespace Artemis.UI.Screens.SurfaceEditor private MouseDragStatus _mouseDragStatus; private Point _mouseDragStartPoint; private bool _colorDevices; + private bool _colorFirstLedOnly; // ReSharper disable once UnusedMember.Global - Called from view public void EditorGridMouseClick(object sender, MouseButtonEventArgs e) From f646f64648b8329f14d67231c07c0f621e257826 Mon Sep 17 00:00:00 2001 From: Robert Date: Tue, 16 Mar 2021 00:15:50 +0100 Subject: [PATCH 4/7] Surface editor - Improved device color overlay method --- .../Screens/Settings/Debug/Tabs/RenderDebugViewModel.cs | 1 + .../Screens/SurfaceEditor/SurfaceEditorViewModel.cs | 7 +++---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Artemis.UI/Screens/Settings/Debug/Tabs/RenderDebugViewModel.cs b/src/Artemis.UI/Screens/Settings/Debug/Tabs/RenderDebugViewModel.cs index 7bfd7ef8d..e07bf9d37 100644 --- a/src/Artemis.UI/Screens/Settings/Debug/Tabs/RenderDebugViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Debug/Tabs/RenderDebugViewModel.cs @@ -66,6 +66,7 @@ namespace Artemis.UI.Screens.Settings.Debug.Tabs { Execute.PostToUIThread(() => { + // TODO: Remove, frames shouldn't even be rendered if this is the case if (e.Texture.Bitmap.Pixels.Length == 0) return; diff --git a/src/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorViewModel.cs b/src/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorViewModel.cs index 831640a64..641a288d5 100644 --- a/src/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorViewModel.cs +++ b/src/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorViewModel.cs @@ -136,16 +136,15 @@ namespace Artemis.UI.Screens.SurfaceEditor if (!ColorDevices) return; + e.Canvas.Clear(new SKColor(0, 0, 0)); foreach (ListDeviceViewModel listDeviceViewModel in ListDeviceViewModels) { // Order by position to accurately get the first LED - List leds = listDeviceViewModel.Device.Leds.OrderBy(l => l.Rectangle.Left).ThenBy(l => l.Rectangle.Top).ToList(); + List leds = listDeviceViewModel.Device.Leds.OrderBy(l => l.RgbLed.Location.Y).ThenBy(l => l.RgbLed.Location.X).ToList(); for (int index = 0; index < leds.Count; index++) { ArtemisLed artemisLed = leds[index]; - if (ColorFirstLedOnly && index > 0) - e.Canvas.DrawRect(artemisLed.AbsoluteRectangle, new SKPaint {Color = new SKColor(0, 0, 0)}); - else + if (ColorFirstLedOnly && index == 0 || !ColorFirstLedOnly) e.Canvas.DrawRect(artemisLed.AbsoluteRectangle, new SKPaint {Color = listDeviceViewModel.Color}); } } From 12227b35306a22ba7c778ca5ea4f54c6853d589a Mon Sep 17 00:00:00 2001 From: Robert Date: Tue, 16 Mar 2021 19:29:18 +0100 Subject: [PATCH 5/7] Color picker - Remember last 18 picked colors --- .../Controls/ColorPicker.xaml | 36 +++++++++---- .../Controls/ColorPicker.xaml.cs | 24 +++++++++ .../Services/ColorPickerService.cs | 50 ++++++++++++------- .../Interfaces/IColorPickerService.cs | 6 ++- 4 files changed, 89 insertions(+), 27 deletions(-) diff --git a/src/Artemis.UI.Shared/Controls/ColorPicker.xaml b/src/Artemis.UI.Shared/Controls/ColorPicker.xaml index 0717700b2..7b671e60d 100644 --- a/src/Artemis.UI.Shared/Controls/ColorPicker.xaml +++ b/src/Artemis.UI.Shared/Controls/ColorPicker.xaml @@ -94,19 +94,20 @@ StaysOpen="{Binding StaysOpen, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}" PopupAnimation="Fade" IsOpen="{Binding PopupOpen, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"> - + - - - + + + + - + + + + + + + + + + + + + + + + + + + - - Preview on devices - diff --git a/src/Artemis.UI.Shared/Controls/ColorPicker.xaml.cs b/src/Artemis.UI.Shared/Controls/ColorPicker.xaml.cs index 6f1255b10..9a5a4fb02 100644 --- a/src/Artemis.UI.Shared/Controls/ColorPicker.xaml.cs +++ b/src/Artemis.UI.Shared/Controls/ColorPicker.xaml.cs @@ -1,9 +1,11 @@ using System; +using System.Collections.ObjectModel; using System.ComponentModel; using System.Runtime.CompilerServices; using System.Windows; using System.Windows.Input; using System.Windows.Media; +using System.Windows.Shapes; using Artemis.UI.Shared.Services; namespace Artemis.UI.Shared @@ -146,6 +148,12 @@ namespace Artemis.UI.Shared colorPicker._inCallback = true; colorPicker.OnPropertyChanged(nameof(PopupOpen)); + + if ((bool) e.NewValue) + colorPicker.PopupOpened(); + else + colorPicker.PopupClosed(); + colorPicker._inCallback = false; } @@ -230,6 +238,22 @@ namespace Artemis.UI.Shared PreviewCheckBox.IsChecked = _colorPickerService.PreviewSetting.Value; } + private void PopupClosed() + { + _colorPickerService?.QueueRecentColor(Color); + } + + private void PopupOpened() + { + RecentColorsContainer.ItemsSource = new ObservableCollection(_colorPickerService.RecentColors); + } + + private void SelectRecentColor(object sender, MouseButtonEventArgs e) + { + Color = (Color) ((Rectangle) sender).DataContext; + PopupOpen = false; + } + /// public event PropertyChangedEventHandler? PropertyChanged; diff --git a/src/Artemis.UI.Shared/Services/ColorPickerService.cs b/src/Artemis.UI.Shared/Services/ColorPickerService.cs index aaea09b8e..a3602c1b7 100644 --- a/src/Artemis.UI.Shared/Services/ColorPickerService.cs +++ b/src/Artemis.UI.Shared/Services/ColorPickerService.cs @@ -24,10 +24,35 @@ namespace Artemis.UI.Shared.Services PreviewSetting = settingsService.GetSetting("UI.PreviewColorPickerOnDevices", false); PreviewSetting.AutoSave = true; + RecentColorsSetting = settingsService.GetSetting("UI.ColorPickerRecentColors", new LinkedList()); + } + + private void RenderColorPickerOverlay(object? sender, FrameRenderingEventArgs e) + { + if (_mustRenderOverlay) + _overlayOpacity += 0.2f; + else + _overlayOpacity -= 0.2f; + + if (_overlayOpacity <= 0f) + { + _coreService.FrameRendering -= RenderColorPickerOverlay; + return; + } + + if (_overlayOpacity > 1f) + _overlayOpacity = 1f; + + using SKPaint overlayPaint = new() {Color = new SKColor(0, 0, 0, (byte) (255 * _overlayOpacity))}; + overlayPaint.Color = _overlayColor.WithAlpha((byte) (_overlayColor.Alpha * _overlayOpacity)); + e.Canvas.DrawRect(0, 0, e.Canvas.LocalClipBounds.Width, e.Canvas.LocalClipBounds.Height, overlayPaint); } public PluginSetting PreviewSetting { get; } - + public PluginSetting> RecentColorsSetting { get; } + + public LinkedList RecentColors => RecentColorsSetting.Value; + public Task ShowGradientPicker(ColorGradient colorGradient, string dialogHost) { if (!string.IsNullOrWhiteSpace(dialogHost)) @@ -56,25 +81,16 @@ namespace Artemis.UI.Shared.Services _overlayColor = new SKColor(color.R, color.G, color.B, color.A); } - private void RenderColorPickerOverlay(object? sender, FrameRenderingEventArgs e) + public void QueueRecentColor(Color color) { - if (_mustRenderOverlay) - _overlayOpacity += 0.2f; - else - _overlayOpacity -= 0.2f; + if (RecentColors.Contains(color)) + RecentColors.Remove(color); - if (_overlayOpacity <= 0f) - { - _coreService.FrameRendering -= RenderColorPickerOverlay; - return; - } + RecentColors.AddFirst(color); + while (RecentColors.Count > 18) + RecentColors.RemoveLast(); - if (_overlayOpacity > 1f) - _overlayOpacity = 1f; - - using SKPaint overlayPaint = new() {Color = new SKColor(0, 0, 0, (byte) (255 * _overlayOpacity))}; - overlayPaint.Color = _overlayColor.WithAlpha((byte) (_overlayColor.Alpha * _overlayOpacity)); - e.Canvas.DrawRect(0, 0, e.Canvas.LocalClipBounds.Width, e.Canvas.LocalClipBounds.Height, overlayPaint); + RecentColorsSetting.Save(); } } } \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Services/Interfaces/IColorPickerService.cs b/src/Artemis.UI.Shared/Services/Interfaces/IColorPickerService.cs index 4258d7596..a5bbc26f8 100644 --- a/src/Artemis.UI.Shared/Services/Interfaces/IColorPickerService.cs +++ b/src/Artemis.UI.Shared/Services/Interfaces/IColorPickerService.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Collections.Generic; +using System.Threading.Tasks; using System.Windows.Media; using Artemis.Core; @@ -9,8 +10,11 @@ namespace Artemis.UI.Shared.Services Task ShowGradientPicker(ColorGradient colorGradient, string dialogHost); PluginSetting PreviewSetting { get; } + LinkedList RecentColors { get; } + PluginSetting> RecentColorsSetting { get; } void StartColorDisplay(); void StopColorDisplay(); void UpdateColorDisplay(Color color); + void QueueRecentColor(Color color); } } \ No newline at end of file From 1a7a7e05829def3aded4e16b32f31cd2effbb722 Mon Sep 17 00:00:00 2001 From: Robert Date: Tue, 16 Mar 2021 19:47:27 +0100 Subject: [PATCH 6/7] Data bindings UI - Update evaluation status of conditional data bindings Profiles - Don't fail to activate a module when its last profile fails to load --- .../JsonConverters/ForgivingIntConverter.cs | 4 ++-- src/Artemis.Core/Services/ModuleService.cs | 17 ++++++++----- .../ConditionalDataBindingModeView.xaml | 7 +++--- .../ConditionalDataBindingModeViewModel.cs | 24 ++++++++++++++++--- .../DataBindingConditionViewModel.cs | 5 ++++ 5 files changed, 43 insertions(+), 14 deletions(-) diff --git a/src/Artemis.Core/JsonConverters/ForgivingIntConverter.cs b/src/Artemis.Core/JsonConverters/ForgivingIntConverter.cs index 54edb8ed6..05845fc2e 100644 --- a/src/Artemis.Core/JsonConverters/ForgivingIntConverter.cs +++ b/src/Artemis.Core/JsonConverters/ForgivingIntConverter.cs @@ -20,14 +20,14 @@ namespace Artemis.Core.JsonConverters { JValue? jsonValue = serializer.Deserialize(reader); if (jsonValue == null) - throw new FormatException(); + throw new JsonReaderException("Failed to deserialize forgiving int value"); if (jsonValue.Type == JTokenType.Float) return (int) Math.Round(jsonValue.Value()); if (jsonValue.Type == JTokenType.Integer) return jsonValue.Value(); - throw new FormatException(); + throw new JsonReaderException("Failed to deserialize forgiving int value"); } } } \ No newline at end of file diff --git a/src/Artemis.Core/Services/ModuleService.cs b/src/Artemis.Core/Services/ModuleService.cs index f71cfac76..a6f85bcfd 100644 --- a/src/Artemis.Core/Services/ModuleService.cs +++ b/src/Artemis.Core/Services/ModuleService.cs @@ -47,15 +47,20 @@ namespace Artemis.Core.Services { module.Activate(false); - // If this is a profile module, activate the last active profile after module activation - if (module is ProfileModule profileModule) - await _profileService.ActivateLastProfileAnimated(profileModule); + try + { + // If this is a profile module, activate the last active profile after module activation + if (module is ProfileModule profileModule) + await _profileService.ActivateLastProfileAnimated(profileModule); + } + catch (Exception e) + { + _logger.Warning(e, $"Failed to activate last profile on module {module}"); + } } catch (Exception e) { - _logger.Error(new ArtemisPluginFeatureException( - module, "Failed to activate module and last profile.", e), "Failed to activate module and last profile" - ); + _logger.Error(new ArtemisPluginFeatureException(module, "Failed to activate module.", e), "Failed to activate module"); throw; } } diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/ConditionalDataBinding/ConditionalDataBindingModeView.xaml b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/ConditionalDataBinding/ConditionalDataBindingModeView.xaml index bed240696..493b65c76 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/ConditionalDataBinding/ConditionalDataBindingModeView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/ConditionalDataBinding/ConditionalDataBindingModeView.xaml @@ -15,7 +15,7 @@ - + @@ -26,7 +26,8 @@ Background="{StaticResource PrimaryHueMidBrush}" BorderBrush="{StaticResource PrimaryHueMidBrush}" HorizontalAlignment="Right" - Command="{s:Action AddCondition}"> + Command="{s:Action AddCondition}" + Margin="0 5"> ADD CONDITION @@ -38,7 +39,7 @@ dd:DragDrop.UseDefaultDragAdorner="True" HorizontalContentAlignment="Stretch" VirtualizingPanel.ScrollUnit="Pixel" - Margin="0 5 0 0"> + Margin="-10 0 -10 -5"> diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/ConditionalDataBinding/ConditionalDataBindingModeViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/ConditionalDataBinding/ConditionalDataBindingModeViewModel.cs index 620bdaee2..e3b54179a 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/ConditionalDataBinding/ConditionalDataBindingModeViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/ConditionalDataBinding/ConditionalDataBindingModeViewModel.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using Artemis.Core; +using Artemis.Core.Services; using Artemis.UI.Extensions; using Artemis.UI.Ninject.Factories; using Artemis.UI.Shared.Services; @@ -10,16 +11,20 @@ using Stylet; namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings.ConditionalDataBinding { - public sealed class ConditionalDataBindingModeViewModel : Conductor>.Collection.AllActive, IDataBindingModeViewModel + public sealed class ConditionalDataBindingModeViewModel : Conductor>.Collection.AllActive, + IDataBindingModeViewModel { private readonly IDataBindingsVmFactory _dataBindingsVmFactory; + private readonly ICoreService _coreService; private readonly IProfileEditorService _profileEditorService; private bool _updating; public ConditionalDataBindingModeViewModel(ConditionalDataBinding conditionalDataBinding, + ICoreService coreService, IProfileEditorService profileEditorService, IDataBindingsVmFactory dataBindingsVmFactory) { + _coreService = coreService; _profileEditorService = profileEditorService; _dataBindingsVmFactory = dataBindingsVmFactory; @@ -41,8 +46,21 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings.Conditio protected override void OnInitialActivate() { - base.OnInitialActivate(); Initialize(); + _coreService.FrameRendered += CoreServiceOnFrameRendered; + base.OnInitialActivate(); + } + + protected override void OnClose() + { + _coreService.FrameRendered -= CoreServiceOnFrameRendered; + base.OnClose(); + } + + private void CoreServiceOnFrameRendered(object? sender, FrameRenderedEventArgs e) + { + foreach (DataBindingConditionViewModel dataBindingConditionViewModel in Items) + dataBindingConditionViewModel.Evaluate(); } private void UpdateItems() @@ -109,7 +127,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings.Conditio } #region IDisposable - + /// public void Dispose() { diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/ConditionalDataBinding/DataBindingConditionViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/ConditionalDataBinding/DataBindingConditionViewModel.cs index e5980c115..81d9dfe6a 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/ConditionalDataBinding/DataBindingConditionViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/ConditionalDataBinding/DataBindingConditionViewModel.cs @@ -56,6 +56,11 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings.Conditio _profileEditorService.UpdateSelectedProfileElement(); } + public void Evaluate() + { + ActiveItem?.Evaluate(); + } + #region IDisposable /// From 456af693b09c924f5be6593ecb783aef9a4ff32a Mon Sep 17 00:00:00 2001 From: Robert Date: Tue, 16 Mar 2021 20:18:53 +0100 Subject: [PATCH 7/7] Rendering - Moved bitmap management logic & prevent access violations --- .../Conditions/DataModelConditionGroup.cs | 19 +- src/Artemis.Core/RGB.NET/SKTexture.cs | 31 +- src/Artemis.Core/Services/CoreService.cs | 291 ++++++++---------- .../Services/Interfaces/ICoreService.cs | 2 +- .../Services/Interfaces/IRgbService.cs | 31 +- src/Artemis.Core/Services/RgbService.cs | 206 +++++++------ .../Controls/ColorPicker.xaml.cs | 3 +- .../Input/DataModelDynamicViewModel.cs | 7 +- .../ConditionalDataBindingModeViewModel.cs | 2 +- .../Debug/Tabs/RenderDebugViewModel.cs | 38 +-- .../Settings/Device/DeviceDialogViewModel.cs | 2 +- 11 files changed, 310 insertions(+), 322 deletions(-) diff --git a/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionGroup.cs b/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionGroup.cs index 851fabb62..53dcaa0bd 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionGroup.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionGroup.cs @@ -94,19 +94,14 @@ namespace Artemis.Core private bool EvaluateWithOperator(IEnumerable targets) { - switch (BooleanOperator) + return BooleanOperator switch { - case BooleanOperator.And: - return targets.All(c => c.Evaluate()); - case BooleanOperator.Or: - return targets.Any(c => c.Evaluate()); - case BooleanOperator.AndNot: - return targets.All(c => !c.Evaluate()); - case BooleanOperator.OrNot: - return targets.Any(c => !c.Evaluate()); - default: - throw new ArgumentOutOfRangeException(); - } + BooleanOperator.And => targets.All(c => c.Evaluate()), + BooleanOperator.Or => targets.Any(c => c.Evaluate()), + BooleanOperator.AndNot => targets.All(c => !c.Evaluate()), + BooleanOperator.OrNot => targets.Any(c => !c.Evaluate()), + _ => throw new ArgumentOutOfRangeException() + }; } #region IDisposable diff --git a/src/Artemis.Core/RGB.NET/SKTexture.cs b/src/Artemis.Core/RGB.NET/SKTexture.cs index 59f8d0558..87e2c8cfb 100644 --- a/src/Artemis.Core/RGB.NET/SKTexture.cs +++ b/src/Artemis.Core/RGB.NET/SKTexture.cs @@ -10,12 +10,15 @@ namespace Artemis.Core /// public sealed class SKTexture : PixelTexture, IDisposable { + private bool _disposed; + #region Constructors - internal SKTexture(SKBitmap bitmap) - : base(bitmap.Width, bitmap.Height, 4, new AverageByteSampler()) + internal SKTexture(int width, int height, float renderScale) + : base(width, height, 4, new AverageByteSampler()) { - Bitmap = bitmap; + Bitmap = new SKBitmap(new SKImageInfo(width, height, SKColorType.Rgb888x)); + RenderScale = renderScale; } #endregion @@ -40,11 +43,31 @@ namespace Artemis.Core /// /// Gets the color data in RGB format /// - protected override ReadOnlySpan Data => Bitmap.GetPixelSpan(); + protected override ReadOnlySpan Data => _disposed ? new ReadOnlySpan() : Bitmap.GetPixelSpan(); + /// + /// Gets the render scale of the texture + /// + public float RenderScale { get; } + + /// + /// Gets a boolean indicating whether has been called on this texture, indicating it should + /// be replaced + /// + public bool IsInvalid { get; private set; } + + /// + /// Invalidates the texture + /// + public void Invalidate() + { + IsInvalid = true; + } + /// public void Dispose() { + _disposed = true; Bitmap.Dispose(); } diff --git a/src/Artemis.Core/Services/CoreService.cs b/src/Artemis.Core/Services/CoreService.cs index 19b9b6210..239aaa567 100644 --- a/src/Artemis.Core/Services/CoreService.cs +++ b/src/Artemis.Core/Services/CoreService.cs @@ -29,14 +29,11 @@ namespace Artemis.Core.Services private readonly PluginSetting _loggingLevel; private readonly IPluginManagementService _pluginManagementService; private readonly IProfileService _profileService; - private readonly PluginSetting _renderScale; private readonly IRgbService _rgbService; private readonly List _updateExceptions = new(); private List _dataModelExpansions = new(); private DateTime _lastExceptionLog; private List _modules = new(); - private SKBitmap? _bitmap; - private readonly object _bitmapLock = new(); // ReSharper disable UnusedParameter.Local public CoreService(IKernel kernel, @@ -57,17 +54,13 @@ namespace Artemis.Core.Services _rgbService = rgbService; _profileService = profileService; _loggingLevel = settingsService.GetSetting("Core.LoggingLevel", LogEventLevel.Debug); - _renderScale = settingsService.GetSetting("Core.RenderScale", 0.5); _frameStopWatch = new Stopwatch(); StartupArguments = new List(); UpdatePluginCache(); _rgbService.Surface.Updating += SurfaceOnUpdating; - _rgbService.Surface.Updated += SurfaceOnUpdated; - _rgbService.Surface.SurfaceLayoutChanged += SurfaceOnSurfaceLayoutChanged; _loggingLevel.SettingChanged += (sender, args) => ApplyLoggingLevel(); - _renderScale.SettingChanged += RenderScaleSettingChanged; _pluginManagementService.PluginFeatureEnabled += (sender, args) => UpdatePluginCache(); _pluginManagementService.PluginFeatureDisabled += (sender, args) => UpdatePluginCache(); @@ -75,6 +68,133 @@ namespace Artemis.Core.Services // ReSharper restore UnusedParameter.Local + protected virtual void OnFrameRendering(FrameRenderingEventArgs e) + { + FrameRendering?.Invoke(this, e); + } + + protected virtual void OnFrameRendered(FrameRenderedEventArgs e) + { + FrameRendered?.Invoke(this, e); + } + + private void UpdatePluginCache() + { + _modules = _pluginManagementService.GetFeaturesOfType().Where(p => p.IsEnabled).ToList(); + _dataModelExpansions = _pluginManagementService.GetFeaturesOfType().Where(p => p.IsEnabled).ToList(); + } + + private void ApplyLoggingLevel() + { + string? argument = StartupArguments.FirstOrDefault(a => a.StartsWith("--logging")); + if (argument != null) + { + // Parse the provided log level + string[] parts = argument.Split('='); + if (parts.Length == 2 && Enum.TryParse(typeof(LogEventLevel), parts[1], true, out object? logLevelArgument)) + { + _logger.Information("Setting logging level to {loggingLevel} from startup argument", (LogEventLevel) logLevelArgument!); + LoggerProvider.LoggingLevelSwitch.MinimumLevel = (LogEventLevel) logLevelArgument; + } + else + { + _logger.Warning("Failed to set log level from startup argument {argument}", argument); + _logger.Information("Setting logging level to {loggingLevel}", _loggingLevel.Value); + LoggerProvider.LoggingLevelSwitch.MinimumLevel = _loggingLevel.Value; + } + } + else + { + _logger.Information("Setting logging level to {loggingLevel}", _loggingLevel.Value); + LoggerProvider.LoggingLevelSwitch.MinimumLevel = _loggingLevel.Value; + } + } + + private void SurfaceOnUpdating(UpdatingEventArgs args) + { + if (_rgbService.IsRenderPaused) + return; + + try + { + _frameStopWatch.Restart(); + lock (_dataModelExpansions) + { + // Update all active modules, check Enabled status because it may go false before before the _dataModelExpansions list is updated + foreach (BaseDataModelExpansion dataModelExpansion in _dataModelExpansions.Where(e => e.IsEnabled)) + dataModelExpansion.InternalUpdate(args.DeltaTime); + } + + List modules; + lock (_modules) + { + modules = _modules.Where(m => m.IsActivated || m.InternalExpandsMainDataModel) + .OrderBy(m => m.PriorityCategory) + .ThenByDescending(m => m.Priority) + .ToList(); + } + + // Update all active modules + foreach (Module module in modules) + module.InternalUpdate(args.DeltaTime); + + // Render all active modules + SKTexture texture =_rgbService.OpenRender(); + + using (SKCanvas canvas = new(texture.Bitmap)) + { + canvas.Scale(texture.RenderScale); + canvas.Clear(new SKColor(0, 0, 0)); + // While non-activated modules may be updated above if they expand the main data model, they may never render + if (!ModuleRenderingDisabled) + { + foreach (Module module in modules.Where(m => m.IsActivated)) + module.InternalRender(args.DeltaTime, canvas, texture.Bitmap.Info); + } + + OnFrameRendering(new FrameRenderingEventArgs(canvas, args.DeltaTime, _rgbService.Surface)); + } + + OnFrameRendered(new FrameRenderedEventArgs(texture, _rgbService.Surface)); + } + catch (Exception e) + { + _updateExceptions.Add(e); + } + finally + { + _frameStopWatch.Stop(); + FrameTime = _frameStopWatch.Elapsed; + _rgbService.CloseRender(); + + LogUpdateExceptions(); + } + } + + private void LogUpdateExceptions() + { + // Only log update exceptions every 10 seconds to avoid spamming the logs + if (DateTime.Now - _lastExceptionLog < TimeSpan.FromSeconds(10)) + return; + _lastExceptionLog = DateTime.Now; + + if (!_updateExceptions.Any()) + return; + + // Group by stack trace, that should gather up duplicate exceptions + foreach (IGrouping exceptions in _updateExceptions.GroupBy(e => e.StackTrace)) + _logger.Warning(exceptions.First(), "Exception was thrown {count} times during update in the last 10 seconds", exceptions.Count()); + + // When logging is finished start with a fresh slate + _updateExceptions.Clear(); + } + + private void OnInitialized() + { + IsInitialized = true; + Initialized?.Invoke(this, EventArgs.Empty); + } + public TimeSpan FrameTime { get; private set; } public bool ModuleRenderingDisabled { get; set; } public List StartupArguments { get; set; } @@ -123,16 +243,6 @@ namespace Artemis.Core.Services OnInitialized(); } - protected virtual void OnFrameRendering(FrameRenderingEventArgs e) - { - FrameRendering?.Invoke(this, e); - } - - protected virtual void OnFrameRendered(FrameRenderedEventArgs e) - { - FrameRendered?.Invoke(this, e); - } - public void PlayIntroAnimation() { IntroAnimation intro = new(_logger, _profileService, _rgbService.EnabledDevices); @@ -156,155 +266,8 @@ namespace Artemis.Core.Services }); } - private void UpdatePluginCache() - { - _modules = _pluginManagementService.GetFeaturesOfType().Where(p => p.IsEnabled).ToList(); - _dataModelExpansions = _pluginManagementService.GetFeaturesOfType().Where(p => p.IsEnabled).ToList(); - } - - private void ApplyLoggingLevel() - { - string? argument = StartupArguments.FirstOrDefault(a => a.StartsWith("--logging")); - if (argument != null) - { - // Parse the provided log level - string[] parts = argument.Split('='); - if (parts.Length == 2 && Enum.TryParse(typeof(LogEventLevel), parts[1], true, out object? logLevelArgument)) - { - _logger.Information("Setting logging level to {loggingLevel} from startup argument", (LogEventLevel)logLevelArgument!); - LoggerProvider.LoggingLevelSwitch.MinimumLevel = (LogEventLevel)logLevelArgument; - } - else - { - _logger.Warning("Failed to set log level from startup argument {argument}", argument); - _logger.Information("Setting logging level to {loggingLevel}", _loggingLevel.Value); - LoggerProvider.LoggingLevelSwitch.MinimumLevel = _loggingLevel.Value; - } - } - else - { - _logger.Information("Setting logging level to {loggingLevel}", _loggingLevel.Value); - LoggerProvider.LoggingLevelSwitch.MinimumLevel = _loggingLevel.Value; - } - } - - private void SurfaceOnUpdating(UpdatingEventArgs args) - { - if (_rgbService.IsRenderPaused) - return; - - try - { - _frameStopWatch.Restart(); - lock (_dataModelExpansions) - { - // Update all active modules, check Enabled status because it may go false before before the _dataModelExpansions list is updated - foreach (BaseDataModelExpansion dataModelExpansion in _dataModelExpansions.Where(e => e.IsEnabled)) - dataModelExpansion.InternalUpdate(args.DeltaTime); - } - - List modules; - lock (_modules) - { - modules = _modules.Where(m => m.IsActivated || m.InternalExpandsMainDataModel) - .OrderBy(m => m.PriorityCategory) - .ThenByDescending(m => m.Priority) - .ToList(); - } - - // Update all active modules - foreach (Module module in modules) - module.InternalUpdate(args.DeltaTime); - - lock (_bitmapLock) - { - if (_bitmap == null) - { - _bitmap = CreateBitmap(); - _rgbService.UpdateTexture(_bitmap); - } - - // Render all active modules - using SKCanvas canvas = new(_bitmap); - canvas.Scale((float)_renderScale.Value); - canvas.Clear(new SKColor(0, 0, 0)); - if (!ModuleRenderingDisabled) - // While non-activated modules may be updated above if they expand the main data model, they may never render - foreach (Module module in modules.Where(m => m.IsActivated)) - module.InternalRender(args.DeltaTime, canvas, _bitmap.Info); - - OnFrameRendering(new FrameRenderingEventArgs(canvas, args.DeltaTime, _rgbService.Surface)); - } - } - catch (Exception e) - { - _updateExceptions.Add(e); - } - finally - { - _frameStopWatch.Stop(); - FrameTime = _frameStopWatch.Elapsed; - - LogUpdateExceptions(); - } - } - - private SKBitmap CreateBitmap() - { - float width = MathF.Min(_rgbService.Surface.Boundary.Size.Width * (float)_renderScale.Value, 4096); - float height = MathF.Min(_rgbService.Surface.Boundary.Size.Height * (float)_renderScale.Value, 4096); - return new SKBitmap(new SKImageInfo(width.RoundToInt(), height.RoundToInt(), SKColorType.Rgb888x)); - } - - private void InvalidateBitmap() - { - lock (_bitmapLock) - { - _bitmap = null; - } - } - - private void SurfaceOnSurfaceLayoutChanged(SurfaceLayoutChangedEventArgs args) => InvalidateBitmap(); - private void RenderScaleSettingChanged(object? sender, EventArgs e) => InvalidateBitmap(); - - private void LogUpdateExceptions() - { - // Only log update exceptions every 10 seconds to avoid spamming the logs - if (DateTime.Now - _lastExceptionLog < TimeSpan.FromSeconds(10)) - return; - _lastExceptionLog = DateTime.Now; - - if (!_updateExceptions.Any()) - return; - - // Group by stack trace, that should gather up duplicate exceptions - foreach (IGrouping exceptions in _updateExceptions.GroupBy(e => e.StackTrace)) - _logger.Warning(exceptions.First(), "Exception was thrown {count} times during update in the last 10 seconds", exceptions.Count()); - - // When logging is finished start with a fresh slate - _updateExceptions.Clear(); - } - - private void SurfaceOnUpdated(UpdatedEventArgs args) - { - if (_rgbService.IsRenderPaused) - return; - - OnFrameRendered(new FrameRenderedEventArgs(_rgbService.Texture!, _rgbService.Surface)); - } - - #region Events - public event EventHandler? Initialized; public event EventHandler? FrameRendering; public event EventHandler? FrameRendered; - - private void OnInitialized() - { - IsInitialized = true; - Initialized?.Invoke(this, EventArgs.Empty); - } - - #endregion } } \ No newline at end of file diff --git a/src/Artemis.Core/Services/Interfaces/ICoreService.cs b/src/Artemis.Core/Services/Interfaces/ICoreService.cs index f3d934d11..4e1077c0f 100644 --- a/src/Artemis.Core/Services/Interfaces/ICoreService.cs +++ b/src/Artemis.Core/Services/Interfaces/ICoreService.cs @@ -54,7 +54,7 @@ namespace Artemis.Core.Services event EventHandler FrameRendering; /// - /// Occurs whenever a frame is finished rendering and processed by RGB.NET + /// Occurs whenever a frame is finished rendering and the render pipeline is closed /// event EventHandler FrameRendered; } diff --git a/src/Artemis.Core/Services/Interfaces/IRgbService.cs b/src/Artemis.Core/Services/Interfaces/IRgbService.cs index 565fb31a3..099b4bb3f 100644 --- a/src/Artemis.Core/Services/Interfaces/IRgbService.cs +++ b/src/Artemis.Core/Services/Interfaces/IRgbService.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using RGB.NET.Core; -using SkiaSharp; namespace Artemis.Core.Services { @@ -31,31 +30,25 @@ namespace Artemis.Core.Services /// RGBSurface Surface { get; set; } - /// - /// Gets the texture brush used to convert the rendered frame to LED-colors - /// - TextureBrush TextureBrush { get; } - - /// - /// Gets the texture used to convert the rendered frame to LED-colors - /// - SKTexture? Texture { get; } - - /// - /// Gets the update trigger that drives the render loop - /// - TimerUpdateTrigger UpdateTrigger { get; } - /// /// Gets or sets whether rendering should be paused /// bool IsRenderPaused { get; set; } /// - /// Recreates the Texture to use the given + /// Gets a boolean indicating whether the render pipeline is open /// - /// - void UpdateTexture(SKBitmap bitmap); + bool RenderOpen { get; } + + /// + /// Opens the render pipeline + /// + SKTexture OpenRender(); + + /// + /// Closes the render pipeline + /// + void CloseRender(); /// /// Adds the given device provider to the diff --git a/src/Artemis.Core/Services/RgbService.cs b/src/Artemis.Core/Services/RgbService.cs index 6d322b8d0..28937a7c3 100644 --- a/src/Artemis.Core/Services/RgbService.cs +++ b/src/Artemis.Core/Services/RgbService.cs @@ -17,16 +17,18 @@ namespace Artemis.Core.Services /// internal class RgbService : IRgbService { + private readonly IDeviceRepository _deviceRepository; private readonly List _devices; private readonly List _enabledDevices; - private Dictionary _ledMap; - private readonly ILogger _logger; private readonly IPluginManagementService _pluginManagementService; - private readonly IDeviceRepository _deviceRepository; + private readonly PluginSetting _renderScaleSetting; private readonly PluginSetting _targetFrameRateSetting; - private ListLedGroup? _surfaceLedGroup; + private readonly TextureBrush _textureBrush = new(ITexture.Empty) {CalculationMode = RenderMode.Absolute}; + private Dictionary _ledMap; private bool _modifyingProviders; + private ListLedGroup? _surfaceLedGroup; + private SKTexture? _texture; public RgbService(ILogger logger, ISettingsService settingsService, IPluginManagementService pluginManagementService, IDeviceRepository deviceRepository) { @@ -34,6 +36,7 @@ namespace Artemis.Core.Services _pluginManagementService = pluginManagementService; _deviceRepository = deviceRepository; _targetFrameRateSetting = settingsService.GetSetting("Core.TargetFrameRate", 25); + _renderScaleSetting = settingsService.GetSetting("Core.RenderScale", 0.5); Surface = new RGBSurface(); @@ -45,10 +48,72 @@ namespace Artemis.Core.Services _devices = new List(); _ledMap = new Dictionary(); - UpdateTrigger = new TimerUpdateTrigger { UpdateFrequency = 1.0 / _targetFrameRateSetting.Value }; + UpdateTrigger = new TimerUpdateTrigger {UpdateFrequency = 1.0 / _targetFrameRateSetting.Value}; Surface.RegisterUpdateTrigger(UpdateTrigger); } + public TimerUpdateTrigger UpdateTrigger { get; } + + protected virtual void OnDeviceRemoved(DeviceEventArgs e) + { + DeviceRemoved?.Invoke(this, e); + } + + protected virtual void OnLedsChanged() + { + LedsChanged?.Invoke(this, EventArgs.Empty); + _texture?.Invalidate(); + } + + private void SurfaceOnLayoutChanged(SurfaceLayoutChangedEventArgs args) + { + UpdateLedGroup(); + } + + private void UpdateLedGroup() + { + lock (_devices) + { + if (_modifyingProviders) + return; + + _ledMap = new Dictionary(_devices.SelectMany(d => d.Leds).ToDictionary(l => l.RgbLed)); + + if (_surfaceLedGroup == null) + { + _surfaceLedGroup = new ListLedGroup(Surface, LedMap.Select(l => l.Key)) {Brush = _textureBrush}; + OnLedsChanged(); + return; + } + + lock (_surfaceLedGroup) + { + // Clean up the old background + _surfaceLedGroup.Detach(); + + // Apply the application wide brush and decorator + _surfaceLedGroup = new ListLedGroup(Surface, LedMap.Select(l => l.Key)) {Brush = _textureBrush}; + OnLedsChanged(); + } + } + } + + private void TargetFrameRateSettingOnSettingChanged(object? sender, EventArgs e) + { + UpdateTrigger.UpdateFrequency = 1.0 / _targetFrameRateSetting.Value; + } + + private void SurfaceOnException(ExceptionEventArgs args) + { + _logger.Warning("Surface threw e"); + throw args.Exception; + } + + private void OnDeviceAdded(DeviceEventArgs e) + { + DeviceAdded?.Invoke(this, e); + } + public IReadOnlyCollection EnabledDevices => _enabledDevices.AsReadOnly(); public IReadOnlyCollection Devices => _devices.AsReadOnly(); public IReadOnlyDictionary LedMap => new ReadOnlyDictionary(_ledMap); @@ -56,11 +121,8 @@ namespace Artemis.Core.Services /// public RGBSurface Surface { get; set; } - public TimerUpdateTrigger UpdateTrigger { get; } - public TextureBrush TextureBrush { get; private set; } = new(ITexture.Empty) { CalculationMode = RenderMode.Absolute }; - public SKTexture? Texture { get; private set; } - public bool IsRenderPaused { get; set; } + public bool RenderOpen { get; private set; } public void AddDeviceProvider(IRGBDeviceProvider deviceProvider) { @@ -88,7 +150,7 @@ namespace Artemis.Core.Services { ArtemisDevice artemisDevice = GetArtemisDevice(rgbDevice); AddDevice(artemisDevice); - _logger.Debug("Device provider {deviceProvider} added {deviceName}", deviceProvider.GetType().Name, rgbDevice.DeviceInfo?.DeviceName); + _logger.Debug("Device provider {deviceProvider} added {deviceName}", deviceProvider.GetType().Name, rgbDevice.DeviceInfo.DeviceName); } _devices.Sort((a, b) => a.ZIndex - b.ZIndex); @@ -134,46 +196,6 @@ namespace Artemis.Core.Services } } - public void UpdateTexture(SKBitmap bitmap) - { - SKTexture? oldTexture = Texture; - Texture = new SKTexture(bitmap); - TextureBrush.Texture = Texture; - - oldTexture?.Dispose(); - } - - private void SurfaceOnLayoutChanged(SurfaceLayoutChangedEventArgs args) => UpdateLedGroup(); - - private void UpdateLedGroup() - { - lock (_devices) - { - if (_modifyingProviders) - return; - - _ledMap = new Dictionary(_devices.SelectMany(d => d.Leds).ToDictionary(l => l.RgbLed)); - - if (_surfaceLedGroup == null) - { - _surfaceLedGroup = new ListLedGroup(Surface, LedMap.Select(l => l.Key)) { Brush = TextureBrush }; - OnLedsChanged(); - return; - } - - lock (_surfaceLedGroup) - { - // Clean up the old background - _surfaceLedGroup.Detach(); - - // Apply the application wide brush and decorator - _surfaceLedGroup = new ListLedGroup(Surface, LedMap.Select(l => l.Key)) { Brush = TextureBrush }; - OnLedsChanged(); - } - } - } - - #region IDisposable public void Dispose() { @@ -183,6 +205,48 @@ namespace Artemis.Core.Services Surface.Dispose(); } + public event EventHandler? DeviceAdded; + public event EventHandler? DeviceRemoved; + public event EventHandler? LedsChanged; + + #region Rendering + + public SKTexture OpenRender() + { + if (RenderOpen) + throw new ArtemisCoreException("Render pipeline is already open"); + + if (_texture == null || _texture.IsInvalid) + CreateTexture(); + + RenderOpen = true; + return _texture!; + } + + public void CloseRender() + { + if (!RenderOpen) + throw new ArtemisCoreException("Render pipeline is already closed"); + + RenderOpen = false; + } + + public void CreateTexture() + { + if (RenderOpen) + throw new ArtemisCoreException("Cannot update the texture while rendering"); + + SKTexture? oldTexture = _texture; + + float renderScale = (float) _renderScaleSetting.Value; + int width = Math.Max(1, MathF.Min(Surface.Boundary.Size.Width * renderScale, 4096).RoundToInt()); + int height = Math.Max(1, MathF.Min(Surface.Boundary.Size.Height * renderScale, 4096).RoundToInt()); + _texture = new SKTexture(width, height, renderScale); + _textureBrush.Texture = _texture; + + oldTexture?.Dispose(); + } + #endregion #region EnabledDevices @@ -197,7 +261,7 @@ namespace Artemis.Core.Services public ArtemisLayout ApplyBestDeviceLayout(ArtemisDevice device) { ArtemisLayout layout; - + // Configured layout path takes precedence over all other options if (device.CustomLayoutPath != null) { @@ -233,7 +297,7 @@ namespace Artemis.Core.Services private ArtemisLayout LoadDefaultLayout(ArtemisDevice device) { - return new ArtemisLayout("NYI", LayoutSource.Default); + return new("NYI", LayoutSource.Default); } public void ApplyDeviceLayout(ArtemisDevice device, ArtemisLayout layout, bool createMissingLeds, bool removeExessiveLeds) @@ -347,43 +411,5 @@ namespace Artemis.Core.Services } #endregion - - #region Event handlers - - private void TargetFrameRateSettingOnSettingChanged(object? sender, EventArgs e) - { - UpdateTrigger.UpdateFrequency = 1.0 / _targetFrameRateSetting.Value; - } - - private void SurfaceOnException(ExceptionEventArgs args) - { - _logger.Warning("Surface threw e"); - throw args.Exception; - } - - #endregion - - #region Events - - public event EventHandler? DeviceAdded; - public event EventHandler? DeviceRemoved; - public event EventHandler? LedsChanged; - - private void OnDeviceAdded(DeviceEventArgs e) - { - DeviceAdded?.Invoke(this, e); - } - - protected virtual void OnDeviceRemoved(DeviceEventArgs e) - { - DeviceRemoved?.Invoke(this, e); - } - - protected virtual void OnLedsChanged() - { - LedsChanged?.Invoke(this, EventArgs.Empty); - } - - #endregion } } \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Controls/ColorPicker.xaml.cs b/src/Artemis.UI.Shared/Controls/ColorPicker.xaml.cs index 9a5a4fb02..84656d58a 100644 --- a/src/Artemis.UI.Shared/Controls/ColorPicker.xaml.cs +++ b/src/Artemis.UI.Shared/Controls/ColorPicker.xaml.cs @@ -245,7 +245,8 @@ namespace Artemis.UI.Shared private void PopupOpened() { - RecentColorsContainer.ItemsSource = new ObservableCollection(_colorPickerService.RecentColors); + if (_colorPickerService != null) + RecentColorsContainer.ItemsSource = new ObservableCollection(_colorPickerService.RecentColors); } private void SelectRecentColor(object sender, MouseButtonEventArgs e) diff --git a/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelDynamicViewModel.cs b/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelDynamicViewModel.cs index f7ead0cbb..12292fc6d 100644 --- a/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelDynamicViewModel.cs +++ b/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelDynamicViewModel.cs @@ -26,7 +26,7 @@ namespace Artemis.UI.Shared.Input private DataModelPath? _dataModelPath; private DataModelPropertiesViewModel? _dataModelViewModel; private bool _displaySwitchButton; - private Type[] _filterTypes = new Type[0]; + private Type[] _filterTypes = Array.Empty(); private bool _isDataModelViewModelOpen; private bool _isEnabled = true; private string _placeholder = "Select a property"; @@ -143,7 +143,8 @@ namespace Artemis.UI.Shared.Input if (value) { UpdateDataModelVisualization(); - OpenSelectedValue(DataModelViewModel); + if (DataModelViewModel != null) + OpenSelectedValue(DataModelViewModel); } } } @@ -242,7 +243,7 @@ namespace Artemis.UI.Shared.Input private void ExecuteSelectPropertyCommand(object? context) { - if (!(context is DataModelVisualizationViewModel selected)) + if (context is not DataModelVisualizationViewModel selected) return; ChangeDataModelPath(selected.DataModelPath); diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/ConditionalDataBinding/ConditionalDataBindingModeViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/ConditionalDataBinding/ConditionalDataBindingModeViewModel.cs index e3b54179a..b45c7728b 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/ConditionalDataBinding/ConditionalDataBindingModeViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/ConditionalDataBinding/ConditionalDataBindingModeViewModel.cs @@ -57,7 +57,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings.Conditio base.OnClose(); } - private void CoreServiceOnFrameRendered(object? sender, FrameRenderedEventArgs e) + private void CoreServiceOnFrameRendered(object sender, FrameRenderedEventArgs e) { foreach (DataBindingConditionViewModel dataBindingConditionViewModel in Items) dataBindingConditionViewModel.Evaluate(); diff --git a/src/Artemis.UI/Screens/Settings/Debug/Tabs/RenderDebugViewModel.cs b/src/Artemis.UI/Screens/Settings/Debug/Tabs/RenderDebugViewModel.cs index e07bf9d37..b66263387 100644 --- a/src/Artemis.UI/Screens/Settings/Debug/Tabs/RenderDebugViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Debug/Tabs/RenderDebugViewModel.cs @@ -64,43 +64,29 @@ namespace Artemis.UI.Screens.Settings.Debug.Tabs private void CoreServiceOnFrameRendered(object sender, FrameRenderedEventArgs e) { - Execute.PostToUIThread(() => + Execute.OnUIThreadSync(() => { - // TODO: Remove, frames shouldn't even be rendered if this is the case - if (e.Texture.Bitmap.Pixels.Length == 0) - return; - SKImageInfo bitmapInfo = e.Texture.Bitmap.Info; RenderHeight = bitmapInfo.Height; RenderWidth = bitmapInfo.Width; - if (!(CurrentFrame is WriteableBitmap writeableBitmap) || - writeableBitmap.Width != bitmapInfo.Width || - writeableBitmap.Height != bitmapInfo.Height) + // ReSharper disable twice CompareOfFloatsByEqualityOperator + if (CurrentFrame is not WriteableBitmap writable || writable.Width != bitmapInfo.Width || writable.Height != bitmapInfo.Height) { CurrentFrame = e.Texture.Bitmap.ToWriteableBitmap(); return; } - - try + + using SKImage skImage = SKImage.FromPixels(e.Texture.Bitmap.PeekPixels()); + SKImageInfo info = new(skImage.Width, skImage.Height); + writable.Lock(); + using (SKPixmap pixmap = new(info, writable.BackBuffer, writable.BackBufferStride)) { - using (SKImage skiaImage = SKImage.FromPixels(e.Texture.Bitmap.PeekPixels())) - { - SKImageInfo info = new(skiaImage.Width, skiaImage.Height); - writeableBitmap.Lock(); - using (SKPixmap pixmap = new(info, writeableBitmap.BackBuffer, writeableBitmap.BackBufferStride)) - { - skiaImage.ReadPixels(pixmap, 0, 0); - } + skImage.ReadPixels(pixmap, 0, 0); + } - writeableBitmap.AddDirtyRect(new Int32Rect(0, 0, writeableBitmap.PixelWidth, writeableBitmap.PixelHeight)); - writeableBitmap.Unlock(); - } - } - catch (AccessViolationException) - { - // oops - } + writable.AddDirtyRect(new Int32Rect(0, 0, writable.PixelWidth, writable.PixelHeight)); + writable.Unlock(); }); } diff --git a/src/Artemis.UI/Screens/Settings/Device/DeviceDialogViewModel.cs b/src/Artemis.UI/Screens/Settings/Device/DeviceDialogViewModel.cs index c1472c721..760f3fb96 100644 --- a/src/Artemis.UI/Screens/Settings/Device/DeviceDialogViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Device/DeviceDialogViewModel.cs @@ -192,7 +192,7 @@ namespace Artemis.UI.Screens.Settings.Device #region Event handlers - private void DeviceOnDeviceUpdated(object? sender, EventArgs e) + private void DeviceOnDeviceUpdated(object sender, EventArgs e) { NotifyOfPropertyChange(nameof(CanExportLayout)); }