1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-13 05:48:35 +00:00

Merge branch 'vulkan' into development

This commit is contained in:
Robert 2021-03-23 19:56:16 +01:00
commit b3b7d6819d
33 changed files with 762 additions and 210 deletions

View File

@ -2,7 +2,9 @@
using System.Collections.Generic;
using System.IO;
using Artemis.Core.JsonConverters;
using Artemis.Core.Services;
using Artemis.Core.Services.Core;
using Artemis.Core.SkiaSharp;
using Newtonsoft.Json;
namespace Artemis.Core
@ -116,5 +118,11 @@ namespace Artemis.Core
typeof(double),
typeof(decimal)
};
/// <summary>
/// Gets the graphics context to be used for rendering by SkiaSharp. Can be set via
/// <see cref="IRgbService.UpdateGraphicsContext" />.
/// </summary>
public static IManagedGraphicsContext? ManagedGraphicsContext { get; internal set; }
}
}

View File

@ -0,0 +1,25 @@
using System;
namespace Artemis.Core
{
/// <summary>
/// Represents SkiaSharp graphics-context related errors
/// </summary>
public class ArtemisGraphicsContextException : Exception
{
/// <inheritdoc />
public ArtemisGraphicsContextException()
{
}
/// <inheritdoc />
public ArtemisGraphicsContextException(string message) : base(message)
{
}
/// <inheritdoc />
public ArtemisGraphicsContextException(string message, Exception innerException) : base(message, innerException)
{
}
}
}

View File

@ -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();
}
}
}

View File

@ -168,7 +168,7 @@ namespace Artemis.Core
#region Rendering
/// <inheritdoc />
public override void Render(SKCanvas canvas)
public override void Render(SKCanvas canvas, SKPoint basePosition)
{
if (Disposed)
throw new ObjectDisposedException("Folder");
@ -189,41 +189,38 @@ namespace Artemis.Core
baseLayerEffect.Update(Timeline.Delta.TotalSeconds);
}
SKPaint layerPaint = new();
try
{
canvas.Save();
Renderer.Open(Path, Parent as Folder);
if (Renderer.Canvas == null || Renderer.Path == null || Renderer.Paint == null)
throw new ArtemisCoreException("Failed to open folder render context");
SKRect rendererBounds = Renderer.Path.Bounds;
SKRect rendererBounds = SKRect.Create(0, 0, Path.Bounds.Width, Path.Bounds.Height);
foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => e.Enabled))
baseLayerEffect.PreProcess(Renderer.Canvas, rendererBounds, Renderer.Paint);
baseLayerEffect.PreProcess(canvas, rendererBounds, layerPaint);
canvas.SaveLayer(layerPaint);
canvas.Translate(Path.Bounds.Left - basePosition.X, Path.Bounds.Top - basePosition.Y);
// If required, apply the opacity override of the module to the root folder
if (IsRootFolder && Profile.Module.OpacityOverride < 1)
{
double multiplier = Easings.SineEaseInOut(Profile.Module.OpacityOverride);
Renderer.Paint.Color = Renderer.Paint.Color.WithAlpha((byte) (Renderer.Paint.Color.Alpha * multiplier));
layerPaint.Color = layerPaint.Color.WithAlpha((byte) (layerPaint.Color.Alpha * multiplier));
}
// No point rendering if the alpha was set to zero by one of the effects
if (Renderer.Paint.Color.Alpha == 0)
if (layerPaint.Color.Alpha == 0)
return;
// Iterate the children in reverse because the first layer must be rendered last to end up on top
for (int index = Children.Count - 1; index > -1; index--)
Children[index].Render(Renderer.Canvas);
Children[index].Render(canvas, new SKPoint(Path.Bounds.Left, Path.Bounds.Top));
foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => e.Enabled))
baseLayerEffect.PostProcess(Renderer.Canvas, rendererBounds, Renderer.Paint);
canvas.DrawBitmap(Renderer.Bitmap, Renderer.TargetLocation, Renderer.Paint);
baseLayerEffect.PostProcess(canvas, rendererBounds, layerPaint);
}
finally
{
canvas.Restore();
Renderer.Close();
layerPaint.DisposeSelfAndProperties();
}
Timeline.ClearDelta();
@ -239,7 +236,6 @@ namespace Artemis.Core
foreach (ProfileElement profileElement in Children)
profileElement.Dispose();
Renderer.Dispose();
base.Dispose(disposing);
}

View File

@ -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
}
/// <inheritdoc />
public override void Render(SKCanvas canvas)
public override void Render(SKCanvas canvas, SKPoint basePosition)
{
if (Disposed)
throw new ObjectDisposedException("Layer");
@ -290,9 +289,9 @@ namespace Artemis.Core
if (LayerBrush == null || LayerBrush?.BaseProperties?.PropertiesInitialized == false)
return;
RenderTimeline(Timeline, canvas);
RenderTimeline(Timeline, canvas, basePosition);
foreach (Timeline extraTimeline in Timeline.ExtraTimelines.ToList())
RenderTimeline(extraTimeline, canvas);
RenderTimeline(extraTimeline, canvas, basePosition);
Timeline.ClearDelta();
}
@ -313,36 +312,40 @@ namespace Artemis.Core
}
}
private void RenderTimeline(Timeline timeline, SKCanvas canvas)
private void RenderTimeline(Timeline timeline, SKCanvas canvas, SKPoint basePosition)
{
if (Path == null || LayerBrush == null)
throw new ArtemisCoreException("The layer is not yet ready for rendering");
if (timeline.IsFinished)
if (!Leds.Any() || timeline.IsFinished)
return;
ApplyTimeline(timeline);
if (LayerBrush?.BrushType != LayerBrushType.Regular)
return;
SKPaint layerPaint = new();
try
{
canvas.Save();
Renderer.Open(Path, Parent as Folder);
canvas.Translate(Path.Bounds.Left - basePosition.X, Path.Bounds.Top - basePosition.Y);
using SKPath clipPath = new(Path);
clipPath.Transform(SKMatrix.CreateTranslation(Path.Bounds.Left * -1, Path.Bounds.Top * -1));
canvas.ClipPath(clipPath);
if (Renderer.Canvas == null || Renderer.Path == null || Renderer.Paint == null)
throw new ArtemisCoreException("Failed to open layer render context");
SKRect layerBounds = SKRect.Create(0, 0, Path.Bounds.Width, Path.Bounds.Height);
// Apply blend mode and color
Renderer.Paint.BlendMode = General.BlendMode.CurrentValue;
Renderer.Paint.Color = new SKColor(0, 0, 0, (byte) (Transform.Opacity.CurrentValue * 2.55f));
layerPaint.BlendMode = General.BlendMode.CurrentValue;
layerPaint.Color = new SKColor(0, 0, 0, (byte) (Transform.Opacity.CurrentValue * 2.55f));
using SKPath renderPath = new();
if (General.ShapeType.CurrentValue == LayerShapeType.Rectangle)
renderPath.AddRect(Renderer.Path.Bounds);
renderPath.AddRect(layerBounds);
else
renderPath.AddOval(Renderer.Path.Bounds);
renderPath.AddOval(layerBounds);
if (General.TransformMode.CurrentValue == LayerTransformMode.Normal)
{
@ -357,54 +360,50 @@ namespace Artemis.Core
if (LayerBrush.SupportsTransformation)
{
SKMatrix rotationMatrix = GetTransformMatrix(true, false, false, true);
Renderer.Canvas.SetMatrix(Renderer.Canvas.TotalMatrix.PreConcat(rotationMatrix));
canvas.SetMatrix(canvas.TotalMatrix.PreConcat(rotationMatrix));
}
// If a brush is a bad boy and tries to color outside the lines, ensure that its clipped off
Renderer.Canvas.ClipPath(renderPath);
DelegateRendering(renderPath.Bounds);
DelegateRendering(canvas, renderPath, renderPath.Bounds, layerPaint);
}
else if (General.TransformMode.CurrentValue == LayerTransformMode.Clip)
{
SKMatrix renderPathMatrix = GetTransformMatrix(true, true, true, true);
renderPath.Transform(renderPathMatrix);
// If a brush is a bad boy and tries to color outside the lines, ensure that its clipped off
Renderer.Canvas.ClipPath(renderPath);
DelegateRendering(Renderer.Path.Bounds);
DelegateRendering(canvas, renderPath, layerBounds, layerPaint);
}
canvas.DrawBitmap(Renderer.Bitmap, Renderer.TargetLocation, Renderer.Paint);
}
finally
{
try
{
canvas.Restore();
}
catch
{
// ignored
}
Renderer.Close();
canvas.Restore();
layerPaint.DisposeSelfAndProperties();
}
}
private void DelegateRendering(SKRect bounds)
private void DelegateRendering(SKCanvas canvas, SKPath renderPath, SKRect bounds, SKPaint layerPaint)
{
if (LayerBrush == null)
throw new ArtemisCoreException("The layer is not yet ready for rendering");
if (Renderer.Canvas == null || Renderer.Paint == null)
throw new ArtemisCoreException("Failed to open layer render context");
foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => e.Enabled))
baseLayerEffect.PreProcess(Renderer.Canvas, bounds, Renderer.Paint);
baseLayerEffect.PreProcess(canvas, bounds, layerPaint);
LayerBrush.InternalRender(Renderer.Canvas, bounds, Renderer.Paint);
try
{
canvas.SaveLayer(layerPaint);
foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => e.Enabled))
baseLayerEffect.PostProcess(Renderer.Canvas, bounds, Renderer.Paint);
// If a brush is a bad boy and tries to color outside the lines, ensure that its clipped off
canvas.ClipPath(renderPath);
LayerBrush.InternalRender(canvas, bounds, layerPaint);
foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => e.Enabled))
baseLayerEffect.PostProcess(canvas, bounds, layerPaint);
}
finally
{
canvas.Restore();
}
}
internal void CalculateRenderProperties()

View File

@ -81,7 +81,7 @@ namespace Artemis.Core
}
/// <inheritdoc />
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);
}
}

View File

@ -104,7 +104,7 @@ namespace Artemis.Core
/// <summary>
/// Renders the element
/// </summary>
public abstract void Render(SKCanvas canvas);
public abstract void Render(SKCanvas canvas, SKPoint basePosition);
/// <summary>
/// Resets the internal state of the element

View File

@ -19,7 +19,6 @@ namespace Artemis.Core
internal RenderProfileElement(Profile profile) : base(profile)
{
Timeline = new Timeline();
Renderer = new Renderer();
ExpandedPropertyGroups = new List<string>();
LayerEffectsList = new List<BaseLayerEffect>();
@ -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

View File

@ -9,8 +9,8 @@ namespace Artemis.Core
private bool _disposed;
private SKRect _lastBounds;
private SKRect _lastParentBounds;
public SKBitmap? Bitmap { get; private set; }
public SKCanvas? Canvas { get; private set; }
private GRContext? _lastGraphicsContext;
public SKSurface? Surface { get; private set; }
public SKPaint? Paint { get; private set; }
public SKPath? Path { get; private set; }
public SKPoint TargetLocation { get; private set; }
@ -28,35 +28,40 @@ namespace Artemis.Core
if (IsOpen)
throw new ArtemisCoreException("Cannot open render context because it is already open");
if (path.Bounds != _lastBounds || (parent != null && parent.Bounds != _lastParentBounds))
if (path.Bounds != _lastBounds || (parent != null && parent.Bounds != _lastParentBounds) || _lastGraphicsContext != Constants.ManagedGraphicsContext?.GraphicsContext)
Invalidate();
if (!_valid || Canvas == null)
if (!_valid || Surface == null)
{
SKRect pathBounds = path.Bounds;
int width = (int) pathBounds.Width;
int height = (int) pathBounds.Height;
Bitmap = new SKBitmap(width, height);
SKImageInfo imageInfo = new(width, height);
if (Constants.ManagedGraphicsContext?.GraphicsContext == null)
Surface = SKSurface.Create(imageInfo);
else
Surface = SKSurface.Create(Constants.ManagedGraphicsContext.GraphicsContext, true, imageInfo);
Path = new SKPath(path);
Canvas = new SKCanvas(Bitmap);
Path.Transform(SKMatrix.CreateTranslation(pathBounds.Left * -1, pathBounds.Top * -1));
TargetLocation = new SKPoint(pathBounds.Location.X, pathBounds.Location.Y);
if (parent != null)
TargetLocation -= parent.Bounds.Location;
Canvas.ClipPath(Path);
Surface.Canvas.ClipPath(Path);
_lastParentBounds = parent?.Bounds ?? new SKRect();
_lastBounds = path.Bounds;
_lastGraphicsContext = Constants.ManagedGraphicsContext?.GraphicsContext;
_valid = true;
}
Paint = new SKPaint();
Canvas.Clear();
Canvas.Save();
Surface.Canvas.Clear();
Surface.Canvas.Save();
IsOpen = true;
}
@ -66,8 +71,15 @@ namespace Artemis.Core
if (_disposed)
throw new ObjectDisposedException("Renderer");
Canvas?.Restore();
Surface?.Canvas.Restore();
// Looks like every part of the paint needs to be disposed :(
Paint?.ColorFilter?.Dispose();
Paint?.ImageFilter?.Dispose();
Paint?.MaskFilter?.Dispose();
Paint?.PathEffect?.Dispose();
Paint?.Dispose();
Paint = null;
IsOpen = false;
@ -86,17 +98,21 @@ namespace Artemis.Core
if (IsOpen)
Close();
Canvas?.Dispose();
Surface?.Dispose();
Paint?.Dispose();
Path?.Dispose();
Bitmap?.Dispose();
Canvas = null;
Surface = null;
Paint = null;
Path = null;
Bitmap = null;
_disposed = true;
}
~Renderer()
{
if (IsOpen)
Close();
}
}
}

View File

@ -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)

View File

@ -150,6 +150,7 @@ namespace Artemis.Core.Modules
internal override void InternalUpdate(double deltaTime)
{
StartUpdateMeasure();
if (IsUpdateAllowed)
Update(deltaTime);
@ -165,19 +166,22 @@ namespace Artemis.Core.Modules
}
ProfileUpdated(deltaTime);
StopUpdateMeasure();
}
internal override void InternalRender(double deltaTime, SKCanvas canvas, SKImageInfo canvasInfo)
{
StartRenderMeasure();
Render(deltaTime, canvas, canvasInfo);
lock (_lock)
{
// Render the profile
ActiveProfile?.Render(canvas);
ActiveProfile?.Render(canvas, SKPoint.Empty);
}
ProfileRendered(deltaTime, canvas, canvasInfo);
StopRenderMeasure();
}
internal async Task ChangeActiveProfileAnimated(Profile? profile, IEnumerable<ArtemisDevice> devices)

View File

@ -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
/// </summary>
public abstract class PluginFeature : CorePropertyChanged, IDisposable
{
private readonly Stopwatch _renderStopwatch = new();
private readonly Stopwatch _updateStopwatch = new();
private bool _isEnabled;
private Exception? _loadException;
/// <summary>
/// Gets the plugin feature info related to this feature
/// </summary>
@ -46,6 +49,16 @@ namespace Artemis.Core
/// </summary>
public string Id => $"{GetType().FullName}-{Plugin.Guid.ToString().Substring(0, 8)}"; // Not as unique as a GUID but good enough and stays readable
/// <summary>
/// Gets the last measured update time of the feature
/// </summary>
public TimeSpan UpdateTime { get; private set; }
/// <summary>
/// Gets the last measured render time of the feature
/// </summary>
public TimeSpan RenderTime { get; private set; }
internal PluginFeatureEntity Entity { get; set; } = null!; // Will be set right after construction
/// <summary>
@ -58,6 +71,66 @@ namespace Artemis.Core
/// </summary>
public abstract void Disable();
/// <summary>
/// Occurs when the feature is enabled
/// </summary>
public event EventHandler? Enabled;
/// <summary>
/// Occurs when the feature is disabled
/// </summary>
public event EventHandler? Disabled;
/// <summary>
/// Releases the unmanaged resources used by the plugin feature and optionally releases the managed resources.
/// </summary>
/// <param name="disposing">
/// <see langword="true" /> to release both managed and unmanaged resources;
/// <see langword="false" /> to release only unmanaged resources.
/// </param>
protected virtual void Dispose(bool disposing)
{
if (disposing) InternalDisable();
}
/// <summary>
/// Triggers the Enabled event
/// </summary>
protected virtual void OnEnabled()
{
Enabled?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Triggers the Disabled event
/// </summary>
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
/// <summary>
/// Releases the unmanaged resources used by the plugin feature and optionally releases the managed resources.
/// </summary>
/// <param name="disposing">
/// <see langword="true" /> to release both managed and unmanaged resources;
/// <see langword="false" /> to release only unmanaged resources.
/// </param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
InternalDisable();
}
}
#endregion
/// <inheritdoc />
public void Dispose()
{
@ -187,35 +241,5 @@ namespace Artemis.Core
}
#endregion
#region Events
/// <summary>
/// Occurs when the feature is enabled
/// </summary>
public event EventHandler? Enabled;
/// <summary>
/// Occurs when the feature is disabled
/// </summary>
public event EventHandler? Disabled;
/// <summary>
/// Triggers the Enabled event
/// </summary>
protected virtual void OnEnabled()
{
Enabled?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Triggers the Disabled event
/// </summary>
protected virtual void OnDisabled()
{
Disabled?.Invoke(this, EventArgs.Empty);
}
#endregion
}
}

View File

@ -1,4 +1,6 @@
using System;
using System.Runtime.InteropServices;
using Artemis.Core.SkiaSharp;
using RGB.NET.Core;
using RGB.NET.Presets.Textures.Sampler;
using SkiaSharp;
@ -10,25 +12,45 @@ namespace Artemis.Core
/// </summary>
public sealed class SKTexture : PixelTexture<byte>, IDisposable
{
private bool _disposed;
private readonly SKPixmap _pixelData;
private readonly IntPtr _pixelDataPtr;
#region Constructors
internal SKTexture(int width, int height, float renderScale)
: base(width, height, 4, new AverageByteSampler())
internal SKTexture(IManagedGraphicsContext? graphicsContext, int width, int height, float scale) : base(width, height, 4, new AverageByteSampler())
{
Bitmap = new SKBitmap(new SKImageInfo(width, height, SKColorType.Rgb888x));
RenderScale = renderScale;
ImageInfo = new SKImageInfo(width, height);
Surface = graphicsContext == null
? SKSurface.Create(ImageInfo)
: SKSurface.Create(graphicsContext.GraphicsContext, true, ImageInfo);
RenderScale = scale;
_pixelDataPtr = Marshal.AllocHGlobal(ImageInfo.BytesSize);
_pixelData = new SKPixmap(ImageInfo, _pixelDataPtr, ImageInfo.RowBytes);
}
#endregion
#region Methods
/// <summary>
/// Invalidates the texture
/// </summary>
public void Invalidate()
{
IsInvalid = true;
}
internal void CopyPixelData()
{
using SKImage skImage = Surface.Snapshot();
skImage.ReadPixels(_pixelData);
}
/// <inheritdoc />
protected override Color GetColor(in ReadOnlySpan<byte> pixel)
{
return new(pixel[0], pixel[1], pixel[2]);
return new(pixel[2], pixel[1], pixel[0]);
}
#endregion
@ -38,12 +60,17 @@ namespace Artemis.Core
/// <summary>
/// Gets the SKBitmap backing this texture
/// </summary>
public SKBitmap Bitmap { get; }
public SKSurface Surface { get; }
/// <summary>
/// Gets the image info used to create the <see cref="Surface" />
/// </summary>
public SKImageInfo ImageInfo { get; }
/// <summary>
/// Gets the color data in RGB format
/// </summary>
protected override ReadOnlySpan<byte> Data => _disposed ? new ReadOnlySpan<byte>() : Bitmap.GetPixelSpan();
protected override ReadOnlySpan<byte> Data => _pixelData.GetPixelSpan();
/// <summary>
/// Gets the render scale of the texture
@ -56,19 +83,29 @@ namespace Artemis.Core
/// </summary>
public bool IsInvalid { get; private set; }
/// <summary>
/// Invalidates the texture
/// </summary>
public void Invalidate()
#endregion
#region IDisposable
private void ReleaseUnmanagedResources()
{
IsInvalid = true;
Marshal.FreeHGlobal(_pixelDataPtr);
}
/// <inheritdoc />
public void Dispose()
{
_disposed = true;
Bitmap.Dispose();
Surface.Dispose();
_pixelData.Dispose();
ReleaseUnmanagedResources();
GC.SuppressFinalize(this);
}
/// <inheritdoc />
~SKTexture()
{
ReleaseUnmanagedResources();
}
#endregion

View File

@ -59,6 +59,7 @@ namespace Artemis.Core.Services
UpdatePluginCache();
_rgbService.IsRenderPaused = true;
_rgbService.Surface.Updating += SurfaceOnUpdating;
_loggingLevel.SettingChanged += (sender, args) => ApplyLoggingLevel();
@ -139,21 +140,23 @@ namespace Artemis.Core.Services
module.InternalUpdate(args.DeltaTime);
// Render all active modules
SKTexture texture =_rgbService.OpenRender();
SKTexture texture = _rgbService.OpenRender();
using (SKCanvas canvas = new(texture.Bitmap))
SKCanvas canvas = texture.Surface.Canvas;
canvas.Save();
canvas.Scale(texture.RenderScale);
canvas.Clear(new SKColor(0, 0, 0));
// While non-activated modules may be updated above if they expand the main data model, they may never render
if (!ModuleRenderingDisabled)
{
canvas.Scale(texture.RenderScale);
canvas.Clear(new SKColor(0, 0, 0));
// While non-activated modules may be updated above if they expand the main data model, they may never render
if (!ModuleRenderingDisabled)
{
foreach (Module module in modules.Where(m => m.IsActivated))
module.InternalRender(args.DeltaTime, canvas, texture.Bitmap.Info);
}
OnFrameRendering(new FrameRenderingEventArgs(canvas, args.DeltaTime, _rgbService.Surface));
foreach (Module module in modules.Where(m => m.IsActivated))
module.InternalRender(args.DeltaTime, canvas, texture.ImageInfo);
}
OnFrameRendering(new FrameRenderingEventArgs(canvas, args.DeltaTime, _rgbService.Surface));
canvas.RestoreToCount(-1);
canvas.Flush();
OnFrameRendered(new FrameRenderedEventArgs(texture, _rgbService.Surface));
}
@ -163,9 +166,9 @@ namespace Artemis.Core.Services
}
finally
{
_rgbService.CloseRender();
_frameStopWatch.Stop();
FrameTime = _frameStopWatch.Elapsed;
_rgbService.CloseRender();
LogUpdateExceptions();
}
@ -240,6 +243,7 @@ namespace Artemis.Core.Services
IsElevated
);
_rgbService.IsRenderPaused = false;
OnInitialized();
}

View File

@ -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
/// </summary>
void CloseRender();
/// <summary>
/// Updates the graphics context to the provided <paramref name="managedGraphicsContext"></paramref>.
/// <para>Note: The old graphics context will be used until the next frame starts rendering and is disposed afterwards.</para>
/// </summary>
/// <param name="managedGraphicsContext">
/// The new managed graphics context. If <see langword="null" />, software rendering
/// is used.
/// </param>
void UpdateGraphicsContext(IManagedGraphicsContext? managedGraphicsContext);
/// <summary>
/// Adds the given device provider to the <see cref="Surface" />
/// </summary>

View File

@ -4,11 +4,11 @@ using System.Collections.ObjectModel;
using System.Linq;
using Artemis.Core.DeviceProviders;
using Artemis.Core.Services.Models;
using Artemis.Core.SkiaSharp;
using Artemis.Storage.Entities.Surface;
using Artemis.Storage.Repositories.Interfaces;
using RGB.NET.Core;
using Serilog;
using SkiaSharp;
namespace Artemis.Core.Services
{
@ -51,6 +51,8 @@ namespace Artemis.Core.Services
UpdateTrigger = new TimerUpdateTrigger {UpdateFrequency = 1.0 / _targetFrameRateSetting.Value};
Surface.RegisterUpdateTrigger(UpdateTrigger);
Utilities.ShutdownRequested += UtilitiesOnShutdownRequested;
}
public TimerUpdateTrigger UpdateTrigger { get; }
@ -66,6 +68,11 @@ namespace Artemis.Core.Services
_texture?.Invalidate();
}
private void UtilitiesOnShutdownRequested(object? sender, EventArgs e)
{
IsRenderPaused = true;
}
private void SurfaceOnLayoutChanged(SurfaceLayoutChangedEventArgs args)
{
UpdateLedGroup();
@ -115,13 +122,16 @@ namespace Artemis.Core.Services
DeviceAdded?.Invoke(this, e);
}
private void RenderScaleSettingOnSettingChanged(object? sender, EventArgs e)
{
_texture?.Invalidate();
}
public IReadOnlyCollection<ArtemisDevice> EnabledDevices => _enabledDevices.AsReadOnly();
public IReadOnlyCollection<ArtemisDevice> Devices => _devices.AsReadOnly();
public IReadOnlyDictionary<Led, ArtemisLed> LedMap => new ReadOnlyDictionary<Led, ArtemisLed>(_ledMap);
/// <inheritdoc />
public RGBSurface Surface { get; set; }
public bool IsRenderPaused { get; set; }
public bool RenderOpen { get; private set; }
@ -197,11 +207,6 @@ namespace Artemis.Core.Services
}
}
private void RenderScaleSettingOnSettingChanged(object? sender, EventArgs e)
{
_texture?.Invalidate();
}
public void Dispose()
{
Surface.UnregisterUpdateTrigger(UpdateTrigger);
@ -216,6 +221,8 @@ namespace Artemis.Core.Services
#region Rendering
private IManagedGraphicsContext? _newGraphicsContext;
public SKTexture OpenRender()
{
if (RenderOpen)
@ -234,6 +241,7 @@ namespace Artemis.Core.Services
throw new ArtemisCoreException("Render pipeline is already closed");
RenderOpen = false;
_texture?.CopyPixelData();
}
public void CreateTexture()
@ -241,15 +249,38 @@ namespace Artemis.Core.Services
if (RenderOpen)
throw new ArtemisCoreException("Cannot update the texture while rendering");
SKTexture? oldTexture = _texture;
IManagedGraphicsContext? graphicsContext = Constants.ManagedGraphicsContext = _newGraphicsContext;
if (!ReferenceEquals(graphicsContext, _newGraphicsContext))
graphicsContext = _newGraphicsContext;
if (graphicsContext != null)
_logger.Debug("Creating SKTexture with graphics context {graphicsContext}", graphicsContext.GetType().Name);
else
_logger.Debug("Creating SKTexture with software-based graphics context");
float renderScale = (float) _renderScaleSetting.Value;
int width = Math.Max(1, MathF.Min(Surface.Boundary.Size.Width * renderScale, 4096).RoundToInt());
int height = Math.Max(1, MathF.Min(Surface.Boundary.Size.Height * renderScale, 4096).RoundToInt());
_texture = new SKTexture(width, height, renderScale);
_texture?.Dispose();
_texture = new SKTexture(graphicsContext, width, height, renderScale);
_textureBrush.Texture = _texture;
oldTexture?.Dispose();
if (!ReferenceEquals(_newGraphicsContext, Constants.ManagedGraphicsContext = _newGraphicsContext))
{
Constants.ManagedGraphicsContext?.Dispose();
Constants.ManagedGraphicsContext = _newGraphicsContext;
_newGraphicsContext = null;
}
}
public void UpdateGraphicsContext(IManagedGraphicsContext? managedGraphicsContext)
{
if (ReferenceEquals(managedGraphicsContext, Constants.ManagedGraphicsContext))
return;
_newGraphicsContext = managedGraphicsContext;
_texture?.Invalidate();
}
#endregion

View File

@ -0,0 +1,16 @@
using System;
using SkiaSharp;
namespace Artemis.Core.SkiaSharp
{
/// <summary>
/// Represents a managed wrapper around a SkiaSharp context
/// </summary>
public interface IManagedGraphicsContext : IDisposable
{
/// <summary>
/// Gets the graphics context created by this wrapper
/// </summary>
GRContext GraphicsContext { get; }
}
}

View File

@ -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()

View File

@ -149,6 +149,7 @@
<PackageReference Include="RawInput.Sharp" Version="0.0.3" />
<PackageReference Include="Serilog" Version="2.10.0" />
<PackageReference Include="SkiaSharp.Views.WPF" Version="2.80.2" />
<PackageReference Include="SkiaSharp.Vulkan.SharpVk" Version="2.80.2" />
<PackageReference Include="Stylet" Version="1.3.5" />
<PackageReference Include="System.Buffers" Version="4.5.1" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />

View File

@ -6,6 +6,7 @@ using System.Threading.Tasks;
using System.Windows;
using System.Windows.Markup;
using System.Windows.Threading;
using Artemis.Core;
using Artemis.Core.Ninject;
using Artemis.Core.Services;
using Artemis.UI.Ninject;
@ -13,9 +14,11 @@ using Artemis.UI.Screens;
using Artemis.UI.Services;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
using Artemis.UI.SkiaSharp;
using Artemis.UI.Stylet;
using Ninject;
using Serilog;
using SkiaSharp;
using Stylet;
namespace Artemis.UI
@ -65,7 +68,7 @@ namespace Artemis.UI
FrameworkElement.LanguageProperty.OverrideMetadata(typeof(FrameworkElement), new FrameworkPropertyMetadata(XmlLanguage.GetLanguage(CultureInfo.CurrentCulture.IetfLanguageTag)));
// Create and bind the root view, this is a tray icon so don't show it with the window manager
Execute.OnUIThread(() =>
Execute.OnUIThreadSync(() =>
{
UIElement view = viewManager.CreateAndBindViewForModelIfNecessary(RootViewModel);
((TrayViewModel) RootViewModel).SetTaskbarIcon(view);
@ -90,7 +93,12 @@ namespace Artemis.UI
IRegistrationService registrationService = Kernel.Get<IRegistrationService>();
registrationService.RegisterInputProvider();
registrationService.RegisterControllers();
Execute.OnUIThreadSync(() =>
{
registrationService.ApplyPreferredGraphicsContext();
});
// Initialize background services
Kernel.Get<IDeviceLayoutService>();
}

View File

@ -0,0 +1,22 @@
using System;
namespace Artemis.UI.Exceptions
{
public class ArtemisGraphicsContextException : Exception
{
/// <inheritdoc />
public ArtemisGraphicsContextException()
{
}
/// <inheritdoc />
public ArtemisGraphicsContextException(string message) : base(message)
{
}
/// <inheritdoc />
public ArtemisGraphicsContextException(string message, Exception innerException) : base(message, innerException)
{
}
}
}

View File

@ -2,7 +2,7 @@
"profiles": {
"Artemis.UI": {
"commandName": "Project",
"commandLineArgs": "--force-elevation"
"commandLineArgs": "--force-elevation --pcmr"
}
}
}

View File

@ -32,9 +32,10 @@
<TextBlock Grid.Column="1" HorizontalAlignment="Right" Margin="0,0,5,0">
<Run Text="FPS: " />
<Run FontWeight="Bold" Text="{Binding CurrentFps}" />
<Run Text=" at " />
<Run Text="{Binding RenderWidth}" /><Run Text="x" />
<Run Text="{Binding RenderHeight}" />
<Run Text="at" />
<Run Text="{Binding RenderWidth}" /><Run Text="x" /><Run Text="{Binding RenderHeight}" />
<Run Text="- Renderer:"></Run>
<Run Text="{Binding Renderer}"></Run>
</TextBlock>
</Grid>

View File

@ -20,6 +20,9 @@ namespace Artemis.UI.Screens.Settings.Debug.Tabs
private int _renderWidth;
private int _renderHeight;
private string _frameTargetPath;
private string _renderer;
private int _frames;
private DateTime _frameCountStart;
public RenderDebugViewModel(ICoreService coreService)
{
@ -51,6 +54,12 @@ namespace Artemis.UI.Screens.Settings.Debug.Tabs
set => SetAndNotify(ref _renderHeight, value);
}
public string Renderer
{
get => _renderer;
set => SetAndNotify(ref _renderer, value);
}
public void SaveFrame()
{
VistaSaveFileDialog dialog = new VistaSaveFileDialog {Filter = "Portable network graphic (*.png)|*.png", Title = "Save render frame"};
@ -81,38 +90,38 @@ namespace Artemis.UI.Screens.Settings.Debug.Tabs
private void CoreServiceOnFrameRendered(object sender, FrameRenderedEventArgs e)
{
using SKImage skImage = e.Texture.Surface.Snapshot();
SKImageInfo bitmapInfo = e.Texture.ImageInfo;
if (_frameTargetPath != null)
{
using (SKData data = skImage.Encode(SKEncodedImageFormat.Png, 100))
{
using (FileStream stream = File.OpenWrite(_frameTargetPath))
{
data.SaveTo(stream);
}
}
_frameTargetPath = null;
}
Execute.OnUIThreadSync(() =>
{
SKImageInfo bitmapInfo = e.Texture.Bitmap.Info;
RenderHeight = bitmapInfo.Height;
RenderWidth = bitmapInfo.Width;
// ReSharper disable twice CompareOfFloatsByEqualityOperator
if (CurrentFrame is not WriteableBitmap writable || writable.Width != bitmapInfo.Width || writable.Height != bitmapInfo.Height)
{
CurrentFrame = e.Texture.Bitmap.ToWriteableBitmap();
CurrentFrame = e.Texture.Surface.Snapshot().ToWriteableBitmap();
return;
}
using SKImage skImage = SKImage.FromPixels(e.Texture.Bitmap.PeekPixels());
if (_frameTargetPath != null)
{
using (SKData data = skImage.Encode(SKEncodedImageFormat.Png, 100))
{
using (FileStream stream = File.OpenWrite(_frameTargetPath))
{
data.SaveTo(stream);
}
}
_frameTargetPath = null;
}
SKImageInfo info = new(skImage.Width, skImage.Height);
writable.Lock();
using (SKPixmap pixmap = new(info, writable.BackBuffer, writable.BackBufferStride))
using (SKPixmap pixmap = new(bitmapInfo, writable.BackBuffer, writable.BackBufferStride))
{
// ReSharper disable once AccessToDisposedClosure - Looks fine
skImage.ReadPixels(pixmap, 0, 0);
}
@ -123,7 +132,16 @@ namespace Artemis.UI.Screens.Settings.Debug.Tabs
private void CoreServiceOnFrameRendering(object sender, FrameRenderingEventArgs e)
{
CurrentFps = Math.Round(1.0 / e.DeltaTime, 2);
if (DateTime.Now - _frameCountStart >= TimeSpan.FromSeconds(1))
{
CurrentFps = _frames;
Renderer = Constants.ManagedGraphicsContext != null ? Constants.ManagedGraphicsContext.GetType().Name : "Software";
_frames = 0;
_frameCountStart = DateTime.Now;
}
_frames++;
}
}
}

View File

@ -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 @@
<TextBlock Style="{StaticResource MaterialDesignHeadline5TextBlock}" Margin="0 15">Rendering</TextBlock>
<materialDesign:Card materialDesign:ShadowAssist.ShadowDepth="Depth1" VerticalAlignment="Stretch" Margin="0,0,5,0">
<StackPanel Margin="15">
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<TextBlock Style="{StaticResource MaterialDesignTextBlock}">Preferred render method</TextBlock>
<TextBlock Style="{StaticResource MaterialDesignTextBlock}" Foreground="{DynamicResource MaterialDesignNavigationItemSubheader}" TextWrapping="Wrap">
Software-based rendering is done purely on the CPU while Vulkan uses GPU-acceleration
</TextBlock>
</StackPanel>
<StackPanel Grid.Row="0" Grid.Column="1" VerticalAlignment="Center">
<ComboBox Width="80" SelectedItem="{Binding PreferredGraphicsContext}" >
<system:String>Software</system:String>
<system:String>Vulkan</system:String>
</ComboBox>
</StackPanel>
</Grid>
<Separator Style="{StaticResource MaterialDesignSeparator}" Margin="-15 5" />
<Grid>
<Grid.RowDefinitions>
<RowDefinition />

View File

@ -3,15 +3,10 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection.Metadata;
using System.Security.Principal;
using System.Threading.Tasks;
using System.Xml.Linq;
using Artemis.Core;
using Artemis.Core.LayerBrushes;
using Artemis.Core.Services;
using Artemis.Core.Services.Core;
using Artemis.UI.Properties;
using Artemis.UI.Screens.StartupWizard;
using Artemis.UI.Services;
using Artemis.UI.Shared;
@ -30,6 +25,7 @@ namespace Artemis.UI.Screens.Settings.Tabs.General
private readonly IDialogService _dialogService;
private readonly IKernel _kernel;
private readonly IMessageService _messageService;
private readonly IRegistrationService _registrationService;
private readonly ISettingsService _settingsService;
private readonly IUpdateService _updateService;
private readonly IWindowManager _windowManager;
@ -45,7 +41,10 @@ namespace Artemis.UI.Screens.Settings.Tabs.General
ISettingsService settingsService,
IUpdateService updateService,
IPluginManagementService pluginManagementService,
IMessageService messageService)
IMessageService messageService,
IRegistrationService registrationService,
ICoreService coreService
)
{
DisplayName = "GENERAL";
@ -56,6 +55,7 @@ namespace Artemis.UI.Screens.Settings.Tabs.General
_settingsService = settingsService;
_updateService = updateService;
_messageService = messageService;
_registrationService = registrationService;
LogLevels = new BindableCollection<ValueDescription>(EnumUtilities.GetAllValuesAndDescriptions(typeof(LogEventLevel)));
ColorSchemes = new BindableCollection<ValueDescription>(EnumUtilities.GetAllValuesAndDescriptions(typeof(ApplicationColorScheme)));
@ -66,6 +66,11 @@ namespace Artemis.UI.Screens.Settings.Tabs.General
TargetFrameRates = new List<Tuple<string, int>>();
for (int i = 10; i <= 30; i += 5)
TargetFrameRates.Add(new Tuple<string, int>(i + " FPS", i));
if (coreService.StartupArguments.Contains("--pcmr"))
{
TargetFrameRates.Add(new Tuple<string, int>("60 FPS (lol)", 60));
TargetFrameRates.Add(new Tuple<string, int>("144 FPS (omegalol)", 144));
}
List<LayerBrushProvider> layerBrushProviders = pluginManagementService.GetFeaturesOfType<LayerBrushProvider>();
@ -209,6 +214,17 @@ namespace Artemis.UI.Screens.Settings.Tabs.General
}
}
public string PreferredGraphicsContext
{
get => _settingsService.GetSetting("Core.PreferredGraphicsContext", "Vulkan").Value;
set
{
_settingsService.GetSetting("Core.PreferredGraphicsContext", "Vulkan").Value = value;
_settingsService.GetSetting("Core.PreferredGraphicsContext", "Vulkan").Save();
_registrationService.ApplyPreferredGraphicsContext();
}
}
public double RenderScale
{
get => _settingsService.GetSetting("Core.RenderScale", 0.5).Value;
@ -316,10 +332,7 @@ namespace Artemis.UI.Screens.Settings.Tabs.General
try
{
bool taskCreated = false;
if (!recreate)
{
taskCreated = SettingsUtilities.IsAutoRunTaskCreated();
}
if (!recreate) taskCreated = SettingsUtilities.IsAutoRunTaskCreated();
if (StartWithWindows && !taskCreated)
SettingsUtilities.CreateAutoRunTask(TimeSpan.FromSeconds(AutoRunDelay));
@ -335,7 +348,6 @@ namespace Artemis.UI.Screens.Settings.Tabs.General
}
public enum ApplicationColorScheme
{
Light,

View File

@ -1,4 +1,5 @@
using System.Linq;
using System;
using System.Linq;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Controllers;
@ -8,6 +9,7 @@ using Artemis.UI.DefaultTypes.PropertyInput;
using Artemis.UI.InputProviders;
using Artemis.UI.Ninject;
using Artemis.UI.Shared.Services;
using Artemis.UI.SkiaSharp;
using Serilog;
namespace Artemis.UI.Services
@ -15,28 +17,38 @@ namespace Artemis.UI.Services
public class RegistrationService : IRegistrationService
{
private readonly ILogger _logger;
private readonly ICoreService _coreService;
private readonly IDataModelUIService _dataModelUIService;
private readonly IProfileEditorService _profileEditorService;
private readonly IPluginManagementService _pluginManagementService;
private readonly IInputService _inputService;
private readonly IWebServerService _webServerService;
private readonly IRgbService _rgbService;
private readonly ISettingsService _settingsService;
private bool _registeredBuiltInDataModelDisplays;
private bool _registeredBuiltInDataModelInputs;
private bool _registeredBuiltInPropertyEditors;
private VulkanContext _vulkanContext;
public RegistrationService(ILogger logger,
ICoreService coreService,
IDataModelUIService dataModelUIService,
IProfileEditorService profileEditorService,
IPluginManagementService pluginManagementService,
IInputService inputService,
IWebServerService webServerService)
IWebServerService webServerService,
IRgbService rgbService,
ISettingsService settingsService)
{
_logger = logger;
_coreService = coreService;
_dataModelUIService = dataModelUIService;
_profileEditorService = profileEditorService;
_pluginManagementService = pluginManagementService;
_inputService = inputService;
_webServerService = webServerService;
_rgbService = rgbService;
_settingsService = settingsService;
LoadPluginModules();
pluginManagementService.PluginEnabling += PluginServiceOnPluginEnabling;
@ -97,6 +109,40 @@ namespace Artemis.UI.Services
_webServerService.AddController<RemoteController>();
}
/// <inheritdoc />
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<string> preferredGraphicsContext = _settingsService.GetSetting("Core.PreferredGraphicsContext", "Vulkan");
try
{
switch (preferredGraphicsContext.Value)
{
case "Software":
_rgbService.UpdateGraphicsContext(null);
break;
case "Vulkan":
_vulkanContext ??= new VulkanContext();
_rgbService.UpdateGraphicsContext(_vulkanContext);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
catch (Exception e)
{
_logger.Warning(e, "Failed to apply preferred graphics context {preferred}", preferredGraphicsContext.Value);
_rgbService.UpdateGraphicsContext(null);
}
}
private void PluginServiceOnPluginEnabling(object sender, PluginEventArgs e)
{
e.Plugin.Kernel.Load(new[] {new PluginUIModule(e.Plugin)});
@ -116,5 +162,6 @@ namespace Artemis.UI.Services
void RegisterBuiltInPropertyEditors();
void RegisterInputProvider();
void RegisterControllers();
void ApplyPreferredGraphicsContext();
}
}

View File

@ -0,0 +1,24 @@
using System;
using System.Runtime.InteropServices;
namespace Artemis.UI.SkiaSharp.Vulkan
{
internal class Kernel32
{
private const string kernel32 = "kernel32.dll";
static Kernel32()
{
CurrentModuleHandle = Kernel32.GetModuleHandle(null);
if (CurrentModuleHandle == IntPtr.Zero)
{
throw new Exception("Could not get module handle.");
}
}
public static IntPtr CurrentModuleHandle { get; }
[DllImport(kernel32, CallingConvention = CallingConvention.Winapi, BestFitMapping = false, ThrowOnUnmappableChar = true)]
public static extern IntPtr GetModuleHandle([MarshalAs(UnmanagedType.LPTStr)] string lpModuleName);
}
}

View File

@ -0,0 +1,35 @@
using System;
using SharpVk.Khronos;
using SkiaSharp;
using Device = SharpVk.Device;
using Instance = SharpVk.Instance;
using PhysicalDevice = SharpVk.PhysicalDevice;
using Queue = SharpVk.Queue;
namespace Artemis.UI.SkiaSharp.Vulkan
{
internal class VkContext : IDisposable
{
public virtual Instance Instance { get; protected set; }
public virtual PhysicalDevice PhysicalDevice { get; protected set; }
public virtual Surface Surface { get; protected set; }
public virtual Device Device { get; protected set; }
public virtual Queue GraphicsQueue { get; protected set; }
public virtual Queue PresentQueue { get; protected set; }
public virtual uint GraphicsFamily { get; protected set; }
public virtual uint PresentFamily { get; protected set; }
public virtual GRVkGetProcedureAddressDelegate GetProc { get; protected set; }
public virtual GRSharpVkGetProcedureAddressDelegate SharpVkGetProc { get; protected set; }
public virtual void Dispose() => Instance?.Dispose();
}
}

View File

@ -0,0 +1,86 @@
using System;
using System.Linq;
using System.Windows.Forms;
using SharpVk;
using SharpVk.Khronos;
namespace Artemis.UI.SkiaSharp.Vulkan
{
internal sealed class Win32VkContext : VkContext
{
public NativeWindow Window { get; }
public Win32VkContext()
{
Window = new NativeWindow();
Instance = Instance.Create(null, new[] {"VK_KHR_surface", "VK_KHR_win32_surface"});
PhysicalDevice = Instance.EnumeratePhysicalDevices().First();
Surface = Instance.CreateWin32Surface(Kernel32.CurrentModuleHandle, Window.Handle);
(GraphicsFamily, PresentFamily) = FindQueueFamilies();
DeviceQueueCreateInfo[] queueInfos =
{
new() {QueueFamilyIndex = GraphicsFamily, QueuePriorities = new[] {1f}},
new() {QueueFamilyIndex = PresentFamily, QueuePriorities = new[] {1f}}
};
Device = PhysicalDevice.CreateDevice(queueInfos, null, null);
GraphicsQueue = Device.GetQueue(GraphicsFamily, 0);
PresentQueue = Device.GetQueue(PresentFamily, 0);
GetProc = (name, instanceHandle, deviceHandle) =>
{
if (deviceHandle != IntPtr.Zero)
return Device.GetProcedureAddress(name);
return Instance.GetProcedureAddress(name);
};
SharpVkGetProc = (name, instance, device) =>
{
if (device != null)
return device.GetProcedureAddress(name);
if (instance != null)
return instance.GetProcedureAddress(name);
// SharpVk includes the static functions on Instance, but this is not actually correct
// since the functions are static, they are not tied to an instance. For example,
// VkCreateInstance is not found on an instance, it is creating said instance.
// Other libraries, such as VulkanCore, use another type to do this.
return Instance.GetProcedureAddress(name);
};
}
public override void Dispose()
{
base.Dispose();
Window.DestroyHandle();
}
private (uint, uint) FindQueueFamilies()
{
QueueFamilyProperties[] queueFamilyProperties = PhysicalDevice.GetQueueFamilyProperties();
var graphicsFamily = queueFamilyProperties
.Select((properties, index) => new {properties, index})
.SkipWhile(pair => !pair.properties.QueueFlags.HasFlag(QueueFlags.Graphics))
.FirstOrDefault();
if (graphicsFamily == null)
throw new Exception("Unable to find graphics queue");
uint? presentFamily = default;
for (uint i = 0; i < queueFamilyProperties.Length; ++i)
{
if (PhysicalDevice.GetSurfaceSupport(i, Surface))
presentFamily = i;
}
if (!presentFamily.HasValue)
throw new Exception("Unable to find present queue");
return ((uint) graphicsFamily.index, presentFamily.Value);
}
}
}

View File

@ -0,0 +1,64 @@
using System;
using Artemis.Core.SkiaSharp;
using Artemis.UI.Exceptions;
using Artemis.UI.SkiaSharp.Vulkan;
using SkiaSharp;
namespace Artemis.UI.SkiaSharp
{
public class VulkanContext : IManagedGraphicsContext
{
private readonly GRVkBackendContext _vulkanBackendContext;
private readonly Win32VkContext _vulkanContext;
public VulkanContext()
{
// Try everything in separate try-catch blocks to provide some accuracy in error reporting
try
{
_vulkanContext = new Win32VkContext();
}
catch (Exception e)
{
throw new ArtemisGraphicsContextException("Failed to create Vulkan context", e);
}
try
{
_vulkanBackendContext = new GRVkBackendContext
{
VkInstance = (IntPtr) _vulkanContext.Instance.RawHandle.ToUInt64(),
VkPhysicalDevice = (IntPtr) _vulkanContext.PhysicalDevice.RawHandle.ToUInt64(),
VkDevice = (IntPtr) _vulkanContext.Device.RawHandle.ToUInt64(),
VkQueue = (IntPtr) _vulkanContext.GraphicsQueue.RawHandle.ToUInt64(),
GraphicsQueueIndex = _vulkanContext.GraphicsFamily,
GetProcedureAddress = _vulkanContext.GetProc
};
}
catch (Exception e)
{
throw new ArtemisGraphicsContextException("Failed to create Vulkan backend context", e);
}
try
{
GraphicsContext = GRContext.CreateVulkan(_vulkanBackendContext);
if (GraphicsContext == null)
throw new ArtemisGraphicsContextException("GRContext.CreateVulkan returned null");
}
catch (Exception e)
{
throw new ArtemisGraphicsContextException("Failed to create Vulkan graphics context", e);
}
GraphicsContext.Flush();
}
/// <inheritdoc />
public void Dispose()
{
}
public GRContext GraphicsContext { get; }
}
}

View File

@ -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",

View File

@ -225,7 +225,9 @@
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EAlwaysTreatStructAsNotReorderableMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=luma/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=pixmap/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=snackbar/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Timelines/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Timelines/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Vulkan/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>