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