diff --git a/ChocolArm64/ChocolArm64.csproj b/ChocolArm64/ChocolArm64.csproj index 0b4051b0..ea98003f 100644 --- a/ChocolArm64/ChocolArm64.csproj +++ b/ChocolArm64/ChocolArm64.csproj @@ -3,19 +3,36 @@ netcoreapp2.1 win10-x64;osx-x64;linux-x64 + Debug;Release;Profile Debug;Profile Release true + + true + TRACE;USE_PROFILING + false + + true + + true + TRACE;USE_PROFILING + true + + + + + + diff --git a/Ryujinx.Audio/Ryujinx.Audio.csproj b/Ryujinx.Audio/Ryujinx.Audio.csproj index 82d2a4d1..a6a34f40 100644 --- a/Ryujinx.Audio/Ryujinx.Audio.csproj +++ b/Ryujinx.Audio/Ryujinx.Audio.csproj @@ -3,16 +3,29 @@ netcoreapp2.1 win10-x64;osx-x64;linux-x64 + Debug;Release;Profile Debug;Profile Release true + + true + TRACE;USE_PROFILING + false + + true + + true + TRACE;USE_PROFILING + true + + diff --git a/Ryujinx.Common/Ryujinx.Common.csproj b/Ryujinx.Common/Ryujinx.Common.csproj index bba481e6..cf078db8 100644 --- a/Ryujinx.Common/Ryujinx.Common.csproj +++ b/Ryujinx.Common/Ryujinx.Common.csproj @@ -3,16 +3,29 @@ netcoreapp2.1 win10-x64;osx-x64;linux-x64 + Debug;Release;Profile Debug;Profile Release true + + true + TRACE;USE_PROFILING + false + + true + + true + TRACE;USE_PROFILING + true + + diff --git a/Ryujinx.Graphics/Ryujinx.Graphics.csproj b/Ryujinx.Graphics/Ryujinx.Graphics.csproj index a4324715..74000895 100644 --- a/Ryujinx.Graphics/Ryujinx.Graphics.csproj +++ b/Ryujinx.Graphics/Ryujinx.Graphics.csproj @@ -3,16 +3,29 @@ netcoreapp2.1 win10-x64;osx-x64;linux-x64 + Debug;Release;Profile Debug;Profile Release true + + true + TRACE;USE_PROFILING + false + + true + + true + TRACE;USE_PROFILING + true + + diff --git a/Ryujinx.HLE/HOS/Services/IpcService.cs b/Ryujinx.HLE/HOS/Services/IpcService.cs index 2a4a9319..b93c8422 100644 --- a/Ryujinx.HLE/HOS/Services/IpcService.cs +++ b/Ryujinx.HLE/HOS/Services/IpcService.cs @@ -6,6 +6,7 @@ using Ryujinx.HLE.HOS.Kernel.Ipc; using System; using System.Collections.Generic; using System.IO; +using Ryujinx.Profiler; namespace Ryujinx.HLE.HOS.Services { @@ -101,7 +102,13 @@ namespace Ryujinx.HLE.HOS.Services { Logger.PrintDebug(LogClass.KernelIpc, $"{service.GetType().Name}: {processRequest.Method.Name}"); + ProfileConfig profile = Profiles.ServiceCall; + profile.SessionGroup = service.GetType().Name; + profile.SessionItem = processRequest.Method.Name; + + Profile.Begin(profile); result = processRequest(context); + Profile.End(profile); } else { @@ -203,4 +210,4 @@ namespace Ryujinx.HLE.HOS.Services return _domainObjects.GetData(id); } } -} \ No newline at end of file +} diff --git a/Ryujinx.HLE/PerformanceStatistics.cs b/Ryujinx.HLE/PerformanceStatistics.cs index 408e5d72..896ab67b 100644 --- a/Ryujinx.HLE/PerformanceStatistics.cs +++ b/Ryujinx.HLE/PerformanceStatistics.cs @@ -1,4 +1,5 @@ -using System.Diagnostics; +using Ryujinx.Profiler; +using System.Diagnostics; using System.Timers; namespace Ryujinx.HLE @@ -82,11 +83,13 @@ namespace Ryujinx.HLE public void RecordSystemFrameTime() { RecordFrameTime(FrameTypeSystem); + Profile.FlagTime(TimingFlagType.SystemFrame); } public void RecordGameFrameTime() { RecordFrameTime(FrameTypeGame); + Profile.FlagTime(TimingFlagType.FrameSwap); } private void RecordFrameTime(int frameType) diff --git a/Ryujinx.HLE/Ryujinx.HLE.csproj b/Ryujinx.HLE/Ryujinx.HLE.csproj index fd404863..a653b53f 100644 --- a/Ryujinx.HLE/Ryujinx.HLE.csproj +++ b/Ryujinx.HLE/Ryujinx.HLE.csproj @@ -3,16 +3,29 @@ netcoreapp2.1 win10-x64;osx-x64;linux-x64 + Debug;Release;Profile Debug;Profile Release true + + true + TRACE;USE_PROFILING + false + + true + + true + TRACE;USE_PROFILING + true + + @@ -28,6 +41,7 @@ + diff --git a/Ryujinx.LLE/Luea.csproj b/Ryujinx.LLE/Luea.csproj index 5c571568..719a0ef3 100644 --- a/Ryujinx.LLE/Luea.csproj +++ b/Ryujinx.LLE/Luea.csproj @@ -4,6 +4,17 @@ netcoreapp2.1 win10-x64;osx-x64;linux-x64 Exe + Debug;Release;Profile Debug;Profile Release + + + + TRACE;USE_PROFILING + true + + + + TRACE;USE_PROFILING + false diff --git a/Ryujinx.Profiler/DumpProfile.cs b/Ryujinx.Profiler/DumpProfile.cs new file mode 100644 index 00000000..62a02761 --- /dev/null +++ b/Ryujinx.Profiler/DumpProfile.cs @@ -0,0 +1,35 @@ +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 new file mode 100644 index 00000000..bd522b00 --- /dev/null +++ b/Ryujinx.Profiler/InternalProfile.cs @@ -0,0 +1,220 @@ +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); + _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 sucessfully cleared + if (queueCleared) + { + Thread.Sleep(5); + } + } + } + + private bool ClearTimerQueue() + { + int count = 0; + + while (_timerQueue.TryDequeue(out var item)) + { + if (!Timers.TryGetValue(item.Config, out var 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 RegisterFlagReciever(Action reciever) + { + _timingFlagCallback = reciever; + } + + public void Dispose() + { + _cleanupRunning = false; + _cleanupThread.Join(); + } + } +} diff --git a/Ryujinx.Profiler/Profile.cs b/Ryujinx.Profiler/Profile.cs new file mode 100644 index 00000000..fcd50c69 --- /dev/null +++ b/Ryujinx.Profiler/Profile.cs @@ -0,0 +1,143 @@ +using Ryujinx.Common; +using System; +using System.Collections.Generic; +using System.Diagnostics; + +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 Initalize() + { + var config = ProfilerConfiguration.Load("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 RegisterFlagReciever(Action reciever) + { + if (!ProfilingEnabled()) + return; + _profileInstance.RegisterFlagReciever(reciever); + } + + [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 new file mode 100644 index 00000000..6a2b2bc0 --- /dev/null +++ b/Ryujinx.Profiler/ProfileConfig.cs @@ -0,0 +1,113 @@ +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 ProfileConfig ServiceCall = new ProfileConfig() + { + Category = "ServiceCall", + }; + } +} diff --git a/Ryujinx.Profiler/ProfilerConfig.jsonc b/Ryujinx.Profiler/ProfilerConfig.jsonc new file mode 100644 index 00000000..e6714386 --- /dev/null +++ b/Ryujinx.Profiler/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.Profiler/ProfilerConfiguration.cs b/Ryujinx.Profiler/ProfilerConfiguration.cs new file mode 100644 index 00000000..b4d629e4 --- /dev/null +++ b/Ryujinx.Profiler/ProfilerConfiguration.cs @@ -0,0 +1,73 @@ +using OpenTK.Input; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading.Tasks; +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); + } + + var 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 new file mode 100644 index 00000000..e1075c8d --- /dev/null +++ b/Ryujinx.Profiler/ProfilerKeyboardHandler.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Text; +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 new file mode 100644 index 00000000..5a4c8f4f --- /dev/null +++ b/Ryujinx.Profiler/Ryujinx.Profiler.csproj @@ -0,0 +1,39 @@ + + + + netcoreapp2.1 + win10-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 new file mode 100644 index 00000000..c0393545 --- /dev/null +++ b/Ryujinx.Profiler/Settings.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Text; + +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 new file mode 100644 index 00000000..7d7c715f --- /dev/null +++ b/Ryujinx.Profiler/TimingFlag.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Text; + +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 new file mode 100644 index 00000000..e444e423 --- /dev/null +++ b/Ryujinx.Profiler/TimingInfo.cs @@ -0,0 +1,174 @@ +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); + } + + var 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 new file mode 100644 index 00000000..7e2ae728 --- /dev/null +++ b/Ryujinx.Profiler/UI/ProfileButton.cs @@ -0,0 +1,110 @@ +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 new file mode 100644 index 00000000..2d06f426 --- /dev/null +++ b/Ryujinx.Profiler/UI/ProfileSorters.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Text; + +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 new file mode 100644 index 00000000..c58b9235 --- /dev/null +++ b/Ryujinx.Profiler/UI/ProfileWindow.cs @@ -0,0 +1,773 @@ +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() + // Graphigs 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.RegisterFlagReciever((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.InitalizeTextures(); + _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 new file mode 100644 index 00000000..b1955a07 --- /dev/null +++ b/Ryujinx.Profiler/UI/ProfileWindowBars.cs @@ -0,0 +1,85 @@ +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 + var 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 new file mode 100644 index 00000000..9d34be97 --- /dev/null +++ b/Ryujinx.Profiler/UI/ProfileWindowGraph.cs @@ -0,0 +1,151 @@ +using System; +using OpenTK; +using OpenTK.Graphics.OpenGL; +using Ryujinx.Common; + +namespace Ryujinx.Profiler.UI +{ + public partial class ProfileWindow + { + // Colour index equal to timing flag type as int + private Color[] _timingFlagColours = 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(_timingFlagColours[(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 new file mode 100644 index 00000000..4ba0c881 --- /dev/null +++ b/Ryujinx.Profiler/UI/ProfileWindowManager.cs @@ -0,0 +1,90 @@ +using System.Diagnostics; +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); + _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); + _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 new file mode 100644 index 00000000..e64c9da3 --- /dev/null +++ b/Ryujinx.Profiler/UI/SharpFontHelpers/FontService.cs @@ -0,0 +1,257 @@ +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 = System.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 InitalizeTextures() + { + // 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 var xBearing, out var yBearing, out var 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 18452f0a..04cab832 100644 --- a/Ryujinx.ShaderTools/Ryujinx.ShaderTools.csproj +++ b/Ryujinx.ShaderTools/Ryujinx.ShaderTools.csproj @@ -4,6 +4,17 @@ netcoreapp2.1 win10-x64;osx-x64;linux-x64 Exe + Debug;Release;Profile Debug;Profile Release + + + + TRACE;USE_PROFILING + true + + + + TRACE;USE_PROFILING + false diff --git a/Ryujinx.Tests.Unicorn/Ryujinx.Tests.Unicorn.csproj b/Ryujinx.Tests.Unicorn/Ryujinx.Tests.Unicorn.csproj index ee7c103d..5a99b39f 100644 --- a/Ryujinx.Tests.Unicorn/Ryujinx.Tests.Unicorn.csproj +++ b/Ryujinx.Tests.Unicorn/Ryujinx.Tests.Unicorn.csproj @@ -4,12 +4,23 @@ netcoreapp2.1 win10-x64;osx-x64;linux-x64 true + Debug;Release;Profile Debug;Profile Release false + + TRACE;USE_PROFILING + true + + + + TRACE;USE_PROFILING + false + + diff --git a/Ryujinx.Tests/Ryujinx.Tests.csproj b/Ryujinx.Tests/Ryujinx.Tests.csproj index ce94326d..9ddeb314 100644 --- a/Ryujinx.Tests/Ryujinx.Tests.csproj +++ b/Ryujinx.Tests/Ryujinx.Tests.csproj @@ -9,12 +9,23 @@ windows osx linux + Debug;Release;Profile Debug;Profile Release false + + TRACE;USE_PROFILING + true + + + + TRACE;USE_PROFILING + false + + diff --git a/Ryujinx.sln b/Ryujinx.sln index 990a89a2..b928a06d 100644 --- a/Ryujinx.sln +++ b/Ryujinx.sln @@ -10,6 +10,9 @@ 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}") = "ChocolArm64", "ChocolArm64\ChocolArm64.csproj", "{2345A1A7-8DEF-419B-9AFB-4DFD41D20D05}" EndProject @@ -23,54 +26,106 @@ 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("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{464D8AB7-B056-4A99-B207-B8DCFB47AAA9}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Profile Debug|Any CPU = Profile Debug|Any CPU + Profile Release|Any CPU = Profile Release|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {074045D4-3ED2-4711-9169-E385F2BFB5A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {074045D4-3ED2-4711-9169-E385F2BFB5A0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {074045D4-3ED2-4711-9169-E385F2BFB5A0}.Profile Debug|Any CPU.ActiveCfg = Profile Debug|Any CPU + {074045D4-3ED2-4711-9169-E385F2BFB5A0}.Profile Debug|Any CPU.Build.0 = Profile Debug|Any CPU + {074045D4-3ED2-4711-9169-E385F2BFB5A0}.Profile Release|Any CPU.ActiveCfg = Profile Release|Any CPU + {074045D4-3ED2-4711-9169-E385F2BFB5A0}.Profile Release|Any CPU.Build.0 = Profile Release|Any CPU {074045D4-3ED2-4711-9169-E385F2BFB5A0}.Release|Any CPU.ActiveCfg = Release|Any CPU {074045D4-3ED2-4711-9169-E385F2BFB5A0}.Release|Any CPU.Build.0 = Release|Any CPU {EBB55AEA-C7D7-4DEB-BF96-FA1789E225E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EBB55AEA-C7D7-4DEB-BF96-FA1789E225E9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EBB55AEA-C7D7-4DEB-BF96-FA1789E225E9}.Profile Debug|Any CPU.ActiveCfg = Profile Debug|Any CPU + {EBB55AEA-C7D7-4DEB-BF96-FA1789E225E9}.Profile Debug|Any CPU.Build.0 = Profile Debug|Any CPU + {EBB55AEA-C7D7-4DEB-BF96-FA1789E225E9}.Profile Release|Any CPU.ActiveCfg = Profile Release|Any CPU + {EBB55AEA-C7D7-4DEB-BF96-FA1789E225E9}.Profile Release|Any CPU.Build.0 = Profile Release|Any CPU {EBB55AEA-C7D7-4DEB-BF96-FA1789E225E9}.Release|Any CPU.ActiveCfg = Release|Any CPU {EBB55AEA-C7D7-4DEB-BF96-FA1789E225E9}.Release|Any CPU.Build.0 = Release|Any CPU {D8F72938-78EF-4E8C-BAFE-531C9C3C8F15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D8F72938-78EF-4E8C-BAFE-531C9C3C8F15}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D8F72938-78EF-4E8C-BAFE-531C9C3C8F15}.Profile Debug|Any CPU.ActiveCfg = Profile Debug|Any CPU + {D8F72938-78EF-4E8C-BAFE-531C9C3C8F15}.Profile Debug|Any CPU.Build.0 = Profile Debug|Any CPU + {D8F72938-78EF-4E8C-BAFE-531C9C3C8F15}.Profile Release|Any CPU.ActiveCfg = Profile Release|Any CPU + {D8F72938-78EF-4E8C-BAFE-531C9C3C8F15}.Profile Release|Any CPU.Build.0 = Profile Release|Any CPU {D8F72938-78EF-4E8C-BAFE-531C9C3C8F15}.Release|Any CPU.ActiveCfg = Release|Any CPU {D8F72938-78EF-4E8C-BAFE-531C9C3C8F15}.Release|Any CPU.Build.0 = Release|Any CPU {CB92CFF9-1D62-4D4F-9E88-8130EF61E351}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CB92CFF9-1D62-4D4F-9E88-8130EF61E351}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CB92CFF9-1D62-4D4F-9E88-8130EF61E351}.Profile Debug|Any CPU.ActiveCfg = Profile Debug|Any CPU + {CB92CFF9-1D62-4D4F-9E88-8130EF61E351}.Profile Debug|Any CPU.Build.0 = Profile Debug|Any CPU + {CB92CFF9-1D62-4D4F-9E88-8130EF61E351}.Profile Release|Any CPU.ActiveCfg = Profile Release|Any CPU + {CB92CFF9-1D62-4D4F-9E88-8130EF61E351}.Profile Release|Any CPU.Build.0 = Profile Release|Any CPU {CB92CFF9-1D62-4D4F-9E88-8130EF61E351}.Release|Any CPU.ActiveCfg = Release|Any CPU {CB92CFF9-1D62-4D4F-9E88-8130EF61E351}.Release|Any CPU.Build.0 = Release|Any CPU {2345A1A7-8DEF-419B-9AFB-4DFD41D20D05}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2345A1A7-8DEF-419B-9AFB-4DFD41D20D05}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2345A1A7-8DEF-419B-9AFB-4DFD41D20D05}.Profile Debug|Any CPU.ActiveCfg = Profile Debug|Any CPU + {2345A1A7-8DEF-419B-9AFB-4DFD41D20D05}.Profile Debug|Any CPU.Build.0 = Profile Debug|Any CPU + {2345A1A7-8DEF-419B-9AFB-4DFD41D20D05}.Profile Release|Any CPU.ActiveCfg = Profile Release|Any CPU + {2345A1A7-8DEF-419B-9AFB-4DFD41D20D05}.Profile Release|Any CPU.Build.0 = Profile Release|Any CPU {2345A1A7-8DEF-419B-9AFB-4DFD41D20D05}.Release|Any CPU.ActiveCfg = Release|Any CPU {2345A1A7-8DEF-419B-9AFB-4DFD41D20D05}.Release|Any CPU.Build.0 = Release|Any CPU {EAAE36AF-7781-4578-A7E0-F0EFD2025569}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EAAE36AF-7781-4578-A7E0-F0EFD2025569}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EAAE36AF-7781-4578-A7E0-F0EFD2025569}.Profile Debug|Any CPU.ActiveCfg = Profile Debug|Any CPU + {EAAE36AF-7781-4578-A7E0-F0EFD2025569}.Profile Debug|Any CPU.Build.0 = Profile Debug|Any CPU + {EAAE36AF-7781-4578-A7E0-F0EFD2025569}.Profile Release|Any CPU.ActiveCfg = Profile Release|Any CPU + {EAAE36AF-7781-4578-A7E0-F0EFD2025569}.Profile Release|Any CPU.Build.0 = Profile Release|Any CPU {EAAE36AF-7781-4578-A7E0-F0EFD2025569}.Release|Any CPU.ActiveCfg = Release|Any CPU {EAAE36AF-7781-4578-A7E0-F0EFD2025569}.Release|Any CPU.Build.0 = Release|Any CPU {5C1D818E-682A-46A5-9D54-30006E26C270}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5C1D818E-682A-46A5-9D54-30006E26C270}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5C1D818E-682A-46A5-9D54-30006E26C270}.Profile Debug|Any CPU.ActiveCfg = Profile Debug|Any CPU + {5C1D818E-682A-46A5-9D54-30006E26C270}.Profile Debug|Any CPU.Build.0 = Profile Debug|Any CPU + {5C1D818E-682A-46A5-9D54-30006E26C270}.Profile Release|Any CPU.ActiveCfg = Profile Release|Any CPU + {5C1D818E-682A-46A5-9D54-30006E26C270}.Profile Release|Any CPU.Build.0 = Profile Release|Any CPU {5C1D818E-682A-46A5-9D54-30006E26C270}.Release|Any CPU.ActiveCfg = Release|Any CPU {5C1D818E-682A-46A5-9D54-30006E26C270}.Release|Any CPU.Build.0 = Release|Any CPU {3AB294D0-2230-468F-9EB3-BDFCAEAE99A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3AB294D0-2230-468F-9EB3-BDFCAEAE99A5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3AB294D0-2230-468F-9EB3-BDFCAEAE99A5}.Profile Debug|Any CPU.ActiveCfg = Profile Debug|Any CPU + {3AB294D0-2230-468F-9EB3-BDFCAEAE99A5}.Profile Debug|Any CPU.Build.0 = Profile Debug|Any CPU + {3AB294D0-2230-468F-9EB3-BDFCAEAE99A5}.Profile Release|Any CPU.ActiveCfg = Profile Release|Any CPU + {3AB294D0-2230-468F-9EB3-BDFCAEAE99A5}.Profile Release|Any CPU.Build.0 = Profile Release|Any CPU {3AB294D0-2230-468F-9EB3-BDFCAEAE99A5}.Release|Any CPU.ActiveCfg = Release|Any CPU {3AB294D0-2230-468F-9EB3-BDFCAEAE99A5}.Release|Any CPU.Build.0 = Release|Any CPU {8E7D36DD-9626-47E2-8EF5-8F2F66751C9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8E7D36DD-9626-47E2-8EF5-8F2F66751C9C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8E7D36DD-9626-47E2-8EF5-8F2F66751C9C}.Profile Debug|Any CPU.ActiveCfg = Profile Debug|Any CPU + {8E7D36DD-9626-47E2-8EF5-8F2F66751C9C}.Profile Debug|Any CPU.Build.0 = Profile Debug|Any CPU + {8E7D36DD-9626-47E2-8EF5-8F2F66751C9C}.Profile Release|Any CPU.ActiveCfg = Profile Release|Any CPU + {8E7D36DD-9626-47E2-8EF5-8F2F66751C9C}.Profile Release|Any CPU.Build.0 = Profile Release|Any CPU {8E7D36DD-9626-47E2-8EF5-8F2F66751C9C}.Release|Any CPU.ActiveCfg = Release|Any CPU {8E7D36DD-9626-47E2-8EF5-8F2F66751C9C}.Release|Any CPU.Build.0 = Release|Any CPU {5FD4E4F6-8928-4B3C-BE07-28A675C17226}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5FD4E4F6-8928-4B3C-BE07-28A675C17226}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5FD4E4F6-8928-4B3C-BE07-28A675C17226}.Profile Debug|Any CPU.ActiveCfg = Profile Debug|Any CPU + {5FD4E4F6-8928-4B3C-BE07-28A675C17226}.Profile Debug|Any CPU.Build.0 = Profile Debug|Any CPU + {5FD4E4F6-8928-4B3C-BE07-28A675C17226}.Profile Release|Any CPU.ActiveCfg = Profile Release|Any CPU + {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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Ryujinx/Program.cs b/Ryujinx/Program.cs index 42a6a741..a72cd39e 100644 --- a/Ryujinx/Program.cs +++ b/Ryujinx/Program.cs @@ -3,6 +3,7 @@ using Ryujinx.Common.Logging; using Ryujinx.Graphics.Gal; using Ryujinx.Graphics.Gal.OpenGL; using Ryujinx.HLE; +using Ryujinx.Profiler; using System; using System.IO; @@ -25,6 +26,8 @@ namespace Ryujinx Configuration.Load(Path.Combine(ApplicationDirectory, "Config.jsonc")); Configuration.Configure(device); + Profile.Initalize(); + AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; AppDomain.CurrentDomain.ProcessExit += CurrentDomain_ProcessExit; @@ -89,6 +92,8 @@ namespace Ryujinx { screen.MainLoop(); + Profile.FinishProfiling(); + device.Dispose(); } diff --git a/Ryujinx/Ryujinx.csproj b/Ryujinx/Ryujinx.csproj index 08725846..ab0ee599 100644 --- a/Ryujinx/Ryujinx.csproj +++ b/Ryujinx/Ryujinx.csproj @@ -5,6 +5,17 @@ win10-x64;osx-x64;linux-x64 Exe true + Debug;Release;Profile Debug;Profile Release + + + + TRACE;USE_PROFILING + true + + + + TRACE;USE_PROFILING + false @@ -17,6 +28,7 @@ + diff --git a/Ryujinx/Ui/GLScreen.cs b/Ryujinx/Ui/GLScreen.cs index d9661237..c4fe65ab 100644 --- a/Ryujinx/Ui/GLScreen.cs +++ b/Ryujinx/Ui/GLScreen.cs @@ -4,6 +4,8 @@ using OpenTK.Input; using Ryujinx.Graphics.Gal; using Ryujinx.HLE; using Ryujinx.HLE.Input; +using Ryujinx.Profiler; +using Ryujinx.Profiler.UI; using System; using System.Threading; @@ -36,6 +38,10 @@ namespace Ryujinx private string _newTitle; +#if USE_PROFILING + private ProfileWindowManager _profileWindow; +#endif + public GlScreen(Switch device, IGalRenderer renderer) : base(1280, 720, new GraphicsMode(), "Ryujinx", 0, @@ -48,6 +54,11 @@ namespace Ryujinx 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() @@ -145,6 +156,12 @@ namespace Ryujinx { 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 = Configuration.Instance.KeyboardControls.GetHotkeyButtons(keyboard); currentButton = Configuration.Instance.KeyboardControls.GetButtons(keyboard); @@ -278,6 +295,10 @@ namespace Ryujinx protected override void OnUnload(EventArgs e) { +#if USE_PROFILING + _profileWindow.Close(); +#endif + _renderThread.Join(); base.OnUnload(e); @@ -336,4 +357,4 @@ namespace Ryujinx _mouse = e.Mouse; } } -} \ No newline at end of file +} diff --git a/appveyor.yml b/appveyor.yml index b29a9233..a1201aa6 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -3,26 +3,32 @@ branches: only: - master image: Visual Studio 2017 -configuration: Release +environment: + matrix: + - config: Release + config_name: '-' + + - config: Profile Release + config_name: '-profiled-' build_script: - ps: >- dotnet --version - dotnet publish -c Release -r win-x64 + dotnet publish -c $env:config -r win-x64 - dotnet publish -c Release -r linux-x64 + dotnet publish -c $env:config -r linux-x64 - dotnet publish -c Release -r osx-x64 + dotnet publish -c $env:config -r osx-x64 - 7z a ryujinx-$env:APPVEYOR_BUILD_VERSION-win_x64.zip $env:APPVEYOR_BUILD_FOLDER\Ryujinx\bin\Release\netcoreapp2.1\win-x64\publish\ + 7z a ryujinx$env:config_name$env:APPVEYOR_BUILD_VERSION-win_x64.zip $env:APPVEYOR_BUILD_FOLDER\Ryujinx\bin\$env:config\netcoreapp2.1\win-x64\publish\ - 7z a ryujinx-$env:APPVEYOR_BUILD_VERSION-linux_x64.tar $env:APPVEYOR_BUILD_FOLDER\Ryujinx\bin\Release\netcoreapp2.1\linux-x64\publish\ + 7z a ryujinx$env:config_name$env:APPVEYOR_BUILD_VERSION-linux_x64.tar $env:APPVEYOR_BUILD_FOLDER\Ryujinx\bin\$env:config\netcoreapp2.1\linux-x64\publish\ - 7z a ryujinx-$env:APPVEYOR_BUILD_VERSION-linux_x64.tar.gz ryujinx-$env:APPVEYOR_BUILD_VERSION-linux_x64.tar + 7z a ryujinx$env:config_name$env:APPVEYOR_BUILD_VERSION-linux_x64.tar.gz ryujinx$env:config_name$env:APPVEYOR_BUILD_VERSION-linux_x64.tar - 7z a ryujinx-$env:APPVEYOR_BUILD_VERSION-osx_x64.zip $env:APPVEYOR_BUILD_FOLDER\Ryujinx\bin\Release\netcoreapp2.1\osx-x64\publish\ + 7z a ryujinx$env:config_name$env:APPVEYOR_BUILD_VERSION-osx_x64.zip $env:APPVEYOR_BUILD_FOLDER\Ryujinx\bin\$env:config\netcoreapp2.1\osx-x64\publish\ artifacts: -- path: ryujinx-%APPVEYOR_BUILD_VERSION%-win_x64.zip -- path: ryujinx-%APPVEYOR_BUILD_VERSION%-linux_x64.tar.gz -- path: ryujinx-%APPVEYOR_BUILD_VERSION%-osx_x64.zip +- path: ryujinx%config_name%%APPVEYOR_BUILD_VERSION%-win_x64.zip +- path: ryujinx%config_name%%APPVEYOR_BUILD_VERSION%-linux_x64.tar.gz +- path: ryujinx%config_name%%APPVEYOR_BUILD_VERSION%-osx_x64.zip