diff --git a/src/Artemis.Core/Constants.cs b/src/Artemis.Core/Constants.cs
index 96e72a0f6..21c2ca973 100644
--- a/src/Artemis.Core/Constants.cs
+++ b/src/Artemis.Core/Constants.cs
@@ -2,7 +2,9 @@
using System.Collections.Generic;
using System.IO;
using Artemis.Core.JsonConverters;
+using Artemis.Core.Services;
using Artemis.Core.Services.Core;
+using Artemis.Core.SkiaSharp;
using Newtonsoft.Json;
namespace Artemis.Core
@@ -116,5 +118,11 @@ namespace Artemis.Core
typeof(double),
typeof(decimal)
};
+
+ ///
+ /// Gets the graphics context to be used for rendering by SkiaSharp. Can be set via
+ /// .
+ ///
+ public static IManagedGraphicsContext? ManagedGraphicsContext { get; internal set; }
}
}
\ No newline at end of file
diff --git a/src/Artemis.Core/Exceptions/ArtemisGraphicsContextException.cs b/src/Artemis.Core/Exceptions/ArtemisGraphicsContextException.cs
new file mode 100644
index 000000000..975dd8c04
--- /dev/null
+++ b/src/Artemis.Core/Exceptions/ArtemisGraphicsContextException.cs
@@ -0,0 +1,25 @@
+using System;
+
+namespace Artemis.Core
+{
+ ///
+ /// Represents SkiaSharp graphics-context related errors
+ ///
+ public class ArtemisGraphicsContextException : Exception
+ {
+ ///
+ public ArtemisGraphicsContextException()
+ {
+ }
+
+ ///
+ public ArtemisGraphicsContextException(string message) : base(message)
+ {
+ }
+
+ ///
+ public ArtemisGraphicsContextException(string message, Exception innerException) : base(message, innerException)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.Core/Extensions/SKPaintExtensions.cs b/src/Artemis.Core/Extensions/SKPaintExtensions.cs
new file mode 100644
index 000000000..ec2ebe6e2
--- /dev/null
+++ b/src/Artemis.Core/Extensions/SKPaintExtensions.cs
@@ -0,0 +1,16 @@
+using SkiaSharp;
+
+namespace Artemis.Core
+{
+ internal static class SKPaintExtensions
+ {
+ internal static void DisposeSelfAndProperties(this SKPaint paint)
+ {
+ paint.ImageFilter?.Dispose();
+ paint.ColorFilter?.Dispose();
+ paint.MaskFilter?.Dispose();
+ paint.Shader?.Dispose();
+ paint.Dispose();
+ }
+ }
+}
\ 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 366d146b9..1d273c71b 100644
--- a/src/Artemis.Core/Models/Profile/Folder.cs
+++ b/src/Artemis.Core/Models/Profile/Folder.cs
@@ -168,7 +168,7 @@ namespace Artemis.Core
#region Rendering
///
- public override void Render(SKCanvas canvas)
+ public override void Render(SKCanvas canvas, SKPoint basePosition)
{
if (Disposed)
throw new ObjectDisposedException("Folder");
@@ -189,41 +189,38 @@ namespace Artemis.Core
baseLayerEffect.Update(Timeline.Delta.TotalSeconds);
}
+ SKPaint layerPaint = new();
try
{
- canvas.Save();
- Renderer.Open(Path, Parent as Folder);
- if (Renderer.Canvas == null || Renderer.Path == null || Renderer.Paint == null)
- throw new ArtemisCoreException("Failed to open folder render context");
-
- SKRect rendererBounds = Renderer.Path.Bounds;
+ SKRect rendererBounds = SKRect.Create(0, 0, Path.Bounds.Width, Path.Bounds.Height);
foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => e.Enabled))
- baseLayerEffect.PreProcess(Renderer.Canvas, rendererBounds, Renderer.Paint);
+ baseLayerEffect.PreProcess(canvas, rendererBounds, layerPaint);
+
+ canvas.SaveLayer(layerPaint);
+ canvas.Translate(Path.Bounds.Left - basePosition.X, Path.Bounds.Top - basePosition.Y);
// If required, apply the opacity override of the module to the root folder
if (IsRootFolder && Profile.Module.OpacityOverride < 1)
{
double multiplier = Easings.SineEaseInOut(Profile.Module.OpacityOverride);
- Renderer.Paint.Color = Renderer.Paint.Color.WithAlpha((byte) (Renderer.Paint.Color.Alpha * multiplier));
+ layerPaint.Color = layerPaint.Color.WithAlpha((byte) (layerPaint.Color.Alpha * multiplier));
}
// No point rendering if the alpha was set to zero by one of the effects
- if (Renderer.Paint.Color.Alpha == 0)
+ if (layerPaint.Color.Alpha == 0)
return;
// 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(Renderer.Canvas);
+ Children[index].Render(canvas, new SKPoint(Path.Bounds.Left, Path.Bounds.Top));
foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => e.Enabled))
- baseLayerEffect.PostProcess(Renderer.Canvas, rendererBounds, Renderer.Paint);
-
- canvas.DrawBitmap(Renderer.Bitmap, Renderer.TargetLocation, Renderer.Paint);
+ baseLayerEffect.PostProcess(canvas, rendererBounds, layerPaint);
}
finally
{
canvas.Restore();
- Renderer.Close();
+ layerPaint.DisposeSelfAndProperties();
}
Timeline.ClearDelta();
@@ -239,7 +236,6 @@ namespace Artemis.Core
foreach (ProfileElement profileElement in Children)
profileElement.Dispose();
- Renderer.Dispose();
base.Dispose(disposing);
}
diff --git a/src/Artemis.Core/Models/Profile/Layer.cs b/src/Artemis.Core/Models/Profile/Layer.cs
index 31d8d8ae4..3feac187a 100644
--- a/src/Artemis.Core/Models/Profile/Layer.cs
+++ b/src/Artemis.Core/Models/Profile/Layer.cs
@@ -154,7 +154,6 @@ namespace Artemis.Core
_layerBrush?.Dispose();
_general.Dispose();
_transform.Dispose();
- Renderer.Dispose();
base.Dispose(disposing);
}
@@ -183,7 +182,7 @@ namespace Artemis.Core
General.ShapeType.CurrentValueSet += ShapeTypeOnCurrentValueSet;
ApplyShapeType();
ActivateLayerBrush();
-
+
Reset();
}
@@ -278,7 +277,7 @@ namespace Artemis.Core
}
///
- public override void Render(SKCanvas canvas)
+ public override void Render(SKCanvas canvas, SKPoint basePosition)
{
if (Disposed)
throw new ObjectDisposedException("Layer");
@@ -290,9 +289,9 @@ namespace Artemis.Core
if (LayerBrush == null || LayerBrush?.BaseProperties?.PropertiesInitialized == false)
return;
- RenderTimeline(Timeline, canvas);
+ RenderTimeline(Timeline, canvas, basePosition);
foreach (Timeline extraTimeline in Timeline.ExtraTimelines.ToList())
- RenderTimeline(extraTimeline, canvas);
+ RenderTimeline(extraTimeline, canvas, basePosition);
Timeline.ClearDelta();
}
@@ -313,36 +312,40 @@ namespace Artemis.Core
}
}
- private void RenderTimeline(Timeline timeline, SKCanvas canvas)
+ private void RenderTimeline(Timeline timeline, SKCanvas canvas, SKPoint basePosition)
{
if (Path == null || LayerBrush == null)
throw new ArtemisCoreException("The layer is not yet ready for rendering");
- if (timeline.IsFinished)
+ if (!Leds.Any() || timeline.IsFinished)
return;
ApplyTimeline(timeline);
-
+
if (LayerBrush?.BrushType != LayerBrushType.Regular)
return;
+ SKPaint layerPaint = new();
try
{
canvas.Save();
- Renderer.Open(Path, Parent as Folder);
+ canvas.Translate(Path.Bounds.Left - basePosition.X, Path.Bounds.Top - basePosition.Y);
+ using SKPath clipPath = new(Path);
+ clipPath.Transform(SKMatrix.CreateTranslation(Path.Bounds.Left * -1, Path.Bounds.Top * -1));
+ canvas.ClipPath(clipPath);
- if (Renderer.Canvas == null || Renderer.Path == null || Renderer.Paint == null)
- throw new ArtemisCoreException("Failed to open layer render context");
+ SKRect layerBounds = SKRect.Create(0, 0, Path.Bounds.Width, Path.Bounds.Height);
// Apply blend mode and color
- Renderer.Paint.BlendMode = General.BlendMode.CurrentValue;
- Renderer.Paint.Color = new SKColor(0, 0, 0, (byte) (Transform.Opacity.CurrentValue * 2.55f));
+ layerPaint.BlendMode = General.BlendMode.CurrentValue;
+ layerPaint.Color = new SKColor(0, 0, 0, (byte) (Transform.Opacity.CurrentValue * 2.55f));
using SKPath renderPath = new();
+
if (General.ShapeType.CurrentValue == LayerShapeType.Rectangle)
- renderPath.AddRect(Renderer.Path.Bounds);
+ renderPath.AddRect(layerBounds);
else
- renderPath.AddOval(Renderer.Path.Bounds);
+ renderPath.AddOval(layerBounds);
if (General.TransformMode.CurrentValue == LayerTransformMode.Normal)
{
@@ -357,54 +360,50 @@ namespace Artemis.Core
if (LayerBrush.SupportsTransformation)
{
SKMatrix rotationMatrix = GetTransformMatrix(true, false, false, true);
- Renderer.Canvas.SetMatrix(Renderer.Canvas.TotalMatrix.PreConcat(rotationMatrix));
+ canvas.SetMatrix(canvas.TotalMatrix.PreConcat(rotationMatrix));
}
- // If a brush is a bad boy and tries to color outside the lines, ensure that its clipped off
- Renderer.Canvas.ClipPath(renderPath);
- DelegateRendering(renderPath.Bounds);
+ DelegateRendering(canvas, renderPath, renderPath.Bounds, layerPaint);
}
else if (General.TransformMode.CurrentValue == LayerTransformMode.Clip)
{
SKMatrix renderPathMatrix = GetTransformMatrix(true, true, true, true);
renderPath.Transform(renderPathMatrix);
- // If a brush is a bad boy and tries to color outside the lines, ensure that its clipped off
- Renderer.Canvas.ClipPath(renderPath);
- DelegateRendering(Renderer.Path.Bounds);
+ DelegateRendering(canvas, renderPath, layerBounds, layerPaint);
}
-
- canvas.DrawBitmap(Renderer.Bitmap, Renderer.TargetLocation, Renderer.Paint);
}
finally
{
- try
- {
- canvas.Restore();
- }
- catch
- {
- // ignored
- }
-
- Renderer.Close();
+ canvas.Restore();
+ layerPaint.DisposeSelfAndProperties();
}
}
- private void DelegateRendering(SKRect bounds)
+ private void DelegateRendering(SKCanvas canvas, SKPath renderPath, SKRect bounds, SKPaint layerPaint)
{
if (LayerBrush == null)
throw new ArtemisCoreException("The layer is not yet ready for rendering");
- if (Renderer.Canvas == null || Renderer.Paint == null)
- throw new ArtemisCoreException("Failed to open layer render context");
foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => e.Enabled))
- baseLayerEffect.PreProcess(Renderer.Canvas, bounds, Renderer.Paint);
+ baseLayerEffect.PreProcess(canvas, bounds, layerPaint);
- LayerBrush.InternalRender(Renderer.Canvas, bounds, Renderer.Paint);
+ try
+ {
+ canvas.SaveLayer(layerPaint);
- foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => e.Enabled))
- baseLayerEffect.PostProcess(Renderer.Canvas, bounds, Renderer.Paint);
+ // If a brush is a bad boy and tries to color outside the lines, ensure that its clipped off
+ canvas.ClipPath(renderPath);
+ LayerBrush.InternalRender(canvas, bounds, layerPaint);
+
+ foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => e.Enabled))
+ baseLayerEffect.PostProcess(canvas, bounds, layerPaint);
+ }
+
+ finally
+ {
+ canvas.Restore();
+ }
}
internal void CalculateRenderProperties()
diff --git a/src/Artemis.Core/Models/Profile/Profile.cs b/src/Artemis.Core/Models/Profile/Profile.cs
index 1432d3157..96616f104 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)
+ public override void Render(SKCanvas canvas, SKPoint basePosition)
{
lock (_lock)
{
@@ -91,7 +91,7 @@ namespace Artemis.Core
throw new ArtemisCoreException($"Cannot render inactive profile: {this}");
foreach (ProfileElement profileElement in Children)
- profileElement.Render(canvas);
+ profileElement.Render(canvas, basePosition);
}
}
diff --git a/src/Artemis.Core/Models/Profile/ProfileElement.cs b/src/Artemis.Core/Models/Profile/ProfileElement.cs
index 086a27886..1072e4a94 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);
+ public abstract void Render(SKCanvas canvas, SKPoint 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 7af947574..899a01ff9 100644
--- a/src/Artemis.Core/Models/Profile/RenderProfileElement.cs
+++ b/src/Artemis.Core/Models/Profile/RenderProfileElement.cs
@@ -19,7 +19,6 @@ namespace Artemis.Core
internal RenderProfileElement(Profile profile) : base(profile)
{
Timeline = new Timeline();
- Renderer = new Renderer();
ExpandedPropertyGroups = new List();
LayerEffectsList = new List();
@@ -127,7 +126,6 @@ namespace Artemis.Core
{
base.Parent = value;
OnPropertyChanged(nameof(Parent));
- Renderer.Invalidate();
}
}
@@ -144,7 +142,6 @@ namespace Artemis.Core
// 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;
- Renderer.Invalidate();
}
}
@@ -157,7 +154,6 @@ namespace Artemis.Core
private set => SetAndNotify(ref _bounds, value);
}
- internal Renderer Renderer { get; }
#region Property group expansion
diff --git a/src/Artemis.Core/Models/Profile/Renderer.cs b/src/Artemis.Core/Models/Profile/Renderer.cs
index 7e502b277..4de0fa2d5 100644
--- a/src/Artemis.Core/Models/Profile/Renderer.cs
+++ b/src/Artemis.Core/Models/Profile/Renderer.cs
@@ -9,8 +9,8 @@ namespace Artemis.Core
private bool _disposed;
private SKRect _lastBounds;
private SKRect _lastParentBounds;
- public SKBitmap? Bitmap { get; private set; }
- public SKCanvas? Canvas { get; private set; }
+ private GRContext? _lastGraphicsContext;
+ public SKSurface? Surface { get; private set; }
public SKPaint? Paint { get; private set; }
public SKPath? Path { get; private set; }
public SKPoint TargetLocation { get; private set; }
@@ -28,35 +28,40 @@ namespace Artemis.Core
if (IsOpen)
throw new ArtemisCoreException("Cannot open render context because it is already open");
- if (path.Bounds != _lastBounds || (parent != null && parent.Bounds != _lastParentBounds))
+ if (path.Bounds != _lastBounds || (parent != null && parent.Bounds != _lastParentBounds) || _lastGraphicsContext != Constants.ManagedGraphicsContext?.GraphicsContext)
Invalidate();
- if (!_valid || Canvas == null)
+ if (!_valid || Surface == null)
{
SKRect pathBounds = path.Bounds;
int width = (int) pathBounds.Width;
int height = (int) pathBounds.Height;
- Bitmap = new SKBitmap(width, height);
+ SKImageInfo imageInfo = new(width, height);
+ if (Constants.ManagedGraphicsContext?.GraphicsContext == null)
+ Surface = SKSurface.Create(imageInfo);
+ else
+ Surface = SKSurface.Create(Constants.ManagedGraphicsContext.GraphicsContext, true, imageInfo);
+
Path = new SKPath(path);
- Canvas = new SKCanvas(Bitmap);
Path.Transform(SKMatrix.CreateTranslation(pathBounds.Left * -1, pathBounds.Top * -1));
TargetLocation = new SKPoint(pathBounds.Location.X, pathBounds.Location.Y);
if (parent != null)
TargetLocation -= parent.Bounds.Location;
- Canvas.ClipPath(Path);
+ Surface.Canvas.ClipPath(Path);
_lastParentBounds = parent?.Bounds ?? new SKRect();
_lastBounds = path.Bounds;
+ _lastGraphicsContext = Constants.ManagedGraphicsContext?.GraphicsContext;
_valid = true;
}
Paint = new SKPaint();
- Canvas.Clear();
- Canvas.Save();
+ Surface.Canvas.Clear();
+ Surface.Canvas.Save();
IsOpen = true;
}
@@ -66,8 +71,15 @@ namespace Artemis.Core
if (_disposed)
throw new ObjectDisposedException("Renderer");
- Canvas?.Restore();
+ Surface?.Canvas.Restore();
+
+ // Looks like every part of the paint needs to be disposed :(
+ Paint?.ColorFilter?.Dispose();
+ Paint?.ImageFilter?.Dispose();
+ Paint?.MaskFilter?.Dispose();
+ Paint?.PathEffect?.Dispose();
Paint?.Dispose();
+
Paint = null;
IsOpen = false;
@@ -86,17 +98,21 @@ namespace Artemis.Core
if (IsOpen)
Close();
- Canvas?.Dispose();
+ Surface?.Dispose();
Paint?.Dispose();
Path?.Dispose();
- Bitmap?.Dispose();
- Canvas = null;
+ Surface = null;
Paint = null;
Path = null;
- Bitmap = null;
_disposed = true;
}
+
+ ~Renderer()
+ {
+ if (IsOpen)
+ Close();
+ }
}
}
\ No newline at end of file
diff --git a/src/Artemis.Core/Plugins/Modules/Module.cs b/src/Artemis.Core/Plugins/Modules/Module.cs
index 70d4e5bac..8224cc826 100644
--- a/src/Artemis.Core/Plugins/Modules/Module.cs
+++ b/src/Artemis.Core/Plugins/Modules/Module.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
using Artemis.Core.DataModelExpansions;
using Artemis.Storage.Entities.Module;
@@ -186,13 +187,17 @@ namespace Artemis.Core.Modules
internal virtual void InternalUpdate(double deltaTime)
{
+ StartUpdateMeasure();
if (IsUpdateAllowed)
Update(deltaTime);
+ StopUpdateMeasure();
}
internal virtual void InternalRender(double deltaTime, SKCanvas canvas, SKImageInfo canvasInfo)
{
+ StartRenderMeasure();
Render(deltaTime, canvas, canvasInfo);
+ StopRenderMeasure();
}
internal virtual void Activate(bool isOverride)
diff --git a/src/Artemis.Core/Plugins/Modules/ProfileModule.cs b/src/Artemis.Core/Plugins/Modules/ProfileModule.cs
index 94fbd3125..fb1a8ccad 100644
--- a/src/Artemis.Core/Plugins/Modules/ProfileModule.cs
+++ b/src/Artemis.Core/Plugins/Modules/ProfileModule.cs
@@ -150,6 +150,7 @@ namespace Artemis.Core.Modules
internal override void InternalUpdate(double deltaTime)
{
+ StartUpdateMeasure();
if (IsUpdateAllowed)
Update(deltaTime);
@@ -165,19 +166,22 @@ namespace Artemis.Core.Modules
}
ProfileUpdated(deltaTime);
+ StopUpdateMeasure();
}
internal override void InternalRender(double deltaTime, SKCanvas canvas, SKImageInfo canvasInfo)
{
+ StartRenderMeasure();
Render(deltaTime, canvas, canvasInfo);
lock (_lock)
{
// Render the profile
- ActiveProfile?.Render(canvas);
+ ActiveProfile?.Render(canvas, SKPoint.Empty);
}
ProfileRendered(deltaTime, canvas, canvasInfo);
+ StopRenderMeasure();
}
internal async Task ChangeActiveProfileAnimated(Profile? profile, IEnumerable devices)
diff --git a/src/Artemis.Core/Plugins/PluginFeature.cs b/src/Artemis.Core/Plugins/PluginFeature.cs
index f2fb01fad..8b9943a0a 100644
--- a/src/Artemis.Core/Plugins/PluginFeature.cs
+++ b/src/Artemis.Core/Plugins/PluginFeature.cs
@@ -1,4 +1,5 @@
using System;
+using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using Artemis.Storage.Entities.Plugins;
@@ -10,9 +11,11 @@ namespace Artemis.Core
///
public abstract class PluginFeature : CorePropertyChanged, IDisposable
{
+ private readonly Stopwatch _renderStopwatch = new();
+ private readonly Stopwatch _updateStopwatch = new();
private bool _isEnabled;
private Exception? _loadException;
-
+
///
/// Gets the plugin feature info related to this feature
///
@@ -46,6 +49,16 @@ namespace Artemis.Core
///
public string Id => $"{GetType().FullName}-{Plugin.Guid.ToString().Substring(0, 8)}"; // Not as unique as a GUID but good enough and stays readable
+ ///
+ /// Gets the last measured update time of the feature
+ ///
+ public TimeSpan UpdateTime { get; private set; }
+
+ ///
+ /// Gets the last measured render time of the feature
+ ///
+ public TimeSpan RenderTime { get; private set; }
+
internal PluginFeatureEntity Entity { get; set; } = null!; // Will be set right after construction
///
@@ -58,6 +71,66 @@ namespace Artemis.Core
///
public abstract void Disable();
+ ///
+ /// Occurs when the feature is enabled
+ ///
+ public event EventHandler? Enabled;
+
+ ///
+ /// Occurs when the feature is disabled
+ ///
+ public event EventHandler? Disabled;
+
+ ///
+ /// Releases the unmanaged resources used by the plugin feature and optionally releases the managed resources.
+ ///
+ ///
+ /// to release both managed and unmanaged resources;
+ /// to release only unmanaged resources.
+ ///
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing) InternalDisable();
+ }
+
+ ///
+ /// Triggers the Enabled event
+ ///
+ protected virtual void OnEnabled()
+ {
+ Enabled?.Invoke(this, EventArgs.Empty);
+ }
+
+ ///
+ /// Triggers the Disabled event
+ ///
+ protected virtual void OnDisabled()
+ {
+ Disabled?.Invoke(this, EventArgs.Empty);
+ }
+
+ internal void StartUpdateMeasure()
+ {
+ _updateStopwatch.Start();
+ }
+
+ internal void StopUpdateMeasure()
+ {
+ UpdateTime = _updateStopwatch.Elapsed;
+ _updateStopwatch.Reset();
+ }
+
+ internal void StartRenderMeasure()
+ {
+ _renderStopwatch.Start();
+ }
+
+ internal void StopRenderMeasure()
+ {
+ RenderTime = _renderStopwatch.Elapsed;
+ _renderStopwatch.Reset();
+ }
+
internal void SetEnabled(bool enable, bool isAutoEnable = false)
{
if (enable == IsEnabled)
@@ -133,25 +206,6 @@ namespace Artemis.Core
Disable();
}
- #region IDisposable
-
- ///
- /// Releases the unmanaged resources used by the plugin feature and optionally releases the managed resources.
- ///
- ///
- /// to release both managed and unmanaged resources;
- /// to release only unmanaged resources.
- ///
- protected virtual void Dispose(bool disposing)
- {
- if (disposing)
- {
- InternalDisable();
- }
- }
-
- #endregion
-
///
public void Dispose()
{
@@ -187,35 +241,5 @@ namespace Artemis.Core
}
#endregion
-
- #region Events
-
- ///
- /// Occurs when the feature is enabled
- ///
- public event EventHandler? Enabled;
-
- ///
- /// Occurs when the feature is disabled
- ///
- public event EventHandler? Disabled;
-
- ///
- /// Triggers the Enabled event
- ///
- protected virtual void OnEnabled()
- {
- Enabled?.Invoke(this, EventArgs.Empty);
- }
-
- ///
- /// Triggers the Disabled event
- ///
- protected virtual void OnDisabled()
- {
- Disabled?.Invoke(this, EventArgs.Empty);
- }
-
- #endregion
}
}
\ No newline at end of file
diff --git a/src/Artemis.Core/RGB.NET/SKTexture.cs b/src/Artemis.Core/RGB.NET/SKTexture.cs
index 87e2c8cfb..77a540282 100644
--- a/src/Artemis.Core/RGB.NET/SKTexture.cs
+++ b/src/Artemis.Core/RGB.NET/SKTexture.cs
@@ -1,4 +1,6 @@
using System;
+using System.Runtime.InteropServices;
+using Artemis.Core.SkiaSharp;
using RGB.NET.Core;
using RGB.NET.Presets.Textures.Sampler;
using SkiaSharp;
@@ -10,25 +12,45 @@ namespace Artemis.Core
///
public sealed class SKTexture : PixelTexture, IDisposable
{
- private bool _disposed;
+ private readonly SKPixmap _pixelData;
+ private readonly IntPtr _pixelDataPtr;
#region Constructors
- internal SKTexture(int width, int height, float renderScale)
- : base(width, height, 4, new AverageByteSampler())
+ internal SKTexture(IManagedGraphicsContext? graphicsContext, int width, int height, float scale) : base(width, height, 4, new AverageByteSampler())
{
- Bitmap = new SKBitmap(new SKImageInfo(width, height, SKColorType.Rgb888x));
- RenderScale = renderScale;
+ ImageInfo = new SKImageInfo(width, height);
+ Surface = graphicsContext == null
+ ? SKSurface.Create(ImageInfo)
+ : SKSurface.Create(graphicsContext.GraphicsContext, true, ImageInfo);
+ RenderScale = scale;
+
+ _pixelDataPtr = Marshal.AllocHGlobal(ImageInfo.BytesSize);
+ _pixelData = new SKPixmap(ImageInfo, _pixelDataPtr, ImageInfo.RowBytes);
}
#endregion
#region Methods
+ ///
+ /// Invalidates the texture
+ ///
+ public void Invalidate()
+ {
+ IsInvalid = true;
+ }
+
+ internal void CopyPixelData()
+ {
+ using SKImage skImage = Surface.Snapshot();
+ skImage.ReadPixels(_pixelData);
+ }
+
///
protected override Color GetColor(in ReadOnlySpan pixel)
{
- return new(pixel[0], pixel[1], pixel[2]);
+ return new(pixel[2], pixel[1], pixel[0]);
}
#endregion
@@ -38,12 +60,17 @@ namespace Artemis.Core
///
/// Gets the SKBitmap backing this texture
///
- public SKBitmap Bitmap { get; }
+ public SKSurface Surface { get; }
+
+ ///
+ /// Gets the image info used to create the
+ ///
+ public SKImageInfo ImageInfo { get; }
///
/// Gets the color data in RGB format
///
- protected override ReadOnlySpan Data => _disposed ? new ReadOnlySpan() : Bitmap.GetPixelSpan();
+ protected override ReadOnlySpan Data => _pixelData.GetPixelSpan();
///
/// Gets the render scale of the texture
@@ -56,19 +83,29 @@ namespace Artemis.Core
///
public bool IsInvalid { get; private set; }
- ///
- /// Invalidates the texture
- ///
- public void Invalidate()
+ #endregion
+
+ #region IDisposable
+
+ private void ReleaseUnmanagedResources()
{
- IsInvalid = true;
+ Marshal.FreeHGlobal(_pixelDataPtr);
}
-
+
///
public void Dispose()
{
- _disposed = true;
- Bitmap.Dispose();
+ Surface.Dispose();
+ _pixelData.Dispose();
+
+ ReleaseUnmanagedResources();
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ ~SKTexture()
+ {
+ ReleaseUnmanagedResources();
}
#endregion
diff --git a/src/Artemis.Core/Services/CoreService.cs b/src/Artemis.Core/Services/CoreService.cs
index 239aaa567..17961a9e2 100644
--- a/src/Artemis.Core/Services/CoreService.cs
+++ b/src/Artemis.Core/Services/CoreService.cs
@@ -59,6 +59,7 @@ namespace Artemis.Core.Services
UpdatePluginCache();
+ _rgbService.IsRenderPaused = true;
_rgbService.Surface.Updating += SurfaceOnUpdating;
_loggingLevel.SettingChanged += (sender, args) => ApplyLoggingLevel();
@@ -139,21 +140,23 @@ namespace Artemis.Core.Services
module.InternalUpdate(args.DeltaTime);
// Render all active modules
- SKTexture texture =_rgbService.OpenRender();
+ SKTexture texture = _rgbService.OpenRender();
- using (SKCanvas canvas = new(texture.Bitmap))
+ SKCanvas canvas = texture.Surface.Canvas;
+ canvas.Save();
+ 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)
{
- 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));
+ 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();
OnFrameRendered(new FrameRenderedEventArgs(texture, _rgbService.Surface));
}
@@ -163,9 +166,9 @@ namespace Artemis.Core.Services
}
finally
{
+ _rgbService.CloseRender();
_frameStopWatch.Stop();
FrameTime = _frameStopWatch.Elapsed;
- _rgbService.CloseRender();
LogUpdateExceptions();
}
@@ -240,6 +243,7 @@ namespace Artemis.Core.Services
IsElevated
);
+ _rgbService.IsRenderPaused = false;
OnInitialized();
}
diff --git a/src/Artemis.Core/Services/Interfaces/IRgbService.cs b/src/Artemis.Core/Services/Interfaces/IRgbService.cs
index 099b4bb3f..00c5f85d0 100644
--- a/src/Artemis.Core/Services/Interfaces/IRgbService.cs
+++ b/src/Artemis.Core/Services/Interfaces/IRgbService.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using Artemis.Core.SkiaSharp;
using RGB.NET.Core;
namespace Artemis.Core.Services
@@ -50,6 +51,16 @@ namespace Artemis.Core.Services
///
void CloseRender();
+ ///
+ /// Updates the graphics context to the provided .
+ /// Note: The old graphics context will be used until the next frame starts rendering and is disposed afterwards.
+ ///
+ ///
+ /// The new managed graphics context. If , software rendering
+ /// is used.
+ ///
+ void UpdateGraphicsContext(IManagedGraphicsContext? managedGraphicsContext);
+
///
/// Adds the given device provider to the
///
diff --git a/src/Artemis.Core/Services/RgbService.cs b/src/Artemis.Core/Services/RgbService.cs
index ed226a331..40b502208 100644
--- a/src/Artemis.Core/Services/RgbService.cs
+++ b/src/Artemis.Core/Services/RgbService.cs
@@ -4,11 +4,11 @@ using System.Collections.ObjectModel;
using System.Linq;
using Artemis.Core.DeviceProviders;
using Artemis.Core.Services.Models;
+using Artemis.Core.SkiaSharp;
using Artemis.Storage.Entities.Surface;
using Artemis.Storage.Repositories.Interfaces;
using RGB.NET.Core;
using Serilog;
-using SkiaSharp;
namespace Artemis.Core.Services
{
@@ -51,6 +51,8 @@ namespace Artemis.Core.Services
UpdateTrigger = new TimerUpdateTrigger {UpdateFrequency = 1.0 / _targetFrameRateSetting.Value};
Surface.RegisterUpdateTrigger(UpdateTrigger);
+
+ Utilities.ShutdownRequested += UtilitiesOnShutdownRequested;
}
public TimerUpdateTrigger UpdateTrigger { get; }
@@ -66,6 +68,11 @@ namespace Artemis.Core.Services
_texture?.Invalidate();
}
+ private void UtilitiesOnShutdownRequested(object? sender, EventArgs e)
+ {
+ IsRenderPaused = true;
+ }
+
private void SurfaceOnLayoutChanged(SurfaceLayoutChangedEventArgs args)
{
UpdateLedGroup();
@@ -115,13 +122,16 @@ namespace Artemis.Core.Services
DeviceAdded?.Invoke(this, e);
}
+ private void RenderScaleSettingOnSettingChanged(object? sender, EventArgs e)
+ {
+ _texture?.Invalidate();
+ }
+
public IReadOnlyCollection EnabledDevices => _enabledDevices.AsReadOnly();
public IReadOnlyCollection Devices => _devices.AsReadOnly();
public IReadOnlyDictionary LedMap => new ReadOnlyDictionary(_ledMap);
- ///
public RGBSurface Surface { get; set; }
-
public bool IsRenderPaused { get; set; }
public bool RenderOpen { get; private set; }
@@ -197,11 +207,6 @@ namespace Artemis.Core.Services
}
}
- private void RenderScaleSettingOnSettingChanged(object? sender, EventArgs e)
- {
- _texture?.Invalidate();
- }
-
public void Dispose()
{
Surface.UnregisterUpdateTrigger(UpdateTrigger);
@@ -216,6 +221,8 @@ namespace Artemis.Core.Services
#region Rendering
+ private IManagedGraphicsContext? _newGraphicsContext;
+
public SKTexture OpenRender()
{
if (RenderOpen)
@@ -234,6 +241,7 @@ namespace Artemis.Core.Services
throw new ArtemisCoreException("Render pipeline is already closed");
RenderOpen = false;
+ _texture?.CopyPixelData();
}
public void CreateTexture()
@@ -241,15 +249,38 @@ namespace Artemis.Core.Services
if (RenderOpen)
throw new ArtemisCoreException("Cannot update the texture while rendering");
- SKTexture? oldTexture = _texture;
+ IManagedGraphicsContext? graphicsContext = Constants.ManagedGraphicsContext = _newGraphicsContext;
+ if (!ReferenceEquals(graphicsContext, _newGraphicsContext))
+ graphicsContext = _newGraphicsContext;
+
+ if (graphicsContext != null)
+ _logger.Debug("Creating SKTexture with graphics context {graphicsContext}", graphicsContext.GetType().Name);
+ else
+ _logger.Debug("Creating SKTexture with software-based graphics context");
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);
+ _texture?.Dispose();
+ _texture = new SKTexture(graphicsContext, width, height, renderScale);
_textureBrush.Texture = _texture;
- oldTexture?.Dispose();
+
+ if (!ReferenceEquals(_newGraphicsContext, Constants.ManagedGraphicsContext = _newGraphicsContext))
+ {
+ Constants.ManagedGraphicsContext?.Dispose();
+ Constants.ManagedGraphicsContext = _newGraphicsContext;
+ _newGraphicsContext = null;
+ }
+ }
+
+ public void UpdateGraphicsContext(IManagedGraphicsContext? managedGraphicsContext)
+ {
+ if (ReferenceEquals(managedGraphicsContext, Constants.ManagedGraphicsContext))
+ return;
+
+ _newGraphicsContext = managedGraphicsContext;
+ _texture?.Invalidate();
}
#endregion
diff --git a/src/Artemis.Core/SkiaSharp/IManagedGraphicsContext.cs b/src/Artemis.Core/SkiaSharp/IManagedGraphicsContext.cs
new file mode 100644
index 000000000..d2e7ff03b
--- /dev/null
+++ b/src/Artemis.Core/SkiaSharp/IManagedGraphicsContext.cs
@@ -0,0 +1,16 @@
+using System;
+using SkiaSharp;
+
+namespace Artemis.Core.SkiaSharp
+{
+ ///
+ /// Represents a managed wrapper around a SkiaSharp context
+ ///
+ public interface IManagedGraphicsContext : IDisposable
+ {
+ ///
+ /// Gets the graphics context created by this wrapper
+ ///
+ GRContext GraphicsContext { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.Core/Utilities/IntroAnimation.cs b/src/Artemis.Core/Utilities/IntroAnimation.cs
index 4b6d36ffd..06cd3d765 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);
+ AnimationProfile.Render(canvas, SKPoint.Empty);
}
private Profile CreateIntroProfile()
diff --git a/src/Artemis.UI/Artemis.UI.csproj b/src/Artemis.UI/Artemis.UI.csproj
index 127c6a5c9..7f06db7c9 100644
--- a/src/Artemis.UI/Artemis.UI.csproj
+++ b/src/Artemis.UI/Artemis.UI.csproj
@@ -149,6 +149,7 @@
+
diff --git a/src/Artemis.UI/Bootstrapper.cs b/src/Artemis.UI/Bootstrapper.cs
index 585b1b1a4..888851231 100644
--- a/src/Artemis.UI/Bootstrapper.cs
+++ b/src/Artemis.UI/Bootstrapper.cs
@@ -6,6 +6,7 @@ using System.Threading.Tasks;
using System.Windows;
using System.Windows.Markup;
using System.Windows.Threading;
+using Artemis.Core;
using Artemis.Core.Ninject;
using Artemis.Core.Services;
using Artemis.UI.Ninject;
@@ -13,9 +14,11 @@ using Artemis.UI.Screens;
using Artemis.UI.Services;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
+using Artemis.UI.SkiaSharp;
using Artemis.UI.Stylet;
using Ninject;
using Serilog;
+using SkiaSharp;
using Stylet;
namespace Artemis.UI
@@ -65,7 +68,7 @@ namespace Artemis.UI
FrameworkElement.LanguageProperty.OverrideMetadata(typeof(FrameworkElement), new FrameworkPropertyMetadata(XmlLanguage.GetLanguage(CultureInfo.CurrentCulture.IetfLanguageTag)));
// Create and bind the root view, this is a tray icon so don't show it with the window manager
- Execute.OnUIThread(() =>
+ Execute.OnUIThreadSync(() =>
{
UIElement view = viewManager.CreateAndBindViewForModelIfNecessary(RootViewModel);
((TrayViewModel) RootViewModel).SetTaskbarIcon(view);
@@ -90,7 +93,12 @@ namespace Artemis.UI
IRegistrationService registrationService = Kernel.Get();
registrationService.RegisterInputProvider();
registrationService.RegisterControllers();
-
+
+ Execute.OnUIThreadSync(() =>
+ {
+ registrationService.ApplyPreferredGraphicsContext();
+ });
+
// Initialize background services
Kernel.Get();
}
diff --git a/src/Artemis.UI/Exceptions/ArtemisGraphicsContextException.cs b/src/Artemis.UI/Exceptions/ArtemisGraphicsContextException.cs
new file mode 100644
index 000000000..653d65ef4
--- /dev/null
+++ b/src/Artemis.UI/Exceptions/ArtemisGraphicsContextException.cs
@@ -0,0 +1,22 @@
+using System;
+
+namespace Artemis.UI.Exceptions
+{
+ public class ArtemisGraphicsContextException : Exception
+ {
+ ///
+ public ArtemisGraphicsContextException()
+ {
+ }
+
+ ///
+ public ArtemisGraphicsContextException(string message) : base(message)
+ {
+ }
+
+ ///
+ public ArtemisGraphicsContextException(string message, Exception innerException) : base(message, innerException)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Properties/launchSettings.json b/src/Artemis.UI/Properties/launchSettings.json
index be8d62f45..1268340dc 100644
--- a/src/Artemis.UI/Properties/launchSettings.json
+++ b/src/Artemis.UI/Properties/launchSettings.json
@@ -2,7 +2,7 @@
"profiles": {
"Artemis.UI": {
"commandName": "Project",
- "commandLineArgs": "--force-elevation"
+ "commandLineArgs": "--force-elevation --pcmr"
}
}
}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Settings/Debug/Tabs/RenderDebugView.xaml b/src/Artemis.UI/Screens/Settings/Debug/Tabs/RenderDebugView.xaml
index 629db8ada..4a5c4205d 100644
--- a/src/Artemis.UI/Screens/Settings/Debug/Tabs/RenderDebugView.xaml
+++ b/src/Artemis.UI/Screens/Settings/Debug/Tabs/RenderDebugView.xaml
@@ -32,9 +32,10 @@
-
-
-
+
+
+
+
diff --git a/src/Artemis.UI/Screens/Settings/Debug/Tabs/RenderDebugViewModel.cs b/src/Artemis.UI/Screens/Settings/Debug/Tabs/RenderDebugViewModel.cs
index a879a5a78..d7532cb2c 100644
--- a/src/Artemis.UI/Screens/Settings/Debug/Tabs/RenderDebugViewModel.cs
+++ b/src/Artemis.UI/Screens/Settings/Debug/Tabs/RenderDebugViewModel.cs
@@ -20,6 +20,9 @@ namespace Artemis.UI.Screens.Settings.Debug.Tabs
private int _renderWidth;
private int _renderHeight;
private string _frameTargetPath;
+ private string _renderer;
+ private int _frames;
+ private DateTime _frameCountStart;
public RenderDebugViewModel(ICoreService coreService)
{
@@ -51,6 +54,12 @@ namespace Artemis.UI.Screens.Settings.Debug.Tabs
set => SetAndNotify(ref _renderHeight, value);
}
+ public string Renderer
+ {
+ get => _renderer;
+ set => SetAndNotify(ref _renderer, value);
+ }
+
public void SaveFrame()
{
VistaSaveFileDialog dialog = new VistaSaveFileDialog {Filter = "Portable network graphic (*.png)|*.png", Title = "Save render frame"};
@@ -81,38 +90,38 @@ namespace Artemis.UI.Screens.Settings.Debug.Tabs
private void CoreServiceOnFrameRendered(object sender, FrameRenderedEventArgs e)
{
+ using SKImage skImage = e.Texture.Surface.Snapshot();
+ SKImageInfo bitmapInfo = e.Texture.ImageInfo;
+
+ if (_frameTargetPath != null)
+ {
+ using (SKData data = skImage.Encode(SKEncodedImageFormat.Png, 100))
+ {
+ using (FileStream stream = File.OpenWrite(_frameTargetPath))
+ {
+ data.SaveTo(stream);
+ }
+ }
+
+ _frameTargetPath = null;
+ }
+
Execute.OnUIThreadSync(() =>
{
- SKImageInfo bitmapInfo = e.Texture.Bitmap.Info;
RenderHeight = bitmapInfo.Height;
RenderWidth = bitmapInfo.Width;
// ReSharper disable twice CompareOfFloatsByEqualityOperator
if (CurrentFrame is not WriteableBitmap writable || writable.Width != bitmapInfo.Width || writable.Height != bitmapInfo.Height)
{
- CurrentFrame = e.Texture.Bitmap.ToWriteableBitmap();
+ CurrentFrame = e.Texture.Surface.Snapshot().ToWriteableBitmap();
return;
}
- using SKImage skImage = SKImage.FromPixels(e.Texture.Bitmap.PeekPixels());
-
- if (_frameTargetPath != null)
- {
- using (SKData data = skImage.Encode(SKEncodedImageFormat.Png, 100))
- {
- using (FileStream stream = File.OpenWrite(_frameTargetPath))
- {
- data.SaveTo(stream);
- }
- }
-
- _frameTargetPath = null;
- }
-
- SKImageInfo info = new(skImage.Width, skImage.Height);
writable.Lock();
- using (SKPixmap pixmap = new(info, writable.BackBuffer, writable.BackBufferStride))
+ using (SKPixmap pixmap = new(bitmapInfo, writable.BackBuffer, writable.BackBufferStride))
{
+ // ReSharper disable once AccessToDisposedClosure - Looks fine
skImage.ReadPixels(pixmap, 0, 0);
}
@@ -123,7 +132,16 @@ namespace Artemis.UI.Screens.Settings.Debug.Tabs
private void CoreServiceOnFrameRendering(object sender, FrameRenderingEventArgs e)
{
- CurrentFps = Math.Round(1.0 / e.DeltaTime, 2);
+ if (DateTime.Now - _frameCountStart >= TimeSpan.FromSeconds(1))
+ {
+ CurrentFps = _frames;
+ Renderer = Constants.ManagedGraphicsContext != null ? Constants.ManagedGraphicsContext.GetType().Name : "Software";
+
+ _frames = 0;
+ _frameCountStart = DateTime.Now;
+ }
+
+ _frames++;
}
}
}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabView.xaml b/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabView.xaml
index 6aef820f3..ecd070137 100644
--- a/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabView.xaml
+++ b/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabView.xaml
@@ -7,6 +7,7 @@
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:dataTemplateSelectors="clr-namespace:Artemis.UI.DataTemplateSelectors"
+ xmlns:system="clr-namespace:System;assembly=System.Runtime"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
d:DataContext="{d:DesignInstance local:GeneralSettingsTabViewModel}">
@@ -307,6 +308,30 @@
Rendering
+
+
+
+
+
+
+
+
+
+
+ Preferred render method
+
+ Software-based rendering is done purely on the CPU while Vulkan uses GPU-acceleration
+
+
+
+
+ Software
+ Vulkan
+
+
+
+
+
diff --git a/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabViewModel.cs
index 86e8fdb7b..c8caa051e 100644
--- a/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabViewModel.cs
+++ b/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabViewModel.cs
@@ -3,15 +3,10 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
-using System.Reflection.Metadata;
-using System.Security.Principal;
using System.Threading.Tasks;
-using System.Xml.Linq;
using Artemis.Core;
using Artemis.Core.LayerBrushes;
using Artemis.Core.Services;
-using Artemis.Core.Services.Core;
-using Artemis.UI.Properties;
using Artemis.UI.Screens.StartupWizard;
using Artemis.UI.Services;
using Artemis.UI.Shared;
@@ -30,6 +25,7 @@ namespace Artemis.UI.Screens.Settings.Tabs.General
private readonly IDialogService _dialogService;
private readonly IKernel _kernel;
private readonly IMessageService _messageService;
+ private readonly IRegistrationService _registrationService;
private readonly ISettingsService _settingsService;
private readonly IUpdateService _updateService;
private readonly IWindowManager _windowManager;
@@ -45,7 +41,10 @@ namespace Artemis.UI.Screens.Settings.Tabs.General
ISettingsService settingsService,
IUpdateService updateService,
IPluginManagementService pluginManagementService,
- IMessageService messageService)
+ IMessageService messageService,
+ IRegistrationService registrationService,
+ ICoreService coreService
+ )
{
DisplayName = "GENERAL";
@@ -56,6 +55,7 @@ namespace Artemis.UI.Screens.Settings.Tabs.General
_settingsService = settingsService;
_updateService = updateService;
_messageService = messageService;
+ _registrationService = registrationService;
LogLevels = new BindableCollection(EnumUtilities.GetAllValuesAndDescriptions(typeof(LogEventLevel)));
ColorSchemes = new BindableCollection(EnumUtilities.GetAllValuesAndDescriptions(typeof(ApplicationColorScheme)));
@@ -66,6 +66,11 @@ namespace Artemis.UI.Screens.Settings.Tabs.General
TargetFrameRates = new List>();
for (int i = 10; i <= 30; i += 5)
TargetFrameRates.Add(new Tuple(i + " FPS", i));
+ if (coreService.StartupArguments.Contains("--pcmr"))
+ {
+ TargetFrameRates.Add(new Tuple("60 FPS (lol)", 60));
+ TargetFrameRates.Add(new Tuple("144 FPS (omegalol)", 144));
+ }
List layerBrushProviders = pluginManagementService.GetFeaturesOfType();
@@ -209,6 +214,17 @@ namespace Artemis.UI.Screens.Settings.Tabs.General
}
}
+ public string PreferredGraphicsContext
+ {
+ get => _settingsService.GetSetting("Core.PreferredGraphicsContext", "Vulkan").Value;
+ set
+ {
+ _settingsService.GetSetting("Core.PreferredGraphicsContext", "Vulkan").Value = value;
+ _settingsService.GetSetting("Core.PreferredGraphicsContext", "Vulkan").Save();
+ _registrationService.ApplyPreferredGraphicsContext();
+ }
+ }
+
public double RenderScale
{
get => _settingsService.GetSetting("Core.RenderScale", 0.5).Value;
@@ -316,10 +332,7 @@ namespace Artemis.UI.Screens.Settings.Tabs.General
try
{
bool taskCreated = false;
- if (!recreate)
- {
- taskCreated = SettingsUtilities.IsAutoRunTaskCreated();
- }
+ if (!recreate) taskCreated = SettingsUtilities.IsAutoRunTaskCreated();
if (StartWithWindows && !taskCreated)
SettingsUtilities.CreateAutoRunTask(TimeSpan.FromSeconds(AutoRunDelay));
@@ -335,7 +348,6 @@ namespace Artemis.UI.Screens.Settings.Tabs.General
}
-
public enum ApplicationColorScheme
{
Light,
diff --git a/src/Artemis.UI/Services/RegistrationService.cs b/src/Artemis.UI/Services/RegistrationService.cs
index 0d51ce376..9cbda99e6 100644
--- a/src/Artemis.UI/Services/RegistrationService.cs
+++ b/src/Artemis.UI/Services/RegistrationService.cs
@@ -1,4 +1,5 @@
-using System.Linq;
+using System;
+using System.Linq;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Controllers;
@@ -8,6 +9,7 @@ using Artemis.UI.DefaultTypes.PropertyInput;
using Artemis.UI.InputProviders;
using Artemis.UI.Ninject;
using Artemis.UI.Shared.Services;
+using Artemis.UI.SkiaSharp;
using Serilog;
namespace Artemis.UI.Services
@@ -15,28 +17,38 @@ namespace Artemis.UI.Services
public class RegistrationService : IRegistrationService
{
private readonly ILogger _logger;
+ private readonly ICoreService _coreService;
private readonly IDataModelUIService _dataModelUIService;
private readonly IProfileEditorService _profileEditorService;
private readonly IPluginManagementService _pluginManagementService;
private readonly IInputService _inputService;
private readonly IWebServerService _webServerService;
+ private readonly IRgbService _rgbService;
+ private readonly ISettingsService _settingsService;
private bool _registeredBuiltInDataModelDisplays;
private bool _registeredBuiltInDataModelInputs;
private bool _registeredBuiltInPropertyEditors;
+ private VulkanContext _vulkanContext;
public RegistrationService(ILogger logger,
+ ICoreService coreService,
IDataModelUIService dataModelUIService,
IProfileEditorService profileEditorService,
IPluginManagementService pluginManagementService,
IInputService inputService,
- IWebServerService webServerService)
+ IWebServerService webServerService,
+ IRgbService rgbService,
+ ISettingsService settingsService)
{
_logger = logger;
+ _coreService = coreService;
_dataModelUIService = dataModelUIService;
_profileEditorService = profileEditorService;
_pluginManagementService = pluginManagementService;
_inputService = inputService;
_webServerService = webServerService;
+ _rgbService = rgbService;
+ _settingsService = settingsService;
LoadPluginModules();
pluginManagementService.PluginEnabling += PluginServiceOnPluginEnabling;
@@ -97,6 +109,40 @@ namespace Artemis.UI.Services
_webServerService.AddController();
}
+ ///
+ public void ApplyPreferredGraphicsContext()
+ {
+ if (_coreService.StartupArguments.Contains("--force-software-render"))
+ {
+ _logger.Warning("Startup argument '--force-software-render' is applied, forcing software rendering.");
+ _rgbService.UpdateGraphicsContext(null);
+ return;
+ }
+
+ PluginSetting preferredGraphicsContext = _settingsService.GetSetting("Core.PreferredGraphicsContext", "Vulkan");
+
+ try
+ {
+ switch (preferredGraphicsContext.Value)
+ {
+ case "Software":
+ _rgbService.UpdateGraphicsContext(null);
+ break;
+ case "Vulkan":
+ _vulkanContext ??= new VulkanContext();
+ _rgbService.UpdateGraphicsContext(_vulkanContext);
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+ catch (Exception e)
+ {
+ _logger.Warning(e, "Failed to apply preferred graphics context {preferred}", preferredGraphicsContext.Value);
+ _rgbService.UpdateGraphicsContext(null);
+ }
+ }
+
private void PluginServiceOnPluginEnabling(object sender, PluginEventArgs e)
{
e.Plugin.Kernel.Load(new[] {new PluginUIModule(e.Plugin)});
@@ -116,5 +162,6 @@ namespace Artemis.UI.Services
void RegisterBuiltInPropertyEditors();
void RegisterInputProvider();
void RegisterControllers();
+ void ApplyPreferredGraphicsContext();
}
}
\ No newline at end of file
diff --git a/src/Artemis.UI/SkiaSharp/Vulkan/Kernel32.cs b/src/Artemis.UI/SkiaSharp/Vulkan/Kernel32.cs
new file mode 100644
index 000000000..8f094b4ca
--- /dev/null
+++ b/src/Artemis.UI/SkiaSharp/Vulkan/Kernel32.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace Artemis.UI.SkiaSharp.Vulkan
+{
+ internal class Kernel32
+ {
+ private const string kernel32 = "kernel32.dll";
+
+ static Kernel32()
+ {
+ CurrentModuleHandle = Kernel32.GetModuleHandle(null);
+ if (CurrentModuleHandle == IntPtr.Zero)
+ {
+ throw new Exception("Could not get module handle.");
+ }
+ }
+
+ public static IntPtr CurrentModuleHandle { get; }
+
+ [DllImport(kernel32, CallingConvention = CallingConvention.Winapi, BestFitMapping = false, ThrowOnUnmappableChar = true)]
+ public static extern IntPtr GetModuleHandle([MarshalAs(UnmanagedType.LPTStr)] string lpModuleName);
+ }
+}
diff --git a/src/Artemis.UI/SkiaSharp/Vulkan/VkContext.cs b/src/Artemis.UI/SkiaSharp/Vulkan/VkContext.cs
new file mode 100644
index 000000000..c7cb8de7f
--- /dev/null
+++ b/src/Artemis.UI/SkiaSharp/Vulkan/VkContext.cs
@@ -0,0 +1,35 @@
+using System;
+using SharpVk.Khronos;
+using SkiaSharp;
+using Device = SharpVk.Device;
+using Instance = SharpVk.Instance;
+using PhysicalDevice = SharpVk.PhysicalDevice;
+using Queue = SharpVk.Queue;
+
+namespace Artemis.UI.SkiaSharp.Vulkan
+{
+ internal class VkContext : IDisposable
+ {
+ public virtual Instance Instance { get; protected set; }
+
+ public virtual PhysicalDevice PhysicalDevice { get; protected set; }
+
+ public virtual Surface Surface { get; protected set; }
+
+ public virtual Device Device { get; protected set; }
+
+ public virtual Queue GraphicsQueue { get; protected set; }
+
+ public virtual Queue PresentQueue { get; protected set; }
+
+ public virtual uint GraphicsFamily { get; protected set; }
+
+ public virtual uint PresentFamily { get; protected set; }
+
+ public virtual GRVkGetProcedureAddressDelegate GetProc { get; protected set; }
+
+ public virtual GRSharpVkGetProcedureAddressDelegate SharpVkGetProc { get; protected set; }
+
+ public virtual void Dispose() => Instance?.Dispose();
+ }
+}
diff --git a/src/Artemis.UI/SkiaSharp/Vulkan/Win32VkContext.cs b/src/Artemis.UI/SkiaSharp/Vulkan/Win32VkContext.cs
new file mode 100644
index 000000000..0bf695f5a
--- /dev/null
+++ b/src/Artemis.UI/SkiaSharp/Vulkan/Win32VkContext.cs
@@ -0,0 +1,86 @@
+using System;
+using System.Linq;
+using System.Windows.Forms;
+using SharpVk;
+using SharpVk.Khronos;
+
+namespace Artemis.UI.SkiaSharp.Vulkan
+{
+ internal sealed class Win32VkContext : VkContext
+ {
+ public NativeWindow Window { get; }
+
+ public Win32VkContext()
+ {
+ Window = new NativeWindow();
+ Instance = Instance.Create(null, new[] {"VK_KHR_surface", "VK_KHR_win32_surface"});
+ PhysicalDevice = Instance.EnumeratePhysicalDevices().First();
+ Surface = Instance.CreateWin32Surface(Kernel32.CurrentModuleHandle, Window.Handle);
+
+ (GraphicsFamily, PresentFamily) = FindQueueFamilies();
+
+ DeviceQueueCreateInfo[] queueInfos =
+ {
+ new() {QueueFamilyIndex = GraphicsFamily, QueuePriorities = new[] {1f}},
+ new() {QueueFamilyIndex = PresentFamily, QueuePriorities = new[] {1f}}
+ };
+ Device = PhysicalDevice.CreateDevice(queueInfos, null, null);
+ GraphicsQueue = Device.GetQueue(GraphicsFamily, 0);
+ PresentQueue = Device.GetQueue(PresentFamily, 0);
+
+ GetProc = (name, instanceHandle, deviceHandle) =>
+ {
+ if (deviceHandle != IntPtr.Zero)
+ return Device.GetProcedureAddress(name);
+
+ return Instance.GetProcedureAddress(name);
+ };
+
+ SharpVkGetProc = (name, instance, device) =>
+ {
+ if (device != null)
+ return device.GetProcedureAddress(name);
+ if (instance != null)
+ return instance.GetProcedureAddress(name);
+
+ // SharpVk includes the static functions on Instance, but this is not actually correct
+ // since the functions are static, they are not tied to an instance. For example,
+ // VkCreateInstance is not found on an instance, it is creating said instance.
+ // Other libraries, such as VulkanCore, use another type to do this.
+ return Instance.GetProcedureAddress(name);
+ };
+ }
+
+ public override void Dispose()
+ {
+ base.Dispose();
+ Window.DestroyHandle();
+ }
+
+ private (uint, uint) FindQueueFamilies()
+ {
+ QueueFamilyProperties[] queueFamilyProperties = PhysicalDevice.GetQueueFamilyProperties();
+
+ var graphicsFamily = queueFamilyProperties
+ .Select((properties, index) => new {properties, index})
+ .SkipWhile(pair => !pair.properties.QueueFlags.HasFlag(QueueFlags.Graphics))
+ .FirstOrDefault();
+
+ if (graphicsFamily == null)
+ throw new Exception("Unable to find graphics queue");
+
+ uint? presentFamily = default;
+
+ for (uint i = 0; i < queueFamilyProperties.Length; ++i)
+ {
+ if (PhysicalDevice.GetSurfaceSupport(i, Surface))
+ presentFamily = i;
+ }
+
+ if (!presentFamily.HasValue)
+ throw new Exception("Unable to find present queue");
+
+ return ((uint) graphicsFamily.index, presentFamily.Value);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/SkiaSharp/VulkanContext.cs b/src/Artemis.UI/SkiaSharp/VulkanContext.cs
new file mode 100644
index 000000000..2c96684db
--- /dev/null
+++ b/src/Artemis.UI/SkiaSharp/VulkanContext.cs
@@ -0,0 +1,64 @@
+using System;
+using Artemis.Core.SkiaSharp;
+using Artemis.UI.Exceptions;
+using Artemis.UI.SkiaSharp.Vulkan;
+using SkiaSharp;
+
+namespace Artemis.UI.SkiaSharp
+{
+ public class VulkanContext : IManagedGraphicsContext
+ {
+ private readonly GRVkBackendContext _vulkanBackendContext;
+ private readonly Win32VkContext _vulkanContext;
+
+ public VulkanContext()
+ {
+ // Try everything in separate try-catch blocks to provide some accuracy in error reporting
+ try
+ {
+ _vulkanContext = new Win32VkContext();
+ }
+ catch (Exception e)
+ {
+ throw new ArtemisGraphicsContextException("Failed to create Vulkan context", e);
+ }
+
+ try
+ {
+ _vulkanBackendContext = new GRVkBackendContext
+ {
+ VkInstance = (IntPtr) _vulkanContext.Instance.RawHandle.ToUInt64(),
+ VkPhysicalDevice = (IntPtr) _vulkanContext.PhysicalDevice.RawHandle.ToUInt64(),
+ VkDevice = (IntPtr) _vulkanContext.Device.RawHandle.ToUInt64(),
+ VkQueue = (IntPtr) _vulkanContext.GraphicsQueue.RawHandle.ToUInt64(),
+ GraphicsQueueIndex = _vulkanContext.GraphicsFamily,
+ GetProcedureAddress = _vulkanContext.GetProc
+ };
+ }
+ catch (Exception e)
+ {
+ throw new ArtemisGraphicsContextException("Failed to create Vulkan backend context", e);
+ }
+
+ try
+ {
+ GraphicsContext = GRContext.CreateVulkan(_vulkanBackendContext);
+ if (GraphicsContext == null)
+ throw new ArtemisGraphicsContextException("GRContext.CreateVulkan returned null");
+ }
+ catch (Exception e)
+ {
+ throw new ArtemisGraphicsContextException("Failed to create Vulkan graphics context", e);
+ }
+
+ GraphicsContext.Flush();
+ }
+
+ ///
+ public void Dispose()
+ {
+ }
+
+ public GRContext GraphicsContext { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/packages.lock.json b/src/Artemis.UI/packages.lock.json
index 524e0ec98..12cac355f 100644
--- a/src/Artemis.UI/packages.lock.json
+++ b/src/Artemis.UI/packages.lock.json
@@ -126,6 +126,16 @@
"SkiaSharp.Views.Desktop.Common": "2.80.2"
}
},
+ "SkiaSharp.Vulkan.SharpVk": {
+ "type": "Direct",
+ "requested": "[2.80.2, )",
+ "resolved": "2.80.2",
+ "contentHash": "qiqlbgMsSdxTsaPErtE1lXoMXolVVF9E6irmSTzlW++6BbW8tzA89n7GNsgMYJgyo2ljHZhX5ydhFn0Rkj7VHw==",
+ "dependencies": {
+ "SharpVk": "0.4.2",
+ "SkiaSharp": "2.80.2"
+ }
+ },
"Stylet": {
"type": "Direct",
"requested": "[1.3.5, )",
@@ -513,6 +523,15 @@
"resolved": "1.7.1",
"contentHash": "ljl9iVpmGOjgmxXxyulMBfl7jCLEMmTOSIrQwJJQLIm5PFhtaxRRgdQPY5ElXz+vfPKqX7Aj3RGnAN+SUN7V3w=="
},
+ "SharpVk": {
+ "type": "Transitive",
+ "resolved": "0.4.2",
+ "contentHash": "0CzZJWKw6CTmxKOXzCCyTKCD7tZB6g2+tm2VSSCXWTHlIMHxlRzbH5BaqkYCGo9Y23wp0hPuz2U3NifMH1VI6w==",
+ "dependencies": {
+ "System.Runtime.CompilerServices.Unsafe": "4.4.0",
+ "System.ValueTuple": "4.4.0"
+ }
+ },
"SkiaSharp": {
"type": "Transitive",
"resolved": "2.80.2",
diff --git a/src/Artemis.sln.DotSettings b/src/Artemis.sln.DotSettings
index 46df82de4..3a843dade 100644
--- a/src/Artemis.sln.DotSettings
+++ b/src/Artemis.sln.DotSettings
@@ -225,7 +225,9 @@
True
True
True
+ True
True
- True
\ No newline at end of file
+ True
+ True
\ No newline at end of file