mirror of
https://github.com/Artemis-RGB/Artemis
synced 2026-11-11 03:19:56 +00:00
Core - Split up RGBService into RenderService and DeviceService
Core - Removed rendering from CoreService Core - Store and restore original device LEDs when applying/clearing layouts
This commit is contained in:
1 parent
919199a8d0
commit
2a0a0f3cfd
43 files changed
+1363
-1189
No files matched your search
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Artemis.Core.DeviceProviders;
|
||||
|
||||
namespace Artemis.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Provides data about device provider related events
|
||||
/// </summary>
|
||||
public class DeviceProviderEventArgs : EventArgs
|
||||
{
|
||||
internal DeviceProviderEventArgs(DeviceProvider deviceProvider, List<ArtemisDevice> devices)
|
||||
{
|
||||
DeviceProvider = deviceProvider;
|
||||
Devices = devices;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the device provider the event is related to.
|
||||
/// </summary>
|
||||
public DeviceProvider DeviceProvider { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of the affected devices.
|
||||
/// </summary>
|
||||
public List<ArtemisDevice> Devices { get; set; }
|
||||
}
|
||||
@@ -15,11 +15,17 @@ namespace Artemis.Core;
|
||||
/// </summary>
|
||||
public class ArtemisDevice : CorePropertyChanged
|
||||
{
|
||||
private readonly List<OriginalLed> _originalLeds;
|
||||
private readonly Size _originalSize;
|
||||
private SKPath? _path;
|
||||
private SKRect _rectangle;
|
||||
|
||||
internal ArtemisDevice(IRGBDevice rgbDevice, DeviceProvider deviceProvider)
|
||||
{
|
||||
_originalLeds = new List<OriginalLed>(rgbDevice.Select(l => new OriginalLed(l)));
|
||||
Rectangle ledRectangle = new(rgbDevice.Select(x => x.Boundary));
|
||||
_originalSize = ledRectangle.Size + new Size(ledRectangle.Location.X, ledRectangle.Location.Y);
|
||||
|
||||
Identifier = rgbDevice.GetDeviceIdentifier();
|
||||
DeviceEntity = new DeviceEntity();
|
||||
RgbDevice = rgbDevice;
|
||||
@@ -48,6 +54,10 @@ public class ArtemisDevice : CorePropertyChanged
|
||||
|
||||
internal ArtemisDevice(IRGBDevice rgbDevice, DeviceProvider deviceProvider, DeviceEntity deviceEntity)
|
||||
{
|
||||
_originalLeds = new List<OriginalLed>(rgbDevice.Select(l => new OriginalLed(l)));
|
||||
Rectangle ledRectangle = new(rgbDevice.Select(x => x.Boundary));
|
||||
_originalSize = ledRectangle.Size + new Size(ledRectangle.Location.X, ledRectangle.Location.Y);
|
||||
|
||||
Identifier = rgbDevice.GetDeviceIdentifier();
|
||||
DeviceEntity = deviceEntity;
|
||||
RgbDevice = rgbDevice;
|
||||
@@ -350,6 +360,40 @@ public class ArtemisDevice : CorePropertyChanged
|
||||
return artemisLed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the most preferred device layout for this device.
|
||||
/// </summary>
|
||||
/// <returns>The most preferred device layout for this device.</returns>
|
||||
public ArtemisLayout? GetBestDeviceLayout()
|
||||
{
|
||||
ArtemisLayout? layout;
|
||||
|
||||
// Configured layout path takes precedence over all other options
|
||||
if (CustomLayoutPath != null)
|
||||
{
|
||||
layout = new ArtemisLayout(CustomLayoutPath, LayoutSource.Configured);
|
||||
if (layout.IsValid)
|
||||
return layout;
|
||||
}
|
||||
|
||||
// Look for a layout provided by the user
|
||||
layout = DeviceProvider.LoadUserLayout(this);
|
||||
if (layout.IsValid)
|
||||
return layout;
|
||||
|
||||
if (DisableDefaultLayout)
|
||||
return null;
|
||||
|
||||
// Look for a layout provided by the plugin
|
||||
layout = DeviceProvider.LoadLayout(this);
|
||||
if (layout.IsValid)
|
||||
return layout;
|
||||
|
||||
// Finally fall back to a default layout
|
||||
layout = ArtemisLayout.GetDefaultLayout(this);
|
||||
return layout;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the underlying RGB.NET device was updated
|
||||
/// </summary>
|
||||
@@ -415,8 +459,18 @@ public class ArtemisDevice : CorePropertyChanged
|
||||
/// A boolean indicating whether to remove excess LEDs present in the device but missing
|
||||
/// in the layout
|
||||
/// </param>
|
||||
internal void ApplyLayout(ArtemisLayout layout, bool createMissingLeds, bool removeExcessiveLeds)
|
||||
internal void ApplyLayout(ArtemisLayout? layout, bool createMissingLeds, bool removeExcessiveLeds)
|
||||
{
|
||||
if (layout == null)
|
||||
{
|
||||
ClearLayout();
|
||||
UpdateLeds();
|
||||
|
||||
CalculateRenderProperties();
|
||||
OnDeviceUpdated();
|
||||
return;
|
||||
}
|
||||
|
||||
if (createMissingLeds && !DeviceProvider.CreateMissingLedsSupported)
|
||||
throw new ArtemisCoreException($"Cannot apply layout with {nameof(createMissingLeds)} " +
|
||||
"set to true because the device provider does not support it");
|
||||
@@ -424,18 +478,33 @@ public class ArtemisDevice : CorePropertyChanged
|
||||
throw new ArtemisCoreException($"Cannot apply layout with {nameof(removeExcessiveLeds)} " +
|
||||
"set to true because the device provider does not support it");
|
||||
|
||||
ClearLayout();
|
||||
if (layout.IsValid)
|
||||
layout.ApplyTo(RgbDevice, createMissingLeds, removeExcessiveLeds);
|
||||
|
||||
|
||||
UpdateLeds();
|
||||
|
||||
|
||||
Layout = layout;
|
||||
Layout.ApplyDevice(this);
|
||||
|
||||
CalculateRenderProperties();
|
||||
OnDeviceUpdated();
|
||||
}
|
||||
|
||||
private void ClearLayout()
|
||||
{
|
||||
if (Layout == null)
|
||||
return;
|
||||
|
||||
RgbDevice.DeviceInfo.LayoutMetadata = null;
|
||||
RgbDevice.Size = _originalSize;
|
||||
Layout = null;
|
||||
|
||||
while (RgbDevice.Any())
|
||||
RgbDevice.RemoveLed(RgbDevice.First().Id);
|
||||
foreach (OriginalLed originalLed in _originalLeds)
|
||||
RgbDevice.AddLed(originalLed.Id, originalLed.Location, originalLed.Size, originalLed.CustomData);
|
||||
}
|
||||
|
||||
internal void ApplyToEntity()
|
||||
{
|
||||
// Other properties are computed
|
||||
|
||||
@@ -59,13 +59,13 @@ public class ArtemisLed : CorePropertyChanged
|
||||
|
||||
internal void CalculateRectangles()
|
||||
{
|
||||
Rectangle = Utilities.CreateScaleCompatibleRect(
|
||||
Rectangle = RenderScale.CreateScaleCompatibleRect(
|
||||
RgbLed.Boundary.Location.X,
|
||||
RgbLed.Boundary.Location.Y,
|
||||
RgbLed.Boundary.Size.Width,
|
||||
RgbLed.Boundary.Size.Height
|
||||
);
|
||||
AbsoluteRectangle = Utilities.CreateScaleCompatibleRect(
|
||||
AbsoluteRectangle = RenderScale.CreateScaleCompatibleRect(
|
||||
RgbLed.AbsoluteBoundary.Location.X,
|
||||
RgbLed.AbsoluteBoundary.Location.Y,
|
||||
RgbLed.AbsoluteBoundary.Size.Width,
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
using RGB.NET.Core;
|
||||
|
||||
namespace Artemis.Core;
|
||||
|
||||
internal class OriginalLed
|
||||
{
|
||||
public OriginalLed(Led source)
|
||||
{
|
||||
Id = source.Id;
|
||||
Location = source.Location;
|
||||
Size = source.Size;
|
||||
CustomData = source.CustomData;
|
||||
}
|
||||
|
||||
public LedId Id { get; set; }
|
||||
public Point Location { get; set; }
|
||||
public Size Size { get; set; }
|
||||
public object? CustomData { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Artemis.Core.Services.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a renderer that renders to a canvas.
|
||||
/// </summary>
|
||||
public interface IRenderer
|
||||
{
|
||||
/// <summary>
|
||||
/// Renders to the provided canvas, the delta is the time in seconds since the last time <see cref="Render" /> was
|
||||
/// called.
|
||||
/// </summary>
|
||||
/// <param name="canvas">The canvas to render to.</param>
|
||||
/// <param name="delta">The time in seconds since the last time <see cref="Render" /> was called.</param>
|
||||
void Render(SKCanvas canvas, double delta);
|
||||
|
||||
/// <summary>
|
||||
/// Called after the rendering has taken place.
|
||||
/// </summary>
|
||||
/// <param name="texture">The texture that the render resulted in.</param>
|
||||
void PostRender(SKTexture texture);
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Artemis.Core.SkiaSharp;
|
||||
using RGB.NET.Core;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Artemis.Core.Services.Core;
|
||||
|
||||
/// <summary>
|
||||
/// An engine drivers an update loop for a set of devices using a graphics context
|
||||
/// </summary>
|
||||
internal sealed class SurfaceManager : IDisposable
|
||||
{
|
||||
private readonly IRenderer _renderer;
|
||||
private readonly TimerUpdateTrigger _updateTrigger;
|
||||
private readonly object _renderLock = new();
|
||||
private readonly List<ArtemisDevice> _devices = new();
|
||||
private readonly SKTextureBrush _textureBrush = new(null) {CalculationMode = RenderMode.Absolute};
|
||||
|
||||
private ListLedGroup? _surfaceLedGroup;
|
||||
private SKTexture? _texture;
|
||||
|
||||
public SurfaceManager(IRenderer renderer, IManagedGraphicsContext? graphicsContext, int targetFrameRate, float renderScale)
|
||||
{
|
||||
_renderer = renderer;
|
||||
_updateTrigger = new TimerUpdateTrigger {UpdateFrequency = 1.0 / targetFrameRate};
|
||||
|
||||
GraphicsContext = graphicsContext;
|
||||
TargetFrameRate = targetFrameRate;
|
||||
RenderScale = renderScale;
|
||||
Surface = new RGBSurface();
|
||||
Surface.Updating += SurfaceOnUpdating;
|
||||
Surface.RegisterUpdateTrigger(_updateTrigger);
|
||||
|
||||
SetPaused(true);
|
||||
}
|
||||
|
||||
private void SurfaceOnUpdating(UpdatingEventArgs args)
|
||||
{
|
||||
lock (_renderLock)
|
||||
{
|
||||
SKTexture? texture = _texture;
|
||||
if (texture == null || texture.IsInvalid)
|
||||
texture = CreateTexture();
|
||||
|
||||
// Prepare a canvas
|
||||
SKCanvas canvas = texture.Surface.Canvas;
|
||||
canvas.Save();
|
||||
|
||||
// Apply scaling if necessary
|
||||
if (Math.Abs(texture.RenderScale - 1) > 0.001)
|
||||
canvas.Scale(texture.RenderScale);
|
||||
|
||||
// Fresh start!
|
||||
canvas.Clear(new SKColor(0, 0, 0));
|
||||
|
||||
try
|
||||
{
|
||||
_renderer.Render(canvas, args.DeltaTime);
|
||||
}
|
||||
finally
|
||||
{
|
||||
canvas.RestoreToCount(-1);
|
||||
canvas.Flush();
|
||||
texture.CopyPixelData();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_renderer.PostRender(texture);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IManagedGraphicsContext? GraphicsContext { get; private set; }
|
||||
public int TargetFrameRate { get; private set; }
|
||||
public float RenderScale { get; private set; }
|
||||
public RGBSurface Surface { get; }
|
||||
|
||||
public bool IsPaused { get; private set; }
|
||||
|
||||
public void AddDevices(IEnumerable<ArtemisDevice> devices)
|
||||
{
|
||||
lock (_renderLock)
|
||||
{
|
||||
foreach (ArtemisDevice artemisDevice in devices)
|
||||
{
|
||||
if (_devices.Contains(artemisDevice))
|
||||
continue;
|
||||
_devices.Add(artemisDevice);
|
||||
Surface.Attach(artemisDevice.RgbDevice);
|
||||
artemisDevice.DeviceUpdated += ArtemisDeviceOnDeviceUpdated;
|
||||
}
|
||||
|
||||
Update();
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveDevices(IEnumerable<ArtemisDevice> devices)
|
||||
{
|
||||
lock (_renderLock)
|
||||
{
|
||||
foreach (ArtemisDevice artemisDevice in devices)
|
||||
{
|
||||
artemisDevice.DeviceUpdated -= ArtemisDeviceOnDeviceUpdated;
|
||||
Surface.Detach(artemisDevice.RgbDevice);
|
||||
_devices.Remove(artemisDevice);
|
||||
}
|
||||
|
||||
Update();
|
||||
}
|
||||
}
|
||||
|
||||
public bool SetPaused(bool paused)
|
||||
{
|
||||
if (IsPaused == paused)
|
||||
return false;
|
||||
|
||||
if (paused)
|
||||
_updateTrigger.Stop();
|
||||
else
|
||||
_updateTrigger.Start();
|
||||
|
||||
IsPaused = paused;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
lock (_renderLock)
|
||||
{
|
||||
UpdateLedGroup();
|
||||
CreateTexture();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateLedGroup()
|
||||
{
|
||||
List<Led> leds = _devices.SelectMany(d => d.Leds).Select(l => l.RgbLed).ToList();
|
||||
|
||||
if (_surfaceLedGroup == null)
|
||||
{
|
||||
_surfaceLedGroup = new ListLedGroup(Surface, leds) {Brush = _textureBrush};
|
||||
LedsChanged?.Invoke(this, EventArgs.Empty);
|
||||
return;
|
||||
}
|
||||
|
||||
// Clean up the old background
|
||||
_surfaceLedGroup.Detach();
|
||||
|
||||
// Apply the application wide brush and decorator
|
||||
_surfaceLedGroup = new ListLedGroup(Surface, leds) {Brush = _textureBrush};
|
||||
LedsChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private SKTexture CreateTexture()
|
||||
{
|
||||
float evenWidth = Surface.Boundary.Size.Width;
|
||||
if (evenWidth % 2 != 0)
|
||||
evenWidth++;
|
||||
float evenHeight = Surface.Boundary.Size.Height;
|
||||
if (evenHeight % 2 != 0)
|
||||
evenHeight++;
|
||||
|
||||
int width = Math.Max(1, MathF.Min(evenWidth * RenderScale, 4096).RoundToInt());
|
||||
int height = Math.Max(1, MathF.Min(evenHeight * RenderScale, 4096).RoundToInt());
|
||||
|
||||
_texture?.Dispose();
|
||||
_texture = new SKTexture(GraphicsContext, width, height, RenderScale, _devices);
|
||||
_textureBrush.Texture = _texture;
|
||||
|
||||
return _texture;
|
||||
}
|
||||
|
||||
private void ArtemisDeviceOnDeviceUpdated(object? sender, EventArgs e)
|
||||
{
|
||||
Update();
|
||||
}
|
||||
|
||||
public event EventHandler? LedsChanged;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
SetPaused(true);
|
||||
|
||||
Surface.UnregisterUpdateTrigger(_updateTrigger);
|
||||
_updateTrigger.Dispose();
|
||||
_texture?.Dispose();
|
||||
Surface.Dispose();
|
||||
}
|
||||
|
||||
public void UpdateTargetFrameRate(int targetFrameRate)
|
||||
{
|
||||
TargetFrameRate = targetFrameRate;
|
||||
_updateTrigger.UpdateFrequency = 1.0 / TargetFrameRate;
|
||||
}
|
||||
|
||||
public void UpdateRenderScale(float renderScale)
|
||||
{
|
||||
lock (_renderLock)
|
||||
{
|
||||
RenderScale = renderScale;
|
||||
_texture?.Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateGraphicsContext(IManagedGraphicsContext? graphicsContext)
|
||||
{
|
||||
lock (_renderLock)
|
||||
{
|
||||
GraphicsContext = graphicsContext;
|
||||
_texture?.Dispose();
|
||||
_texture = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,19 +20,11 @@ namespace Artemis.Core.Services;
|
||||
/// </summary>
|
||||
internal class CoreService : ICoreService
|
||||
{
|
||||
private readonly Stopwatch _frameStopWatch;
|
||||
private readonly ILogger _logger;
|
||||
private readonly PluginSetting<LogEventLevel> _loggingLevel;
|
||||
private readonly IModuleService _moduleService;
|
||||
private readonly IPluginManagementService _pluginManagementService;
|
||||
private readonly IProfileService _profileService;
|
||||
private readonly IRgbService _rgbService;
|
||||
private readonly IScriptingService _scriptingService;
|
||||
private readonly List<Exception> _updateExceptions = new();
|
||||
private readonly IRenderService _renderService;
|
||||
|
||||
private int _frames;
|
||||
private DateTime _lastExceptionLog;
|
||||
private DateTime _lastFrameRateSample;
|
||||
|
||||
// ReSharper disable UnusedParameter.Local
|
||||
public CoreService(IContainer container,
|
||||
@@ -40,36 +32,49 @@ internal class CoreService : ICoreService
|
||||
StorageMigrationService _1, // injected to ensure migration runs early
|
||||
ISettingsService settingsService,
|
||||
IPluginManagementService pluginManagementService,
|
||||
IRgbService rgbService,
|
||||
IProfileService profileService,
|
||||
IModuleService moduleService,
|
||||
IScriptingService scriptingService)
|
||||
IScriptingService scriptingService,
|
||||
IRenderService renderService)
|
||||
{
|
||||
Constants.CorePlugin.Container = container;
|
||||
|
||||
_logger = logger;
|
||||
_pluginManagementService = pluginManagementService;
|
||||
_rgbService = rgbService;
|
||||
_profileService = profileService;
|
||||
_moduleService = moduleService;
|
||||
_scriptingService = scriptingService;
|
||||
_renderService = renderService;
|
||||
_loggingLevel = settingsService.GetSetting("Core.LoggingLevel", LogEventLevel.Debug);
|
||||
_frameStopWatch = new Stopwatch();
|
||||
|
||||
_rgbService.Surface.Updating += SurfaceOnUpdating;
|
||||
_loggingLevel.SettingChanged += (sender, args) => ApplyLoggingLevel();
|
||||
}
|
||||
|
||||
public bool IsElevated { get; set; }
|
||||
|
||||
// ReSharper restore UnusedParameter.Local
|
||||
public bool IsInitialized { get; set; }
|
||||
|
||||
protected virtual void OnFrameRendering(FrameRenderingEventArgs e)
|
||||
public void Initialize()
|
||||
{
|
||||
FrameRendering?.Invoke(this, e);
|
||||
}
|
||||
if (IsInitialized)
|
||||
throw new ArtemisCoreException("Cannot initialize the core as it is already initialized.");
|
||||
|
||||
protected virtual void OnFrameRendered(FrameRenderedEventArgs e)
|
||||
{
|
||||
FrameRendered?.Invoke(this, e);
|
||||
_logger.Information("Initializing Artemis Core version {CurrentVersion}", Constants.CurrentVersion);
|
||||
_logger.Information("Startup arguments: {StartupArguments}", Constants.StartupArguments);
|
||||
_logger.Information("Elevated permissions: {IsElevated}", IsElevated);
|
||||
_logger.Information("Stopwatch high resolution: {IsHighResolution}", Stopwatch.IsHighResolution);
|
||||
|
||||
ApplyLoggingLevel();
|
||||
|
||||
ProcessMonitor.Start();
|
||||
|
||||
// Don't remove even if it looks useless
|
||||
// Just this line should prevent a certain someone from removing HidSharp as an unused dependency as well
|
||||
Version? hidSharpVersion = Assembly.GetAssembly(typeof(HidDevice))!.GetName().Version;
|
||||
_logger.Debug("Forcing plugins to use HidSharp {HidSharpVersion}", hidSharpVersion);
|
||||
|
||||
// Initialize the services
|
||||
_pluginManagementService.CopyBuiltInPlugins();
|
||||
_pluginManagementService.LoadPlugins(IsElevated);
|
||||
_renderService.Initialize();
|
||||
|
||||
OnInitialized();
|
||||
}
|
||||
|
||||
private void ApplyLoggingLevel()
|
||||
@@ -98,131 +103,11 @@ internal class CoreService : ICoreService
|
||||
}
|
||||
}
|
||||
|
||||
private void SurfaceOnUpdating(UpdatingEventArgs args)
|
||||
{
|
||||
if (_rgbService.IsRenderPaused)
|
||||
return;
|
||||
|
||||
if (_rgbService.FlushLeds)
|
||||
{
|
||||
_rgbService.FlushLeds = false;
|
||||
_rgbService.Surface.Update(true);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_frameStopWatch.Restart();
|
||||
|
||||
foreach (GlobalScript script in _scriptingService.GlobalScripts)
|
||||
script.OnCoreUpdating(args.DeltaTime);
|
||||
|
||||
_moduleService.UpdateActiveModules(args.DeltaTime);
|
||||
SKTexture texture = _rgbService.OpenRender();
|
||||
SKCanvas canvas = texture.Surface.Canvas;
|
||||
canvas.Save();
|
||||
if (Math.Abs(texture.RenderScale - 1) > 0.001)
|
||||
canvas.Scale(texture.RenderScale);
|
||||
canvas.Clear(new SKColor(0, 0, 0));
|
||||
|
||||
if (!ProfileRenderingDisabled)
|
||||
{
|
||||
_profileService.UpdateProfiles(args.DeltaTime);
|
||||
_profileService.RenderProfiles(canvas);
|
||||
}
|
||||
|
||||
OnFrameRendering(new FrameRenderingEventArgs(canvas, args.DeltaTime, _rgbService.Surface));
|
||||
canvas.RestoreToCount(-1);
|
||||
canvas.Flush();
|
||||
|
||||
OnFrameRendered(new FrameRenderedEventArgs(texture, _rgbService.Surface));
|
||||
|
||||
foreach (GlobalScript script in _scriptingService.GlobalScripts)
|
||||
script.OnCoreUpdated(args.DeltaTime);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_updateExceptions.Add(e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_rgbService.CloseRender();
|
||||
_frameStopWatch.Stop();
|
||||
_frames++;
|
||||
|
||||
if ((DateTime.Now - _lastFrameRateSample).TotalSeconds >= 1)
|
||||
{
|
||||
FrameRate = _frames;
|
||||
_frames = 0;
|
||||
_lastFrameRateSample = DateTime.Now;
|
||||
}
|
||||
|
||||
FrameTime = _frameStopWatch.Elapsed;
|
||||
|
||||
LogUpdateExceptions();
|
||||
}
|
||||
}
|
||||
|
||||
private void LogUpdateExceptions()
|
||||
{
|
||||
// Only log update exceptions every 10 seconds to avoid spamming the logs
|
||||
if (DateTime.Now - _lastExceptionLog < TimeSpan.FromSeconds(10))
|
||||
return;
|
||||
_lastExceptionLog = DateTime.Now;
|
||||
|
||||
if (!_updateExceptions.Any())
|
||||
return;
|
||||
|
||||
// Group by stack trace, that should gather up duplicate exceptions
|
||||
foreach (IGrouping<string?, Exception> exceptions in _updateExceptions.GroupBy(e => e.StackTrace))
|
||||
_logger.Warning(exceptions.First(), "Exception was thrown {count} times during update in the last 10 seconds", exceptions.Count());
|
||||
|
||||
// When logging is finished start with a fresh slate
|
||||
_updateExceptions.Clear();
|
||||
}
|
||||
public event EventHandler? Initialized;
|
||||
|
||||
private void OnInitialized()
|
||||
{
|
||||
IsInitialized = true;
|
||||
Initialized?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
public int FrameRate { get; private set; }
|
||||
public TimeSpan FrameTime { get; private set; }
|
||||
public bool ProfileRenderingDisabled { get; set; }
|
||||
public bool IsElevated { get; set; }
|
||||
|
||||
public bool IsInitialized { get; set; }
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
if (IsInitialized)
|
||||
throw new ArtemisCoreException("Cannot initialize the core as it is already initialized.");
|
||||
|
||||
_logger.Information("Initializing Artemis Core version {CurrentVersion}", Constants.CurrentVersion);
|
||||
_logger.Information("Startup arguments: {StartupArguments}", Constants.StartupArguments);
|
||||
_logger.Information("Elevated permissions: {IsElevated}", IsElevated);
|
||||
_logger.Information("Stopwatch high resolution: {IsHighResolution}", Stopwatch.IsHighResolution);
|
||||
|
||||
ApplyLoggingLevel();
|
||||
|
||||
ProcessMonitor.Start();
|
||||
|
||||
// Don't remove even if it looks useless
|
||||
// Just this line should prevent a certain someone from removing HidSharp as an unused dependency as well
|
||||
Version? hidSharpVersion = Assembly.GetAssembly(typeof(HidDevice))!.GetName().Version;
|
||||
_logger.Debug("Forcing plugins to use HidSharp {HidSharpVersion}", hidSharpVersion);
|
||||
|
||||
// Initialize the services
|
||||
_pluginManagementService.CopyBuiltInPlugins();
|
||||
_pluginManagementService.LoadPlugins(IsElevated);
|
||||
|
||||
_rgbService.ApplyPreferredGraphicsContext(Constants.StartupArguments.Contains("--force-software-render"));
|
||||
_rgbService.SetRenderPaused(false);
|
||||
OnInitialized();
|
||||
}
|
||||
|
||||
public event EventHandler? Initialized;
|
||||
public event EventHandler<FrameRenderingEventArgs>? FrameRendering;
|
||||
public event EventHandler<FrameRenderedEventArgs>? FrameRendered;
|
||||
}
|
||||
@@ -1,22 +1,252 @@
|
||||
using System.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.Core.DeviceProviders;
|
||||
using Artemis.Core.Services.Models;
|
||||
using Artemis.Storage.Entities.Surface;
|
||||
using Artemis.Storage.Repositories.Interfaces;
|
||||
using RGB.NET.Core;
|
||||
using Serilog;
|
||||
|
||||
namespace Artemis.Core.Services;
|
||||
|
||||
internal class DeviceService : IDeviceService
|
||||
{
|
||||
private readonly IRgbService _rgbService;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IPluginManagementService _pluginManagementService;
|
||||
private readonly IDeviceRepository _deviceRepository;
|
||||
private readonly Lazy<IRenderService> _renderService;
|
||||
private readonly List<ArtemisDevice> _enabledDevices = new();
|
||||
private readonly List<ArtemisDevice> _devices = new();
|
||||
|
||||
public DeviceService(IRgbService rgbService)
|
||||
public DeviceService(ILogger logger, IPluginManagementService pluginManagementService, IDeviceRepository deviceRepository, Lazy<IRenderService> renderService)
|
||||
{
|
||||
_rgbService = rgbService;
|
||||
_logger = logger;
|
||||
_pluginManagementService = pluginManagementService;
|
||||
_deviceRepository = deviceRepository;
|
||||
_renderService = renderService;
|
||||
|
||||
EnabledDevices = new ReadOnlyCollection<ArtemisDevice>(_enabledDevices);
|
||||
Devices = new ReadOnlyCollection<ArtemisDevice>(_devices);
|
||||
|
||||
RenderScale.RenderScaleMultiplierChanged += RenderScaleOnRenderScaleMultiplierChanged;
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<ArtemisDevice> EnabledDevices { get; }
|
||||
public IReadOnlyCollection<ArtemisDevice> Devices { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void IdentifyDevice(ArtemisDevice device)
|
||||
{
|
||||
BlinkDevice(device, 0);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void AddDeviceProvider(DeviceProvider deviceProvider)
|
||||
{
|
||||
_logger.Verbose("[AddDeviceProvider] Adding {DeviceProvider}", deviceProvider.GetType().Name);
|
||||
IRGBDeviceProvider rgbDeviceProvider = deviceProvider.RgbDeviceProvider;
|
||||
|
||||
try
|
||||
{
|
||||
// Can't see why this would happen, RgbService used to do this though
|
||||
List<ArtemisDevice> toRemove = _devices.Where(a => rgbDeviceProvider.Devices.Any(d => a.RgbDevice == d)).ToList();
|
||||
_logger.Verbose("[AddDeviceProvider] Removing {Count} old device(s)", toRemove.Count);
|
||||
foreach (ArtemisDevice device in toRemove)
|
||||
{
|
||||
_devices.Remove(device);
|
||||
OnDeviceRemoved(new DeviceEventArgs(device));
|
||||
}
|
||||
|
||||
List<Exception> providerExceptions = new();
|
||||
|
||||
void DeviceProviderOnException(object? sender, ExceptionEventArgs e)
|
||||
{
|
||||
if (e.IsCritical)
|
||||
providerExceptions.Add(e.Exception);
|
||||
else
|
||||
_logger.Warning(e.Exception, "Device provider {deviceProvider} threw non-critical exception", deviceProvider.GetType().Name);
|
||||
}
|
||||
|
||||
_logger.Verbose("[AddDeviceProvider] Initializing device provider");
|
||||
rgbDeviceProvider.Exception += DeviceProviderOnException;
|
||||
rgbDeviceProvider.Initialize();
|
||||
_logger.Verbose("[AddDeviceProvider] Attaching devices of device provider");
|
||||
rgbDeviceProvider.Exception -= DeviceProviderOnException;
|
||||
if (providerExceptions.Count == 1)
|
||||
throw new ArtemisPluginException("RGB.NET threw exception: " + providerExceptions.First().Message, providerExceptions.First());
|
||||
if (providerExceptions.Count > 1)
|
||||
throw new ArtemisPluginException("RGB.NET threw multiple exceptions", new AggregateException(providerExceptions));
|
||||
|
||||
if (!rgbDeviceProvider.Devices.Any())
|
||||
{
|
||||
_logger.Warning("Device provider {deviceProvider} has no devices", deviceProvider.GetType().Name);
|
||||
return;
|
||||
}
|
||||
|
||||
List<ArtemisDevice> addedDevices = new();
|
||||
foreach (IRGBDevice rgbDevice in rgbDeviceProvider.Devices)
|
||||
{
|
||||
ArtemisDevice artemisDevice = GetArtemisDevice(rgbDevice);
|
||||
addedDevices.Add(artemisDevice);
|
||||
_devices.Add(artemisDevice);
|
||||
if (artemisDevice.IsEnabled)
|
||||
_enabledDevices.Add(artemisDevice);
|
||||
|
||||
_logger.Debug("Device provider {deviceProvider} added {deviceName}", deviceProvider.GetType().Name, rgbDevice.DeviceInfo.DeviceName);
|
||||
}
|
||||
|
||||
_devices.Sort((a, b) => a.ZIndex - b.ZIndex);
|
||||
_enabledDevices.Sort((a, b) => a.ZIndex - b.ZIndex);
|
||||
|
||||
OnDeviceProviderAdded(new DeviceProviderEventArgs(deviceProvider, addedDevices));
|
||||
foreach (ArtemisDevice artemisDevice in addedDevices)
|
||||
OnDeviceAdded(new DeviceEventArgs(artemisDevice));
|
||||
|
||||
UpdateLeds();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error(e, "Exception during device loading for device provider {deviceProvider}", deviceProvider.GetType().Name);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void RemoveDeviceProvider(DeviceProvider deviceProvider)
|
||||
{
|
||||
_logger.Verbose("[RemoveDeviceProvider] Pausing rendering to remove {DeviceProvider}", deviceProvider.GetType().Name);
|
||||
IRGBDeviceProvider rgbDeviceProvider = deviceProvider.RgbDeviceProvider;
|
||||
List<ArtemisDevice> toRemove = _devices.Where(a => rgbDeviceProvider.Devices.Any(d => a.RgbDevice == d)).ToList();
|
||||
|
||||
try
|
||||
{
|
||||
_logger.Verbose("[RemoveDeviceProvider] Removing {Count} old device(s)", toRemove.Count);
|
||||
foreach (ArtemisDevice device in toRemove)
|
||||
{
|
||||
_devices.Remove(device);
|
||||
_enabledDevices.Remove(device);
|
||||
}
|
||||
|
||||
_devices.Sort((a, b) => a.ZIndex - b.ZIndex);
|
||||
|
||||
OnDeviceProviderRemoved(new DeviceProviderEventArgs(deviceProvider, toRemove));
|
||||
foreach (ArtemisDevice artemisDevice in toRemove)
|
||||
OnDeviceRemoved(new DeviceEventArgs(artemisDevice));
|
||||
|
||||
UpdateLeds();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error(e, "Exception during device removal for device provider {deviceProvider}", deviceProvider.GetType().Name);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void AutoArrangeDevices()
|
||||
{
|
||||
SurfaceArrangement surfaceArrangement = SurfaceArrangement.GetDefaultArrangement();
|
||||
surfaceArrangement.Arrange(_devices);
|
||||
foreach (ArtemisDevice artemisDevice in _devices)
|
||||
artemisDevice.ApplyDefaultCategories();
|
||||
|
||||
SaveDevices();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ApplyDeviceLayout(ArtemisDevice device, ArtemisLayout? layout)
|
||||
{
|
||||
if (layout == null || layout.Source == LayoutSource.Default)
|
||||
device.ApplyLayout(layout, false, false);
|
||||
else
|
||||
device.ApplyLayout(layout, device.DeviceProvider.CreateMissingLedsSupported, device.DeviceProvider.RemoveExcessiveLedsSupported);
|
||||
|
||||
UpdateLeds();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void EnableDevice(ArtemisDevice device)
|
||||
{
|
||||
if (device.IsEnabled)
|
||||
return;
|
||||
|
||||
_enabledDevices.Add(device);
|
||||
device.IsEnabled = true;
|
||||
device.ApplyToEntity();
|
||||
_deviceRepository.Save(device.DeviceEntity);
|
||||
|
||||
OnDeviceEnabled(new DeviceEventArgs(device));
|
||||
UpdateLeds();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void DisableDevice(ArtemisDevice device)
|
||||
{
|
||||
if (!device.IsEnabled)
|
||||
return;
|
||||
|
||||
_enabledDevices.Remove(device);
|
||||
device.IsEnabled = false;
|
||||
device.ApplyToEntity();
|
||||
_deviceRepository.Save(device.DeviceEntity);
|
||||
|
||||
OnDeviceDisabled(new DeviceEventArgs(device));
|
||||
UpdateLeds();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SaveDevice(ArtemisDevice artemisDevice)
|
||||
{
|
||||
artemisDevice.ApplyToEntity();
|
||||
artemisDevice.ApplyToRgbDevice();
|
||||
|
||||
_deviceRepository.Save(artemisDevice.DeviceEntity);
|
||||
UpdateLeds();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SaveDevices()
|
||||
{
|
||||
foreach (ArtemisDevice artemisDevice in _devices)
|
||||
{
|
||||
artemisDevice.ApplyToEntity();
|
||||
artemisDevice.ApplyToRgbDevice();
|
||||
}
|
||||
|
||||
_deviceRepository.Save(_devices.Select(d => d.DeviceEntity));
|
||||
UpdateLeds();
|
||||
}
|
||||
|
||||
private ArtemisDevice GetArtemisDevice(IRGBDevice rgbDevice)
|
||||
{
|
||||
string deviceIdentifier = rgbDevice.GetDeviceIdentifier();
|
||||
DeviceEntity? deviceEntity = _deviceRepository.Get(deviceIdentifier);
|
||||
DeviceProvider deviceProvider = _pluginManagementService.GetDeviceProviderByDevice(rgbDevice);
|
||||
|
||||
ArtemisDevice device;
|
||||
if (deviceEntity != null)
|
||||
device = new ArtemisDevice(rgbDevice, deviceProvider, deviceEntity);
|
||||
// Fall back on creating a new device
|
||||
else
|
||||
{
|
||||
_logger.Information("No device config found for {DeviceInfo}, device hash: {DeviceHashCode}. Adding a new entry", rgbDevice.DeviceInfo, deviceIdentifier);
|
||||
device = new ArtemisDevice(rgbDevice, deviceProvider);
|
||||
}
|
||||
|
||||
device.ApplyToRgbDevice();
|
||||
ApplyDeviceLayout(device, device.GetBestDeviceLayout());
|
||||
return device;
|
||||
}
|
||||
|
||||
private void BlinkDevice(ArtemisDevice device, int blinkCount)
|
||||
{
|
||||
RGBSurface surface = _renderService.Value.Surface;
|
||||
|
||||
// Create a LED group way at the top
|
||||
ListLedGroup ledGroup = new(_rgbService.Surface, device.Leds.Select(l => l.RgbLed))
|
||||
ListLedGroup ledGroup = new(surface, device.Leds.Select(l => l.RgbLed))
|
||||
{
|
||||
Brush = new SolidColorBrush(new Color(255, 255, 255)),
|
||||
ZIndex = 999
|
||||
@@ -36,9 +266,81 @@ internal class DeviceService : IDeviceService
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void IdentifyDevice(ArtemisDevice device)
|
||||
|
||||
private void CalculateRenderProperties()
|
||||
{
|
||||
BlinkDevice(device, 0);
|
||||
foreach (ArtemisDevice artemisDevice in Devices)
|
||||
artemisDevice.CalculateRenderProperties();
|
||||
UpdateLeds();
|
||||
}
|
||||
|
||||
private void UpdateLeds()
|
||||
{
|
||||
OnLedsChanged();
|
||||
}
|
||||
|
||||
private void RenderScaleOnRenderScaleMultiplierChanged(object? sender, EventArgs e)
|
||||
{
|
||||
CalculateRenderProperties();
|
||||
}
|
||||
|
||||
#region Events
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<DeviceEventArgs>? DeviceAdded;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<DeviceEventArgs>? DeviceRemoved;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<DeviceEventArgs>? DeviceEnabled;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<DeviceEventArgs>? DeviceDisabled;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<DeviceProviderEventArgs>? DeviceProviderAdded;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<DeviceProviderEventArgs>? DeviceProviderRemoved;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler? LedsChanged;
|
||||
|
||||
protected virtual void OnDeviceAdded(DeviceEventArgs e)
|
||||
{
|
||||
DeviceAdded?.Invoke(this, e);
|
||||
}
|
||||
|
||||
protected virtual void OnDeviceRemoved(DeviceEventArgs e)
|
||||
{
|
||||
DeviceRemoved?.Invoke(this, e);
|
||||
}
|
||||
|
||||
protected virtual void OnDeviceEnabled(DeviceEventArgs e)
|
||||
{
|
||||
DeviceEnabled?.Invoke(this, e);
|
||||
}
|
||||
|
||||
protected virtual void OnDeviceDisabled(DeviceEventArgs e)
|
||||
{
|
||||
DeviceDisabled?.Invoke(this, e);
|
||||
}
|
||||
|
||||
protected virtual void OnDeviceProviderAdded(DeviceProviderEventArgs e)
|
||||
{
|
||||
DeviceProviderAdded?.Invoke(this, e);
|
||||
}
|
||||
|
||||
protected virtual void OnDeviceProviderRemoved(DeviceProviderEventArgs e)
|
||||
{
|
||||
DeviceProviderRemoved?.Invoke(this, e);
|
||||
}
|
||||
|
||||
protected virtual void OnLedsChanged()
|
||||
{
|
||||
LedsChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -9,19 +9,19 @@ namespace Artemis.Core.Services;
|
||||
internal class InputService : IInputService
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly IRgbService _rgbService;
|
||||
private readonly IDeviceService _deviceService;
|
||||
private ArtemisDevice? _firstKeyboard;
|
||||
private ArtemisDevice? _firstMouse;
|
||||
private int _keyboardCount;
|
||||
private int _mouseCount;
|
||||
|
||||
public InputService(ILogger logger, IRgbService rgbService)
|
||||
public InputService(ILogger logger, IDeviceService deviceService)
|
||||
{
|
||||
_logger = logger;
|
||||
_rgbService = rgbService;
|
||||
_deviceService = deviceService;
|
||||
|
||||
_rgbService.DeviceAdded += RgbServiceOnDevicesModified;
|
||||
_rgbService.DeviceRemoved += RgbServiceOnDevicesModified;
|
||||
_deviceService.DeviceAdded += DeviceServiceOnDevicesModified;
|
||||
_deviceService.DeviceRemoved += DeviceServiceOnDevicesModified;
|
||||
BustIdentifierCache();
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ internal class InputService : IInputService
|
||||
|
||||
_logger.Debug("Stop identifying device {device}", _identifyingDevice);
|
||||
_identifyingDevice = null;
|
||||
_rgbService.SaveDevices();
|
||||
_deviceService.SaveDevices();
|
||||
|
||||
BustIdentifierCache();
|
||||
}
|
||||
@@ -209,7 +209,7 @@ internal class InputService : IInputService
|
||||
public void BustIdentifierCache()
|
||||
{
|
||||
_deviceCache.Clear();
|
||||
_devices = _rgbService.EnabledDevices.Where(d => d.InputIdentifiers.Any()).ToList();
|
||||
_devices = _deviceService.EnabledDevices.Where(d => d.InputIdentifiers.Any()).ToList();
|
||||
}
|
||||
|
||||
private void AddDeviceToCache(ArtemisDevice match, InputProvider provider, object identifier)
|
||||
@@ -241,12 +241,12 @@ internal class InputService : IInputService
|
||||
OnDeviceIdentified();
|
||||
}
|
||||
|
||||
private void RgbServiceOnDevicesModified(object? sender, DeviceEventArgs args)
|
||||
private void DeviceServiceOnDevicesModified(object? sender, DeviceEventArgs args)
|
||||
{
|
||||
_firstKeyboard = _rgbService.Devices.FirstOrDefault(d => d.DeviceType == RGBDeviceType.Keyboard);
|
||||
_firstMouse = _rgbService.Devices.FirstOrDefault(d => d.DeviceType == RGBDeviceType.Mouse);
|
||||
_keyboardCount = _rgbService.Devices.Count(d => d.DeviceType == RGBDeviceType.Keyboard);
|
||||
_mouseCount = _rgbService.Devices.Count(d => d.DeviceType == RGBDeviceType.Mouse);
|
||||
_firstKeyboard = _deviceService.Devices.FirstOrDefault(d => d.DeviceType == RGBDeviceType.Keyboard);
|
||||
_firstMouse = _deviceService.Devices.FirstOrDefault(d => d.DeviceType == RGBDeviceType.Mouse);
|
||||
_keyboardCount = _deviceService.Devices.Count(d => d.DeviceType == RGBDeviceType.Keyboard);
|
||||
_mouseCount = _deviceService.Devices.Count(d => d.DeviceType == RGBDeviceType.Mouse);
|
||||
|
||||
BustIdentifierCache();
|
||||
}
|
||||
|
||||
@@ -12,21 +12,6 @@ public interface ICoreService : IArtemisService
|
||||
/// </summary>
|
||||
bool IsInitialized { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The time the last frame took to render
|
||||
/// </summary>
|
||||
TimeSpan FrameTime { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The amount of frames rendered each second
|
||||
/// </summary>
|
||||
public int FrameRate { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether profiles are rendered each frame by calling their Render method
|
||||
/// </summary>
|
||||
bool ProfileRenderingDisabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a boolean indicating whether Artemis is running in an elevated environment (admin permissions)
|
||||
/// </summary>
|
||||
@@ -41,14 +26,4 @@ public interface ICoreService : IArtemisService
|
||||
/// Occurs the core has finished initializing
|
||||
/// </summary>
|
||||
event EventHandler Initialized;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs whenever a frame is rendering, after modules have rendered
|
||||
/// </summary>
|
||||
event EventHandler<FrameRenderingEventArgs> FrameRendering;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs whenever a frame is finished rendering and the render pipeline is closed
|
||||
/// </summary>
|
||||
event EventHandler<FrameRenderedEventArgs> FrameRendered;
|
||||
}
|
||||
@@ -1,13 +1,109 @@
|
||||
namespace Artemis.Core.Services;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Artemis.Core.DeviceProviders;
|
||||
|
||||
namespace Artemis.Core.Services;
|
||||
|
||||
/// <summary>
|
||||
/// A service that allows you manage an <see cref="ArtemisDevice" />
|
||||
/// </summary>
|
||||
public interface IDeviceService : IArtemisService
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a read-only collection containing all enabled devices
|
||||
/// </summary>
|
||||
IReadOnlyCollection<ArtemisDevice> EnabledDevices { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a read-only collection containing all registered devices
|
||||
/// </summary>
|
||||
IReadOnlyCollection<ArtemisDevice> Devices { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Identifies the device by making it blink white 5 times
|
||||
/// </summary>
|
||||
/// <param name="device"></param>
|
||||
void IdentifyDevice(ArtemisDevice device);
|
||||
|
||||
/// <summary>
|
||||
/// Adds the given device provider and its devices.
|
||||
/// </summary>
|
||||
/// <param name="deviceProvider"></param>
|
||||
void AddDeviceProvider(DeviceProvider deviceProvider);
|
||||
|
||||
/// <summary>
|
||||
/// Removes the given device provider and its devices.
|
||||
/// </summary>
|
||||
/// <param name="deviceProvider"></param>
|
||||
void RemoveDeviceProvider(DeviceProvider deviceProvider);
|
||||
|
||||
/// <summary>
|
||||
/// Applies auto-arranging logic to the surface
|
||||
/// </summary>
|
||||
void AutoArrangeDevices();
|
||||
|
||||
/// <summary>
|
||||
/// Apples the provided <see cref="ArtemisLayout" /> to the provided <see cref="ArtemisDevice" />
|
||||
/// </summary>
|
||||
/// <param name="device"></param>
|
||||
/// <param name="layout"></param>
|
||||
void ApplyDeviceLayout(ArtemisDevice device, ArtemisLayout? layout);
|
||||
|
||||
/// <summary>
|
||||
/// Enables the provided device
|
||||
/// </summary>
|
||||
/// <param name="device">The device to enable</param>
|
||||
void EnableDevice(ArtemisDevice device);
|
||||
|
||||
/// <summary>
|
||||
/// Disables the provided device
|
||||
/// </summary>
|
||||
/// <param name="device">The device to disable</param>
|
||||
void DisableDevice(ArtemisDevice device);
|
||||
|
||||
/// <summary>
|
||||
/// Saves the configuration of the provided device to persistent storage
|
||||
/// </summary>
|
||||
/// <param name="artemisDevice"></param>
|
||||
void SaveDevice(ArtemisDevice artemisDevice);
|
||||
|
||||
/// <summary>
|
||||
/// Saves the configuration of all current devices to persistent storage
|
||||
/// </summary>
|
||||
void SaveDevices();
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a single device was added.
|
||||
/// </summary>
|
||||
event EventHandler<DeviceEventArgs> DeviceAdded;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a single device was removed.
|
||||
/// </summary>
|
||||
event EventHandler<DeviceEventArgs> DeviceRemoved;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a single device was disabled
|
||||
/// </summary>
|
||||
event EventHandler<DeviceEventArgs> DeviceEnabled;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a single device was disabled.
|
||||
/// </summary>
|
||||
event EventHandler<DeviceEventArgs> DeviceDisabled;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a device provider was added.
|
||||
/// </summary>
|
||||
event EventHandler<DeviceProviderEventArgs> DeviceProviderAdded;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a device provider was removed.
|
||||
/// </summary>
|
||||
event EventHandler<DeviceProviderEventArgs> DeviceProviderRemoved;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the surface has had modifications to its LED collection
|
||||
/// </summary>
|
||||
event EventHandler LedsChanged;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Artemis.Core.Services.Core;
|
||||
using Artemis.Core.SkiaSharp;
|
||||
using RGB.NET.Core;
|
||||
|
||||
namespace Artemis.Core.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a service that manages the render loop and renderers.
|
||||
/// </summary>
|
||||
public interface IRenderService : IArtemisService
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the graphics context to be used for rendering
|
||||
/// </summary>
|
||||
IManagedGraphicsContext? GraphicsContext { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the RGB surface to which is being rendered.
|
||||
/// </summary>
|
||||
RGBSurface Surface { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of registered renderers.
|
||||
/// </summary>
|
||||
List<IRenderer> Renderers { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a boolean indicating whether rendering is paused.
|
||||
/// </summary>
|
||||
bool IsPaused { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a boolean indicating whether to flush the RGB.NET LEDs during next update
|
||||
/// </summary>
|
||||
bool FlushLeds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The time the last frame took to render
|
||||
/// </summary>
|
||||
TimeSpan FrameTime { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The amount of frames rendered each second
|
||||
/// </summary>
|
||||
public int FrameRate { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the render service and starts rendering.
|
||||
/// </summary>
|
||||
void Initialize();
|
||||
|
||||
/// <summary>
|
||||
/// Occurs whenever a frame is rendering, after modules have rendered
|
||||
/// </summary>
|
||||
event EventHandler<FrameRenderingEventArgs> FrameRendering;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs whenever a frame is finished rendering and the render pipeline is closed
|
||||
/// </summary>
|
||||
event EventHandler<FrameRenderedEventArgs> FrameRendered;
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Artemis.Core.SkiaSharp;
|
||||
using RGB.NET.Core;
|
||||
|
||||
namespace Artemis.Core.Services;
|
||||
|
||||
/// <summary>
|
||||
/// A service that allows you to manage the <see cref="RGBSurface" /> and its contents
|
||||
/// </summary>
|
||||
public interface IRgbService : IArtemisService, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a read-only collection containing all enabled devices
|
||||
/// </summary>
|
||||
IReadOnlyCollection<ArtemisDevice> EnabledDevices { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a read-only collection containing all registered devices
|
||||
/// </summary>
|
||||
IReadOnlyCollection<ArtemisDevice> Devices { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a dictionary containing all <see cref="ArtemisLed" />s on the surface with their corresponding RGB.NET
|
||||
/// <see cref="Led" /> as key
|
||||
/// </summary>
|
||||
IReadOnlyDictionary<Led, ArtemisLed> LedMap { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the RGB surface rendering is performed on
|
||||
/// </summary>
|
||||
RGBSurface Surface { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether rendering should be paused
|
||||
/// </summary>
|
||||
bool IsRenderPaused { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a boolean indicating whether the render pipeline is open
|
||||
/// </summary>
|
||||
bool RenderOpen { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a boolean indicating whether to flush the RGB.NET LEDs during next update
|
||||
/// </summary>
|
||||
bool FlushLeds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Opens the render pipeline
|
||||
/// </summary>
|
||||
SKTexture OpenRender();
|
||||
|
||||
/// <summary>
|
||||
/// Closes the render pipeline
|
||||
/// </summary>
|
||||
void CloseRender();
|
||||
|
||||
/// <summary>
|
||||
/// Applies the current value of the <c>Core.PreferredGraphicsContext</c> setting to the graphics context.
|
||||
/// </summary>
|
||||
/// <param name="forceSoftware">A boolean to indicate whether or not to force the graphics context to software mode.</param>
|
||||
void ApplyPreferredGraphicsContext(bool forceSoftware);
|
||||
|
||||
/// <summary>
|
||||
/// Updates the graphics context to the provided <paramref name="managedGraphicsContext"></paramref>.
|
||||
/// <para>Note: The old graphics context will be used until the next frame starts rendering and is disposed afterwards.</para>
|
||||
/// </summary>
|
||||
/// <param name="managedGraphicsContext">
|
||||
/// The new managed graphics context. If <see langword="null" />, software rendering
|
||||
/// is used.
|
||||
/// </param>
|
||||
void UpdateGraphicsContext(IManagedGraphicsContext? managedGraphicsContext);
|
||||
|
||||
/// <summary>
|
||||
/// Adds the given device provider to the <see cref="Surface" />
|
||||
/// </summary>
|
||||
/// <param name="deviceProvider"></param>
|
||||
void AddDeviceProvider(IRGBDeviceProvider deviceProvider);
|
||||
|
||||
/// <summary>
|
||||
/// Removes the given device provider from the <see cref="Surface" />
|
||||
/// </summary>
|
||||
/// <param name="deviceProvider"></param>
|
||||
void RemoveDeviceProvider(IRGBDeviceProvider deviceProvider);
|
||||
|
||||
/// <summary>
|
||||
/// Applies auto-arranging logic to the surface
|
||||
/// </summary>
|
||||
void AutoArrangeDevices();
|
||||
|
||||
/// <summary>
|
||||
/// Applies the best available layout for the given <see cref="ArtemisDevice" />
|
||||
/// </summary>
|
||||
/// <param name="device">The device to apply the best available layout to</param>
|
||||
/// <returns>The layout that was applied to the device</returns>
|
||||
ArtemisLayout? ApplyBestDeviceLayout(ArtemisDevice device);
|
||||
|
||||
/// <summary>
|
||||
/// Apples the provided <see cref="ArtemisLayout" /> to the provided <see cref="ArtemisDevice" />
|
||||
/// </summary>
|
||||
/// <param name="device"></param>
|
||||
/// <param name="layout"></param>
|
||||
void ApplyDeviceLayout(ArtemisDevice device, ArtemisLayout layout);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to retrieve the <see cref="ArtemisDevice" /> that corresponds the provided RGB.NET
|
||||
/// <see cref="IRGBDevice" />
|
||||
/// </summary>
|
||||
/// <param name="rgbDevice">
|
||||
/// The RGB.NET <see cref="IRGBDevice" /> to find the corresponding <see cref="ArtemisDevice" />
|
||||
/// for
|
||||
/// </param>
|
||||
/// <returns>If found, the corresponding <see cref="ArtemisDevice" />; otherwise <see langword="null" />.</returns>
|
||||
ArtemisDevice? GetDevice(IRGBDevice rgbDevice);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to retrieve the <see cref="ArtemisLed" /> that corresponds the provided RGB.NET <see cref="Led" />
|
||||
/// </summary>
|
||||
/// <param name="led">The RGB.NET <see cref="Led" /> to find the corresponding <see cref="ArtemisLed" /> for </param>
|
||||
/// <returns>If found, the corresponding <see cref="ArtemisLed" />; otherwise <see langword="null" />.</returns>
|
||||
ArtemisLed? GetLed(Led led);
|
||||
|
||||
/// <summary>
|
||||
/// Saves the configuration of the provided device to persistent storage
|
||||
/// </summary>
|
||||
/// <param name="artemisDevice"></param>
|
||||
void SaveDevice(ArtemisDevice artemisDevice);
|
||||
|
||||
/// <summary>
|
||||
/// Saves the configuration of all current devices to persistent storage
|
||||
/// </summary>
|
||||
void SaveDevices();
|
||||
|
||||
/// <summary>
|
||||
/// Enables the provided device
|
||||
/// </summary>
|
||||
/// <param name="device">The device to enable</param>
|
||||
void EnableDevice(ArtemisDevice device);
|
||||
|
||||
/// <summary>
|
||||
/// Disables the provided device
|
||||
/// </summary>
|
||||
/// <param name="device">The device to disable</param>
|
||||
void DisableDevice(ArtemisDevice device);
|
||||
|
||||
/// <summary>
|
||||
/// Pauses or resumes rendering, method won't return until the current frame finished rendering
|
||||
/// </summary>
|
||||
/// <param name="paused"></param>
|
||||
/// <returns><see langword="true" /> if the pause state was changed; otherwise <see langword="false" />.</returns>
|
||||
bool SetRenderPaused(bool paused);
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a single device was added
|
||||
/// </summary>
|
||||
event EventHandler<DeviceEventArgs> DeviceAdded;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a single device was removed
|
||||
/// </summary>
|
||||
event EventHandler<DeviceEventArgs> DeviceRemoved;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the surface has had modifications to its LED collection
|
||||
/// </summary>
|
||||
event EventHandler LedsChanged;
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using Artemis.Core.Providers;
|
||||
using Artemis.Core.Services.Core;
|
||||
using Artemis.Core.SkiaSharp;
|
||||
using DryIoc;
|
||||
using RGB.NET.Core;
|
||||
using Serilog;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Artemis.Core.Services;
|
||||
|
||||
internal class RenderService : IRenderService, IRenderer, IDisposable
|
||||
{
|
||||
private readonly Stopwatch _frameStopWatch;
|
||||
private readonly List<Exception> _updateExceptions = new();
|
||||
|
||||
private readonly ILogger _logger;
|
||||
private readonly IDeviceService _deviceService;
|
||||
private readonly LazyEnumerable<IGraphicsContextProvider> _graphicsContextProviders;
|
||||
private readonly PluginSetting<int> _targetFrameRateSetting;
|
||||
private readonly PluginSetting<double> _renderScaleSetting;
|
||||
private readonly PluginSetting<string> _preferredGraphicsContext;
|
||||
|
||||
private SurfaceManager _surfaceManager;
|
||||
private int _frames;
|
||||
private DateTime _lastExceptionLog;
|
||||
private DateTime _lastFrameRateSample;
|
||||
private bool _initialized;
|
||||
|
||||
public RenderService(ILogger logger, ISettingsService settingsService, IDeviceService deviceService, LazyEnumerable<IGraphicsContextProvider> graphicsContextProviders)
|
||||
{
|
||||
_frameStopWatch = new Stopwatch();
|
||||
_logger = logger;
|
||||
_deviceService = deviceService;
|
||||
_graphicsContextProviders = graphicsContextProviders;
|
||||
|
||||
_targetFrameRateSetting = settingsService.GetSetting("Core.TargetFrameRate", 30);
|
||||
_renderScaleSetting = settingsService.GetSetting("Core.RenderScale", 0.25);
|
||||
_preferredGraphicsContext = settingsService.GetSetting("Core.PreferredGraphicsContext", "Software");
|
||||
_targetFrameRateSetting.SettingChanged += OnRenderSettingsChanged;
|
||||
_renderScaleSetting.SettingChanged += RenderScaleSettingOnSettingChanged;
|
||||
_preferredGraphicsContext.SettingChanged += PreferredGraphicsContextOnSettingChanged;
|
||||
|
||||
Utilities.ShutdownRequested += UtilitiesOnShutdownRequested;
|
||||
_surfaceManager = new SurfaceManager(this, GraphicsContext, _targetFrameRateSetting.Value, (float) _renderScaleSetting.Value);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IManagedGraphicsContext? GraphicsContext { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public RGBSurface Surface => _surfaceManager.Surface;
|
||||
|
||||
/// <inheritdoc />
|
||||
public List<IRenderer> Renderers { get; } = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsPaused
|
||||
{
|
||||
get => _surfaceManager.IsPaused;
|
||||
set => _surfaceManager.SetPaused(value);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int FrameRate { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public TimeSpan FrameTime { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool FlushLeds { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Render(SKCanvas canvas, double delta)
|
||||
{
|
||||
_frameStopWatch.Restart();
|
||||
|
||||
if (FlushLeds)
|
||||
{
|
||||
FlushLeds = false;
|
||||
Surface.Update(true);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
OnFrameRendering(new FrameRenderingEventArgs(canvas, delta, _surfaceManager.Surface));
|
||||
foreach (IRenderer renderer in Renderers)
|
||||
renderer.Render(canvas, delta);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_updateExceptions.Add(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void PostRender(SKTexture texture)
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (IRenderer renderer in Renderers)
|
||||
renderer.PostRender(texture);
|
||||
OnFrameRendered(new FrameRenderedEventArgs(texture, _surfaceManager.Surface));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_updateExceptions.Add(e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_frameStopWatch.Stop();
|
||||
_frames++;
|
||||
|
||||
if ((DateTime.Now - _lastFrameRateSample).TotalSeconds >= 1)
|
||||
{
|
||||
FrameRate = _frames;
|
||||
_frames = 0;
|
||||
_lastFrameRateSample = DateTime.Now;
|
||||
}
|
||||
|
||||
FrameTime = _frameStopWatch.Elapsed;
|
||||
|
||||
LogUpdateExceptions();
|
||||
}
|
||||
}
|
||||
|
||||
private void SetGraphicsContext()
|
||||
{
|
||||
if (Constants.StartupArguments.Contains("--force-software-render"))
|
||||
{
|
||||
_logger.Warning("Startup argument '--force-software-render' is applied, forcing software rendering");
|
||||
GraphicsContext = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_preferredGraphicsContext.Value == "Software")
|
||||
{
|
||||
GraphicsContext = null;
|
||||
return;
|
||||
}
|
||||
|
||||
List<IGraphicsContextProvider> providers = _graphicsContextProviders.ToList();
|
||||
if (!providers.Any())
|
||||
{
|
||||
_logger.Warning("No graphics context provider found, defaulting to software rendering");
|
||||
GraphicsContext = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
IManagedGraphicsContext? context = providers.FirstOrDefault(p => p.GraphicsContextName == _preferredGraphicsContext.Value)?.GetGraphicsContext();
|
||||
if (context == null)
|
||||
_logger.Warning("No graphics context named '{Context}' found, defaulting to software rendering", _preferredGraphicsContext.Value);
|
||||
|
||||
GraphicsContext = context;
|
||||
}
|
||||
}
|
||||
|
||||
private void LogUpdateExceptions()
|
||||
{
|
||||
// Only log update exceptions every 10 seconds to avoid spamming the logs
|
||||
if (DateTime.Now - _lastExceptionLog < TimeSpan.FromSeconds(10))
|
||||
return;
|
||||
_lastExceptionLog = DateTime.Now;
|
||||
|
||||
if (!_updateExceptions.Any())
|
||||
return;
|
||||
|
||||
// Group by stack trace, that should gather up duplicate exceptions
|
||||
foreach (IGrouping<string?, Exception> exceptions in _updateExceptions.GroupBy(e => e.StackTrace))
|
||||
_logger.Warning(exceptions.First(), "Exception was thrown {count} times during update in the last 10 seconds", exceptions.Count());
|
||||
|
||||
// When logging is finished start with a fresh slate
|
||||
_updateExceptions.Clear();
|
||||
}
|
||||
|
||||
private void DeviceServiceOnDeviceProviderAdded(object? sender, DeviceProviderEventArgs e)
|
||||
{
|
||||
_surfaceManager.AddDevices(e.Devices.Where(d => d.IsEnabled));
|
||||
}
|
||||
|
||||
private void DeviceServiceOnDeviceProviderRemoved(object? sender, DeviceProviderEventArgs e)
|
||||
{
|
||||
_surfaceManager.RemoveDevices(e.Devices);
|
||||
}
|
||||
|
||||
private void DeviceServiceOnDeviceEnabled(object? sender, DeviceEventArgs e)
|
||||
{
|
||||
_surfaceManager.AddDevices(new List<ArtemisDevice> {e.Device});
|
||||
}
|
||||
|
||||
private void DeviceServiceOnDeviceDisabled(object? sender, DeviceEventArgs e)
|
||||
{
|
||||
_surfaceManager.RemoveDevices(new List<ArtemisDevice> {e.Device});
|
||||
}
|
||||
|
||||
private void OnRenderSettingsChanged(object? sender, EventArgs e)
|
||||
{
|
||||
_surfaceManager.UpdateTargetFrameRate(_targetFrameRateSetting.Value);
|
||||
}
|
||||
|
||||
private void RenderScaleSettingOnSettingChanged(object? sender, EventArgs e)
|
||||
{
|
||||
RenderScale.SetRenderScaleMultiplier((int) (1 / _renderScaleSetting.Value));
|
||||
_surfaceManager.UpdateRenderScale((float) _renderScaleSetting.Value);
|
||||
}
|
||||
|
||||
private void PreferredGraphicsContextOnSettingChanged(object? sender, EventArgs e)
|
||||
{
|
||||
SetGraphicsContext();
|
||||
_surfaceManager.UpdateGraphicsContext(GraphicsContext);
|
||||
}
|
||||
|
||||
private void UtilitiesOnShutdownRequested(object? sender, EventArgs e)
|
||||
{
|
||||
IsPaused = true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
IsPaused = true;
|
||||
_surfaceManager.Dispose();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<FrameRenderingEventArgs>? FrameRendering;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<FrameRenderedEventArgs>? FrameRendered;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Initialize()
|
||||
{
|
||||
if (_initialized)
|
||||
return;
|
||||
|
||||
SetGraphicsContext();
|
||||
_surfaceManager.AddDevices(_deviceService.EnabledDevices);
|
||||
|
||||
_deviceService.DeviceProviderAdded += DeviceServiceOnDeviceProviderAdded;
|
||||
_deviceService.DeviceProviderRemoved += DeviceServiceOnDeviceProviderRemoved;
|
||||
_deviceService.DeviceEnabled += DeviceServiceOnDeviceEnabled;
|
||||
_deviceService.DeviceDisabled += DeviceServiceOnDeviceDisabled;
|
||||
|
||||
IsPaused = false;
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
protected virtual void OnFrameRendering(FrameRenderingEventArgs e)
|
||||
{
|
||||
FrameRendering?.Invoke(this, e);
|
||||
}
|
||||
|
||||
protected virtual void OnFrameRendered(FrameRenderedEventArgs e)
|
||||
{
|
||||
FrameRendered?.Invoke(this, e);
|
||||
}
|
||||
}
|
||||
@@ -1,613 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Artemis.Core.DeviceProviders;
|
||||
using Artemis.Core.Providers;
|
||||
using Artemis.Core.Services.Models;
|
||||
using Artemis.Core.SkiaSharp;
|
||||
using Artemis.Storage.Entities.Surface;
|
||||
using Artemis.Storage.Repositories.Interfaces;
|
||||
using DryIoc;
|
||||
using RGB.NET.Core;
|
||||
using Serilog;
|
||||
|
||||
namespace Artemis.Core.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Provides wrapped access the RGB.NET
|
||||
/// </summary>
|
||||
internal class RgbService : IRgbService
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly IPluginManagementService _pluginManagementService;
|
||||
private readonly IDeviceRepository _deviceRepository;
|
||||
private readonly LazyEnumerable<IGraphicsContextProvider> _graphicsContextProviders;
|
||||
|
||||
private readonly PluginSetting<string> _preferredGraphicsContext;
|
||||
private readonly PluginSetting<double> _renderScaleSetting;
|
||||
private readonly PluginSetting<int> _targetFrameRateSetting;
|
||||
|
||||
private readonly List<ArtemisDevice> _devices;
|
||||
private readonly List<ArtemisDevice> _enabledDevices;
|
||||
private readonly SKTextureBrush _textureBrush = new(null) {CalculationMode = RenderMode.Absolute};
|
||||
private Dictionary<Led, ArtemisLed> _ledMap;
|
||||
private ListLedGroup? _surfaceLedGroup;
|
||||
private SKTexture? _texture;
|
||||
|
||||
public RgbService(ILogger logger,
|
||||
ISettingsService settingsService,
|
||||
IPluginManagementService pluginManagementService,
|
||||
IDeviceRepository deviceRepository,
|
||||
LazyEnumerable<IGraphicsContextProvider> graphicsContextProviders)
|
||||
{
|
||||
_logger = logger;
|
||||
_pluginManagementService = pluginManagementService;
|
||||
_deviceRepository = deviceRepository;
|
||||
_graphicsContextProviders = graphicsContextProviders;
|
||||
|
||||
_targetFrameRateSetting = settingsService.GetSetting("Core.TargetFrameRate", 30);
|
||||
_renderScaleSetting = settingsService.GetSetting("Core.RenderScale", 0.25);
|
||||
_preferredGraphicsContext = settingsService.GetSetting("Core.PreferredGraphicsContext", "Software");
|
||||
|
||||
Surface = new RGBSurface();
|
||||
Utilities.RenderScaleMultiplier = (int) (1 / _renderScaleSetting.Value);
|
||||
|
||||
// Let's throw these for now
|
||||
Surface.Exception += SurfaceOnException;
|
||||
Surface.SurfaceLayoutChanged += SurfaceOnLayoutChanged;
|
||||
_targetFrameRateSetting.SettingChanged += TargetFrameRateSettingOnSettingChanged;
|
||||
_renderScaleSetting.SettingChanged += RenderScaleSettingOnSettingChanged;
|
||||
_preferredGraphicsContext.SettingChanged += PreferredGraphicsContextOnSettingChanged;
|
||||
_enabledDevices = new List<ArtemisDevice>();
|
||||
_devices = new List<ArtemisDevice>();
|
||||
_ledMap = new Dictionary<Led, ArtemisLed>();
|
||||
|
||||
EnabledDevices = new ReadOnlyCollection<ArtemisDevice>(_enabledDevices);
|
||||
Devices = new ReadOnlyCollection<ArtemisDevice>(_devices);
|
||||
LedMap = new ReadOnlyDictionary<Led, ArtemisLed>(_ledMap);
|
||||
|
||||
UpdateTrigger = new TimerUpdateTrigger {UpdateFrequency = 1.0 / _targetFrameRateSetting.Value};
|
||||
SetRenderPaused(true);
|
||||
Surface.RegisterUpdateTrigger(UpdateTrigger);
|
||||
|
||||
Utilities.ShutdownRequested += UtilitiesOnShutdownRequested;
|
||||
}
|
||||
|
||||
|
||||
public TimerUpdateTrigger UpdateTrigger { get; }
|
||||
|
||||
protected virtual void OnDeviceRemoved(DeviceEventArgs e)
|
||||
{
|
||||
DeviceRemoved?.Invoke(this, e);
|
||||
}
|
||||
|
||||
protected virtual void OnLedsChanged()
|
||||
{
|
||||
LedsChanged?.Invoke(this, EventArgs.Empty);
|
||||
_texture?.Invalidate();
|
||||
}
|
||||
|
||||
private void UtilitiesOnShutdownRequested(object? sender, EventArgs e)
|
||||
{
|
||||
SetRenderPaused(true);
|
||||
}
|
||||
|
||||
private void SurfaceOnLayoutChanged(SurfaceLayoutChangedEventArgs args)
|
||||
{
|
||||
UpdateLedGroup();
|
||||
}
|
||||
|
||||
private void UpdateLedGroup()
|
||||
{
|
||||
bool changedRenderPaused = SetRenderPaused(true);
|
||||
try
|
||||
{
|
||||
_ledMap = new Dictionary<Led, ArtemisLed>(_devices.SelectMany(d => d.Leds).ToDictionary(l => l.RgbLed));
|
||||
LedMap = new ReadOnlyDictionary<Led, ArtemisLed>(_ledMap);
|
||||
|
||||
if (_surfaceLedGroup == null)
|
||||
{
|
||||
_surfaceLedGroup = new ListLedGroup(Surface, LedMap.Select(l => l.Key)) {Brush = _textureBrush};
|
||||
OnLedsChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_surfaceLedGroup)
|
||||
{
|
||||
// Clean up the old background
|
||||
_surfaceLedGroup.Detach();
|
||||
|
||||
// Apply the application wide brush and decorator
|
||||
_surfaceLedGroup = new ListLedGroup(Surface, LedMap.Select(l => l.Key)) {Brush = _textureBrush};
|
||||
OnLedsChanged();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (changedRenderPaused)
|
||||
SetRenderPaused(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void TargetFrameRateSettingOnSettingChanged(object? sender, EventArgs e)
|
||||
{
|
||||
UpdateTrigger.UpdateFrequency = 1.0 / _targetFrameRateSetting.Value;
|
||||
}
|
||||
|
||||
private void SurfaceOnException(ExceptionEventArgs args)
|
||||
{
|
||||
_logger.Warning(args.Exception, "Surface caught exception");
|
||||
throw args.Exception;
|
||||
}
|
||||
|
||||
private void OnDeviceAdded(DeviceEventArgs e)
|
||||
{
|
||||
DeviceAdded?.Invoke(this, e);
|
||||
}
|
||||
|
||||
private void RenderScaleSettingOnSettingChanged(object? sender, EventArgs e)
|
||||
{
|
||||
Utilities.RenderScaleMultiplier = (int) (1 / _renderScaleSetting.Value);
|
||||
|
||||
_texture?.Invalidate();
|
||||
foreach (ArtemisDevice artemisDevice in Devices)
|
||||
artemisDevice.CalculateRenderProperties();
|
||||
OnLedsChanged();
|
||||
}
|
||||
|
||||
private void PreferredGraphicsContextOnSettingChanged(object? sender, EventArgs e)
|
||||
{
|
||||
ApplyPreferredGraphicsContext(false);
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<ArtemisDevice> EnabledDevices { get; }
|
||||
public IReadOnlyCollection<ArtemisDevice> Devices { get; }
|
||||
public IReadOnlyDictionary<Led, ArtemisLed> LedMap { get; private set; }
|
||||
|
||||
public RGBSurface Surface { get; set; }
|
||||
public bool IsRenderPaused { get; set; }
|
||||
public bool RenderOpen { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool FlushLeds { get; set; }
|
||||
|
||||
public void AddDeviceProvider(IRGBDeviceProvider deviceProvider)
|
||||
{
|
||||
_logger.Verbose("[AddDeviceProvider] Pausing rendering to add {DeviceProvider}", deviceProvider.GetType().Name);
|
||||
bool changedRenderPaused = SetRenderPaused(true);
|
||||
|
||||
try
|
||||
{
|
||||
List<ArtemisDevice> toRemove = _devices.Where(a => deviceProvider.Devices.Any(d => a.RgbDevice == d)).ToList();
|
||||
_logger.Verbose("[AddDeviceProvider] Removing {Count} old device(s)", toRemove.Count);
|
||||
Surface.Detach(toRemove.Select(d => d.RgbDevice));
|
||||
foreach (ArtemisDevice device in toRemove)
|
||||
RemoveDevice(device);
|
||||
|
||||
List<Exception> providerExceptions = new();
|
||||
|
||||
void DeviceProviderOnException(object? sender, ExceptionEventArgs e)
|
||||
{
|
||||
if (e.IsCritical)
|
||||
providerExceptions.Add(e.Exception);
|
||||
else
|
||||
_logger.Warning(e.Exception, "Device provider {deviceProvider} threw non-critical exception", deviceProvider.GetType().Name);
|
||||
}
|
||||
|
||||
_logger.Verbose("[AddDeviceProvider] Initializing device provider");
|
||||
deviceProvider.Exception += DeviceProviderOnException;
|
||||
deviceProvider.Initialize();
|
||||
_logger.Verbose("[AddDeviceProvider] Attaching devices of device provider");
|
||||
Surface.Attach(deviceProvider.Devices);
|
||||
deviceProvider.Exception -= DeviceProviderOnException;
|
||||
if (providerExceptions.Count == 1)
|
||||
throw new ArtemisPluginException("RGB.NET threw exception: " + providerExceptions.First().Message, providerExceptions.First());
|
||||
if (providerExceptions.Count > 1)
|
||||
throw new ArtemisPluginException("RGB.NET threw multiple exceptions", new AggregateException(providerExceptions));
|
||||
|
||||
if (!deviceProvider.Devices.Any())
|
||||
{
|
||||
_logger.Warning("Device provider {deviceProvider} has no devices", deviceProvider.GetType().Name);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (IRGBDevice rgbDevice in deviceProvider.Devices)
|
||||
{
|
||||
ArtemisDevice artemisDevice = GetArtemisDevice(rgbDevice);
|
||||
AddDevice(artemisDevice);
|
||||
_logger.Debug("Device provider {deviceProvider} added {deviceName}", deviceProvider.GetType().Name, rgbDevice.DeviceInfo.DeviceName);
|
||||
}
|
||||
|
||||
_devices.Sort((a, b) => a.ZIndex - b.ZIndex);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error(e, "Exception during device loading for device provider {deviceProvider}", deviceProvider.GetType().Name);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_logger.Verbose("[AddDeviceProvider] Updating the LED group");
|
||||
UpdateLedGroup();
|
||||
|
||||
_logger.Verbose("[AddDeviceProvider] Resuming rendering after adding {DeviceProvider}", deviceProvider.GetType().Name);
|
||||
if (changedRenderPaused)
|
||||
SetRenderPaused(false);
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveDeviceProvider(IRGBDeviceProvider deviceProvider)
|
||||
{
|
||||
_logger.Verbose("[RemoveDeviceProvider] Pausing rendering to remove {DeviceProvider}", deviceProvider.GetType().Name);
|
||||
bool changedRenderPaused = SetRenderPaused(true);
|
||||
|
||||
try
|
||||
{
|
||||
List<ArtemisDevice> toRemove = _devices.Where(a => deviceProvider.Devices.Any(d => a.RgbDevice == d)).ToList();
|
||||
_logger.Verbose("[RemoveDeviceProvider] Removing {Count} old device(s)", toRemove.Count);
|
||||
Surface.Detach(toRemove.Select(d => d.RgbDevice));
|
||||
foreach (ArtemisDevice device in toRemove)
|
||||
RemoveDevice(device);
|
||||
|
||||
_devices.Sort((a, b) => a.ZIndex - b.ZIndex);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error(e, "Exception during device removal for device provider {deviceProvider}", deviceProvider.GetType().Name);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_logger.Verbose("[RemoveDeviceProvider] Updating the LED group");
|
||||
UpdateLedGroup();
|
||||
|
||||
_logger.Verbose("[RemoveDeviceProvider] Resuming rendering after adding {DeviceProvider}", deviceProvider.GetType().Name);
|
||||
if (changedRenderPaused)
|
||||
SetRenderPaused(false);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Surface.UnregisterUpdateTrigger(UpdateTrigger);
|
||||
|
||||
UpdateTrigger.Dispose();
|
||||
Surface.Dispose();
|
||||
}
|
||||
|
||||
public bool SetRenderPaused(bool paused)
|
||||
{
|
||||
if (IsRenderPaused == paused)
|
||||
return false;
|
||||
|
||||
if (paused)
|
||||
UpdateTrigger.Stop();
|
||||
else
|
||||
UpdateTrigger.Start();
|
||||
|
||||
IsRenderPaused = paused;
|
||||
return true;
|
||||
}
|
||||
|
||||
public event EventHandler<DeviceEventArgs>? DeviceAdded;
|
||||
public event EventHandler<DeviceEventArgs>? DeviceRemoved;
|
||||
public event EventHandler? LedsChanged;
|
||||
|
||||
#region Rendering
|
||||
|
||||
private IManagedGraphicsContext? _newGraphicsContext;
|
||||
|
||||
|
||||
public SKTexture OpenRender()
|
||||
{
|
||||
if (RenderOpen)
|
||||
throw new ArtemisCoreException("Render pipeline is already open");
|
||||
|
||||
if (_texture == null || _texture.IsInvalid)
|
||||
CreateTexture();
|
||||
|
||||
RenderOpen = true;
|
||||
return _texture!;
|
||||
}
|
||||
|
||||
public void CloseRender()
|
||||
{
|
||||
if (!RenderOpen)
|
||||
throw new ArtemisCoreException("Render pipeline is already closed");
|
||||
|
||||
RenderOpen = false;
|
||||
_texture?.CopyPixelData();
|
||||
}
|
||||
|
||||
public void CreateTexture()
|
||||
{
|
||||
if (RenderOpen)
|
||||
throw new ArtemisCoreException("Cannot update the texture while rendering");
|
||||
|
||||
lock (_devices)
|
||||
{
|
||||
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 evenWidth = Surface.Boundary.Size.Width;
|
||||
if (evenWidth % 2 != 0)
|
||||
evenWidth++;
|
||||
float evenHeight = Surface.Boundary.Size.Height;
|
||||
if (evenHeight % 2 != 0)
|
||||
evenHeight++;
|
||||
|
||||
float renderScale = (float) _renderScaleSetting.Value;
|
||||
int width = Math.Max(1, MathF.Min(evenWidth * renderScale, 4096).RoundToInt());
|
||||
int height = Math.Max(1, MathF.Min(evenHeight * renderScale, 4096).RoundToInt());
|
||||
|
||||
_texture?.Dispose();
|
||||
_texture = new SKTexture(graphicsContext, width, height, renderScale, Devices);
|
||||
_textureBrush.Texture = _texture;
|
||||
|
||||
|
||||
if (!ReferenceEquals(_newGraphicsContext, Constants.ManagedGraphicsContext = _newGraphicsContext))
|
||||
{
|
||||
Constants.ManagedGraphicsContext?.Dispose();
|
||||
Constants.ManagedGraphicsContext = _newGraphicsContext;
|
||||
_newGraphicsContext = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ApplyPreferredGraphicsContext(bool forceSoftware)
|
||||
{
|
||||
if (forceSoftware)
|
||||
{
|
||||
_logger.Warning("Startup argument '--force-software-render' is applied, forcing software rendering");
|
||||
UpdateGraphicsContext(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_preferredGraphicsContext.Value == "Software")
|
||||
{
|
||||
UpdateGraphicsContext(null);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
List<IGraphicsContextProvider> providers = _graphicsContextProviders.ToList();
|
||||
if (!providers.Any())
|
||||
{
|
||||
_logger.Warning("No graphics context provider found, defaulting to software rendering");
|
||||
UpdateGraphicsContext(null);
|
||||
return;
|
||||
}
|
||||
|
||||
IManagedGraphicsContext? context = providers.FirstOrDefault(p => p.GraphicsContextName == _preferredGraphicsContext.Value)?.GetGraphicsContext();
|
||||
if (context == null)
|
||||
{
|
||||
_logger.Warning("No graphics context named '{Context}' found, defaulting to software rendering", _preferredGraphicsContext.Value);
|
||||
UpdateGraphicsContext(null);
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateGraphicsContext(context);
|
||||
}
|
||||
|
||||
public void UpdateGraphicsContext(IManagedGraphicsContext? managedGraphicsContext)
|
||||
{
|
||||
if (ReferenceEquals(managedGraphicsContext, Constants.ManagedGraphicsContext))
|
||||
return;
|
||||
|
||||
_newGraphicsContext = managedGraphicsContext;
|
||||
_texture?.Invalidate();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region EnabledDevices
|
||||
|
||||
public void AutoArrangeDevices()
|
||||
{
|
||||
bool changedRenderPaused = SetRenderPaused(true);
|
||||
|
||||
try
|
||||
{
|
||||
SurfaceArrangement surfaceArrangement = SurfaceArrangement.GetDefaultArrangement();
|
||||
surfaceArrangement.Arrange(_devices);
|
||||
foreach (ArtemisDevice artemisDevice in _devices)
|
||||
artemisDevice.ApplyDefaultCategories();
|
||||
|
||||
SaveDevices();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (changedRenderPaused)
|
||||
SetRenderPaused(false);
|
||||
}
|
||||
}
|
||||
|
||||
public ArtemisLayout? ApplyBestDeviceLayout(ArtemisDevice device)
|
||||
{
|
||||
ArtemisLayout? layout;
|
||||
|
||||
// Configured layout path takes precedence over all other options
|
||||
if (device.CustomLayoutPath != null)
|
||||
{
|
||||
layout = new ArtemisLayout(device.CustomLayoutPath, LayoutSource.Configured);
|
||||
if (layout.IsValid)
|
||||
{
|
||||
ApplyDeviceLayout(device, layout);
|
||||
return layout;
|
||||
}
|
||||
}
|
||||
|
||||
// Look for a layout provided by the user
|
||||
layout = device.DeviceProvider.LoadUserLayout(device);
|
||||
if (layout.IsValid)
|
||||
{
|
||||
ApplyDeviceLayout(device, layout);
|
||||
return layout;
|
||||
}
|
||||
|
||||
if (device.DisableDefaultLayout)
|
||||
{
|
||||
layout = null;
|
||||
ApplyDeviceLayout(device, layout);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Look for a layout provided by the plugin
|
||||
layout = device.DeviceProvider.LoadLayout(device);
|
||||
if (layout.IsValid)
|
||||
{
|
||||
ApplyDeviceLayout(device, layout);
|
||||
return layout;
|
||||
}
|
||||
|
||||
// Finally fall back to a default layout
|
||||
layout = ArtemisLayout.GetDefaultLayout(device);
|
||||
ApplyDeviceLayout(device, layout);
|
||||
return layout;
|
||||
}
|
||||
|
||||
public void ApplyDeviceLayout(ArtemisDevice device, ArtemisLayout? layout)
|
||||
{
|
||||
if (layout == null)
|
||||
{
|
||||
if (device.Layout != null)
|
||||
ReloadDevice(device);
|
||||
return;
|
||||
}
|
||||
|
||||
if (layout.Source == LayoutSource.Default)
|
||||
device.ApplyLayout(layout, false, false);
|
||||
else
|
||||
device.ApplyLayout(layout, device.DeviceProvider.CreateMissingLedsSupported, device.DeviceProvider.RemoveExcessiveLedsSupported);
|
||||
|
||||
UpdateLedGroup();
|
||||
}
|
||||
|
||||
private void ReloadDevice(ArtemisDevice device)
|
||||
{
|
||||
// Any pending changes are otherwise lost including DisableDefaultLayout
|
||||
device.ApplyToEntity();
|
||||
_deviceRepository.Save(device.DeviceEntity);
|
||||
|
||||
DeviceProvider deviceProvider = device.DeviceProvider;
|
||||
|
||||
// Feels bad but need to in order to get the initial LEDs back
|
||||
_pluginManagementService.DisablePluginFeature(deviceProvider, false);
|
||||
Thread.Sleep(500);
|
||||
_pluginManagementService.EnablePluginFeature(deviceProvider, false);
|
||||
}
|
||||
|
||||
public ArtemisDevice? GetDevice(IRGBDevice rgbDevice)
|
||||
{
|
||||
return _devices.FirstOrDefault(d => d.RgbDevice == rgbDevice);
|
||||
}
|
||||
|
||||
public ArtemisLed? GetLed(Led led)
|
||||
{
|
||||
LedMap.TryGetValue(led, out ArtemisLed? artemisLed);
|
||||
return artemisLed;
|
||||
}
|
||||
|
||||
public void EnableDevice(ArtemisDevice device)
|
||||
{
|
||||
if (device.IsEnabled)
|
||||
return;
|
||||
|
||||
_enabledDevices.Add(device);
|
||||
device.IsEnabled = true;
|
||||
device.ApplyToEntity();
|
||||
_deviceRepository.Save(device.DeviceEntity);
|
||||
|
||||
OnDeviceAdded(new DeviceEventArgs(device));
|
||||
}
|
||||
|
||||
public void DisableDevice(ArtemisDevice device)
|
||||
{
|
||||
if (!device.IsEnabled)
|
||||
return;
|
||||
|
||||
_enabledDevices.Remove(device);
|
||||
device.IsEnabled = false;
|
||||
device.ApplyToEntity();
|
||||
_deviceRepository.Save(device.DeviceEntity);
|
||||
|
||||
OnDeviceRemoved(new DeviceEventArgs(device));
|
||||
}
|
||||
|
||||
private void AddDevice(ArtemisDevice device)
|
||||
{
|
||||
if (_devices.Any(d => d.RgbDevice == device.RgbDevice))
|
||||
throw new ArtemisCoreException("Attempted to add a duplicate device to the RGB Service");
|
||||
|
||||
device.ApplyToRgbDevice();
|
||||
_devices.Add(device);
|
||||
if (device.IsEnabled)
|
||||
_enabledDevices.Add(device);
|
||||
|
||||
// Will call UpdateBitmapBrush()
|
||||
ApplyBestDeviceLayout(device);
|
||||
OnDeviceAdded(new DeviceEventArgs(device));
|
||||
}
|
||||
|
||||
private void RemoveDevice(ArtemisDevice device)
|
||||
{
|
||||
_devices.Remove(device);
|
||||
if (device.IsEnabled)
|
||||
_enabledDevices.Remove(device);
|
||||
|
||||
OnDeviceRemoved(new DeviceEventArgs(device));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Storage
|
||||
|
||||
private ArtemisDevice GetArtemisDevice(IRGBDevice rgbDevice)
|
||||
{
|
||||
string deviceIdentifier = rgbDevice.GetDeviceIdentifier();
|
||||
DeviceEntity? deviceEntity = _deviceRepository.Get(deviceIdentifier);
|
||||
DeviceProvider deviceProvider = _pluginManagementService.GetDeviceProviderByDevice(rgbDevice);
|
||||
|
||||
if (deviceEntity != null)
|
||||
return new ArtemisDevice(rgbDevice, deviceProvider, deviceEntity);
|
||||
|
||||
// Fall back on creating a new device
|
||||
_logger.Information(
|
||||
"No device config found for {deviceInfo}, device hash: {deviceHashCode}. Adding a new entry.",
|
||||
rgbDevice.DeviceInfo,
|
||||
deviceIdentifier
|
||||
);
|
||||
return new ArtemisDevice(rgbDevice, deviceProvider);
|
||||
}
|
||||
|
||||
public void SaveDevice(ArtemisDevice artemisDevice)
|
||||
{
|
||||
artemisDevice.ApplyToEntity();
|
||||
artemisDevice.ApplyToRgbDevice();
|
||||
|
||||
_deviceRepository.Save(artemisDevice.DeviceEntity);
|
||||
OnLedsChanged();
|
||||
}
|
||||
|
||||
public void SaveDevices()
|
||||
{
|
||||
foreach (ArtemisDevice artemisDevice in _devices)
|
||||
{
|
||||
artemisDevice.ApplyToEntity();
|
||||
artemisDevice.ApplyToRgbDevice();
|
||||
}
|
||||
|
||||
_deviceRepository.Save(_devices.Select(d => d.DeviceEntity));
|
||||
OnLedsChanged();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -41,6 +41,11 @@ public interface IProfileService : IArtemisService
|
||||
/// Gets or sets a value indicating whether the currently focused profile should receive updates.
|
||||
/// </summary>
|
||||
bool UpdateFocusProfile { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether profiles are rendered each frame by calling their Render method
|
||||
/// </summary>
|
||||
bool ProfileRenderingDisabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a copy of the provided profile configuration.
|
||||
|
||||
@@ -5,9 +5,9 @@ using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.Core.Modules;
|
||||
using Artemis.Core.Services.Core;
|
||||
using Artemis.Storage.Entities.Profile;
|
||||
using Artemis.Storage.Repositories.Interfaces;
|
||||
using Newtonsoft.Json;
|
||||
@@ -16,36 +16,37 @@ using SkiaSharp;
|
||||
|
||||
namespace Artemis.Core.Services;
|
||||
|
||||
internal class ProfileService : IProfileService
|
||||
internal class ProfileService : IProfileService, IRenderer
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly IRgbService _rgbService;
|
||||
private readonly IProfileCategoryRepository _profileCategoryRepository;
|
||||
private readonly IPluginManagementService _pluginManagementService;
|
||||
|
||||
private readonly IDeviceService _deviceService;
|
||||
private readonly List<ArtemisKeyboardKeyEventArgs> _pendingKeyboardEvents = new();
|
||||
private readonly List<ProfileCategory> _profileCategories;
|
||||
private readonly IProfileRepository _profileRepository;
|
||||
private readonly List<Exception> _renderExceptions = new();
|
||||
private readonly List<Exception> _updateExceptions = new();
|
||||
|
||||
private DateTime _lastRenderExceptionLog;
|
||||
private DateTime _lastUpdateExceptionLog;
|
||||
|
||||
public ProfileService(ILogger logger,
|
||||
IRgbService rgbService,
|
||||
IProfileCategoryRepository profileCategoryRepository,
|
||||
IPluginManagementService pluginManagementService,
|
||||
IInputService inputService,
|
||||
IDeviceService deviceService,
|
||||
IRenderService renderService,
|
||||
IProfileRepository profileRepository)
|
||||
{
|
||||
_logger = logger;
|
||||
_rgbService = rgbService;
|
||||
_profileCategoryRepository = profileCategoryRepository;
|
||||
_pluginManagementService = pluginManagementService;
|
||||
_deviceService = deviceService;
|
||||
_profileRepository = profileRepository;
|
||||
_profileCategories = new List<ProfileCategory>(_profileCategoryRepository.GetAll().Select(c => new ProfileCategory(c)).OrderBy(c => c.Order));
|
||||
|
||||
_rgbService.LedsChanged += RgbServiceOnLedsChanged;
|
||||
_deviceService.LedsChanged += DeviceServiceOnLedsChanged;
|
||||
_pluginManagementService.PluginFeatureEnabled += PluginManagementServiceOnPluginFeatureToggled;
|
||||
_pluginManagementService.PluginFeatureDisabled += PluginManagementServiceOnPluginFeatureToggled;
|
||||
|
||||
@@ -54,11 +55,15 @@ internal class ProfileService : IProfileService
|
||||
if (!_profileCategories.Any())
|
||||
CreateDefaultProfileCategories();
|
||||
UpdateModules();
|
||||
|
||||
renderService.Renderers.Add(this);
|
||||
}
|
||||
|
||||
public ProfileConfiguration? FocusProfile { get; set; }
|
||||
public ProfileElement? FocusProfileElement { get; set; }
|
||||
public bool UpdateFocusProfile { get; set; }
|
||||
|
||||
public bool ProfileRenderingDisabled { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void UpdateProfiles(double deltaTime)
|
||||
@@ -182,6 +187,21 @@ internal class ProfileService : IProfileService
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Render(SKCanvas canvas, double delta)
|
||||
{
|
||||
if (ProfileRenderingDisabled)
|
||||
return;
|
||||
|
||||
UpdateProfiles(delta);
|
||||
RenderProfiles(canvas);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void PostRender(SKTexture texture)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void LoadProfileConfigurationIcon(ProfileConfiguration profileConfiguration)
|
||||
@@ -235,7 +255,7 @@ internal class ProfileService : IProfileService
|
||||
throw new ArtemisCoreException($"Cannot find profile named: {profileConfiguration.Name} ID: {profileConfiguration.Entity.ProfileId}");
|
||||
|
||||
Profile profile = new(profileConfiguration, profileEntity);
|
||||
profile.PopulateLeds(_rgbService.EnabledDevices);
|
||||
profile.PopulateLeds(_deviceService.EnabledDevices);
|
||||
|
||||
if (profile.IsFreshImport)
|
||||
{
|
||||
@@ -523,7 +543,7 @@ internal class ProfileService : IProfileService
|
||||
/// <inheritdoc />
|
||||
public void AdaptProfile(Profile profile)
|
||||
{
|
||||
List<ArtemisDevice> devices = _rgbService.EnabledDevices.ToList();
|
||||
List<ArtemisDevice> devices = _deviceService.EnabledDevices.ToList();
|
||||
foreach (Layer layer in profile.GetAllLayers())
|
||||
layer.Adapter.Adapt(devices);
|
||||
|
||||
@@ -552,7 +572,7 @@ internal class ProfileService : IProfileService
|
||||
foreach (ProfileConfiguration profileConfiguration in ProfileConfigurations)
|
||||
{
|
||||
if (profileConfiguration.Profile == null) continue;
|
||||
profileConfiguration.Profile.PopulateLeds(_rgbService.EnabledDevices);
|
||||
profileConfiguration.Profile.PopulateLeds(_deviceService.EnabledDevices);
|
||||
|
||||
if (!profileConfiguration.Profile.IsFreshImport) continue;
|
||||
_logger.Debug("Profile is a fresh import, adapting to surface - {profile}", profileConfiguration.Profile);
|
||||
@@ -573,7 +593,7 @@ internal class ProfileService : IProfileService
|
||||
}
|
||||
}
|
||||
|
||||
private void RgbServiceOnLedsChanged(object? sender, EventArgs e)
|
||||
private void DeviceServiceOnLedsChanged(object? sender, EventArgs e)
|
||||
{
|
||||
ActiveProfilesPopulateLeds();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Artemis.Core;
|
||||
|
||||
internal static class RenderScale
|
||||
{
|
||||
internal static int RenderScaleMultiplier { get; private set; } = 2;
|
||||
|
||||
internal static event EventHandler? RenderScaleMultiplierChanged;
|
||||
|
||||
internal static void SetRenderScaleMultiplier(int renderScaleMultiplier)
|
||||
{
|
||||
RenderScaleMultiplier = renderScaleMultiplier;
|
||||
RenderScaleMultiplierChanged?.Invoke(null, EventArgs.Empty);
|
||||
}
|
||||
|
||||
internal static SKRectI CreateScaleCompatibleRect(float x, float y, float width, float height)
|
||||
{
|
||||
int roundX = (int) MathF.Floor(x);
|
||||
int roundY = (int) MathF.Floor(y);
|
||||
int roundWidth = (int) MathF.Ceiling(width);
|
||||
int roundHeight = (int) MathF.Ceiling(height);
|
||||
|
||||
if (RenderScaleMultiplier == 1)
|
||||
return SKRectI.Create(roundX, roundY, roundWidth, roundHeight);
|
||||
|
||||
return SKRectI.Create(
|
||||
roundX - roundX % RenderScaleMultiplier,
|
||||
roundY - roundY % RenderScaleMultiplier,
|
||||
roundWidth - roundWidth % RenderScaleMultiplier,
|
||||
roundHeight - roundHeight % RenderScaleMultiplier
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -156,28 +156,4 @@ public static class Utilities
|
||||
{
|
||||
UpdateRequested?.Invoke(null, e);
|
||||
}
|
||||
|
||||
#region Scaling
|
||||
|
||||
internal static int RenderScaleMultiplier { get; set; } = 2;
|
||||
|
||||
internal static SKRectI CreateScaleCompatibleRect(float x, float y, float width, float height)
|
||||
{
|
||||
int roundX = (int) MathF.Floor(x);
|
||||
int roundY = (int) MathF.Floor(y);
|
||||
int roundWidth = (int) MathF.Ceiling(width);
|
||||
int roundHeight = (int) MathF.Ceiling(height);
|
||||
|
||||
if (RenderScaleMultiplier == 1)
|
||||
return SKRectI.Create(roundX, roundY, roundWidth, roundHeight);
|
||||
|
||||
return SKRectI.Create(
|
||||
roundX - roundX % RenderScaleMultiplier,
|
||||
roundY - roundY % RenderScaleMultiplier,
|
||||
roundWidth - roundWidth % RenderScaleMultiplier,
|
||||
roundHeight - roundHeight % RenderScaleMultiplier
|
||||
);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in new issue
Block a user