diff options
Diffstat (limited to 'Ryujinx.Debugger')
| -rw-r--r-- | Ryujinx.Debugger/Debugger.cs | 32 | ||||
| -rw-r--r-- | Ryujinx.Debugger/Profiler/DumpProfile.cs | 35 | ||||
| -rw-r--r-- | Ryujinx.Debugger/Profiler/InternalProfile.cs | 223 | ||||
| -rw-r--r-- | Ryujinx.Debugger/Profiler/Profile.cs | 141 | ||||
| -rw-r--r-- | Ryujinx.Debugger/Profiler/ProfileConfig.cs | 254 | ||||
| -rw-r--r-- | Ryujinx.Debugger/Profiler/ProfileSorters.cs | 32 | ||||
| -rw-r--r-- | Ryujinx.Debugger/Profiler/ProfilerConfiguration.cs | 68 | ||||
| -rw-r--r-- | Ryujinx.Debugger/Profiler/Settings.cs | 17 | ||||
| -rw-r--r-- | Ryujinx.Debugger/Profiler/TimingFlag.cs | 17 | ||||
| -rw-r--r-- | Ryujinx.Debugger/Profiler/TimingInfo.cs | 174 | ||||
| -rw-r--r-- | Ryujinx.Debugger/ProfilerConfig.jsonc | 28 | ||||
| -rw-r--r-- | Ryujinx.Debugger/Ryujinx.Debugger.csproj | 42 | ||||
| -rw-r--r-- | Ryujinx.Debugger/UI/DebuggerWidget.cs | 42 | ||||
| -rw-r--r-- | Ryujinx.Debugger/UI/DebuggerWidget.glade | 44 | ||||
| -rw-r--r-- | Ryujinx.Debugger/UI/ProfilerWidget.cs | 801 | ||||
| -rw-r--r-- | Ryujinx.Debugger/UI/ProfilerWidget.glade | 232 | ||||
| -rw-r--r-- | Ryujinx.Debugger/UI/SkRenderer.cs | 23 |
17 files changed, 2205 insertions, 0 deletions
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<ProfileConfig, TimingInfo> 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<ProfileConfig, TimingInfo> Timers { get; set; } + + private readonly object _timerQueueClearLock = new object(); + private ConcurrentQueue<TimerQueueValue> _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<TimingFlag> _timingFlagCallback; + + public InternalProfile(long history, int maxFlags) + { + _maxFlags = maxFlags; + Timers = new Dictionary<ProfileConfig, TimingInfo>(); + _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<TimerQueueValue>(); + _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<KeyValuePair<ProfileConfig, TimingInfo>> 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<TimingFlag> 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<TimingFlag> 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<KeyValuePair<ProfileConfig, TimingInfo>> GetProfilingData() + { +#if USE_DEBUGGING + if (!ProfilingEnabled()) + return new List<KeyValuePair<ProfileConfig, TimingInfo>>(); + return _profileInstance.GetProfilingData(); +#else + return new List<KeyValuePair<ProfileConfig, TimingInfo>>(); +#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<ProfileConfig> + { + 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; + } + } + + /// <summary> + /// The default comparison is far too slow for the number of comparisons needed because it doesn't know what's important to compare + /// </summary> + /// <param name="obj">Object to compare to</param> + /// <returns></returns> + 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); + } + } + + /// <summary> + /// Predefined configs to make profiling easier, + /// nested so you can reference as Profiles.Category.Group.Item where item and group may be optional + /// </summary> + 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<KeyValuePair<ProfileConfig, TimingInfo>> + { + public int Compare(KeyValuePair<ProfileConfig, TimingInfo> pair1, KeyValuePair<ProfileConfig, TimingInfo> pair2) + => pair2.Value.Instant.CompareTo(pair1.Value.Instant); + } + + public class AverageAscending : IComparer<KeyValuePair<ProfileConfig, TimingInfo>> + { + public int Compare(KeyValuePair<ProfileConfig, TimingInfo> pair1, KeyValuePair<ProfileConfig, TimingInfo> pair2) + => pair2.Value.AverageTime.CompareTo(pair1.Value.AverageTime); + } + + public class TotalAscending : IComparer<KeyValuePair<ProfileConfig, TimingInfo>> + { + public int Compare(KeyValuePair<ProfileConfig, TimingInfo> pair1, KeyValuePair<ProfileConfig, TimingInfo> pair2) + => pair2.Value.TotalTime.CompareTo(pair1.Value.TotalTime); + } + + public class TagAscending : IComparer<KeyValuePair<ProfileConfig, TimingInfo>> + { + public int Compare(KeyValuePair<ProfileConfig, TimingInfo> pair1, KeyValuePair<ProfileConfig, TimingInfo> 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; } + + /// <summary> + /// Loads a configuration file from disk + /// </summary> + /// <param name="path">The path to the JSON configuration file</param> + public static ProfilerConfiguration Load(string path) + { + var resolver = CompositeResolver.Create( + new[] { new ConfigurationEnumFormatter<Key>() }, + 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<ProfilerConfiguration>(stream, resolver); + } + } + + private class ConfigurationEnumFormatter<T> : IJsonFormatter<T> + where T : struct + { + public void Serialize(ref JsonWriter writer, T value, IJsonFormatterResolver formatterResolver) + { + formatterResolver.GetFormatterWithVerify<string>() + .Serialize(ref writer, value.ToString(), formatterResolver); + } + + public T Deserialize(ref JsonReader reader, IJsonFormatterResolver formatterResolver) + { + if (reader.ReadIsNull()) + { + return default(T); + } + + string enumName = formatterResolver.GetFormatterWithVerify<string>() + .Deserialize(ref reader, formatterResolver); + + if (Enum.TryParse<T>(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<Timestamp> _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<Timestamp>(); + _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 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>netcoreapp3.0</TargetFramework> + <Configurations>Debug;Release;Profile Release;Profile Debug</Configurations> + </PropertyGroup> + + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Debug|AnyCPU'"> + <DefineConstants>TRACE;USE_DEBUGGING</DefineConstants> + </PropertyGroup> + + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Release|AnyCPU'"> + <DefineConstants>TRACE;USE_DEBUGGING</DefineConstants> + </PropertyGroup> + + <ItemGroup> + <None Remove="UI\DebuggerWidget.glade" /> + <None Remove="UI\ProfilerWidget.glade" /> + </ItemGroup> + + <ItemGroup> + <EmbeddedResource Include="UI\DebuggerWidget.glade" /> + <EmbeddedResource Include="UI\ProfilerWidget.glade" /> + </ItemGroup> + + <ItemGroup> + <PackageReference Include="GtkSharp" Version="3.22.25.56" /> + <PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="1.68.1.1" /> + <PackageReference Include="SkiaSharp.Views.Gtk3" Version="1.68.1.1" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" /> + </ItemGroup> + + <ItemGroup> + <None Update="ProfilerConfig.jsonc"> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </None> + </ItemGroup> + +</Project> 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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.21.0 --> +<interface> + <requires lib="gtk+" version="3.20"/> + <object class="GtkBox" id="_debuggerBox"> + <property name="name">DebuggerBox</property> + <property name="width_request">1024</property> + <property name="height_request">720</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkNotebook" id="_widgetNotebook"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> + <child> + <placeholder/> + </child> + <child type="tab"> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child type="tab"> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child type="tab"> + <placeholder/> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + </object> +</interface> 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<KeyValuePair<ProfileConfig, TimingInfo>> _unsortedProfileData; + private IComparer<KeyValuePair<ProfileConfig, TimingInfo>> _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<KeyValuePair<ProfileConfig, TimingInfo>> _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<ProfileConfig, TimingInfo> 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<ProfileConfig, TimingInfo> 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<ProfileConfig, TimingInfo> 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<ProfileConfig, TimingInfo> 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<ProfileConfig, TimingInfo> 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<ProfileConfig, TimingInfo> 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<ProfileConfig, TimingInfo> 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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.21.0 --> +<interface> + <requires lib="gtk+" version="3.20"/> + <object class="GtkListStore" id="viewMode"> + <columns> + <!-- column-name mode --> + <column type="gint"/> + <!-- column-name label --> + <column type="gchararray"/> + </columns> + <data> + <row> + <col id="0">0</col> + <col id="1" translatable="yes">Graph</col> + </row> + <row> + <col id="0">1</col> + <col id="1" translatable="yes">Bars</col> + </row> + </data> + </object> + <object class="GtkBox" id="_profilerBox"> + <property name="name">ProfilerBox</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_left">5</property> + <property name="margin_right">5</property> + <property name="margin_top">5</property> + <property name="margin_bottom">5</property> + <property name="orientation">vertical</property> + <property name="spacing">10</property> + <child> + <object class="GtkCheckButton" id="_enableCheckbutton"> + <property name="label" translatable="yes">Enable Profiler</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkScrolledWindow" id="_scrollview"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="vscrollbar_policy">never</property> + <property name="shadow_type">in</property> + <child> + <placeholder/> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkScrollbar" id="_outputScrollbar"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">10</property> + <child> + <object class="GtkCheckButton" id="_showInactive"> + <property name="label" translatable="yes">Show Inactive</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="active">True</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="_showFlags"> + <property name="label" translatable="yes">Show Flags</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="active">True</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="_pauseCheckbutton"> + <property name="label" translatable="yes">Paused</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">View Mode: </property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkComboBox" id="_modeBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="model">viewMode</property> + <property name="active">0</property> + <child> + <object class="GtkCellRendererText" id="modeTextRenderer"/> + <attributes> + <attribute name="text">1</attribute> + </attributes> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">3</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Filter: </property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="_filterBox"> + <property name="visible">True</property> + <property name="can_focus">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">4</property> + </packing> + </child> + <child> + <object class="GtkButton" id="_stepButton"> + <property name="label" translatable="yes">Step</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">5</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> +</interface> 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); + } + } +} |
