mirror of
https://github.com/Artemis-RGB/Artemis
synced 2025-12-13 05:48:35 +00:00
Rendering - Moved bitmap management logic & prevent access violations
This commit is contained in:
parent
1a7a7e0582
commit
456af693b0
@ -94,19 +94,14 @@ namespace Artemis.Core
|
||||
|
||||
private bool EvaluateWithOperator(IEnumerable<DataModelConditionPart> targets)
|
||||
{
|
||||
switch (BooleanOperator)
|
||||
return BooleanOperator switch
|
||||
{
|
||||
case BooleanOperator.And:
|
||||
return targets.All(c => c.Evaluate());
|
||||
case BooleanOperator.Or:
|
||||
return targets.Any(c => c.Evaluate());
|
||||
case BooleanOperator.AndNot:
|
||||
return targets.All(c => !c.Evaluate());
|
||||
case BooleanOperator.OrNot:
|
||||
return targets.Any(c => !c.Evaluate());
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
BooleanOperator.And => targets.All(c => c.Evaluate()),
|
||||
BooleanOperator.Or => targets.Any(c => c.Evaluate()),
|
||||
BooleanOperator.AndNot => targets.All(c => !c.Evaluate()),
|
||||
BooleanOperator.OrNot => targets.Any(c => !c.Evaluate()),
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
}
|
||||
|
||||
#region IDisposable
|
||||
|
||||
@ -10,12 +10,15 @@ namespace Artemis.Core
|
||||
/// </summary>
|
||||
public sealed class SKTexture : PixelTexture<byte>, IDisposable
|
||||
{
|
||||
private bool _disposed;
|
||||
|
||||
#region Constructors
|
||||
|
||||
internal SKTexture(SKBitmap bitmap)
|
||||
: base(bitmap.Width, bitmap.Height, 4, new AverageByteSampler())
|
||||
internal SKTexture(int width, int height, float renderScale)
|
||||
: base(width, height, 4, new AverageByteSampler())
|
||||
{
|
||||
Bitmap = bitmap;
|
||||
Bitmap = new SKBitmap(new SKImageInfo(width, height, SKColorType.Rgb888x));
|
||||
RenderScale = renderScale;
|
||||
}
|
||||
|
||||
#endregion
|
||||
@ -40,11 +43,31 @@ namespace Artemis.Core
|
||||
/// <summary>
|
||||
/// Gets the color data in RGB format
|
||||
/// </summary>
|
||||
protected override ReadOnlySpan<byte> Data => Bitmap.GetPixelSpan();
|
||||
protected override ReadOnlySpan<byte> Data => _disposed ? new ReadOnlySpan<byte>() : Bitmap.GetPixelSpan();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the render scale of the texture
|
||||
/// </summary>
|
||||
public float RenderScale { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a boolean indicating whether <see cref="Invalidate" /> has been called on this texture, indicating it should
|
||||
/// be replaced
|
||||
/// </summary>
|
||||
public bool IsInvalid { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Invalidates the texture
|
||||
/// </summary>
|
||||
public void Invalidate()
|
||||
{
|
||||
IsInvalid = true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
_disposed = true;
|
||||
Bitmap.Dispose();
|
||||
}
|
||||
|
||||
|
||||
@ -29,14 +29,11 @@ namespace Artemis.Core.Services
|
||||
private readonly PluginSetting<LogEventLevel> _loggingLevel;
|
||||
private readonly IPluginManagementService _pluginManagementService;
|
||||
private readonly IProfileService _profileService;
|
||||
private readonly PluginSetting<double> _renderScale;
|
||||
private readonly IRgbService _rgbService;
|
||||
private readonly List<Exception> _updateExceptions = new();
|
||||
private List<BaseDataModelExpansion> _dataModelExpansions = new();
|
||||
private DateTime _lastExceptionLog;
|
||||
private List<Module> _modules = new();
|
||||
private SKBitmap? _bitmap;
|
||||
private readonly object _bitmapLock = new();
|
||||
|
||||
// ReSharper disable UnusedParameter.Local
|
||||
public CoreService(IKernel kernel,
|
||||
@ -57,17 +54,13 @@ namespace Artemis.Core.Services
|
||||
_rgbService = rgbService;
|
||||
_profileService = profileService;
|
||||
_loggingLevel = settingsService.GetSetting("Core.LoggingLevel", LogEventLevel.Debug);
|
||||
_renderScale = settingsService.GetSetting("Core.RenderScale", 0.5);
|
||||
_frameStopWatch = new Stopwatch();
|
||||
StartupArguments = new List<string>();
|
||||
|
||||
UpdatePluginCache();
|
||||
|
||||
_rgbService.Surface.Updating += SurfaceOnUpdating;
|
||||
_rgbService.Surface.Updated += SurfaceOnUpdated;
|
||||
_rgbService.Surface.SurfaceLayoutChanged += SurfaceOnSurfaceLayoutChanged;
|
||||
_loggingLevel.SettingChanged += (sender, args) => ApplyLoggingLevel();
|
||||
_renderScale.SettingChanged += RenderScaleSettingChanged;
|
||||
|
||||
_pluginManagementService.PluginFeatureEnabled += (sender, args) => UpdatePluginCache();
|
||||
_pluginManagementService.PluginFeatureDisabled += (sender, args) => UpdatePluginCache();
|
||||
@ -75,6 +68,133 @@ namespace Artemis.Core.Services
|
||||
|
||||
// ReSharper restore UnusedParameter.Local
|
||||
|
||||
protected virtual void OnFrameRendering(FrameRenderingEventArgs e)
|
||||
{
|
||||
FrameRendering?.Invoke(this, e);
|
||||
}
|
||||
|
||||
protected virtual void OnFrameRendered(FrameRenderedEventArgs e)
|
||||
{
|
||||
FrameRendered?.Invoke(this, e);
|
||||
}
|
||||
|
||||
private void UpdatePluginCache()
|
||||
{
|
||||
_modules = _pluginManagementService.GetFeaturesOfType<Module>().Where(p => p.IsEnabled).ToList();
|
||||
_dataModelExpansions = _pluginManagementService.GetFeaturesOfType<BaseDataModelExpansion>().Where(p => p.IsEnabled).ToList();
|
||||
}
|
||||
|
||||
private void ApplyLoggingLevel()
|
||||
{
|
||||
string? argument = StartupArguments.FirstOrDefault(a => a.StartsWith("--logging"));
|
||||
if (argument != null)
|
||||
{
|
||||
// Parse the provided log level
|
||||
string[] parts = argument.Split('=');
|
||||
if (parts.Length == 2 && Enum.TryParse(typeof(LogEventLevel), parts[1], true, out object? logLevelArgument))
|
||||
{
|
||||
_logger.Information("Setting logging level to {loggingLevel} from startup argument", (LogEventLevel) logLevelArgument!);
|
||||
LoggerProvider.LoggingLevelSwitch.MinimumLevel = (LogEventLevel) logLevelArgument;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Warning("Failed to set log level from startup argument {argument}", argument);
|
||||
_logger.Information("Setting logging level to {loggingLevel}", _loggingLevel.Value);
|
||||
LoggerProvider.LoggingLevelSwitch.MinimumLevel = _loggingLevel.Value;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Information("Setting logging level to {loggingLevel}", _loggingLevel.Value);
|
||||
LoggerProvider.LoggingLevelSwitch.MinimumLevel = _loggingLevel.Value;
|
||||
}
|
||||
}
|
||||
|
||||
private void SurfaceOnUpdating(UpdatingEventArgs args)
|
||||
{
|
||||
if (_rgbService.IsRenderPaused)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
_frameStopWatch.Restart();
|
||||
lock (_dataModelExpansions)
|
||||
{
|
||||
// Update all active modules, check Enabled status because it may go false before before the _dataModelExpansions list is updated
|
||||
foreach (BaseDataModelExpansion dataModelExpansion in _dataModelExpansions.Where(e => e.IsEnabled))
|
||||
dataModelExpansion.InternalUpdate(args.DeltaTime);
|
||||
}
|
||||
|
||||
List<Module> modules;
|
||||
lock (_modules)
|
||||
{
|
||||
modules = _modules.Where(m => m.IsActivated || m.InternalExpandsMainDataModel)
|
||||
.OrderBy(m => m.PriorityCategory)
|
||||
.ThenByDescending(m => m.Priority)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
// Update all active modules
|
||||
foreach (Module module in modules)
|
||||
module.InternalUpdate(args.DeltaTime);
|
||||
|
||||
// Render all active modules
|
||||
SKTexture texture =_rgbService.OpenRender();
|
||||
|
||||
using (SKCanvas canvas = new(texture.Bitmap))
|
||||
{
|
||||
canvas.Scale(texture.RenderScale);
|
||||
canvas.Clear(new SKColor(0, 0, 0));
|
||||
// While non-activated modules may be updated above if they expand the main data model, they may never render
|
||||
if (!ModuleRenderingDisabled)
|
||||
{
|
||||
foreach (Module module in modules.Where(m => m.IsActivated))
|
||||
module.InternalRender(args.DeltaTime, canvas, texture.Bitmap.Info);
|
||||
}
|
||||
|
||||
OnFrameRendering(new FrameRenderingEventArgs(canvas, args.DeltaTime, _rgbService.Surface));
|
||||
}
|
||||
|
||||
OnFrameRendered(new FrameRenderedEventArgs(texture, _rgbService.Surface));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_updateExceptions.Add(e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_frameStopWatch.Stop();
|
||||
FrameTime = _frameStopWatch.Elapsed;
|
||||
_rgbService.CloseRender();
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
private void OnInitialized()
|
||||
{
|
||||
IsInitialized = true;
|
||||
Initialized?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
public TimeSpan FrameTime { get; private set; }
|
||||
public bool ModuleRenderingDisabled { get; set; }
|
||||
public List<string> StartupArguments { get; set; }
|
||||
@ -123,16 +243,6 @@ namespace Artemis.Core.Services
|
||||
OnInitialized();
|
||||
}
|
||||
|
||||
protected virtual void OnFrameRendering(FrameRenderingEventArgs e)
|
||||
{
|
||||
FrameRendering?.Invoke(this, e);
|
||||
}
|
||||
|
||||
protected virtual void OnFrameRendered(FrameRenderedEventArgs e)
|
||||
{
|
||||
FrameRendered?.Invoke(this, e);
|
||||
}
|
||||
|
||||
public void PlayIntroAnimation()
|
||||
{
|
||||
IntroAnimation intro = new(_logger, _profileService, _rgbService.EnabledDevices);
|
||||
@ -156,155 +266,8 @@ namespace Artemis.Core.Services
|
||||
});
|
||||
}
|
||||
|
||||
private void UpdatePluginCache()
|
||||
{
|
||||
_modules = _pluginManagementService.GetFeaturesOfType<Module>().Where(p => p.IsEnabled).ToList();
|
||||
_dataModelExpansions = _pluginManagementService.GetFeaturesOfType<BaseDataModelExpansion>().Where(p => p.IsEnabled).ToList();
|
||||
}
|
||||
|
||||
private void ApplyLoggingLevel()
|
||||
{
|
||||
string? argument = StartupArguments.FirstOrDefault(a => a.StartsWith("--logging"));
|
||||
if (argument != null)
|
||||
{
|
||||
// Parse the provided log level
|
||||
string[] parts = argument.Split('=');
|
||||
if (parts.Length == 2 && Enum.TryParse(typeof(LogEventLevel), parts[1], true, out object? logLevelArgument))
|
||||
{
|
||||
_logger.Information("Setting logging level to {loggingLevel} from startup argument", (LogEventLevel)logLevelArgument!);
|
||||
LoggerProvider.LoggingLevelSwitch.MinimumLevel = (LogEventLevel)logLevelArgument;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Warning("Failed to set log level from startup argument {argument}", argument);
|
||||
_logger.Information("Setting logging level to {loggingLevel}", _loggingLevel.Value);
|
||||
LoggerProvider.LoggingLevelSwitch.MinimumLevel = _loggingLevel.Value;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Information("Setting logging level to {loggingLevel}", _loggingLevel.Value);
|
||||
LoggerProvider.LoggingLevelSwitch.MinimumLevel = _loggingLevel.Value;
|
||||
}
|
||||
}
|
||||
|
||||
private void SurfaceOnUpdating(UpdatingEventArgs args)
|
||||
{
|
||||
if (_rgbService.IsRenderPaused)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
_frameStopWatch.Restart();
|
||||
lock (_dataModelExpansions)
|
||||
{
|
||||
// Update all active modules, check Enabled status because it may go false before before the _dataModelExpansions list is updated
|
||||
foreach (BaseDataModelExpansion dataModelExpansion in _dataModelExpansions.Where(e => e.IsEnabled))
|
||||
dataModelExpansion.InternalUpdate(args.DeltaTime);
|
||||
}
|
||||
|
||||
List<Module> modules;
|
||||
lock (_modules)
|
||||
{
|
||||
modules = _modules.Where(m => m.IsActivated || m.InternalExpandsMainDataModel)
|
||||
.OrderBy(m => m.PriorityCategory)
|
||||
.ThenByDescending(m => m.Priority)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
// Update all active modules
|
||||
foreach (Module module in modules)
|
||||
module.InternalUpdate(args.DeltaTime);
|
||||
|
||||
lock (_bitmapLock)
|
||||
{
|
||||
if (_bitmap == null)
|
||||
{
|
||||
_bitmap = CreateBitmap();
|
||||
_rgbService.UpdateTexture(_bitmap);
|
||||
}
|
||||
|
||||
// Render all active modules
|
||||
using SKCanvas canvas = new(_bitmap);
|
||||
canvas.Scale((float)_renderScale.Value);
|
||||
canvas.Clear(new SKColor(0, 0, 0));
|
||||
if (!ModuleRenderingDisabled)
|
||||
// While non-activated modules may be updated above if they expand the main data model, they may never render
|
||||
foreach (Module module in modules.Where(m => m.IsActivated))
|
||||
module.InternalRender(args.DeltaTime, canvas, _bitmap.Info);
|
||||
|
||||
OnFrameRendering(new FrameRenderingEventArgs(canvas, args.DeltaTime, _rgbService.Surface));
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_updateExceptions.Add(e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_frameStopWatch.Stop();
|
||||
FrameTime = _frameStopWatch.Elapsed;
|
||||
|
||||
LogUpdateExceptions();
|
||||
}
|
||||
}
|
||||
|
||||
private SKBitmap CreateBitmap()
|
||||
{
|
||||
float width = MathF.Min(_rgbService.Surface.Boundary.Size.Width * (float)_renderScale.Value, 4096);
|
||||
float height = MathF.Min(_rgbService.Surface.Boundary.Size.Height * (float)_renderScale.Value, 4096);
|
||||
return new SKBitmap(new SKImageInfo(width.RoundToInt(), height.RoundToInt(), SKColorType.Rgb888x));
|
||||
}
|
||||
|
||||
private void InvalidateBitmap()
|
||||
{
|
||||
lock (_bitmapLock)
|
||||
{
|
||||
_bitmap = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void SurfaceOnSurfaceLayoutChanged(SurfaceLayoutChangedEventArgs args) => InvalidateBitmap();
|
||||
private void RenderScaleSettingChanged(object? sender, EventArgs e) => InvalidateBitmap();
|
||||
|
||||
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 SurfaceOnUpdated(UpdatedEventArgs args)
|
||||
{
|
||||
if (_rgbService.IsRenderPaused)
|
||||
return;
|
||||
|
||||
OnFrameRendered(new FrameRenderedEventArgs(_rgbService.Texture!, _rgbService.Surface));
|
||||
}
|
||||
|
||||
#region Events
|
||||
|
||||
public event EventHandler? Initialized;
|
||||
public event EventHandler<FrameRenderingEventArgs>? FrameRendering;
|
||||
public event EventHandler<FrameRenderedEventArgs>? FrameRendered;
|
||||
|
||||
private void OnInitialized()
|
||||
{
|
||||
IsInitialized = true;
|
||||
Initialized?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@ -54,7 +54,7 @@ namespace Artemis.Core.Services
|
||||
event EventHandler<FrameRenderingEventArgs> FrameRendering;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs whenever a frame is finished rendering and processed by RGB.NET
|
||||
/// Occurs whenever a frame is finished rendering and the render pipeline is closed
|
||||
/// </summary>
|
||||
event EventHandler<FrameRenderedEventArgs> FrameRendered;
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using RGB.NET.Core;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Artemis.Core.Services
|
||||
{
|
||||
@ -31,31 +30,25 @@ namespace Artemis.Core.Services
|
||||
/// </summary>
|
||||
RGBSurface Surface { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the texture brush used to convert the rendered frame to LED-colors
|
||||
/// </summary>
|
||||
TextureBrush TextureBrush { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the texture used to convert the rendered frame to LED-colors
|
||||
/// </summary>
|
||||
SKTexture? Texture { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the update trigger that drives the render loop
|
||||
/// </summary>
|
||||
TimerUpdateTrigger UpdateTrigger { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether rendering should be paused
|
||||
/// </summary>
|
||||
bool IsRenderPaused { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Recreates the Texture to use the given <see cref="SKBitmap" />
|
||||
/// Gets a boolean indicating whether the render pipeline is open
|
||||
/// </summary>
|
||||
/// <param name="bitmap"></param>
|
||||
void UpdateTexture(SKBitmap bitmap);
|
||||
bool RenderOpen { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Opens the render pipeline
|
||||
/// </summary>
|
||||
SKTexture OpenRender();
|
||||
|
||||
/// <summary>
|
||||
/// Closes the render pipeline
|
||||
/// </summary>
|
||||
void CloseRender();
|
||||
|
||||
/// <summary>
|
||||
/// Adds the given device provider to the <see cref="Surface" />
|
||||
|
||||
@ -17,16 +17,18 @@ namespace Artemis.Core.Services
|
||||
/// </summary>
|
||||
internal class RgbService : IRgbService
|
||||
{
|
||||
private readonly IDeviceRepository _deviceRepository;
|
||||
private readonly List<ArtemisDevice> _devices;
|
||||
private readonly List<ArtemisDevice> _enabledDevices;
|
||||
private Dictionary<Led, ArtemisLed> _ledMap;
|
||||
|
||||
private readonly ILogger _logger;
|
||||
private readonly IPluginManagementService _pluginManagementService;
|
||||
private readonly IDeviceRepository _deviceRepository;
|
||||
private readonly PluginSetting<double> _renderScaleSetting;
|
||||
private readonly PluginSetting<int> _targetFrameRateSetting;
|
||||
private ListLedGroup? _surfaceLedGroup;
|
||||
private readonly TextureBrush _textureBrush = new(ITexture.Empty) {CalculationMode = RenderMode.Absolute};
|
||||
private Dictionary<Led, ArtemisLed> _ledMap;
|
||||
private bool _modifyingProviders;
|
||||
private ListLedGroup? _surfaceLedGroup;
|
||||
private SKTexture? _texture;
|
||||
|
||||
public RgbService(ILogger logger, ISettingsService settingsService, IPluginManagementService pluginManagementService, IDeviceRepository deviceRepository)
|
||||
{
|
||||
@ -34,6 +36,7 @@ namespace Artemis.Core.Services
|
||||
_pluginManagementService = pluginManagementService;
|
||||
_deviceRepository = deviceRepository;
|
||||
_targetFrameRateSetting = settingsService.GetSetting("Core.TargetFrameRate", 25);
|
||||
_renderScaleSetting = settingsService.GetSetting("Core.RenderScale", 0.5);
|
||||
|
||||
Surface = new RGBSurface();
|
||||
|
||||
@ -45,10 +48,72 @@ namespace Artemis.Core.Services
|
||||
_devices = new List<ArtemisDevice>();
|
||||
_ledMap = new Dictionary<Led, ArtemisLed>();
|
||||
|
||||
UpdateTrigger = new TimerUpdateTrigger { UpdateFrequency = 1.0 / _targetFrameRateSetting.Value };
|
||||
UpdateTrigger = new TimerUpdateTrigger {UpdateFrequency = 1.0 / _targetFrameRateSetting.Value};
|
||||
Surface.RegisterUpdateTrigger(UpdateTrigger);
|
||||
}
|
||||
|
||||
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 SurfaceOnLayoutChanged(SurfaceLayoutChangedEventArgs args)
|
||||
{
|
||||
UpdateLedGroup();
|
||||
}
|
||||
|
||||
private void UpdateLedGroup()
|
||||
{
|
||||
lock (_devices)
|
||||
{
|
||||
if (_modifyingProviders)
|
||||
return;
|
||||
|
||||
_ledMap = new Dictionary<Led, ArtemisLed>(_devices.SelectMany(d => d.Leds).ToDictionary(l => l.RgbLed));
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void TargetFrameRateSettingOnSettingChanged(object? sender, EventArgs e)
|
||||
{
|
||||
UpdateTrigger.UpdateFrequency = 1.0 / _targetFrameRateSetting.Value;
|
||||
}
|
||||
|
||||
private void SurfaceOnException(ExceptionEventArgs args)
|
||||
{
|
||||
_logger.Warning("Surface threw e");
|
||||
throw args.Exception;
|
||||
}
|
||||
|
||||
private void OnDeviceAdded(DeviceEventArgs e)
|
||||
{
|
||||
DeviceAdded?.Invoke(this, e);
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<ArtemisDevice> EnabledDevices => _enabledDevices.AsReadOnly();
|
||||
public IReadOnlyCollection<ArtemisDevice> Devices => _devices.AsReadOnly();
|
||||
public IReadOnlyDictionary<Led, ArtemisLed> LedMap => new ReadOnlyDictionary<Led, ArtemisLed>(_ledMap);
|
||||
@ -56,11 +121,8 @@ namespace Artemis.Core.Services
|
||||
/// <inheritdoc />
|
||||
public RGBSurface Surface { get; set; }
|
||||
|
||||
public TimerUpdateTrigger UpdateTrigger { get; }
|
||||
public TextureBrush TextureBrush { get; private set; } = new(ITexture.Empty) { CalculationMode = RenderMode.Absolute };
|
||||
public SKTexture? Texture { get; private set; }
|
||||
|
||||
public bool IsRenderPaused { get; set; }
|
||||
public bool RenderOpen { get; private set; }
|
||||
|
||||
public void AddDeviceProvider(IRGBDeviceProvider deviceProvider)
|
||||
{
|
||||
@ -88,7 +150,7 @@ namespace Artemis.Core.Services
|
||||
{
|
||||
ArtemisDevice artemisDevice = GetArtemisDevice(rgbDevice);
|
||||
AddDevice(artemisDevice);
|
||||
_logger.Debug("Device provider {deviceProvider} added {deviceName}", deviceProvider.GetType().Name, rgbDevice.DeviceInfo?.DeviceName);
|
||||
_logger.Debug("Device provider {deviceProvider} added {deviceName}", deviceProvider.GetType().Name, rgbDevice.DeviceInfo.DeviceName);
|
||||
}
|
||||
|
||||
_devices.Sort((a, b) => a.ZIndex - b.ZIndex);
|
||||
@ -134,46 +196,6 @@ namespace Artemis.Core.Services
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateTexture(SKBitmap bitmap)
|
||||
{
|
||||
SKTexture? oldTexture = Texture;
|
||||
Texture = new SKTexture(bitmap);
|
||||
TextureBrush.Texture = Texture;
|
||||
|
||||
oldTexture?.Dispose();
|
||||
}
|
||||
|
||||
private void SurfaceOnLayoutChanged(SurfaceLayoutChangedEventArgs args) => UpdateLedGroup();
|
||||
|
||||
private void UpdateLedGroup()
|
||||
{
|
||||
lock (_devices)
|
||||
{
|
||||
if (_modifyingProviders)
|
||||
return;
|
||||
|
||||
_ledMap = new Dictionary<Led, ArtemisLed>(_devices.SelectMany(d => d.Leds).ToDictionary(l => l.RgbLed));
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#region IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
@ -183,6 +205,48 @@ namespace Artemis.Core.Services
|
||||
Surface.Dispose();
|
||||
}
|
||||
|
||||
public event EventHandler<DeviceEventArgs>? DeviceAdded;
|
||||
public event EventHandler<DeviceEventArgs>? DeviceRemoved;
|
||||
public event EventHandler? LedsChanged;
|
||||
|
||||
#region Rendering
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public void CreateTexture()
|
||||
{
|
||||
if (RenderOpen)
|
||||
throw new ArtemisCoreException("Cannot update the texture while rendering");
|
||||
|
||||
SKTexture? oldTexture = _texture;
|
||||
|
||||
float renderScale = (float) _renderScaleSetting.Value;
|
||||
int width = Math.Max(1, MathF.Min(Surface.Boundary.Size.Width * renderScale, 4096).RoundToInt());
|
||||
int height = Math.Max(1, MathF.Min(Surface.Boundary.Size.Height * renderScale, 4096).RoundToInt());
|
||||
_texture = new SKTexture(width, height, renderScale);
|
||||
_textureBrush.Texture = _texture;
|
||||
|
||||
oldTexture?.Dispose();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region EnabledDevices
|
||||
@ -197,7 +261,7 @@ namespace Artemis.Core.Services
|
||||
public ArtemisLayout ApplyBestDeviceLayout(ArtemisDevice device)
|
||||
{
|
||||
ArtemisLayout layout;
|
||||
|
||||
|
||||
// Configured layout path takes precedence over all other options
|
||||
if (device.CustomLayoutPath != null)
|
||||
{
|
||||
@ -233,7 +297,7 @@ namespace Artemis.Core.Services
|
||||
|
||||
private ArtemisLayout LoadDefaultLayout(ArtemisDevice device)
|
||||
{
|
||||
return new ArtemisLayout("NYI", LayoutSource.Default);
|
||||
return new("NYI", LayoutSource.Default);
|
||||
}
|
||||
|
||||
public void ApplyDeviceLayout(ArtemisDevice device, ArtemisLayout layout, bool createMissingLeds, bool removeExessiveLeds)
|
||||
@ -347,43 +411,5 @@ namespace Artemis.Core.Services
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event handlers
|
||||
|
||||
private void TargetFrameRateSettingOnSettingChanged(object? sender, EventArgs e)
|
||||
{
|
||||
UpdateTrigger.UpdateFrequency = 1.0 / _targetFrameRateSetting.Value;
|
||||
}
|
||||
|
||||
private void SurfaceOnException(ExceptionEventArgs args)
|
||||
{
|
||||
_logger.Warning("Surface threw e");
|
||||
throw args.Exception;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Events
|
||||
|
||||
public event EventHandler<DeviceEventArgs>? DeviceAdded;
|
||||
public event EventHandler<DeviceEventArgs>? DeviceRemoved;
|
||||
public event EventHandler? LedsChanged;
|
||||
|
||||
private void OnDeviceAdded(DeviceEventArgs e)
|
||||
{
|
||||
DeviceAdded?.Invoke(this, e);
|
||||
}
|
||||
|
||||
protected virtual void OnDeviceRemoved(DeviceEventArgs e)
|
||||
{
|
||||
DeviceRemoved?.Invoke(this, e);
|
||||
}
|
||||
|
||||
protected virtual void OnLedsChanged()
|
||||
{
|
||||
LedsChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@ -245,7 +245,8 @@ namespace Artemis.UI.Shared
|
||||
|
||||
private void PopupOpened()
|
||||
{
|
||||
RecentColorsContainer.ItemsSource = new ObservableCollection<Color>(_colorPickerService.RecentColors);
|
||||
if (_colorPickerService != null)
|
||||
RecentColorsContainer.ItemsSource = new ObservableCollection<Color>(_colorPickerService.RecentColors);
|
||||
}
|
||||
|
||||
private void SelectRecentColor(object sender, MouseButtonEventArgs e)
|
||||
|
||||
@ -26,7 +26,7 @@ namespace Artemis.UI.Shared.Input
|
||||
private DataModelPath? _dataModelPath;
|
||||
private DataModelPropertiesViewModel? _dataModelViewModel;
|
||||
private bool _displaySwitchButton;
|
||||
private Type[] _filterTypes = new Type[0];
|
||||
private Type[] _filterTypes = Array.Empty<Type>();
|
||||
private bool _isDataModelViewModelOpen;
|
||||
private bool _isEnabled = true;
|
||||
private string _placeholder = "Select a property";
|
||||
@ -143,7 +143,8 @@ namespace Artemis.UI.Shared.Input
|
||||
if (value)
|
||||
{
|
||||
UpdateDataModelVisualization();
|
||||
OpenSelectedValue(DataModelViewModel);
|
||||
if (DataModelViewModel != null)
|
||||
OpenSelectedValue(DataModelViewModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -242,7 +243,7 @@ namespace Artemis.UI.Shared.Input
|
||||
|
||||
private void ExecuteSelectPropertyCommand(object? context)
|
||||
{
|
||||
if (!(context is DataModelVisualizationViewModel selected))
|
||||
if (context is not DataModelVisualizationViewModel selected)
|
||||
return;
|
||||
|
||||
ChangeDataModelPath(selected.DataModelPath);
|
||||
|
||||
@ -57,7 +57,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings.Conditio
|
||||
base.OnClose();
|
||||
}
|
||||
|
||||
private void CoreServiceOnFrameRendered(object? sender, FrameRenderedEventArgs e)
|
||||
private void CoreServiceOnFrameRendered(object sender, FrameRenderedEventArgs e)
|
||||
{
|
||||
foreach (DataBindingConditionViewModel<TLayerProperty, TProperty> dataBindingConditionViewModel in Items)
|
||||
dataBindingConditionViewModel.Evaluate();
|
||||
|
||||
@ -64,43 +64,29 @@ namespace Artemis.UI.Screens.Settings.Debug.Tabs
|
||||
|
||||
private void CoreServiceOnFrameRendered(object sender, FrameRenderedEventArgs e)
|
||||
{
|
||||
Execute.PostToUIThread(() =>
|
||||
Execute.OnUIThreadSync(() =>
|
||||
{
|
||||
// TODO: Remove, frames shouldn't even be rendered if this is the case
|
||||
if (e.Texture.Bitmap.Pixels.Length == 0)
|
||||
return;
|
||||
|
||||
SKImageInfo bitmapInfo = e.Texture.Bitmap.Info;
|
||||
RenderHeight = bitmapInfo.Height;
|
||||
RenderWidth = bitmapInfo.Width;
|
||||
|
||||
if (!(CurrentFrame is WriteableBitmap writeableBitmap) ||
|
||||
writeableBitmap.Width != bitmapInfo.Width ||
|
||||
writeableBitmap.Height != bitmapInfo.Height)
|
||||
// ReSharper disable twice CompareOfFloatsByEqualityOperator
|
||||
if (CurrentFrame is not WriteableBitmap writable || writable.Width != bitmapInfo.Width || writable.Height != bitmapInfo.Height)
|
||||
{
|
||||
CurrentFrame = e.Texture.Bitmap.ToWriteableBitmap();
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
|
||||
using SKImage skImage = SKImage.FromPixels(e.Texture.Bitmap.PeekPixels());
|
||||
SKImageInfo info = new(skImage.Width, skImage.Height);
|
||||
writable.Lock();
|
||||
using (SKPixmap pixmap = new(info, writable.BackBuffer, writable.BackBufferStride))
|
||||
{
|
||||
using (SKImage skiaImage = SKImage.FromPixels(e.Texture.Bitmap.PeekPixels()))
|
||||
{
|
||||
SKImageInfo info = new(skiaImage.Width, skiaImage.Height);
|
||||
writeableBitmap.Lock();
|
||||
using (SKPixmap pixmap = new(info, writeableBitmap.BackBuffer, writeableBitmap.BackBufferStride))
|
||||
{
|
||||
skiaImage.ReadPixels(pixmap, 0, 0);
|
||||
}
|
||||
skImage.ReadPixels(pixmap, 0, 0);
|
||||
}
|
||||
|
||||
writeableBitmap.AddDirtyRect(new Int32Rect(0, 0, writeableBitmap.PixelWidth, writeableBitmap.PixelHeight));
|
||||
writeableBitmap.Unlock();
|
||||
}
|
||||
}
|
||||
catch (AccessViolationException)
|
||||
{
|
||||
// oops
|
||||
}
|
||||
writable.AddDirtyRect(new Int32Rect(0, 0, writable.PixelWidth, writable.PixelHeight));
|
||||
writable.Unlock();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -192,7 +192,7 @@ namespace Artemis.UI.Screens.Settings.Device
|
||||
|
||||
#region Event handlers
|
||||
|
||||
private void DeviceOnDeviceUpdated(object? sender, EventArgs e)
|
||||
private void DeviceOnDeviceUpdated(object sender, EventArgs e)
|
||||
{
|
||||
NotifyOfPropertyChange(nameof(CanExportLayout));
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user