From a02431bb82371b1c72455489060006cab07de3b7 Mon Sep 17 00:00:00 2001 From: Robert Date: Sun, 21 Mar 2021 22:40:23 +0100 Subject: [PATCH 1/6] Core - Reworked rendering pipeline to work with different contexts Core - Added GPU-based Vulkan rendering context --- src/Artemis.Core/Constants.cs | 17 +++ src/Artemis.Core/Models/Profile/Folder.cs | 10 +- src/Artemis.Core/Models/Profile/Layer.cs | 19 ++- src/Artemis.Core/Models/Profile/Renderer.cs | 27 ++--- src/Artemis.Core/RGB.NET/SKTexture.cs | 38 ++++-- src/Artemis.Core/Services/CoreService.cs | 26 +++-- src/Artemis.Core/Services/RgbService.cs | 6 +- src/Artemis.UI/Artemis.UI.csproj | 1 + src/Artemis.UI/Bootstrapper.cs | 23 +++- .../Debug/Tabs/RenderDebugViewModel.cs | 8 +- src/Artemis.UI/SkiaSharp/Kernel32.cs | 24 ++++ src/Artemis.UI/SkiaSharp/User32.cs | 109 ++++++++++++++++++ src/Artemis.UI/SkiaSharp/VkContext.cs | 35 ++++++ src/Artemis.UI/SkiaSharp/Win32VkContext.cs | 83 +++++++++++++ src/Artemis.UI/SkiaSharp/Win32Window.cs | 97 ++++++++++++++++ src/Artemis.UI/packages.lock.json | 19 +++ src/Artemis.sln.DotSettings | 3 +- 17 files changed, 489 insertions(+), 56 deletions(-) create mode 100644 src/Artemis.UI/SkiaSharp/Kernel32.cs create mode 100644 src/Artemis.UI/SkiaSharp/User32.cs create mode 100644 src/Artemis.UI/SkiaSharp/VkContext.cs create mode 100644 src/Artemis.UI/SkiaSharp/Win32VkContext.cs create mode 100644 src/Artemis.UI/SkiaSharp/Win32Window.cs diff --git a/src/Artemis.Core/Constants.cs b/src/Artemis.Core/Constants.cs index 96e72a0f6..068524a1d 100644 --- a/src/Artemis.Core/Constants.cs +++ b/src/Artemis.Core/Constants.cs @@ -4,6 +4,7 @@ using System.IO; using Artemis.Core.JsonConverters; using Artemis.Core.Services.Core; using Newtonsoft.Json; +using SkiaSharp; namespace Artemis.Core { @@ -116,5 +117,21 @@ namespace Artemis.Core typeof(double), typeof(decimal) }; + + private static GRContext? _skiaGraphicsContext; + + /// + /// Gets or sets the graphics context to be used for rendering by SkiaSharp + /// + public static GRContext? SkiaGraphicsContext + { + get => _skiaGraphicsContext; + set + { + if (_skiaGraphicsContext != null) + throw new ArtemisCoreException($"{nameof(SkiaGraphicsContext)} can only be set once."); + _skiaGraphicsContext = value; + } + } } } \ 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..0c8b909eb 100644 --- a/src/Artemis.Core/Models/Profile/Folder.cs +++ b/src/Artemis.Core/Models/Profile/Folder.cs @@ -193,12 +193,12 @@ namespace Artemis.Core { canvas.Save(); Renderer.Open(Path, Parent as Folder); - if (Renderer.Canvas == null || Renderer.Path == null || Renderer.Paint == null) + if (Renderer.Surface == null || Renderer.Path == null || Renderer.Paint == null) throw new ArtemisCoreException("Failed to open folder render context"); SKRect rendererBounds = Renderer.Path.Bounds; foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => e.Enabled)) - baseLayerEffect.PreProcess(Renderer.Canvas, rendererBounds, Renderer.Paint); + baseLayerEffect.PreProcess(Renderer.Surface.Canvas, rendererBounds, Renderer.Paint); // If required, apply the opacity override of the module to the root folder if (IsRootFolder && Profile.Module.OpacityOverride < 1) @@ -213,12 +213,12 @@ namespace Artemis.Core // Iterate the children in reverse because the first layer must be rendered last to end up on top for (int index = Children.Count - 1; index > -1; index--) - Children[index].Render(Renderer.Canvas); + Children[index].Render(Renderer.Surface.Canvas); foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => e.Enabled)) - baseLayerEffect.PostProcess(Renderer.Canvas, rendererBounds, Renderer.Paint); + baseLayerEffect.PostProcess(Renderer.Surface.Canvas, rendererBounds, Renderer.Paint); - canvas.DrawBitmap(Renderer.Bitmap, Renderer.TargetLocation, Renderer.Paint); + canvas.DrawSurface(Renderer.Surface, Renderer.TargetLocation, Renderer.Paint); } finally { diff --git a/src/Artemis.Core/Models/Profile/Layer.cs b/src/Artemis.Core/Models/Profile/Layer.cs index 31d8d8ae4..14dda2b79 100644 --- a/src/Artemis.Core/Models/Profile/Layer.cs +++ b/src/Artemis.Core/Models/Profile/Layer.cs @@ -330,8 +330,7 @@ namespace Artemis.Core { canvas.Save(); Renderer.Open(Path, Parent as Folder); - - if (Renderer.Canvas == null || Renderer.Path == null || Renderer.Paint == null) + if (Renderer.Surface == null || Renderer.Path == null || Renderer.Paint == null) throw new ArtemisCoreException("Failed to open layer render context"); // Apply blend mode and color @@ -357,11 +356,11 @@ namespace Artemis.Core if (LayerBrush.SupportsTransformation) { SKMatrix rotationMatrix = GetTransformMatrix(true, false, false, true); - Renderer.Canvas.SetMatrix(Renderer.Canvas.TotalMatrix.PreConcat(rotationMatrix)); + Renderer.Surface.Canvas.SetMatrix(Renderer.Surface.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); + Renderer.Surface.Canvas.ClipPath(renderPath); DelegateRendering(renderPath.Bounds); } else if (General.TransformMode.CurrentValue == LayerTransformMode.Clip) @@ -370,11 +369,11 @@ namespace Artemis.Core 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); + Renderer.Surface.Canvas.ClipPath(renderPath); DelegateRendering(Renderer.Path.Bounds); } - canvas.DrawBitmap(Renderer.Bitmap, Renderer.TargetLocation, Renderer.Paint); + canvas.DrawSurface(Renderer.Surface, Renderer.TargetLocation, Renderer.Paint); } finally { @@ -395,16 +394,16 @@ namespace Artemis.Core { if (LayerBrush == null) throw new ArtemisCoreException("The layer is not yet ready for rendering"); - if (Renderer.Canvas == null || Renderer.Paint == null) + if (Renderer.Surface == 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(Renderer.Surface.Canvas, bounds, Renderer.Paint); - LayerBrush.InternalRender(Renderer.Canvas, bounds, Renderer.Paint); + LayerBrush.InternalRender(Renderer.Surface.Canvas, bounds, Renderer.Paint); foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => e.Enabled)) - baseLayerEffect.PostProcess(Renderer.Canvas, bounds, Renderer.Paint); + baseLayerEffect.PostProcess(Renderer.Surface.Canvas, bounds, Renderer.Paint); } internal void CalculateRenderProperties() diff --git a/src/Artemis.Core/Models/Profile/Renderer.cs b/src/Artemis.Core/Models/Profile/Renderer.cs index 7e502b277..fee081904 100644 --- a/src/Artemis.Core/Models/Profile/Renderer.cs +++ b/src/Artemis.Core/Models/Profile/Renderer.cs @@ -9,8 +9,7 @@ namespace Artemis.Core private bool _disposed; private SKRect _lastBounds; private SKRect _lastParentBounds; - public SKBitmap? Bitmap { get; private set; } - public SKCanvas? Canvas { get; private set; } + public SKSurface? Surface { get; private set; } public SKPaint? Paint { get; private set; } public SKPath? Path { get; private set; } public SKPoint TargetLocation { get; private set; } @@ -31,22 +30,26 @@ namespace Artemis.Core if (path.Bounds != _lastBounds || (parent != null && parent.Bounds != _lastParentBounds)) 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 SKImageInfo(width, height); + if (Constants.SkiaGraphicsContext == null) + Surface = SKSurface.Create(imageInfo); + else + Surface = SKSurface.Create(Constants.SkiaGraphicsContext, 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; @@ -55,8 +58,8 @@ namespace Artemis.Core Paint = new SKPaint(); - Canvas.Clear(); - Canvas.Save(); + Surface.Canvas.Clear(); + Surface.Canvas.Save(); IsOpen = true; } @@ -66,7 +69,7 @@ namespace Artemis.Core if (_disposed) throw new ObjectDisposedException("Renderer"); - Canvas?.Restore(); + Surface?.Canvas.Restore(); Paint?.Dispose(); Paint = null; @@ -86,15 +89,13 @@ 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; } diff --git a/src/Artemis.Core/RGB.NET/SKTexture.cs b/src/Artemis.Core/RGB.NET/SKTexture.cs index 87e2c8cfb..20f80b066 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.Diagnostics; +using System.Runtime.InteropServices; using RGB.NET.Core; using RGB.NET.Presets.Textures.Sampler; using SkiaSharp; @@ -10,14 +12,19 @@ namespace Artemis.Core /// public sealed class SKTexture : PixelTexture, IDisposable { - private bool _disposed; + private SKPixmap? _pixelData; + private SKImage? _rasterImage; #region Constructors internal SKTexture(int width, int height, float renderScale) : base(width, height, 4, new AverageByteSampler()) { - Bitmap = new SKBitmap(new SKImageInfo(width, height, SKColorType.Rgb888x)); + ImageInfo = new SKImageInfo(width, height); + if (Constants.SkiaGraphicsContext == null) + Surface = SKSurface.Create(ImageInfo); + else + Surface = SKSurface.Create(Constants.SkiaGraphicsContext, true, ImageInfo); RenderScale = renderScale; } @@ -25,10 +32,20 @@ namespace Artemis.Core #region Methods + internal void CopyPixelData() + { + using SKImage skImage = Surface.Snapshot(); + + _rasterImage?.Dispose(); + _pixelData?.Dispose(); + _rasterImage = skImage.ToRasterImage(); + _pixelData = _rasterImage.PeekPixels(); + } + /// 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 +55,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 != null ? _pixelData.GetPixelSpan() : ReadOnlySpan.Empty; /// /// Gets the render scale of the texture @@ -63,12 +85,12 @@ namespace Artemis.Core { IsInvalid = true; } - + /// public void Dispose() { - _disposed = true; - Bitmap.Dispose(); + Surface.Dispose(); + _pixelData?.Dispose(); } #endregion diff --git a/src/Artemis.Core/Services/CoreService.cs b/src/Artemis.Core/Services/CoreService.cs index 239aaa567..e489e0a31 100644 --- a/src/Artemis.Core/Services/CoreService.cs +++ b/src/Artemis.Core/Services/CoreService.cs @@ -139,21 +139,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)); } diff --git a/src/Artemis.Core/Services/RgbService.cs b/src/Artemis.Core/Services/RgbService.cs index ed226a331..2e34d27dd 100644 --- a/src/Artemis.Core/Services/RgbService.cs +++ b/src/Artemis.Core/Services/RgbService.cs @@ -8,7 +8,6 @@ using Artemis.Storage.Entities.Surface; using Artemis.Storage.Repositories.Interfaces; using RGB.NET.Core; using Serilog; -using SkiaSharp; namespace Artemis.Core.Services { @@ -52,7 +51,7 @@ namespace Artemis.Core.Services UpdateTrigger = new TimerUpdateTrigger {UpdateFrequency = 1.0 / _targetFrameRateSetting.Value}; Surface.RegisterUpdateTrigger(UpdateTrigger); } - + public TimerUpdateTrigger UpdateTrigger { get; } protected virtual void OnDeviceRemoved(DeviceEventArgs e) @@ -232,8 +231,9 @@ namespace Artemis.Core.Services { if (!RenderOpen) throw new ArtemisCoreException("Render pipeline is already closed"); - + RenderOpen = false; + _texture?.CopyPixelData(); } public void CreateTexture() 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..29383b8fe 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 @@ -69,6 +72,8 @@ namespace Artemis.UI { UIElement view = viewManager.CreateAndBindViewForModelIfNecessary(RootViewModel); ((TrayViewModel) RootViewModel).SetTaskbarIcon(view); + + CreateGraphicsContext(); }); // Initialize the core async so the UI can show the progress @@ -90,7 +95,7 @@ namespace Artemis.UI IRegistrationService registrationService = Kernel.Get(); registrationService.RegisterInputProvider(); registrationService.RegisterControllers(); - + // Initialize background services Kernel.Get(); } @@ -128,6 +133,22 @@ namespace Artemis.UI e.Handled = true; } + private void CreateGraphicsContext() + { + Win32VkContext vulkanContext = new(); + GRVkBackendContext vulkanBackendContext = new() + { + 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 + }; + + Constants.SkiaGraphicsContext = GRContext.CreateVulkan(vulkanBackendContext); + } + private void HandleFatalException(Exception e, ILogger logger) { logger.Fatal(e, "Fatal exception during initialization, shutting down."); diff --git a/src/Artemis.UI/Screens/Settings/Debug/Tabs/RenderDebugViewModel.cs b/src/Artemis.UI/Screens/Settings/Debug/Tabs/RenderDebugViewModel.cs index 934ba89b9..224cb5adc 100644 --- a/src/Artemis.UI/Screens/Settings/Debug/Tabs/RenderDebugViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Debug/Tabs/RenderDebugViewModel.cs @@ -83,18 +83,20 @@ namespace Artemis.UI.Screens.Settings.Debug.Tabs { Execute.OnUIThreadSync(() => { - SKImageInfo bitmapInfo = e.Texture.Bitmap.Info; + SKImageInfo bitmapInfo = e.Texture.ImageInfo; 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()); + using SKImage skImage = e.Texture.Surface.Snapshot(); if (_frameTargetPath != null) { diff --git a/src/Artemis.UI/SkiaSharp/Kernel32.cs b/src/Artemis.UI/SkiaSharp/Kernel32.cs new file mode 100644 index 000000000..6fd86aab0 --- /dev/null +++ b/src/Artemis.UI/SkiaSharp/Kernel32.cs @@ -0,0 +1,24 @@ +using System; +using System.Runtime.InteropServices; + +namespace Artemis.UI.SkiaSharp +{ + 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/User32.cs b/src/Artemis.UI/SkiaSharp/User32.cs new file mode 100644 index 000000000..a53212d5f --- /dev/null +++ b/src/Artemis.UI/SkiaSharp/User32.cs @@ -0,0 +1,109 @@ +using System; +using System.Runtime.InteropServices; +using System.Text; + +namespace Artemis.UI.SkiaSharp +{ + internal class User32 + { + private const string user32 = "user32.dll"; + + public const uint IDC_ARROW = 32512; + + public const uint IDI_APPLICATION = 32512; + public const uint IDI_WINLOGO = 32517; + + public const int SW_HIDE = 0; + + public const uint CS_VREDRAW = 0x1; + public const uint CS_HREDRAW = 0x2; + public const uint CS_OWNDC = 0x20; + + public const uint WS_EX_CLIENTEDGE = 0x00000200; + + [DllImport(user32, CallingConvention = CallingConvention.Winapi, SetLastError = true, BestFitMapping = false, ThrowOnUnmappableChar = true)] + public static extern ushort RegisterClass(ref Win32Window.WNDCLASS lpWndClass); + + [DllImport(user32, CallingConvention = CallingConvention.Winapi, SetLastError = true, BestFitMapping = false, ThrowOnUnmappableChar = true)] + public static extern ushort UnregisterClass([MarshalAs(UnmanagedType.LPTStr)] string lpClassName, IntPtr hInstance); + + [DllImport(user32, CallingConvention = CallingConvention.Winapi, SetLastError = true)] + public static extern IntPtr LoadCursor(IntPtr hInstance, int lpCursorName); + + [DllImport(user32, CallingConvention = CallingConvention.Winapi, SetLastError = true)] + public static extern IntPtr LoadIcon(IntPtr hInstance, IntPtr lpIconName); + + [DllImport(user32, CallingConvention = CallingConvention.Winapi)] + public static extern IntPtr DefWindowProc(IntPtr hWnd, uint uMsg, IntPtr wParam, IntPtr lParam); + + [DllImport(user32, CallingConvention = CallingConvention.Winapi, SetLastError = true, BestFitMapping = false, ThrowOnUnmappableChar = true)] + public static extern IntPtr CreateWindowEx(uint dwExStyle, [MarshalAs(UnmanagedType.LPTStr)] string lpClassName, [MarshalAs(UnmanagedType.LPTStr)] string lpWindowName, WindowStyles dwStyle, int x, int y, int nWidth, int nHeight, IntPtr hWndParent, IntPtr hMenu, IntPtr hInstance, IntPtr lpParam); + + public static IntPtr CreateWindow(string lpClassName, string lpWindowName, WindowStyles dwStyle, int x, int y, int nWidth, int nHeight, IntPtr hWndParent, IntPtr hMenu, IntPtr hInstance, IntPtr lpParam) + { + return CreateWindowEx(0, lpClassName, lpWindowName, dwStyle, x, y, nWidth, nHeight, hWndParent, hMenu, hInstance, lpParam); + } + + [DllImport(user32, CallingConvention = CallingConvention.Winapi, SetLastError = true)] + public static extern IntPtr GetDC(IntPtr hWnd); + + [DllImport(user32, CallingConvention = CallingConvention.Winapi, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool ReleaseDC(IntPtr hWnd, IntPtr hDC); + + [DllImport(user32, CallingConvention = CallingConvention.Winapi, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool DestroyWindow(IntPtr hWnd); + + [DllImport(user32, CallingConvention = CallingConvention.Winapi, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool IsWindow(IntPtr hWnd); + + [DllImport(user32, CallingConvention = CallingConvention.Winapi, SetLastError = true)] + public static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount); + + [DllImport(user32, CallingConvention = CallingConvention.Winapi, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool AdjustWindowRectEx(ref RECT lpRect, WindowStyles dwStyle, bool bMenu, uint dwExStyle); + + [DllImport(user32, CallingConvention = CallingConvention.Winapi, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool ShowWindow(IntPtr hWnd, uint nCmdShow); + + [StructLayout(LayoutKind.Sequential)] + public struct RECT + { + public int left; + public int top; + public int right; + public int bottom; + } + + [Flags] + public enum WindowStyles : uint + { + WS_BORDER = 0x800000, + WS_CAPTION = 0xc00000, + WS_CHILD = 0x40000000, + WS_CLIPCHILDREN = 0x2000000, + WS_CLIPSIBLINGS = 0x4000000, + WS_DISABLED = 0x8000000, + WS_DLGFRAME = 0x400000, + WS_GROUP = 0x20000, + WS_HSCROLL = 0x100000, + WS_MAXIMIZE = 0x1000000, + WS_MAXIMIZEBOX = 0x10000, + WS_MINIMIZE = 0x20000000, + WS_MINIMIZEBOX = 0x20000, + WS_OVERLAPPED = 0x0, + WS_OVERLAPPEDWINDOW = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_SIZEFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX, + WS_POPUP = 0x80000000u, + WS_POPUPWINDOW = WS_POPUP | WS_BORDER | WS_SYSMENU, + WS_SIZEFRAME = 0x40000, + WS_SYSMENU = 0x80000, + WS_TABSTOP = 0x10000, + WS_VISIBLE = 0x10000000, + WS_VSCROLL = 0x200000 + } + } +} diff --git a/src/Artemis.UI/SkiaSharp/VkContext.cs b/src/Artemis.UI/SkiaSharp/VkContext.cs new file mode 100644 index 000000000..48937cf46 --- /dev/null +++ b/src/Artemis.UI/SkiaSharp/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 +{ + 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/Win32VkContext.cs b/src/Artemis.UI/SkiaSharp/Win32VkContext.cs new file mode 100644 index 000000000..1ddc7c539 --- /dev/null +++ b/src/Artemis.UI/SkiaSharp/Win32VkContext.cs @@ -0,0 +1,83 @@ +using System; +using System.Linq; +using System.Windows.Forms; +using SharpVk; +using SharpVk.Khronos; + +namespace Artemis.UI.SkiaSharp +{ + internal sealed class Win32VkContext : VkContext + { + private static readonly NativeWindow window = new NativeWindow(); + + public Win32VkContext() + { + 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[] + { + new DeviceQueueCreateInfo { QueueFamilyIndex = GraphicsFamily, QueuePriorities = new[] { 1f } }, + new DeviceQueueCreateInfo { 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); + }; + } + + 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); + } + } +} diff --git a/src/Artemis.UI/SkiaSharp/Win32Window.cs b/src/Artemis.UI/SkiaSharp/Win32Window.cs new file mode 100644 index 000000000..c7dd0c48c --- /dev/null +++ b/src/Artemis.UI/SkiaSharp/Win32Window.cs @@ -0,0 +1,97 @@ +using System; +using System.Runtime.InteropServices; + +namespace Artemis.UI.SkiaSharp +{ + internal class Win32Window : IDisposable + { + private ushort classRegistration; + + public string WindowClassName { get; } + + public IntPtr WindowHandle { get; private set; } + + public IntPtr DeviceContextHandle { get; private set; } + + public Win32Window(string className) + { + WindowClassName = className; + + var wc = new WNDCLASS + { + cbClsExtra = 0, + cbWndExtra = 0, + hbrBackground = IntPtr.Zero, + hCursor = User32.LoadCursor(IntPtr.Zero, (int)User32.IDC_ARROW), + hIcon = User32.LoadIcon(IntPtr.Zero, (IntPtr)User32.IDI_APPLICATION), + hInstance = Kernel32.CurrentModuleHandle, + lpfnWndProc = (WNDPROC)User32.DefWindowProc, + lpszClassName = WindowClassName, + lpszMenuName = null, + style = User32.CS_HREDRAW | User32.CS_VREDRAW | User32.CS_OWNDC + }; + + classRegistration = User32.RegisterClass(ref wc); + if (classRegistration == 0) + throw new Exception($"Could not register window class: {className}"); + + WindowHandle = User32.CreateWindow( + WindowClassName, + $"The Invisible Man ({className})", + User32.WindowStyles.WS_OVERLAPPEDWINDOW, + 0, 0, + 1, 1, + IntPtr.Zero, IntPtr.Zero, Kernel32.CurrentModuleHandle, IntPtr.Zero); + if (WindowHandle == IntPtr.Zero) + throw new Exception($"Could not create window: {className}"); + + DeviceContextHandle = User32.GetDC(WindowHandle); + if (DeviceContextHandle == IntPtr.Zero) + { + Dispose(); + throw new Exception($"Could not get device context: {className}"); + } + } + + public void Dispose() + { + if (WindowHandle != IntPtr.Zero) + { + if (DeviceContextHandle != IntPtr.Zero) + { + User32.ReleaseDC(WindowHandle, DeviceContextHandle); + DeviceContextHandle = IntPtr.Zero; + } + + User32.DestroyWindow(WindowHandle); + WindowHandle = IntPtr.Zero; + } + + if (classRegistration != 0) + { + User32.UnregisterClass(WindowClassName, Kernel32.CurrentModuleHandle); + classRegistration = 0; + } + } + + public delegate IntPtr WNDPROC(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); + + [StructLayout(LayoutKind.Sequential)] + public struct WNDCLASS + { + public uint style; + [MarshalAs(UnmanagedType.FunctionPtr)] + public WNDPROC lpfnWndProc; + public int cbClsExtra; + public int cbWndExtra; + public IntPtr hInstance; + public IntPtr hIcon; + public IntPtr hCursor; + public IntPtr hbrBackground; + [MarshalAs(UnmanagedType.LPTStr)] + public string lpszMenuName; + [MarshalAs(UnmanagedType.LPTStr)] + public string lpszClassName; + } + } +} 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..ca870ba3a 100644 --- a/src/Artemis.sln.DotSettings +++ b/src/Artemis.sln.DotSettings @@ -228,4 +228,5 @@ True - True \ No newline at end of file + True + True \ No newline at end of file From d7e302fb23ea559a6125b41827fbb85306059f86 Mon Sep 17 00:00:00 2001 From: Robert Date: Sun, 21 Mar 2021 22:53:59 +0100 Subject: [PATCH 2/6] Core - Move frame timing stop-point to after closing the pipeline --- src/Artemis.Core/Services/CoreService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Artemis.Core/Services/CoreService.cs b/src/Artemis.Core/Services/CoreService.cs index e489e0a31..eac279fe1 100644 --- a/src/Artemis.Core/Services/CoreService.cs +++ b/src/Artemis.Core/Services/CoreService.cs @@ -165,9 +165,9 @@ namespace Artemis.Core.Services } finally { + _rgbService.CloseRender(); _frameStopWatch.Stop(); FrameTime = _frameStopWatch.Elapsed; - _rgbService.CloseRender(); LogUpdateExceptions(); } From f888fb569795ec9f36a646f7967dab31a289496f Mon Sep 17 00:00:00 2001 From: Robert Date: Mon, 22 Mar 2021 19:29:53 +0100 Subject: [PATCH 3/6] Core - Added the ability to change graphics context at runtime Settings - Added setting for switching between software-based and Vulkan-based rendering --- src/Artemis.Core/Constants.cs | 19 +-- .../ArtemisGraphicsContextException.cs | 25 ++++ src/Artemis.Core/Models/Profile/Renderer.cs | 6 +- src/Artemis.Core/RGB.NET/SKTexture.cs | 9 +- src/Artemis.Core/Services/CoreService.cs | 2 + .../Services/Interfaces/IRgbService.cs | 11 ++ src/Artemis.Core/Services/RgbService.cs | 38 ++++-- .../SkiaSharp/IManagedGraphicsContext.cs | 16 +++ src/Artemis.UI/Bootstrapper.cs | 25 +--- .../ArtemisGraphicsContextException.cs | 22 ++++ .../Tabs/General/GeneralSettingsTabView.xaml | 25 ++++ .../General/GeneralSettingsTabViewModel.cs | 27 +++-- .../Services/RegistrationService.cs | 49 +++++++- src/Artemis.UI/SkiaSharp/User32.cs | 109 ------------------ .../SkiaSharp/{ => Vulkan}/Kernel32.cs | 2 +- .../SkiaSharp/{ => Vulkan}/VkContext.cs | 2 +- .../SkiaSharp/{ => Vulkan}/Win32VkContext.cs | 2 +- src/Artemis.UI/SkiaSharp/VulkanContext.cs | 65 +++++++++++ src/Artemis.UI/SkiaSharp/Win32Window.cs | 97 ---------------- 19 files changed, 281 insertions(+), 270 deletions(-) create mode 100644 src/Artemis.Core/Exceptions/ArtemisGraphicsContextException.cs create mode 100644 src/Artemis.Core/SkiaSharp/IManagedGraphicsContext.cs create mode 100644 src/Artemis.UI/Exceptions/ArtemisGraphicsContextException.cs delete mode 100644 src/Artemis.UI/SkiaSharp/User32.cs rename src/Artemis.UI/SkiaSharp/{ => Vulkan}/Kernel32.cs (94%) rename src/Artemis.UI/SkiaSharp/{ => Vulkan}/VkContext.cs (96%) rename src/Artemis.UI/SkiaSharp/{ => Vulkan}/Win32VkContext.cs (98%) create mode 100644 src/Artemis.UI/SkiaSharp/VulkanContext.cs delete mode 100644 src/Artemis.UI/SkiaSharp/Win32Window.cs diff --git a/src/Artemis.Core/Constants.cs b/src/Artemis.Core/Constants.cs index 068524a1d..21c2ca973 100644 --- a/src/Artemis.Core/Constants.cs +++ b/src/Artemis.Core/Constants.cs @@ -2,9 +2,10 @@ 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; -using SkiaSharp; namespace Artemis.Core { @@ -118,20 +119,10 @@ namespace Artemis.Core typeof(decimal) }; - private static GRContext? _skiaGraphicsContext; - /// - /// Gets or sets the graphics context to be used for rendering by SkiaSharp + /// Gets the graphics context to be used for rendering by SkiaSharp. Can be set via + /// . /// - public static GRContext? SkiaGraphicsContext - { - get => _skiaGraphicsContext; - set - { - if (_skiaGraphicsContext != null) - throw new ArtemisCoreException($"{nameof(SkiaGraphicsContext)} can only be set once."); - _skiaGraphicsContext = value; - } - } + 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/Models/Profile/Renderer.cs b/src/Artemis.Core/Models/Profile/Renderer.cs index fee081904..05c2fd8e1 100644 --- a/src/Artemis.Core/Models/Profile/Renderer.cs +++ b/src/Artemis.Core/Models/Profile/Renderer.cs @@ -36,11 +36,11 @@ namespace Artemis.Core int width = (int) pathBounds.Width; int height = (int) pathBounds.Height; - SKImageInfo imageInfo = new SKImageInfo(width, height); - if (Constants.SkiaGraphicsContext == null) + SKImageInfo imageInfo = new(width, height); + if (Constants.ManagedGraphicsContext?.GraphicsContext == null) Surface = SKSurface.Create(imageInfo); else - Surface = SKSurface.Create(Constants.SkiaGraphicsContext, true, imageInfo); + Surface = SKSurface.Create(Constants.ManagedGraphicsContext.GraphicsContext, true, imageInfo); Path = new SKPath(path); Path.Transform(SKMatrix.CreateTranslation(pathBounds.Left * -1, pathBounds.Top * -1)); diff --git a/src/Artemis.Core/RGB.NET/SKTexture.cs b/src/Artemis.Core/RGB.NET/SKTexture.cs index 20f80b066..cc0ebc4c9 100644 --- a/src/Artemis.Core/RGB.NET/SKTexture.cs +++ b/src/Artemis.Core/RGB.NET/SKTexture.cs @@ -1,6 +1,5 @@ using System; -using System.Diagnostics; -using System.Runtime.InteropServices; +using Artemis.Core.SkiaSharp; using RGB.NET.Core; using RGB.NET.Presets.Textures.Sampler; using SkiaSharp; @@ -17,14 +16,14 @@ namespace Artemis.Core #region Constructors - internal SKTexture(int width, int height, float renderScale) + internal SKTexture(IManagedGraphicsContext? managedGraphicsContext, int width, int height, float renderScale) : base(width, height, 4, new AverageByteSampler()) { ImageInfo = new SKImageInfo(width, height); - if (Constants.SkiaGraphicsContext == null) + if (managedGraphicsContext == null) Surface = SKSurface.Create(ImageInfo); else - Surface = SKSurface.Create(Constants.SkiaGraphicsContext, true, ImageInfo); + Surface = SKSurface.Create(managedGraphicsContext.GraphicsContext, true, ImageInfo); RenderScale = renderScale; } diff --git a/src/Artemis.Core/Services/CoreService.cs b/src/Artemis.Core/Services/CoreService.cs index eac279fe1..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(); @@ -242,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 2e34d27dd..7be1f8c17 100644 --- a/src/Artemis.Core/Services/RgbService.cs +++ b/src/Artemis.Core/Services/RgbService.cs @@ -4,6 +4,7 @@ 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; @@ -51,7 +52,7 @@ namespace Artemis.Core.Services UpdateTrigger = new TimerUpdateTrigger {UpdateFrequency = 1.0 / _targetFrameRateSetting.Value}; Surface.RegisterUpdateTrigger(UpdateTrigger); } - + public TimerUpdateTrigger UpdateTrigger { get; } protected virtual void OnDeviceRemoved(DeviceEventArgs e) @@ -118,9 +119,7 @@ namespace Artemis.Core.Services 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; } @@ -215,6 +214,8 @@ namespace Artemis.Core.Services #region Rendering + private IManagedGraphicsContext? _newGraphicsContext; + public SKTexture OpenRender() { if (RenderOpen) @@ -231,7 +232,7 @@ namespace Artemis.Core.Services { if (!RenderOpen) throw new ArtemisCoreException("Render pipeline is already closed"); - + RenderOpen = false; _texture?.CopyPixelData(); } @@ -241,15 +242,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.UI/Bootstrapper.cs b/src/Artemis.UI/Bootstrapper.cs index 29383b8fe..888851231 100644 --- a/src/Artemis.UI/Bootstrapper.cs +++ b/src/Artemis.UI/Bootstrapper.cs @@ -68,12 +68,10 @@ 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); - - CreateGraphicsContext(); }); // Initialize the core async so the UI can show the progress @@ -96,6 +94,11 @@ namespace Artemis.UI registrationService.RegisterInputProvider(); registrationService.RegisterControllers(); + Execute.OnUIThreadSync(() => + { + registrationService.ApplyPreferredGraphicsContext(); + }); + // Initialize background services Kernel.Get(); } @@ -133,22 +136,6 @@ namespace Artemis.UI e.Handled = true; } - private void CreateGraphicsContext() - { - Win32VkContext vulkanContext = new(); - GRVkBackendContext vulkanBackendContext = new() - { - 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 - }; - - Constants.SkiaGraphicsContext = GRContext.CreateVulkan(vulkanBackendContext); - } - private void HandleFatalException(Exception e, ILogger logger) { logger.Fatal(e, "Fatal exception during initialization, shutting down."); 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/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..9193ba8ff 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,8 @@ namespace Artemis.UI.Screens.Settings.Tabs.General ISettingsService settingsService, IUpdateService updateService, IPluginManagementService pluginManagementService, - IMessageService messageService) + IMessageService messageService, + IRegistrationService registrationService) { DisplayName = "GENERAL"; @@ -56,6 +53,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))); @@ -209,6 +207,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 +325,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 +341,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..6da26b430 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,37 @@ 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; 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 +108,39 @@ 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": + _rgbService.UpdateGraphicsContext(new 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 +160,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/User32.cs b/src/Artemis.UI/SkiaSharp/User32.cs deleted file mode 100644 index a53212d5f..000000000 --- a/src/Artemis.UI/SkiaSharp/User32.cs +++ /dev/null @@ -1,109 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using System.Text; - -namespace Artemis.UI.SkiaSharp -{ - internal class User32 - { - private const string user32 = "user32.dll"; - - public const uint IDC_ARROW = 32512; - - public const uint IDI_APPLICATION = 32512; - public const uint IDI_WINLOGO = 32517; - - public const int SW_HIDE = 0; - - public const uint CS_VREDRAW = 0x1; - public const uint CS_HREDRAW = 0x2; - public const uint CS_OWNDC = 0x20; - - public const uint WS_EX_CLIENTEDGE = 0x00000200; - - [DllImport(user32, CallingConvention = CallingConvention.Winapi, SetLastError = true, BestFitMapping = false, ThrowOnUnmappableChar = true)] - public static extern ushort RegisterClass(ref Win32Window.WNDCLASS lpWndClass); - - [DllImport(user32, CallingConvention = CallingConvention.Winapi, SetLastError = true, BestFitMapping = false, ThrowOnUnmappableChar = true)] - public static extern ushort UnregisterClass([MarshalAs(UnmanagedType.LPTStr)] string lpClassName, IntPtr hInstance); - - [DllImport(user32, CallingConvention = CallingConvention.Winapi, SetLastError = true)] - public static extern IntPtr LoadCursor(IntPtr hInstance, int lpCursorName); - - [DllImport(user32, CallingConvention = CallingConvention.Winapi, SetLastError = true)] - public static extern IntPtr LoadIcon(IntPtr hInstance, IntPtr lpIconName); - - [DllImport(user32, CallingConvention = CallingConvention.Winapi)] - public static extern IntPtr DefWindowProc(IntPtr hWnd, uint uMsg, IntPtr wParam, IntPtr lParam); - - [DllImport(user32, CallingConvention = CallingConvention.Winapi, SetLastError = true, BestFitMapping = false, ThrowOnUnmappableChar = true)] - public static extern IntPtr CreateWindowEx(uint dwExStyle, [MarshalAs(UnmanagedType.LPTStr)] string lpClassName, [MarshalAs(UnmanagedType.LPTStr)] string lpWindowName, WindowStyles dwStyle, int x, int y, int nWidth, int nHeight, IntPtr hWndParent, IntPtr hMenu, IntPtr hInstance, IntPtr lpParam); - - public static IntPtr CreateWindow(string lpClassName, string lpWindowName, WindowStyles dwStyle, int x, int y, int nWidth, int nHeight, IntPtr hWndParent, IntPtr hMenu, IntPtr hInstance, IntPtr lpParam) - { - return CreateWindowEx(0, lpClassName, lpWindowName, dwStyle, x, y, nWidth, nHeight, hWndParent, hMenu, hInstance, lpParam); - } - - [DllImport(user32, CallingConvention = CallingConvention.Winapi, SetLastError = true)] - public static extern IntPtr GetDC(IntPtr hWnd); - - [DllImport(user32, CallingConvention = CallingConvention.Winapi, SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool ReleaseDC(IntPtr hWnd, IntPtr hDC); - - [DllImport(user32, CallingConvention = CallingConvention.Winapi, SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool DestroyWindow(IntPtr hWnd); - - [DllImport(user32, CallingConvention = CallingConvention.Winapi, SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool IsWindow(IntPtr hWnd); - - [DllImport(user32, CallingConvention = CallingConvention.Winapi, SetLastError = true)] - public static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount); - - [DllImport(user32, CallingConvention = CallingConvention.Winapi, SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool AdjustWindowRectEx(ref RECT lpRect, WindowStyles dwStyle, bool bMenu, uint dwExStyle); - - [DllImport(user32, CallingConvention = CallingConvention.Winapi, SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool ShowWindow(IntPtr hWnd, uint nCmdShow); - - [StructLayout(LayoutKind.Sequential)] - public struct RECT - { - public int left; - public int top; - public int right; - public int bottom; - } - - [Flags] - public enum WindowStyles : uint - { - WS_BORDER = 0x800000, - WS_CAPTION = 0xc00000, - WS_CHILD = 0x40000000, - WS_CLIPCHILDREN = 0x2000000, - WS_CLIPSIBLINGS = 0x4000000, - WS_DISABLED = 0x8000000, - WS_DLGFRAME = 0x400000, - WS_GROUP = 0x20000, - WS_HSCROLL = 0x100000, - WS_MAXIMIZE = 0x1000000, - WS_MAXIMIZEBOX = 0x10000, - WS_MINIMIZE = 0x20000000, - WS_MINIMIZEBOX = 0x20000, - WS_OVERLAPPED = 0x0, - WS_OVERLAPPEDWINDOW = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_SIZEFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX, - WS_POPUP = 0x80000000u, - WS_POPUPWINDOW = WS_POPUP | WS_BORDER | WS_SYSMENU, - WS_SIZEFRAME = 0x40000, - WS_SYSMENU = 0x80000, - WS_TABSTOP = 0x10000, - WS_VISIBLE = 0x10000000, - WS_VSCROLL = 0x200000 - } - } -} diff --git a/src/Artemis.UI/SkiaSharp/Kernel32.cs b/src/Artemis.UI/SkiaSharp/Vulkan/Kernel32.cs similarity index 94% rename from src/Artemis.UI/SkiaSharp/Kernel32.cs rename to src/Artemis.UI/SkiaSharp/Vulkan/Kernel32.cs index 6fd86aab0..8f094b4ca 100644 --- a/src/Artemis.UI/SkiaSharp/Kernel32.cs +++ b/src/Artemis.UI/SkiaSharp/Vulkan/Kernel32.cs @@ -1,7 +1,7 @@ using System; using System.Runtime.InteropServices; -namespace Artemis.UI.SkiaSharp +namespace Artemis.UI.SkiaSharp.Vulkan { internal class Kernel32 { diff --git a/src/Artemis.UI/SkiaSharp/VkContext.cs b/src/Artemis.UI/SkiaSharp/Vulkan/VkContext.cs similarity index 96% rename from src/Artemis.UI/SkiaSharp/VkContext.cs rename to src/Artemis.UI/SkiaSharp/Vulkan/VkContext.cs index 48937cf46..c7cb8de7f 100644 --- a/src/Artemis.UI/SkiaSharp/VkContext.cs +++ b/src/Artemis.UI/SkiaSharp/Vulkan/VkContext.cs @@ -6,7 +6,7 @@ using Instance = SharpVk.Instance; using PhysicalDevice = SharpVk.PhysicalDevice; using Queue = SharpVk.Queue; -namespace Artemis.UI.SkiaSharp +namespace Artemis.UI.SkiaSharp.Vulkan { internal class VkContext : IDisposable { diff --git a/src/Artemis.UI/SkiaSharp/Win32VkContext.cs b/src/Artemis.UI/SkiaSharp/Vulkan/Win32VkContext.cs similarity index 98% rename from src/Artemis.UI/SkiaSharp/Win32VkContext.cs rename to src/Artemis.UI/SkiaSharp/Vulkan/Win32VkContext.cs index 1ddc7c539..eff84d8ac 100644 --- a/src/Artemis.UI/SkiaSharp/Win32VkContext.cs +++ b/src/Artemis.UI/SkiaSharp/Vulkan/Win32VkContext.cs @@ -4,7 +4,7 @@ using System.Windows.Forms; using SharpVk; using SharpVk.Khronos; -namespace Artemis.UI.SkiaSharp +namespace Artemis.UI.SkiaSharp.Vulkan { internal sealed class Win32VkContext : VkContext { diff --git a/src/Artemis.UI/SkiaSharp/VulkanContext.cs b/src/Artemis.UI/SkiaSharp/VulkanContext.cs new file mode 100644 index 000000000..a9aee5dd0 --- /dev/null +++ b/src/Artemis.UI/SkiaSharp/VulkanContext.cs @@ -0,0 +1,65 @@ +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); + } + } + + /// + public void Dispose() + { + _vulkanBackendContext?.Dispose(); + _vulkanContext?.Dispose(); + GraphicsContext?.Dispose(); + } + + public GRContext GraphicsContext { get; } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/SkiaSharp/Win32Window.cs b/src/Artemis.UI/SkiaSharp/Win32Window.cs deleted file mode 100644 index c7dd0c48c..000000000 --- a/src/Artemis.UI/SkiaSharp/Win32Window.cs +++ /dev/null @@ -1,97 +0,0 @@ -using System; -using System.Runtime.InteropServices; - -namespace Artemis.UI.SkiaSharp -{ - internal class Win32Window : IDisposable - { - private ushort classRegistration; - - public string WindowClassName { get; } - - public IntPtr WindowHandle { get; private set; } - - public IntPtr DeviceContextHandle { get; private set; } - - public Win32Window(string className) - { - WindowClassName = className; - - var wc = new WNDCLASS - { - cbClsExtra = 0, - cbWndExtra = 0, - hbrBackground = IntPtr.Zero, - hCursor = User32.LoadCursor(IntPtr.Zero, (int)User32.IDC_ARROW), - hIcon = User32.LoadIcon(IntPtr.Zero, (IntPtr)User32.IDI_APPLICATION), - hInstance = Kernel32.CurrentModuleHandle, - lpfnWndProc = (WNDPROC)User32.DefWindowProc, - lpszClassName = WindowClassName, - lpszMenuName = null, - style = User32.CS_HREDRAW | User32.CS_VREDRAW | User32.CS_OWNDC - }; - - classRegistration = User32.RegisterClass(ref wc); - if (classRegistration == 0) - throw new Exception($"Could not register window class: {className}"); - - WindowHandle = User32.CreateWindow( - WindowClassName, - $"The Invisible Man ({className})", - User32.WindowStyles.WS_OVERLAPPEDWINDOW, - 0, 0, - 1, 1, - IntPtr.Zero, IntPtr.Zero, Kernel32.CurrentModuleHandle, IntPtr.Zero); - if (WindowHandle == IntPtr.Zero) - throw new Exception($"Could not create window: {className}"); - - DeviceContextHandle = User32.GetDC(WindowHandle); - if (DeviceContextHandle == IntPtr.Zero) - { - Dispose(); - throw new Exception($"Could not get device context: {className}"); - } - } - - public void Dispose() - { - if (WindowHandle != IntPtr.Zero) - { - if (DeviceContextHandle != IntPtr.Zero) - { - User32.ReleaseDC(WindowHandle, DeviceContextHandle); - DeviceContextHandle = IntPtr.Zero; - } - - User32.DestroyWindow(WindowHandle); - WindowHandle = IntPtr.Zero; - } - - if (classRegistration != 0) - { - User32.UnregisterClass(WindowClassName, Kernel32.CurrentModuleHandle); - classRegistration = 0; - } - } - - public delegate IntPtr WNDPROC(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); - - [StructLayout(LayoutKind.Sequential)] - public struct WNDCLASS - { - public uint style; - [MarshalAs(UnmanagedType.FunctionPtr)] - public WNDPROC lpfnWndProc; - public int cbClsExtra; - public int cbWndExtra; - public IntPtr hInstance; - public IntPtr hIcon; - public IntPtr hCursor; - public IntPtr hbrBackground; - [MarshalAs(UnmanagedType.LPTStr)] - public string lpszMenuName; - [MarshalAs(UnmanagedType.LPTStr)] - public string lpszClassName; - } - } -} From 97668ee932479df5aae1ca5998d4ff663b4d41e4 Mon Sep 17 00:00:00 2001 From: Robert Date: Mon, 22 Mar 2021 21:12:58 +0100 Subject: [PATCH 4/6] Rendering - Improved sampling performance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plugins - Added update/render time measuring Settings - Added a way to enable 60/144 FPS 🕵🏻‍♂️ --- src/Artemis.Core/Models/Profile/Layer.cs | 2 +- src/Artemis.Core/Plugins/Modules/Module.cs | 5 + .../Plugins/Modules/ProfileModule.cs | 4 + src/Artemis.Core/Plugins/PluginFeature.cs | 124 +++++++++++------- src/Artemis.Core/RGB.NET/SKTexture.cs | 58 +++++--- .../Settings/Debug/Tabs/RenderDebugView.xaml | 3 + .../Debug/Tabs/RenderDebugViewModel.cs | 11 +- .../General/GeneralSettingsTabViewModel.cs | 9 +- 8 files changed, 141 insertions(+), 75 deletions(-) diff --git a/src/Artemis.Core/Models/Profile/Layer.cs b/src/Artemis.Core/Models/Profile/Layer.cs index 14dda2b79..2208c7d0e 100644 --- a/src/Artemis.Core/Models/Profile/Layer.cs +++ b/src/Artemis.Core/Models/Profile/Layer.cs @@ -318,7 +318,7 @@ namespace Artemis.Core 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); 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..ed9201809 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,10 +166,12 @@ namespace Artemis.Core.Modules } ProfileUpdated(deltaTime); + StopUpdateMeasure(); } internal override void InternalRender(double deltaTime, SKCanvas canvas, SKImageInfo canvasInfo) { + StartRenderMeasure(); Render(deltaTime, canvas, canvasInfo); lock (_lock) @@ -178,6 +181,7 @@ namespace Artemis.Core.Modules } 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 cc0ebc4c9..77a540282 100644 --- a/src/Artemis.Core/RGB.NET/SKTexture.cs +++ b/src/Artemis.Core/RGB.NET/SKTexture.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.InteropServices; using Artemis.Core.SkiaSharp; using RGB.NET.Core; using RGB.NET.Presets.Textures.Sampler; @@ -11,34 +12,39 @@ namespace Artemis.Core /// public sealed class SKTexture : PixelTexture, IDisposable { - private SKPixmap? _pixelData; - private SKImage? _rasterImage; + private readonly SKPixmap _pixelData; + private readonly IntPtr _pixelDataPtr; #region Constructors - internal SKTexture(IManagedGraphicsContext? managedGraphicsContext, 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()) { ImageInfo = new SKImageInfo(width, height); - if (managedGraphicsContext == null) - Surface = SKSurface.Create(ImageInfo); - else - Surface = SKSurface.Create(managedGraphicsContext.GraphicsContext, true, ImageInfo); - RenderScale = renderScale; + 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(); - - _rasterImage?.Dispose(); - _pixelData?.Dispose(); - _rasterImage = skImage.ToRasterImage(); - _pixelData = _rasterImage.PeekPixels(); + skImage.ReadPixels(_pixelData); } /// @@ -64,7 +70,7 @@ namespace Artemis.Core /// /// Gets the color data in RGB format /// - protected override ReadOnlySpan Data => _pixelData != null ? _pixelData.GetPixelSpan() : ReadOnlySpan.Empty; + protected override ReadOnlySpan Data => _pixelData.GetPixelSpan(); /// /// Gets the render scale of the texture @@ -77,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() { Surface.Dispose(); - _pixelData?.Dispose(); + _pixelData.Dispose(); + + ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + + /// + ~SKTexture() + { + ReleaseUnmanagedResources(); } #endregion diff --git a/src/Artemis.UI/Screens/Settings/Debug/Tabs/RenderDebugView.xaml b/src/Artemis.UI/Screens/Settings/Debug/Tabs/RenderDebugView.xaml index 629db8ada..69169e4f8 100644 --- a/src/Artemis.UI/Screens/Settings/Debug/Tabs/RenderDebugView.xaml +++ b/src/Artemis.UI/Screens/Settings/Debug/Tabs/RenderDebugView.xaml @@ -35,6 +35,9 @@ + + + diff --git a/src/Artemis.UI/Screens/Settings/Debug/Tabs/RenderDebugViewModel.cs b/src/Artemis.UI/Screens/Settings/Debug/Tabs/RenderDebugViewModel.cs index 1d207cc88..1b515ccd9 100644 --- a/src/Artemis.UI/Screens/Settings/Debug/Tabs/RenderDebugViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Debug/Tabs/RenderDebugViewModel.cs @@ -20,6 +20,7 @@ namespace Artemis.UI.Screens.Settings.Debug.Tabs private int _renderWidth; private int _renderHeight; private string _frameTargetPath; + private string _renderer; public RenderDebugViewModel(ICoreService coreService) { @@ -51,6 +52,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"}; @@ -69,6 +76,8 @@ namespace Artemis.UI.Screens.Settings.Debug.Tabs { _coreService.FrameRendered += CoreServiceOnFrameRendered; _coreService.FrameRendering += CoreServiceOnFrameRendering; + + Renderer = Constants.ManagedGraphicsContext != null ? Constants.ManagedGraphicsContext.GetType().Name : "Software"; base.OnActivate(); } @@ -88,8 +97,6 @@ namespace Artemis.UI.Screens.Settings.Debug.Tabs RenderWidth = bitmapInfo.Width; // ReSharper disable twice CompareOfFloatsByEqualityOperator - - if (CurrentFrame is not WriteableBitmap writable || writable.Width != bitmapInfo.Width || writable.Height != bitmapInfo.Height) { CurrentFrame = e.Texture.Surface.Snapshot().ToWriteableBitmap(); diff --git a/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabViewModel.cs index 9193ba8ff..c8caa051e 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabViewModel.cs @@ -42,7 +42,9 @@ namespace Artemis.UI.Screens.Settings.Tabs.General IUpdateService updateService, IPluginManagementService pluginManagementService, IMessageService messageService, - IRegistrationService registrationService) + IRegistrationService registrationService, + ICoreService coreService + ) { DisplayName = "GENERAL"; @@ -64,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(); From c444ff4e593420d2561fc632db1012ba7d504071 Mon Sep 17 00:00:00 2001 From: Robert Date: Mon, 22 Mar 2021 21:00:37 +0100 Subject: [PATCH 5/6] Software rendering - Fixed memory leak Rendering - Swap back and forth between software and GPU without crash --- src/Artemis.Core/Models/Profile/Renderer.cs | 13 +- src/Artemis.Core/Services/RgbService.cs | 19 ++- src/Artemis.UI/Properties/launchSettings.json | 2 +- .../Settings/Debug/Tabs/RenderDebugView.xaml | 8 +- .../Debug/Tabs/RenderDebugViewModel.cs | 14 ++- .../Services/RegistrationService.cs | 4 +- .../SkiaSharp/Vulkan/Win32VkContext.cs | 119 +++++++++--------- src/Artemis.UI/SkiaSharp/VulkanContext.cs | 5 +- 8 files changed, 105 insertions(+), 79 deletions(-) diff --git a/src/Artemis.Core/Models/Profile/Renderer.cs b/src/Artemis.Core/Models/Profile/Renderer.cs index 05c2fd8e1..f8cd1025e 100644 --- a/src/Artemis.Core/Models/Profile/Renderer.cs +++ b/src/Artemis.Core/Models/Profile/Renderer.cs @@ -9,6 +9,7 @@ namespace Artemis.Core private bool _disposed; private SKRect _lastBounds; private SKRect _lastParentBounds; + private GRContext? _lastGraphicsContext; public SKSurface? Surface { get; private set; } public SKPaint? Paint { get; private set; } public SKPath? Path { get; private set; } @@ -27,7 +28,7 @@ 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 || Surface == null) @@ -41,7 +42,7 @@ namespace Artemis.Core Surface = SKSurface.Create(imageInfo); else Surface = SKSurface.Create(Constants.ManagedGraphicsContext.GraphicsContext, true, imageInfo); - + Path = new SKPath(path); Path.Transform(SKMatrix.CreateTranslation(pathBounds.Left * -1, pathBounds.Top * -1)); @@ -53,6 +54,7 @@ namespace Artemis.Core _lastParentBounds = parent?.Bounds ?? new SKRect(); _lastBounds = path.Bounds; + _lastGraphicsContext = Constants.ManagedGraphicsContext?.GraphicsContext; _valid = true; } @@ -70,7 +72,14 @@ namespace Artemis.Core throw new ObjectDisposedException("Renderer"); 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; diff --git a/src/Artemis.Core/Services/RgbService.cs b/src/Artemis.Core/Services/RgbService.cs index 7be1f8c17..40b502208 100644 --- a/src/Artemis.Core/Services/RgbService.cs +++ b/src/Artemis.Core/Services/RgbService.cs @@ -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,6 +122,11 @@ 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); @@ -195,11 +207,6 @@ namespace Artemis.Core.Services } } - private void RenderScaleSettingOnSettingChanged(object? sender, EventArgs e) - { - _texture?.Invalidate(); - } - public void Dispose() { Surface.UnregisterUpdateTrigger(UpdateTrigger); @@ -245,7 +252,7 @@ namespace Artemis.Core.Services 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 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 69169e4f8..4a5c4205d 100644 --- a/src/Artemis.UI/Screens/Settings/Debug/Tabs/RenderDebugView.xaml +++ b/src/Artemis.UI/Screens/Settings/Debug/Tabs/RenderDebugView.xaml @@ -32,11 +32,9 @@ - - - - - + + + diff --git a/src/Artemis.UI/Screens/Settings/Debug/Tabs/RenderDebugViewModel.cs b/src/Artemis.UI/Screens/Settings/Debug/Tabs/RenderDebugViewModel.cs index 1b515ccd9..6e0f9cdf2 100644 --- a/src/Artemis.UI/Screens/Settings/Debug/Tabs/RenderDebugViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Debug/Tabs/RenderDebugViewModel.cs @@ -21,6 +21,8 @@ namespace Artemis.UI.Screens.Settings.Debug.Tabs private int _renderHeight; private string _frameTargetPath; private string _renderer; + private int _frames; + private DateTime _frameCountStart; public RenderDebugViewModel(ICoreService coreService) { @@ -76,8 +78,6 @@ namespace Artemis.UI.Screens.Settings.Debug.Tabs { _coreService.FrameRendered += CoreServiceOnFrameRendered; _coreService.FrameRendering += CoreServiceOnFrameRendering; - - Renderer = Constants.ManagedGraphicsContext != null ? Constants.ManagedGraphicsContext.GetType().Name : "Software"; base.OnActivate(); } @@ -132,7 +132,15 @@ 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/Services/RegistrationService.cs b/src/Artemis.UI/Services/RegistrationService.cs index 6da26b430..9cbda99e6 100644 --- a/src/Artemis.UI/Services/RegistrationService.cs +++ b/src/Artemis.UI/Services/RegistrationService.cs @@ -28,6 +28,7 @@ namespace Artemis.UI.Services private bool _registeredBuiltInDataModelDisplays; private bool _registeredBuiltInDataModelInputs; private bool _registeredBuiltInPropertyEditors; + private VulkanContext _vulkanContext; public RegistrationService(ILogger logger, ICoreService coreService, @@ -128,7 +129,8 @@ namespace Artemis.UI.Services _rgbService.UpdateGraphicsContext(null); break; case "Vulkan": - _rgbService.UpdateGraphicsContext(new VulkanContext()); + _vulkanContext ??= new VulkanContext(); + _rgbService.UpdateGraphicsContext(_vulkanContext); break; default: throw new ArgumentOutOfRangeException(); diff --git a/src/Artemis.UI/SkiaSharp/Vulkan/Win32VkContext.cs b/src/Artemis.UI/SkiaSharp/Vulkan/Win32VkContext.cs index eff84d8ac..0bf695f5a 100644 --- a/src/Artemis.UI/SkiaSharp/Vulkan/Win32VkContext.cs +++ b/src/Artemis.UI/SkiaSharp/Vulkan/Win32VkContext.cs @@ -8,76 +8,79 @@ namespace Artemis.UI.SkiaSharp.Vulkan { internal sealed class Win32VkContext : VkContext { - private static readonly NativeWindow window = new NativeWindow(); + public NativeWindow Window { get; } - public Win32VkContext() - { - Instance = Instance.Create(null, new[] { "VK_KHR_surface", "VK_KHR_win32_surface" }); + 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); - PhysicalDevice = Instance.EnumeratePhysicalDevices().First(); + (GraphicsFamily, PresentFamily) = FindQueueFamilies(); - Surface = Instance.CreateWin32Surface(Kernel32.CurrentModuleHandle, window.Handle); + 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); - (GraphicsFamily, PresentFamily) = FindQueueFamilies(); + GetProc = (name, instanceHandle, deviceHandle) => + { + if (deviceHandle != IntPtr.Zero) + return Device.GetProcedureAddress(name); - DeviceQueueCreateInfo[]? queueInfos = new[] - { - new DeviceQueueCreateInfo { QueueFamilyIndex = GraphicsFamily, QueuePriorities = new[] { 1f } }, - new DeviceQueueCreateInfo { QueueFamilyIndex = PresentFamily, QueuePriorities = new[] { 1f } }, - }; - Device = PhysicalDevice.CreateDevice(queueInfos, null, null); + return Instance.GetProcedureAddress(name); + }; - GraphicsQueue = Device.GetQueue(GraphicsFamily, 0); + SharpVkGetProc = (name, instance, device) => + { + if (device != null) + return device.GetProcedureAddress(name); + if (instance != null) + return instance.GetProcedureAddress(name); - PresentQueue = Device.GetQueue(PresentFamily, 0); + // 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); + }; + } - GetProc = (name, instanceHandle, deviceHandle) => - { - if (deviceHandle != IntPtr.Zero) - return Device.GetProcedureAddress(name); + public override void Dispose() + { + base.Dispose(); + Window.DestroyHandle(); + } - return Instance.GetProcedureAddress(name); - }; + private (uint, uint) FindQueueFamilies() + { + QueueFamilyProperties[] queueFamilyProperties = PhysicalDevice.GetQueueFamilyProperties(); - SharpVkGetProc = (name, instance, device) => - { - if (device != null) - return device.GetProcedureAddress(name); - if (instance != null) - return instance.GetProcedureAddress(name); + var graphicsFamily = queueFamilyProperties + .Select((properties, index) => new {properties, index}) + .SkipWhile(pair => !pair.properties.QueueFlags.HasFlag(QueueFlags.Graphics)) + .FirstOrDefault(); - // 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); - }; - } + if (graphicsFamily == null) + throw new Exception("Unable to find graphics queue"); - private (uint, uint) FindQueueFamilies() - { - QueueFamilyProperties[]? queueFamilyProperties = PhysicalDevice.GetQueueFamilyProperties(); + uint? presentFamily = default; - var graphicsFamily = queueFamilyProperties - .Select((properties, index) => new { properties, index }) - .SkipWhile(pair => !pair.properties.QueueFlags.HasFlag(QueueFlags.Graphics)) - .FirstOrDefault(); + for (uint i = 0; i < queueFamilyProperties.Length; ++i) + { + if (PhysicalDevice.GetSurfaceSupport(i, Surface)) + presentFamily = i; + } - if (graphicsFamily == null) - throw new Exception("Unable to find graphics queue"); + if (!presentFamily.HasValue) + throw new Exception("Unable to find present 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); - } - } -} + 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 index a9aee5dd0..2c96684db 100644 --- a/src/Artemis.UI/SkiaSharp/VulkanContext.cs +++ b/src/Artemis.UI/SkiaSharp/VulkanContext.cs @@ -50,14 +50,13 @@ namespace Artemis.UI.SkiaSharp { throw new ArtemisGraphicsContextException("Failed to create Vulkan graphics context", e); } + + GraphicsContext.Flush(); } /// public void Dispose() { - _vulkanBackendContext?.Dispose(); - _vulkanContext?.Dispose(); - GraphicsContext?.Dispose(); } public GRContext GraphicsContext { get; } From fa49424cf6472cf83c18c1021ec5a42247fab62d Mon Sep 17 00:00:00 2001 From: Robert Date: Tue, 23 Mar 2021 00:06:58 +0100 Subject: [PATCH 6/6] Rendering - Made the way layering is rendered 99.5% less stupid --- .../Extensions/SKPaintExtensions.cs | 16 ++++ src/Artemis.Core/Models/Profile/Folder.cs | 28 +++---- src/Artemis.Core/Models/Profile/Layer.cs | 80 +++++++++---------- src/Artemis.Core/Models/Profile/Profile.cs | 4 +- .../Models/Profile/ProfileElement.cs | 2 +- .../Models/Profile/RenderProfileElement.cs | 4 - src/Artemis.Core/Models/Profile/Renderer.cs | 6 ++ .../Plugins/Modules/ProfileModule.cs | 2 +- src/Artemis.Core/Utilities/IntroAnimation.cs | 2 +- .../Debug/Tabs/RenderDebugViewModel.cs | 37 ++++----- src/Artemis.sln.DotSettings | 1 + 11 files changed, 99 insertions(+), 83 deletions(-) create mode 100644 src/Artemis.Core/Extensions/SKPaintExtensions.cs 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 0c8b909eb..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.Surface == 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.Surface.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.Surface.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.Surface.Canvas, rendererBounds, Renderer.Paint); - - canvas.DrawSurface(Renderer.Surface, 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 2208c7d0e..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,7 +312,7 @@ 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"); @@ -322,26 +321,31 @@ namespace Artemis.Core return; ApplyTimeline(timeline); - + if (LayerBrush?.BrushType != LayerBrushType.Regular) return; + SKPaint layerPaint = new(); try { canvas.Save(); - Renderer.Open(Path, Parent as Folder); - if (Renderer.Surface == null || Renderer.Path == null || Renderer.Paint == null) - throw new ArtemisCoreException("Failed to open layer render context"); + 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); + + 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) { @@ -356,54 +360,50 @@ namespace Artemis.Core if (LayerBrush.SupportsTransformation) { SKMatrix rotationMatrix = GetTransformMatrix(true, false, false, true); - Renderer.Surface.Canvas.SetMatrix(Renderer.Surface.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.Surface.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.Surface.Canvas.ClipPath(renderPath); - DelegateRendering(Renderer.Path.Bounds); + DelegateRendering(canvas, renderPath, layerBounds, layerPaint); } - - canvas.DrawSurface(Renderer.Surface, 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.Surface == 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.Surface.Canvas, bounds, Renderer.Paint); + baseLayerEffect.PreProcess(canvas, bounds, layerPaint); - LayerBrush.InternalRender(Renderer.Surface.Canvas, bounds, Renderer.Paint); + try + { + canvas.SaveLayer(layerPaint); - foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => e.Enabled)) - baseLayerEffect.PostProcess(Renderer.Surface.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 f8cd1025e..4de0fa2d5 100644 --- a/src/Artemis.Core/Models/Profile/Renderer.cs +++ b/src/Artemis.Core/Models/Profile/Renderer.cs @@ -108,5 +108,11 @@ namespace Artemis.Core _disposed = true; } + + ~Renderer() + { + if (IsOpen) + Close(); + } } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Modules/ProfileModule.cs b/src/Artemis.Core/Plugins/Modules/ProfileModule.cs index ed9201809..fb1a8ccad 100644 --- a/src/Artemis.Core/Plugins/Modules/ProfileModule.cs +++ b/src/Artemis.Core/Plugins/Modules/ProfileModule.cs @@ -177,7 +177,7 @@ namespace Artemis.Core.Modules lock (_lock) { // Render the profile - ActiveProfile?.Render(canvas); + ActiveProfile?.Render(canvas, SKPoint.Empty); } ProfileRendered(deltaTime, canvas, canvasInfo); 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/Screens/Settings/Debug/Tabs/RenderDebugViewModel.cs b/src/Artemis.UI/Screens/Settings/Debug/Tabs/RenderDebugViewModel.cs index 6e0f9cdf2..d7532cb2c 100644 --- a/src/Artemis.UI/Screens/Settings/Debug/Tabs/RenderDebugViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Debug/Tabs/RenderDebugViewModel.cs @@ -90,9 +90,24 @@ 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.ImageInfo; RenderHeight = bitmapInfo.Height; RenderWidth = bitmapInfo.Width; @@ -103,25 +118,10 @@ namespace Artemis.UI.Screens.Settings.Debug.Tabs return; } - using SKImage skImage = e.Texture.Surface.Snapshot(); - - 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); } @@ -140,6 +140,7 @@ namespace Artemis.UI.Screens.Settings.Debug.Tabs _frames = 0; _frameCountStart = DateTime.Now; } + _frames++; } } diff --git a/src/Artemis.sln.DotSettings b/src/Artemis.sln.DotSettings index ca870ba3a..3a843dade 100644 --- a/src/Artemis.sln.DotSettings +++ b/src/Artemis.sln.DotSettings @@ -225,6 +225,7 @@ True True True + True True