From 8431a8cb5e24243ed58223538fa959b64b4efcf8 Mon Sep 17 00:00:00 2001 From: Darth Affe Date: Sat, 11 Feb 2023 22:36:59 +0100 Subject: [PATCH] (MAJOR) Optimized surface-updating to reduce the amount of allocations --- RGB.NET.Core/Events/UpdatingEventArgs.cs | 4 +- RGB.NET.Core/Groups/AbstractLedGroup.cs | 4 ++ RGB.NET.Core/Groups/ILedGroup.cs | 6 +++ RGB.NET.Core/Groups/ListLedGroup.cs | 9 +++- RGB.NET.Core/Positioning/Rectangle.cs | 33 ++++++++++-- RGB.NET.Core/RGBSurface.cs | 28 ++++++---- RGB.NET.Core/Update/CustomUpdateData.cs | 60 +++++++++++++++++++-- RGB.NET.Presets/Groups/RectangleLedGroup.cs | 9 +++- 8 files changed, 129 insertions(+), 24 deletions(-) diff --git a/RGB.NET.Core/Events/UpdatingEventArgs.cs b/RGB.NET.Core/Events/UpdatingEventArgs.cs index 47df4af..d39ea74 100644 --- a/RGB.NET.Core/Events/UpdatingEventArgs.cs +++ b/RGB.NET.Core/Events/UpdatingEventArgs.cs @@ -26,7 +26,7 @@ public class UpdatingEventArgs : EventArgs /// /// Gets the custom-data provided by the trigger for this update. /// - public CustomUpdateData CustomData { get; } + public ICustomUpdateData CustomData { get; } #endregion @@ -39,7 +39,7 @@ public class UpdatingEventArgs : EventArgs /// The elapsed time (in seconds) since the last update. /// The trigger causing this update. /// The custom-data provided by the trigger for this update. - public UpdatingEventArgs(double deltaTime, IUpdateTrigger? trigger, CustomUpdateData customData) + public UpdatingEventArgs(double deltaTime, IUpdateTrigger? trigger, ICustomUpdateData customData) { this.DeltaTime = deltaTime; this.Trigger = trigger; diff --git a/RGB.NET.Core/Groups/AbstractLedGroup.cs b/RGB.NET.Core/Groups/AbstractLedGroup.cs index 6569dac..86659af 100644 --- a/RGB.NET.Core/Groups/AbstractLedGroup.cs +++ b/RGB.NET.Core/Groups/AbstractLedGroup.cs @@ -1,5 +1,6 @@ using System.Collections; using System.Collections.Generic; +using System.Linq; namespace RGB.NET.Core; @@ -51,6 +52,9 @@ public abstract class AbstractLedGroup : AbstractDecoratable /// public virtual void OnDetach() { } + /// + public virtual IList ToList() => GetLeds().ToList(); + /// IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); diff --git a/RGB.NET.Core/Groups/ILedGroup.cs b/RGB.NET.Core/Groups/ILedGroup.cs index 88b5b97..97ed8b2 100644 --- a/RGB.NET.Core/Groups/ILedGroup.cs +++ b/RGB.NET.Core/Groups/ILedGroup.cs @@ -39,4 +39,10 @@ public interface ILedGroup : IDecoratable, IEnumerable /// Called when the is detached from the . /// void OnDetach(); + + /// + /// Returns a list containing all in this group. + /// + /// A list containing all in this group. + IList ToList(); } \ No newline at end of file diff --git a/RGB.NET.Core/Groups/ListLedGroup.cs b/RGB.NET.Core/Groups/ListLedGroup.cs index a33fc56..9c51906 100644 --- a/RGB.NET.Core/Groups/ListLedGroup.cs +++ b/RGB.NET.Core/Groups/ListLedGroup.cs @@ -122,7 +122,14 @@ public class ListLedGroup : AbstractLedGroup /// Gets a list containing the from this group. /// /// The list containing the . - protected override IEnumerable GetLeds() + protected override IEnumerable GetLeds() => ToList(); + + /// + /// + /// Gets a list containing the from this group. + /// + /// The list containing the . + public override IList ToList() { lock (GroupLeds) return new List(GroupLeds); diff --git a/RGB.NET.Core/Positioning/Rectangle.cs b/RGB.NET.Core/Positioning/Rectangle.cs index 456a568..095a210 100644 --- a/RGB.NET.Core/Positioning/Rectangle.cs +++ b/RGB.NET.Core/Positioning/Rectangle.cs @@ -57,7 +57,8 @@ public readonly struct Rectangle : IEquatable /// Initializes a new instance of the class using the (0,0) and the specified . /// /// The size of of this . - public Rectangle(Size size) : this(new Point(), size) + public Rectangle(Size size) + : this(new Point(), size) { } /// @@ -120,15 +121,13 @@ public readonly struct Rectangle : IEquatable public Rectangle(params Point[] points) : this(points.AsEnumerable()) { } - - /// + /// /// Initializes a new instance of the class using the specified list of . /// The and is calculated to contain all points provided as parameters. /// /// The list of used to calculate the and . public Rectangle(IEnumerable points) - : this() { bool hasPoint = false; float posX = float.MaxValue; @@ -145,13 +144,37 @@ public readonly struct Rectangle : IEquatable posY2 = Math.Max(posY2, point.Y); } - (Point location, Size size) = hasPoint ? InitializeFromPoints(new Point(posX, posY), new Point(posX2, posY2)) : InitializeFromPoints(new Point(0, 0), new Point(0, 0)); + (Point location, Size size) = hasPoint ? InitializeFromPoints(new Point(posX, posY), new Point(posX2, posY2)) + : InitializeFromPoints(new Point(0, 0), new Point(0, 0)); Location = location; Size = size; Center = new Point(Location.X + (Size.Width / 2.0f), Location.Y + (Size.Height / 2.0f)); } + internal Rectangle(IList leds) + { + float posX = float.MaxValue; + float posY = float.MaxValue; + float posX2 = float.MinValue; + float posY2 = float.MinValue; + + // ReSharper disable once ForCanBeConvertedToForeach + for (int i = 0; i < leds.Count; i++) + { + Rectangle rectangle = leds[i].AbsoluteBoundary; + posX = Math.Min(posX, rectangle.Location.X); + posY = Math.Min(posY, rectangle.Location.Y); + posX2 = Math.Max(posX2, rectangle.Location.X + rectangle.Size.Width); + posY2 = Math.Max(posY2, rectangle.Location.Y + rectangle.Size.Height); + } + + (Point location, Size size) = leds.Count > 0 ? InitializeFromPoints(new Point(posX, posY), new Point(posX2, posY2)) : InitializeFromPoints(new Point(0, 0), new Point(0, 0)); + Location = location; + Size = size; + Center = new Point(Location.X + (Size.Width / 2.0f), Location.Y + (Size.Height / 2.0f)); + } + #endregion #region Methods diff --git a/RGB.NET.Core/RGBSurface.cs b/RGB.NET.Core/RGBSurface.cs index f8ec437..fc8003f 100644 --- a/RGB.NET.Core/RGBSurface.cs +++ b/RGB.NET.Core/RGBSurface.cs @@ -132,11 +132,12 @@ public sealed class RGBSurface : AbstractBindable, IDisposable /// Perform a full update for all devices. Updates only dirty by default, or all , if flushLeds is set to true. /// /// Specifies whether all , (including clean ones) should be updated. - public void Update(bool flushLeds = false) => Update(null, new CustomUpdateData((CustomUpdateDataIndex.FLUSH_LEDS, flushLeds))); + //public void Update(bool flushLeds = false) => Update(null, new CustomUpdateData((CustomUpdateDataIndex.FLUSH_LEDS, flushLeds))); + public void Update(bool flushLeds = false) => Update(null, flushLeds ? DefaultCustomUpdateData.FLUSH : DefaultCustomUpdateData.NO_FLUSH); - private void Update(object? updateTrigger, CustomUpdateData customData) => Update(updateTrigger as IUpdateTrigger, customData); + private void Update(object? updateTrigger, ICustomUpdateData customData) => Update(updateTrigger as IUpdateTrigger, customData); - private void Update(IUpdateTrigger? updateTrigger, CustomUpdateData customData) + private void Update(IUpdateTrigger? updateTrigger, ICustomUpdateData customData) { try { @@ -149,19 +150,25 @@ public sealed class RGBSurface : AbstractBindable, IDisposable { OnUpdating(updateTrigger, customData); + // ReSharper disable ForCanBeConvertedToForeach - 'for' has a performance benefit (no enumerator allocation) here and since 'Update' is considered a hot path it's optimized if (render) lock (_ledGroups) { // Render brushes - foreach (ILedGroup ledGroup in _ledGroups) - try { Render(ledGroup); } + for (int i = 0; i < _ledGroups.Count; i++) + { + try { Render(_ledGroups[i]); } catch (Exception ex) { OnException(ex); } + } } if (updateDevices) - foreach (IRGBDevice device in _devices) - try { device.Update(flushLeds); } + for (int i = 0; i < _devices.Count; i++) + { + try { _devices[i].Update(flushLeds); } catch (Exception ex) { OnException(ex); } + } + // ReSharper restore ForCanBeConvertedToForeach OnUpdated(); } @@ -197,16 +204,17 @@ public sealed class RGBSurface : AbstractBindable, IDisposable /// Thrown if the of the Brush is not valid. private void Render(ILedGroup ledGroup) { - IList leds = ledGroup.ToList(); IBrush? brush = ledGroup.Brush; if ((brush == null) || !brush.IsEnabled) return; + IList leds = ledGroup.ToList(); + IEnumerable<(RenderTarget renderTarget, Color color)> render; switch (brush.CalculationMode) { case RenderMode.Relative: - Rectangle brushRectangle = new(leds.Select(led => led.AbsoluteBoundary)); + Rectangle brushRectangle = new(leds); Point offset = new(-brushRectangle.Location.X, -brushRectangle.Location.Y); brushRectangle = brushRectangle.SetLocation(new Point(0, 0)); render = brush.Render(brushRectangle, leds.Select(led => new RenderTarget(led, led.AbsoluteBoundary.Translate(offset)))); @@ -358,7 +366,7 @@ public sealed class RGBSurface : AbstractBindable, IDisposable /// /// Handles the needed event-calls before updating. /// - private void OnUpdating(IUpdateTrigger? trigger, CustomUpdateData customData) + private void OnUpdating(IUpdateTrigger? trigger, ICustomUpdateData customData) { try { diff --git a/RGB.NET.Core/Update/CustomUpdateData.cs b/RGB.NET.Core/Update/CustomUpdateData.cs index 88f734a..a657e96 100644 --- a/RGB.NET.Core/Update/CustomUpdateData.cs +++ b/RGB.NET.Core/Update/CustomUpdateData.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; namespace RGB.NET.Core; @@ -34,11 +35,24 @@ public static class CustomUpdateDataIndex /// /// Represents a set of custom data, each indexed by a string-key. /// -public class CustomUpdateData +public interface ICustomUpdateData +{ + /// + /// Gets the value for a specific key. + /// + /// The key of the value. + /// The value represented by the specified key. + object? this[string key] { get; } +} + +/// +/// Represents a set of custom data, each indexed by a string-key. +/// +public class CustomUpdateData : ICustomUpdateData { #region Properties & Fields - private Dictionary _data = new(); + private readonly Dictionary _data = new(); #endregion @@ -51,8 +65,8 @@ public class CustomUpdateData /// The value represented by the specified key. public object? this[string key] { - get => _data.TryGetValue(key.ToUpperInvariant(), out object? data) ? data : default; - set => _data[key.ToUpperInvariant()] = value; + get => _data.TryGetValue(key, out object? data) ? data : default; + set => _data[key] = value; } #endregion @@ -77,3 +91,39 @@ public class CustomUpdateData #endregion } + +internal class DefaultCustomUpdateData : ICustomUpdateData +{ + #region Constants + + public static readonly DefaultCustomUpdateData FLUSH = new(true); + public static readonly DefaultCustomUpdateData NO_FLUSH = new(false); + + #endregion + + #region Properties & Fields + + private readonly bool _flushLeds; + + public object? this[string key] + { + get + { + if (string.Equals(key, CustomUpdateDataIndex.FLUSH_LEDS, StringComparison.Ordinal)) + return _flushLeds; + + return null; + } + } + + #endregion + + #region Constructors + + public DefaultCustomUpdateData(bool flushLeds) + { + this._flushLeds = flushLeds; + } + + #endregion +} \ No newline at end of file diff --git a/RGB.NET.Presets/Groups/RectangleLedGroup.cs b/RGB.NET.Presets/Groups/RectangleLedGroup.cs index 9547476..d6cb650 100644 --- a/RGB.NET.Presets/Groups/RectangleLedGroup.cs +++ b/RGB.NET.Presets/Groups/RectangleLedGroup.cs @@ -114,7 +114,14 @@ public class RectangleLedGroup : AbstractLedGroup /// Gets a list containing all of this . /// /// The list containing all of this . - protected override IEnumerable GetLeds() => _ledCache ??= (Surface?.Leds.Where(led => led.AbsoluteBoundary.CalculateIntersectPercentage(Rectangle) >= MinOverlayPercentage).ToList() ?? new List()); + protected override IEnumerable GetLeds() => ToList(); + + /// + /// + /// Gets a list containing all of this . + /// + /// The list containing all of this . + public override IList ToList() => _ledCache ??= (Surface?.Leds.Where(led => led.AbsoluteBoundary.CalculateIntersectPercentage(Rectangle) >= MinOverlayPercentage).ToList() ?? new List()); private void InvalidateCache() => _ledCache = null;