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 f2537a907..c6139f30c 100644
--- a/src/Artemis.Core/Extensions/RgbDeviceExtensions.cs
+++ b/src/Artemis.Core/Extensions/RgbDeviceExtensions.cs
@@ -31,5 +31,10 @@ namespace Artemis.Core
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)