Render Profiler in GUI (#854)
* move profiler output to gui * addressed commits, rebased * removed whitespaces
This commit is contained in:
parent
db9f8f999f
commit
f2b9a9c2b0
41 changed files with 1358 additions and 1639 deletions
42
Ryujinx.Debugger/UI/DebuggerWidget.cs
Normal file
42
Ryujinx.Debugger/UI/DebuggerWidget.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
44
Ryujinx.Debugger/UI/DebuggerWidget.glade
Normal file
44
Ryujinx.Debugger/UI/DebuggerWidget.glade
Normal 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>
|
801
Ryujinx.Debugger/UI/ProfilerWidget.cs
Normal file
801
Ryujinx.Debugger/UI/ProfilerWidget.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
232
Ryujinx.Debugger/UI/ProfilerWidget.glade
Normal file
232
Ryujinx.Debugger/UI/ProfilerWidget.glade
Normal 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>
|
23
Ryujinx.Debugger/UI/SkRenderer.cs
Normal file
23
Ryujinx.Debugger/UI/SkRenderer.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue