Render Profiler in GUI (#854)

* move profiler output to gui

* addressed commits, rebased

* removed whitespaces
This commit is contained in:
emmauss 2020-02-06 11:25:47 +00:00 committed by GitHub
parent db9f8f999f
commit f2b9a9c2b0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 1358 additions and 1639 deletions

View file

@ -0,0 +1,32 @@
using System;
using Ryujinx.Debugger.UI;
namespace Ryujinx.Debugger
{
public class Debugger : IDisposable
{
public DebuggerWidget Widget { get; set; }
public Debugger()
{
Widget = new DebuggerWidget();
}
public void Enable()
{
Widget.Enable();
}
public void Disable()
{
Widget.Disable();
}
public void Dispose()
{
Disable();
Widget.Dispose();
}
}
}

View file

@ -0,0 +1,35 @@
using Ryujinx.Common;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace Ryujinx.Debugger.Profiler
{
public static class DumpProfile
{
public static void ToFile(string path, InternalProfile profile)
{
String fileData = "Category,Session Group,Session Item,Count,Average(ms),Total(ms)\r\n";
foreach (KeyValuePair<ProfileConfig, TimingInfo> time in profile.Timers.OrderBy(key => key.Key.Tag))
{
fileData += $"{time.Key.Category}," +
$"{time.Key.SessionGroup}," +
$"{time.Key.SessionItem}," +
$"{time.Value.Count}," +
$"{time.Value.AverageTime / PerformanceCounter.TicksPerMillisecond}," +
$"{time.Value.TotalTime / PerformanceCounter.TicksPerMillisecond}\r\n";
}
// Ensure file directory exists before write
FileInfo fileInfo = new FileInfo(path);
if (fileInfo == null)
throw new Exception("Unknown logging error, probably a bad file path");
if (fileInfo.Directory != null && !fileInfo.Directory.Exists)
Directory.CreateDirectory(fileInfo.Directory.FullName);
File.WriteAllText(fileInfo.FullName, fileData);
}
}
}

View file

@ -0,0 +1,223 @@
using Ryujinx.Common;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Ryujinx.Debugger.Profiler
{
public class InternalProfile
{
private struct TimerQueueValue
{
public ProfileConfig Config;
public long Time;
public bool IsBegin;
}
internal Dictionary<ProfileConfig, TimingInfo> Timers { get; set; }
private readonly object _timerQueueClearLock = new object();
private ConcurrentQueue<TimerQueueValue> _timerQueue;
private int _sessionCounter = 0;
// Cleanup thread
private readonly Thread _cleanupThread;
private bool _cleanupRunning;
private readonly long _history;
private long _preserve;
// Timing flags
private TimingFlag[] _timingFlags;
private long[] _timingFlagAverages;
private long[] _timingFlagLast;
private long[] _timingFlagLastDelta;
private int _timingFlagCount;
private int _timingFlagIndex;
private int _maxFlags;
private Action<TimingFlag> _timingFlagCallback;
public InternalProfile(long history, int maxFlags)
{
_maxFlags = maxFlags;
Timers = new Dictionary<ProfileConfig, TimingInfo>();
_timingFlags = new TimingFlag[_maxFlags];
_timingFlagAverages = new long[(int)TimingFlagType.Count];
_timingFlagLast = new long[(int)TimingFlagType.Count];
_timingFlagLastDelta = new long[(int)TimingFlagType.Count];
_timerQueue = new ConcurrentQueue<TimerQueueValue>();
_history = history;
_cleanupRunning = true;
// Create cleanup thread.
_cleanupThread = new Thread(CleanupLoop)
{
Name = "Profiler.CleanupThread"
};
_cleanupThread.Start();
}
private void CleanupLoop()
{
bool queueCleared = false;
while (_cleanupRunning)
{
// Ensure we only ever have 1 instance modifying timers or timerQueue
if (Monitor.TryEnter(_timerQueueClearLock))
{
queueCleared = ClearTimerQueue();
// Calculate before foreach to mitigate redundant calculations
long cleanupBefore = PerformanceCounter.ElapsedTicks - _history;
long preserveStart = _preserve - _history;
// Each cleanup is self contained so run in parallel for maximum efficiency
Parallel.ForEach(Timers, (t) => t.Value.Cleanup(cleanupBefore, preserveStart, _preserve));
Monitor.Exit(_timerQueueClearLock);
}
// Only sleep if queue was successfully cleared
if (queueCleared)
{
Thread.Sleep(5);
}
}
}
private bool ClearTimerQueue()
{
int count = 0;
while (_timerQueue.TryDequeue(out TimerQueueValue item))
{
if (!Timers.TryGetValue(item.Config, out TimingInfo value))
{
value = new TimingInfo();
Timers.Add(item.Config, value);
}
if (item.IsBegin)
{
value.Begin(item.Time);
}
else
{
value.End(item.Time);
}
// Don't block for too long as memory disposal is blocked while this function runs
if (count++ > 10000)
{
return false;
}
}
return true;
}
public void FlagTime(TimingFlagType flagType)
{
int flagId = (int)flagType;
_timingFlags[_timingFlagIndex] = new TimingFlag()
{
FlagType = flagType,
Timestamp = PerformanceCounter.ElapsedTicks
};
_timingFlagCount = Math.Max(_timingFlagCount + 1, _maxFlags);
// Work out average
if (_timingFlagLast[flagId] != 0)
{
_timingFlagLastDelta[flagId] = _timingFlags[_timingFlagIndex].Timestamp - _timingFlagLast[flagId];
_timingFlagAverages[flagId] = (_timingFlagAverages[flagId] == 0) ? _timingFlagLastDelta[flagId] :
(_timingFlagLastDelta[flagId] + _timingFlagAverages[flagId]) >> 1;
}
_timingFlagLast[flagId] = _timingFlags[_timingFlagIndex].Timestamp;
// Notify subscribers
_timingFlagCallback?.Invoke(_timingFlags[_timingFlagIndex]);
if (++_timingFlagIndex >= _maxFlags)
{
_timingFlagIndex = 0;
}
}
public void BeginProfile(ProfileConfig config)
{
_timerQueue.Enqueue(new TimerQueueValue()
{
Config = config,
IsBegin = true,
Time = PerformanceCounter.ElapsedTicks,
});
}
public void EndProfile(ProfileConfig config)
{
_timerQueue.Enqueue(new TimerQueueValue()
{
Config = config,
IsBegin = false,
Time = PerformanceCounter.ElapsedTicks,
});
}
public string GetSession()
{
// Can be called from multiple threads so we need to ensure no duplicate sessions are generated
return Interlocked.Increment(ref _sessionCounter).ToString();
}
public List<KeyValuePair<ProfileConfig, TimingInfo>> GetProfilingData()
{
_preserve = PerformanceCounter.ElapsedTicks;
lock (_timerQueueClearLock)
{
ClearTimerQueue();
return Timers.ToList();
}
}
public TimingFlag[] GetTimingFlags()
{
int count = Math.Max(_timingFlagCount, _maxFlags);
TimingFlag[] outFlags = new TimingFlag[count];
for (int i = 0, sourceIndex = _timingFlagIndex; i < count; i++, sourceIndex++)
{
if (sourceIndex >= _maxFlags)
sourceIndex = 0;
outFlags[i] = _timingFlags[sourceIndex];
}
return outFlags;
}
public (long[], long[]) GetTimingAveragesAndLast()
{
return (_timingFlagAverages, _timingFlagLastDelta);
}
public void RegisterFlagReceiver(Action<TimingFlag> receiver)
{
_timingFlagCallback = receiver;
}
public void Dispose()
{
_cleanupRunning = false;
_cleanupThread.Join();
}
}
}

View file

@ -0,0 +1,141 @@
using Ryujinx.Common;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
namespace Ryujinx.Debugger.Profiler
{
public static class Profile
{
public static float UpdateRate => _settings.UpdateRate;
public static long HistoryLength => _settings.History;
private static InternalProfile _profileInstance;
private static ProfilerSettings _settings;
[Conditional("USE_DEBUGGING")]
public static void Initialize()
{
var config = ProfilerConfiguration.Load(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ProfilerConfig.jsonc"));
_settings = new ProfilerSettings()
{
Enabled = config.Enabled,
FileDumpEnabled = config.DumpPath != "",
DumpLocation = config.DumpPath,
UpdateRate = (config.UpdateRate <= 0) ? -1 : 1.0f / config.UpdateRate,
History = (long)(config.History * PerformanceCounter.TicksPerSecond),
MaxLevel = config.MaxLevel,
MaxFlags = config.MaxFlags,
};
}
public static bool ProfilingEnabled()
{
#if USE_DEBUGGING
if (!_settings.Enabled)
return false;
if (_profileInstance == null)
_profileInstance = new InternalProfile(_settings.History, _settings.MaxFlags);
return true;
#else
return false;
#endif
}
[Conditional("USE_DEBUGGING")]
public static void FinishProfiling()
{
if (!ProfilingEnabled())
return;
if (_settings.FileDumpEnabled)
DumpProfile.ToFile(_settings.DumpLocation, _profileInstance);
_profileInstance.Dispose();
}
[Conditional("USE_DEBUGGING")]
public static void FlagTime(TimingFlagType flagType)
{
if (!ProfilingEnabled())
return;
_profileInstance.FlagTime(flagType);
}
[Conditional("USE_DEBUGGING")]
public static void RegisterFlagReceiver(Action<TimingFlag> receiver)
{
if (!ProfilingEnabled())
return;
_profileInstance.RegisterFlagReceiver(receiver);
}
[Conditional("USE_DEBUGGING")]
public static void Begin(ProfileConfig config)
{
if (!ProfilingEnabled())
return;
if (config.Level > _settings.MaxLevel)
return;
_profileInstance.BeginProfile(config);
}
[Conditional("USE_DEBUGGING")]
public static void End(ProfileConfig config)
{
if (!ProfilingEnabled())
return;
if (config.Level > _settings.MaxLevel)
return;
_profileInstance.EndProfile(config);
}
public static string GetSession()
{
#if USE_DEBUGGING
if (!ProfilingEnabled())
return null;
return _profileInstance.GetSession();
#else
return "";
#endif
}
public static List<KeyValuePair<ProfileConfig, TimingInfo>> GetProfilingData()
{
#if USE_DEBUGGING
if (!ProfilingEnabled())
return new List<KeyValuePair<ProfileConfig, TimingInfo>>();
return _profileInstance.GetProfilingData();
#else
return new List<KeyValuePair<ProfileConfig, TimingInfo>>();
#endif
}
public static TimingFlag[] GetTimingFlags()
{
#if USE_DEBUGGING
if (!ProfilingEnabled())
return new TimingFlag[0];
return _profileInstance.GetTimingFlags();
#else
return new TimingFlag[0];
#endif
}
public static (long[], long[]) GetTimingAveragesAndLast()
{
#if USE_DEBUGGING
if (!ProfilingEnabled())
return (new long[0], new long[0]);
return _profileInstance.GetTimingAveragesAndLast();
#else
return (new long[0], new long[0]);
#endif
}
}
}

View file

@ -0,0 +1,254 @@
using System;
namespace Ryujinx.Debugger.Profiler
{
public struct ProfileConfig : IEquatable<ProfileConfig>
{
public string Category;
public string SessionGroup;
public string SessionItem;
public int Level;
// Private cached variables
private string _cachedTag;
private string _cachedSession;
private string _cachedSearch;
// Public helpers to get config in more user friendly format,
// Cached because they never change and are called often
public string Search
{
get
{
if (_cachedSearch == null)
{
_cachedSearch = $"{Category}.{SessionGroup}.{SessionItem}";
}
return _cachedSearch;
}
}
public string Tag
{
get
{
if (_cachedTag == null)
_cachedTag = $"{Category}{(Session == "" ? "" : $" ({Session})")}";
return _cachedTag;
}
}
public string Session
{
get
{
if (_cachedSession == null)
{
if (SessionGroup != null && SessionItem != null)
{
_cachedSession = $"{SessionGroup}: {SessionItem}";
}
else if (SessionGroup != null)
{
_cachedSession = $"{SessionGroup}";
}
else if (SessionItem != null)
{
_cachedSession = $"---: {SessionItem}";
}
else
{
_cachedSession = "";
}
}
return _cachedSession;
}
}
/// <summary>
/// The default comparison is far too slow for the number of comparisons needed because it doesn't know what's important to compare
/// </summary>
/// <param name="obj">Object to compare to</param>
/// <returns></returns>
public bool Equals(ProfileConfig cmpObj)
{
// Order here is important.
// Multiple entries with the same item is considerable less likely that multiple items with the same group.
// Likewise for group and category.
return (cmpObj.SessionItem == SessionItem &&
cmpObj.SessionGroup == SessionGroup &&
cmpObj.Category == Category);
}
}
/// <summary>
/// Predefined configs to make profiling easier,
/// nested so you can reference as Profiles.Category.Group.Item where item and group may be optional
/// </summary>
public static class Profiles
{
public static class CPU
{
public static ProfileConfig TranslateTier0 = new ProfileConfig()
{
Category = "CPU",
SessionGroup = "TranslateTier0"
};
public static ProfileConfig TranslateTier1 = new ProfileConfig()
{
Category = "CPU",
SessionGroup = "TranslateTier1"
};
}
public static class Input
{
public static ProfileConfig ControllerInput = new ProfileConfig
{
Category = "Input",
SessionGroup = "ControllerInput"
};
public static ProfileConfig TouchInput = new ProfileConfig
{
Category = "Input",
SessionGroup = "TouchInput"
};
}
public static class GPU
{
public static class Engine2d
{
public static ProfileConfig TextureCopy = new ProfileConfig()
{
Category = "GPU.Engine2D",
SessionGroup = "TextureCopy"
};
}
public static class Engine3d
{
public static ProfileConfig CallMethod = new ProfileConfig()
{
Category = "GPU.Engine3D",
SessionGroup = "CallMethod",
};
public static ProfileConfig VertexEnd = new ProfileConfig()
{
Category = "GPU.Engine3D",
SessionGroup = "VertexEnd"
};
public static ProfileConfig ClearBuffers = new ProfileConfig()
{
Category = "GPU.Engine3D",
SessionGroup = "ClearBuffers"
};
public static ProfileConfig SetFrameBuffer = new ProfileConfig()
{
Category = "GPU.Engine3D",
SessionGroup = "SetFrameBuffer",
};
public static ProfileConfig SetZeta = new ProfileConfig()
{
Category = "GPU.Engine3D",
SessionGroup = "SetZeta"
};
public static ProfileConfig UploadShaders = new ProfileConfig()
{
Category = "GPU.Engine3D",
SessionGroup = "UploadShaders"
};
public static ProfileConfig UploadTextures = new ProfileConfig()
{
Category = "GPU.Engine3D",
SessionGroup = "UploadTextures"
};
public static ProfileConfig UploadTexture = new ProfileConfig()
{
Category = "GPU.Engine3D",
SessionGroup = "UploadTexture"
};
public static ProfileConfig UploadConstBuffers = new ProfileConfig()
{
Category = "GPU.Engine3D",
SessionGroup = "UploadConstBuffers"
};
public static ProfileConfig UploadVertexArrays = new ProfileConfig()
{
Category = "GPU.Engine3D",
SessionGroup = "UploadVertexArrays"
};
public static ProfileConfig ConfigureState = new ProfileConfig()
{
Category = "GPU.Engine3D",
SessionGroup = "ConfigureState"
};
}
public static class EngineM2mf
{
public static ProfileConfig CallMethod = new ProfileConfig()
{
Category = "GPU.EngineM2mf",
SessionGroup = "CallMethod",
};
public static ProfileConfig Execute = new ProfileConfig()
{
Category = "GPU.EngineM2mf",
SessionGroup = "Execute",
};
}
public static class EngineP2mf
{
public static ProfileConfig CallMethod = new ProfileConfig()
{
Category = "GPU.EngineP2mf",
SessionGroup = "CallMethod",
};
public static ProfileConfig Execute = new ProfileConfig()
{
Category = "GPU.EngineP2mf",
SessionGroup = "Execute",
};
public static ProfileConfig PushData = new ProfileConfig()
{
Category = "GPU.EngineP2mf",
SessionGroup = "PushData",
};
}
public static class Shader
{
public static ProfileConfig Decompile = new ProfileConfig()
{
Category = "GPU.Shader",
SessionGroup = "Decompile",
};
}
}
public static ProfileConfig ServiceCall = new ProfileConfig()
{
Category = "ServiceCall",
};
}
}

View file

@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
namespace Ryujinx.Debugger.Profiler
{
public static class ProfileSorters
{
public class InstantAscending : IComparer<KeyValuePair<ProfileConfig, TimingInfo>>
{
public int Compare(KeyValuePair<ProfileConfig, TimingInfo> pair1, KeyValuePair<ProfileConfig, TimingInfo> pair2)
=> pair2.Value.Instant.CompareTo(pair1.Value.Instant);
}
public class AverageAscending : IComparer<KeyValuePair<ProfileConfig, TimingInfo>>
{
public int Compare(KeyValuePair<ProfileConfig, TimingInfo> pair1, KeyValuePair<ProfileConfig, TimingInfo> pair2)
=> pair2.Value.AverageTime.CompareTo(pair1.Value.AverageTime);
}
public class TotalAscending : IComparer<KeyValuePair<ProfileConfig, TimingInfo>>
{
public int Compare(KeyValuePair<ProfileConfig, TimingInfo> pair1, KeyValuePair<ProfileConfig, TimingInfo> pair2)
=> pair2.Value.TotalTime.CompareTo(pair1.Value.TotalTime);
}
public class TagAscending : IComparer<KeyValuePair<ProfileConfig, TimingInfo>>
{
public int Compare(KeyValuePair<ProfileConfig, TimingInfo> pair1, KeyValuePair<ProfileConfig, TimingInfo> pair2)
=> StringComparer.CurrentCulture.Compare(pair1.Key.Search, pair2.Key.Search);
}
}
}

View file

@ -0,0 +1,68 @@
using Gdk;
using System;
using System.IO;
using Utf8Json;
using Utf8Json.Resolvers;
namespace Ryujinx.Debugger.Profiler
{
public class ProfilerConfiguration
{
public bool Enabled { get; private set; }
public string DumpPath { get; private set; }
public float UpdateRate { get; private set; }
public int MaxLevel { get; private set; }
public int MaxFlags { get; private set; }
public float History { get; private set; }
/// <summary>
/// Loads a configuration file from disk
/// </summary>
/// <param name="path">The path to the JSON configuration file</param>
public static ProfilerConfiguration Load(string path)
{
var resolver = CompositeResolver.Create(
new[] { new ConfigurationEnumFormatter<Key>() },
new[] { StandardResolver.AllowPrivateSnakeCase }
);
if (!File.Exists(path))
{
throw new FileNotFoundException($"Profiler configuration file {path} not found");
}
using (Stream stream = File.OpenRead(path))
{
return JsonSerializer.Deserialize<ProfilerConfiguration>(stream, resolver);
}
}
private class ConfigurationEnumFormatter<T> : IJsonFormatter<T>
where T : struct
{
public void Serialize(ref JsonWriter writer, T value, IJsonFormatterResolver formatterResolver)
{
formatterResolver.GetFormatterWithVerify<string>()
.Serialize(ref writer, value.ToString(), formatterResolver);
}
public T Deserialize(ref JsonReader reader, IJsonFormatterResolver formatterResolver)
{
if (reader.ReadIsNull())
{
return default(T);
}
string enumName = formatterResolver.GetFormatterWithVerify<string>()
.Deserialize(ref reader, formatterResolver);
if (Enum.TryParse<T>(enumName, out T result))
{
return result;
}
return default(T);
}
}
}
}

View file

@ -0,0 +1,17 @@
namespace Ryujinx.Debugger.Profiler
{
public class ProfilerSettings
{
// Default settings for profiler
public bool Enabled { get; set; } = false;
public bool FileDumpEnabled { get; set; } = false;
public string DumpLocation { get; set; } = "";
public float UpdateRate { get; set; } = 0.1f;
public int MaxLevel { get; set; } = 0;
public int MaxFlags { get; set; } = 1000;
// 19531225 = 5 seconds in ticks on most pc's.
// It should get set on boot to the time specified in config
public long History { get; set; } = 19531225;
}
}

View file

@ -0,0 +1,17 @@
namespace Ryujinx.Debugger.Profiler
{
public enum TimingFlagType
{
FrameSwap = 0,
SystemFrame = 1,
// Update this for new flags
Count = 2,
}
public struct TimingFlag
{
public TimingFlagType FlagType;
public long Timestamp;
}
}

View file

@ -0,0 +1,174 @@
using System;
using System.Collections.Generic;
namespace Ryujinx.Debugger.Profiler
{
public struct Timestamp
{
public long BeginTime;
public long EndTime;
}
public class TimingInfo
{
// Timestamps
public long TotalTime { get; set; }
public long Instant { get; set; }
// Measurement counts
public int Count { get; set; }
public int InstantCount { get; set; }
// Work out average
public long AverageTime => (Count == 0) ? -1 : TotalTime / Count;
// Intentionally not locked as it's only a get count
public bool IsActive => _timestamps.Count > 0;
public long BeginTime
{
get
{
lock (_timestampLock)
{
if (_depth > 0)
{
return _currentTimestamp.BeginTime;
}
return -1;
}
}
}
// Timestamp collection
private List<Timestamp> _timestamps;
private readonly object _timestampLock = new object();
private readonly object _timestampListLock = new object();
private Timestamp _currentTimestamp;
// Depth of current timer,
// each begin call increments and each end call decrements
private int _depth;
public TimingInfo()
{
_timestamps = new List<Timestamp>();
_depth = 0;
}
public void Begin(long beginTime)
{
lock (_timestampLock)
{
// Finish current timestamp if already running
if (_depth > 0)
{
EndUnsafe(beginTime);
}
BeginUnsafe(beginTime);
_depth++;
}
}
private void BeginUnsafe(long beginTime)
{
_currentTimestamp.BeginTime = beginTime;
_currentTimestamp.EndTime = -1;
}
public void End(long endTime)
{
lock (_timestampLock)
{
_depth--;
if (_depth < 0)
{
throw new Exception("Timing info end called without corresponding begin");
}
EndUnsafe(endTime);
// Still have others using this timing info so recreate start for them
if (_depth > 0)
{
BeginUnsafe(endTime);
}
}
}
private void EndUnsafe(long endTime)
{
_currentTimestamp.EndTime = endTime;
lock (_timestampListLock)
{
_timestamps.Add(_currentTimestamp);
}
long delta = _currentTimestamp.EndTime - _currentTimestamp.BeginTime;
TotalTime += delta;
Instant += delta;
Count++;
InstantCount++;
}
// Remove any timestamps before given timestamp to free memory
public void Cleanup(long before, long preserveStart, long preserveEnd)
{
lock (_timestampListLock)
{
int toRemove = 0;
int toPreserveStart = 0;
int toPreserveLen = 0;
for (int i = 0; i < _timestamps.Count; i++)
{
if (_timestamps[i].EndTime < preserveStart)
{
toPreserveStart++;
InstantCount--;
Instant -= _timestamps[i].EndTime - _timestamps[i].BeginTime;
}
else if (_timestamps[i].EndTime < preserveEnd)
{
toPreserveLen++;
}
else if (_timestamps[i].EndTime < before)
{
toRemove++;
InstantCount--;
Instant -= _timestamps[i].EndTime - _timestamps[i].BeginTime;
}
else
{
// Assume timestamps are in chronological order so no more need to be removed
break;
}
}
if (toPreserveStart > 0)
{
_timestamps.RemoveRange(0, toPreserveStart);
}
if (toRemove > 0)
{
_timestamps.RemoveRange(toPreserveLen, toRemove);
}
}
}
public Timestamp[] GetAllTimestamps()
{
lock (_timestampListLock)
{
Timestamp[] returnTimestamps = new Timestamp[_timestamps.Count];
_timestamps.CopyTo(returnTimestamps);
return returnTimestamps;
}
}
}
}

View file

@ -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"
}
}
}

View file

@ -0,0 +1,42 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
<Configurations>Debug;Release;Profile Release;Profile Debug</Configurations>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Debug|AnyCPU'">
<DefineConstants>TRACE;USE_DEBUGGING</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Release|AnyCPU'">
<DefineConstants>TRACE;USE_DEBUGGING</DefineConstants>
</PropertyGroup>
<ItemGroup>
<None Remove="UI\DebuggerWidget.glade" />
<None Remove="UI\ProfilerWidget.glade" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="UI\DebuggerWidget.glade" />
<EmbeddedResource Include="UI\ProfilerWidget.glade" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="GtkSharp" Version="3.22.25.56" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="1.68.1.1" />
<PackageReference Include="SkiaSharp.Views.Gtk3" Version="1.68.1.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="ProfilerConfig.jsonc">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View file

@ -0,0 +1,42 @@
using Gtk;
using System;
using GUI = Gtk.Builder.ObjectAttribute;
namespace Ryujinx.Debugger.UI
{
public class DebuggerWidget : Box
{
public event EventHandler DebuggerEnabled;
public event EventHandler DebuggerDisabled;
[GUI] Notebook _widgetNotebook;
public DebuggerWidget() : this(new Builder("Ryujinx.Debugger.UI.DebuggerWidget.glade")) { }
public DebuggerWidget(Builder builder) : base(builder.GetObject("_debuggerBox").Handle)
{
builder.Autoconnect(this);
LoadProfiler();
}
public void LoadProfiler()
{
ProfilerWidget widget = new ProfilerWidget();
widget.RegisterParentDebugger(this);
_widgetNotebook.AppendPage(widget, new Label("Profiler"));
}
public void Enable()
{
DebuggerEnabled.Invoke(this, null);
}
public void Disable()
{
DebuggerDisabled.Invoke(this, null);
}
}
}

View file

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.21.0 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkBox" id="_debuggerBox">
<property name="name">DebuggerBox</property>
<property name="width_request">1024</property>
<property name="height_request">720</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkNotebook" id="_widgetNotebook">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<child>
<placeholder/>
</child>
<child type="tab">
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child type="tab">
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child type="tab">
<placeholder/>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
</interface>

View file

@ -0,0 +1,801 @@
using Gtk;
using Ryujinx.Common;
using Ryujinx.Debugger.Profiler;
using SkiaSharp;
using SkiaSharp.Views.Desktop;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using GUI = Gtk.Builder.ObjectAttribute;
namespace Ryujinx.Debugger.UI
{
public class ProfilerWidget : Box
{
private Thread _profilerThread;
private double _prevTime;
private bool _profilerRunning;
private TimingFlag[] _timingFlags;
private bool _initComplete = false;
private bool _redrawPending = true;
private bool _doStep = false;
// Layout
private const int LineHeight = 16;
private const int MinimumColumnWidth = 200;
private const int TitleHeight = 24;
private const int TitleFontHeight = 16;
private const int LinePadding = 2;
private const int ColumnSpacing = 15;
private const int FilterHeight = 24;
private const int BottomBarHeight = FilterHeight + LineHeight;
// Sorting
private List<KeyValuePair<ProfileConfig, TimingInfo>> _unsortedProfileData;
private IComparer<KeyValuePair<ProfileConfig, TimingInfo>> _sortAction = new ProfileSorters.TagAscending();
// Flag data
private long[] _timingFlagsAverages;
private long[] _timingFlagsLast;
// Filtering
private string _filterText = "";
private bool _regexEnabled = false;
// Scrolling
private float _scrollPos = 0;
// Profile data storage
private List<KeyValuePair<ProfileConfig, TimingInfo>> _sortedProfileData;
private long _captureTime;
// Graph
private SKColor[] _timingFlagColors = new[]
{
new SKColor(150, 25, 25, 50), // FrameSwap = 0
new SKColor(25, 25, 150, 50), // SystemFrame = 1
};
private const float GraphMoveSpeed = 40000;
private const float GraphZoomSpeed = 50;
private float _graphZoom = 1;
private float _graphPosition = 0;
private int _rendererHeight => _renderer.AllocatedHeight;
private int _rendererWidth => _renderer.AllocatedWidth;
// Event management
private long _lastOutputUpdate;
private long _lastOutputDraw;
private long _lastOutputUpdateDuration;
private long _lastOutputDrawDuration;
private double _lastFrameTimeMs;
private double _updateTimer;
private bool _profileUpdated = false;
private readonly object _profileDataLock = new object();
private SkRenderer _renderer;
[GUI] ScrolledWindow _scrollview;
[GUI] CheckButton _enableCheckbutton;
[GUI] Scrollbar _outputScrollbar;
[GUI] Entry _filterBox;
[GUI] ComboBox _modeBox;
[GUI] CheckButton _showFlags;
[GUI] CheckButton _showInactive;
[GUI] Button _stepButton;
[GUI] CheckButton _pauseCheckbutton;
public ProfilerWidget() : this(new Builder("Ryujinx.Debugger.UI.ProfilerWidget.glade")) { }
public ProfilerWidget(Builder builder) : base(builder.GetObject("_profilerBox").Handle)
{
builder.Autoconnect(this);
this.KeyPressEvent += ProfilerWidget_KeyPressEvent;
this.Expand = true;
_renderer = new SkRenderer();
_renderer.Expand = true;
_outputScrollbar.ValueChanged += _outputScrollbar_ValueChanged;
_renderer.DrawGraphs += _renderer_DrawGraphs;
_filterBox.Changed += _filterBox_Changed;
_stepButton.Clicked += _stepButton_Clicked;
_scrollview.Add(_renderer);
if (Profile.UpdateRate <= 0)
{
// Perform step regardless of flag type
Profile.RegisterFlagReceiver((t) =>
{
if (_pauseCheckbutton.Active)
{
_doStep = true;
}
});
}
}
private void _stepButton_Clicked(object sender, EventArgs e)
{
if (_pauseCheckbutton.Active)
{
_doStep = true;
}
_profileUpdated = true;
}
private void _filterBox_Changed(object sender, EventArgs e)
{
_filterText = _filterBox.Text;
_profileUpdated = true;
}
private void _outputScrollbar_ValueChanged(object sender, EventArgs e)
{
_scrollPos = -(float)Math.Max(0, _outputScrollbar.Value);
_profileUpdated = true;
}
private void _renderer_DrawGraphs(object sender, EventArgs e)
{
if (e is SKPaintSurfaceEventArgs se)
{
Draw(se.Surface.Canvas);
}
}
public void RegisterParentDebugger(DebuggerWidget debugger)
{
debugger.DebuggerEnabled += Debugger_DebuggerAttached;
debugger.DebuggerDisabled += Debugger_DebuggerDettached;
}
private void Debugger_DebuggerDettached(object sender, EventArgs e)
{
_profilerRunning = false;
if (_profilerThread != null)
{
_profilerThread.Join();
}
}
private void Debugger_DebuggerAttached(object sender, EventArgs e)
{
_profilerRunning = false;
if (_profilerThread != null)
{
_profilerThread.Join();
}
_profilerRunning = true;
_profilerThread = new Thread(UpdateLoop)
{
Name = "Profiler.UpdateThread"
};
_profilerThread.Start();
}
private void ProfilerWidget_KeyPressEvent(object o, Gtk.KeyPressEventArgs args)
{
switch (args.Event.Key)
{
case Gdk.Key.Left:
_graphPosition += (long)(GraphMoveSpeed * _lastFrameTimeMs);
break;
case Gdk.Key.Right:
_graphPosition = Math.Max(_graphPosition - (long)(GraphMoveSpeed * _lastFrameTimeMs), 0);
break;
case Gdk.Key.Up:
_graphZoom = MathF.Min(_graphZoom + (float)(GraphZoomSpeed * _lastFrameTimeMs), 100.0f);
break;
case Gdk.Key.Down:
_graphZoom = MathF.Max(_graphZoom - (float)(GraphZoomSpeed * _lastFrameTimeMs), 1f);
break;
}
_profileUpdated = true;
}
public void UpdateLoop()
{
_lastOutputUpdate = PerformanceCounter.ElapsedTicks;
_lastOutputDraw = PerformanceCounter.ElapsedTicks;
while (_profilerRunning)
{
_lastOutputUpdate = PerformanceCounter.ElapsedTicks;
int timeToSleepMs = (_pauseCheckbutton.Active || !_enableCheckbutton.Active) ? 33 : 1;
if (Profile.ProfilingEnabled() && _enableCheckbutton.Active)
{
double time = (double)PerformanceCounter.ElapsedTicks / PerformanceCounter.TicksPerSecond;
Update(time - _prevTime);
_lastOutputUpdateDuration = PerformanceCounter.ElapsedTicks - _lastOutputUpdate;
_prevTime = time;
Gdk.Threads.AddIdle(1000, ()=>
{
_renderer.QueueDraw();
return true;
});
}
Thread.Sleep(timeToSleepMs);
}
}
public void Update(double frameTime)
{
_lastFrameTimeMs = frameTime;
// Get timing data if enough time has passed
_updateTimer += frameTime;
if (_doStep || ((Profile.UpdateRate > 0) && (!_pauseCheckbutton.Active && (_updateTimer > Profile.UpdateRate))))
{
_updateTimer = 0;
_captureTime = PerformanceCounter.ElapsedTicks;
_timingFlags = Profile.GetTimingFlags();
_doStep = false;
_profileUpdated = true;
_unsortedProfileData = Profile.GetProfilingData();
(_timingFlagsAverages, _timingFlagsLast) = Profile.GetTimingAveragesAndLast();
}
// Filtering
if (_profileUpdated)
{
lock (_profileDataLock)
{
_sortedProfileData = _showInactive.Active ? _unsortedProfileData : _unsortedProfileData.FindAll(kvp => kvp.Value.IsActive);
if (_sortAction != null)
{
_sortedProfileData.Sort(_sortAction);
}
if (_regexEnabled)
{
try
{
Regex filterRegex = new Regex(_filterText, RegexOptions.IgnoreCase);
if (_filterText != "")
{
_sortedProfileData = _sortedProfileData.Where((pair => filterRegex.IsMatch(pair.Key.Search))).ToList();
}
}
catch (ArgumentException argException)
{
// Skip filtering for invalid regex
}
}
else
{
// Regular filtering
_sortedProfileData = _sortedProfileData.Where((pair => pair.Key.Search.ToLower().Contains(_filterText.ToLower()))).ToList();
}
}
_profileUpdated = false;
_redrawPending = true;
_initComplete = true;
}
}
private string GetTimeString(long timestamp)
{
float time = (float)timestamp / PerformanceCounter.TicksPerMillisecond;
return (time < 1) ? $"{time * 1000:F3}us" : $"{time:F3}ms";
}
private void FilterBackspace()
{
if (_filterText.Length <= 1)
{
_filterText = "";
}
else
{
_filterText = _filterText.Remove(_filterText.Length - 1, 1);
}
}
private float GetLineY(float offset, float lineHeight, float padding, bool centre, int line)
{
return offset + lineHeight + padding + ((lineHeight + padding) * line) - ((centre) ? padding : 0);
}
public void Draw(SKCanvas canvas)
{
_lastOutputDraw = PerformanceCounter.ElapsedTicks;
if (!Visible ||
!_initComplete ||
!_enableCheckbutton.Active ||
!_redrawPending)
{
return;
}
float viewTop = TitleHeight + 5;
float viewBottom = _rendererHeight - FilterHeight - LineHeight;
float columnWidth;
float maxColumnWidth = MinimumColumnWidth;
float yOffset = _scrollPos + viewTop;
float xOffset = 10;
float timingWidth;
float contentHeight = GetLineY(0, LineHeight, LinePadding, false, _sortedProfileData.Count - 1);
_outputScrollbar.Adjustment.Upper = contentHeight;
_outputScrollbar.Adjustment.Lower = 0;
_outputScrollbar.Adjustment.PageSize = viewBottom - viewTop;
SKPaint textFont = new SKPaint()
{
Color = SKColors.White,
TextSize = LineHeight
};
SKPaint titleFont = new SKPaint()
{
Color = SKColors.White,
TextSize = TitleFontHeight
};
SKPaint evenItemBackground = new SKPaint()
{
Color = SKColors.Gray
};
canvas.Save();
canvas.ClipRect(new SKRect(0, viewTop, _rendererWidth, viewBottom), SKClipOperation.Intersect);
for (int i = 1; i < _sortedProfileData.Count; i += 2)
{
float top = GetLineY(yOffset, LineHeight, LinePadding, false, i - 1);
float bottom = GetLineY(yOffset, LineHeight, LinePadding, false, i);
canvas.DrawRect(new SKRect(0, top, _rendererWidth, bottom), evenItemBackground);
}
lock (_profileDataLock)
{
// Display category
for (int verticalIndex = 0; verticalIndex < _sortedProfileData.Count; verticalIndex++)
{
KeyValuePair<ProfileConfig, TimingInfo> entry = _sortedProfileData[verticalIndex];
if (entry.Key.Category == null)
{
continue;
}
float y = GetLineY(yOffset, LineHeight, LinePadding, true, verticalIndex);
canvas.DrawText(entry.Key.Category, new SKPoint(xOffset, y), textFont);
columnWidth = textFont.MeasureText(entry.Key.Category);
if (columnWidth > maxColumnWidth)
{
maxColumnWidth = columnWidth;
}
}
canvas.Restore();
canvas.DrawText("Category", new SKPoint(xOffset, TitleFontHeight + 2), titleFont);
columnWidth = titleFont.MeasureText("Category");
if (columnWidth > maxColumnWidth)
{
maxColumnWidth = columnWidth;
}
xOffset += maxColumnWidth + ColumnSpacing;
canvas.DrawLine(new SKPoint(xOffset - ColumnSpacing / 2, 0), new SKPoint(xOffset - ColumnSpacing / 2, viewBottom), textFont);
// Display session group
maxColumnWidth = MinimumColumnWidth;
canvas.Save();
canvas.ClipRect(new SKRect(0, viewTop, _rendererWidth, viewBottom), SKClipOperation.Intersect);
for (int verticalIndex = 0; verticalIndex < _sortedProfileData.Count; verticalIndex++)
{
KeyValuePair<ProfileConfig, TimingInfo> entry = _sortedProfileData[verticalIndex];
if (entry.Key.SessionGroup == null)
{
continue;
}
float y = GetLineY(yOffset, LineHeight, LinePadding, true, verticalIndex);
canvas.DrawText(entry.Key.SessionGroup, new SKPoint(xOffset, y), textFont);
columnWidth = textFont.MeasureText(entry.Key.SessionGroup);
if (columnWidth > maxColumnWidth)
{
maxColumnWidth = columnWidth;
}
}
canvas.Restore();
canvas.DrawText("Group", new SKPoint(xOffset, TitleFontHeight + 2), titleFont);
columnWidth = titleFont.MeasureText("Group");
if (columnWidth > maxColumnWidth)
{
maxColumnWidth = columnWidth;
}
xOffset += maxColumnWidth + ColumnSpacing;
canvas.DrawLine(new SKPoint(xOffset - ColumnSpacing / 2, 0), new SKPoint(xOffset - ColumnSpacing / 2, viewBottom), textFont);
// Display session item
maxColumnWidth = MinimumColumnWidth;
canvas.Save();
canvas.ClipRect(new SKRect(0, viewTop, _rendererWidth, viewBottom), SKClipOperation.Intersect);
for (int verticalIndex = 0; verticalIndex < _sortedProfileData.Count; verticalIndex++)
{
KeyValuePair<ProfileConfig, TimingInfo> entry = _sortedProfileData[verticalIndex];
if (entry.Key.SessionItem == null)
{
continue;
}
float y = GetLineY(yOffset, LineHeight, LinePadding, true, verticalIndex);
canvas.DrawText(entry.Key.SessionGroup, new SKPoint(xOffset, y), textFont);
columnWidth = textFont.MeasureText(entry.Key.SessionItem);
if (columnWidth > maxColumnWidth)
{
maxColumnWidth = columnWidth;
}
}
canvas.Restore();
canvas.DrawText("Item", new SKPoint(xOffset, TitleFontHeight + 2), titleFont);
columnWidth = titleFont.MeasureText("Item");
if (columnWidth > maxColumnWidth)
{
maxColumnWidth = columnWidth;
}
xOffset += maxColumnWidth + ColumnSpacing;
timingWidth = _rendererWidth - xOffset - 370;
canvas.Save();
canvas.ClipRect(new SKRect(0, viewTop, _rendererWidth, viewBottom), SKClipOperation.Intersect);
canvas.DrawLine(new SKPoint(xOffset, 0), new SKPoint(xOffset, _rendererHeight), textFont);
int mode = _modeBox.Active;
canvas.Save();
canvas.ClipRect(new SKRect(xOffset, yOffset,xOffset + timingWidth,yOffset + contentHeight),
SKClipOperation.Intersect);
switch (mode)
{
case 0:
DrawGraph(xOffset, yOffset, timingWidth, canvas);
break;
case 1:
DrawBars(xOffset, yOffset, timingWidth, canvas);
canvas.DrawText("Blue: Instant, Green: Avg, Red: Total",
new SKPoint(xOffset, _rendererHeight - TitleFontHeight), titleFont);
break;
}
canvas.Restore();
canvas.DrawLine(new SKPoint(xOffset + timingWidth, 0), new SKPoint(xOffset + timingWidth, _rendererHeight), textFont);
xOffset = _rendererWidth - 360;
// Display timestamps
long totalInstant = 0;
long totalAverage = 0;
long totalTime = 0;
long totalCount = 0;
for (int verticalIndex = 0; verticalIndex < _sortedProfileData.Count; verticalIndex++)
{
KeyValuePair<ProfileConfig, TimingInfo> entry = _sortedProfileData[verticalIndex];
float y = GetLineY(yOffset, LineHeight, LinePadding, true, verticalIndex);
canvas.DrawText($"{GetTimeString(entry.Value.Instant)} ({entry.Value.InstantCount})", new SKPoint(xOffset, y), textFont);
canvas.DrawText(GetTimeString(entry.Value.AverageTime), new SKPoint(150 + xOffset, y), textFont);
canvas.DrawText(GetTimeString(entry.Value.TotalTime), new SKPoint(260 + xOffset, y), textFont);
totalInstant += entry.Value.Instant;
totalAverage += entry.Value.AverageTime;
totalTime += entry.Value.TotalTime;
totalCount += entry.Value.InstantCount;
}
canvas.Restore();
canvas.DrawLine(new SKPoint(0, viewTop), new SKPoint(_rendererWidth, viewTop), titleFont);
float yHeight = 0 + TitleFontHeight;
canvas.DrawText("Instant (Count)", new SKPoint(xOffset, yHeight), titleFont);
canvas.DrawText("Average", new SKPoint(150 + xOffset, yHeight), titleFont);
canvas.DrawText("Total (ms)", new SKPoint(260 + xOffset, yHeight), titleFont);
// Totals
yHeight = _rendererHeight - FilterHeight + 3;
int textHeight = LineHeight - 2;
SKPaint detailFont = new SKPaint()
{
Color = new SKColor(100, 100, 255, 255),
TextSize = textHeight
};
canvas.DrawLine(new SkiaSharp.SKPoint(0, viewBottom), new SkiaSharp.SKPoint(_rendererWidth,viewBottom), textFont);
string hostTimeString = $"Host {GetTimeString(_timingFlagsLast[(int)TimingFlagType.SystemFrame])} " +
$"({GetTimeString(_timingFlagsAverages[(int)TimingFlagType.SystemFrame])})";
canvas.DrawText(hostTimeString, new SKPoint(5, yHeight), detailFont);
float tempWidth = detailFont.MeasureText(hostTimeString);
detailFont.Color = SKColors.Red;
string gameTimeString = $"Game {GetTimeString(_timingFlagsLast[(int)TimingFlagType.FrameSwap])} " +
$"({GetTimeString(_timingFlagsAverages[(int)TimingFlagType.FrameSwap])})";
canvas.DrawText(gameTimeString, new SKPoint(15 + tempWidth, yHeight), detailFont);
tempWidth += detailFont.MeasureText(gameTimeString);
detailFont.Color = SKColors.White;
canvas.DrawText($"Profiler: Update {GetTimeString(_lastOutputUpdateDuration)} Draw {GetTimeString(_lastOutputDrawDuration)}",
new SKPoint(20 + tempWidth, yHeight), detailFont);
detailFont.Color = SKColors.White;
canvas.DrawText($"{GetTimeString(totalInstant)} ({totalCount})", new SKPoint(xOffset, yHeight), detailFont);
canvas.DrawText(GetTimeString(totalAverage), new SKPoint(150 + xOffset, yHeight), detailFont);
canvas.DrawText(GetTimeString(totalTime), new SKPoint(260 + xOffset, yHeight), detailFont);
_lastOutputDrawDuration = PerformanceCounter.ElapsedTicks - _lastOutputDraw;
}
}
private void DrawGraph(float xOffset, float yOffset, float width, SKCanvas canvas)
{
if (_sortedProfileData.Count != 0)
{
int left, right;
float top, bottom;
float graphRight = xOffset + width;
float barHeight = (LineHeight - LinePadding);
long history = Profile.HistoryLength;
double timeWidthTicks = history / (double)_graphZoom;
long graphPositionTicks = (long)(_graphPosition * PerformanceCounter.TicksPerMillisecond);
long ticksPerPixel = (long)(timeWidthTicks / width);
// Reset start point if out of bounds
if (timeWidthTicks + graphPositionTicks > history)
{
graphPositionTicks = history - (long)timeWidthTicks;
_graphPosition = (float)graphPositionTicks / PerformanceCounter.TicksPerMillisecond;
}
graphPositionTicks = _captureTime - graphPositionTicks;
// Draw timing flags
if (_showFlags.Active)
{
TimingFlagType prevType = TimingFlagType.Count;
SKPaint timingPaint = new SKPaint
{
Color = _timingFlagColors.First()
};
foreach (TimingFlag timingFlag in _timingFlags)
{
if (prevType != timingFlag.FlagType)
{
prevType = timingFlag.FlagType;
timingPaint.Color = _timingFlagColors[(int)prevType];
}
int x = (int)(graphRight - ((graphPositionTicks - timingFlag.Timestamp) / timeWidthTicks) * width);
if (x > xOffset)
{
canvas.DrawLine(new SKPoint(x, yOffset), new SKPoint(x, _rendererHeight), timingPaint);
}
}
}
SKPaint barPaint = new SKPaint()
{
Color = SKColors.Green,
};
// Draw bars
for (int verticalIndex = 0; verticalIndex < _sortedProfileData.Count; verticalIndex++)
{
KeyValuePair<ProfileConfig, TimingInfo> entry = _sortedProfileData[verticalIndex];
long furthest = 0;
bottom = GetLineY(yOffset, LineHeight, LinePadding, false, verticalIndex);
top = bottom + barHeight;
// Skip rendering out of bounds bars
if (top < 0 || bottom > _rendererHeight)
{
continue;
}
barPaint.Color = SKColors.Green;
foreach (Timestamp timestamp in entry.Value.GetAllTimestamps())
{
// Skip drawing multiple timestamps on same pixel
if (timestamp.EndTime < furthest)
{
continue;
}
furthest = timestamp.EndTime + ticksPerPixel;
left = (int)(graphRight - ((graphPositionTicks - timestamp.BeginTime) / timeWidthTicks) * width);
right = (int)(graphRight - ((graphPositionTicks - timestamp.EndTime) / timeWidthTicks) * width);
left = (int)Math.Max(xOffset +1, left);
// Make sure width is at least 1px
right = Math.Max(left + 1, right);
canvas.DrawRect(new SKRect(left, top, right, bottom), barPaint);
}
// Currently capturing timestamp
barPaint.Color = SKColors.Red;
long entryBegin = entry.Value.BeginTime;
if (entryBegin != -1)
{
left = (int)(graphRight - ((graphPositionTicks - entryBegin) / timeWidthTicks) * width);
// Make sure width is at least 1px
left = Math.Min(left - 1, (int)graphRight);
left = (int)Math.Max(xOffset + 1, left);
canvas.DrawRect(new SKRect(left, top, graphRight, bottom), barPaint);
}
}
string label = $"-{MathF.Round(_graphPosition, 2)} ms";
SKPaint labelPaint = new SKPaint()
{
Color = SKColors.White,
TextSize = LineHeight
};
float labelWidth = labelPaint.MeasureText(label);
canvas.DrawText(label,new SKPoint(graphRight - labelWidth - LinePadding, FilterHeight + LinePadding) , labelPaint);
canvas.DrawText($"-{MathF.Round((float)((timeWidthTicks / PerformanceCounter.TicksPerMillisecond) + _graphPosition), 2)} ms",
new SKPoint(xOffset + LinePadding, FilterHeight + LinePadding), labelPaint);
}
}
private void DrawBars(float xOffset, float yOffset, float width, SKCanvas canvas)
{
if (_sortedProfileData.Count != 0)
{
long maxAverage = 0;
long maxTotal = 0;
long maxInstant = 0;
float barHeight = (LineHeight - LinePadding) / 3.0f;
// Get max values
foreach (KeyValuePair<ProfileConfig, TimingInfo> kvp in _sortedProfileData)
{
maxInstant = Math.Max(maxInstant, kvp.Value.Instant);
maxAverage = Math.Max(maxAverage, kvp.Value.AverageTime);
maxTotal = Math.Max(maxTotal, kvp.Value.TotalTime);
}
SKPaint barPaint = new SKPaint()
{
Color = SKColors.Blue
};
for (int verticalIndex = 0; verticalIndex < _sortedProfileData.Count; verticalIndex++)
{
KeyValuePair<ProfileConfig, TimingInfo> entry = _sortedProfileData[verticalIndex];
// Instant
barPaint.Color = SKColors.Blue;
float bottom = GetLineY(yOffset, LineHeight, LinePadding, false, verticalIndex);
float top = bottom + barHeight;
float right = (float)entry.Value.Instant / maxInstant * width + xOffset;
// Skip rendering out of bounds bars
if (top < 0 || bottom > _rendererHeight)
{
continue;
}
canvas.DrawRect(new SKRect(xOffset, top, right, bottom), barPaint);
// Average
barPaint.Color = SKColors.Green;
top += barHeight;
bottom += barHeight;
right = (float)entry.Value.AverageTime / maxAverage * width + xOffset;
canvas.DrawRect(new SKRect(xOffset, top, right, bottom), barPaint);
// Total
barPaint.Color = SKColors.Red;
top += barHeight;
bottom += barHeight;
right = (float)entry.Value.TotalTime / maxTotal * width + xOffset;
canvas.DrawRect(new SKRect(xOffset, top, right, bottom), barPaint);
}
}
}
}
}

View file

@ -0,0 +1,232 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.21.0 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkListStore" id="viewMode">
<columns>
<!-- column-name mode -->
<column type="gint"/>
<!-- column-name label -->
<column type="gchararray"/>
</columns>
<data>
<row>
<col id="0">0</col>
<col id="1" translatable="yes">Graph</col>
</row>
<row>
<col id="0">1</col>
<col id="1" translatable="yes">Bars</col>
</row>
</data>
</object>
<object class="GtkBox" id="_profilerBox">
<property name="name">ProfilerBox</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="orientation">vertical</property>
<property name="spacing">10</property>
<child>
<object class="GtkCheckButton" id="_enableCheckbutton">
<property name="label" translatable="yes">Enable Profiler</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkScrolledWindow" id="_scrollview">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="vscrollbar_policy">never</property>
<property name="shadow_type">in</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkScrollbar" id="_outputScrollbar">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">10</property>
<child>
<object class="GtkCheckButton" id="_showInactive">
<property name="label" translatable="yes">Show Inactive</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="active">True</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="_showFlags">
<property name="label" translatable="yes">Show Flags</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="active">True</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="_pauseCheckbutton">
<property name="label" translatable="yes">Paused</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">View Mode: </property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkComboBox" id="_modeBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="model">viewMode</property>
<property name="active">0</property>
<child>
<object class="GtkCellRendererText" id="modeTextRenderer"/>
<attributes>
<attribute name="text">1</attribute>
</attributes>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Filter: </property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="_filterBox">
<property name="visible">True</property>
<property name="can_focus">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">4</property>
</packing>
</child>
<child>
<object class="GtkButton" id="_stepButton">
<property name="label" translatable="yes">Step</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">5</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</interface>

View file

@ -0,0 +1,23 @@
using SkiaSharp;
using SkiaSharp.Views.Gtk;
using System;
namespace Ryujinx.Debugger.UI
{
public class SkRenderer : SKDrawingArea
{
public event EventHandler DrawGraphs;
public SkRenderer()
{
this.PaintSurface += SkRenderer_PaintSurface;
}
private void SkRenderer_PaintSurface(object sender, SkiaSharp.Views.Desktop.SKPaintSurfaceEventArgs e)
{
e.Surface.Canvas.Clear(SKColors.Black);
DrawGraphs.Invoke(this, e);
}
}
}