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