From eb7c89d4ad547b44bb849d8984981af0b46064e0 Mon Sep 17 00:00:00 2001 From: Robert Date: Thu, 25 Mar 2021 19:59:28 +0100 Subject: [PATCH] Rendering - Sovled inconsistencies between software and Vulkan rendering LED sampling - Improved LED sampling on <100% scale --- .../Extensions/FloatExtensions.cs | 2 +- .../Extensions/RgbDeviceExtensions.cs | 13 +- src/Artemis.Core/Models/Profile/Folder.cs | 11 +- src/Artemis.Core/Models/Profile/Layer.cs | 25 ++-- src/Artemis.Core/Models/Profile/Profile.cs | 2 +- .../Models/Profile/ProfileElement.cs | 2 +- .../Models/Profile/RenderProfileElement.cs | 11 +- src/Artemis.Core/Models/Surface/ArtemisLed.cs | 12 +- .../Plugins/Modules/ProfileModule.cs | 2 +- src/Artemis.Core/RGB.NET/SKTexture.cs | 118 +++++++++++++----- src/Artemis.Core/Services/CoreService.cs | 7 +- src/Artemis.Core/Services/RgbService.cs | 12 +- src/Artemis.Core/Utilities/IntroAnimation.cs | 2 +- .../Services/ProfileEditorService.cs | 6 +- .../General/GeneralSettingsTabViewModel.cs | 9 +- 15 files changed, 158 insertions(+), 76 deletions(-) diff --git a/src/Artemis.Core/Extensions/FloatExtensions.cs b/src/Artemis.Core/Extensions/FloatExtensions.cs index aabe25497..0a4cb9568 100644 --- a/src/Artemis.Core/Extensions/FloatExtensions.cs +++ b/src/Artemis.Core/Extensions/FloatExtensions.cs @@ -16,7 +16,7 @@ namespace Artemis.Core [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int RoundToInt(this float number) { - return (int) Math.Round(number, MidpointRounding.AwayFromZero); + return (int) MathF.Round(number, MidpointRounding.AwayFromZero); } } } \ No newline at end of file diff --git a/src/Artemis.Core/Extensions/RgbDeviceExtensions.cs b/src/Artemis.Core/Extensions/RgbDeviceExtensions.cs index 5c1e11353..c6139f30c 100644 --- a/src/Artemis.Core/Extensions/RgbDeviceExtensions.cs +++ b/src/Artemis.Core/Extensions/RgbDeviceExtensions.cs @@ -25,11 +25,16 @@ namespace Artemis.Core public static SKRect ToSKRect(this Rectangle rectangle) { return SKRect.Create( - (float) rectangle.Location.X, - (float) rectangle.Location.Y, - (float) rectangle.Size.Width, - (float) rectangle.Size.Height + rectangle.Location.X, + rectangle.Location.Y, + rectangle.Size.Width, + rectangle.Size.Height ); } + + public static SKRectI ToSKRectI(this Rectangle rectangle) + { + return SKRectI.Round(ToSKRect(rectangle)); + } } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Folder.cs b/src/Artemis.Core/Models/Profile/Folder.cs index 1d273c71b..9557e3771 100644 --- a/src/Artemis.Core/Models/Profile/Folder.cs +++ b/src/Artemis.Core/Models/Profile/Folder.cs @@ -4,7 +4,6 @@ using System.Linq; using Artemis.Core.LayerEffects; using Artemis.Storage.Entities.Profile; using Artemis.Storage.Entities.Profile.Abstract; -using Newtonsoft.Json; using SkiaSharp; namespace Artemis.Core @@ -153,8 +152,10 @@ namespace Artemis.Core SKPath path = new() {FillType = SKPathFillType.Winding}; foreach (ProfileElement child in Children) + { if (child is RenderProfileElement effectChild && effectChild.Path != null) path.AddPath(effectChild.Path); + } Path = path; @@ -168,7 +169,7 @@ namespace Artemis.Core #region Rendering /// - public override void Render(SKCanvas canvas, SKPoint basePosition) + public override void Render(SKCanvas canvas, SKPointI basePosition) { if (Disposed) throw new ObjectDisposedException("Folder"); @@ -192,12 +193,12 @@ namespace Artemis.Core SKPaint layerPaint = new(); try { - SKRect rendererBounds = SKRect.Create(0, 0, Path.Bounds.Width, Path.Bounds.Height); + SKRectI rendererBounds = SKRectI.Create(0, 0, Bounds.Width, Bounds.Height); foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => e.Enabled)) baseLayerEffect.PreProcess(canvas, rendererBounds, layerPaint); canvas.SaveLayer(layerPaint); - canvas.Translate(Path.Bounds.Left - basePosition.X, Path.Bounds.Top - basePosition.Y); + canvas.Translate(Bounds.Left - basePosition.X, Bounds.Top - basePosition.Y); // If required, apply the opacity override of the module to the root folder if (IsRootFolder && Profile.Module.OpacityOverride < 1) @@ -212,7 +213,7 @@ namespace Artemis.Core // Iterate the children in reverse because the first layer must be rendered last to end up on top for (int index = Children.Count - 1; index > -1; index--) - Children[index].Render(canvas, new SKPoint(Path.Bounds.Left, Path.Bounds.Top)); + Children[index].Render(canvas, new SKPointI(Bounds.Left, Bounds.Top)); foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => e.Enabled)) baseLayerEffect.PostProcess(canvas, rendererBounds, layerPaint); diff --git a/src/Artemis.Core/Models/Profile/Layer.cs b/src/Artemis.Core/Models/Profile/Layer.cs index 3feac187a..47a08ab8a 100644 --- a/src/Artemis.Core/Models/Profile/Layer.cs +++ b/src/Artemis.Core/Models/Profile/Layer.cs @@ -277,7 +277,7 @@ namespace Artemis.Core } /// - public override void Render(SKCanvas canvas, SKPoint basePosition) + public override void Render(SKCanvas canvas, SKPointI basePosition) { if (Disposed) throw new ObjectDisposedException("Layer"); @@ -312,7 +312,7 @@ namespace Artemis.Core } } - private void RenderTimeline(Timeline timeline, SKCanvas canvas, SKPoint basePosition) + private void RenderTimeline(Timeline timeline, SKCanvas canvas, SKPointI basePosition) { if (Path == null || LayerBrush == null) throw new ArtemisCoreException("The layer is not yet ready for rendering"); @@ -329,12 +329,11 @@ namespace Artemis.Core try { canvas.Save(); - canvas.Translate(Path.Bounds.Left - basePosition.X, Path.Bounds.Top - basePosition.Y); + canvas.Translate(Bounds.Left - basePosition.X, Bounds.Top - basePosition.Y); using SKPath clipPath = new(Path); - clipPath.Transform(SKMatrix.CreateTranslation(Path.Bounds.Left * -1, Path.Bounds.Top * -1)); - canvas.ClipPath(clipPath); - - SKRect layerBounds = SKRect.Create(0, 0, Path.Bounds.Width, Path.Bounds.Height); + clipPath.Transform(SKMatrix.CreateTranslation(Bounds.Left * -1, Bounds.Top * -1)); + canvas.ClipPath(clipPath, SKClipOperation.Intersect, true); + SKRectI layerBounds = SKRectI.Create(0, 0, Bounds.Width, Bounds.Height); // Apply blend mode and color layerPaint.BlendMode = General.BlendMode.CurrentValue; @@ -435,7 +434,7 @@ namespace Artemis.Core OnRenderPropertiesUpdated(); } - internal SKPoint GetLayerAnchorPosition(SKPath layerPath, bool applyTranslation, bool zeroBased) + internal SKPoint GetLayerAnchorPosition(bool applyTranslation, bool zeroBased) { if (Disposed) throw new ObjectDisposedException("Layer"); @@ -444,14 +443,14 @@ namespace Artemis.Core // Start at the center of the shape SKPoint position = zeroBased - ? new SKPoint(layerPath.Bounds.MidX - layerPath.Bounds.Left, layerPath.Bounds.MidY - layerPath.Bounds.Top) - : new SKPoint(layerPath.Bounds.MidX, layerPath.Bounds.MidY); + ? new SKPointI(Bounds.MidX - Bounds.Left, Bounds.MidY - Bounds.Top) + : new SKPointI(Bounds.MidX, Bounds.MidY); // Apply translation if (applyTranslation) { - position.X += positionProperty.X * layerPath.Bounds.Width; - position.Y += positionProperty.Y * layerPath.Bounds.Height; + position.X += positionProperty.X * Bounds.Width; + position.Y += positionProperty.Y * Bounds.Height; } return position; @@ -479,7 +478,7 @@ namespace Artemis.Core SKSize sizeProperty = Transform.Scale.CurrentValue; float rotationProperty = Transform.Rotation.CurrentValue; - SKPoint anchorPosition = GetLayerAnchorPosition(Path, true, zeroBased); + SKPoint anchorPosition = GetLayerAnchorPosition(true, zeroBased); SKPoint anchorProperty = Transform.AnchorPoint.CurrentValue; // Translation originates from the unscaled center of the shape and is tied to the anchor diff --git a/src/Artemis.Core/Models/Profile/Profile.cs b/src/Artemis.Core/Models/Profile/Profile.cs index 96616f104..8f3cf81a9 100644 --- a/src/Artemis.Core/Models/Profile/Profile.cs +++ b/src/Artemis.Core/Models/Profile/Profile.cs @@ -81,7 +81,7 @@ namespace Artemis.Core } /// - public override void Render(SKCanvas canvas, SKPoint basePosition) + public override void Render(SKCanvas canvas, SKPointI basePosition) { lock (_lock) { diff --git a/src/Artemis.Core/Models/Profile/ProfileElement.cs b/src/Artemis.Core/Models/Profile/ProfileElement.cs index 1072e4a94..a88d13f8e 100644 --- a/src/Artemis.Core/Models/Profile/ProfileElement.cs +++ b/src/Artemis.Core/Models/Profile/ProfileElement.cs @@ -104,7 +104,7 @@ namespace Artemis.Core /// /// Renders the element /// - public abstract void Render(SKCanvas canvas, SKPoint basePosition); + public abstract void Render(SKCanvas canvas, SKPointI basePosition); /// /// Resets the internal state of the element diff --git a/src/Artemis.Core/Models/Profile/RenderProfileElement.cs b/src/Artemis.Core/Models/Profile/RenderProfileElement.cs index 899a01ff9..c2c967c54 100644 --- a/src/Artemis.Core/Models/Profile/RenderProfileElement.cs +++ b/src/Artemis.Core/Models/Profile/RenderProfileElement.cs @@ -16,6 +16,9 @@ namespace Artemis.Core /// public abstract class RenderProfileElement : ProfileElement { + private SKPath? _path; + private SKRectI _bounds; + internal RenderProfileElement(Profile profile) : base(profile) { Timeline = new Timeline(); @@ -113,7 +116,6 @@ namespace Artemis.Core #region Properties - private SKPath? _path; internal abstract RenderElementEntity RenderElementEntity { get; } /// @@ -141,14 +143,14 @@ namespace Artemis.Core SetAndNotify(ref _path, value); // I can't really be sure about the performance impact of calling Bounds often but // SkiaSharp calls SkiaApi.sk_path_get_bounds (Handle, &rect); which sounds expensive - Bounds = value?.Bounds ?? SKRect.Empty; + Bounds = SKRectI.Round(value?.Bounds ?? SKRect.Empty); } } /// /// The bounds of this entity /// - public SKRect Bounds + public SKRectI Bounds { get => _bounds; private set => SetAndNotify(ref _bounds, value); @@ -158,8 +160,7 @@ namespace Artemis.Core #region Property group expansion internal List ExpandedPropertyGroups; - private SKRect _bounds; - + /// /// Determines whether the provided property group is expanded /// diff --git a/src/Artemis.Core/Models/Surface/ArtemisLed.cs b/src/Artemis.Core/Models/Surface/ArtemisLed.cs index b8b81ec5b..cec97bd94 100644 --- a/src/Artemis.Core/Models/Surface/ArtemisLed.cs +++ b/src/Artemis.Core/Models/Surface/ArtemisLed.cs @@ -8,8 +8,8 @@ namespace Artemis.Core /// public class ArtemisLed : CorePropertyChanged { - private SKRect _absoluteRectangle; - private SKRect _rectangle; + private SKRectI _absoluteRectangle; + private SKRectI _rectangle; internal ArtemisLed(Led led, ArtemisDevice device) { @@ -31,7 +31,7 @@ namespace Artemis.Core /// /// Gets the rectangle covering the LED positioned relative to the /// - public SKRect Rectangle + public SKRectI Rectangle { get => _rectangle; private set => SetAndNotify(ref _rectangle, value); @@ -40,7 +40,7 @@ namespace Artemis.Core /// /// Gets the rectangle covering the LED /// - public SKRect AbsoluteRectangle + public SKRectI AbsoluteRectangle { get => _absoluteRectangle; private set => SetAndNotify(ref _absoluteRectangle, value); @@ -59,8 +59,8 @@ namespace Artemis.Core internal void CalculateRectangles() { - Rectangle = RgbLed.Boundary.ToSKRect(); - AbsoluteRectangle = RgbLed.AbsoluteBoundary.ToSKRect(); + Rectangle = RgbLed.Boundary.ToSKRectI(); + AbsoluteRectangle = RgbLed.AbsoluteBoundary.ToSKRectI(); } } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Modules/ProfileModule.cs b/src/Artemis.Core/Plugins/Modules/ProfileModule.cs index fb1a8ccad..b2f6536ba 100644 --- a/src/Artemis.Core/Plugins/Modules/ProfileModule.cs +++ b/src/Artemis.Core/Plugins/Modules/ProfileModule.cs @@ -177,7 +177,7 @@ namespace Artemis.Core.Modules lock (_lock) { // Render the profile - ActiveProfile?.Render(canvas, SKPoint.Empty); + ActiveProfile?.Render(canvas, SKPointI.Empty); } ProfileRendered(deltaTime, canvas, canvasInfo); diff --git a/src/Artemis.Core/RGB.NET/SKTexture.cs b/src/Artemis.Core/RGB.NET/SKTexture.cs index 77a540282..27a9f6d5b 100644 --- a/src/Artemis.Core/RGB.NET/SKTexture.cs +++ b/src/Artemis.Core/RGB.NET/SKTexture.cs @@ -1,4 +1,5 @@ using System; +using System.Buffers; using System.Runtime.InteropServices; using Artemis.Core.SkiaSharp; using RGB.NET.Core; @@ -12,25 +13,54 @@ namespace Artemis.Core /// public sealed class SKTexture : PixelTexture, IDisposable { + private readonly bool _isScaledDown; private readonly SKPixmap _pixelData; private readonly IntPtr _pixelDataPtr; #region Constructors - internal SKTexture(IManagedGraphicsContext? graphicsContext, int width, int height, float scale) : base(width, height, 4, new AverageByteSampler()) + internal SKTexture(IManagedGraphicsContext? graphicsContext, int width, int height, float scale) : base(width, height, DATA_PER_PIXEL, new AverageByteSampler()) { ImageInfo = new SKImageInfo(width, height); - Surface = graphicsContext == null - ? SKSurface.Create(ImageInfo) + Surface = graphicsContext == null + ? SKSurface.Create(ImageInfo) : SKSurface.Create(graphicsContext.GraphicsContext, true, ImageInfo); RenderScale = scale; - + _isScaledDown = Math.Abs(scale - 1) > 0.001; _pixelDataPtr = Marshal.AllocHGlobal(ImageInfo.BytesSize); _pixelData = new SKPixmap(ImageInfo, _pixelDataPtr, ImageInfo.RowBytes); } #endregion + private void ReleaseUnmanagedResources() + { + Marshal.FreeHGlobal(_pixelDataPtr); + } + + /// + ~SKTexture() + { + ReleaseUnmanagedResources(); + } + + /// + public void Dispose() + { + Surface.Dispose(); + _pixelData.Dispose(); + + ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + + #region Constants + + private const int STACK_ALLOC_LIMIT = 1024; + private const int DATA_PER_PIXEL = 4; + + #endregion + #region Methods /// @@ -53,6 +83,61 @@ namespace Artemis.Core return new(pixel[2], pixel[1], pixel[0]); } + /// + public override Color this[in Rectangle rectangle] + { + get + { + if (Data.Length == 0) return Color.Transparent; + + SKRectI skRectI = CreatedFlooredRectI( + Size.Width * rectangle.Location.X.Clamp(0, 1), + Size.Height * rectangle.Location.Y.Clamp(0, 1), + Size.Width * rectangle.Size.Width.Clamp(0, 1), + Size.Height * rectangle.Size.Height.Clamp(0, 1) + ); + + if (skRectI.Width == 0 || skRectI.Height == 0) return Color.Transparent; + if (skRectI.Width == 1 && skRectI.Height == 1) return GetColor(GetPixelData(skRectI.Left, skRectI.Top)); + + int bufferSize = skRectI.Width * skRectI.Height * DATA_PER_PIXEL; + if (bufferSize <= STACK_ALLOC_LIMIT) + { + Span buffer = stackalloc byte[bufferSize]; + GetRegionData(skRectI.Left, skRectI.Top, skRectI.Width, skRectI.Height, buffer); + + Span pixelData = stackalloc byte[DATA_PER_PIXEL]; + Sampler.SampleColor(new SamplerInfo(skRectI.Width, skRectI.Height, buffer), pixelData); + + return GetColor(pixelData); + } + else + { + byte[] rent = ArrayPool.Shared.Rent(bufferSize); + + Span buffer = new Span(rent).Slice(0, bufferSize); + GetRegionData(skRectI.Left, skRectI.Top, skRectI.Width, skRectI.Height, buffer); + + Span pixelData = stackalloc byte[DATA_PER_PIXEL]; + Sampler.SampleColor(new SamplerInfo(skRectI.Width, skRectI.Height, buffer), pixelData); + + ArrayPool.Shared.Return(rent); + + return GetColor(pixelData); + } + } + } + + private static SKRectI CreatedFlooredRectI(float x, float y, float width, float height) + { + return new( + width <= 0.0 ? checked((int) Math.Floor(x)) : checked((int) Math.Ceiling(x)), + height <= 0.0 ? checked((int) Math.Floor(y)) : checked((int) Math.Ceiling(y)), + width >= 0.0 ? checked((int) Math.Floor(x + width)) : checked((int) Math.Ceiling(x + width)), + height >= 0.0 ? checked((int) Math.Floor(y + height)) : checked((int) Math.Ceiling(y + height)) + ); + } + #endregion #region Properties & Fields @@ -84,30 +169,5 @@ namespace Artemis.Core public bool IsInvalid { get; private set; } #endregion - - #region IDisposable - - private void ReleaseUnmanagedResources() - { - Marshal.FreeHGlobal(_pixelDataPtr); - } - - /// - public void Dispose() - { - Surface.Dispose(); - _pixelData.Dispose(); - - ReleaseUnmanagedResources(); - GC.SuppressFinalize(this); - } - - /// - ~SKTexture() - { - ReleaseUnmanagedResources(); - } - - #endregion } } \ No newline at end of file diff --git a/src/Artemis.Core/Services/CoreService.cs b/src/Artemis.Core/Services/CoreService.cs index 17961a9e2..1a0f26a4a 100644 --- a/src/Artemis.Core/Services/CoreService.cs +++ b/src/Artemis.Core/Services/CoreService.cs @@ -144,16 +144,17 @@ namespace Artemis.Core.Services SKCanvas canvas = texture.Surface.Canvas; canvas.Save(); - canvas.Scale(texture.RenderScale); + if (Math.Abs(texture.RenderScale - 1) > 0.001) + 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.ImageInfo); } - + OnFrameRendering(new FrameRenderingEventArgs(canvas, args.DeltaTime, _rgbService.Surface)); canvas.RestoreToCount(-1); canvas.Flush(); diff --git a/src/Artemis.Core/Services/RgbService.cs b/src/Artemis.Core/Services/RgbService.cs index 40b502208..4f812c886 100644 --- a/src/Artemis.Core/Services/RgbService.cs +++ b/src/Artemis.Core/Services/RgbService.cs @@ -258,9 +258,17 @@ namespace Artemis.Core.Services else _logger.Debug("Creating SKTexture with software-based graphics context"); + float evenWidth = Surface.Boundary.Size.Width; + if (evenWidth % 2 != 0) + evenWidth++; + float evenHeight = Surface.Boundary.Size.Height; + if (evenHeight % 2 != 0) + evenHeight++; + 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()); + int width = Math.Max(1, MathF.Min(evenWidth * renderScale, 4096).RoundToInt()); + int height = Math.Max(1, MathF.Min(evenHeight * renderScale, 4096).RoundToInt()); + _texture?.Dispose(); _texture = new SKTexture(graphicsContext, width, height, renderScale); _textureBrush.Texture = _texture; diff --git a/src/Artemis.Core/Utilities/IntroAnimation.cs b/src/Artemis.Core/Utilities/IntroAnimation.cs index 06cd3d765..9bb5456e4 100644 --- a/src/Artemis.Core/Utilities/IntroAnimation.cs +++ b/src/Artemis.Core/Utilities/IntroAnimation.cs @@ -30,7 +30,7 @@ namespace Artemis.Core public void Render(double deltaTime, SKCanvas canvas) { AnimationProfile.Update(deltaTime); - AnimationProfile.Render(canvas, SKPoint.Empty); + AnimationProfile.Render(canvas, SKPointI.Empty); } private Profile CreateIntroProfile() diff --git a/src/Artemis.UI.Shared/Services/ProfileEditorService.cs b/src/Artemis.UI.Shared/Services/ProfileEditorService.cs index 720bf0ad3..79d71359b 100644 --- a/src/Artemis.UI.Shared/Services/ProfileEditorService.cs +++ b/src/Artemis.UI.Shared/Services/ProfileEditorService.cs @@ -11,6 +11,7 @@ using Artemis.UI.Shared.Services.Models; using Ninject; using Ninject.Parameters; using Serilog; +using SkiaSharp; using SkiaSharp.Views.WPF; using Stylet; @@ -350,7 +351,10 @@ namespace Artemis.UI.Shared.Services public List GetLedsInRectangle(Rect rect) { - return _rgbService.EnabledDevices.SelectMany(d => d.Leds).Where(led => led.AbsoluteRectangle.IntersectsWith(rect.ToSKRect())).ToList(); + return _rgbService.EnabledDevices + .SelectMany(d => d.Leds) + .Where(led => led.AbsoluteRectangle.IntersectsWith(SKRectI.Round(rect.ToSKRect()))) + .ToList(); } #region Copy/paste diff --git a/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabViewModel.cs index c8caa051e..2f8978ef4 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabViewModel.cs @@ -59,9 +59,12 @@ namespace Artemis.UI.Screens.Settings.Tabs.General LogLevels = new BindableCollection(EnumUtilities.GetAllValuesAndDescriptions(typeof(LogEventLevel))); ColorSchemes = new BindableCollection(EnumUtilities.GetAllValuesAndDescriptions(typeof(ApplicationColorScheme))); - RenderScales = new List> {new("10%", 0.1)}; - for (int i = 25; i <= 100; i += 25) - RenderScales.Add(new Tuple(i + "%", i / 100.0)); + RenderScales = new List> + { + new("25%", 0.25), + new("50%", 0.5), + new("100%", 1), + }; TargetFrameRates = new List>(); for (int i = 10; i <= 30; i += 5)