1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-31 17:53:32 +00:00

Merge branch 'development'

This commit is contained in:
Robert 2021-03-23 20:44:48 +01:00
commit dc81b905b4
47 changed files with 1054 additions and 234 deletions

View File

@ -38,6 +38,7 @@
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=models_005Cprofile_005Cdatabindings_005Cmodifiers/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=models_005Cprofile_005Cdatabindings_005Cmodifiers/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=models_005Cprofile_005Cdatabindings_005Cmodifiers_005Cabstract/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=models_005Cprofile_005Cdatabindings_005Cmodifiers_005Cabstract/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=models_005Cprofile_005Cdatamodel/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=models_005Cprofile_005Cdatamodel/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=models_005Cprofile_005Cdatamodel_005Cvaluechangedevent/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=models_005Cprofile_005Clayerproperties/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=models_005Cprofile_005Clayerproperties/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=models_005Cprofile_005Clayerproperties_005Cattributes/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=models_005Cprofile_005Clayerproperties_005Cattributes/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=models_005Cprofile_005Clayerproperties_005Ctypes/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=models_005Cprofile_005Clayerproperties_005Ctypes/@EntryIndexedValue">True</s:Boolean>

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

@ -12,6 +12,7 @@ namespace Artemis.Core
{ {
private bool _disposed; private bool _disposed;
private bool _reinitializing; private bool _reinitializing;
private IDataModelEvent? _valueChangedEvent;
/// <summary> /// <summary>
/// Creates a new instance of the <see cref="DataModelConditionEvent" /> class /// Creates a new instance of the <see cref="DataModelConditionEvent" /> class
@ -56,8 +57,11 @@ namespace Artemis.Core
if (_disposed) if (_disposed)
throw new ObjectDisposedException("DataModelConditionEvent"); throw new ObjectDisposedException("DataModelConditionEvent");
if (EventPath?.GetValue() is not IDataModelEvent dataModelEvent) IDataModelEvent? dataModelEvent = GetDataModelEvent();
if (dataModelEvent == null)
return false; return false;
dataModelEvent.Update();
// Only evaluate to true once every time the event has been triggered since the last evaluation // Only evaluate to true once every time the event has been triggered since the last evaluation
if (dataModelEvent.LastTrigger <= LastTrigger) if (dataModelEvent.LastTrigger <= LastTrigger)
return false; return false;
@ -86,6 +90,7 @@ namespace Artemis.Core
EventPath?.Dispose(); EventPath?.Dispose();
EventPath = path != null ? new DataModelPath(path) : null; EventPath = path != null ? new DataModelPath(path) : null;
SubscribeToEventPath(); SubscribeToEventPath();
CreateValueChangedEventIfNeeded();
// Remove the old root group that was tied to the old data model // Remove the old root group that was tied to the old data model
ClearChildren(); ClearChildren();
@ -100,6 +105,19 @@ namespace Artemis.Core
{ {
EventArgumentType = null; EventArgumentType = null;
} }
LastTrigger = GetDataModelEvent()?.LastTrigger ?? DateTime.Now;
}
/// <summary>
/// Returns the <see cref="IDataModelEvent" /> this <see cref="DataModelConditionEvent" /> is triggered by
/// </summary>
/// <returns>The <see cref="IDataModelEvent" /> this <see cref="DataModelConditionEvent" /> is triggered by</returns>
public IDataModelEvent? GetDataModelEvent()
{
if (_valueChangedEvent != null)
return _valueChangedEvent;
return EventPath?.GetValue() as IDataModelEvent;
} }
#region IDisposable #region IDisposable
@ -153,14 +171,10 @@ namespace Artemis.Core
if (Entity.EventPath == null) if (Entity.EventPath == null)
return; return;
// Ensure the list path is valid and points to a list
DataModelPath eventPath = new(null, Entity.EventPath); DataModelPath eventPath = new(null, Entity.EventPath);
// Can't check this on an invalid list, if it becomes valid later lets hope for the best
if (eventPath.IsValid && !PointsToEvent(eventPath))
return;
EventPath = eventPath; EventPath = eventPath;
SubscribeToEventPath(); SubscribeToEventPath();
CreateValueChangedEventIfNeeded();
EventArgumentType = GetEventArgumentType(); EventArgumentType = GetEventArgumentType();
// There should only be one child and it should be a group // There should only be one child and it should be a group
@ -174,8 +188,7 @@ namespace Artemis.Core
AddChild(new DataModelConditionGroup(this)); AddChild(new DataModelConditionGroup(this));
} }
if (EventPath?.GetValue() is IDataModelEvent dataModelEvent) LastTrigger = GetDataModelEvent()?.LastTrigger ?? DateTime.Now;
LastTrigger = dataModelEvent.LastTrigger;
} }
private Type? GetEventArgumentType() private Type? GetEventArgumentType()
@ -183,20 +196,14 @@ namespace Artemis.Core
if (EventPath == null || !EventPath.IsValid) if (EventPath == null || !EventPath.IsValid)
return null; return null;
if (_valueChangedEvent != null)
return _valueChangedEvent.ArgumentsType;
// Cannot rely on EventPath.GetValue() because part of the path might be null // Cannot rely on EventPath.GetValue() because part of the path might be null
Type eventType = EventPath.GetPropertyType()!; Type eventType = EventPath.GetPropertyType()!;
return eventType.IsGenericType ? eventType.GetGenericArguments()[0] : typeof(DataModelEventArgs); return eventType.IsGenericType ? eventType.GetGenericArguments()[0] : typeof(DataModelEventArgs);
} }
private bool PointsToEvent(DataModelPath dataModelPath)
{
Type? type = dataModelPath.GetPropertyType();
if (type == null)
return false;
return typeof(IDataModelEvent).IsAssignableFrom(type);
}
private void SubscribeToEventPath() private void SubscribeToEventPath()
{ {
if (EventPath == null) return; if (EventPath == null) return;
@ -204,6 +211,23 @@ namespace Artemis.Core
EventPath.PathInvalidated += EventPathOnPathInvalidated; EventPath.PathInvalidated += EventPathOnPathInvalidated;
} }
private void CreateValueChangedEventIfNeeded()
{
Type? propertyType = EventPath?.GetPropertyType();
if (propertyType == null)
return;
if (!typeof(IDataModelEvent).IsAssignableFrom(propertyType))
{
IDataModelEvent? instance = (IDataModelEvent?) Activator.CreateInstance(typeof(DataModelValueChangedEvent<>).MakeGenericType(propertyType), EventPath);
_valueChangedEvent = instance ?? throw new ArtemisCoreException("Failed to create a DataModelValueChangedEvent for a property changed data model event");
}
else
{
_valueChangedEvent = null;
}
}
#region Event handlers #region Event handlers
private void EventPathOnPathValidated(object? sender, EventArgs e) private void EventPathOnPathValidated(object? sender, EventArgs e)

View File

@ -84,6 +84,9 @@ namespace Artemis.Core
[DataModelIgnore] [DataModelIgnore]
public Type ArgumentsType => typeof(T); public Type ArgumentsType => typeof(T);
/// <inheritdoc />
public string TriggerPastParticiple => "triggered";
/// <inheritdoc /> /// <inheritdoc />
[DataModelIgnore] [DataModelIgnore]
public bool TrackHistory public bool TrackHistory
@ -113,6 +116,11 @@ namespace Artemis.Core
TriggerCount = 0; TriggerCount = 0;
EventArgumentsHistory.Clear(); EventArgumentsHistory.Clear();
} }
/// <inheritdoc />
public void Update()
{
}
} }
/// <summary> /// <summary>
@ -192,6 +200,9 @@ namespace Artemis.Core
[DataModelIgnore] [DataModelIgnore]
public Type ArgumentsType => typeof(DataModelEventArgs); public Type ArgumentsType => typeof(DataModelEventArgs);
/// <inheritdoc />
public string TriggerPastParticiple => "triggered";
/// <inheritdoc /> /// <inheritdoc />
[DataModelIgnore] [DataModelIgnore]
public bool TrackHistory public bool TrackHistory
@ -221,5 +232,10 @@ namespace Artemis.Core
TriggerCount = 0; TriggerCount = 0;
EventArgumentsHistory.Clear(); EventArgumentsHistory.Clear();
} }
/// <inheritdoc />
public void Update()
{
}
} }
} }

View File

@ -1,4 +1,5 @@
using System; using System;
using Artemis.Core.DataModelExpansions;
namespace Artemis.Core namespace Artemis.Core
{ {
@ -10,6 +11,7 @@ namespace Artemis.Core
/// <summary> /// <summary>
/// Gets the time at which the event with these arguments was triggered /// Gets the time at which the event with these arguments was triggered
/// </summary> /// </summary>
[DataModelIgnore]
public DateTime TriggerTime { get; internal set; } public DateTime TriggerTime { get; internal set; }
} }
} }

View File

@ -3,7 +3,10 @@ using System.Collections.Generic;
namespace Artemis.Core namespace Artemis.Core
{ {
internal interface IDataModelEvent /// <summary>
/// Represents a data model event that can trigger <see cref="DataModelConditionEvent" />s.
/// </summary>
public interface IDataModelEvent
{ {
/// <summary> /// <summary>
/// Gets the last time the event was triggered /// Gets the last time the event was triggered
@ -20,6 +23,11 @@ namespace Artemis.Core
/// </summary> /// </summary>
Type ArgumentsType { get; } Type ArgumentsType { get; }
/// <summary>
/// Gets the past participle for this event shown in the UI
/// </summary>
string TriggerPastParticiple { get; }
/// <summary> /// <summary>
/// Gets or sets a boolean indicating whether the last 20 events should be tracked /// Gets or sets a boolean indicating whether the last 20 events should be tracked
/// <para>Note: setting this to <see langword="false" /> will clear the current history</para> /// <para>Note: setting this to <see langword="false" /> will clear the current history</para>
@ -46,5 +54,11 @@ namespace Artemis.Core
/// Resets the trigger count and history of this data model event /// Resets the trigger count and history of this data model event
/// </summary> /// </summary>
void Reset(); void Reset();
/// <summary>
/// Updates the event, not required for standard events but included in case your custom event needs to update every
/// tick
/// </summary>
void Update();
} }
} }

View File

@ -0,0 +1,63 @@
using System;
using System.Collections.Generic;
namespace Artemis.Core
{
internal class DataModelValueChangedEvent<T> : IDataModelEvent
{
public DataModelValueChangedEvent(DataModelPath path)
{
Path = path;
}
public DataModelPath Path { get; }
public T? LastValue { get; private set; }
public T? CurrentValue { get; private set; }
public DateTime LastTrigger { get; private set; }
public int TriggerCount { get; private set; }
public Type ArgumentsType { get; } = typeof(DataModelValueChangedEventArgs<T>);
public string TriggerPastParticiple => "changed";
public bool TrackHistory { get; set; } = false;
public DataModelEventArgs? LastEventArgumentsUntyped { get; private set; }
public List<DataModelEventArgs> EventArgumentsHistoryUntyped { get; } = new();
public void Update()
{
object? value = Path.GetValue();
if (value != null)
CurrentValue = (T?) value;
else
CurrentValue = default;
if (!Equals(LastValue, CurrentValue))
Trigger();
LastValue = CurrentValue;
}
public void Reset()
{
TriggerCount = 0;
}
private void Trigger()
{
LastEventArgumentsUntyped = new DataModelValueChangedEventArgs<T>(CurrentValue, LastValue);
LastTrigger = DateTime.Now;
TriggerCount++;
OnEventTriggered();
}
#region Events
public event EventHandler? EventTriggered;
internal virtual void OnEventTriggered()
{
EventTriggered?.Invoke(this, EventArgs.Empty);
}
#endregion
}
}

View File

@ -0,0 +1,18 @@
using Artemis.Core.DataModelExpansions;
namespace Artemis.Core
{
internal class DataModelValueChangedEventArgs<T> : DataModelEventArgs
{
public DataModelValueChangedEventArgs(T? currentValue, T? previousValue)
{
CurrentValue = currentValue;
PreviousValue = previousValue;
}
[DataModelProperty(Description = "The current value of the property")]
public T? CurrentValue { get; }
[DataModelProperty(Description = "The previous value of the property")]
public T? PreviousValue { get; }
}
}

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

@ -0,0 +1,21 @@
using System;
using System.Diagnostics;
namespace Artemis.Core.Services
{
/// <summary>
/// Contains data for the ProcessMonitor process events
/// </summary>
public class ProcessEventArgs : EventArgs
{
/// <summary>
/// Gets the process related to the event
/// </summary>
public Process Process { get; }
internal ProcessEventArgs(Process process)
{
Process = process;
}
}
}

View File

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Artemis.Core.Services
{
/// <summary>
/// A service that provides events for started and stopped processes and a list of all running processes.
/// </summary>
public interface IProcessMonitorService : IArtemisService
{
/// <summary>
/// Occurs when a process starts.
/// </summary>
event EventHandler<ProcessEventArgs> ProcessStarted;
/// <summary>
/// Occurs when a process stops.
/// </summary>
event EventHandler<ProcessEventArgs> ProcessStopped;
/// <summary>
/// Returns an enumerable with the processes running on the system.
/// </summary>
IEnumerable<Process> GetRunningProcesses();
}
}

View File

@ -0,0 +1,70 @@
using Serilog;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Timers;
namespace Artemis.Core.Services
{
internal class ProcessMonitorService : IProcessMonitorService
{
private readonly ILogger _logger;
private readonly Timer _processScanTimer;
private readonly ProcessComparer _comparer;
private Process[] _lastScannedProcesses;
public ProcessMonitorService(ILogger logger)
{
_logger = logger;
_lastScannedProcesses = Process.GetProcesses();
_processScanTimer = new Timer(1000);
_processScanTimer.Elapsed += OnTimerElapsed;
_processScanTimer.Start();
_comparer = new ProcessComparer();
}
public event EventHandler<ProcessEventArgs>? ProcessStarted;
public event EventHandler<ProcessEventArgs>? ProcessStopped;
public IEnumerable<Process> GetRunningProcesses()
{
return _lastScannedProcesses;
}
private void OnTimerElapsed(object sender, ElapsedEventArgs e)
{
Process[] newProcesses = Process.GetProcesses();
foreach (Process startedProcess in newProcesses.Except(_lastScannedProcesses, _comparer))
{
ProcessStarted?.Invoke(this, new ProcessEventArgs(startedProcess));
_logger.Debug("Started Process: {startedProcess}", startedProcess.ProcessName);
}
foreach (Process stoppedProcess in _lastScannedProcesses.Except(newProcesses, _comparer))
{
ProcessStopped?.Invoke(this, new ProcessEventArgs(stoppedProcess));
_logger.Debug("Stopped Process: {stoppedProcess}", stoppedProcess.ProcessName);
}
_lastScannedProcesses = newProcesses;
}
}
internal class ProcessComparer : IEqualityComparer<Process>
{
public bool Equals(Process? x, Process? y)
{
if (x == null && y == null) return true;
if (x == null || y == null) return false;
return x.Id == y.Id && x.ProcessName == y.ProcessName && x.SessionId == y.SessionId;
}
public int GetHashCode(Process obj)
{
if (obj == null) return 0;
return obj.Id;
}
}
}

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

@ -56,8 +56,8 @@
<TextBlock Grid.Row="0" <TextBlock Grid.Row="0"
Grid.Column="2" Grid.Column="2"
VerticalAlignment="Center" VerticalAlignment="Center"
Margin="5 0 0 0"> Margin="5 0 0 0" Text="{Binding TriggerPastParticiple}">
triggered
</TextBlock> </TextBlock>
<Border Grid.Row="0" <Border Grid.Row="0"

View File

@ -16,6 +16,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
private readonly IDataModelUIService _dataModelUIService; private readonly IDataModelUIService _dataModelUIService;
private readonly IProfileEditorService _profileEditorService; private readonly IProfileEditorService _profileEditorService;
private DateTime _lastTrigger; private DateTime _lastTrigger;
private string _triggerPastParticiple;
public DataModelConditionEventViewModel(DataModelConditionEvent dataModelConditionEvent, public DataModelConditionEventViewModel(DataModelConditionEvent dataModelConditionEvent,
IProfileEditorService profileEditorService, IProfileEditorService profileEditorService,
@ -37,6 +38,12 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
set => SetAndNotify(ref _lastTrigger, value); set => SetAndNotify(ref _lastTrigger, value);
} }
public string TriggerPastParticiple
{
get => _triggerPastParticiple;
set => SetAndNotify(ref _triggerPastParticiple, value);
}
public void Initialize() public void Initialize()
{ {
LeftSideSelectionViewModel = _dataModelUIService.GetDynamicSelectionViewModel(_profileEditorService.GetCurrentModule()); LeftSideSelectionViewModel = _dataModelUIService.GetDynamicSelectionViewModel(_profileEditorService.GetCurrentModule());
@ -44,7 +51,10 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
LeftSideSelectionViewModel.LoadEventChildren = false; LeftSideSelectionViewModel.LoadEventChildren = false;
IReadOnlyCollection<DataModelVisualizationRegistration> editors = _dataModelUIService.RegisteredDataModelEditors; IReadOnlyCollection<DataModelVisualizationRegistration> editors = _dataModelUIService.RegisteredDataModelEditors;
List<Type> supportedInputTypes = new() {typeof(DataModelEvent), typeof(DataModelEvent<>)}; List<Type> supportedInputTypes = editors.Select(e => e.SupportedType).ToList();
supportedInputTypes.AddRange(editors.Where(e => e.CompatibleConversionTypes != null).SelectMany(e => e.CompatibleConversionTypes));
supportedInputTypes.Add(typeof(DataModelEvent));
supportedInputTypes.Add(typeof(DataModelEvent<>));
LeftSideSelectionViewModel.FilterTypes = supportedInputTypes.ToArray(); LeftSideSelectionViewModel.FilterTypes = supportedInputTypes.ToArray();
LeftSideSelectionViewModel.ButtonBrush = new SolidColorBrush(Color.FromRgb(185, 164, 10)); LeftSideSelectionViewModel.ButtonBrush = new SolidColorBrush(Color.FromRgb(185, 164, 10));
@ -63,6 +73,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
if (DataModelConditionEvent.EventPath == null || !DataModelConditionEvent.EventPath.IsValid) if (DataModelConditionEvent.EventPath == null || !DataModelConditionEvent.EventPath.IsValid)
return; return;
TriggerPastParticiple = DataModelConditionEvent.GetDataModelEvent()?.TriggerPastParticiple;
List<DataModelConditionViewModel> viewModels = new(); List<DataModelConditionViewModel> viewModels = new();
foreach (DataModelConditionPart childModel in Model.Children) foreach (DataModelConditionPart childModel in Model.Children)
{ {

View File

@ -322,8 +322,6 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties
.Where(l => l.LayerPropertyGroup.LayerEffect != null && !SelectedProfileElement.LayerEffects.Contains(l.LayerPropertyGroup.LayerEffect)) .Where(l => l.LayerPropertyGroup.LayerEffect != null && !SelectedProfileElement.LayerEffects.Contains(l.LayerPropertyGroup.LayerEffect))
.ToList(); .ToList();
Items.RemoveRange(toRemove); Items.RemoveRange(toRemove);
foreach (LayerPropertyGroupViewModel layerPropertyGroupViewModel in toRemove)
layerPropertyGroupViewModel.RequestClose();
foreach (BaseLayerEffect layerEffect in SelectedProfileElement.LayerEffects) foreach (BaseLayerEffect layerEffect in SelectedProfileElement.LayerEffects)
{ {

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

@ -149,7 +149,7 @@ namespace Artemis.UI.Screens.Settings.Device.Tabs
Device.GreenScale = GreenScale / 100f; Device.GreenScale = GreenScale / 100f;
Device.BlueScale = BlueScale / 100f; Device.BlueScale = BlueScale / 100f;
_rgbService.SaveDevice(Device); _rgbService.SaveDevice(Device);
_coreService.ModuleRenderingDisabled = false; _coreService.ModuleRenderingDisabled = false;
} }

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>