From f2b9a9c2b0a3d7af3b56df9ae09db8a3b2d8506c Mon Sep 17 00:00:00 2001 From: emmauss Date: Thu, 6 Feb 2020 11:25:47 +0000 Subject: Render Profiler in GUI (#854) * move profiler output to gui * addressed commits, rebased * removed whitespaces --- ARMeilleure/ARMeilleure.csproj | 9 + Ryujinx.Audio/Ryujinx.Audio.csproj | 4 +- Ryujinx.Common/Ryujinx.Common.csproj | 4 +- Ryujinx.Debugger/Debugger.cs | 32 + Ryujinx.Debugger/Profiler/DumpProfile.cs | 35 + Ryujinx.Debugger/Profiler/InternalProfile.cs | 223 ++++++ Ryujinx.Debugger/Profiler/Profile.cs | 141 ++++ Ryujinx.Debugger/Profiler/ProfileConfig.cs | 254 +++++++ Ryujinx.Debugger/Profiler/ProfileSorters.cs | 32 + Ryujinx.Debugger/Profiler/ProfilerConfiguration.cs | 68 ++ Ryujinx.Debugger/Profiler/Settings.cs | 17 + Ryujinx.Debugger/Profiler/TimingFlag.cs | 17 + Ryujinx.Debugger/Profiler/TimingInfo.cs | 174 +++++ Ryujinx.Debugger/ProfilerConfig.jsonc | 28 + Ryujinx.Debugger/Ryujinx.Debugger.csproj | 42 ++ Ryujinx.Debugger/UI/DebuggerWidget.cs | 42 ++ Ryujinx.Debugger/UI/DebuggerWidget.glade | 44 ++ Ryujinx.Debugger/UI/ProfilerWidget.cs | 801 +++++++++++++++++++++ Ryujinx.Debugger/UI/ProfilerWidget.glade | 232 ++++++ Ryujinx.Debugger/UI/SkRenderer.cs | 23 + Ryujinx.HLE/HOS/Services/IpcService.cs | 2 +- Ryujinx.HLE/PerformanceStatistics.cs | 2 +- Ryujinx.HLE/Ryujinx.HLE.csproj | 6 +- Ryujinx.LLE/Luea.csproj | 4 +- Ryujinx.Profiler/DumpProfile.cs | 35 - Ryujinx.Profiler/InternalProfile.cs | 223 ------ Ryujinx.Profiler/Profile.cs | 144 ---- Ryujinx.Profiler/ProfileConfig.cs | 254 ------- Ryujinx.Profiler/ProfilerConfig.jsonc | 28 - Ryujinx.Profiler/ProfilerConfiguration.cs | 70 -- Ryujinx.Profiler/ProfilerKeyboardHandler.cs | 28 - Ryujinx.Profiler/Ryujinx.Profiler.csproj | 39 - Ryujinx.Profiler/Settings.cs | 20 - Ryujinx.Profiler/TimingFlag.cs | 17 - Ryujinx.Profiler/TimingInfo.cs | 174 ----- Ryujinx.Profiler/UI/ProfileButton.cs | 110 --- Ryujinx.Profiler/UI/ProfileSorters.cs | 32 - Ryujinx.Profiler/UI/ProfileWindow.cs | 773 -------------------- Ryujinx.Profiler/UI/ProfileWindowBars.cs | 85 --- Ryujinx.Profiler/UI/ProfileWindowGraph.cs | 151 ---- Ryujinx.Profiler/UI/ProfileWindowManager.cs | 95 --- .../UI/SharpFontHelpers/FontService.cs | 257 ------- Ryujinx.ShaderTools/Ryujinx.ShaderTools.csproj | 4 +- Ryujinx.Tests.Unicorn/Ryujinx.Tests.Unicorn.csproj | 4 +- Ryujinx.Tests/Ryujinx.Tests.csproj | 4 +- Ryujinx.sln | 22 +- Ryujinx/Program.cs | 2 +- Ryujinx/Ryujinx.csproj | 8 +- Ryujinx/Ui/GLScreen.cs | 20 - Ryujinx/Ui/MainWindow.cs | 60 +- Ryujinx/Ui/MainWindow.glade | 18 +- 51 files changed, 2316 insertions(+), 2597 deletions(-) create mode 100644 Ryujinx.Debugger/Debugger.cs create mode 100644 Ryujinx.Debugger/Profiler/DumpProfile.cs create mode 100644 Ryujinx.Debugger/Profiler/InternalProfile.cs create mode 100644 Ryujinx.Debugger/Profiler/Profile.cs create mode 100644 Ryujinx.Debugger/Profiler/ProfileConfig.cs create mode 100644 Ryujinx.Debugger/Profiler/ProfileSorters.cs create mode 100644 Ryujinx.Debugger/Profiler/ProfilerConfiguration.cs create mode 100644 Ryujinx.Debugger/Profiler/Settings.cs create mode 100644 Ryujinx.Debugger/Profiler/TimingFlag.cs create mode 100644 Ryujinx.Debugger/Profiler/TimingInfo.cs create mode 100644 Ryujinx.Debugger/ProfilerConfig.jsonc create mode 100644 Ryujinx.Debugger/Ryujinx.Debugger.csproj create mode 100644 Ryujinx.Debugger/UI/DebuggerWidget.cs create mode 100644 Ryujinx.Debugger/UI/DebuggerWidget.glade create mode 100644 Ryujinx.Debugger/UI/ProfilerWidget.cs create mode 100644 Ryujinx.Debugger/UI/ProfilerWidget.glade create mode 100644 Ryujinx.Debugger/UI/SkRenderer.cs delete mode 100644 Ryujinx.Profiler/DumpProfile.cs delete mode 100644 Ryujinx.Profiler/InternalProfile.cs delete mode 100644 Ryujinx.Profiler/Profile.cs delete mode 100644 Ryujinx.Profiler/ProfileConfig.cs delete mode 100644 Ryujinx.Profiler/ProfilerConfig.jsonc delete mode 100644 Ryujinx.Profiler/ProfilerConfiguration.cs delete mode 100644 Ryujinx.Profiler/ProfilerKeyboardHandler.cs delete mode 100644 Ryujinx.Profiler/Ryujinx.Profiler.csproj delete mode 100644 Ryujinx.Profiler/Settings.cs delete mode 100644 Ryujinx.Profiler/TimingFlag.cs delete mode 100644 Ryujinx.Profiler/TimingInfo.cs delete mode 100644 Ryujinx.Profiler/UI/ProfileButton.cs delete mode 100644 Ryujinx.Profiler/UI/ProfileSorters.cs delete mode 100644 Ryujinx.Profiler/UI/ProfileWindow.cs delete mode 100644 Ryujinx.Profiler/UI/ProfileWindowBars.cs delete mode 100644 Ryujinx.Profiler/UI/ProfileWindowGraph.cs delete mode 100644 Ryujinx.Profiler/UI/ProfileWindowManager.cs delete mode 100644 Ryujinx.Profiler/UI/SharpFontHelpers/FontService.cs diff --git a/ARMeilleure/ARMeilleure.csproj b/ARMeilleure/ARMeilleure.csproj index 4f55243f..9567838e 100644 --- a/ARMeilleure/ARMeilleure.csproj +++ b/ARMeilleure/ARMeilleure.csproj @@ -13,6 +13,15 @@ true + + true + true + + + + true + + diff --git a/Ryujinx.Audio/Ryujinx.Audio.csproj b/Ryujinx.Audio/Ryujinx.Audio.csproj index 588b6918..b541043c 100644 --- a/Ryujinx.Audio/Ryujinx.Audio.csproj +++ b/Ryujinx.Audio/Ryujinx.Audio.csproj @@ -12,7 +12,7 @@ true - TRACE;USE_PROFILING + TRACE;USE_DEBUGGING false @@ -22,7 +22,7 @@ true - TRACE;USE_PROFILING + TRACE;USE_DEBUGGING true diff --git a/Ryujinx.Common/Ryujinx.Common.csproj b/Ryujinx.Common/Ryujinx.Common.csproj index 7f6fa323..43a853a4 100644 --- a/Ryujinx.Common/Ryujinx.Common.csproj +++ b/Ryujinx.Common/Ryujinx.Common.csproj @@ -12,7 +12,7 @@ true - TRACE;USE_PROFILING + TRACE;USE_DEBUGGING false @@ -22,7 +22,7 @@ true - TRACE;USE_PROFILING + TRACE;USE_DEBUGGING true diff --git a/Ryujinx.Debugger/Debugger.cs b/Ryujinx.Debugger/Debugger.cs new file mode 100644 index 00000000..6dd3354c --- /dev/null +++ b/Ryujinx.Debugger/Debugger.cs @@ -0,0 +1,32 @@ +using System; +using Ryujinx.Debugger.UI; + +namespace Ryujinx.Debugger +{ + public class Debugger : IDisposable + { + public DebuggerWidget Widget { get; set; } + + public Debugger() + { + Widget = new DebuggerWidget(); + } + + public void Enable() + { + Widget.Enable(); + } + + public void Disable() + { + Widget.Disable(); + } + + public void Dispose() + { + Disable(); + + Widget.Dispose(); + } + } +} diff --git a/Ryujinx.Debugger/Profiler/DumpProfile.cs b/Ryujinx.Debugger/Profiler/DumpProfile.cs new file mode 100644 index 00000000..e73314d4 --- /dev/null +++ b/Ryujinx.Debugger/Profiler/DumpProfile.cs @@ -0,0 +1,35 @@ +using Ryujinx.Common; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Ryujinx.Debugger.Profiler +{ + public static class DumpProfile + { + public static void ToFile(string path, InternalProfile profile) + { + String fileData = "Category,Session Group,Session Item,Count,Average(ms),Total(ms)\r\n"; + + foreach (KeyValuePair time in profile.Timers.OrderBy(key => key.Key.Tag)) + { + fileData += $"{time.Key.Category}," + + $"{time.Key.SessionGroup}," + + $"{time.Key.SessionItem}," + + $"{time.Value.Count}," + + $"{time.Value.AverageTime / PerformanceCounter.TicksPerMillisecond}," + + $"{time.Value.TotalTime / PerformanceCounter.TicksPerMillisecond}\r\n"; + } + + // Ensure file directory exists before write + FileInfo fileInfo = new FileInfo(path); + if (fileInfo == null) + throw new Exception("Unknown logging error, probably a bad file path"); + if (fileInfo.Directory != null && !fileInfo.Directory.Exists) + Directory.CreateDirectory(fileInfo.Directory.FullName); + + File.WriteAllText(fileInfo.FullName, fileData); + } + } +} diff --git a/Ryujinx.Debugger/Profiler/InternalProfile.cs b/Ryujinx.Debugger/Profiler/InternalProfile.cs new file mode 100644 index 00000000..0bda9e04 --- /dev/null +++ b/Ryujinx.Debugger/Profiler/InternalProfile.cs @@ -0,0 +1,223 @@ +using Ryujinx.Common; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Ryujinx.Debugger.Profiler +{ + public class InternalProfile + { + private struct TimerQueueValue + { + public ProfileConfig Config; + public long Time; + public bool IsBegin; + } + + internal Dictionary Timers { get; set; } + + private readonly object _timerQueueClearLock = new object(); + private ConcurrentQueue _timerQueue; + + private int _sessionCounter = 0; + + // Cleanup thread + private readonly Thread _cleanupThread; + private bool _cleanupRunning; + private readonly long _history; + private long _preserve; + + // Timing flags + private TimingFlag[] _timingFlags; + private long[] _timingFlagAverages; + private long[] _timingFlagLast; + private long[] _timingFlagLastDelta; + private int _timingFlagCount; + private int _timingFlagIndex; + + private int _maxFlags; + + private Action _timingFlagCallback; + + public InternalProfile(long history, int maxFlags) + { + _maxFlags = maxFlags; + Timers = new Dictionary(); + _timingFlags = new TimingFlag[_maxFlags]; + _timingFlagAverages = new long[(int)TimingFlagType.Count]; + _timingFlagLast = new long[(int)TimingFlagType.Count]; + _timingFlagLastDelta = new long[(int)TimingFlagType.Count]; + _timerQueue = new ConcurrentQueue(); + _history = history; + _cleanupRunning = true; + + // Create cleanup thread. + _cleanupThread = new Thread(CleanupLoop) + { + Name = "Profiler.CleanupThread" + }; + _cleanupThread.Start(); + } + + private void CleanupLoop() + { + bool queueCleared = false; + + while (_cleanupRunning) + { + // Ensure we only ever have 1 instance modifying timers or timerQueue + if (Monitor.TryEnter(_timerQueueClearLock)) + { + queueCleared = ClearTimerQueue(); + + // Calculate before foreach to mitigate redundant calculations + long cleanupBefore = PerformanceCounter.ElapsedTicks - _history; + long preserveStart = _preserve - _history; + + // Each cleanup is self contained so run in parallel for maximum efficiency + Parallel.ForEach(Timers, (t) => t.Value.Cleanup(cleanupBefore, preserveStart, _preserve)); + + Monitor.Exit(_timerQueueClearLock); + } + + // Only sleep if queue was successfully cleared + if (queueCleared) + { + Thread.Sleep(5); + } + } + } + + private bool ClearTimerQueue() + { + int count = 0; + + while (_timerQueue.TryDequeue(out TimerQueueValue item)) + { + if (!Timers.TryGetValue(item.Config, out TimingInfo value)) + { + value = new TimingInfo(); + Timers.Add(item.Config, value); + } + + if (item.IsBegin) + { + value.Begin(item.Time); + } + else + { + value.End(item.Time); + } + + // Don't block for too long as memory disposal is blocked while this function runs + if (count++ > 10000) + { + return false; + } + } + + return true; + } + + public void FlagTime(TimingFlagType flagType) + { + int flagId = (int)flagType; + + _timingFlags[_timingFlagIndex] = new TimingFlag() + { + FlagType = flagType, + Timestamp = PerformanceCounter.ElapsedTicks + }; + + _timingFlagCount = Math.Max(_timingFlagCount + 1, _maxFlags); + + // Work out average + if (_timingFlagLast[flagId] != 0) + { + _timingFlagLastDelta[flagId] = _timingFlags[_timingFlagIndex].Timestamp - _timingFlagLast[flagId]; + _timingFlagAverages[flagId] = (_timingFlagAverages[flagId] == 0) ? _timingFlagLastDelta[flagId] : + (_timingFlagLastDelta[flagId] + _timingFlagAverages[flagId]) >> 1; + } + _timingFlagLast[flagId] = _timingFlags[_timingFlagIndex].Timestamp; + + // Notify subscribers + _timingFlagCallback?.Invoke(_timingFlags[_timingFlagIndex]); + + if (++_timingFlagIndex >= _maxFlags) + { + _timingFlagIndex = 0; + } + } + + public void BeginProfile(ProfileConfig config) + { + _timerQueue.Enqueue(new TimerQueueValue() + { + Config = config, + IsBegin = true, + Time = PerformanceCounter.ElapsedTicks, + }); + } + + public void EndProfile(ProfileConfig config) + { + _timerQueue.Enqueue(new TimerQueueValue() + { + Config = config, + IsBegin = false, + Time = PerformanceCounter.ElapsedTicks, + }); + } + + public string GetSession() + { + // Can be called from multiple threads so we need to ensure no duplicate sessions are generated + return Interlocked.Increment(ref _sessionCounter).ToString(); + } + + public List> GetProfilingData() + { + _preserve = PerformanceCounter.ElapsedTicks; + + lock (_timerQueueClearLock) + { + ClearTimerQueue(); + return Timers.ToList(); + } + } + + public TimingFlag[] GetTimingFlags() + { + int count = Math.Max(_timingFlagCount, _maxFlags); + TimingFlag[] outFlags = new TimingFlag[count]; + + for (int i = 0, sourceIndex = _timingFlagIndex; i < count; i++, sourceIndex++) + { + if (sourceIndex >= _maxFlags) + sourceIndex = 0; + outFlags[i] = _timingFlags[sourceIndex]; + } + + return outFlags; + } + + public (long[], long[]) GetTimingAveragesAndLast() + { + return (_timingFlagAverages, _timingFlagLastDelta); + } + + public void RegisterFlagReceiver(Action receiver) + { + _timingFlagCallback = receiver; + } + + public void Dispose() + { + _cleanupRunning = false; + _cleanupThread.Join(); + } + } +} diff --git a/Ryujinx.Debugger/Profiler/Profile.cs b/Ryujinx.Debugger/Profiler/Profile.cs new file mode 100644 index 00000000..862aa845 --- /dev/null +++ b/Ryujinx.Debugger/Profiler/Profile.cs @@ -0,0 +1,141 @@ +using Ryujinx.Common; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; + +namespace Ryujinx.Debugger.Profiler +{ + public static class Profile + { + public static float UpdateRate => _settings.UpdateRate; + public static long HistoryLength => _settings.History; + + private static InternalProfile _profileInstance; + private static ProfilerSettings _settings; + + [Conditional("USE_DEBUGGING")] + public static void Initialize() + { + var config = ProfilerConfiguration.Load(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ProfilerConfig.jsonc")); + + _settings = new ProfilerSettings() + { + Enabled = config.Enabled, + FileDumpEnabled = config.DumpPath != "", + DumpLocation = config.DumpPath, + UpdateRate = (config.UpdateRate <= 0) ? -1 : 1.0f / config.UpdateRate, + History = (long)(config.History * PerformanceCounter.TicksPerSecond), + MaxLevel = config.MaxLevel, + MaxFlags = config.MaxFlags, + }; + } + + public static bool ProfilingEnabled() + { +#if USE_DEBUGGING + if (!_settings.Enabled) + return false; + + if (_profileInstance == null) + _profileInstance = new InternalProfile(_settings.History, _settings.MaxFlags); + + return true; +#else + return false; +#endif + } + + [Conditional("USE_DEBUGGING")] + public static void FinishProfiling() + { + if (!ProfilingEnabled()) + return; + + if (_settings.FileDumpEnabled) + DumpProfile.ToFile(_settings.DumpLocation, _profileInstance); + + _profileInstance.Dispose(); + } + + [Conditional("USE_DEBUGGING")] + public static void FlagTime(TimingFlagType flagType) + { + if (!ProfilingEnabled()) + return; + _profileInstance.FlagTime(flagType); + } + + [Conditional("USE_DEBUGGING")] + public static void RegisterFlagReceiver(Action receiver) + { + if (!ProfilingEnabled()) + return; + _profileInstance.RegisterFlagReceiver(receiver); + } + + [Conditional("USE_DEBUGGING")] + public static void Begin(ProfileConfig config) + { + if (!ProfilingEnabled()) + return; + if (config.Level > _settings.MaxLevel) + return; + _profileInstance.BeginProfile(config); + } + + [Conditional("USE_DEBUGGING")] + public static void End(ProfileConfig config) + { + if (!ProfilingEnabled()) + return; + if (config.Level > _settings.MaxLevel) + return; + _profileInstance.EndProfile(config); + } + + public static string GetSession() + { +#if USE_DEBUGGING + if (!ProfilingEnabled()) + return null; + return _profileInstance.GetSession(); +#else + return ""; +#endif + } + + public static List> GetProfilingData() + { +#if USE_DEBUGGING + if (!ProfilingEnabled()) + return new List>(); + return _profileInstance.GetProfilingData(); +#else + return new List>(); +#endif + } + + public static TimingFlag[] GetTimingFlags() + { +#if USE_DEBUGGING + if (!ProfilingEnabled()) + return new TimingFlag[0]; + return _profileInstance.GetTimingFlags(); +#else + return new TimingFlag[0]; +#endif + } + + public static (long[], long[]) GetTimingAveragesAndLast() + { +#if USE_DEBUGGING + if (!ProfilingEnabled()) + return (new long[0], new long[0]); + return _profileInstance.GetTimingAveragesAndLast(); +#else + return (new long[0], new long[0]); +#endif + } + } +} diff --git a/Ryujinx.Debugger/Profiler/ProfileConfig.cs b/Ryujinx.Debugger/Profiler/ProfileConfig.cs new file mode 100644 index 00000000..0ec3e26d --- /dev/null +++ b/Ryujinx.Debugger/Profiler/ProfileConfig.cs @@ -0,0 +1,254 @@ +using System; + +namespace Ryujinx.Debugger.Profiler +{ + public struct ProfileConfig : IEquatable + { + public string Category; + public string SessionGroup; + public string SessionItem; + + public int Level; + + // Private cached variables + private string _cachedTag; + private string _cachedSession; + private string _cachedSearch; + + // Public helpers to get config in more user friendly format, + // Cached because they never change and are called often + public string Search + { + get + { + if (_cachedSearch == null) + { + _cachedSearch = $"{Category}.{SessionGroup}.{SessionItem}"; + } + + return _cachedSearch; + } + } + + public string Tag + { + get + { + if (_cachedTag == null) + _cachedTag = $"{Category}{(Session == "" ? "" : $" ({Session})")}"; + return _cachedTag; + } + } + + public string Session + { + get + { + if (_cachedSession == null) + { + if (SessionGroup != null && SessionItem != null) + { + _cachedSession = $"{SessionGroup}: {SessionItem}"; + } + else if (SessionGroup != null) + { + _cachedSession = $"{SessionGroup}"; + } + else if (SessionItem != null) + { + _cachedSession = $"---: {SessionItem}"; + } + else + { + _cachedSession = ""; + } + } + + return _cachedSession; + } + } + + /// + /// The default comparison is far too slow for the number of comparisons needed because it doesn't know what's important to compare + /// + /// Object to compare to + /// + public bool Equals(ProfileConfig cmpObj) + { + // Order here is important. + // Multiple entries with the same item is considerable less likely that multiple items with the same group. + // Likewise for group and category. + return (cmpObj.SessionItem == SessionItem && + cmpObj.SessionGroup == SessionGroup && + cmpObj.Category == Category); + } + } + + /// + /// Predefined configs to make profiling easier, + /// nested so you can reference as Profiles.Category.Group.Item where item and group may be optional + /// + public static class Profiles + { + public static class CPU + { + public static ProfileConfig TranslateTier0 = new ProfileConfig() + { + Category = "CPU", + SessionGroup = "TranslateTier0" + }; + + public static ProfileConfig TranslateTier1 = new ProfileConfig() + { + Category = "CPU", + SessionGroup = "TranslateTier1" + }; + } + + public static class Input + { + public static ProfileConfig ControllerInput = new ProfileConfig + { + Category = "Input", + SessionGroup = "ControllerInput" + }; + + public static ProfileConfig TouchInput = new ProfileConfig + { + Category = "Input", + SessionGroup = "TouchInput" + }; + } + + public static class GPU + { + public static class Engine2d + { + public static ProfileConfig TextureCopy = new ProfileConfig() + { + Category = "GPU.Engine2D", + SessionGroup = "TextureCopy" + }; + } + + public static class Engine3d + { + public static ProfileConfig CallMethod = new ProfileConfig() + { + Category = "GPU.Engine3D", + SessionGroup = "CallMethod", + }; + + public static ProfileConfig VertexEnd = new ProfileConfig() + { + Category = "GPU.Engine3D", + SessionGroup = "VertexEnd" + }; + + public static ProfileConfig ClearBuffers = new ProfileConfig() + { + Category = "GPU.Engine3D", + SessionGroup = "ClearBuffers" + }; + + public static ProfileConfig SetFrameBuffer = new ProfileConfig() + { + Category = "GPU.Engine3D", + SessionGroup = "SetFrameBuffer", + }; + + public static ProfileConfig SetZeta = new ProfileConfig() + { + Category = "GPU.Engine3D", + SessionGroup = "SetZeta" + }; + + public static ProfileConfig UploadShaders = new ProfileConfig() + { + Category = "GPU.Engine3D", + SessionGroup = "UploadShaders" + }; + + public static ProfileConfig UploadTextures = new ProfileConfig() + { + Category = "GPU.Engine3D", + SessionGroup = "UploadTextures" + }; + + public static ProfileConfig UploadTexture = new ProfileConfig() + { + Category = "GPU.Engine3D", + SessionGroup = "UploadTexture" + }; + + public static ProfileConfig UploadConstBuffers = new ProfileConfig() + { + Category = "GPU.Engine3D", + SessionGroup = "UploadConstBuffers" + }; + + public static ProfileConfig UploadVertexArrays = new ProfileConfig() + { + Category = "GPU.Engine3D", + SessionGroup = "UploadVertexArrays" + }; + + public static ProfileConfig ConfigureState = new ProfileConfig() + { + Category = "GPU.Engine3D", + SessionGroup = "ConfigureState" + }; + } + + public static class EngineM2mf + { + public static ProfileConfig CallMethod = new ProfileConfig() + { + Category = "GPU.EngineM2mf", + SessionGroup = "CallMethod", + }; + + public static ProfileConfig Execute = new ProfileConfig() + { + Category = "GPU.EngineM2mf", + SessionGroup = "Execute", + }; + } + + public static class EngineP2mf + { + public static ProfileConfig CallMethod = new ProfileConfig() + { + Category = "GPU.EngineP2mf", + SessionGroup = "CallMethod", + }; + + public static ProfileConfig Execute = new ProfileConfig() + { + Category = "GPU.EngineP2mf", + SessionGroup = "Execute", + }; + + public static ProfileConfig PushData = new ProfileConfig() + { + Category = "GPU.EngineP2mf", + SessionGroup = "PushData", + }; + } + + public static class Shader + { + public static ProfileConfig Decompile = new ProfileConfig() + { + Category = "GPU.Shader", + SessionGroup = "Decompile", + }; + } + } + + public static ProfileConfig ServiceCall = new ProfileConfig() + { + Category = "ServiceCall", + }; + } +} diff --git a/Ryujinx.Debugger/Profiler/ProfileSorters.cs b/Ryujinx.Debugger/Profiler/ProfileSorters.cs new file mode 100644 index 00000000..2b730af5 --- /dev/null +++ b/Ryujinx.Debugger/Profiler/ProfileSorters.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; + +namespace Ryujinx.Debugger.Profiler +{ + public static class ProfileSorters + { + public class InstantAscending : IComparer> + { + public int Compare(KeyValuePair pair1, KeyValuePair pair2) + => pair2.Value.Instant.CompareTo(pair1.Value.Instant); + } + + public class AverageAscending : IComparer> + { + public int Compare(KeyValuePair pair1, KeyValuePair pair2) + => pair2.Value.AverageTime.CompareTo(pair1.Value.AverageTime); + } + + public class TotalAscending : IComparer> + { + public int Compare(KeyValuePair pair1, KeyValuePair pair2) + => pair2.Value.TotalTime.CompareTo(pair1.Value.TotalTime); + } + + public class TagAscending : IComparer> + { + public int Compare(KeyValuePair pair1, KeyValuePair pair2) + => StringComparer.CurrentCulture.Compare(pair1.Key.Search, pair2.Key.Search); + } + } +} diff --git a/Ryujinx.Debugger/Profiler/ProfilerConfiguration.cs b/Ryujinx.Debugger/Profiler/ProfilerConfiguration.cs new file mode 100644 index 00000000..e0842f2e --- /dev/null +++ b/Ryujinx.Debugger/Profiler/ProfilerConfiguration.cs @@ -0,0 +1,68 @@ +using Gdk; +using System; +using System.IO; +using Utf8Json; +using Utf8Json.Resolvers; + +namespace Ryujinx.Debugger.Profiler +{ + public class ProfilerConfiguration + { + public bool Enabled { get; private set; } + public string DumpPath { get; private set; } + public float UpdateRate { get; private set; } + public int MaxLevel { get; private set; } + public int MaxFlags { get; private set; } + public float History { get; private set; } + + /// + /// Loads a configuration file from disk + /// + /// The path to the JSON configuration file + public static ProfilerConfiguration Load(string path) + { + var resolver = CompositeResolver.Create( + new[] { new ConfigurationEnumFormatter() }, + new[] { StandardResolver.AllowPrivateSnakeCase } + ); + + if (!File.Exists(path)) + { + throw new FileNotFoundException($"Profiler configuration file {path} not found"); + } + + using (Stream stream = File.OpenRead(path)) + { + return JsonSerializer.Deserialize(stream, resolver); + } + } + + private class ConfigurationEnumFormatter : IJsonFormatter + where T : struct + { + public void Serialize(ref JsonWriter writer, T value, IJsonFormatterResolver formatterResolver) + { + formatterResolver.GetFormatterWithVerify() + .Serialize(ref writer, value.ToString(), formatterResolver); + } + + public T Deserialize(ref JsonReader reader, IJsonFormatterResolver formatterResolver) + { + if (reader.ReadIsNull()) + { + return default(T); + } + + string enumName = formatterResolver.GetFormatterWithVerify() + .Deserialize(ref reader, formatterResolver); + + if (Enum.TryParse(enumName, out T result)) + { + return result; + } + + return default(T); + } + } + } +} diff --git a/Ryujinx.Debugger/Profiler/Settings.cs b/Ryujinx.Debugger/Profiler/Settings.cs new file mode 100644 index 00000000..52aa0d84 --- /dev/null +++ b/Ryujinx.Debugger/Profiler/Settings.cs @@ -0,0 +1,17 @@ +namespace Ryujinx.Debugger.Profiler +{ + public class ProfilerSettings + { + // Default settings for profiler + public bool Enabled { get; set; } = false; + public bool FileDumpEnabled { get; set; } = false; + public string DumpLocation { get; set; } = ""; + public float UpdateRate { get; set; } = 0.1f; + public int MaxLevel { get; set; } = 0; + public int MaxFlags { get; set; } = 1000; + + // 19531225 = 5 seconds in ticks on most pc's. + // It should get set on boot to the time specified in config + public long History { get; set; } = 19531225; + } +} diff --git a/Ryujinx.Debugger/Profiler/TimingFlag.cs b/Ryujinx.Debugger/Profiler/TimingFlag.cs new file mode 100644 index 00000000..8a34ac99 --- /dev/null +++ b/Ryujinx.Debugger/Profiler/TimingFlag.cs @@ -0,0 +1,17 @@ +namespace Ryujinx.Debugger.Profiler +{ + public enum TimingFlagType + { + FrameSwap = 0, + SystemFrame = 1, + + // Update this for new flags + Count = 2, + } + + public struct TimingFlag + { + public TimingFlagType FlagType; + public long Timestamp; + } +} diff --git a/Ryujinx.Debugger/Profiler/TimingInfo.cs b/Ryujinx.Debugger/Profiler/TimingInfo.cs new file mode 100644 index 00000000..90bd63d2 --- /dev/null +++ b/Ryujinx.Debugger/Profiler/TimingInfo.cs @@ -0,0 +1,174 @@ +using System; +using System.Collections.Generic; + +namespace Ryujinx.Debugger.Profiler +{ + public struct Timestamp + { + public long BeginTime; + public long EndTime; + } + + public class TimingInfo + { + // Timestamps + public long TotalTime { get; set; } + public long Instant { get; set; } + + // Measurement counts + public int Count { get; set; } + public int InstantCount { get; set; } + + // Work out average + public long AverageTime => (Count == 0) ? -1 : TotalTime / Count; + + // Intentionally not locked as it's only a get count + public bool IsActive => _timestamps.Count > 0; + + public long BeginTime + { + get + { + lock (_timestampLock) + { + if (_depth > 0) + { + return _currentTimestamp.BeginTime; + } + + return -1; + } + } + } + + // Timestamp collection + private List _timestamps; + private readonly object _timestampLock = new object(); + private readonly object _timestampListLock = new object(); + private Timestamp _currentTimestamp; + + // Depth of current timer, + // each begin call increments and each end call decrements + private int _depth; + + public TimingInfo() + { + _timestamps = new List(); + _depth = 0; + } + + public void Begin(long beginTime) + { + lock (_timestampLock) + { + // Finish current timestamp if already running + if (_depth > 0) + { + EndUnsafe(beginTime); + } + + BeginUnsafe(beginTime); + _depth++; + } + } + + private void BeginUnsafe(long beginTime) + { + _currentTimestamp.BeginTime = beginTime; + _currentTimestamp.EndTime = -1; + } + + public void End(long endTime) + { + lock (_timestampLock) + { + _depth--; + + if (_depth < 0) + { + throw new Exception("Timing info end called without corresponding begin"); + } + + EndUnsafe(endTime); + + // Still have others using this timing info so recreate start for them + if (_depth > 0) + { + BeginUnsafe(endTime); + } + } + } + + private void EndUnsafe(long endTime) + { + _currentTimestamp.EndTime = endTime; + lock (_timestampListLock) + { + _timestamps.Add(_currentTimestamp); + } + + long delta = _currentTimestamp.EndTime - _currentTimestamp.BeginTime; + TotalTime += delta; + Instant += delta; + + Count++; + InstantCount++; + } + + // Remove any timestamps before given timestamp to free memory + public void Cleanup(long before, long preserveStart, long preserveEnd) + { + lock (_timestampListLock) + { + int toRemove = 0; + int toPreserveStart = 0; + int toPreserveLen = 0; + + for (int i = 0; i < _timestamps.Count; i++) + { + if (_timestamps[i].EndTime < preserveStart) + { + toPreserveStart++; + InstantCount--; + Instant -= _timestamps[i].EndTime - _timestamps[i].BeginTime; + } + else if (_timestamps[i].EndTime < preserveEnd) + { + toPreserveLen++; + } + else if (_timestamps[i].EndTime < before) + { + toRemove++; + InstantCount--; + Instant -= _timestamps[i].EndTime - _timestamps[i].BeginTime; + } + else + { + // Assume timestamps are in chronological order so no more need to be removed + break; + } + } + + if (toPreserveStart > 0) + { + _timestamps.RemoveRange(0, toPreserveStart); + } + + if (toRemove > 0) + { + _timestamps.RemoveRange(toPreserveLen, toRemove); + } + } + } + + public Timestamp[] GetAllTimestamps() + { + lock (_timestampListLock) + { + Timestamp[] returnTimestamps = new Timestamp[_timestamps.Count]; + _timestamps.CopyTo(returnTimestamps); + return returnTimestamps; + } + } + } +} diff --git a/Ryujinx.Debugger/ProfilerConfig.jsonc b/Ryujinx.Debugger/ProfilerConfig.jsonc new file mode 100644 index 00000000..e6714386 --- /dev/null +++ b/Ryujinx.Debugger/ProfilerConfig.jsonc @@ -0,0 +1,28 @@ +{ + // Enable profiling (Only available on a profiling enabled builds) + "enabled": true, + + // Set profile file dump location, if blank file dumping disabled. (e.g. `ProfileDump.csv`) + "dump_path": "", + + // Update rate for profiler UI, in hertz. -1 updates every time a frame is issued + "update_rate": 4.0, + + // Set how long to keep profiling data in seconds, reduce if profiling is taking too much RAM + "history": 5.0, + + // Set the maximum profiling level. Higher values may cause a heavy load on your system but will allow you to profile in more detail + "max_level": 0, + + // Sets the maximum number of flags to keep + "max_flags": 1000, + + // Keyboard Controls + // https://github.com/opentk/opentk/blob/master/src/OpenTK/Input/Key.cs + "controls": { + "buttons": { + // Show/Hide the profiler + "toggle_profiler": "F2" + } + } +} \ No newline at end of file diff --git a/Ryujinx.Debugger/Ryujinx.Debugger.csproj b/Ryujinx.Debugger/Ryujinx.Debugger.csproj new file mode 100644 index 00000000..a67662cc --- /dev/null +++ b/Ryujinx.Debugger/Ryujinx.Debugger.csproj @@ -0,0 +1,42 @@ + + + + netcoreapp3.0 + Debug;Release;Profile Release;Profile Debug + + + + TRACE;USE_DEBUGGING + + + + TRACE;USE_DEBUGGING + + + + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/Ryujinx.Debugger/UI/DebuggerWidget.cs b/Ryujinx.Debugger/UI/DebuggerWidget.cs new file mode 100644 index 00000000..b2d8458d --- /dev/null +++ b/Ryujinx.Debugger/UI/DebuggerWidget.cs @@ -0,0 +1,42 @@ +using Gtk; +using System; +using GUI = Gtk.Builder.ObjectAttribute; + +namespace Ryujinx.Debugger.UI +{ + public class DebuggerWidget : Box + { + public event EventHandler DebuggerEnabled; + public event EventHandler DebuggerDisabled; + + [GUI] Notebook _widgetNotebook; + + public DebuggerWidget() : this(new Builder("Ryujinx.Debugger.UI.DebuggerWidget.glade")) { } + + public DebuggerWidget(Builder builder) : base(builder.GetObject("_debuggerBox").Handle) + { + builder.Autoconnect(this); + + LoadProfiler(); + } + + public void LoadProfiler() + { + ProfilerWidget widget = new ProfilerWidget(); + + widget.RegisterParentDebugger(this); + + _widgetNotebook.AppendPage(widget, new Label("Profiler")); + } + + public void Enable() + { + DebuggerEnabled.Invoke(this, null); + } + + public void Disable() + { + DebuggerDisabled.Invoke(this, null); + } + } +} diff --git a/Ryujinx.Debugger/UI/DebuggerWidget.glade b/Ryujinx.Debugger/UI/DebuggerWidget.glade new file mode 100644 index 00000000..7e6e691d --- /dev/null +++ b/Ryujinx.Debugger/UI/DebuggerWidget.glade @@ -0,0 +1,44 @@ + + + + + + DebuggerBox + 1024 + 720 + True + False + vertical + + + True + True + True + True + + + + + + + + + + + + + + + + + + + + + True + True + 0 + + + + diff --git a/Ryujinx.Debugger/UI/ProfilerWidget.cs b/Ryujinx.Debugger/UI/ProfilerWidget.cs new file mode 100644 index 00000000..0dc4b84f --- /dev/null +++ b/Ryujinx.Debugger/UI/ProfilerWidget.cs @@ -0,0 +1,801 @@ +using Gtk; +using Ryujinx.Common; +using Ryujinx.Debugger.Profiler; +using SkiaSharp; +using SkiaSharp.Views.Desktop; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading; + +using GUI = Gtk.Builder.ObjectAttribute; + +namespace Ryujinx.Debugger.UI +{ + public class ProfilerWidget : Box + { + private Thread _profilerThread; + private double _prevTime; + private bool _profilerRunning; + + private TimingFlag[] _timingFlags; + + private bool _initComplete = false; + private bool _redrawPending = true; + private bool _doStep = false; + + // Layout + private const int LineHeight = 16; + private const int MinimumColumnWidth = 200; + private const int TitleHeight = 24; + private const int TitleFontHeight = 16; + private const int LinePadding = 2; + private const int ColumnSpacing = 15; + private const int FilterHeight = 24; + private const int BottomBarHeight = FilterHeight + LineHeight; + + // Sorting + private List> _unsortedProfileData; + private IComparer> _sortAction = new ProfileSorters.TagAscending(); + + // Flag data + private long[] _timingFlagsAverages; + private long[] _timingFlagsLast; + + // Filtering + private string _filterText = ""; + private bool _regexEnabled = false; + + // Scrolling + private float _scrollPos = 0; + + // Profile data storage + private List> _sortedProfileData; + private long _captureTime; + + // Graph + private SKColor[] _timingFlagColors = new[] + { + new SKColor(150, 25, 25, 50), // FrameSwap = 0 + new SKColor(25, 25, 150, 50), // SystemFrame = 1 + }; + + private const float GraphMoveSpeed = 40000; + private const float GraphZoomSpeed = 50; + + private float _graphZoom = 1; + private float _graphPosition = 0; + private int _rendererHeight => _renderer.AllocatedHeight; + private int _rendererWidth => _renderer.AllocatedWidth; + + // Event management + private long _lastOutputUpdate; + private long _lastOutputDraw; + private long _lastOutputUpdateDuration; + private long _lastOutputDrawDuration; + private double _lastFrameTimeMs; + private double _updateTimer; + private bool _profileUpdated = false; + private readonly object _profileDataLock = new object(); + + private SkRenderer _renderer; + + [GUI] ScrolledWindow _scrollview; + [GUI] CheckButton _enableCheckbutton; + [GUI] Scrollbar _outputScrollbar; + [GUI] Entry _filterBox; + [GUI] ComboBox _modeBox; + [GUI] CheckButton _showFlags; + [GUI] CheckButton _showInactive; + [GUI] Button _stepButton; + [GUI] CheckButton _pauseCheckbutton; + + public ProfilerWidget() : this(new Builder("Ryujinx.Debugger.UI.ProfilerWidget.glade")) { } + + public ProfilerWidget(Builder builder) : base(builder.GetObject("_profilerBox").Handle) + { + builder.Autoconnect(this); + + this.KeyPressEvent += ProfilerWidget_KeyPressEvent; + + this.Expand = true; + + _renderer = new SkRenderer(); + _renderer.Expand = true; + + _outputScrollbar.ValueChanged += _outputScrollbar_ValueChanged; + + _renderer.DrawGraphs += _renderer_DrawGraphs; + + _filterBox.Changed += _filterBox_Changed; + + _stepButton.Clicked += _stepButton_Clicked; + + _scrollview.Add(_renderer); + + if (Profile.UpdateRate <= 0) + { + // Perform step regardless of flag type + Profile.RegisterFlagReceiver((t) => + { + if (_pauseCheckbutton.Active) + { + _doStep = true; + } + }); + } + } + + private void _stepButton_Clicked(object sender, EventArgs e) + { + if (_pauseCheckbutton.Active) + { + _doStep = true; + } + + _profileUpdated = true; + } + + private void _filterBox_Changed(object sender, EventArgs e) + { + _filterText = _filterBox.Text; + _profileUpdated = true; + } + + private void _outputScrollbar_ValueChanged(object sender, EventArgs e) + { + _scrollPos = -(float)Math.Max(0, _outputScrollbar.Value); + _profileUpdated = true; + } + + private void _renderer_DrawGraphs(object sender, EventArgs e) + { + if (e is SKPaintSurfaceEventArgs se) + { + Draw(se.Surface.Canvas); + } + } + + public void RegisterParentDebugger(DebuggerWidget debugger) + { + debugger.DebuggerEnabled += Debugger_DebuggerAttached; + debugger.DebuggerDisabled += Debugger_DebuggerDettached; + } + + private void Debugger_DebuggerDettached(object sender, EventArgs e) + { + _profilerRunning = false; + + if (_profilerThread != null) + { + _profilerThread.Join(); + } + } + + private void Debugger_DebuggerAttached(object sender, EventArgs e) + { + _profilerRunning = false; + + if (_profilerThread != null) + { + _profilerThread.Join(); + } + + _profilerRunning = true; + + _profilerThread = new Thread(UpdateLoop) + { + Name = "Profiler.UpdateThread" + }; + _profilerThread.Start(); + } + + private void ProfilerWidget_KeyPressEvent(object o, Gtk.KeyPressEventArgs args) + { + switch (args.Event.Key) + { + case Gdk.Key.Left: + _graphPosition += (long)(GraphMoveSpeed * _lastFrameTimeMs); + break; + + case Gdk.Key.Right: + _graphPosition = Math.Max(_graphPosition - (long)(GraphMoveSpeed * _lastFrameTimeMs), 0); + break; + + case Gdk.Key.Up: + _graphZoom = MathF.Min(_graphZoom + (float)(GraphZoomSpeed * _lastFrameTimeMs), 100.0f); + break; + + case Gdk.Key.Down: + _graphZoom = MathF.Max(_graphZoom - (float)(GraphZoomSpeed * _lastFrameTimeMs), 1f); + break; + } + _profileUpdated = true; + } + + public void UpdateLoop() + { + _lastOutputUpdate = PerformanceCounter.ElapsedTicks; + _lastOutputDraw = PerformanceCounter.ElapsedTicks; + + while (_profilerRunning) + { + _lastOutputUpdate = PerformanceCounter.ElapsedTicks; + int timeToSleepMs = (_pauseCheckbutton.Active || !_enableCheckbutton.Active) ? 33 : 1; + + if (Profile.ProfilingEnabled() && _enableCheckbutton.Active) + { + double time = (double)PerformanceCounter.ElapsedTicks / PerformanceCounter.TicksPerSecond; + + Update(time - _prevTime); + + _lastOutputUpdateDuration = PerformanceCounter.ElapsedTicks - _lastOutputUpdate; + _prevTime = time; + + Gdk.Threads.AddIdle(1000, ()=> + { + _renderer.QueueDraw(); + + return true; + }); + } + + Thread.Sleep(timeToSleepMs); + } + } + + public void Update(double frameTime) + { + _lastFrameTimeMs = frameTime; + + // Get timing data if enough time has passed + _updateTimer += frameTime; + + if (_doStep || ((Profile.UpdateRate > 0) && (!_pauseCheckbutton.Active && (_updateTimer > Profile.UpdateRate)))) + { + _updateTimer = 0; + _captureTime = PerformanceCounter.ElapsedTicks; + _timingFlags = Profile.GetTimingFlags(); + _doStep = false; + _profileUpdated = true; + + _unsortedProfileData = Profile.GetProfilingData(); + + (_timingFlagsAverages, _timingFlagsLast) = Profile.GetTimingAveragesAndLast(); + } + + // Filtering + if (_profileUpdated) + { + lock (_profileDataLock) + { + _sortedProfileData = _showInactive.Active ? _unsortedProfileData : _unsortedProfileData.FindAll(kvp => kvp.Value.IsActive); + + if (_sortAction != null) + { + _sortedProfileData.Sort(_sortAction); + } + + if (_regexEnabled) + { + try + { + Regex filterRegex = new Regex(_filterText, RegexOptions.IgnoreCase); + if (_filterText != "") + { + _sortedProfileData = _sortedProfileData.Where((pair => filterRegex.IsMatch(pair.Key.Search))).ToList(); + } + } + catch (ArgumentException argException) + { + // Skip filtering for invalid regex + } + } + else + { + // Regular filtering + _sortedProfileData = _sortedProfileData.Where((pair => pair.Key.Search.ToLower().Contains(_filterText.ToLower()))).ToList(); + } + } + + _profileUpdated = false; + _redrawPending = true; + _initComplete = true; + } + } + + private string GetTimeString(long timestamp) + { + float time = (float)timestamp / PerformanceCounter.TicksPerMillisecond; + + return (time < 1) ? $"{time * 1000:F3}us" : $"{time:F3}ms"; + } + + private void FilterBackspace() + { + if (_filterText.Length <= 1) + { + _filterText = ""; + } + else + { + _filterText = _filterText.Remove(_filterText.Length - 1, 1); + } + } + + private float GetLineY(float offset, float lineHeight, float padding, bool centre, int line) + { + return offset + lineHeight + padding + ((lineHeight + padding) * line) - ((centre) ? padding : 0); + } + + public void Draw(SKCanvas canvas) + { + _lastOutputDraw = PerformanceCounter.ElapsedTicks; + if (!Visible || + !_initComplete || + !_enableCheckbutton.Active || + !_redrawPending) + { + return; + } + + float viewTop = TitleHeight + 5; + float viewBottom = _rendererHeight - FilterHeight - LineHeight; + + float columnWidth; + float maxColumnWidth = MinimumColumnWidth; + float yOffset = _scrollPos + viewTop; + float xOffset = 10; + float timingWidth; + + float contentHeight = GetLineY(0, LineHeight, LinePadding, false, _sortedProfileData.Count - 1); + + _outputScrollbar.Adjustment.Upper = contentHeight; + _outputScrollbar.Adjustment.Lower = 0; + _outputScrollbar.Adjustment.PageSize = viewBottom - viewTop; + + + SKPaint textFont = new SKPaint() + { + Color = SKColors.White, + TextSize = LineHeight + }; + + SKPaint titleFont = new SKPaint() + { + Color = SKColors.White, + TextSize = TitleFontHeight + }; + + SKPaint evenItemBackground = new SKPaint() + { + Color = SKColors.Gray + }; + + canvas.Save(); + canvas.ClipRect(new SKRect(0, viewTop, _rendererWidth, viewBottom), SKClipOperation.Intersect); + + for (int i = 1; i < _sortedProfileData.Count; i += 2) + { + float top = GetLineY(yOffset, LineHeight, LinePadding, false, i - 1); + float bottom = GetLineY(yOffset, LineHeight, LinePadding, false, i); + + canvas.DrawRect(new SKRect(0, top, _rendererWidth, bottom), evenItemBackground); + } + + lock (_profileDataLock) + { + // Display category + + for (int verticalIndex = 0; verticalIndex < _sortedProfileData.Count; verticalIndex++) + { + KeyValuePair entry = _sortedProfileData[verticalIndex]; + + if (entry.Key.Category == null) + { + continue; + } + + float y = GetLineY(yOffset, LineHeight, LinePadding, true, verticalIndex); + + canvas.DrawText(entry.Key.Category, new SKPoint(xOffset, y), textFont); + + columnWidth = textFont.MeasureText(entry.Key.Category); + + if (columnWidth > maxColumnWidth) + { + maxColumnWidth = columnWidth; + } + } + + canvas.Restore(); + canvas.DrawText("Category", new SKPoint(xOffset, TitleFontHeight + 2), titleFont); + + columnWidth = titleFont.MeasureText("Category"); + + if (columnWidth > maxColumnWidth) + { + maxColumnWidth = columnWidth; + } + + xOffset += maxColumnWidth + ColumnSpacing; + + canvas.DrawLine(new SKPoint(xOffset - ColumnSpacing / 2, 0), new SKPoint(xOffset - ColumnSpacing / 2, viewBottom), textFont); + + // Display session group + maxColumnWidth = MinimumColumnWidth; + + canvas.Save(); + canvas.ClipRect(new SKRect(0, viewTop, _rendererWidth, viewBottom), SKClipOperation.Intersect); + + for (int verticalIndex = 0; verticalIndex < _sortedProfileData.Count; verticalIndex++) + { + KeyValuePair entry = _sortedProfileData[verticalIndex]; + + if (entry.Key.SessionGroup == null) + { + continue; + } + + float y = GetLineY(yOffset, LineHeight, LinePadding, true, verticalIndex); + + canvas.DrawText(entry.Key.SessionGroup, new SKPoint(xOffset, y), textFont); + + columnWidth = textFont.MeasureText(entry.Key.SessionGroup); + + if (columnWidth > maxColumnWidth) + { + maxColumnWidth = columnWidth; + } + } + + canvas.Restore(); + canvas.DrawText("Group", new SKPoint(xOffset, TitleFontHeight + 2), titleFont); + + columnWidth = titleFont.MeasureText("Group"); + + if (columnWidth > maxColumnWidth) + { + maxColumnWidth = columnWidth; + } + + xOffset += maxColumnWidth + ColumnSpacing; + + canvas.DrawLine(new SKPoint(xOffset - ColumnSpacing / 2, 0), new SKPoint(xOffset - ColumnSpacing / 2, viewBottom), textFont); + + // Display session item + maxColumnWidth = MinimumColumnWidth; + + canvas.Save(); + canvas.ClipRect(new SKRect(0, viewTop, _rendererWidth, viewBottom), SKClipOperation.Intersect); + + for (int verticalIndex = 0; verticalIndex < _sortedProfileData.Count; verticalIndex++) + { + KeyValuePair entry = _sortedProfileData[verticalIndex]; + + if (entry.Key.SessionItem == null) + { + continue; + } + + float y = GetLineY(yOffset, LineHeight, LinePadding, true, verticalIndex); + + canvas.DrawText(entry.Key.SessionGroup, new SKPoint(xOffset, y), textFont); + + columnWidth = textFont.MeasureText(entry.Key.SessionItem); + + if (columnWidth > maxColumnWidth) + { + maxColumnWidth = columnWidth; + } + } + + canvas.Restore(); + canvas.DrawText("Item", new SKPoint(xOffset, TitleFontHeight + 2), titleFont); + + columnWidth = titleFont.MeasureText("Item"); + + if (columnWidth > maxColumnWidth) + { + maxColumnWidth = columnWidth; + } + + xOffset += maxColumnWidth + ColumnSpacing; + + timingWidth = _rendererWidth - xOffset - 370; + + canvas.Save(); + canvas.ClipRect(new SKRect(0, viewTop, _rendererWidth, viewBottom), SKClipOperation.Intersect); + canvas.DrawLine(new SKPoint(xOffset, 0), new SKPoint(xOffset, _rendererHeight), textFont); + + int mode = _modeBox.Active; + + canvas.Save(); + canvas.ClipRect(new SKRect(xOffset, yOffset,xOffset + timingWidth,yOffset + contentHeight), + SKClipOperation.Intersect); + + switch (mode) + { + case 0: + DrawGraph(xOffset, yOffset, timingWidth, canvas); + break; + case 1: + DrawBars(xOffset, yOffset, timingWidth, canvas); + + canvas.DrawText("Blue: Instant, Green: Avg, Red: Total", + new SKPoint(xOffset, _rendererHeight - TitleFontHeight), titleFont); + break; + } + + canvas.Restore(); + canvas.DrawLine(new SKPoint(xOffset + timingWidth, 0), new SKPoint(xOffset + timingWidth, _rendererHeight), textFont); + + xOffset = _rendererWidth - 360; + + // Display timestamps + long totalInstant = 0; + long totalAverage = 0; + long totalTime = 0; + long totalCount = 0; + + for (int verticalIndex = 0; verticalIndex < _sortedProfileData.Count; verticalIndex++) + { + KeyValuePair entry = _sortedProfileData[verticalIndex]; + + float y = GetLineY(yOffset, LineHeight, LinePadding, true, verticalIndex); + + canvas.DrawText($"{GetTimeString(entry.Value.Instant)} ({entry.Value.InstantCount})", new SKPoint(xOffset, y), textFont); + canvas.DrawText(GetTimeString(entry.Value.AverageTime), new SKPoint(150 + xOffset, y), textFont); + canvas.DrawText(GetTimeString(entry.Value.TotalTime), new SKPoint(260 + xOffset, y), textFont); + + totalInstant += entry.Value.Instant; + totalAverage += entry.Value.AverageTime; + totalTime += entry.Value.TotalTime; + totalCount += entry.Value.InstantCount; + } + + canvas.Restore(); + canvas.DrawLine(new SKPoint(0, viewTop), new SKPoint(_rendererWidth, viewTop), titleFont); + + float yHeight = 0 + TitleFontHeight; + + canvas.DrawText("Instant (Count)", new SKPoint(xOffset, yHeight), titleFont); + canvas.DrawText("Average", new SKPoint(150 + xOffset, yHeight), titleFont); + canvas.DrawText("Total (ms)", new SKPoint(260 + xOffset, yHeight), titleFont); + + // Totals + yHeight = _rendererHeight - FilterHeight + 3; + + int textHeight = LineHeight - 2; + + SKPaint detailFont = new SKPaint() + { + Color = new SKColor(100, 100, 255, 255), + TextSize = textHeight + }; + + canvas.DrawLine(new SkiaSharp.SKPoint(0, viewBottom), new SkiaSharp.SKPoint(_rendererWidth,viewBottom), textFont); + + string hostTimeString = $"Host {GetTimeString(_timingFlagsLast[(int)TimingFlagType.SystemFrame])} " + + $"({GetTimeString(_timingFlagsAverages[(int)TimingFlagType.SystemFrame])})"; + + canvas.DrawText(hostTimeString, new SKPoint(5, yHeight), detailFont); + + float tempWidth = detailFont.MeasureText(hostTimeString); + + detailFont.Color = SKColors.Red; + + string gameTimeString = $"Game {GetTimeString(_timingFlagsLast[(int)TimingFlagType.FrameSwap])} " + + $"({GetTimeString(_timingFlagsAverages[(int)TimingFlagType.FrameSwap])})"; + + canvas.DrawText(gameTimeString, new SKPoint(15 + tempWidth, yHeight), detailFont); + + tempWidth += detailFont.MeasureText(gameTimeString); + + detailFont.Color = SKColors.White; + + canvas.DrawText($"Profiler: Update {GetTimeString(_lastOutputUpdateDuration)} Draw {GetTimeString(_lastOutputDrawDuration)}", + new SKPoint(20 + tempWidth, yHeight), detailFont); + + detailFont.Color = SKColors.White; + + canvas.DrawText($"{GetTimeString(totalInstant)} ({totalCount})", new SKPoint(xOffset, yHeight), detailFont); + canvas.DrawText(GetTimeString(totalAverage), new SKPoint(150 + xOffset, yHeight), detailFont); + canvas.DrawText(GetTimeString(totalTime), new SKPoint(260 + xOffset, yHeight), detailFont); + + _lastOutputDrawDuration = PerformanceCounter.ElapsedTicks - _lastOutputDraw; + } + } + + private void DrawGraph(float xOffset, float yOffset, float width, SKCanvas canvas) + { + if (_sortedProfileData.Count != 0) + { + int left, right; + float top, bottom; + + float graphRight = xOffset + width; + float barHeight = (LineHeight - LinePadding); + long history = Profile.HistoryLength; + double timeWidthTicks = history / (double)_graphZoom; + long graphPositionTicks = (long)(_graphPosition * PerformanceCounter.TicksPerMillisecond); + long ticksPerPixel = (long)(timeWidthTicks / width); + + // Reset start point if out of bounds + if (timeWidthTicks + graphPositionTicks > history) + { + graphPositionTicks = history - (long)timeWidthTicks; + _graphPosition = (float)graphPositionTicks / PerformanceCounter.TicksPerMillisecond; + } + + graphPositionTicks = _captureTime - graphPositionTicks; + + // Draw timing flags + if (_showFlags.Active) + { + TimingFlagType prevType = TimingFlagType.Count; + + SKPaint timingPaint = new SKPaint + { + Color = _timingFlagColors.First() + }; + + foreach (TimingFlag timingFlag in _timingFlags) + { + if (prevType != timingFlag.FlagType) + { + prevType = timingFlag.FlagType; + timingPaint.Color = _timingFlagColors[(int)prevType]; + } + + int x = (int)(graphRight - ((graphPositionTicks - timingFlag.Timestamp) / timeWidthTicks) * width); + + if (x > xOffset) + { + canvas.DrawLine(new SKPoint(x, yOffset), new SKPoint(x, _rendererHeight), timingPaint); + } + } + } + + SKPaint barPaint = new SKPaint() + { + Color = SKColors.Green, + }; + + // Draw bars + for (int verticalIndex = 0; verticalIndex < _sortedProfileData.Count; verticalIndex++) + { + KeyValuePair entry = _sortedProfileData[verticalIndex]; + long furthest = 0; + + bottom = GetLineY(yOffset, LineHeight, LinePadding, false, verticalIndex); + top = bottom + barHeight; + + // Skip rendering out of bounds bars + if (top < 0 || bottom > _rendererHeight) + { + continue; + } + + barPaint.Color = SKColors.Green; + + foreach (Timestamp timestamp in entry.Value.GetAllTimestamps()) + { + // Skip drawing multiple timestamps on same pixel + if (timestamp.EndTime < furthest) + { + continue; + } + + furthest = timestamp.EndTime + ticksPerPixel; + + left = (int)(graphRight - ((graphPositionTicks - timestamp.BeginTime) / timeWidthTicks) * width); + right = (int)(graphRight - ((graphPositionTicks - timestamp.EndTime) / timeWidthTicks) * width); + + left = (int)Math.Max(xOffset +1, left); + + // Make sure width is at least 1px + right = Math.Max(left + 1, right); + + canvas.DrawRect(new SKRect(left, top, right, bottom), barPaint); + } + + // Currently capturing timestamp + barPaint.Color = SKColors.Red; + + long entryBegin = entry.Value.BeginTime; + + if (entryBegin != -1) + { + left = (int)(graphRight - ((graphPositionTicks - entryBegin) / timeWidthTicks) * width); + + // Make sure width is at least 1px + left = Math.Min(left - 1, (int)graphRight); + + left = (int)Math.Max(xOffset + 1, left); + + canvas.DrawRect(new SKRect(left, top, graphRight, bottom), barPaint); + } + } + + string label = $"-{MathF.Round(_graphPosition, 2)} ms"; + + SKPaint labelPaint = new SKPaint() + { + Color = SKColors.White, + TextSize = LineHeight + }; + + float labelWidth = labelPaint.MeasureText(label); + + canvas.DrawText(label,new SKPoint(graphRight - labelWidth - LinePadding, FilterHeight + LinePadding) , labelPaint); + + canvas.DrawText($"-{MathF.Round((float)((timeWidthTicks / PerformanceCounter.TicksPerMillisecond) + _graphPosition), 2)} ms", + new SKPoint(xOffset + LinePadding, FilterHeight + LinePadding), labelPaint); + } + } + + private void DrawBars(float xOffset, float yOffset, float width, SKCanvas canvas) + { + if (_sortedProfileData.Count != 0) + { + long maxAverage = 0; + long maxTotal = 0; + long maxInstant = 0; + + float barHeight = (LineHeight - LinePadding) / 3.0f; + + // Get max values + foreach (KeyValuePair kvp in _sortedProfileData) + { + maxInstant = Math.Max(maxInstant, kvp.Value.Instant); + maxAverage = Math.Max(maxAverage, kvp.Value.AverageTime); + maxTotal = Math.Max(maxTotal, kvp.Value.TotalTime); + } + + SKPaint barPaint = new SKPaint() + { + Color = SKColors.Blue + }; + + for (int verticalIndex = 0; verticalIndex < _sortedProfileData.Count; verticalIndex++) + { + KeyValuePair entry = _sortedProfileData[verticalIndex]; + // Instant + barPaint.Color = SKColors.Blue; + + float bottom = GetLineY(yOffset, LineHeight, LinePadding, false, verticalIndex); + float top = bottom + barHeight; + float right = (float)entry.Value.Instant / maxInstant * width + xOffset; + + // Skip rendering out of bounds bars + if (top < 0 || bottom > _rendererHeight) + { + continue; + } + + canvas.DrawRect(new SKRect(xOffset, top, right, bottom), barPaint); + + // Average + barPaint.Color = SKColors.Green; + + top += barHeight; + bottom += barHeight; + right = (float)entry.Value.AverageTime / maxAverage * width + xOffset; + + canvas.DrawRect(new SKRect(xOffset, top, right, bottom), barPaint); + + // Total + barPaint.Color = SKColors.Red; + + top += barHeight; + bottom += barHeight; + right = (float)entry.Value.TotalTime / maxTotal * width + xOffset; + + canvas.DrawRect(new SKRect(xOffset, top, right, bottom), barPaint); + } + } + } + } +} diff --git a/Ryujinx.Debugger/UI/ProfilerWidget.glade b/Ryujinx.Debugger/UI/ProfilerWidget.glade new file mode 100644 index 00000000..00dd4f70 --- /dev/null +++ b/Ryujinx.Debugger/UI/ProfilerWidget.glade @@ -0,0 +1,232 @@ + + + + + + + + + + + + + + 0 + Graph + + + 1 + Bars + + + + + ProfilerBox + True + False + 5 + 5 + 5 + 5 + vertical + 10 + + + Enable Profiler + True + True + False + True + + + False + True + 0 + + + + + True + False + + + True + True + never + in + + + + + + True + True + 0 + + + + + True + False + vertical + + + False + True + 1 + + + + + False + True + 1 + + + + + True + False + 10 + + + Show Inactive + True + True + False + True + True + + + False + True + 0 + + + + + Show Flags + True + True + False + True + True + + + False + True + 1 + + + + + Paused + True + True + False + True + + + False + True + 2 + + + + + True + False + + + True + False + View Mode: + + + False + True + 0 + + + + + True + False + viewMode + 0 + + + + 1 + + + + + False + True + 1 + + + + + False + True + 3 + + + + + True + False + + + True + False + Filter: + + + False + True + 0 + + + + + True + True + + + False + True + 1 + + + + + False + True + 4 + + + + + Step + True + True + True + + + False + True + 5 + + + + + False + True + 2 + + + + diff --git a/Ryujinx.Debugger/UI/SkRenderer.cs b/Ryujinx.Debugger/UI/SkRenderer.cs new file mode 100644 index 00000000..a95e4542 --- /dev/null +++ b/Ryujinx.Debugger/UI/SkRenderer.cs @@ -0,0 +1,23 @@ +using SkiaSharp; +using SkiaSharp.Views.Gtk; +using System; + +namespace Ryujinx.Debugger.UI +{ + public class SkRenderer : SKDrawingArea + { + public event EventHandler DrawGraphs; + + public SkRenderer() + { + this.PaintSurface += SkRenderer_PaintSurface; + } + + private void SkRenderer_PaintSurface(object sender, SkiaSharp.Views.Desktop.SKPaintSurfaceEventArgs e) + { + e.Surface.Canvas.Clear(SKColors.Black); + + DrawGraphs.Invoke(this, e); + } + } +} diff --git a/Ryujinx.HLE/HOS/Services/IpcService.cs b/Ryujinx.HLE/HOS/Services/IpcService.cs index e9538683..67df453e 100644 --- a/Ryujinx.HLE/HOS/Services/IpcService.cs +++ b/Ryujinx.HLE/HOS/Services/IpcService.cs @@ -6,7 +6,7 @@ using Ryujinx.HLE.HOS.Kernel.Ipc; using System; using System.Collections.Generic; using System.IO; -using Ryujinx.Profiler; +using Ryujinx.Debugger.Profiler; using System.Reflection; using System.Linq; diff --git a/Ryujinx.HLE/PerformanceStatistics.cs b/Ryujinx.HLE/PerformanceStatistics.cs index 896ab67b..5abf2628 100644 --- a/Ryujinx.HLE/PerformanceStatistics.cs +++ b/Ryujinx.HLE/PerformanceStatistics.cs @@ -1,4 +1,4 @@ -using Ryujinx.Profiler; +using Ryujinx.Debugger.Profiler; using System.Diagnostics; using System.Timers; diff --git a/Ryujinx.HLE/Ryujinx.HLE.csproj b/Ryujinx.HLE/Ryujinx.HLE.csproj index ac4b8224..e9540ab8 100644 --- a/Ryujinx.HLE/Ryujinx.HLE.csproj +++ b/Ryujinx.HLE/Ryujinx.HLE.csproj @@ -12,7 +12,7 @@ true - TRACE;USE_PROFILING + TRACE;USE_DEBUGGING false @@ -22,7 +22,7 @@ true - TRACE;USE_PROFILING + TRACE;USE_DEBUGGING true @@ -44,7 +44,7 @@ - + diff --git a/Ryujinx.LLE/Luea.csproj b/Ryujinx.LLE/Luea.csproj index 7eb546d8..0184d1be 100644 --- a/Ryujinx.LLE/Luea.csproj +++ b/Ryujinx.LLE/Luea.csproj @@ -8,12 +8,12 @@ - TRACE;USE_PROFILING + TRACE;USE_DEBUGGING true - TRACE;USE_PROFILING + TRACE;USE_DEBUGGING false diff --git a/Ryujinx.Profiler/DumpProfile.cs b/Ryujinx.Profiler/DumpProfile.cs deleted file mode 100644 index 62a02761..00000000 --- a/Ryujinx.Profiler/DumpProfile.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Ryujinx.Common; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; - -namespace Ryujinx.Profiler -{ - public static class DumpProfile - { - public static void ToFile(string path, InternalProfile profile) - { - String fileData = "Category,Session Group,Session Item,Count,Average(ms),Total(ms)\r\n"; - - foreach (KeyValuePair time in profile.Timers.OrderBy(key => key.Key.Tag)) - { - fileData += $"{time.Key.Category}," + - $"{time.Key.SessionGroup}," + - $"{time.Key.SessionItem}," + - $"{time.Value.Count}," + - $"{time.Value.AverageTime / PerformanceCounter.TicksPerMillisecond}," + - $"{time.Value.TotalTime / PerformanceCounter.TicksPerMillisecond}\r\n"; - } - - // Ensure file directory exists before write - FileInfo fileInfo = new FileInfo(path); - if (fileInfo == null) - throw new Exception("Unknown logging error, probably a bad file path"); - if (fileInfo.Directory != null && !fileInfo.Directory.Exists) - Directory.CreateDirectory(fileInfo.Directory.FullName); - - File.WriteAllText(fileInfo.FullName, fileData); - } - } -} diff --git a/Ryujinx.Profiler/InternalProfile.cs b/Ryujinx.Profiler/InternalProfile.cs deleted file mode 100644 index 03462444..00000000 --- a/Ryujinx.Profiler/InternalProfile.cs +++ /dev/null @@ -1,223 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Ryujinx.Common; - -namespace Ryujinx.Profiler -{ - public class InternalProfile - { - private struct TimerQueueValue - { - public ProfileConfig Config; - public long Time; - public bool IsBegin; - } - - internal Dictionary Timers { get; set; } - - private readonly object _timerQueueClearLock = new object(); - private ConcurrentQueue _timerQueue; - - private int _sessionCounter = 0; - - // Cleanup thread - private readonly Thread _cleanupThread; - private bool _cleanupRunning; - private readonly long _history; - private long _preserve; - - // Timing flags - private TimingFlag[] _timingFlags; - private long[] _timingFlagAverages; - private long[] _timingFlagLast; - private long[] _timingFlagLastDelta; - private int _timingFlagCount; - private int _timingFlagIndex; - - private int _maxFlags; - - private Action _timingFlagCallback; - - public InternalProfile(long history, int maxFlags) - { - _maxFlags = maxFlags; - Timers = new Dictionary(); - _timingFlags = new TimingFlag[_maxFlags]; - _timingFlagAverages = new long[(int)TimingFlagType.Count]; - _timingFlagLast = new long[(int)TimingFlagType.Count]; - _timingFlagLastDelta = new long[(int)TimingFlagType.Count]; - _timerQueue = new ConcurrentQueue(); - _history = history; - _cleanupRunning = true; - - // Create cleanup thread. - _cleanupThread = new Thread(CleanupLoop) - { - Name = "Profiler.CleanupThread" - }; - _cleanupThread.Start(); - } - - private void CleanupLoop() - { - bool queueCleared = false; - - while (_cleanupRunning) - { - // Ensure we only ever have 1 instance modifying timers or timerQueue - if (Monitor.TryEnter(_timerQueueClearLock)) - { - queueCleared = ClearTimerQueue(); - - // Calculate before foreach to mitigate redundant calculations - long cleanupBefore = PerformanceCounter.ElapsedTicks - _history; - long preserveStart = _preserve - _history; - - // Each cleanup is self contained so run in parallel for maximum efficiency - Parallel.ForEach(Timers, (t) => t.Value.Cleanup(cleanupBefore, preserveStart, _preserve)); - - Monitor.Exit(_timerQueueClearLock); - } - - // Only sleep if queue was successfully cleared - if (queueCleared) - { - Thread.Sleep(5); - } - } - } - - private bool ClearTimerQueue() - { - int count = 0; - - while (_timerQueue.TryDequeue(out TimerQueueValue item)) - { - if (!Timers.TryGetValue(item.Config, out TimingInfo value)) - { - value = new TimingInfo(); - Timers.Add(item.Config, value); - } - - if (item.IsBegin) - { - value.Begin(item.Time); - } - else - { - value.End(item.Time); - } - - // Don't block for too long as memory disposal is blocked while this function runs - if (count++ > 10000) - { - return false; - } - } - - return true; - } - - public void FlagTime(TimingFlagType flagType) - { - int flagId = (int)flagType; - - _timingFlags[_timingFlagIndex] = new TimingFlag() - { - FlagType = flagType, - Timestamp = PerformanceCounter.ElapsedTicks - }; - - _timingFlagCount = Math.Max(_timingFlagCount + 1, _maxFlags); - - // Work out average - if (_timingFlagLast[flagId] != 0) - { - _timingFlagLastDelta[flagId] = _timingFlags[_timingFlagIndex].Timestamp - _timingFlagLast[flagId]; - _timingFlagAverages[flagId] = (_timingFlagAverages[flagId] == 0) ? _timingFlagLastDelta[flagId] : - (_timingFlagLastDelta[flagId] + _timingFlagAverages[flagId]) >> 1; - } - _timingFlagLast[flagId] = _timingFlags[_timingFlagIndex].Timestamp; - - // Notify subscribers - _timingFlagCallback?.Invoke(_timingFlags[_timingFlagIndex]); - - if (++_timingFlagIndex >= _maxFlags) - { - _timingFlagIndex = 0; - } - } - - public void BeginProfile(ProfileConfig config) - { - _timerQueue.Enqueue(new TimerQueueValue() - { - Config = config, - IsBegin = true, - Time = PerformanceCounter.ElapsedTicks, - }); - } - - public void EndProfile(ProfileConfig config) - { - _timerQueue.Enqueue(new TimerQueueValue() - { - Config = config, - IsBegin = false, - Time = PerformanceCounter.ElapsedTicks, - }); - } - - public string GetSession() - { - // Can be called from multiple threads so we need to ensure no duplicate sessions are generated - return Interlocked.Increment(ref _sessionCounter).ToString(); - } - - public List> GetProfilingData() - { - _preserve = PerformanceCounter.ElapsedTicks; - - lock (_timerQueueClearLock) - { - ClearTimerQueue(); - return Timers.ToList(); - } - } - - public TimingFlag[] GetTimingFlags() - { - int count = Math.Max(_timingFlagCount, _maxFlags); - TimingFlag[] outFlags = new TimingFlag[count]; - - for (int i = 0, sourceIndex = _timingFlagIndex; i < count; i++, sourceIndex++) - { - if (sourceIndex >= _maxFlags) - sourceIndex = 0; - outFlags[i] = _timingFlags[sourceIndex]; - } - - return outFlags; - } - - public (long[], long[]) GetTimingAveragesAndLast() - { - return (_timingFlagAverages, _timingFlagLastDelta); - } - - public void RegisterFlagReceiver(Action receiver) - { - _timingFlagCallback = receiver; - } - - public void Dispose() - { - _cleanupRunning = false; - _cleanupThread.Join(); - } - } -} diff --git a/Ryujinx.Profiler/Profile.cs b/Ryujinx.Profiler/Profile.cs deleted file mode 100644 index 4dba6ea5..00000000 --- a/Ryujinx.Profiler/Profile.cs +++ /dev/null @@ -1,144 +0,0 @@ -using Ryujinx.Common; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; - -namespace Ryujinx.Profiler -{ - public static class Profile - { - public static float UpdateRate => _settings.UpdateRate; - public static long HistoryLength => _settings.History; - - public static ProfilerKeyboardHandler Controls => _settings.Controls; - - private static InternalProfile _profileInstance; - private static ProfilerSettings _settings; - - [Conditional("USE_PROFILING")] - public static void Initialize() - { - var config = ProfilerConfiguration.Load(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ProfilerConfig.jsonc")); - - _settings = new ProfilerSettings() - { - Enabled = config.Enabled, - FileDumpEnabled = config.DumpPath != "", - DumpLocation = config.DumpPath, - UpdateRate = (config.UpdateRate <= 0) ? -1 : 1.0f / config.UpdateRate, - History = (long)(config.History * PerformanceCounter.TicksPerSecond), - MaxLevel = config.MaxLevel, - Controls = config.Controls, - MaxFlags = config.MaxFlags, - }; - } - - public static bool ProfilingEnabled() - { -#if USE_PROFILING - if (!_settings.Enabled) - return false; - - if (_profileInstance == null) - _profileInstance = new InternalProfile(_settings.History, _settings.MaxFlags); - - return true; -#else - return false; -#endif - } - - [Conditional("USE_PROFILING")] - public static void FinishProfiling() - { - if (!ProfilingEnabled()) - return; - - if (_settings.FileDumpEnabled) - DumpProfile.ToFile(_settings.DumpLocation, _profileInstance); - - _profileInstance.Dispose(); - } - - [Conditional("USE_PROFILING")] - public static void FlagTime(TimingFlagType flagType) - { - if (!ProfilingEnabled()) - return; - _profileInstance.FlagTime(flagType); - } - - [Conditional("USE_PROFILING")] - public static void RegisterFlagReceiver(Action receiver) - { - if (!ProfilingEnabled()) - return; - _profileInstance.RegisterFlagReceiver(receiver); - } - - [Conditional("USE_PROFILING")] - public static void Begin(ProfileConfig config) - { - if (!ProfilingEnabled()) - return; - if (config.Level > _settings.MaxLevel) - return; - _profileInstance.BeginProfile(config); - } - - [Conditional("USE_PROFILING")] - public static void End(ProfileConfig config) - { - if (!ProfilingEnabled()) - return; - if (config.Level > _settings.MaxLevel) - return; - _profileInstance.EndProfile(config); - } - - public static string GetSession() - { -#if USE_PROFILING - if (!ProfilingEnabled()) - return null; - return _profileInstance.GetSession(); -#else - return ""; -#endif - } - - public static List> GetProfilingData() - { -#if USE_PROFILING - if (!ProfilingEnabled()) - return new List>(); - return _profileInstance.GetProfilingData(); -#else - return new List>(); -#endif - } - - public static TimingFlag[] GetTimingFlags() - { -#if USE_PROFILING - if (!ProfilingEnabled()) - return new TimingFlag[0]; - return _profileInstance.GetTimingFlags(); -#else - return new TimingFlag[0]; -#endif - } - - public static (long[], long[]) GetTimingAveragesAndLast() - { -#if USE_PROFILING - if (!ProfilingEnabled()) - return (new long[0], new long[0]); - return _profileInstance.GetTimingAveragesAndLast(); -#else - return (new long[0], new long[0]); -#endif - } - } -} diff --git a/Ryujinx.Profiler/ProfileConfig.cs b/Ryujinx.Profiler/ProfileConfig.cs deleted file mode 100644 index 4271bd2b..00000000 --- a/Ryujinx.Profiler/ProfileConfig.cs +++ /dev/null @@ -1,254 +0,0 @@ -using System; - -namespace Ryujinx.Profiler -{ - public struct ProfileConfig : IEquatable - { - public string Category; - public string SessionGroup; - public string SessionItem; - - public int Level; - - // Private cached variables - private string _cachedTag; - private string _cachedSession; - private string _cachedSearch; - - // Public helpers to get config in more user friendly format, - // Cached because they never change and are called often - public string Search - { - get - { - if (_cachedSearch == null) - { - _cachedSearch = $"{Category}.{SessionGroup}.{SessionItem}"; - } - - return _cachedSearch; - } - } - - public string Tag - { - get - { - if (_cachedTag == null) - _cachedTag = $"{Category}{(Session == "" ? "" : $" ({Session})")}"; - return _cachedTag; - } - } - - public string Session - { - get - { - if (_cachedSession == null) - { - if (SessionGroup != null && SessionItem != null) - { - _cachedSession = $"{SessionGroup}: {SessionItem}"; - } - else if (SessionGroup != null) - { - _cachedSession = $"{SessionGroup}"; - } - else if (SessionItem != null) - { - _cachedSession = $"---: {SessionItem}"; - } - else - { - _cachedSession = ""; - } - } - - return _cachedSession; - } - } - - /// - /// The default comparison is far too slow for the number of comparisons needed because it doesn't know what's important to compare - /// - /// Object to compare to - /// - public bool Equals(ProfileConfig cmpObj) - { - // Order here is important. - // Multiple entries with the same item is considerable less likely that multiple items with the same group. - // Likewise for group and category. - return (cmpObj.SessionItem == SessionItem && - cmpObj.SessionGroup == SessionGroup && - cmpObj.Category == Category); - } - } - - /// - /// Predefined configs to make profiling easier, - /// nested so you can reference as Profiles.Category.Group.Item where item and group may be optional - /// - public static class Profiles - { - public static class CPU - { - public static ProfileConfig TranslateTier0 = new ProfileConfig() - { - Category = "CPU", - SessionGroup = "TranslateTier0" - }; - - public static ProfileConfig TranslateTier1 = new ProfileConfig() - { - Category = "CPU", - SessionGroup = "TranslateTier1" - }; - } - - public static class Input - { - public static ProfileConfig ControllerInput = new ProfileConfig - { - Category = "Input", - SessionGroup = "ControllerInput" - }; - - public static ProfileConfig TouchInput = new ProfileConfig - { - Category = "Input", - SessionGroup = "TouchInput" - }; - } - - public static class GPU - { - public static class Engine2d - { - public static ProfileConfig TextureCopy = new ProfileConfig() - { - Category = "GPU.Engine2D", - SessionGroup = "TextureCopy" - }; - } - - public static class Engine3d - { - public static ProfileConfig CallMethod = new ProfileConfig() - { - Category = "GPU.Engine3D", - SessionGroup = "CallMethod", - }; - - public static ProfileConfig VertexEnd = new ProfileConfig() - { - Category = "GPU.Engine3D", - SessionGroup = "VertexEnd" - }; - - public static ProfileConfig ClearBuffers = new ProfileConfig() - { - Category = "GPU.Engine3D", - SessionGroup = "ClearBuffers" - }; - - public static ProfileConfig SetFrameBuffer = new ProfileConfig() - { - Category = "GPU.Engine3D", - SessionGroup = "SetFrameBuffer", - }; - - public static ProfileConfig SetZeta = new ProfileConfig() - { - Category = "GPU.Engine3D", - SessionGroup = "SetZeta" - }; - - public static ProfileConfig UploadShaders = new ProfileConfig() - { - Category = "GPU.Engine3D", - SessionGroup = "UploadShaders" - }; - - public static ProfileConfig UploadTextures = new ProfileConfig() - { - Category = "GPU.Engine3D", - SessionGroup = "UploadTextures" - }; - - public static ProfileConfig UploadTexture = new ProfileConfig() - { - Category = "GPU.Engine3D", - SessionGroup = "UploadTexture" - }; - - public static ProfileConfig UploadConstBuffers = new ProfileConfig() - { - Category = "GPU.Engine3D", - SessionGroup = "UploadConstBuffers" - }; - - public static ProfileConfig UploadVertexArrays = new ProfileConfig() - { - Category = "GPU.Engine3D", - SessionGroup = "UploadVertexArrays" - }; - - public static ProfileConfig ConfigureState = new ProfileConfig() - { - Category = "GPU.Engine3D", - SessionGroup = "ConfigureState" - }; - } - - public static class EngineM2mf - { - public static ProfileConfig CallMethod = new ProfileConfig() - { - Category = "GPU.EngineM2mf", - SessionGroup = "CallMethod", - }; - - public static ProfileConfig Execute = new ProfileConfig() - { - Category = "GPU.EngineM2mf", - SessionGroup = "Execute", - }; - } - - public static class EngineP2mf - { - public static ProfileConfig CallMethod = new ProfileConfig() - { - Category = "GPU.EngineP2mf", - SessionGroup = "CallMethod", - }; - - public static ProfileConfig Execute = new ProfileConfig() - { - Category = "GPU.EngineP2mf", - SessionGroup = "Execute", - }; - - public static ProfileConfig PushData = new ProfileConfig() - { - Category = "GPU.EngineP2mf", - SessionGroup = "PushData", - }; - } - - public static class Shader - { - public static ProfileConfig Decompile = new ProfileConfig() - { - Category = "GPU.Shader", - SessionGroup = "Decompile", - }; - } - } - - public static ProfileConfig ServiceCall = new ProfileConfig() - { - Category = "ServiceCall", - }; - } -} diff --git a/Ryujinx.Profiler/ProfilerConfig.jsonc b/Ryujinx.Profiler/ProfilerConfig.jsonc deleted file mode 100644 index e6714386..00000000 --- a/Ryujinx.Profiler/ProfilerConfig.jsonc +++ /dev/null @@ -1,28 +0,0 @@ -{ - // Enable profiling (Only available on a profiling enabled builds) - "enabled": true, - - // Set profile file dump location, if blank file dumping disabled. (e.g. `ProfileDump.csv`) - "dump_path": "", - - // Update rate for profiler UI, in hertz. -1 updates every time a frame is issued - "update_rate": 4.0, - - // Set how long to keep profiling data in seconds, reduce if profiling is taking too much RAM - "history": 5.0, - - // Set the maximum profiling level. Higher values may cause a heavy load on your system but will allow you to profile in more detail - "max_level": 0, - - // Sets the maximum number of flags to keep - "max_flags": 1000, - - // Keyboard Controls - // https://github.com/opentk/opentk/blob/master/src/OpenTK/Input/Key.cs - "controls": { - "buttons": { - // Show/Hide the profiler - "toggle_profiler": "F2" - } - } -} \ No newline at end of file diff --git a/Ryujinx.Profiler/ProfilerConfiguration.cs b/Ryujinx.Profiler/ProfilerConfiguration.cs deleted file mode 100644 index 4fe616fa..00000000 --- a/Ryujinx.Profiler/ProfilerConfiguration.cs +++ /dev/null @@ -1,70 +0,0 @@ -using OpenTK.Input; -using System; -using System.IO; -using Utf8Json; -using Utf8Json.Resolvers; - -namespace Ryujinx.Profiler -{ - public class ProfilerConfiguration - { - public bool Enabled { get; private set; } - public string DumpPath { get; private set; } - public float UpdateRate { get; private set; } - public int MaxLevel { get; private set; } - public int MaxFlags { get; private set; } - public float History { get; private set; } - - public ProfilerKeyboardHandler Controls { get; private set; } - - /// - /// Loads a configuration file from disk - /// - /// The path to the JSON configuration file - public static ProfilerConfiguration Load(string path) - { - var resolver = CompositeResolver.Create( - new[] { new ConfigurationEnumFormatter() }, - new[] { StandardResolver.AllowPrivateSnakeCase } - ); - - if (!File.Exists(path)) - { - throw new FileNotFoundException($"Profiler configuration file {path} not found"); - } - - using (Stream stream = File.OpenRead(path)) - { - return JsonSerializer.Deserialize(stream, resolver); - } - } - - private class ConfigurationEnumFormatter : IJsonFormatter - where T : struct - { - public void Serialize(ref JsonWriter writer, T value, IJsonFormatterResolver formatterResolver) - { - formatterResolver.GetFormatterWithVerify() - .Serialize(ref writer, value.ToString(), formatterResolver); - } - - public T Deserialize(ref JsonReader reader, IJsonFormatterResolver formatterResolver) - { - if (reader.ReadIsNull()) - { - return default(T); - } - - string enumName = formatterResolver.GetFormatterWithVerify() - .Deserialize(ref reader, formatterResolver); - - if (Enum.TryParse(enumName, out T result)) - { - return result; - } - - return default(T); - } - } - } -} diff --git a/Ryujinx.Profiler/ProfilerKeyboardHandler.cs b/Ryujinx.Profiler/ProfilerKeyboardHandler.cs deleted file mode 100644 index e6207e89..00000000 --- a/Ryujinx.Profiler/ProfilerKeyboardHandler.cs +++ /dev/null @@ -1,28 +0,0 @@ -using OpenTK.Input; - -namespace Ryujinx.Profiler -{ - public struct ProfilerButtons - { - public Key ToggleProfiler; - } - - public class ProfilerKeyboardHandler - { - public ProfilerButtons Buttons; - - private KeyboardState _prevKeyboard; - - public ProfilerKeyboardHandler(ProfilerButtons buttons) - { - Buttons = buttons; - } - - public bool TogglePressed(KeyboardState keyboard) => !keyboard[Buttons.ToggleProfiler] && _prevKeyboard[Buttons.ToggleProfiler]; - - public void SetPrevKeyboardState(KeyboardState keyboard) - { - _prevKeyboard = keyboard; - } - } -} diff --git a/Ryujinx.Profiler/Ryujinx.Profiler.csproj b/Ryujinx.Profiler/Ryujinx.Profiler.csproj deleted file mode 100644 index 0e089ccf..00000000 --- a/Ryujinx.Profiler/Ryujinx.Profiler.csproj +++ /dev/null @@ -1,39 +0,0 @@ - - - - netcoreapp3.0 - win-x64;osx-x64;linux-x64 - true - Debug;Release;Profile Debug;Profile Release - - - - TRACE - - - - TRACE;USE_PROFILING - false - - - - TRACE;USE_PROFILING - true - - - - - - - - - - - - - - PreserveNewest - - - - diff --git a/Ryujinx.Profiler/Settings.cs b/Ryujinx.Profiler/Settings.cs deleted file mode 100644 index f0c851b2..00000000 --- a/Ryujinx.Profiler/Settings.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Ryujinx.Profiler -{ - public class ProfilerSettings - { - // Default settings for profiler - public bool Enabled { get; set; } = false; - public bool FileDumpEnabled { get; set; } = false; - public string DumpLocation { get; set; } = ""; - public float UpdateRate { get; set; } = 0.1f; - public int MaxLevel { get; set; } = 0; - public int MaxFlags { get; set; } = 1000; - - // 19531225 = 5 seconds in ticks on most pc's. - // It should get set on boot to the time specified in config - public long History { get; set; } = 19531225; - - // Controls - public ProfilerKeyboardHandler Controls; - } -} diff --git a/Ryujinx.Profiler/TimingFlag.cs b/Ryujinx.Profiler/TimingFlag.cs deleted file mode 100644 index 0cf55bdf..00000000 --- a/Ryujinx.Profiler/TimingFlag.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Ryujinx.Profiler -{ - public enum TimingFlagType - { - FrameSwap = 0, - SystemFrame = 1, - - // Update this for new flags - Count = 2, - } - - public struct TimingFlag - { - public TimingFlagType FlagType; - public long Timestamp; - } -} diff --git a/Ryujinx.Profiler/TimingInfo.cs b/Ryujinx.Profiler/TimingInfo.cs deleted file mode 100644 index 6058ddbd..00000000 --- a/Ryujinx.Profiler/TimingInfo.cs +++ /dev/null @@ -1,174 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Ryujinx.Profiler -{ - public struct Timestamp - { - public long BeginTime; - public long EndTime; - } - - public class TimingInfo - { - // Timestamps - public long TotalTime { get; set; } - public long Instant { get; set; } - - // Measurement counts - public int Count { get; set; } - public int InstantCount { get; set; } - - // Work out average - public long AverageTime => (Count == 0) ? -1 : TotalTime / Count; - - // Intentionally not locked as it's only a get count - public bool IsActive => _timestamps.Count > 0; - - public long BeginTime - { - get - { - lock (_timestampLock) - { - if (_depth > 0) - { - return _currentTimestamp.BeginTime; - } - - return -1; - } - } - } - - // Timestamp collection - private List _timestamps; - private readonly object _timestampLock = new object(); - private readonly object _timestampListLock = new object(); - private Timestamp _currentTimestamp; - - // Depth of current timer, - // each begin call increments and each end call decrements - private int _depth; - - public TimingInfo() - { - _timestamps = new List(); - _depth = 0; - } - - public void Begin(long beginTime) - { - lock (_timestampLock) - { - // Finish current timestamp if already running - if (_depth > 0) - { - EndUnsafe(beginTime); - } - - BeginUnsafe(beginTime); - _depth++; - } - } - - private void BeginUnsafe(long beginTime) - { - _currentTimestamp.BeginTime = beginTime; - _currentTimestamp.EndTime = -1; - } - - public void End(long endTime) - { - lock (_timestampLock) - { - _depth--; - - if (_depth < 0) - { - throw new Exception("Timing info end called without corresponding begin"); - } - - EndUnsafe(endTime); - - // Still have others using this timing info so recreate start for them - if (_depth > 0) - { - BeginUnsafe(endTime); - } - } - } - - private void EndUnsafe(long endTime) - { - _currentTimestamp.EndTime = endTime; - lock (_timestampListLock) - { - _timestamps.Add(_currentTimestamp); - } - - long delta = _currentTimestamp.EndTime - _currentTimestamp.BeginTime; - TotalTime += delta; - Instant += delta; - - Count++; - InstantCount++; - } - - // Remove any timestamps before given timestamp to free memory - public void Cleanup(long before, long preserveStart, long preserveEnd) - { - lock (_timestampListLock) - { - int toRemove = 0; - int toPreserveStart = 0; - int toPreserveLen = 0; - - for (int i = 0; i < _timestamps.Count; i++) - { - if (_timestamps[i].EndTime < preserveStart) - { - toPreserveStart++; - InstantCount--; - Instant -= _timestamps[i].EndTime - _timestamps[i].BeginTime; - } - else if (_timestamps[i].EndTime < preserveEnd) - { - toPreserveLen++; - } - else if (_timestamps[i].EndTime < before) - { - toRemove++; - InstantCount--; - Instant -= _timestamps[i].EndTime - _timestamps[i].BeginTime; - } - else - { - // Assume timestamps are in chronological order so no more need to be removed - break; - } - } - - if (toPreserveStart > 0) - { - _timestamps.RemoveRange(0, toPreserveStart); - } - - if (toRemove > 0) - { - _timestamps.RemoveRange(toPreserveLen, toRemove); - } - } - } - - public Timestamp[] GetAllTimestamps() - { - lock (_timestampListLock) - { - Timestamp[] returnTimestamps = new Timestamp[_timestamps.Count]; - _timestamps.CopyTo(returnTimestamps); - return returnTimestamps; - } - } - } -} diff --git a/Ryujinx.Profiler/UI/ProfileButton.cs b/Ryujinx.Profiler/UI/ProfileButton.cs deleted file mode 100644 index 7e2ae728..00000000 --- a/Ryujinx.Profiler/UI/ProfileButton.cs +++ /dev/null @@ -1,110 +0,0 @@ -using System; -using OpenTK; -using OpenTK.Graphics.OpenGL; -using Ryujinx.Profiler.UI.SharpFontHelpers; - -namespace Ryujinx.Profiler.UI -{ - public class ProfileButton - { - // Store font service - private FontService _fontService; - - // Layout information - private int _left, _right; - private int _bottom, _top; - private int _height; - private int _padding; - - // Label information - private int _labelX, _labelY; - private string _label; - - // Misc - private Action _clicked; - private bool _visible; - - public ProfileButton(FontService fontService, Action clicked) - : this(fontService, clicked, 0, 0, 0, 0, 0) - { - _visible = false; - } - - public ProfileButton(FontService fontService, Action clicked, int x, int y, int padding, int height, int width) - : this(fontService, "", clicked, x, y, padding, height, width) - { - _visible = false; - } - - public ProfileButton(FontService fontService, string label, Action clicked, int x, int y, int padding, int height, int width = -1) - { - _fontService = fontService; - _clicked = clicked; - - UpdateSize(label, x, y, padding, height, width); - } - - public int UpdateSize(string label, int x, int y, int padding, int height, int width = -1) - { - _visible = true; - _label = label; - - if (width == -1) - { - // Dummy draw to measure size - width = (int)_fontService.DrawText(label, 0, 0, height, false); - } - - UpdateSize(x, y, padding, width, height); - - return _right - _left; - } - - public void UpdateSize(int x, int y, int padding, int width, int height) - { - _height = height; - _left = x; - _bottom = y; - _labelX = x + padding / 2; - _labelY = y + padding / 2; - _top = y + height + padding; - _right = x + width + padding; - } - - public void Draw() - { - if (!_visible) - { - return; - } - - // Draw backing rectangle - GL.Begin(PrimitiveType.Triangles); - GL.Color3(Color.Black); - GL.Vertex2(_left, _bottom); - GL.Vertex2(_left, _top); - GL.Vertex2(_right, _top); - - GL.Vertex2(_right, _top); - GL.Vertex2(_right, _bottom); - GL.Vertex2(_left, _bottom); - GL.End(); - - // Use font service to draw label - _fontService.DrawText(_label, _labelX, _labelY, _height); - } - - public bool ProcessClick(int x, int y) - { - // If button contains x, y - if (x > _left && x < _right && - y > _bottom && y < _top) - { - _clicked(); - return true; - } - - return false; - } - } -} diff --git a/Ryujinx.Profiler/UI/ProfileSorters.cs b/Ryujinx.Profiler/UI/ProfileSorters.cs deleted file mode 100644 index 9f66de22..00000000 --- a/Ryujinx.Profiler/UI/ProfileSorters.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Ryujinx.Profiler.UI -{ - public static class ProfileSorters - { - public class InstantAscending : IComparer> - { - public int Compare(KeyValuePair pair1, KeyValuePair pair2) - => pair2.Value.Instant.CompareTo(pair1.Value.Instant); - } - - public class AverageAscending : IComparer> - { - public int Compare(KeyValuePair pair1, KeyValuePair pair2) - => pair2.Value.AverageTime.CompareTo(pair1.Value.AverageTime); - } - - public class TotalAscending : IComparer> - { - public int Compare(KeyValuePair pair1, KeyValuePair pair2) - => pair2.Value.TotalTime.CompareTo(pair1.Value.TotalTime); - } - - public class TagAscending : IComparer> - { - public int Compare(KeyValuePair pair1, KeyValuePair pair2) - => StringComparer.CurrentCulture.Compare(pair1.Key.Search, pair2.Key.Search); - } - } -} diff --git a/Ryujinx.Profiler/UI/ProfileWindow.cs b/Ryujinx.Profiler/UI/ProfileWindow.cs deleted file mode 100644 index 1db70bc7..00000000 --- a/Ryujinx.Profiler/UI/ProfileWindow.cs +++ /dev/null @@ -1,773 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Text.RegularExpressions; -using OpenTK; -using OpenTK.Graphics; -using OpenTK.Graphics.OpenGL; -using OpenTK.Input; -using Ryujinx.Common; -using Ryujinx.Profiler.UI.SharpFontHelpers; - -namespace Ryujinx.Profiler.UI -{ - public partial class ProfileWindow : GameWindow - { - // List all buttons for index in button array - private enum ButtonIndex - { - TagTitle = 0, - InstantTitle = 1, - AverageTitle = 2, - TotalTitle = 3, - FilterBar = 4, - ShowHideInactive = 5, - Pause = 6, - ChangeDisplay = 7, - - // Don't automatically draw after here - ToggleFlags = 8, - Step = 9, - - // Update this when new buttons are added. - // These are indexes to the enum list - Autodraw = 8, - Count = 10, - } - - // Font service - private FontService _fontService; - - // UI variables - private ProfileButton[] _buttons; - - private bool _initComplete = false; - private bool _visible = true; - private bool _visibleChanged = true; - private bool _viewportUpdated = true; - private bool _redrawPending = true; - private bool _displayGraph = true; - private bool _displayFlags = true; - private bool _showInactive = true; - private bool _paused = false; - private bool _doStep = false; - - // Layout - private const int LineHeight = 16; - private const int TitleHeight = 24; - private const int TitleFontHeight = 16; - private const int LinePadding = 2; - private const int ColumnSpacing = 15; - private const int FilterHeight = 24; - private const int BottomBarHeight = FilterHeight + LineHeight; - - // Sorting - private List> _unsortedProfileData; - private IComparer> _sortAction = new ProfileSorters.TagAscending(); - - // Flag data - private long[] _timingFlagsAverages; - private long[] _timingFlagsLast; - - // Filtering - private string _filterText = ""; - private bool _regexEnabled = false; - - // Scrolling - private float _scrollPos = 0; - private float _minScroll = 0; - private float _maxScroll = 0; - - // Profile data storage - private List> _sortedProfileData; - private long _captureTime; - - // Input - private bool _backspaceDown = false; - private bool _prevBackspaceDown = false; - private double _backspaceDownTime = 0; - - // F35 used as no key - private Key _graphControlKey = Key.F35; - - // Event management - private double _updateTimer; - private double _processEventTimer; - private bool _profileUpdated = false; - private readonly object _profileDataLock = new object(); - - public ProfileWindow() - // Graphics mode enables 2xAA - : base(1280, 720, new GraphicsMode(new ColorFormat(8, 8, 8, 8), 1, 1, 2)) - { - Title = "Profiler"; - Location = new Point(DisplayDevice.Default.Width - 1280, - (DisplayDevice.Default.Height - 720) - 50); - - if (Profile.UpdateRate <= 0) - { - // Perform step regardless of flag type - Profile.RegisterFlagReceiver((t) => - { - if (!_paused) - { - _doStep = true; - } - }); - } - - // Large number to force an update on first update - _updateTimer = 0xFFFF; - - Init(); - - // Release context for render thread - Context.MakeCurrent(null); - } - - public void ToggleVisible() - { - _visible = !_visible; - _visibleChanged = true; - } - - private void SetSort(IComparer> filter) - { - _sortAction = filter; - _profileUpdated = true; - } - -#region OnLoad - /// - /// Setup OpenGL and load resources - /// - public void Init() - { - GL.ClearColor(Color.Black); - _fontService = new FontService(); - _fontService.InitializeTextures(); - _fontService.UpdateScreenHeight(Height); - - _buttons = new ProfileButton[(int)ButtonIndex.Count]; - _buttons[(int)ButtonIndex.TagTitle] = new ProfileButton(_fontService, () => SetSort(new ProfileSorters.TagAscending())); - _buttons[(int)ButtonIndex.InstantTitle] = new ProfileButton(_fontService, () => SetSort(new ProfileSorters.InstantAscending())); - _buttons[(int)ButtonIndex.AverageTitle] = new ProfileButton(_fontService, () => SetSort(new ProfileSorters.AverageAscending())); - _buttons[(int)ButtonIndex.TotalTitle] = new ProfileButton(_fontService, () => SetSort(new ProfileSorters.TotalAscending())); - _buttons[(int)ButtonIndex.Step] = new ProfileButton(_fontService, () => _doStep = true); - _buttons[(int)ButtonIndex.FilterBar] = new ProfileButton(_fontService, () => - { - _profileUpdated = true; - _regexEnabled = !_regexEnabled; - }); - - _buttons[(int)ButtonIndex.ShowHideInactive] = new ProfileButton(_fontService, () => - { - _profileUpdated = true; - _showInactive = !_showInactive; - }); - - _buttons[(int)ButtonIndex.Pause] = new ProfileButton(_fontService, () => - { - _profileUpdated = true; - _paused = !_paused; - }); - - _buttons[(int)ButtonIndex.ToggleFlags] = new ProfileButton(_fontService, () => - { - _displayFlags = !_displayFlags; - _redrawPending = true; - }); - - _buttons[(int)ButtonIndex.ChangeDisplay] = new ProfileButton(_fontService, () => - { - _displayGraph = !_displayGraph; - _redrawPending = true; - }); - - Visible = _visible; - } -#endregion - -#region OnResize - /// - /// Respond to resize events - /// - /// Contains information on the new GameWindow size. - /// There is no need to call the base implementation. - protected override void OnResize(EventArgs e) - { - _viewportUpdated = true; - } -#endregion - -#region OnClose - /// - /// Intercept close event and hide instead - /// - protected override void OnClosing(CancelEventArgs e) - { - // Hide window - _visible = false; - _visibleChanged = true; - - // Cancel close - e.Cancel = true; - - base.OnClosing(e); - } -#endregion - -#region OnUpdateFrame - /// - /// Profile Update Loop - /// - /// Contains timing information. - /// There is no need to call the base implementation. - public void Update(FrameEventArgs e) - { - if (_visibleChanged) - { - Visible = _visible; - _visibleChanged = false; - } - - // Backspace handling - if (_backspaceDown) - { - if (!_prevBackspaceDown) - { - _backspaceDownTime = 0; - FilterBackspace(); - } - else - { - _backspaceDownTime += e.Time; - if (_backspaceDownTime > 0.3) - { - _backspaceDownTime -= 0.05; - FilterBackspace(); - } - } - } - _prevBackspaceDown = _backspaceDown; - - // Get timing data if enough time has passed - _updateTimer += e.Time; - if (_doStep || ((Profile.UpdateRate > 0) && (!_paused && (_updateTimer > Profile.UpdateRate)))) - { - _updateTimer = 0; - _captureTime = PerformanceCounter.ElapsedTicks; - _timingFlags = Profile.GetTimingFlags(); - _doStep = false; - _profileUpdated = true; - - _unsortedProfileData = Profile.GetProfilingData(); - (_timingFlagsAverages, _timingFlagsLast) = Profile.GetTimingAveragesAndLast(); - - } - - // Filtering - if (_profileUpdated) - { - lock (_profileDataLock) - { - _sortedProfileData = _showInactive ? _unsortedProfileData : _unsortedProfileData.FindAll(kvp => kvp.Value.IsActive); - - if (_sortAction != null) - { - _sortedProfileData.Sort(_sortAction); - } - - if (_regexEnabled) - { - try - { - Regex filterRegex = new Regex(_filterText, RegexOptions.IgnoreCase); - if (_filterText != "") - { - _sortedProfileData = _sortedProfileData.Where((pair => filterRegex.IsMatch(pair.Key.Search))).ToList(); - } - } - catch (ArgumentException argException) - { - // Skip filtering for invalid regex - } - } - else - { - // Regular filtering - _sortedProfileData = _sortedProfileData.Where((pair => pair.Key.Search.ToLower().Contains(_filterText.ToLower()))).ToList(); - } - } - - _profileUpdated = false; - _redrawPending = true; - _initComplete = true; - } - - // Check for events 20 times a second - _processEventTimer += e.Time; - if (_processEventTimer > 0.05) - { - ProcessEvents(); - - if (_graphControlKey != Key.F35) - { - switch (_graphControlKey) - { - case Key.Left: - _graphPosition += (long) (GraphMoveSpeed * e.Time); - break; - - case Key.Right: - _graphPosition = Math.Max(_graphPosition - (long) (GraphMoveSpeed * e.Time), 0); - break; - - case Key.Up: - _graphZoom = MathF.Min(_graphZoom + (float) (GraphZoomSpeed * e.Time), 100.0f); - break; - - case Key.Down: - _graphZoom = MathF.Max(_graphZoom - (float) (GraphZoomSpeed * e.Time), 1f); - break; - } - - _redrawPending = true; - } - - _processEventTimer = 0; - } - } -#endregion - -#region OnRenderFrame - /// - /// Profile Render Loop - /// - /// There is no need to call the base implementation. - public void Draw() - { - if (!_visible || !_initComplete) - { - return; - } - - // Update viewport - if (_viewportUpdated) - { - GL.Viewport(0, 0, Width, Height); - - GL.MatrixMode(MatrixMode.Projection); - GL.LoadIdentity(); - GL.Ortho(0, Width, 0, Height, 0.0, 4.0); - - _fontService.UpdateScreenHeight(Height); - - _viewportUpdated = false; - _redrawPending = true; - } - - if (!_redrawPending) - { - return; - } - - // Frame setup - GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); - GL.ClearColor(Color.Black); - - _fontService.fontColor = Color.White; - int verticalIndex = 0; - - float width; - float maxWidth = 0; - float yOffset = _scrollPos - TitleHeight; - float xOffset = 10; - float timingDataLeft; - float timingWidth; - - // Background lines to make reading easier - #region Background Lines - GL.Enable(EnableCap.ScissorTest); - GL.Scissor(0, BottomBarHeight, Width, Height - TitleHeight - BottomBarHeight); - GL.Begin(PrimitiveType.Triangles); - GL.Color3(0.2f, 0.2f, 0.2f); - for (int i = 0; i < _sortedProfileData.Count; i += 2) - { - float top = GetLineY(yOffset, LineHeight, LinePadding, false, i - 1); - float bottom = GetLineY(yOffset, LineHeight, LinePadding, false, i); - - // Skip rendering out of bounds bars - if (top < 0 || bottom > Height) - continue; - - GL.Vertex2(0, bottom); - GL.Vertex2(0, top); - GL.Vertex2(Width, top); - - GL.Vertex2(Width, top); - GL.Vertex2(Width, bottom); - GL.Vertex2(0, bottom); - } - GL.End(); - _maxScroll = (LineHeight + LinePadding) * (_sortedProfileData.Count - 1); -#endregion - - lock (_profileDataLock) - { -// Display category -#region Category - verticalIndex = 0; - foreach (var entry in _sortedProfileData) - { - if (entry.Key.Category == null) - { - verticalIndex++; - continue; - } - - float y = GetLineY(yOffset, LineHeight, LinePadding, true, verticalIndex++); - width = _fontService.DrawText(entry.Key.Category, xOffset, y, LineHeight); - - if (width > maxWidth) - { - maxWidth = width; - } - } - GL.Disable(EnableCap.ScissorTest); - - width = _fontService.DrawText("Category", xOffset, Height - TitleFontHeight, TitleFontHeight); - if (width > maxWidth) - maxWidth = width; - - xOffset += maxWidth + ColumnSpacing; -#endregion - -// Display session group -#region Session Group - maxWidth = 0; - verticalIndex = 0; - - GL.Enable(EnableCap.ScissorTest); - foreach (var entry in _sortedProfileData) - { - if (entry.Key.SessionGroup == null) - { - verticalIndex++; - continue; - } - - float y = GetLineY(yOffset, LineHeight, LinePadding, true, verticalIndex++); - width = _fontService.DrawText(entry.Key.SessionGroup, xOffset, y, LineHeight); - - if (width > maxWidth) - { - maxWidth = width; - } - } - GL.Disable(EnableCap.ScissorTest); - - width = _fontService.DrawText("Group", xOffset, Height - TitleFontHeight, TitleFontHeight); - if (width > maxWidth) - maxWidth = width; - - xOffset += maxWidth + ColumnSpacing; -#endregion - -// Display session item -#region Session Item - maxWidth = 0; - verticalIndex = 0; - GL.Enable(EnableCap.ScissorTest); - foreach (var entry in _sortedProfileData) - { - if (entry.Key.SessionItem == null) - { - verticalIndex++; - continue; - } - - float y = GetLineY(yOffset, LineHeight, LinePadding, true, verticalIndex++); - width = _fontService.DrawText(entry.Key.SessionItem, xOffset, y, LineHeight); - - if (width > maxWidth) - { - maxWidth = width; - } - } - GL.Disable(EnableCap.ScissorTest); - - width = _fontService.DrawText("Item", xOffset, Height - TitleFontHeight, TitleFontHeight); - if (width > maxWidth) - maxWidth = width; - - xOffset += maxWidth + ColumnSpacing; - _buttons[(int)ButtonIndex.TagTitle].UpdateSize(0, Height - TitleFontHeight, 0, (int)xOffset, TitleFontHeight); -#endregion - - // Timing data - timingWidth = Width - xOffset - 370; - timingDataLeft = xOffset; - - GL.Scissor((int)xOffset, BottomBarHeight, (int)timingWidth, Height - TitleHeight - BottomBarHeight); - - if (_displayGraph) - { - DrawGraph(xOffset, yOffset, timingWidth); - } - else - { - DrawBars(xOffset, yOffset, timingWidth); - } - - GL.Scissor(0, BottomBarHeight, Width, Height - TitleHeight - BottomBarHeight); - - if (!_displayGraph) - { - _fontService.DrawText("Blue: Instant, Green: Avg, Red: Total", xOffset, Height - TitleFontHeight, TitleFontHeight); - } - - xOffset = Width - 360; - -// Display timestamps -#region Timestamps - verticalIndex = 0; - long totalInstant = 0; - long totalAverage = 0; - long totalTime = 0; - long totalCount = 0; - - GL.Enable(EnableCap.ScissorTest); - foreach (var entry in _sortedProfileData) - { - float y = GetLineY(yOffset, LineHeight, LinePadding, true, verticalIndex++); - - _fontService.DrawText($"{GetTimeString(entry.Value.Instant)} ({entry.Value.InstantCount})", xOffset, y, LineHeight); - - _fontService.DrawText(GetTimeString(entry.Value.AverageTime), 150 + xOffset, y, LineHeight); - - _fontService.DrawText(GetTimeString(entry.Value.TotalTime), 260 + xOffset, y, LineHeight); - - totalInstant += entry.Value.Instant; - totalAverage += entry.Value.AverageTime; - totalTime += entry.Value.TotalTime; - totalCount += entry.Value.InstantCount; - } - GL.Disable(EnableCap.ScissorTest); - - float yHeight = Height - TitleFontHeight; - - _fontService.DrawText("Instant (Count)", xOffset, yHeight, TitleFontHeight); - _buttons[(int)ButtonIndex.InstantTitle].UpdateSize((int)xOffset, (int)yHeight, 0, 130, TitleFontHeight); - - _fontService.DrawText("Average", 150 + xOffset, yHeight, TitleFontHeight); - _buttons[(int)ButtonIndex.AverageTitle].UpdateSize((int)(150 + xOffset), (int)yHeight, 0, 130, TitleFontHeight); - - _fontService.DrawText("Total (ms)", 260 + xOffset, yHeight, TitleFontHeight); - _buttons[(int)ButtonIndex.TotalTitle].UpdateSize((int)(260 + xOffset), (int)yHeight, 0, Width, TitleFontHeight); - - // Totals - yHeight = FilterHeight + 3; - int textHeight = LineHeight - 2; - - _fontService.fontColor = new Color(100, 100, 255, 255); - float tempWidth = _fontService.DrawText($"Host {GetTimeString(_timingFlagsLast[(int)TimingFlagType.SystemFrame])} " + - $"({GetTimeString(_timingFlagsAverages[(int)TimingFlagType.SystemFrame])})", 5, yHeight, textHeight); - - _fontService.fontColor = Color.Red; - _fontService.DrawText($"Game {GetTimeString(_timingFlagsLast[(int)TimingFlagType.FrameSwap])} " + - $"({GetTimeString(_timingFlagsAverages[(int)TimingFlagType.FrameSwap])})", 15 + tempWidth, yHeight, textHeight); - _fontService.fontColor = Color.White; - - - _fontService.DrawText($"{GetTimeString(totalInstant)} ({totalCount})", xOffset, yHeight, textHeight); - _fontService.DrawText(GetTimeString(totalAverage), 150 + xOffset, yHeight, textHeight); - _fontService.DrawText(GetTimeString(totalTime), 260 + xOffset, yHeight, textHeight); -#endregion - } - -#region Bottom bar - // Show/Hide Inactive - float widthShowHideButton = _buttons[(int)ButtonIndex.ShowHideInactive].UpdateSize($"{(_showInactive ? "Hide" : "Show")} Inactive", 5, 5, 4, 16); - - // Play/Pause - float widthPlayPauseButton = _buttons[(int)ButtonIndex.Pause].UpdateSize(_paused ? "Play" : "Pause", 15 + (int)widthShowHideButton, 5, 4, 16) + widthShowHideButton; - - // Step - float widthStepButton = widthPlayPauseButton; - - if (_paused) - { - widthStepButton += _buttons[(int)ButtonIndex.Step].UpdateSize("Step", (int)(25 + widthPlayPauseButton), 5, 4, 16) + 10; - _buttons[(int)ButtonIndex.Step].Draw(); - } - - // Change display - float widthChangeDisplay = _buttons[(int)ButtonIndex.ChangeDisplay].UpdateSize($"View: {(_displayGraph ? "Graph" : "Bars")}", 25 + (int)widthStepButton, 5, 4, 16) + widthStepButton; - - width = widthChangeDisplay; - - if (_displayGraph) - { - width += _buttons[(int) ButtonIndex.ToggleFlags].UpdateSize($"{(_displayFlags ? "Hide" : "Show")} Flags", 35 + (int)widthChangeDisplay, 5, 4, 16) + 10; - _buttons[(int)ButtonIndex.ToggleFlags].Draw(); - } - - // Filter bar - _fontService.DrawText($"{(_regexEnabled ? "Regex " : "Filter")}: {_filterText}", 35 + width, 7, 16); - _buttons[(int)ButtonIndex.FilterBar].UpdateSize((int)(45 + width), 0, 0, Width, FilterHeight); -#endregion - - // Draw buttons - for (int i = 0; i < (int)ButtonIndex.Autodraw; i++) - { - _buttons[i].Draw(); - } - -// Dividing lines -#region Dividing lines - GL.Color3(Color.White); - GL.Begin(PrimitiveType.Lines); - // Top divider - GL.Vertex2(0, Height -TitleHeight); - GL.Vertex2(Width, Height - TitleHeight); - - // Bottom divider - GL.Vertex2(0, FilterHeight); - GL.Vertex2(Width, FilterHeight); - - GL.Vertex2(0, BottomBarHeight); - GL.Vertex2(Width, BottomBarHeight); - - // Bottom vertical dividers - GL.Vertex2(widthShowHideButton + 10, 0); - GL.Vertex2(widthShowHideButton + 10, FilterHeight); - - GL.Vertex2(widthPlayPauseButton + 20, 0); - GL.Vertex2(widthPlayPauseButton + 20, FilterHeight); - - if (_paused) - { - GL.Vertex2(widthStepButton + 20, 0); - GL.Vertex2(widthStepButton + 20, FilterHeight); - } - - if (_displayGraph) - { - GL.Vertex2(widthChangeDisplay + 30, 0); - GL.Vertex2(widthChangeDisplay + 30, FilterHeight); - } - - GL.Vertex2(width + 30, 0); - GL.Vertex2(width + 30, FilterHeight); - - // Column dividers - float timingDataTop = Height - TitleHeight; - - GL.Vertex2(timingDataLeft, FilterHeight); - GL.Vertex2(timingDataLeft, timingDataTop); - - GL.Vertex2(timingWidth + timingDataLeft, FilterHeight); - GL.Vertex2(timingWidth + timingDataLeft, timingDataTop); - GL.End(); -#endregion - - _redrawPending = false; - SwapBuffers(); - } -#endregion - - private string GetTimeString(long timestamp) - { - float time = (float)timestamp / PerformanceCounter.TicksPerMillisecond; - return (time < 1) ? $"{time * 1000:F3}us" : $"{time:F3}ms"; - } - - private void FilterBackspace() - { - if (_filterText.Length <= 1) - { - _filterText = ""; - } - else - { - _filterText = _filterText.Remove(_filterText.Length - 1, 1); - } - } - - private float GetLineY(float offset, float lineHeight, float padding, bool centre, int line) - { - return Height + offset - lineHeight - padding - ((lineHeight + padding) * line) + ((centre) ? padding : 0); - } - - protected override void OnKeyPress(KeyPressEventArgs e) - { - _filterText += e.KeyChar; - _profileUpdated = true; - } - - protected override void OnKeyDown(KeyboardKeyEventArgs e) - { - switch (e.Key) - { - case Key.BackSpace: - _profileUpdated = _backspaceDown = true; - return; - - case Key.Left: - case Key.Right: - case Key.Up: - case Key.Down: - _graphControlKey = e.Key; - return; - } - base.OnKeyUp(e); - } - - protected override void OnKeyUp(KeyboardKeyEventArgs e) - { - // Can't go into switch as value isn't constant - if (e.Key == Profile.Controls.Buttons.ToggleProfiler) - { - ToggleVisible(); - return; - } - - switch (e.Key) - { - case Key.BackSpace: - _backspaceDown = false; - return; - - case Key.Left: - case Key.Right: - case Key.Up: - case Key.Down: - _graphControlKey = Key.F35; - return; - } - base.OnKeyUp(e); - } - - protected override void OnMouseUp(MouseButtonEventArgs e) - { - foreach (ProfileButton button in _buttons) - { - if (button.ProcessClick(e.X, Height - e.Y)) - return; - } - } - - protected override void OnMouseWheel(MouseWheelEventArgs e) - { - _scrollPos += e.Delta * -30; - if (_scrollPos < _minScroll) - _scrollPos = _minScroll; - if (_scrollPos > _maxScroll) - _scrollPos = _maxScroll; - - _redrawPending = true; - } - } -} \ No newline at end of file diff --git a/Ryujinx.Profiler/UI/ProfileWindowBars.cs b/Ryujinx.Profiler/UI/ProfileWindowBars.cs deleted file mode 100644 index ab5b4fd1..00000000 --- a/Ryujinx.Profiler/UI/ProfileWindowBars.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System; -using System.Collections.Generic; -using OpenTK; -using OpenTK.Graphics.OpenGL; - -namespace Ryujinx.Profiler.UI -{ - public partial class ProfileWindow - { - private void DrawBars(float xOffset, float yOffset, float width) - { - if (_sortedProfileData.Count != 0) - { - long maxAverage; - long maxTotal; - - int verticalIndex = 0; - float barHeight = (LineHeight - LinePadding) / 3.0f; - - // Get max values - long maxInstant = maxAverage = maxTotal = 0; - foreach (KeyValuePair kvp in _sortedProfileData) - { - maxInstant = Math.Max(maxInstant, kvp.Value.Instant); - maxAverage = Math.Max(maxAverage, kvp.Value.AverageTime); - maxTotal = Math.Max(maxTotal, kvp.Value.TotalTime); - } - - GL.Enable(EnableCap.ScissorTest); - GL.Begin(PrimitiveType.Triangles); - foreach (var entry in _sortedProfileData) - { - // Instant - GL.Color3(Color.Blue); - float bottom = GetLineY(yOffset, LineHeight, LinePadding, true, verticalIndex++); - float top = bottom + barHeight; - float right = (float)entry.Value.Instant / maxInstant * width + xOffset; - - // Skip rendering out of bounds bars - if (top < 0 || bottom > Height) - continue; - - GL.Vertex2(xOffset, bottom); - GL.Vertex2(xOffset, top); - GL.Vertex2(right, top); - - GL.Vertex2(right, top); - GL.Vertex2(right, bottom); - GL.Vertex2(xOffset, bottom); - - // Average - GL.Color3(Color.Green); - top += barHeight; - bottom += barHeight; - right = (float)entry.Value.AverageTime / maxAverage * width + xOffset; - - GL.Vertex2(xOffset, bottom); - GL.Vertex2(xOffset, top); - GL.Vertex2(right, top); - - GL.Vertex2(right, top); - GL.Vertex2(right, bottom); - GL.Vertex2(xOffset, bottom); - - // Total - GL.Color3(Color.Red); - top += barHeight; - bottom += barHeight; - right = (float)entry.Value.TotalTime / maxTotal * width + xOffset; - - GL.Vertex2(xOffset, bottom); - GL.Vertex2(xOffset, top); - GL.Vertex2(right, top); - - GL.Vertex2(right, top); - GL.Vertex2(right, bottom); - GL.Vertex2(xOffset, bottom); - } - - GL.End(); - GL.Disable(EnableCap.ScissorTest); - } - } - } -} diff --git a/Ryujinx.Profiler/UI/ProfileWindowGraph.cs b/Ryujinx.Profiler/UI/ProfileWindowGraph.cs deleted file mode 100644 index 6a4a52a9..00000000 --- a/Ryujinx.Profiler/UI/ProfileWindowGraph.cs +++ /dev/null @@ -1,151 +0,0 @@ -using System; -using OpenTK; -using OpenTK.Graphics.OpenGL; -using Ryujinx.Common; - -namespace Ryujinx.Profiler.UI -{ - public partial class ProfileWindow - { - // Color index equal to timing flag type as int - private Color[] _timingFlagColors = new[] - { - new Color(150, 25, 25, 50), // FrameSwap = 0 - new Color(25, 25, 150, 50), // SystemFrame = 1 - }; - - private TimingFlag[] _timingFlags; - - private const float GraphMoveSpeed = 40000; - private const float GraphZoomSpeed = 50; - - private float _graphZoom = 1; - private float _graphPosition = 0; - - private void DrawGraph(float xOffset, float yOffset, float width) - { - if (_sortedProfileData.Count != 0) - { - int left, right; - float top, bottom; - - int verticalIndex = 0; - float graphRight = xOffset + width; - float barHeight = (LineHeight - LinePadding); - long history = Profile.HistoryLength; - double timeWidthTicks = history / (double)_graphZoom; - long graphPositionTicks = (long)(_graphPosition * PerformanceCounter.TicksPerMillisecond); - long ticksPerPixel = (long)(timeWidthTicks / width); - - // Reset start point if out of bounds - if (timeWidthTicks + graphPositionTicks > history) - { - graphPositionTicks = history - (long)timeWidthTicks; - _graphPosition = (float)graphPositionTicks / PerformanceCounter.TicksPerMillisecond; - } - - graphPositionTicks = _captureTime - graphPositionTicks; - - GL.Enable(EnableCap.ScissorTest); - - // Draw timing flags - if (_displayFlags) - { - TimingFlagType prevType = TimingFlagType.Count; - - GL.Enable(EnableCap.Blend); - GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha); - - GL.Begin(PrimitiveType.Lines); - foreach (TimingFlag timingFlag in _timingFlags) - { - if (prevType != timingFlag.FlagType) - { - prevType = timingFlag.FlagType; - GL.Color4(_timingFlagColors[(int)prevType]); - } - - int x = (int)(graphRight - ((graphPositionTicks - timingFlag.Timestamp) / timeWidthTicks) * width); - GL.Vertex2(x, 0); - GL.Vertex2(x, Height); - } - GL.End(); - GL.Disable(EnableCap.Blend); - } - - // Draw bars - GL.Begin(PrimitiveType.Triangles); - foreach (var entry in _sortedProfileData) - { - long furthest = 0; - - bottom = GetLineY(yOffset, LineHeight, LinePadding, true, verticalIndex); - top = bottom + barHeight; - - // Skip rendering out of bounds bars - if (top < 0 || bottom > Height) - { - verticalIndex++; - continue; - } - - - GL.Color3(Color.Green); - foreach (Timestamp timestamp in entry.Value.GetAllTimestamps()) - { - // Skip drawing multiple timestamps on same pixel - if (timestamp.EndTime < furthest) - continue; - furthest = timestamp.EndTime + ticksPerPixel; - - left = (int)(graphRight - ((graphPositionTicks - timestamp.BeginTime) / timeWidthTicks) * width); - right = (int)(graphRight - ((graphPositionTicks - timestamp.EndTime) / timeWidthTicks) * width); - - // Make sure width is at least 1px - right = Math.Max(left + 1, right); - - GL.Vertex2(left, bottom); - GL.Vertex2(left, top); - GL.Vertex2(right, top); - - GL.Vertex2(right, top); - GL.Vertex2(right, bottom); - GL.Vertex2(left, bottom); - } - - // Currently capturing timestamp - GL.Color3(Color.Red); - long entryBegin = entry.Value.BeginTime; - if (entryBegin != -1) - { - left = (int)(graphRight - ((graphPositionTicks - entryBegin) / timeWidthTicks) * width); - - // Make sure width is at least 1px - left = Math.Min(left - 1, (int)graphRight); - - GL.Vertex2(left, bottom); - GL.Vertex2(left, top); - GL.Vertex2(graphRight, top); - - GL.Vertex2(graphRight, top); - GL.Vertex2(graphRight, bottom); - GL.Vertex2(left, bottom); - } - - verticalIndex++; - } - - GL.End(); - GL.Disable(EnableCap.ScissorTest); - - string label = $"-{MathF.Round(_graphPosition, 2)} ms"; - - // Dummy draw for measure - float labelWidth = _fontService.DrawText(label, 0, 0, LineHeight, false); - _fontService.DrawText(label, graphRight - labelWidth - LinePadding, FilterHeight + LinePadding, LineHeight); - - _fontService.DrawText($"-{MathF.Round((float)((timeWidthTicks / PerformanceCounter.TicksPerMillisecond) + _graphPosition), 2)} ms", xOffset + LinePadding, FilterHeight + LinePadding, LineHeight); - } - } - } -} diff --git a/Ryujinx.Profiler/UI/ProfileWindowManager.cs b/Ryujinx.Profiler/UI/ProfileWindowManager.cs deleted file mode 100644 index 13603029..00000000 --- a/Ryujinx.Profiler/UI/ProfileWindowManager.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System.Threading; -using OpenTK; -using OpenTK.Input; -using Ryujinx.Common; - -namespace Ryujinx.Profiler.UI -{ - public class ProfileWindowManager - { - private ProfileWindow _window; - private Thread _profileThread; - private Thread _renderThread; - private bool _profilerRunning; - - // Timing - private double _prevTime; - - public ProfileWindowManager() - { - if (Profile.ProfilingEnabled()) - { - _profilerRunning = true; - _prevTime = 0; - _profileThread = new Thread(ProfileLoop) - { - Name = "Profiler.ProfileThread" - }; - _profileThread.Start(); - } - } - - public void ToggleVisible() - { - if (Profile.ProfilingEnabled()) - { - _window.ToggleVisible(); - } - } - - public void Close() - { - if (_window != null) - { - _profilerRunning = false; - _window.Close(); - _window.Dispose(); - } - - _window = null; - } - - public void UpdateKeyInput(KeyboardState keyboard) - { - if (Profile.Controls.TogglePressed(keyboard)) - { - ToggleVisible(); - } - Profile.Controls.SetPrevKeyboardState(keyboard); - } - - private void ProfileLoop() - { - using (_window = new ProfileWindow()) - { - // Create thread for render loop - _renderThread = new Thread(RenderLoop) - { - Name = "Profiler.RenderThread" - }; - _renderThread.Start(); - - while (_profilerRunning) - { - double time = (double)PerformanceCounter.ElapsedTicks / PerformanceCounter.TicksPerSecond; - _window.Update(new FrameEventArgs(time - _prevTime)); - _prevTime = time; - - // Sleep to be less taxing, update usually does very little - Thread.Sleep(1); - } - } - } - - private void RenderLoop() - { - _window.Context.MakeCurrent(_window.WindowInfo); - - while (_profilerRunning) - { - _window.Draw(); - Thread.Sleep(1); - } - } - } -} diff --git a/Ryujinx.Profiler/UI/SharpFontHelpers/FontService.cs b/Ryujinx.Profiler/UI/SharpFontHelpers/FontService.cs deleted file mode 100644 index 32846977..00000000 --- a/Ryujinx.Profiler/UI/SharpFontHelpers/FontService.cs +++ /dev/null @@ -1,257 +0,0 @@ -using System; -using System.IO; -using System.Runtime.InteropServices; -using OpenTK; -using OpenTK.Graphics.OpenGL; -using SharpFont; - -namespace Ryujinx.Profiler.UI.SharpFontHelpers -{ - public class FontService - { - private struct CharacterInfo - { - public float Left; - public float Right; - public float Top; - public float Bottom; - - public int Width; - public float Height; - - public float AspectRatio; - - public float BearingX; - public float BearingY; - public float Advance; - } - - private const int SheetWidth = 1024; - private const int SheetHeight = 512; - private int ScreenWidth, ScreenHeight; - private int CharacterTextureSheet; - private CharacterInfo[] characters; - - public Color fontColor { get; set; } = Color.Black; - - private string GetFontPath() - { - string fontFolder = Environment.GetFolderPath(Environment.SpecialFolder.Fonts); - - // Only uses Arial, add more fonts here if wanted - string path = Path.Combine(fontFolder, "arial.ttf"); - if (File.Exists(path)) - { - return path; - } - - throw new Exception($"Profiler exception. Required font Courier New or Arial not installed to {fontFolder}"); - } - - public void InitializeTextures() - { - // Create and init some vars - uint[] rawCharacterSheet = new uint[SheetWidth * SheetHeight]; - int x; - int y; - int lineOffset; - int maxHeight; - - x = y = lineOffset = maxHeight = 0; - characters = new CharacterInfo[94]; - - // Get font - var font = new FontFace(File.OpenRead(GetFontPath())); - - // Update raw data for each character - for (int i = 0; i < 94; i++) - { - var surface = RenderSurface((char)(i + 33), font, out float xBearing, out float yBearing, out float advance); - - characters[i] = UpdateTexture(surface, ref rawCharacterSheet, ref x, ref y, ref lineOffset); - characters[i].BearingX = xBearing; - characters[i].BearingY = yBearing; - characters[i].Advance = advance; - - if (maxHeight < characters[i].Height) - maxHeight = (int)characters[i].Height; - } - - // Fix height for characters shorter than line height - for (int i = 0; i < 94; i++) - { - characters[i].BearingX /= characters[i].Width; - characters[i].BearingY /= maxHeight; - characters[i].Advance /= characters[i].Width; - characters[i].Height /= maxHeight; - characters[i].AspectRatio = (float)characters[i].Width / maxHeight; - } - - // Convert raw data into texture - CharacterTextureSheet = GL.GenTexture(); - GL.BindTexture(TextureTarget.Texture2D, CharacterTextureSheet); - - GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear); - GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear); - GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)TextureWrapMode.Clamp); - GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)TextureWrapMode.Clamp); - - GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba, SheetWidth, SheetHeight, 0, PixelFormat.Rgba, PixelType.UnsignedInt8888, rawCharacterSheet); - - GL.BindTexture(TextureTarget.Texture2D, 0); - } - - public void UpdateScreenHeight(int height) - { - ScreenHeight = height; - } - - public float DrawText(string text, float x, float y, float height, bool draw = true) - { - float originalX = x; - - // Skip out of bounds draw - if (y < height * -2 || y > ScreenHeight + height * 2) - { - draw = false; - } - - if (draw) - { - // Use font map texture - GL.BindTexture(TextureTarget.Texture2D, CharacterTextureSheet); - - // Enable blending and textures - GL.Enable(EnableCap.Texture2D); - GL.Enable(EnableCap.Blend); - GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha); - - // Draw all characters - GL.Begin(PrimitiveType.Triangles); - GL.Color4(fontColor); - } - - for (int i = 0; i < text.Length; i++) - { - if (text[i] == ' ') - { - x += height / 4; - continue; - } - - CharacterInfo charInfo = characters[text[i] - 33]; - float width = (charInfo.AspectRatio * height); - x += (charInfo.BearingX * charInfo.AspectRatio) * width; - float right = x + width; - if (draw) - { - DrawChar(charInfo, x, right, y + height * (charInfo.Height - charInfo.BearingY), y - height * charInfo.BearingY); - } - x = right + charInfo.Advance * charInfo.AspectRatio + 1; - } - - if (draw) - { - GL.End(); - - // Cleanup for caller - GL.BindTexture(TextureTarget.Texture2D, 0); - GL.Disable(EnableCap.Texture2D); - GL.Disable(EnableCap.Blend); - } - - // Return width of rendered text - return x - originalX; - } - - private void DrawChar(CharacterInfo charInfo, float left, float right, float top, float bottom) - { - GL.TexCoord2(charInfo.Left, charInfo.Bottom); GL.Vertex2(left, bottom); - GL.TexCoord2(charInfo.Left, charInfo.Top); GL.Vertex2(left, top); - GL.TexCoord2(charInfo.Right, charInfo.Top); GL.Vertex2(right, top); - - GL.TexCoord2(charInfo.Right, charInfo.Top); GL.Vertex2(right, top); - GL.TexCoord2(charInfo.Right, charInfo.Bottom); GL.Vertex2(right, bottom); - GL.TexCoord2(charInfo.Left, charInfo.Bottom); GL.Vertex2(left, bottom); - } - - public unsafe Surface RenderSurface(char c, FontFace font, out float xBearing, out float yBearing, out float advance) - { - var glyph = font.GetGlyph(c, 64); - xBearing = glyph.HorizontalMetrics.Bearing.X; - yBearing = glyph.RenderHeight - glyph.HorizontalMetrics.Bearing.Y; - advance = glyph.HorizontalMetrics.Advance; - - var surface = new Surface - { - Bits = Marshal.AllocHGlobal(glyph.RenderWidth * glyph.RenderHeight), - Width = glyph.RenderWidth, - Height = glyph.RenderHeight, - Pitch = glyph.RenderWidth - }; - - var stuff = (byte*)surface.Bits; - for (int i = 0; i < surface.Width * surface.Height; i++) - *stuff++ = 0; - - glyph.RenderTo(surface); - - return surface; - } - - private CharacterInfo UpdateTexture(Surface surface, ref uint[] rawCharMap, ref int posX, ref int posY, ref int lineOffset) - { - int width = surface.Width; - int height = surface.Height; - int len = width * height; - byte[] data = new byte[len]; - - // Get character bitmap - Marshal.Copy(surface.Bits, data, 0, len); - - // Find a slot - if (posX + width > SheetWidth) - { - posX = 0; - posY += lineOffset; - lineOffset = 0; - } - - // Update lineOffset - if (lineOffset < height) - { - lineOffset = height + 1; - } - - // Copy char to sheet - for (int y = 0; y < height; y++) - { - int destOffset = (y + posY) * SheetWidth + posX; - int sourceOffset = y * width; - - for (int x = 0; x < width; x++) - { - rawCharMap[destOffset + x] = (uint)((0xFFFFFF << 8) | data[sourceOffset + x]); - } - } - - // Generate character info - CharacterInfo charInfo = new CharacterInfo() - { - Left = (float)posX / SheetWidth, - Right = (float)(posX + width) / SheetWidth, - Top = (float)(posY - 1) / SheetHeight, - Bottom = (float)(posY + height) / SheetHeight, - Width = width, - Height = height, - }; - - // Update x - posX += width + 1; - - // Give the memory back - Marshal.FreeHGlobal(surface.Bits); - return charInfo; - } - } -} \ No newline at end of file diff --git a/Ryujinx.ShaderTools/Ryujinx.ShaderTools.csproj b/Ryujinx.ShaderTools/Ryujinx.ShaderTools.csproj index 2f9c7772..10d66cd2 100644 --- a/Ryujinx.ShaderTools/Ryujinx.ShaderTools.csproj +++ b/Ryujinx.ShaderTools/Ryujinx.ShaderTools.csproj @@ -12,12 +12,12 @@ - TRACE;USE_PROFILING + TRACE;USE_DEBUGGING true - TRACE;USE_PROFILING + TRACE;USE_DEBUGGING false diff --git a/Ryujinx.Tests.Unicorn/Ryujinx.Tests.Unicorn.csproj b/Ryujinx.Tests.Unicorn/Ryujinx.Tests.Unicorn.csproj index 36310f3d..f99f504a 100644 --- a/Ryujinx.Tests.Unicorn/Ryujinx.Tests.Unicorn.csproj +++ b/Ryujinx.Tests.Unicorn/Ryujinx.Tests.Unicorn.csproj @@ -12,12 +12,12 @@ - TRACE;USE_PROFILING + TRACE;USE_DEBUGGING true - TRACE;USE_PROFILING + TRACE;USE_DEBUGGING false diff --git a/Ryujinx.Tests/Ryujinx.Tests.csproj b/Ryujinx.Tests/Ryujinx.Tests.csproj index 83ec2e96..b256cc6c 100644 --- a/Ryujinx.Tests/Ryujinx.Tests.csproj +++ b/Ryujinx.Tests/Ryujinx.Tests.csproj @@ -17,12 +17,12 @@ - TRACE;USE_PROFILING + TRACE;USE_DEBUGGING true - TRACE;USE_PROFILING + TRACE;USE_DEBUGGING false diff --git a/Ryujinx.sln b/Ryujinx.sln index 4ad74077..f023368b 100644 --- a/Ryujinx.sln +++ b/Ryujinx.sln @@ -10,9 +10,6 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Tests.Unicorn", "Ryujinx.Tests.Unicorn\Ryujinx.Tests.Unicorn.csproj", "{D8F72938-78EF-4E8C-BAFE-531C9C3C8F15}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.HLE", "Ryujinx.HLE\Ryujinx.HLE.csproj", "{CB92CFF9-1D62-4D4F-9E88-8130EF61E351}" - ProjectSection(ProjectDependencies) = postProject - {4E69B67F-8CA7-42CF-A9E1-CCB0915DFB34} = {4E69B67F-8CA7-42CF-A9E1-CCB0915DFB34} - EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Audio", "Ryujinx.Audio\Ryujinx.Audio.csproj", "{5C1D818E-682A-46A5-9D54-30006E26C270}" EndProject @@ -22,8 +19,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Luea", "Ryujinx.LLE\Luea.cs EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Common", "Ryujinx.Common\Ryujinx.Common.csproj", "{5FD4E4F6-8928-4B3C-BE07-28A675C17226}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Profiler", "Ryujinx.Profiler\Ryujinx.Profiler.csproj", "{4E69B67F-8CA7-42CF-A9E1-CCB0915DFB34}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ARMeilleure", "ARMeilleure\ARMeilleure.csproj", "{ABF09A5E-2D8B-4B6F-A51D-5CE414DDB15A}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.Gpu", "Ryujinx.Graphics.Gpu\Ryujinx.Graphics.Gpu.csproj", "{ADA7EA87-0D63-4D97-9433-922A2124401F}" @@ -37,6 +32,7 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.Shader", "Ryujinx.Graphics.Shader\Ryujinx.Graphics.Shader.csproj", "{03B955CD-AD84-4B93-AAA7-BF17923BBAA5}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ryujinx.Graphics.Nvdec", "Ryujinx.Graphics.Nvdec\Ryujinx.Graphics.Nvdec.csproj", "{85A0FA56-DC01-4A42-8808-70DAC76BD66D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Debugger", "Ryujinx.Debugger\Ryujinx.Debugger.csproj", "{2E02B7F3-245E-43B1-AE5B-44167A0FDA20}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -110,14 +106,6 @@ Global {5FD4E4F6-8928-4B3C-BE07-28A675C17226}.Profile Release|Any CPU.Build.0 = Profile Release|Any CPU {5FD4E4F6-8928-4B3C-BE07-28A675C17226}.Release|Any CPU.ActiveCfg = Release|Any CPU {5FD4E4F6-8928-4B3C-BE07-28A675C17226}.Release|Any CPU.Build.0 = Release|Any CPU - {4E69B67F-8CA7-42CF-A9E1-CCB0915DFB34}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4E69B67F-8CA7-42CF-A9E1-CCB0915DFB34}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4E69B67F-8CA7-42CF-A9E1-CCB0915DFB34}.Profile Debug|Any CPU.ActiveCfg = Profile Debug|Any CPU - {4E69B67F-8CA7-42CF-A9E1-CCB0915DFB34}.Profile Debug|Any CPU.Build.0 = Profile Debug|Any CPU - {4E69B67F-8CA7-42CF-A9E1-CCB0915DFB34}.Profile Release|Any CPU.ActiveCfg = Profile Release|Any CPU - {4E69B67F-8CA7-42CF-A9E1-CCB0915DFB34}.Profile Release|Any CPU.Build.0 = Profile Release|Any CPU - {4E69B67F-8CA7-42CF-A9E1-CCB0915DFB34}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4E69B67F-8CA7-42CF-A9E1-CCB0915DFB34}.Release|Any CPU.Build.0 = Release|Any CPU {ABF09A5E-2D8B-4B6F-A51D-5CE414DDB15A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {ABF09A5E-2D8B-4B6F-A51D-5CE414DDB15A}.Debug|Any CPU.Build.0 = Debug|Any CPU {ABF09A5E-2D8B-4B6F-A51D-5CE414DDB15A}.Profile Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -174,6 +162,14 @@ Global {85A0FA56-DC01-4A42-8808-70DAC76BD66D}.Profile Release|Any CPU.Build.0 = Release|Any CPU {85A0FA56-DC01-4A42-8808-70DAC76BD66D}.Release|Any CPU.ActiveCfg = Release|Any CPU {85A0FA56-DC01-4A42-8808-70DAC76BD66D}.Release|Any CPU.Build.0 = Release|Any CPU + {2E02B7F3-245E-43B1-AE5B-44167A0FDA20}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2E02B7F3-245E-43B1-AE5B-44167A0FDA20}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2E02B7F3-245E-43B1-AE5B-44167A0FDA20}.Profile Debug|Any CPU.ActiveCfg = Profile Debug|Any CPU + {2E02B7F3-245E-43B1-AE5B-44167A0FDA20}.Profile Debug|Any CPU.Build.0 = Profile Debug|Any CPU + {2E02B7F3-245E-43B1-AE5B-44167A0FDA20}.Profile Release|Any CPU.ActiveCfg = Profile Release|Any CPU + {2E02B7F3-245E-43B1-AE5B-44167A0FDA20}.Profile Release|Any CPU.Build.0 = Profile Release|Any CPU + {2E02B7F3-245E-43B1-AE5B-44167A0FDA20}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2E02B7F3-245E-43B1-AE5B-44167A0FDA20}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Ryujinx/Program.cs b/Ryujinx/Program.cs index 9bab74a2..5ce33a9d 100644 --- a/Ryujinx/Program.cs +++ b/Ryujinx/Program.cs @@ -1,7 +1,7 @@ using Gtk; using Ryujinx.Common.Logging; using Ryujinx.Configuration; -using Ryujinx.Profiler; +using Ryujinx.Debugger.Profiler; using Ryujinx.Ui; using System; using System.IO; diff --git a/Ryujinx/Ryujinx.csproj b/Ryujinx/Ryujinx.csproj index 1ff98001..cc8e6d54 100644 --- a/Ryujinx/Ryujinx.csproj +++ b/Ryujinx/Ryujinx.csproj @@ -9,12 +9,12 @@ - TRACE;USE_PROFILING + TRACE;USE_DEBUGGING true - TRACE;USE_PROFILING + TRACE;USE_DEBUGGING false @@ -72,7 +72,7 @@ - + @@ -80,8 +80,8 @@ + - diff --git a/Ryujinx/Ui/GLScreen.cs b/Ryujinx/Ui/GLScreen.cs index 295cf22f..5e83458a 100644 --- a/Ryujinx/Ui/GLScreen.cs +++ b/Ryujinx/Ui/GLScreen.cs @@ -5,8 +5,6 @@ using Ryujinx.Configuration; using Ryujinx.Graphics.OpenGL; using Ryujinx.HLE; using Ryujinx.HLE.Input; -using Ryujinx.Profiler.UI; -using Ryujinx.Ui; using System; using System.Threading; @@ -41,10 +39,6 @@ namespace Ryujinx.Ui private string _newTitle; -#if USE_PROFILING - private ProfileWindowManager _profileWindow; -#endif - public GlScreen(Switch device) : base(1280, 720, new GraphicsMode(), "Ryujinx", 0, @@ -65,11 +59,6 @@ namespace Ryujinx.Ui Location = new Point( (DisplayDevice.Default.Width / 2) - (Width / 2), (DisplayDevice.Default.Height / 2) - (Height / 2)); - -#if USE_PROFILING - // Start profile window, it will handle itself from there - _profileWindow = new ProfileWindowManager(); -#endif } private void RenderLoop() @@ -171,11 +160,6 @@ namespace Ryujinx.Ui { KeyboardState keyboard = _keyboard.Value; -#if USE_PROFILING - // Profiler input, lets the profiler get access to the main windows keyboard state - _profileWindow.UpdateKeyInput(keyboard); -#endif - // Normal Input currentHotkeyButtons = KeyboardControls.GetHotkeyButtons(ConfigurationState.Instance.Hid.KeyboardControls, keyboard); currentButton = KeyboardControls.GetButtons(ConfigurationState.Instance.Hid.KeyboardControls, keyboard); @@ -330,10 +314,6 @@ namespace Ryujinx.Ui protected override void OnUnload(EventArgs e) { -#if USE_PROFILING - _profileWindow.Close(); -#endif - _renderThread.Join(); base.OnUnload(e); diff --git a/Ryujinx/Ui/MainWindow.cs b/Ryujinx/Ui/MainWindow.cs index 61c59d35..84c736be 100644 --- a/Ryujinx/Ui/MainWindow.cs +++ b/Ryujinx/Ui/MainWindow.cs @@ -3,11 +3,12 @@ using JsonPrettyPrinterPlus; using Ryujinx.Audio; using Ryujinx.Common.Logging; using Ryujinx.Configuration; +using Ryujinx.Debugger.Profiler; using Ryujinx.Graphics.GAL; using Ryujinx.Graphics.OpenGL; using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.FileSystem.Content; -using Ryujinx.Profiler; +using Ryujinx.HLE.FileSystem; using System; using System.Diagnostics; using System.IO; @@ -36,9 +37,12 @@ namespace Ryujinx.Ui private static bool _updatingGameTable; private static bool _gameLoaded; private static bool _ending; + private static bool _debuggerOpened; private static TreeView _treeView; + private static Debugger.Debugger _debugger; + #pragma warning disable CS0649 #pragma warning disable IDE0044 [GUI] Window _mainWin; @@ -61,6 +65,8 @@ namespace Ryujinx.Ui [GUI] Label _progressLabel; [GUI] Label _firmwareVersionLabel; [GUI] LevelBar _progressBar; + [GUI] MenuItem _openDebugger; + [GUI] MenuItem _toolsMenu; #pragma warning restore CS0649 #pragma warning restore IDE0044 @@ -118,6 +124,13 @@ namespace Ryujinx.Ui if (ConfigurationState.Instance.Ui.GuiColumns.FileSizeColumn) _fileSizeToggle.Active = true; if (ConfigurationState.Instance.Ui.GuiColumns.PathColumn) _pathToggle.Active = true; +#if USE_DEBUGGING + _debugger = new Debugger.Debugger(); + _openDebugger.Activated += _openDebugger_Opened; +#else + _openDebugger.Visible = false; +#endif + _gameTable.Model = _tableStore = new ListStore( typeof(bool), typeof(Gdk.Pixbuf), @@ -141,6 +154,36 @@ namespace Ryujinx.Ui Task.Run(RefreshFirmwareLabel); } +#if USE_DEBUGGING + private void _openDebugger_Opened(object sender, EventArgs e) + { + if (_debuggerOpened) + { + return; + } + + Window debugWindow = new Window("Debugger"); + + debugWindow.SetSizeRequest(1280, 640); + debugWindow.Child = _debugger.Widget; + debugWindow.DeleteEvent += DebugWindow_DeleteEvent; + debugWindow.ShowAll(); + + _debugger.Enable(); + + _debuggerOpened = true; + } + + private void DebugWindow_DeleteEvent(object o, DeleteEventArgs args) + { + _debuggerOpened = false; + + _debugger.Disable(); + + (_debugger.Widget.Parent as Window)?.Remove(_debugger.Widget); + } +#endif + internal static void ApplyTheme() { if (!ConfigurationState.Instance.Ui.EnableCustomTheme) @@ -307,7 +350,15 @@ namespace Ryujinx.Ui #if MACOS_BUILD CreateGameWindow(device); #else - new Thread(() => CreateGameWindow(device)).Start(); + var windowThread = new Thread(() => + { + CreateGameWindow(device); + }) + { + Name = "GUI.WindowThread" + }; + + windowThread.Start(); #endif _gameLoaded = true; @@ -366,6 +417,11 @@ namespace Ryujinx.Ui private void End(HLE.Switch device) { + +#if USE_DEBUGGING + _debugger.Dispose(); +#endif + if (_ending) { return; diff --git a/Ryujinx/Ui/MainWindow.glade b/Ryujinx/Ui/MainWindow.glade index 8e2eab93..d3cdc593 100644 --- a/Ryujinx/Ui/MainWindow.glade +++ b/Ryujinx/Ui/MainWindow.glade @@ -1,5 +1,5 @@ - + @@ -8,6 +8,9 @@ center 1280 750 + + + True @@ -255,7 +258,7 @@ - + True False Tools @@ -296,6 +299,14 @@ + + + True + False + Open Debugger + True + + @@ -499,8 +510,5 @@ - - - -- cgit v1.2.3