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