0
0
Fork 0
mirror of https://github.com/ryujinx-mirror/ryujinx.git synced 2024-12-23 03:45:46 +00:00

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

@ -13,6 +13,15 @@
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Release|AnyCPU'">
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Optimize>true</Optimize>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Debug|AnyCPU'">
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" /> <PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
</ItemGroup> </ItemGroup>

View file

@ -12,7 +12,7 @@
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Debug|AnyCPU'">
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DefineConstants>TRACE;USE_PROFILING</DefineConstants> <DefineConstants>TRACE;USE_DEBUGGING</DefineConstants>
<Optimize>false</Optimize> <Optimize>false</Optimize>
</PropertyGroup> </PropertyGroup>
@ -22,7 +22,7 @@
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Release|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Release|AnyCPU'">
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DefineConstants>TRACE;USE_PROFILING</DefineConstants> <DefineConstants>TRACE;USE_DEBUGGING</DefineConstants>
<Optimize>true</Optimize> <Optimize>true</Optimize>
</PropertyGroup> </PropertyGroup>

View file

@ -12,7 +12,7 @@
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Debug|AnyCPU'">
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DefineConstants>TRACE;USE_PROFILING</DefineConstants> <DefineConstants>TRACE;USE_DEBUGGING</DefineConstants>
<Optimize>false</Optimize> <Optimize>false</Optimize>
</PropertyGroup> </PropertyGroup>
@ -22,7 +22,7 @@
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Release|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Release|AnyCPU'">
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DefineConstants>TRACE;USE_PROFILING</DefineConstants> <DefineConstants>TRACE;USE_DEBUGGING</DefineConstants>
<Optimize>true</Optimize> <Optimize>true</Optimize>
</PropertyGroup> </PropertyGroup>

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

@ -4,7 +4,7 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
namespace Ryujinx.Profiler namespace Ryujinx.Debugger.Profiler
{ {
public static class DumpProfile public static class DumpProfile
{ {

View file

@ -1,12 +1,12 @@
using System; using Ryujinx.Common;
using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Ryujinx.Common;
namespace Ryujinx.Profiler namespace Ryujinx.Debugger.Profiler
{ {
public class InternalProfile public class InternalProfile
{ {
@ -26,17 +26,17 @@ namespace Ryujinx.Profiler
// Cleanup thread // Cleanup thread
private readonly Thread _cleanupThread; private readonly Thread _cleanupThread;
private bool _cleanupRunning; private bool _cleanupRunning;
private readonly long _history; private readonly long _history;
private long _preserve; private long _preserve;
// Timing flags // Timing flags
private TimingFlag[] _timingFlags; private TimingFlag[] _timingFlags;
private long[] _timingFlagAverages; private long[] _timingFlagAverages;
private long[] _timingFlagLast; private long[] _timingFlagLast;
private long[] _timingFlagLastDelta; private long[] _timingFlagLastDelta;
private int _timingFlagCount; private int _timingFlagCount;
private int _timingFlagIndex; private int _timingFlagIndex;
private int _maxFlags; private int _maxFlags;

View file

@ -4,19 +4,17 @@ using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
namespace Ryujinx.Profiler namespace Ryujinx.Debugger.Profiler
{ {
public static class Profile public static class Profile
{ {
public static float UpdateRate => _settings.UpdateRate; public static float UpdateRate => _settings.UpdateRate;
public static long HistoryLength => _settings.History; public static long HistoryLength => _settings.History;
public static ProfilerKeyboardHandler Controls => _settings.Controls;
private static InternalProfile _profileInstance; private static InternalProfile _profileInstance;
private static ProfilerSettings _settings; private static ProfilerSettings _settings;
[Conditional("USE_PROFILING")] [Conditional("USE_DEBUGGING")]
public static void Initialize() public static void Initialize()
{ {
var config = ProfilerConfiguration.Load(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ProfilerConfig.jsonc")); var config = ProfilerConfiguration.Load(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ProfilerConfig.jsonc"));
@ -29,14 +27,13 @@ namespace Ryujinx.Profiler
UpdateRate = (config.UpdateRate <= 0) ? -1 : 1.0f / config.UpdateRate, UpdateRate = (config.UpdateRate <= 0) ? -1 : 1.0f / config.UpdateRate,
History = (long)(config.History * PerformanceCounter.TicksPerSecond), History = (long)(config.History * PerformanceCounter.TicksPerSecond),
MaxLevel = config.MaxLevel, MaxLevel = config.MaxLevel,
Controls = config.Controls,
MaxFlags = config.MaxFlags, MaxFlags = config.MaxFlags,
}; };
} }
public static bool ProfilingEnabled() public static bool ProfilingEnabled()
{ {
#if USE_PROFILING #if USE_DEBUGGING
if (!_settings.Enabled) if (!_settings.Enabled)
return false; return false;
@ -49,7 +46,7 @@ namespace Ryujinx.Profiler
#endif #endif
} }
[Conditional("USE_PROFILING")] [Conditional("USE_DEBUGGING")]
public static void FinishProfiling() public static void FinishProfiling()
{ {
if (!ProfilingEnabled()) if (!ProfilingEnabled())
@ -61,7 +58,7 @@ namespace Ryujinx.Profiler
_profileInstance.Dispose(); _profileInstance.Dispose();
} }
[Conditional("USE_PROFILING")] [Conditional("USE_DEBUGGING")]
public static void FlagTime(TimingFlagType flagType) public static void FlagTime(TimingFlagType flagType)
{ {
if (!ProfilingEnabled()) if (!ProfilingEnabled())
@ -69,7 +66,7 @@ namespace Ryujinx.Profiler
_profileInstance.FlagTime(flagType); _profileInstance.FlagTime(flagType);
} }
[Conditional("USE_PROFILING")] [Conditional("USE_DEBUGGING")]
public static void RegisterFlagReceiver(Action<TimingFlag> receiver) public static void RegisterFlagReceiver(Action<TimingFlag> receiver)
{ {
if (!ProfilingEnabled()) if (!ProfilingEnabled())
@ -77,7 +74,7 @@ namespace Ryujinx.Profiler
_profileInstance.RegisterFlagReceiver(receiver); _profileInstance.RegisterFlagReceiver(receiver);
} }
[Conditional("USE_PROFILING")] [Conditional("USE_DEBUGGING")]
public static void Begin(ProfileConfig config) public static void Begin(ProfileConfig config)
{ {
if (!ProfilingEnabled()) if (!ProfilingEnabled())
@ -87,7 +84,7 @@ namespace Ryujinx.Profiler
_profileInstance.BeginProfile(config); _profileInstance.BeginProfile(config);
} }
[Conditional("USE_PROFILING")] [Conditional("USE_DEBUGGING")]
public static void End(ProfileConfig config) public static void End(ProfileConfig config)
{ {
if (!ProfilingEnabled()) if (!ProfilingEnabled())
@ -99,7 +96,7 @@ namespace Ryujinx.Profiler
public static string GetSession() public static string GetSession()
{ {
#if USE_PROFILING #if USE_DEBUGGING
if (!ProfilingEnabled()) if (!ProfilingEnabled())
return null; return null;
return _profileInstance.GetSession(); return _profileInstance.GetSession();
@ -110,7 +107,7 @@ namespace Ryujinx.Profiler
public static List<KeyValuePair<ProfileConfig, TimingInfo>> GetProfilingData() public static List<KeyValuePair<ProfileConfig, TimingInfo>> GetProfilingData()
{ {
#if USE_PROFILING #if USE_DEBUGGING
if (!ProfilingEnabled()) if (!ProfilingEnabled())
return new List<KeyValuePair<ProfileConfig, TimingInfo>>(); return new List<KeyValuePair<ProfileConfig, TimingInfo>>();
return _profileInstance.GetProfilingData(); return _profileInstance.GetProfilingData();
@ -121,7 +118,7 @@ namespace Ryujinx.Profiler
public static TimingFlag[] GetTimingFlags() public static TimingFlag[] GetTimingFlags()
{ {
#if USE_PROFILING #if USE_DEBUGGING
if (!ProfilingEnabled()) if (!ProfilingEnabled())
return new TimingFlag[0]; return new TimingFlag[0];
return _profileInstance.GetTimingFlags(); return _profileInstance.GetTimingFlags();
@ -132,7 +129,7 @@ namespace Ryujinx.Profiler
public static (long[], long[]) GetTimingAveragesAndLast() public static (long[], long[]) GetTimingAveragesAndLast()
{ {
#if USE_PROFILING #if USE_DEBUGGING
if (!ProfilingEnabled()) if (!ProfilingEnabled())
return (new long[0], new long[0]); return (new long[0], new long[0]);
return _profileInstance.GetTimingAveragesAndLast(); return _profileInstance.GetTimingAveragesAndLast();

View file

@ -1,6 +1,6 @@
using System; using System;
namespace Ryujinx.Profiler namespace Ryujinx.Debugger.Profiler
{ {
public struct ProfileConfig : IEquatable<ProfileConfig> public struct ProfileConfig : IEquatable<ProfileConfig>
{ {

View file

@ -1,7 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace Ryujinx.Profiler.UI namespace Ryujinx.Debugger.Profiler
{ {
public static class ProfileSorters public static class ProfileSorters
{ {

View file

@ -1,10 +1,10 @@
using OpenTK.Input; using Gdk;
using System; using System;
using System.IO; using System.IO;
using Utf8Json; using Utf8Json;
using Utf8Json.Resolvers; using Utf8Json.Resolvers;
namespace Ryujinx.Profiler namespace Ryujinx.Debugger.Profiler
{ {
public class ProfilerConfiguration public class ProfilerConfiguration
{ {
@ -15,8 +15,6 @@ namespace Ryujinx.Profiler
public int MaxFlags { get; private set; } public int MaxFlags { get; private set; }
public float History { get; private set; } public float History { get; private set; }
public ProfilerKeyboardHandler Controls { get; private set; }
/// <summary> /// <summary>
/// Loads a configuration file from disk /// Loads a configuration file from disk
/// </summary> /// </summary>

View file

@ -1,4 +1,4 @@
namespace Ryujinx.Profiler namespace Ryujinx.Debugger.Profiler
{ {
public class ProfilerSettings public class ProfilerSettings
{ {
@ -13,8 +13,5 @@
// 19531225 = 5 seconds in ticks on most pc's. // 19531225 = 5 seconds in ticks on most pc's.
// It should get set on boot to the time specified in config // It should get set on boot to the time specified in config
public long History { get; set; } = 19531225; public long History { get; set; } = 19531225;
// Controls
public ProfilerKeyboardHandler Controls;
} }
} }

View file

@ -1,4 +1,4 @@
namespace Ryujinx.Profiler namespace Ryujinx.Debugger.Profiler
{ {
public enum TimingFlagType public enum TimingFlagType
{ {

View file

@ -1,7 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace Ryujinx.Profiler namespace Ryujinx.Debugger.Profiler
{ {
public struct Timestamp public struct Timestamp
{ {

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);
}
}
}

View file

@ -6,7 +6,7 @@ using Ryujinx.HLE.HOS.Kernel.Ipc;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using Ryujinx.Profiler; using Ryujinx.Debugger.Profiler;
using System.Reflection; using System.Reflection;
using System.Linq; using System.Linq;

View file

@ -1,4 +1,4 @@
using Ryujinx.Profiler; using Ryujinx.Debugger.Profiler;
using System.Diagnostics; using System.Diagnostics;
using System.Timers; using System.Timers;

View file

@ -12,7 +12,7 @@
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Debug|AnyCPU'">
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DefineConstants>TRACE;USE_PROFILING</DefineConstants> <DefineConstants>TRACE;USE_DEBUGGING</DefineConstants>
<Optimize>false</Optimize> <Optimize>false</Optimize>
</PropertyGroup> </PropertyGroup>
@ -22,7 +22,7 @@
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Release|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Release|AnyCPU'">
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DefineConstants>TRACE;USE_PROFILING</DefineConstants> <DefineConstants>TRACE;USE_DEBUGGING</DefineConstants>
<Optimize>true</Optimize> <Optimize>true</Optimize>
</PropertyGroup> </PropertyGroup>
@ -44,7 +44,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Ryujinx.Audio\Ryujinx.Audio.csproj" /> <ProjectReference Include="..\Ryujinx.Audio\Ryujinx.Audio.csproj" />
<ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" /> <ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" />
<ProjectReference Include="..\Ryujinx.Profiler\Ryujinx.Profiler.csproj" /> <ProjectReference Include="..\Ryujinx.Debugger\Ryujinx.Debugger.csproj" />
<ProjectReference Include="..\ARMeilleure\ARMeilleure.csproj" /> <ProjectReference Include="..\ARMeilleure\ARMeilleure.csproj" />
<ProjectReference Include="..\Ryujinx.Graphics.Gpu\Ryujinx.Graphics.Gpu.csproj" /> <ProjectReference Include="..\Ryujinx.Graphics.Gpu\Ryujinx.Graphics.Gpu.csproj" />
<ProjectReference Include="..\Ryujinx.Graphics.GAL\Ryujinx.Graphics.GAL.csproj" /> <ProjectReference Include="..\Ryujinx.Graphics.GAL\Ryujinx.Graphics.GAL.csproj" />

View file

@ -8,12 +8,12 @@
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Release|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Release|AnyCPU'">
<DefineConstants>TRACE;USE_PROFILING</DefineConstants> <DefineConstants>TRACE;USE_DEBUGGING</DefineConstants>
<Optimize>true</Optimize> <Optimize>true</Optimize>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Debug|AnyCPU'">
<DefineConstants>TRACE;USE_PROFILING</DefineConstants> <DefineConstants>TRACE;USE_DEBUGGING</DefineConstants>
<Optimize>false</Optimize> <Optimize>false</Optimize>
</PropertyGroup> </PropertyGroup>

View file

@ -1,28 +0,0 @@
using OpenTK.Input;
namespace Ryujinx.Profiler
{
public struct ProfilerButtons
{
public Key ToggleProfiler;
}
public class ProfilerKeyboardHandler
{
public ProfilerButtons Buttons;
private KeyboardState _prevKeyboard;
public ProfilerKeyboardHandler(ProfilerButtons buttons)
{
Buttons = buttons;
}
public bool TogglePressed(KeyboardState keyboard) => !keyboard[Buttons.ToggleProfiler] && _prevKeyboard[Buttons.ToggleProfiler];
public void SetPrevKeyboardState(KeyboardState keyboard)
{
_prevKeyboard = keyboard;
}
}
}

View file

@ -1,39 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
<RuntimeIdentifiers>win-x64;osx-x64;linux-x64</RuntimeIdentifiers>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Configurations>Debug;Release;Profile Debug;Profile Release</Configurations>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DefineConstants>TRACE</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Debug|AnyCPU'">
<DefineConstants>TRACE;USE_PROFILING</DefineConstants>
<Optimize>false</Optimize>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Release|AnyCPU'">
<DefineConstants>TRACE;USE_PROFILING</DefineConstants>
<Optimize>true</Optimize>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="OpenTK.NetStandard" Version="1.0.4" />
<PackageReference Include="SharpFontCore" Version="0.1.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" />
</ItemGroup>
<ItemGroup Condition="'$(Configuration)' == 'Profile Debug' Or '$(Configuration)' == 'Profile Release'">
<None Update="ProfilerConfig.jsonc">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View file

@ -1,110 +0,0 @@
using System;
using OpenTK;
using OpenTK.Graphics.OpenGL;
using Ryujinx.Profiler.UI.SharpFontHelpers;
namespace Ryujinx.Profiler.UI
{
public class ProfileButton
{
// Store font service
private FontService _fontService;
// Layout information
private int _left, _right;
private int _bottom, _top;
private int _height;
private int _padding;
// Label information
private int _labelX, _labelY;
private string _label;
// Misc
private Action _clicked;
private bool _visible;
public ProfileButton(FontService fontService, Action clicked)
: this(fontService, clicked, 0, 0, 0, 0, 0)
{
_visible = false;
}
public ProfileButton(FontService fontService, Action clicked, int x, int y, int padding, int height, int width)
: this(fontService, "", clicked, x, y, padding, height, width)
{
_visible = false;
}
public ProfileButton(FontService fontService, string label, Action clicked, int x, int y, int padding, int height, int width = -1)
{
_fontService = fontService;
_clicked = clicked;
UpdateSize(label, x, y, padding, height, width);
}
public int UpdateSize(string label, int x, int y, int padding, int height, int width = -1)
{
_visible = true;
_label = label;
if (width == -1)
{
// Dummy draw to measure size
width = (int)_fontService.DrawText(label, 0, 0, height, false);
}
UpdateSize(x, y, padding, width, height);
return _right - _left;
}
public void UpdateSize(int x, int y, int padding, int width, int height)
{
_height = height;
_left = x;
_bottom = y;
_labelX = x + padding / 2;
_labelY = y + padding / 2;
_top = y + height + padding;
_right = x + width + padding;
}
public void Draw()
{
if (!_visible)
{
return;
}
// Draw backing rectangle
GL.Begin(PrimitiveType.Triangles);
GL.Color3(Color.Black);
GL.Vertex2(_left, _bottom);
GL.Vertex2(_left, _top);
GL.Vertex2(_right, _top);
GL.Vertex2(_right, _top);
GL.Vertex2(_right, _bottom);
GL.Vertex2(_left, _bottom);
GL.End();
// Use font service to draw label
_fontService.DrawText(_label, _labelX, _labelY, _height);
}
public bool ProcessClick(int x, int y)
{
// If button contains x, y
if (x > _left && x < _right &&
y > _bottom && y < _top)
{
_clicked();
return true;
}
return false;
}
}
}

View file

@ -1,773 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text.RegularExpressions;
using OpenTK;
using OpenTK.Graphics;
using OpenTK.Graphics.OpenGL;
using OpenTK.Input;
using Ryujinx.Common;
using Ryujinx.Profiler.UI.SharpFontHelpers;
namespace Ryujinx.Profiler.UI
{
public partial class ProfileWindow : GameWindow
{
// List all buttons for index in button array
private enum ButtonIndex
{
TagTitle = 0,
InstantTitle = 1,
AverageTitle = 2,
TotalTitle = 3,
FilterBar = 4,
ShowHideInactive = 5,
Pause = 6,
ChangeDisplay = 7,
// Don't automatically draw after here
ToggleFlags = 8,
Step = 9,
// Update this when new buttons are added.
// These are indexes to the enum list
Autodraw = 8,
Count = 10,
}
// Font service
private FontService _fontService;
// UI variables
private ProfileButton[] _buttons;
private bool _initComplete = false;
private bool _visible = true;
private bool _visibleChanged = true;
private bool _viewportUpdated = true;
private bool _redrawPending = true;
private bool _displayGraph = true;
private bool _displayFlags = true;
private bool _showInactive = true;
private bool _paused = false;
private bool _doStep = false;
// Layout
private const int LineHeight = 16;
private const int TitleHeight = 24;
private const int TitleFontHeight = 16;
private const int LinePadding = 2;
private const int ColumnSpacing = 15;
private const int FilterHeight = 24;
private const int BottomBarHeight = FilterHeight + LineHeight;
// Sorting
private List<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;
private float _minScroll = 0;
private float _maxScroll = 0;
// Profile data storage
private List<KeyValuePair<ProfileConfig, TimingInfo>> _sortedProfileData;
private long _captureTime;
// Input
private bool _backspaceDown = false;
private bool _prevBackspaceDown = false;
private double _backspaceDownTime = 0;
// F35 used as no key
private Key _graphControlKey = Key.F35;
// Event management
private double _updateTimer;
private double _processEventTimer;
private bool _profileUpdated = false;
private readonly object _profileDataLock = new object();
public ProfileWindow()
// Graphics mode enables 2xAA
: base(1280, 720, new GraphicsMode(new ColorFormat(8, 8, 8, 8), 1, 1, 2))
{
Title = "Profiler";
Location = new Point(DisplayDevice.Default.Width - 1280,
(DisplayDevice.Default.Height - 720) - 50);
if (Profile.UpdateRate <= 0)
{
// Perform step regardless of flag type
Profile.RegisterFlagReceiver((t) =>
{
if (!_paused)
{
_doStep = true;
}
});
}
// Large number to force an update on first update
_updateTimer = 0xFFFF;
Init();
// Release context for render thread
Context.MakeCurrent(null);
}
public void ToggleVisible()
{
_visible = !_visible;
_visibleChanged = true;
}
private void SetSort(IComparer<KeyValuePair<ProfileConfig, TimingInfo>> filter)
{
_sortAction = filter;
_profileUpdated = true;
}
#region OnLoad
/// <summary>
/// Setup OpenGL and load resources
/// </summary>
public void Init()
{
GL.ClearColor(Color.Black);
_fontService = new FontService();
_fontService.InitializeTextures();
_fontService.UpdateScreenHeight(Height);
_buttons = new ProfileButton[(int)ButtonIndex.Count];
_buttons[(int)ButtonIndex.TagTitle] = new ProfileButton(_fontService, () => SetSort(new ProfileSorters.TagAscending()));
_buttons[(int)ButtonIndex.InstantTitle] = new ProfileButton(_fontService, () => SetSort(new ProfileSorters.InstantAscending()));
_buttons[(int)ButtonIndex.AverageTitle] = new ProfileButton(_fontService, () => SetSort(new ProfileSorters.AverageAscending()));
_buttons[(int)ButtonIndex.TotalTitle] = new ProfileButton(_fontService, () => SetSort(new ProfileSorters.TotalAscending()));
_buttons[(int)ButtonIndex.Step] = new ProfileButton(_fontService, () => _doStep = true);
_buttons[(int)ButtonIndex.FilterBar] = new ProfileButton(_fontService, () =>
{
_profileUpdated = true;
_regexEnabled = !_regexEnabled;
});
_buttons[(int)ButtonIndex.ShowHideInactive] = new ProfileButton(_fontService, () =>
{
_profileUpdated = true;
_showInactive = !_showInactive;
});
_buttons[(int)ButtonIndex.Pause] = new ProfileButton(_fontService, () =>
{
_profileUpdated = true;
_paused = !_paused;
});
_buttons[(int)ButtonIndex.ToggleFlags] = new ProfileButton(_fontService, () =>
{
_displayFlags = !_displayFlags;
_redrawPending = true;
});
_buttons[(int)ButtonIndex.ChangeDisplay] = new ProfileButton(_fontService, () =>
{
_displayGraph = !_displayGraph;
_redrawPending = true;
});
Visible = _visible;
}
#endregion
#region OnResize
/// <summary>
/// Respond to resize events
/// </summary>
/// <param name="e">Contains information on the new GameWindow size.</param>
/// <remarks>There is no need to call the base implementation.</remarks>
protected override void OnResize(EventArgs e)
{
_viewportUpdated = true;
}
#endregion
#region OnClose
/// <summary>
/// Intercept close event and hide instead
/// </summary>
protected override void OnClosing(CancelEventArgs e)
{
// Hide window
_visible = false;
_visibleChanged = true;
// Cancel close
e.Cancel = true;
base.OnClosing(e);
}
#endregion
#region OnUpdateFrame
/// <summary>
/// Profile Update Loop
/// </summary>
/// <param name="e">Contains timing information.</param>
/// <remarks>There is no need to call the base implementation.</remarks>
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
/// <summary>
/// Profile Render Loop
/// </summary>
/// <remarks>There is no need to call the base implementation.</remarks>
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;
}
}
}

View file

@ -1,85 +0,0 @@
using System;
using System.Collections.Generic;
using OpenTK;
using OpenTK.Graphics.OpenGL;
namespace Ryujinx.Profiler.UI
{
public partial class ProfileWindow
{
private void DrawBars(float xOffset, float yOffset, float width)
{
if (_sortedProfileData.Count != 0)
{
long maxAverage;
long maxTotal;
int verticalIndex = 0;
float barHeight = (LineHeight - LinePadding) / 3.0f;
// Get max values
long maxInstant = maxAverage = maxTotal = 0;
foreach (KeyValuePair<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);
}
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);
}
}
}
}

View file

@ -1,151 +0,0 @@
using System;
using OpenTK;
using OpenTK.Graphics.OpenGL;
using Ryujinx.Common;
namespace Ryujinx.Profiler.UI
{
public partial class ProfileWindow
{
// Color index equal to timing flag type as int
private Color[] _timingFlagColors = new[]
{
new Color(150, 25, 25, 50), // FrameSwap = 0
new Color(25, 25, 150, 50), // SystemFrame = 1
};
private TimingFlag[] _timingFlags;
private const float GraphMoveSpeed = 40000;
private const float GraphZoomSpeed = 50;
private float _graphZoom = 1;
private float _graphPosition = 0;
private void DrawGraph(float xOffset, float yOffset, float width)
{
if (_sortedProfileData.Count != 0)
{
int left, right;
float top, bottom;
int verticalIndex = 0;
float graphRight = xOffset + width;
float barHeight = (LineHeight - LinePadding);
long history = Profile.HistoryLength;
double timeWidthTicks = history / (double)_graphZoom;
long graphPositionTicks = (long)(_graphPosition * PerformanceCounter.TicksPerMillisecond);
long ticksPerPixel = (long)(timeWidthTicks / width);
// Reset start point if out of bounds
if (timeWidthTicks + graphPositionTicks > history)
{
graphPositionTicks = history - (long)timeWidthTicks;
_graphPosition = (float)graphPositionTicks / PerformanceCounter.TicksPerMillisecond;
}
graphPositionTicks = _captureTime - graphPositionTicks;
GL.Enable(EnableCap.ScissorTest);
// Draw timing flags
if (_displayFlags)
{
TimingFlagType prevType = TimingFlagType.Count;
GL.Enable(EnableCap.Blend);
GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
GL.Begin(PrimitiveType.Lines);
foreach (TimingFlag timingFlag in _timingFlags)
{
if (prevType != timingFlag.FlagType)
{
prevType = timingFlag.FlagType;
GL.Color4(_timingFlagColors[(int)prevType]);
}
int x = (int)(graphRight - ((graphPositionTicks - timingFlag.Timestamp) / timeWidthTicks) * width);
GL.Vertex2(x, 0);
GL.Vertex2(x, Height);
}
GL.End();
GL.Disable(EnableCap.Blend);
}
// Draw bars
GL.Begin(PrimitiveType.Triangles);
foreach (var entry in _sortedProfileData)
{
long furthest = 0;
bottom = GetLineY(yOffset, LineHeight, LinePadding, true, verticalIndex);
top = bottom + barHeight;
// Skip rendering out of bounds bars
if (top < 0 || bottom > Height)
{
verticalIndex++;
continue;
}
GL.Color3(Color.Green);
foreach (Timestamp timestamp in entry.Value.GetAllTimestamps())
{
// Skip drawing multiple timestamps on same pixel
if (timestamp.EndTime < furthest)
continue;
furthest = timestamp.EndTime + ticksPerPixel;
left = (int)(graphRight - ((graphPositionTicks - timestamp.BeginTime) / timeWidthTicks) * width);
right = (int)(graphRight - ((graphPositionTicks - timestamp.EndTime) / timeWidthTicks) * width);
// Make sure width is at least 1px
right = Math.Max(left + 1, right);
GL.Vertex2(left, bottom);
GL.Vertex2(left, top);
GL.Vertex2(right, top);
GL.Vertex2(right, top);
GL.Vertex2(right, bottom);
GL.Vertex2(left, bottom);
}
// Currently capturing timestamp
GL.Color3(Color.Red);
long entryBegin = entry.Value.BeginTime;
if (entryBegin != -1)
{
left = (int)(graphRight - ((graphPositionTicks - entryBegin) / timeWidthTicks) * width);
// Make sure width is at least 1px
left = Math.Min(left - 1, (int)graphRight);
GL.Vertex2(left, bottom);
GL.Vertex2(left, top);
GL.Vertex2(graphRight, top);
GL.Vertex2(graphRight, top);
GL.Vertex2(graphRight, bottom);
GL.Vertex2(left, bottom);
}
verticalIndex++;
}
GL.End();
GL.Disable(EnableCap.ScissorTest);
string label = $"-{MathF.Round(_graphPosition, 2)} ms";
// Dummy draw for measure
float labelWidth = _fontService.DrawText(label, 0, 0, LineHeight, false);
_fontService.DrawText(label, graphRight - labelWidth - LinePadding, FilterHeight + LinePadding, LineHeight);
_fontService.DrawText($"-{MathF.Round((float)((timeWidthTicks / PerformanceCounter.TicksPerMillisecond) + _graphPosition), 2)} ms", xOffset + LinePadding, FilterHeight + LinePadding, LineHeight);
}
}
}
}

View file

@ -1,95 +0,0 @@
using System.Threading;
using OpenTK;
using OpenTK.Input;
using Ryujinx.Common;
namespace Ryujinx.Profiler.UI
{
public class ProfileWindowManager
{
private ProfileWindow _window;
private Thread _profileThread;
private Thread _renderThread;
private bool _profilerRunning;
// Timing
private double _prevTime;
public ProfileWindowManager()
{
if (Profile.ProfilingEnabled())
{
_profilerRunning = true;
_prevTime = 0;
_profileThread = new Thread(ProfileLoop)
{
Name = "Profiler.ProfileThread"
};
_profileThread.Start();
}
}
public void ToggleVisible()
{
if (Profile.ProfilingEnabled())
{
_window.ToggleVisible();
}
}
public void Close()
{
if (_window != null)
{
_profilerRunning = false;
_window.Close();
_window.Dispose();
}
_window = null;
}
public void UpdateKeyInput(KeyboardState keyboard)
{
if (Profile.Controls.TogglePressed(keyboard))
{
ToggleVisible();
}
Profile.Controls.SetPrevKeyboardState(keyboard);
}
private void ProfileLoop()
{
using (_window = new ProfileWindow())
{
// Create thread for render loop
_renderThread = new Thread(RenderLoop)
{
Name = "Profiler.RenderThread"
};
_renderThread.Start();
while (_profilerRunning)
{
double time = (double)PerformanceCounter.ElapsedTicks / PerformanceCounter.TicksPerSecond;
_window.Update(new FrameEventArgs(time - _prevTime));
_prevTime = time;
// Sleep to be less taxing, update usually does very little
Thread.Sleep(1);
}
}
}
private void RenderLoop()
{
_window.Context.MakeCurrent(_window.WindowInfo);
while (_profilerRunning)
{
_window.Draw();
Thread.Sleep(1);
}
}
}
}

View file

@ -1,257 +0,0 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
using OpenTK;
using OpenTK.Graphics.OpenGL;
using SharpFont;
namespace Ryujinx.Profiler.UI.SharpFontHelpers
{
public class FontService
{
private struct CharacterInfo
{
public float Left;
public float Right;
public float Top;
public float Bottom;
public int Width;
public float Height;
public float AspectRatio;
public float BearingX;
public float BearingY;
public float Advance;
}
private const int SheetWidth = 1024;
private const int SheetHeight = 512;
private int ScreenWidth, ScreenHeight;
private int CharacterTextureSheet;
private CharacterInfo[] characters;
public Color fontColor { get; set; } = Color.Black;
private string GetFontPath()
{
string fontFolder = Environment.GetFolderPath(Environment.SpecialFolder.Fonts);
// Only uses Arial, add more fonts here if wanted
string path = Path.Combine(fontFolder, "arial.ttf");
if (File.Exists(path))
{
return path;
}
throw new Exception($"Profiler exception. Required font Courier New or Arial not installed to {fontFolder}");
}
public void InitializeTextures()
{
// Create and init some vars
uint[] rawCharacterSheet = new uint[SheetWidth * SheetHeight];
int x;
int y;
int lineOffset;
int maxHeight;
x = y = lineOffset = maxHeight = 0;
characters = new CharacterInfo[94];
// Get font
var font = new FontFace(File.OpenRead(GetFontPath()));
// Update raw data for each character
for (int i = 0; i < 94; i++)
{
var surface = RenderSurface((char)(i + 33), font, out float xBearing, out float yBearing, out float advance);
characters[i] = UpdateTexture(surface, ref rawCharacterSheet, ref x, ref y, ref lineOffset);
characters[i].BearingX = xBearing;
characters[i].BearingY = yBearing;
characters[i].Advance = advance;
if (maxHeight < characters[i].Height)
maxHeight = (int)characters[i].Height;
}
// Fix height for characters shorter than line height
for (int i = 0; i < 94; i++)
{
characters[i].BearingX /= characters[i].Width;
characters[i].BearingY /= maxHeight;
characters[i].Advance /= characters[i].Width;
characters[i].Height /= maxHeight;
characters[i].AspectRatio = (float)characters[i].Width / maxHeight;
}
// Convert raw data into texture
CharacterTextureSheet = GL.GenTexture();
GL.BindTexture(TextureTarget.Texture2D, CharacterTextureSheet);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)TextureWrapMode.Clamp);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)TextureWrapMode.Clamp);
GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba, SheetWidth, SheetHeight, 0, PixelFormat.Rgba, PixelType.UnsignedInt8888, rawCharacterSheet);
GL.BindTexture(TextureTarget.Texture2D, 0);
}
public void UpdateScreenHeight(int height)
{
ScreenHeight = height;
}
public float DrawText(string text, float x, float y, float height, bool draw = true)
{
float originalX = x;
// Skip out of bounds draw
if (y < height * -2 || y > ScreenHeight + height * 2)
{
draw = false;
}
if (draw)
{
// Use font map texture
GL.BindTexture(TextureTarget.Texture2D, CharacterTextureSheet);
// Enable blending and textures
GL.Enable(EnableCap.Texture2D);
GL.Enable(EnableCap.Blend);
GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
// Draw all characters
GL.Begin(PrimitiveType.Triangles);
GL.Color4(fontColor);
}
for (int i = 0; i < text.Length; i++)
{
if (text[i] == ' ')
{
x += height / 4;
continue;
}
CharacterInfo charInfo = characters[text[i] - 33];
float width = (charInfo.AspectRatio * height);
x += (charInfo.BearingX * charInfo.AspectRatio) * width;
float right = x + width;
if (draw)
{
DrawChar(charInfo, x, right, y + height * (charInfo.Height - charInfo.BearingY), y - height * charInfo.BearingY);
}
x = right + charInfo.Advance * charInfo.AspectRatio + 1;
}
if (draw)
{
GL.End();
// Cleanup for caller
GL.BindTexture(TextureTarget.Texture2D, 0);
GL.Disable(EnableCap.Texture2D);
GL.Disable(EnableCap.Blend);
}
// Return width of rendered text
return x - originalX;
}
private void DrawChar(CharacterInfo charInfo, float left, float right, float top, float bottom)
{
GL.TexCoord2(charInfo.Left, charInfo.Bottom); GL.Vertex2(left, bottom);
GL.TexCoord2(charInfo.Left, charInfo.Top); GL.Vertex2(left, top);
GL.TexCoord2(charInfo.Right, charInfo.Top); GL.Vertex2(right, top);
GL.TexCoord2(charInfo.Right, charInfo.Top); GL.Vertex2(right, top);
GL.TexCoord2(charInfo.Right, charInfo.Bottom); GL.Vertex2(right, bottom);
GL.TexCoord2(charInfo.Left, charInfo.Bottom); GL.Vertex2(left, bottom);
}
public unsafe Surface RenderSurface(char c, FontFace font, out float xBearing, out float yBearing, out float advance)
{
var glyph = font.GetGlyph(c, 64);
xBearing = glyph.HorizontalMetrics.Bearing.X;
yBearing = glyph.RenderHeight - glyph.HorizontalMetrics.Bearing.Y;
advance = glyph.HorizontalMetrics.Advance;
var surface = new Surface
{
Bits = Marshal.AllocHGlobal(glyph.RenderWidth * glyph.RenderHeight),
Width = glyph.RenderWidth,
Height = glyph.RenderHeight,
Pitch = glyph.RenderWidth
};
var stuff = (byte*)surface.Bits;
for (int i = 0; i < surface.Width * surface.Height; i++)
*stuff++ = 0;
glyph.RenderTo(surface);
return surface;
}
private CharacterInfo UpdateTexture(Surface surface, ref uint[] rawCharMap, ref int posX, ref int posY, ref int lineOffset)
{
int width = surface.Width;
int height = surface.Height;
int len = width * height;
byte[] data = new byte[len];
// Get character bitmap
Marshal.Copy(surface.Bits, data, 0, len);
// Find a slot
if (posX + width > SheetWidth)
{
posX = 0;
posY += lineOffset;
lineOffset = 0;
}
// Update lineOffset
if (lineOffset < height)
{
lineOffset = height + 1;
}
// Copy char to sheet
for (int y = 0; y < height; y++)
{
int destOffset = (y + posY) * SheetWidth + posX;
int sourceOffset = y * width;
for (int x = 0; x < width; x++)
{
rawCharMap[destOffset + x] = (uint)((0xFFFFFF << 8) | data[sourceOffset + x]);
}
}
// Generate character info
CharacterInfo charInfo = new CharacterInfo()
{
Left = (float)posX / SheetWidth,
Right = (float)(posX + width) / SheetWidth,
Top = (float)(posY - 1) / SheetHeight,
Bottom = (float)(posY + height) / SheetHeight,
Width = width,
Height = height,
};
// Update x
posX += width + 1;
// Give the memory back
Marshal.FreeHGlobal(surface.Bits);
return charInfo;
}
}
}

View file

@ -12,12 +12,12 @@
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Release|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Release|AnyCPU'">
<DefineConstants>TRACE;USE_PROFILING</DefineConstants> <DefineConstants>TRACE;USE_DEBUGGING</DefineConstants>
<Optimize>true</Optimize> <Optimize>true</Optimize>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Debug|AnyCPU'">
<DefineConstants>TRACE;USE_PROFILING</DefineConstants> <DefineConstants>TRACE;USE_DEBUGGING</DefineConstants>
<Optimize>false</Optimize> <Optimize>false</Optimize>
</PropertyGroup> </PropertyGroup>

View file

@ -12,12 +12,12 @@
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Release|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Release|AnyCPU'">
<DefineConstants>TRACE;USE_PROFILING</DefineConstants> <DefineConstants>TRACE;USE_DEBUGGING</DefineConstants>
<Optimize>true</Optimize> <Optimize>true</Optimize>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Debug|AnyCPU'">
<DefineConstants>TRACE;USE_PROFILING</DefineConstants> <DefineConstants>TRACE;USE_DEBUGGING</DefineConstants>
<Optimize>false</Optimize> <Optimize>false</Optimize>
</PropertyGroup> </PropertyGroup>

View file

@ -17,12 +17,12 @@
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Release|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Release|AnyCPU'">
<DefineConstants>TRACE;USE_PROFILING</DefineConstants> <DefineConstants>TRACE;USE_DEBUGGING</DefineConstants>
<Optimize>true</Optimize> <Optimize>true</Optimize>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Debug|AnyCPU'">
<DefineConstants>TRACE;USE_PROFILING</DefineConstants> <DefineConstants>TRACE;USE_DEBUGGING</DefineConstants>
<Optimize>false</Optimize> <Optimize>false</Optimize>
</PropertyGroup> </PropertyGroup>

View file

@ -10,9 +10,6 @@ EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Tests.Unicorn", "Ryujinx.Tests.Unicorn\Ryujinx.Tests.Unicorn.csproj", "{D8F72938-78EF-4E8C-BAFE-531C9C3C8F15}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Tests.Unicorn", "Ryujinx.Tests.Unicorn\Ryujinx.Tests.Unicorn.csproj", "{D8F72938-78EF-4E8C-BAFE-531C9C3C8F15}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.HLE", "Ryujinx.HLE\Ryujinx.HLE.csproj", "{CB92CFF9-1D62-4D4F-9E88-8130EF61E351}" 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 EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Audio", "Ryujinx.Audio\Ryujinx.Audio.csproj", "{5C1D818E-682A-46A5-9D54-30006E26C270}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Audio", "Ryujinx.Audio\Ryujinx.Audio.csproj", "{5C1D818E-682A-46A5-9D54-30006E26C270}"
EndProject EndProject
@ -22,8 +19,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Luea", "Ryujinx.LLE\Luea.cs
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Common", "Ryujinx.Common\Ryujinx.Common.csproj", "{5FD4E4F6-8928-4B3C-BE07-28A675C17226}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Common", "Ryujinx.Common\Ryujinx.Common.csproj", "{5FD4E4F6-8928-4B3C-BE07-28A675C17226}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Profiler", "Ryujinx.Profiler\Ryujinx.Profiler.csproj", "{4E69B67F-8CA7-42CF-A9E1-CCB0915DFB34}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ARMeilleure", "ARMeilleure\ARMeilleure.csproj", "{ABF09A5E-2D8B-4B6F-A51D-5CE414DDB15A}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ARMeilleure", "ARMeilleure\ARMeilleure.csproj", "{ABF09A5E-2D8B-4B6F-A51D-5CE414DDB15A}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.Gpu", "Ryujinx.Graphics.Gpu\Ryujinx.Graphics.Gpu.csproj", "{ADA7EA87-0D63-4D97-9433-922A2124401F}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.Gpu", "Ryujinx.Graphics.Gpu\Ryujinx.Graphics.Gpu.csproj", "{ADA7EA87-0D63-4D97-9433-922A2124401F}"
@ -37,6 +32,7 @@ EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.Shader", "Ryujinx.Graphics.Shader\Ryujinx.Graphics.Shader.csproj", "{03B955CD-AD84-4B93-AAA7-BF17923BBAA5}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.Shader", "Ryujinx.Graphics.Shader\Ryujinx.Graphics.Shader.csproj", "{03B955CD-AD84-4B93-AAA7-BF17923BBAA5}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ryujinx.Graphics.Nvdec", "Ryujinx.Graphics.Nvdec\Ryujinx.Graphics.Nvdec.csproj", "{85A0FA56-DC01-4A42-8808-70DAC76BD66D}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ryujinx.Graphics.Nvdec", "Ryujinx.Graphics.Nvdec\Ryujinx.Graphics.Nvdec.csproj", "{85A0FA56-DC01-4A42-8808-70DAC76BD66D}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Debugger", "Ryujinx.Debugger\Ryujinx.Debugger.csproj", "{2E02B7F3-245E-43B1-AE5B-44167A0FDA20}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -110,14 +106,6 @@ Global
{5FD4E4F6-8928-4B3C-BE07-28A675C17226}.Profile Release|Any CPU.Build.0 = Profile Release|Any CPU {5FD4E4F6-8928-4B3C-BE07-28A675C17226}.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.ActiveCfg = Release|Any CPU
{5FD4E4F6-8928-4B3C-BE07-28A675C17226}.Release|Any CPU.Build.0 = Release|Any CPU {5FD4E4F6-8928-4B3C-BE07-28A675C17226}.Release|Any CPU.Build.0 = Release|Any CPU
{4E69B67F-8CA7-42CF-A9E1-CCB0915DFB34}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4E69B67F-8CA7-42CF-A9E1-CCB0915DFB34}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4E69B67F-8CA7-42CF-A9E1-CCB0915DFB34}.Profile Debug|Any CPU.ActiveCfg = Profile Debug|Any CPU
{4E69B67F-8CA7-42CF-A9E1-CCB0915DFB34}.Profile Debug|Any CPU.Build.0 = Profile Debug|Any CPU
{4E69B67F-8CA7-42CF-A9E1-CCB0915DFB34}.Profile Release|Any CPU.ActiveCfg = Profile Release|Any CPU
{4E69B67F-8CA7-42CF-A9E1-CCB0915DFB34}.Profile Release|Any CPU.Build.0 = Profile Release|Any CPU
{4E69B67F-8CA7-42CF-A9E1-CCB0915DFB34}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4E69B67F-8CA7-42CF-A9E1-CCB0915DFB34}.Release|Any CPU.Build.0 = Release|Any CPU
{ABF09A5E-2D8B-4B6F-A51D-5CE414DDB15A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {ABF09A5E-2D8B-4B6F-A51D-5CE414DDB15A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{ABF09A5E-2D8B-4B6F-A51D-5CE414DDB15A}.Debug|Any CPU.Build.0 = Debug|Any CPU {ABF09A5E-2D8B-4B6F-A51D-5CE414DDB15A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{ABF09A5E-2D8B-4B6F-A51D-5CE414DDB15A}.Profile Debug|Any CPU.ActiveCfg = Debug|Any CPU {ABF09A5E-2D8B-4B6F-A51D-5CE414DDB15A}.Profile Debug|Any CPU.ActiveCfg = Debug|Any CPU
@ -174,6 +162,14 @@ Global
{85A0FA56-DC01-4A42-8808-70DAC76BD66D}.Profile Release|Any CPU.Build.0 = Release|Any CPU {85A0FA56-DC01-4A42-8808-70DAC76BD66D}.Profile Release|Any CPU.Build.0 = Release|Any CPU
{85A0FA56-DC01-4A42-8808-70DAC76BD66D}.Release|Any CPU.ActiveCfg = Release|Any CPU {85A0FA56-DC01-4A42-8808-70DAC76BD66D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{85A0FA56-DC01-4A42-8808-70DAC76BD66D}.Release|Any CPU.Build.0 = Release|Any CPU {85A0FA56-DC01-4A42-8808-70DAC76BD66D}.Release|Any CPU.Build.0 = Release|Any CPU
{2E02B7F3-245E-43B1-AE5B-44167A0FDA20}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2E02B7F3-245E-43B1-AE5B-44167A0FDA20}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2E02B7F3-245E-43B1-AE5B-44167A0FDA20}.Profile Debug|Any CPU.ActiveCfg = Profile Debug|Any CPU
{2E02B7F3-245E-43B1-AE5B-44167A0FDA20}.Profile Debug|Any CPU.Build.0 = Profile Debug|Any CPU
{2E02B7F3-245E-43B1-AE5B-44167A0FDA20}.Profile Release|Any CPU.ActiveCfg = Profile Release|Any CPU
{2E02B7F3-245E-43B1-AE5B-44167A0FDA20}.Profile Release|Any CPU.Build.0 = Profile Release|Any CPU
{2E02B7F3-245E-43B1-AE5B-44167A0FDA20}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2E02B7F3-245E-43B1-AE5B-44167A0FDA20}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

View file

@ -1,7 +1,7 @@
using Gtk; using Gtk;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Configuration; using Ryujinx.Configuration;
using Ryujinx.Profiler; using Ryujinx.Debugger.Profiler;
using Ryujinx.Ui; using Ryujinx.Ui;
using System; using System;
using System.IO; using System.IO;

View file

@ -9,12 +9,12 @@
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Release|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Release|AnyCPU'">
<DefineConstants>TRACE;USE_PROFILING</DefineConstants> <DefineConstants>TRACE;USE_DEBUGGING</DefineConstants>
<Optimize>true</Optimize> <Optimize>true</Optimize>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Debug|AnyCPU'">
<DefineConstants>TRACE;USE_PROFILING</DefineConstants> <DefineConstants>TRACE;USE_DEBUGGING</DefineConstants>
<Optimize>false</Optimize> <Optimize>false</Optimize>
</PropertyGroup> </PropertyGroup>
@ -72,7 +72,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="DiscordRichPresence" Version="1.0.147" /> <PackageReference Include="DiscordRichPresence" Version="1.0.147" />
<PackageReference Include="GtkSharp" Version="3.22.25.24" /> <PackageReference Include="GtkSharp" Version="3.22.25.56" />
<PackageReference Include="GtkSharp.Dependencies" Version="1.1.0" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'osx-x64'" /> <PackageReference Include="GtkSharp.Dependencies" Version="1.1.0" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'osx-x64'" />
<PackageReference Include="OpenTK.NetStandard" Version="1.0.4" /> <PackageReference Include="OpenTK.NetStandard" Version="1.0.4" />
</ItemGroup> </ItemGroup>
@ -80,8 +80,8 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Ryujinx.Audio\Ryujinx.Audio.csproj" /> <ProjectReference Include="..\Ryujinx.Audio\Ryujinx.Audio.csproj" />
<ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" /> <ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" />
<ProjectReference Include="..\Ryujinx.Debugger\Ryujinx.Debugger.csproj" />
<ProjectReference Include="..\Ryujinx.HLE\Ryujinx.HLE.csproj" /> <ProjectReference Include="..\Ryujinx.HLE\Ryujinx.HLE.csproj" />
<ProjectReference Include="..\Ryujinx.Profiler\Ryujinx.Profiler.csproj" />
<ProjectReference Include="..\ARMeilleure\ARMeilleure.csproj" /> <ProjectReference Include="..\ARMeilleure\ARMeilleure.csproj" />
<ProjectReference Include="..\Ryujinx.Graphics.OpenGL\Ryujinx.Graphics.OpenGL.csproj" /> <ProjectReference Include="..\Ryujinx.Graphics.OpenGL\Ryujinx.Graphics.OpenGL.csproj" />
<ProjectReference Include="..\Ryujinx.Graphics.Gpu\Ryujinx.Graphics.Gpu.csproj" /> <ProjectReference Include="..\Ryujinx.Graphics.Gpu\Ryujinx.Graphics.Gpu.csproj" />

View file

@ -5,8 +5,6 @@ using Ryujinx.Configuration;
using Ryujinx.Graphics.OpenGL; using Ryujinx.Graphics.OpenGL;
using Ryujinx.HLE; using Ryujinx.HLE;
using Ryujinx.HLE.Input; using Ryujinx.HLE.Input;
using Ryujinx.Profiler.UI;
using Ryujinx.Ui;
using System; using System;
using System.Threading; using System.Threading;
@ -41,10 +39,6 @@ namespace Ryujinx.Ui
private string _newTitle; private string _newTitle;
#if USE_PROFILING
private ProfileWindowManager _profileWindow;
#endif
public GlScreen(Switch device) public GlScreen(Switch device)
: base(1280, 720, : base(1280, 720,
new GraphicsMode(), "Ryujinx", 0, new GraphicsMode(), "Ryujinx", 0,
@ -65,11 +59,6 @@ namespace Ryujinx.Ui
Location = new Point( Location = new Point(
(DisplayDevice.Default.Width / 2) - (Width / 2), (DisplayDevice.Default.Width / 2) - (Width / 2),
(DisplayDevice.Default.Height / 2) - (Height / 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() private void RenderLoop()
@ -171,11 +160,6 @@ namespace Ryujinx.Ui
{ {
KeyboardState keyboard = _keyboard.Value; 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 // Normal Input
currentHotkeyButtons = KeyboardControls.GetHotkeyButtons(ConfigurationState.Instance.Hid.KeyboardControls, keyboard); currentHotkeyButtons = KeyboardControls.GetHotkeyButtons(ConfigurationState.Instance.Hid.KeyboardControls, keyboard);
currentButton = KeyboardControls.GetButtons(ConfigurationState.Instance.Hid.KeyboardControls, keyboard); currentButton = KeyboardControls.GetButtons(ConfigurationState.Instance.Hid.KeyboardControls, keyboard);
@ -330,10 +314,6 @@ namespace Ryujinx.Ui
protected override void OnUnload(EventArgs e) protected override void OnUnload(EventArgs e)
{ {
#if USE_PROFILING
_profileWindow.Close();
#endif
_renderThread.Join(); _renderThread.Join();
base.OnUnload(e); base.OnUnload(e);

View file

@ -3,11 +3,12 @@ using JsonPrettyPrinterPlus;
using Ryujinx.Audio; using Ryujinx.Audio;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Configuration; using Ryujinx.Configuration;
using Ryujinx.Debugger.Profiler;
using Ryujinx.Graphics.GAL; using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.OpenGL; using Ryujinx.Graphics.OpenGL;
using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.FileSystem.Content; using Ryujinx.HLE.FileSystem.Content;
using Ryujinx.Profiler; using Ryujinx.HLE.FileSystem;
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
@ -36,9 +37,12 @@ namespace Ryujinx.Ui
private static bool _updatingGameTable; private static bool _updatingGameTable;
private static bool _gameLoaded; private static bool _gameLoaded;
private static bool _ending; private static bool _ending;
private static bool _debuggerOpened;
private static TreeView _treeView; private static TreeView _treeView;
private static Debugger.Debugger _debugger;
#pragma warning disable CS0649 #pragma warning disable CS0649
#pragma warning disable IDE0044 #pragma warning disable IDE0044
[GUI] Window _mainWin; [GUI] Window _mainWin;
@ -61,6 +65,8 @@ namespace Ryujinx.Ui
[GUI] Label _progressLabel; [GUI] Label _progressLabel;
[GUI] Label _firmwareVersionLabel; [GUI] Label _firmwareVersionLabel;
[GUI] LevelBar _progressBar; [GUI] LevelBar _progressBar;
[GUI] MenuItem _openDebugger;
[GUI] MenuItem _toolsMenu;
#pragma warning restore CS0649 #pragma warning restore CS0649
#pragma warning restore IDE0044 #pragma warning restore IDE0044
@ -118,6 +124,13 @@ namespace Ryujinx.Ui
if (ConfigurationState.Instance.Ui.GuiColumns.FileSizeColumn) _fileSizeToggle.Active = true; if (ConfigurationState.Instance.Ui.GuiColumns.FileSizeColumn) _fileSizeToggle.Active = true;
if (ConfigurationState.Instance.Ui.GuiColumns.PathColumn) _pathToggle.Active = true; if (ConfigurationState.Instance.Ui.GuiColumns.PathColumn) _pathToggle.Active = true;
#if USE_DEBUGGING
_debugger = new Debugger.Debugger();
_openDebugger.Activated += _openDebugger_Opened;
#else
_openDebugger.Visible = false;
#endif
_gameTable.Model = _tableStore = new ListStore( _gameTable.Model = _tableStore = new ListStore(
typeof(bool), typeof(bool),
typeof(Gdk.Pixbuf), typeof(Gdk.Pixbuf),
@ -141,6 +154,36 @@ namespace Ryujinx.Ui
Task.Run(RefreshFirmwareLabel); Task.Run(RefreshFirmwareLabel);
} }
#if USE_DEBUGGING
private void _openDebugger_Opened(object sender, EventArgs e)
{
if (_debuggerOpened)
{
return;
}
Window debugWindow = new Window("Debugger");
debugWindow.SetSizeRequest(1280, 640);
debugWindow.Child = _debugger.Widget;
debugWindow.DeleteEvent += DebugWindow_DeleteEvent;
debugWindow.ShowAll();
_debugger.Enable();
_debuggerOpened = true;
}
private void DebugWindow_DeleteEvent(object o, DeleteEventArgs args)
{
_debuggerOpened = false;
_debugger.Disable();
(_debugger.Widget.Parent as Window)?.Remove(_debugger.Widget);
}
#endif
internal static void ApplyTheme() internal static void ApplyTheme()
{ {
if (!ConfigurationState.Instance.Ui.EnableCustomTheme) if (!ConfigurationState.Instance.Ui.EnableCustomTheme)
@ -307,7 +350,15 @@ namespace Ryujinx.Ui
#if MACOS_BUILD #if MACOS_BUILD
CreateGameWindow(device); CreateGameWindow(device);
#else #else
new Thread(() => CreateGameWindow(device)).Start(); var windowThread = new Thread(() =>
{
CreateGameWindow(device);
})
{
Name = "GUI.WindowThread"
};
windowThread.Start();
#endif #endif
_gameLoaded = true; _gameLoaded = true;
@ -366,6 +417,11 @@ namespace Ryujinx.Ui
private void End(HLE.Switch device) private void End(HLE.Switch device)
{ {
#if USE_DEBUGGING
_debugger.Dispose();
#endif
if (_ending) if (_ending)
{ {
return; return;

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.21.0 --> <!-- Generated with glade 3.22.1 -->
<interface> <interface>
<requires lib="gtk+" version="3.20"/> <requires lib="gtk+" version="3.20"/>
<object class="GtkApplicationWindow" id="_mainWin"> <object class="GtkApplicationWindow" id="_mainWin">
@ -8,6 +8,9 @@
<property name="window_position">center</property> <property name="window_position">center</property>
<property name="default_width">1280</property> <property name="default_width">1280</property>
<property name="default_height">750</property> <property name="default_height">750</property>
<child type="titlebar">
<placeholder/>
</child>
<child> <child>
<object class="GtkBox" id="_box"> <object class="GtkBox" id="_box">
<property name="visible">True</property> <property name="visible">True</property>
@ -255,7 +258,7 @@
</object> </object>
</child> </child>
<child> <child>
<object class="GtkMenuItem" id="ToolsMenu"> <object class="GtkMenuItem" id="_toolsMenu">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="label" translatable="yes">Tools</property> <property name="label" translatable="yes">Tools</property>
@ -296,6 +299,14 @@
</child> </child>
</object> </object>
</child> </child>
<child>
<object class="GtkMenuItem" id="_openDebugger">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Open Debugger</property>
<property name="use_underline">True</property>
</object>
</child>
</object> </object>
</child> </child>
</object> </object>
@ -499,8 +510,5 @@
</child> </child>
</object> </object>
</child> </child>
<child type="titlebar">
<placeholder/>
</child>
</object> </object>
</interface> </interface>