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.Collections.Generic;
using System.IO; using System.IO;
using Artemis.Core.JsonConverters; using Artemis.Core.JsonConverters;
using Artemis.Core.Services;
using Artemis.Core.Services.Core; using Artemis.Core.Services.Core;
using Artemis.Core.SkiaSharp;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace Artemis.Core namespace Artemis.Core
@ -116,5 +118,11 @@ namespace Artemis.Core
typeof(double), typeof(double),
typeof(decimal) 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 #region Rendering
/// <inheritdoc /> /// <inheritdoc />
public override void Render(SKCanvas canvas) public override void Render(SKCanvas canvas, SKPoint basePosition)
{ {
if (Disposed) if (Disposed)
throw new ObjectDisposedException("Folder"); throw new ObjectDisposedException("Folder");
@ -189,41 +189,38 @@ namespace Artemis.Core
baseLayerEffect.Update(Timeline.Delta.TotalSeconds); baseLayerEffect.Update(Timeline.Delta.TotalSeconds);
} }
SKPaint layerPaint = new();
try try
{ {
canvas.Save(); SKRect rendererBounds = SKRect.Create(0, 0, Path.Bounds.Width, Path.Bounds.Height);
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;
foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => e.Enabled)) 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 required, apply the opacity override of the module to the root folder
if (IsRootFolder && Profile.Module.OpacityOverride < 1) if (IsRootFolder && Profile.Module.OpacityOverride < 1)
{ {
double multiplier = Easings.SineEaseInOut(Profile.Module.OpacityOverride); 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 // 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; return;
// Iterate the children in reverse because the first layer must be rendered last to end up on top // 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--) 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)) foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => e.Enabled))
baseLayerEffect.PostProcess(Renderer.Canvas, rendererBounds, Renderer.Paint); baseLayerEffect.PostProcess(canvas, rendererBounds, layerPaint);
canvas.DrawBitmap(Renderer.Bitmap, Renderer.TargetLocation, Renderer.Paint);
} }
finally finally
{ {
canvas.Restore(); canvas.Restore();
Renderer.Close(); layerPaint.DisposeSelfAndProperties();
} }
Timeline.ClearDelta(); Timeline.ClearDelta();
@ -239,7 +236,6 @@ namespace Artemis.Core
foreach (ProfileElement profileElement in Children) foreach (ProfileElement profileElement in Children)
profileElement.Dispose(); profileElement.Dispose();
Renderer.Dispose();
base.Dispose(disposing); base.Dispose(disposing);
} }

View File

@ -154,7 +154,6 @@ namespace Artemis.Core
_layerBrush?.Dispose(); _layerBrush?.Dispose();
_general.Dispose(); _general.Dispose();
_transform.Dispose(); _transform.Dispose();
Renderer.Dispose();
base.Dispose(disposing); base.Dispose(disposing);
} }
@ -183,7 +182,7 @@ namespace Artemis.Core
General.ShapeType.CurrentValueSet += ShapeTypeOnCurrentValueSet; General.ShapeType.CurrentValueSet += ShapeTypeOnCurrentValueSet;
ApplyShapeType(); ApplyShapeType();
ActivateLayerBrush(); ActivateLayerBrush();
Reset(); Reset();
} }
@ -278,7 +277,7 @@ namespace Artemis.Core
} }
/// <inheritdoc /> /// <inheritdoc />
public override void Render(SKCanvas canvas) public override void Render(SKCanvas canvas, SKPoint basePosition)
{ {
if (Disposed) if (Disposed)
throw new ObjectDisposedException("Layer"); throw new ObjectDisposedException("Layer");
@ -290,9 +289,9 @@ namespace Artemis.Core
if (LayerBrush == null || LayerBrush?.BaseProperties?.PropertiesInitialized == false) if (LayerBrush == null || LayerBrush?.BaseProperties?.PropertiesInitialized == false)
return; return;
RenderTimeline(Timeline, canvas); RenderTimeline(Timeline, canvas, basePosition);
foreach (Timeline extraTimeline in Timeline.ExtraTimelines.ToList()) foreach (Timeline extraTimeline in Timeline.ExtraTimelines.ToList())
RenderTimeline(extraTimeline, canvas); RenderTimeline(extraTimeline, canvas, basePosition);
Timeline.ClearDelta(); 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) if (Path == null || LayerBrush == null)
throw new ArtemisCoreException("The layer is not yet ready for rendering"); throw new ArtemisCoreException("The layer is not yet ready for rendering");
if (timeline.IsFinished) if (!Leds.Any() || timeline.IsFinished)
return; return;
ApplyTimeline(timeline); ApplyTimeline(timeline);
if (LayerBrush?.BrushType != LayerBrushType.Regular) if (LayerBrush?.BrushType != LayerBrushType.Regular)
return; return;
SKPaint layerPaint = new();
try try
{ {
canvas.Save(); 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) SKRect layerBounds = SKRect.Create(0, 0, Path.Bounds.Width, Path.Bounds.Height);
throw new ArtemisCoreException("Failed to open layer render context");
// Apply blend mode and color // Apply blend mode and color
Renderer.Paint.BlendMode = General.BlendMode.CurrentValue; layerPaint.BlendMode = General.BlendMode.CurrentValue;
Renderer.Paint.Color = new SKColor(0, 0, 0, (byte) (Transform.Opacity.CurrentValue * 2.55f)); layerPaint.Color = new SKColor(0, 0, 0, (byte) (Transform.Opacity.CurrentValue * 2.55f));
using SKPath renderPath = new(); using SKPath renderPath = new();
if (General.ShapeType.CurrentValue == LayerShapeType.Rectangle) if (General.ShapeType.CurrentValue == LayerShapeType.Rectangle)
renderPath.AddRect(Renderer.Path.Bounds); renderPath.AddRect(layerBounds);
else else
renderPath.AddOval(Renderer.Path.Bounds); renderPath.AddOval(layerBounds);
if (General.TransformMode.CurrentValue == LayerTransformMode.Normal) if (General.TransformMode.CurrentValue == LayerTransformMode.Normal)
{ {
@ -357,54 +360,50 @@ namespace Artemis.Core
if (LayerBrush.SupportsTransformation) if (LayerBrush.SupportsTransformation)
{ {
SKMatrix rotationMatrix = GetTransformMatrix(true, false, false, true); 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 DelegateRendering(canvas, renderPath, renderPath.Bounds, layerPaint);
Renderer.Canvas.ClipPath(renderPath);
DelegateRendering(renderPath.Bounds);
} }
else if (General.TransformMode.CurrentValue == LayerTransformMode.Clip) else if (General.TransformMode.CurrentValue == LayerTransformMode.Clip)
{ {
SKMatrix renderPathMatrix = GetTransformMatrix(true, true, true, true); SKMatrix renderPathMatrix = GetTransformMatrix(true, true, true, true);
renderPath.Transform(renderPathMatrix); renderPath.Transform(renderPathMatrix);
// If a brush is a bad boy and tries to color outside the lines, ensure that its clipped off DelegateRendering(canvas, renderPath, layerBounds, layerPaint);
Renderer.Canvas.ClipPath(renderPath);
DelegateRendering(Renderer.Path.Bounds);
} }
canvas.DrawBitmap(Renderer.Bitmap, Renderer.TargetLocation, Renderer.Paint);
} }
finally finally
{ {
try canvas.Restore();
{ layerPaint.DisposeSelfAndProperties();
canvas.Restore();
}
catch
{
// ignored
}
Renderer.Close();
} }
} }
private void DelegateRendering(SKRect bounds) private void DelegateRendering(SKCanvas canvas, SKPath renderPath, SKRect bounds, SKPaint layerPaint)
{ {
if (LayerBrush == null) if (LayerBrush == null)
throw new ArtemisCoreException("The layer is not yet ready for rendering"); 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)) 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)) // If a brush is a bad boy and tries to color outside the lines, ensure that its clipped off
baseLayerEffect.PostProcess(Renderer.Canvas, bounds, Renderer.Paint); 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() internal void CalculateRenderProperties()

View File

@ -81,7 +81,7 @@ namespace Artemis.Core
} }
/// <inheritdoc /> /// <inheritdoc />
public override void Render(SKCanvas canvas) public override void Render(SKCanvas canvas, SKPoint basePosition)
{ {
lock (_lock) lock (_lock)
{ {
@ -91,7 +91,7 @@ namespace Artemis.Core
throw new ArtemisCoreException($"Cannot render inactive profile: {this}"); throw new ArtemisCoreException($"Cannot render inactive profile: {this}");
foreach (ProfileElement profileElement in Children) foreach (ProfileElement profileElement in Children)
profileElement.Render(canvas); profileElement.Render(canvas, basePosition);
} }
} }

View File

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

View File

@ -19,7 +19,6 @@ namespace Artemis.Core
internal RenderProfileElement(Profile profile) : base(profile) internal RenderProfileElement(Profile profile) : base(profile)
{ {
Timeline = new Timeline(); Timeline = new Timeline();
Renderer = new Renderer();
ExpandedPropertyGroups = new List<string>(); ExpandedPropertyGroups = new List<string>();
LayerEffectsList = new List<BaseLayerEffect>(); LayerEffectsList = new List<BaseLayerEffect>();
@ -127,7 +126,6 @@ namespace Artemis.Core
{ {
base.Parent = value; base.Parent = value;
OnPropertyChanged(nameof(Parent)); 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 // 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 // SkiaSharp calls SkiaApi.sk_path_get_bounds (Handle, &rect); which sounds expensive
Bounds = value?.Bounds ?? SKRect.Empty; Bounds = value?.Bounds ?? SKRect.Empty;
Renderer.Invalidate();
} }
} }
@ -157,7 +154,6 @@ namespace Artemis.Core
private set => SetAndNotify(ref _bounds, value); private set => SetAndNotify(ref _bounds, value);
} }
internal Renderer Renderer { get; }
#region Property group expansion #region Property group expansion

View File

@ -9,8 +9,8 @@ namespace Artemis.Core
private bool _disposed; private bool _disposed;
private SKRect _lastBounds; private SKRect _lastBounds;
private SKRect _lastParentBounds; private SKRect _lastParentBounds;
public SKBitmap? Bitmap { get; private set; } private GRContext? _lastGraphicsContext;
public SKCanvas? Canvas { get; private set; } public SKSurface? Surface { get; private set; }
public SKPaint? Paint { get; private set; } public SKPaint? Paint { get; private set; }
public SKPath? Path { get; private set; } public SKPath? Path { get; private set; }
public SKPoint TargetLocation { get; private set; } public SKPoint TargetLocation { get; private set; }
@ -28,35 +28,40 @@ namespace Artemis.Core
if (IsOpen) if (IsOpen)
throw new ArtemisCoreException("Cannot open render context because it is already open"); 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(); Invalidate();
if (!_valid || Canvas == null) if (!_valid || Surface == null)
{ {
SKRect pathBounds = path.Bounds; SKRect pathBounds = path.Bounds;
int width = (int) pathBounds.Width; int width = (int) pathBounds.Width;
int height = (int) pathBounds.Height; 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); Path = new SKPath(path);
Canvas = new SKCanvas(Bitmap);
Path.Transform(SKMatrix.CreateTranslation(pathBounds.Left * -1, pathBounds.Top * -1)); Path.Transform(SKMatrix.CreateTranslation(pathBounds.Left * -1, pathBounds.Top * -1));
TargetLocation = new SKPoint(pathBounds.Location.X, pathBounds.Location.Y); TargetLocation = new SKPoint(pathBounds.Location.X, pathBounds.Location.Y);
if (parent != null) if (parent != null)
TargetLocation -= parent.Bounds.Location; TargetLocation -= parent.Bounds.Location;
Canvas.ClipPath(Path); Surface.Canvas.ClipPath(Path);
_lastParentBounds = parent?.Bounds ?? new SKRect(); _lastParentBounds = parent?.Bounds ?? new SKRect();
_lastBounds = path.Bounds; _lastBounds = path.Bounds;
_lastGraphicsContext = Constants.ManagedGraphicsContext?.GraphicsContext;
_valid = true; _valid = true;
} }
Paint = new SKPaint(); Paint = new SKPaint();
Canvas.Clear(); Surface.Canvas.Clear();
Canvas.Save(); Surface.Canvas.Save();
IsOpen = true; IsOpen = true;
} }
@ -66,8 +71,15 @@ namespace Artemis.Core
if (_disposed) if (_disposed)
throw new ObjectDisposedException("Renderer"); 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?.Dispose();
Paint = null; Paint = null;
IsOpen = false; IsOpen = false;
@ -86,17 +98,21 @@ namespace Artemis.Core
if (IsOpen) if (IsOpen)
Close(); Close();
Canvas?.Dispose(); Surface?.Dispose();
Paint?.Dispose(); Paint?.Dispose();
Path?.Dispose(); Path?.Dispose();
Bitmap?.Dispose();
Canvas = null; Surface = null;
Paint = null; Paint = null;
Path = null; Path = null;
Bitmap = null;
_disposed = true; _disposed = true;
} }
~Renderer()
{
if (IsOpen)
Close();
}
} }
} }

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using Artemis.Core.DataModelExpansions; using Artemis.Core.DataModelExpansions;
using Artemis.Storage.Entities.Module; using Artemis.Storage.Entities.Module;
@ -186,13 +187,17 @@ namespace Artemis.Core.Modules
internal virtual void InternalUpdate(double deltaTime) internal virtual void InternalUpdate(double deltaTime)
{ {
StartUpdateMeasure();
if (IsUpdateAllowed) if (IsUpdateAllowed)
Update(deltaTime); Update(deltaTime);
StopUpdateMeasure();
} }
internal virtual void InternalRender(double deltaTime, SKCanvas canvas, SKImageInfo canvasInfo) internal virtual void InternalRender(double deltaTime, SKCanvas canvas, SKImageInfo canvasInfo)
{ {
StartRenderMeasure();
Render(deltaTime, canvas, canvasInfo); Render(deltaTime, canvas, canvasInfo);
StopRenderMeasure();
} }
internal virtual void Activate(bool isOverride) internal virtual void Activate(bool isOverride)

View File

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

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using Artemis.Storage.Entities.Plugins; using Artemis.Storage.Entities.Plugins;
@ -10,9 +11,11 @@ namespace Artemis.Core
/// </summary> /// </summary>
public abstract class PluginFeature : CorePropertyChanged, IDisposable public abstract class PluginFeature : CorePropertyChanged, IDisposable
{ {
private readonly Stopwatch _renderStopwatch = new();
private readonly Stopwatch _updateStopwatch = new();
private bool _isEnabled; private bool _isEnabled;
private Exception? _loadException; private Exception? _loadException;
/// <summary> /// <summary>
/// Gets the plugin feature info related to this feature /// Gets the plugin feature info related to this feature
/// </summary> /// </summary>
@ -46,6 +49,16 @@ namespace Artemis.Core
/// </summary> /// </summary>
public string Id => $"{GetType().FullName}-{Plugin.Guid.ToString().Substring(0, 8)}"; // Not as unique as a GUID but good enough and stays readable 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 internal PluginFeatureEntity Entity { get; set; } = null!; // Will be set right after construction
/// <summary> /// <summary>
@ -58,6 +71,66 @@ namespace Artemis.Core
/// </summary> /// </summary>
public abstract void Disable(); 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) internal void SetEnabled(bool enable, bool isAutoEnable = false)
{ {
if (enable == IsEnabled) if (enable == IsEnabled)
@ -133,25 +206,6 @@ namespace Artemis.Core
Disable(); 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 /> /// <inheritdoc />
public void Dispose() public void Dispose()
{ {
@ -187,35 +241,5 @@ namespace Artemis.Core
} }
#endregion #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;
using System.Runtime.InteropServices;
using Artemis.Core.SkiaSharp;
using RGB.NET.Core; using RGB.NET.Core;
using RGB.NET.Presets.Textures.Sampler; using RGB.NET.Presets.Textures.Sampler;
using SkiaSharp; using SkiaSharp;
@ -10,25 +12,45 @@ namespace Artemis.Core
/// </summary> /// </summary>
public sealed class SKTexture : PixelTexture<byte>, IDisposable public sealed class SKTexture : PixelTexture<byte>, IDisposable
{ {
private bool _disposed; private readonly SKPixmap _pixelData;
private readonly IntPtr _pixelDataPtr;
#region Constructors #region Constructors
internal SKTexture(int width, int height, float renderScale) internal SKTexture(IManagedGraphicsContext? graphicsContext, int width, int height, float scale) : base(width, height, 4, new AverageByteSampler())
: base(width, height, 4, new AverageByteSampler())
{ {
Bitmap = new SKBitmap(new SKImageInfo(width, height, SKColorType.Rgb888x)); ImageInfo = new SKImageInfo(width, height);
RenderScale = renderScale; Surface = graphicsContext == null
? SKSurface.Create(ImageInfo)
: SKSurface.Create(graphicsContext.GraphicsContext, true, ImageInfo);
RenderScale = scale;
_pixelDataPtr = Marshal.AllocHGlobal(ImageInfo.BytesSize);
_pixelData = new SKPixmap(ImageInfo, _pixelDataPtr, ImageInfo.RowBytes);
} }
#endregion #endregion
#region Methods #region Methods
/// <summary>
/// Invalidates the texture
/// </summary>
public void Invalidate()
{
IsInvalid = true;
}
internal void CopyPixelData()
{
using SKImage skImage = Surface.Snapshot();
skImage.ReadPixels(_pixelData);
}
/// <inheritdoc /> /// <inheritdoc />
protected override Color GetColor(in ReadOnlySpan<byte> pixel) 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 #endregion
@ -38,12 +60,17 @@ namespace Artemis.Core
/// <summary> /// <summary>
/// Gets the SKBitmap backing this texture /// Gets the SKBitmap backing this texture
/// </summary> /// </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> /// <summary>
/// Gets the color data in RGB format /// Gets the color data in RGB format
/// </summary> /// </summary>
protected override ReadOnlySpan<byte> Data => _disposed ? new ReadOnlySpan<byte>() : Bitmap.GetPixelSpan(); protected override ReadOnlySpan<byte> Data => _pixelData.GetPixelSpan();
/// <summary> /// <summary>
/// Gets the render scale of the texture /// Gets the render scale of the texture
@ -56,19 +83,29 @@ namespace Artemis.Core
/// </summary> /// </summary>
public bool IsInvalid { get; private set; } public bool IsInvalid { get; private set; }
/// <summary> #endregion
/// Invalidates the texture
/// </summary> #region IDisposable
public void Invalidate()
private void ReleaseUnmanagedResources()
{ {
IsInvalid = true; Marshal.FreeHGlobal(_pixelDataPtr);
} }
/// <inheritdoc /> /// <inheritdoc />
public void Dispose() public void Dispose()
{ {
_disposed = true; Surface.Dispose();
Bitmap.Dispose(); _pixelData.Dispose();
ReleaseUnmanagedResources();
GC.SuppressFinalize(this);
}
/// <inheritdoc />
~SKTexture()
{
ReleaseUnmanagedResources();
} }
#endregion #endregion

View File

@ -59,6 +59,7 @@ namespace Artemis.Core.Services
UpdatePluginCache(); UpdatePluginCache();
_rgbService.IsRenderPaused = true;
_rgbService.Surface.Updating += SurfaceOnUpdating; _rgbService.Surface.Updating += SurfaceOnUpdating;
_loggingLevel.SettingChanged += (sender, args) => ApplyLoggingLevel(); _loggingLevel.SettingChanged += (sender, args) => ApplyLoggingLevel();
@ -139,21 +140,23 @@ namespace Artemis.Core.Services
module.InternalUpdate(args.DeltaTime); module.InternalUpdate(args.DeltaTime);
// Render all active modules // 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); foreach (Module module in modules.Where(m => m.IsActivated))
canvas.Clear(new SKColor(0, 0, 0)); module.InternalRender(args.DeltaTime, canvas, texture.ImageInfo);
// 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));
} }
OnFrameRendering(new FrameRenderingEventArgs(canvas, args.DeltaTime, _rgbService.Surface));
canvas.RestoreToCount(-1);
canvas.Flush();
OnFrameRendered(new FrameRenderedEventArgs(texture, _rgbService.Surface)); OnFrameRendered(new FrameRenderedEventArgs(texture, _rgbService.Surface));
} }
@ -163,9 +166,9 @@ namespace Artemis.Core.Services
} }
finally finally
{ {
_rgbService.CloseRender();
_frameStopWatch.Stop(); _frameStopWatch.Stop();
FrameTime = _frameStopWatch.Elapsed; FrameTime = _frameStopWatch.Elapsed;
_rgbService.CloseRender();
LogUpdateExceptions(); LogUpdateExceptions();
} }
@ -240,6 +243,7 @@ namespace Artemis.Core.Services
IsElevated IsElevated
); );
_rgbService.IsRenderPaused = false;
OnInitialized(); OnInitialized();
} }

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Artemis.Core.SkiaSharp;
using RGB.NET.Core; using RGB.NET.Core;
namespace Artemis.Core.Services namespace Artemis.Core.Services
@ -50,6 +51,16 @@ namespace Artemis.Core.Services
/// </summary> /// </summary>
void CloseRender(); 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> /// <summary>
/// Adds the given device provider to the <see cref="Surface" /> /// Adds the given device provider to the <see cref="Surface" />
/// </summary> /// </summary>

View File

@ -4,11 +4,11 @@ using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using Artemis.Core.DeviceProviders; using Artemis.Core.DeviceProviders;
using Artemis.Core.Services.Models; using Artemis.Core.Services.Models;
using Artemis.Core.SkiaSharp;
using Artemis.Storage.Entities.Surface; using Artemis.Storage.Entities.Surface;
using Artemis.Storage.Repositories.Interfaces; using Artemis.Storage.Repositories.Interfaces;
using RGB.NET.Core; using RGB.NET.Core;
using Serilog; using Serilog;
using SkiaSharp;
namespace Artemis.Core.Services namespace Artemis.Core.Services
{ {
@ -51,6 +51,8 @@ namespace Artemis.Core.Services
UpdateTrigger = new TimerUpdateTrigger {UpdateFrequency = 1.0 / _targetFrameRateSetting.Value}; UpdateTrigger = new TimerUpdateTrigger {UpdateFrequency = 1.0 / _targetFrameRateSetting.Value};
Surface.RegisterUpdateTrigger(UpdateTrigger); Surface.RegisterUpdateTrigger(UpdateTrigger);
Utilities.ShutdownRequested += UtilitiesOnShutdownRequested;
} }
public TimerUpdateTrigger UpdateTrigger { get; } public TimerUpdateTrigger UpdateTrigger { get; }
@ -66,6 +68,11 @@ namespace Artemis.Core.Services
_texture?.Invalidate(); _texture?.Invalidate();
} }
private void UtilitiesOnShutdownRequested(object? sender, EventArgs e)
{
IsRenderPaused = true;
}
private void SurfaceOnLayoutChanged(SurfaceLayoutChangedEventArgs args) private void SurfaceOnLayoutChanged(SurfaceLayoutChangedEventArgs args)
{ {
UpdateLedGroup(); UpdateLedGroup();
@ -115,13 +122,16 @@ namespace Artemis.Core.Services
DeviceAdded?.Invoke(this, e); DeviceAdded?.Invoke(this, e);
} }
private void RenderScaleSettingOnSettingChanged(object? sender, EventArgs e)
{
_texture?.Invalidate();
}
public IReadOnlyCollection<ArtemisDevice> EnabledDevices => _enabledDevices.AsReadOnly(); public IReadOnlyCollection<ArtemisDevice> EnabledDevices => _enabledDevices.AsReadOnly();
public IReadOnlyCollection<ArtemisDevice> Devices => _devices.AsReadOnly(); public IReadOnlyCollection<ArtemisDevice> Devices => _devices.AsReadOnly();
public IReadOnlyDictionary<Led, ArtemisLed> LedMap => new ReadOnlyDictionary<Led, ArtemisLed>(_ledMap); public IReadOnlyDictionary<Led, ArtemisLed> LedMap => new ReadOnlyDictionary<Led, ArtemisLed>(_ledMap);
/// <inheritdoc />
public RGBSurface Surface { get; set; } public RGBSurface Surface { get; set; }
public bool IsRenderPaused { get; set; } public bool IsRenderPaused { get; set; }
public bool RenderOpen { get; private 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() public void Dispose()
{ {
Surface.UnregisterUpdateTrigger(UpdateTrigger); Surface.UnregisterUpdateTrigger(UpdateTrigger);
@ -216,6 +221,8 @@ namespace Artemis.Core.Services
#region Rendering #region Rendering
private IManagedGraphicsContext? _newGraphicsContext;
public SKTexture OpenRender() public SKTexture OpenRender()
{ {
if (RenderOpen) if (RenderOpen)
@ -234,6 +241,7 @@ namespace Artemis.Core.Services
throw new ArtemisCoreException("Render pipeline is already closed"); throw new ArtemisCoreException("Render pipeline is already closed");
RenderOpen = false; RenderOpen = false;
_texture?.CopyPixelData();
} }
public void CreateTexture() public void CreateTexture()
@ -241,15 +249,38 @@ namespace Artemis.Core.Services
if (RenderOpen) if (RenderOpen)
throw new ArtemisCoreException("Cannot update the texture while rendering"); 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; float renderScale = (float) _renderScaleSetting.Value;
int width = Math.Max(1, MathF.Min(Surface.Boundary.Size.Width * renderScale, 4096).RoundToInt()); 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()); 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; _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 #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) public void Render(double deltaTime, SKCanvas canvas)
{ {
AnimationProfile.Update(deltaTime); AnimationProfile.Update(deltaTime);
AnimationProfile.Render(canvas); AnimationProfile.Render(canvas, SKPoint.Empty);
} }
private Profile CreateIntroProfile() private Profile CreateIntroProfile()

View File

@ -149,6 +149,7 @@
<PackageReference Include="RawInput.Sharp" Version="0.0.3" /> <PackageReference Include="RawInput.Sharp" Version="0.0.3" />
<PackageReference Include="Serilog" Version="2.10.0" /> <PackageReference Include="Serilog" Version="2.10.0" />
<PackageReference Include="SkiaSharp.Views.WPF" Version="2.80.2" /> <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="Stylet" Version="1.3.5" />
<PackageReference Include="System.Buffers" Version="4.5.1" /> <PackageReference Include="System.Buffers" Version="4.5.1" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" /> <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;
using System.Windows.Markup; using System.Windows.Markup;
using System.Windows.Threading; using System.Windows.Threading;
using Artemis.Core;
using Artemis.Core.Ninject; using Artemis.Core.Ninject;
using Artemis.Core.Services; using Artemis.Core.Services;
using Artemis.UI.Ninject; using Artemis.UI.Ninject;
@ -13,9 +14,11 @@ using Artemis.UI.Screens;
using Artemis.UI.Services; using Artemis.UI.Services;
using Artemis.UI.Shared; using Artemis.UI.Shared;
using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services;
using Artemis.UI.SkiaSharp;
using Artemis.UI.Stylet; using Artemis.UI.Stylet;
using Ninject; using Ninject;
using Serilog; using Serilog;
using SkiaSharp;
using Stylet; using Stylet;
namespace Artemis.UI namespace Artemis.UI
@ -65,7 +68,7 @@ namespace Artemis.UI
FrameworkElement.LanguageProperty.OverrideMetadata(typeof(FrameworkElement), new FrameworkPropertyMetadata(XmlLanguage.GetLanguage(CultureInfo.CurrentCulture.IetfLanguageTag))); 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 // 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); UIElement view = viewManager.CreateAndBindViewForModelIfNecessary(RootViewModel);
((TrayViewModel) RootViewModel).SetTaskbarIcon(view); ((TrayViewModel) RootViewModel).SetTaskbarIcon(view);
@ -90,7 +93,12 @@ namespace Artemis.UI
IRegistrationService registrationService = Kernel.Get<IRegistrationService>(); IRegistrationService registrationService = Kernel.Get<IRegistrationService>();
registrationService.RegisterInputProvider(); registrationService.RegisterInputProvider();
registrationService.RegisterControllers(); registrationService.RegisterControllers();
Execute.OnUIThreadSync(() =>
{
registrationService.ApplyPreferredGraphicsContext();
});
// Initialize background services // Initialize background services
Kernel.Get<IDeviceLayoutService>(); 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": { "profiles": {
"Artemis.UI": { "Artemis.UI": {
"commandName": "Project", "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"> <TextBlock Grid.Column="1" HorizontalAlignment="Right" Margin="0,0,5,0">
<Run Text="FPS: " /> <Run Text="FPS: " />
<Run FontWeight="Bold" Text="{Binding CurrentFps}" /> <Run FontWeight="Bold" Text="{Binding CurrentFps}" />
<Run Text=" at " /> <Run Text="at" />
<Run Text="{Binding RenderWidth}" /><Run Text="x" /> <Run Text="{Binding RenderWidth}" /><Run Text="x" /><Run Text="{Binding RenderHeight}" />
<Run Text="{Binding RenderHeight}" /> <Run Text="- Renderer:"></Run>
<Run Text="{Binding Renderer}"></Run>
</TextBlock> </TextBlock>
</Grid> </Grid>

View File

@ -20,6 +20,9 @@ namespace Artemis.UI.Screens.Settings.Debug.Tabs
private int _renderWidth; private int _renderWidth;
private int _renderHeight; private int _renderHeight;
private string _frameTargetPath; private string _frameTargetPath;
private string _renderer;
private int _frames;
private DateTime _frameCountStart;
public RenderDebugViewModel(ICoreService coreService) public RenderDebugViewModel(ICoreService coreService)
{ {
@ -51,6 +54,12 @@ namespace Artemis.UI.Screens.Settings.Debug.Tabs
set => SetAndNotify(ref _renderHeight, value); set => SetAndNotify(ref _renderHeight, value);
} }
public string Renderer
{
get => _renderer;
set => SetAndNotify(ref _renderer, value);
}
public void SaveFrame() public void SaveFrame()
{ {
VistaSaveFileDialog dialog = new VistaSaveFileDialog {Filter = "Portable network graphic (*.png)|*.png", Title = "Save render frame"}; 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) 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(() => Execute.OnUIThreadSync(() =>
{ {
SKImageInfo bitmapInfo = e.Texture.Bitmap.Info;
RenderHeight = bitmapInfo.Height; RenderHeight = bitmapInfo.Height;
RenderWidth = bitmapInfo.Width; RenderWidth = bitmapInfo.Width;
// ReSharper disable twice CompareOfFloatsByEqualityOperator // ReSharper disable twice CompareOfFloatsByEqualityOperator
if (CurrentFrame is not WriteableBitmap writable || writable.Width != bitmapInfo.Width || writable.Height != bitmapInfo.Height) 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; 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(); 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); skImage.ReadPixels(pixmap, 0, 0);
} }
@ -123,7 +132,16 @@ namespace Artemis.UI.Screens.Settings.Debug.Tabs
private void CoreServiceOnFrameRendering(object sender, FrameRenderingEventArgs e) 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:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:s="https://github.com/canton7/Stylet" xmlns:s="https://github.com/canton7/Stylet"
xmlns:dataTemplateSelectors="clr-namespace:Artemis.UI.DataTemplateSelectors" xmlns:dataTemplateSelectors="clr-namespace:Artemis.UI.DataTemplateSelectors"
xmlns:system="clr-namespace:System;assembly=System.Runtime"
mc:Ignorable="d" mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800" d:DesignHeight="450" d:DesignWidth="800"
d:DataContext="{d:DesignInstance local:GeneralSettingsTabViewModel}"> d:DataContext="{d:DesignInstance local:GeneralSettingsTabViewModel}">
@ -307,6 +308,30 @@
<TextBlock Style="{StaticResource MaterialDesignHeadline5TextBlock}" Margin="0 15">Rendering</TextBlock> <TextBlock Style="{StaticResource MaterialDesignHeadline5TextBlock}" Margin="0 15">Rendering</TextBlock>
<materialDesign:Card materialDesign:ShadowAssist.ShadowDepth="Depth1" VerticalAlignment="Stretch" Margin="0,0,5,0"> <materialDesign:Card materialDesign:ShadowAssist.ShadowDepth="Depth1" VerticalAlignment="Stretch" Margin="0,0,5,0">
<StackPanel Margin="15"> <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>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition /> <RowDefinition />

View File

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

View File

@ -1,4 +1,5 @@
using System.Linq; using System;
using System.Linq;
using Artemis.Core; using Artemis.Core;
using Artemis.Core.Services; using Artemis.Core.Services;
using Artemis.UI.Controllers; using Artemis.UI.Controllers;
@ -8,6 +9,7 @@ using Artemis.UI.DefaultTypes.PropertyInput;
using Artemis.UI.InputProviders; using Artemis.UI.InputProviders;
using Artemis.UI.Ninject; using Artemis.UI.Ninject;
using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services;
using Artemis.UI.SkiaSharp;
using Serilog; using Serilog;
namespace Artemis.UI.Services namespace Artemis.UI.Services
@ -15,28 +17,38 @@ namespace Artemis.UI.Services
public class RegistrationService : IRegistrationService public class RegistrationService : IRegistrationService
{ {
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly ICoreService _coreService;
private readonly IDataModelUIService _dataModelUIService; private readonly IDataModelUIService _dataModelUIService;
private readonly IProfileEditorService _profileEditorService; private readonly IProfileEditorService _profileEditorService;
private readonly IPluginManagementService _pluginManagementService; private readonly IPluginManagementService _pluginManagementService;
private readonly IInputService _inputService; private readonly IInputService _inputService;
private readonly IWebServerService _webServerService; private readonly IWebServerService _webServerService;
private readonly IRgbService _rgbService;
private readonly ISettingsService _settingsService;
private bool _registeredBuiltInDataModelDisplays; private bool _registeredBuiltInDataModelDisplays;
private bool _registeredBuiltInDataModelInputs; private bool _registeredBuiltInDataModelInputs;
private bool _registeredBuiltInPropertyEditors; private bool _registeredBuiltInPropertyEditors;
private VulkanContext _vulkanContext;
public RegistrationService(ILogger logger, public RegistrationService(ILogger logger,
ICoreService coreService,
IDataModelUIService dataModelUIService, IDataModelUIService dataModelUIService,
IProfileEditorService profileEditorService, IProfileEditorService profileEditorService,
IPluginManagementService pluginManagementService, IPluginManagementService pluginManagementService,
IInputService inputService, IInputService inputService,
IWebServerService webServerService) IWebServerService webServerService,
IRgbService rgbService,
ISettingsService settingsService)
{ {
_logger = logger; _logger = logger;
_coreService = coreService;
_dataModelUIService = dataModelUIService; _dataModelUIService = dataModelUIService;
_profileEditorService = profileEditorService; _profileEditorService = profileEditorService;
_pluginManagementService = pluginManagementService; _pluginManagementService = pluginManagementService;
_inputService = inputService; _inputService = inputService;
_webServerService = webServerService; _webServerService = webServerService;
_rgbService = rgbService;
_settingsService = settingsService;
LoadPluginModules(); LoadPluginModules();
pluginManagementService.PluginEnabling += PluginServiceOnPluginEnabling; pluginManagementService.PluginEnabling += PluginServiceOnPluginEnabling;
@ -97,6 +109,40 @@ namespace Artemis.UI.Services
_webServerService.AddController<RemoteController>(); _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) private void PluginServiceOnPluginEnabling(object sender, PluginEventArgs e)
{ {
e.Plugin.Kernel.Load(new[] {new PluginUIModule(e.Plugin)}); e.Plugin.Kernel.Load(new[] {new PluginUIModule(e.Plugin)});
@ -116,5 +162,6 @@ namespace Artemis.UI.Services
void RegisterBuiltInPropertyEditors(); void RegisterBuiltInPropertyEditors();
void RegisterInputProvider(); void RegisterInputProvider();
void RegisterControllers(); 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.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": { "Stylet": {
"type": "Direct", "type": "Direct",
"requested": "[1.3.5, )", "requested": "[1.3.5, )",
@ -513,6 +523,15 @@
"resolved": "1.7.1", "resolved": "1.7.1",
"contentHash": "ljl9iVpmGOjgmxXxyulMBfl7jCLEMmTOSIrQwJJQLIm5PFhtaxRRgdQPY5ElXz+vfPKqX7Aj3RGnAN+SUN7V3w==" "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": { "SkiaSharp": {
"type": "Transitive", "type": "Transitive",
"resolved": "2.80.2", "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_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/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/=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/=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>