using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; using Artemis.Core.DryIoc.Factories; using Artemis.Core.ScriptingProviders; using Artemis.Storage; using DryIoc; using HidSharp; using RGB.NET.Core; using Serilog; using Serilog.Events; using SkiaSharp; namespace Artemis.Core.Services; /// /// Provides Artemis's core update loop /// internal class CoreService : ICoreService { private readonly Stopwatch _frameStopWatch; private readonly ILogger _logger; private readonly PluginSetting _loggingLevel; private readonly IModuleService _moduleService; private readonly IPluginManagementService _pluginManagementService; private readonly IProfileService _profileService; private readonly IRgbService _rgbService; private readonly IScriptingService _scriptingService; private readonly List _updateExceptions = new(); private int _frames; private DateTime _lastExceptionLog; private DateTime _lastFrameRateSample; // ReSharper disable UnusedParameter.Local public CoreService(IContainer container, ILogger logger, StorageMigrationService _1, // injected to ensure migration runs early ISettingsService settingsService, IPluginManagementService pluginManagementService, IRgbService rgbService, IProfileService profileService, IModuleService moduleService, IScriptingService scriptingService, IProcessMonitorService _2) { Constants.CorePlugin.Container = container; _logger = logger; _pluginManagementService = pluginManagementService; _rgbService = rgbService; _profileService = profileService; _moduleService = moduleService; _scriptingService = scriptingService; _loggingLevel = settingsService.GetSetting("Core.LoggingLevel", LogEventLevel.Debug); _frameStopWatch = new Stopwatch(); _rgbService.Surface.Updating += SurfaceOnUpdating; _loggingLevel.SettingChanged += (sender, args) => ApplyLoggingLevel(); } // 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 ApplyLoggingLevel() { string? argument = Constants.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!); LoggerFactory.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); LoggerFactory.LoggingLevelSwitch.MinimumLevel = _loggingLevel.Value; } } else { _logger.Information("Setting logging level to {loggingLevel}", _loggingLevel.Value); LoggerFactory.LoggingLevelSwitch.MinimumLevel = _loggingLevel.Value; } } 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 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 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."); AssemblyInformationalVersionAttribute? versionAttribute = typeof(CoreService).Assembly.GetCustomAttribute(); _logger.Information( "Initializing Artemis Core version {version}, build {buildNumber} branch {branch}.", versionAttribute?.InformationalVersion, Constants.BuildInfo.BuildNumber, Constants.BuildInfo.SourceBranch ); _logger.Information("Startup arguments: {args}", Constants.StartupArguments); _logger.Information("Elevated permissions: {perms}", IsElevated); _logger.Information("Stopwatch high resolution: {perms}", Stopwatch.IsHighResolution); ApplyLoggingLevel(); // 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? FrameRendering; public event EventHandler? FrameRendered; }