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