diff options
Diffstat (limited to 'Ryujinx.Profiler/UI')
| -rw-r--r-- | Ryujinx.Profiler/UI/ProfileButton.cs | 110 | ||||
| -rw-r--r-- | Ryujinx.Profiler/UI/ProfileSorters.cs | 33 | ||||
| -rw-r--r-- | Ryujinx.Profiler/UI/ProfileWindow.cs | 773 | ||||
| -rw-r--r-- | Ryujinx.Profiler/UI/ProfileWindowBars.cs | 85 | ||||
| -rw-r--r-- | Ryujinx.Profiler/UI/ProfileWindowGraph.cs | 151 | ||||
| -rw-r--r-- | Ryujinx.Profiler/UI/ProfileWindowManager.cs | 90 | ||||
| -rw-r--r-- | Ryujinx.Profiler/UI/SharpFontHelpers/FontService.cs | 257 |
7 files changed, 1499 insertions, 0 deletions
diff --git a/Ryujinx.Profiler/UI/ProfileButton.cs b/Ryujinx.Profiler/UI/ProfileButton.cs new file mode 100644 index 00000000..7e2ae728 --- /dev/null +++ b/Ryujinx.Profiler/UI/ProfileButton.cs @@ -0,0 +1,110 @@ +using System; +using OpenTK; +using OpenTK.Graphics.OpenGL; +using Ryujinx.Profiler.UI.SharpFontHelpers; + +namespace Ryujinx.Profiler.UI +{ + public class ProfileButton + { + // Store font service + private FontService _fontService; + + // Layout information + private int _left, _right; + private int _bottom, _top; + private int _height; + private int _padding; + + // Label information + private int _labelX, _labelY; + private string _label; + + // Misc + private Action _clicked; + private bool _visible; + + public ProfileButton(FontService fontService, Action clicked) + : this(fontService, clicked, 0, 0, 0, 0, 0) + { + _visible = false; + } + + public ProfileButton(FontService fontService, Action clicked, int x, int y, int padding, int height, int width) + : this(fontService, "", clicked, x, y, padding, height, width) + { + _visible = false; + } + + public ProfileButton(FontService fontService, string label, Action clicked, int x, int y, int padding, int height, int width = -1) + { + _fontService = fontService; + _clicked = clicked; + + UpdateSize(label, x, y, padding, height, width); + } + + public int UpdateSize(string label, int x, int y, int padding, int height, int width = -1) + { + _visible = true; + _label = label; + + if (width == -1) + { + // Dummy draw to measure size + width = (int)_fontService.DrawText(label, 0, 0, height, false); + } + + UpdateSize(x, y, padding, width, height); + + return _right - _left; + } + + public void UpdateSize(int x, int y, int padding, int width, int height) + { + _height = height; + _left = x; + _bottom = y; + _labelX = x + padding / 2; + _labelY = y + padding / 2; + _top = y + height + padding; + _right = x + width + padding; + } + + public void Draw() + { + if (!_visible) + { + return; + } + + // Draw backing rectangle + GL.Begin(PrimitiveType.Triangles); + GL.Color3(Color.Black); + GL.Vertex2(_left, _bottom); + GL.Vertex2(_left, _top); + GL.Vertex2(_right, _top); + + GL.Vertex2(_right, _top); + GL.Vertex2(_right, _bottom); + GL.Vertex2(_left, _bottom); + GL.End(); + + // Use font service to draw label + _fontService.DrawText(_label, _labelX, _labelY, _height); + } + + public bool ProcessClick(int x, int y) + { + // If button contains x, y + if (x > _left && x < _right && + y > _bottom && y < _top) + { + _clicked(); + return true; + } + + return false; + } + } +} diff --git a/Ryujinx.Profiler/UI/ProfileSorters.cs b/Ryujinx.Profiler/UI/ProfileSorters.cs new file mode 100644 index 00000000..2d06f426 --- /dev/null +++ b/Ryujinx.Profiler/UI/ProfileSorters.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Ryujinx.Profiler.UI +{ + public static class ProfileSorters + { + public class InstantAscending : IComparer<KeyValuePair<ProfileConfig, TimingInfo>> + { + public int Compare(KeyValuePair<ProfileConfig, TimingInfo> pair1, KeyValuePair<ProfileConfig, TimingInfo> pair2) + => pair2.Value.Instant.CompareTo(pair1.Value.Instant); + } + + public class AverageAscending : IComparer<KeyValuePair<ProfileConfig, TimingInfo>> + { + public int Compare(KeyValuePair<ProfileConfig, TimingInfo> pair1, KeyValuePair<ProfileConfig, TimingInfo> pair2) + => pair2.Value.AverageTime.CompareTo(pair1.Value.AverageTime); + } + + public class TotalAscending : IComparer<KeyValuePair<ProfileConfig, TimingInfo>> + { + public int Compare(KeyValuePair<ProfileConfig, TimingInfo> pair1, KeyValuePair<ProfileConfig, TimingInfo> pair2) + => pair2.Value.TotalTime.CompareTo(pair1.Value.TotalTime); + } + + public class TagAscending : IComparer<KeyValuePair<ProfileConfig, TimingInfo>> + { + public int Compare(KeyValuePair<ProfileConfig, TimingInfo> pair1, KeyValuePair<ProfileConfig, TimingInfo> pair2) + => StringComparer.CurrentCulture.Compare(pair1.Key.Search, pair2.Key.Search); + } + } +} diff --git a/Ryujinx.Profiler/UI/ProfileWindow.cs b/Ryujinx.Profiler/UI/ProfileWindow.cs new file mode 100644 index 00000000..c58b9235 --- /dev/null +++ b/Ryujinx.Profiler/UI/ProfileWindow.cs @@ -0,0 +1,773 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text.RegularExpressions; +using OpenTK; +using OpenTK.Graphics; +using OpenTK.Graphics.OpenGL; +using OpenTK.Input; +using Ryujinx.Common; +using Ryujinx.Profiler.UI.SharpFontHelpers; + +namespace Ryujinx.Profiler.UI +{ + public partial class ProfileWindow : GameWindow + { + // List all buttons for index in button array + private enum ButtonIndex + { + TagTitle = 0, + InstantTitle = 1, + AverageTitle = 2, + TotalTitle = 3, + FilterBar = 4, + ShowHideInactive = 5, + Pause = 6, + ChangeDisplay = 7, + + // Don't automatically draw after here + ToggleFlags = 8, + Step = 9, + + // Update this when new buttons are added. + // These are indexes to the enum list + Autodraw = 8, + Count = 10, + } + + // Font service + private FontService _fontService; + + // UI variables + private ProfileButton[] _buttons; + + private bool _initComplete = false; + private bool _visible = true; + private bool _visibleChanged = true; + private bool _viewportUpdated = true; + private bool _redrawPending = true; + private bool _displayGraph = true; + private bool _displayFlags = true; + private bool _showInactive = true; + private bool _paused = false; + private bool _doStep = false; + + // Layout + private const int LineHeight = 16; + private const int TitleHeight = 24; + private const int TitleFontHeight = 16; + private const int LinePadding = 2; + private const int ColumnSpacing = 15; + private const int FilterHeight = 24; + private const int BottomBarHeight = FilterHeight + LineHeight; + + // Sorting + private List<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() + // Graphigs mode enables 2xAA + : base(1280, 720, new GraphicsMode(new ColorFormat(8, 8, 8, 8), 1, 1, 2)) + { + Title = "Profiler"; + Location = new Point(DisplayDevice.Default.Width - 1280, + (DisplayDevice.Default.Height - 720) - 50); + + if (Profile.UpdateRate <= 0) + { + // Perform step regardless of flag type + Profile.RegisterFlagReciever((t) => + { + if (!_paused) + { + _doStep = true; + } + }); + } + + // Large number to force an update on first update + _updateTimer = 0xFFFF; + + Init(); + + // Release context for render thread + Context.MakeCurrent(null); + } + + public void ToggleVisible() + { + _visible = !_visible; + _visibleChanged = true; + } + + private void SetSort(IComparer<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.InitalizeTextures(); + _fontService.UpdateScreenHeight(Height); + + _buttons = new ProfileButton[(int)ButtonIndex.Count]; + _buttons[(int)ButtonIndex.TagTitle] = new ProfileButton(_fontService, () => SetSort(new ProfileSorters.TagAscending())); + _buttons[(int)ButtonIndex.InstantTitle] = new ProfileButton(_fontService, () => SetSort(new ProfileSorters.InstantAscending())); + _buttons[(int)ButtonIndex.AverageTitle] = new ProfileButton(_fontService, () => SetSort(new ProfileSorters.AverageAscending())); + _buttons[(int)ButtonIndex.TotalTitle] = new ProfileButton(_fontService, () => SetSort(new ProfileSorters.TotalAscending())); + _buttons[(int)ButtonIndex.Step] = new ProfileButton(_fontService, () => _doStep = true); + _buttons[(int)ButtonIndex.FilterBar] = new ProfileButton(_fontService, () => + { + _profileUpdated = true; + _regexEnabled = !_regexEnabled; + }); + + _buttons[(int)ButtonIndex.ShowHideInactive] = new ProfileButton(_fontService, () => + { + _profileUpdated = true; + _showInactive = !_showInactive; + }); + + _buttons[(int)ButtonIndex.Pause] = new ProfileButton(_fontService, () => + { + _profileUpdated = true; + _paused = !_paused; + }); + + _buttons[(int)ButtonIndex.ToggleFlags] = new ProfileButton(_fontService, () => + { + _displayFlags = !_displayFlags; + _redrawPending = true; + }); + + _buttons[(int)ButtonIndex.ChangeDisplay] = new ProfileButton(_fontService, () => + { + _displayGraph = !_displayGraph; + _redrawPending = true; + }); + + Visible = _visible; + } +#endregion + +#region OnResize + /// <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; + } + } +}
\ No newline at end of file diff --git a/Ryujinx.Profiler/UI/ProfileWindowBars.cs b/Ryujinx.Profiler/UI/ProfileWindowBars.cs new file mode 100644 index 00000000..b1955a07 --- /dev/null +++ b/Ryujinx.Profiler/UI/ProfileWindowBars.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using OpenTK; +using OpenTK.Graphics.OpenGL; + +namespace Ryujinx.Profiler.UI +{ + public partial class ProfileWindow + { + private void DrawBars(float xOffset, float yOffset, float width) + { + if (_sortedProfileData.Count != 0) + { + long maxAverage; + long maxTotal; + + int verticalIndex = 0; + float barHeight = (LineHeight - LinePadding) / 3.0f; + + // Get max values + var maxInstant = maxAverage = maxTotal = 0; + foreach (KeyValuePair<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); + } + } + } +} diff --git a/Ryujinx.Profiler/UI/ProfileWindowGraph.cs b/Ryujinx.Profiler/UI/ProfileWindowGraph.cs new file mode 100644 index 00000000..9d34be97 --- /dev/null +++ b/Ryujinx.Profiler/UI/ProfileWindowGraph.cs @@ -0,0 +1,151 @@ +using System; +using OpenTK; +using OpenTK.Graphics.OpenGL; +using Ryujinx.Common; + +namespace Ryujinx.Profiler.UI +{ + public partial class ProfileWindow + { + // Colour index equal to timing flag type as int + private Color[] _timingFlagColours = new[] + { + new Color(150, 25, 25, 50), // FrameSwap = 0 + new Color(25, 25, 150, 50), // SystemFrame = 1 + }; + + private TimingFlag[] _timingFlags; + + private const float GraphMoveSpeed = 40000; + private const float GraphZoomSpeed = 50; + + private float _graphZoom = 1; + private float _graphPosition = 0; + + private void DrawGraph(float xOffset, float yOffset, float width) + { + if (_sortedProfileData.Count != 0) + { + int left, right; + float top, bottom; + + int verticalIndex = 0; + float graphRight = xOffset + width; + float barHeight = (LineHeight - LinePadding); + long history = Profile.HistoryLength; + double timeWidthTicks = history / (double)_graphZoom; + long graphPositionTicks = (long)(_graphPosition * PerformanceCounter.TicksPerMillisecond); + long ticksPerPixel = (long)(timeWidthTicks / width); + + // Reset start point if out of bounds + if (timeWidthTicks + graphPositionTicks > history) + { + graphPositionTicks = history - (long)timeWidthTicks; + _graphPosition = (float)graphPositionTicks / PerformanceCounter.TicksPerMillisecond; + } + + graphPositionTicks = _captureTime - graphPositionTicks; + + GL.Enable(EnableCap.ScissorTest); + + // Draw timing flags + if (_displayFlags) + { + TimingFlagType prevType = TimingFlagType.Count; + + GL.Enable(EnableCap.Blend); + GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha); + + GL.Begin(PrimitiveType.Lines); + foreach (TimingFlag timingFlag in _timingFlags) + { + if (prevType != timingFlag.FlagType) + { + prevType = timingFlag.FlagType; + GL.Color4(_timingFlagColours[(int)prevType]); + } + + int x = (int)(graphRight - ((graphPositionTicks - timingFlag.Timestamp) / timeWidthTicks) * width); + GL.Vertex2(x, 0); + GL.Vertex2(x, Height); + } + GL.End(); + GL.Disable(EnableCap.Blend); + } + + // Draw bars + GL.Begin(PrimitiveType.Triangles); + foreach (var entry in _sortedProfileData) + { + long furthest = 0; + + bottom = GetLineY(yOffset, LineHeight, LinePadding, true, verticalIndex); + top = bottom + barHeight; + + // Skip rendering out of bounds bars + if (top < 0 || bottom > Height) + { + verticalIndex++; + continue; + } + + + GL.Color3(Color.Green); + foreach (Timestamp timestamp in entry.Value.GetAllTimestamps()) + { + // Skip drawing multiple timestamps on same pixel + if (timestamp.EndTime < furthest) + continue; + furthest = timestamp.EndTime + ticksPerPixel; + + left = (int)(graphRight - ((graphPositionTicks - timestamp.BeginTime) / timeWidthTicks) * width); + right = (int)(graphRight - ((graphPositionTicks - timestamp.EndTime) / timeWidthTicks) * width); + + // Make sure width is at least 1px + right = Math.Max(left + 1, right); + + GL.Vertex2(left, bottom); + GL.Vertex2(left, top); + GL.Vertex2(right, top); + + GL.Vertex2(right, top); + GL.Vertex2(right, bottom); + GL.Vertex2(left, bottom); + } + + // Currently capturing timestamp + GL.Color3(Color.Red); + long entryBegin = entry.Value.BeginTime; + if (entryBegin != -1) + { + left = (int)(graphRight - ((graphPositionTicks - entryBegin) / timeWidthTicks) * width); + + // Make sure width is at least 1px + left = Math.Min(left - 1, (int)graphRight); + + GL.Vertex2(left, bottom); + GL.Vertex2(left, top); + GL.Vertex2(graphRight, top); + + GL.Vertex2(graphRight, top); + GL.Vertex2(graphRight, bottom); + GL.Vertex2(left, bottom); + } + + verticalIndex++; + } + + GL.End(); + GL.Disable(EnableCap.ScissorTest); + + string label = $"-{MathF.Round(_graphPosition, 2)} ms"; + + // Dummy draw for measure + float labelWidth = _fontService.DrawText(label, 0, 0, LineHeight, false); + _fontService.DrawText(label, graphRight - labelWidth - LinePadding, FilterHeight + LinePadding, LineHeight); + + _fontService.DrawText($"-{MathF.Round((float)((timeWidthTicks / PerformanceCounter.TicksPerMillisecond) + _graphPosition), 2)} ms", xOffset + LinePadding, FilterHeight + LinePadding, LineHeight); + } + } + } +} diff --git a/Ryujinx.Profiler/UI/ProfileWindowManager.cs b/Ryujinx.Profiler/UI/ProfileWindowManager.cs new file mode 100644 index 00000000..4ba0c881 --- /dev/null +++ b/Ryujinx.Profiler/UI/ProfileWindowManager.cs @@ -0,0 +1,90 @@ +using System.Diagnostics; +using System.Threading; +using OpenTK; +using OpenTK.Input; +using Ryujinx.Common; + +namespace Ryujinx.Profiler.UI +{ + public class ProfileWindowManager + { + private ProfileWindow _window; + private Thread _profileThread; + private Thread _renderThread; + private bool _profilerRunning; + + // Timing + private double _prevTime; + + public ProfileWindowManager() + { + if (Profile.ProfilingEnabled()) + { + _profilerRunning = true; + _prevTime = 0; + _profileThread = new Thread(ProfileLoop); + _profileThread.Start(); + } + } + + public void ToggleVisible() + { + if (Profile.ProfilingEnabled()) + { + _window.ToggleVisible(); + } + } + + public void Close() + { + if (_window != null) + { + _profilerRunning = false; + _window.Close(); + _window.Dispose(); + } + + _window = null; + } + + public void UpdateKeyInput(KeyboardState keyboard) + { + if (Profile.Controls.TogglePressed(keyboard)) + { + ToggleVisible(); + } + Profile.Controls.SetPrevKeyboardState(keyboard); + } + + private void ProfileLoop() + { + using (_window = new ProfileWindow()) + { + // Create thread for render loop + _renderThread = new Thread(RenderLoop); + _renderThread.Start(); + + while (_profilerRunning) + { + double time = (double)PerformanceCounter.ElapsedTicks / PerformanceCounter.TicksPerSecond; + _window.Update(new FrameEventArgs(time - _prevTime)); + _prevTime = time; + + // Sleep to be less taxing, update usually does very little + Thread.Sleep(1); + } + } + } + + private void RenderLoop() + { + _window.Context.MakeCurrent(_window.WindowInfo); + + while (_profilerRunning) + { + _window.Draw(); + Thread.Sleep(1); + } + } + } +} diff --git a/Ryujinx.Profiler/UI/SharpFontHelpers/FontService.cs b/Ryujinx.Profiler/UI/SharpFontHelpers/FontService.cs new file mode 100644 index 00000000..e64c9da3 --- /dev/null +++ b/Ryujinx.Profiler/UI/SharpFontHelpers/FontService.cs @@ -0,0 +1,257 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using OpenTK; +using OpenTK.Graphics.OpenGL; +using SharpFont; + +namespace Ryujinx.Profiler.UI.SharpFontHelpers +{ + public class FontService + { + private struct CharacterInfo + { + public float Left; + public float Right; + public float Top; + public float Bottom; + + public int Width; + public float Height; + + public float AspectRatio; + + public float BearingX; + public float BearingY; + public float Advance; + } + + private const int SheetWidth = 1024; + private const int SheetHeight = 512; + private int ScreenWidth, ScreenHeight; + private int CharacterTextureSheet; + private CharacterInfo[] characters; + + public Color fontColor { get; set; } = Color.Black; + + private string GetFontPath() + { + string fontFolder = System.Environment.GetFolderPath(Environment.SpecialFolder.Fonts); + + // Only uses Arial, add more fonts here if wanted + string path = Path.Combine(fontFolder, "arial.ttf"); + if (File.Exists(path)) + { + return path; + } + + throw new Exception($"Profiler exception. Required font Courier New or Arial not installed to {fontFolder}"); + } + + public void InitalizeTextures() + { + // Create and init some vars + uint[] rawCharacterSheet = new uint[SheetWidth * SheetHeight]; + int x; + int y; + int lineOffset; + int maxHeight; + + x = y = lineOffset = maxHeight = 0; + characters = new CharacterInfo[94]; + + // Get font + var font = new FontFace(File.OpenRead(GetFontPath())); + + // Update raw data for each character + for (int i = 0; i < 94; i++) + { + var surface = RenderSurface((char)(i + 33), font, out var xBearing, out var yBearing, out var advance); + + characters[i] = UpdateTexture(surface, ref rawCharacterSheet, ref x, ref y, ref lineOffset); + characters[i].BearingX = xBearing; + characters[i].BearingY = yBearing; + characters[i].Advance = advance; + + if (maxHeight < characters[i].Height) + maxHeight = (int)characters[i].Height; + } + + // Fix height for characters shorter than line height + for (int i = 0; i < 94; i++) + { + characters[i].BearingX /= characters[i].Width; + characters[i].BearingY /= maxHeight; + characters[i].Advance /= characters[i].Width; + characters[i].Height /= maxHeight; + characters[i].AspectRatio = (float)characters[i].Width / maxHeight; + } + + // Convert raw data into texture + CharacterTextureSheet = GL.GenTexture(); + GL.BindTexture(TextureTarget.Texture2D, CharacterTextureSheet); + + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear); + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear); + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)TextureWrapMode.Clamp); + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)TextureWrapMode.Clamp); + + GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba, SheetWidth, SheetHeight, 0, PixelFormat.Rgba, PixelType.UnsignedInt8888, rawCharacterSheet); + + GL.BindTexture(TextureTarget.Texture2D, 0); + } + + public void UpdateScreenHeight(int height) + { + ScreenHeight = height; + } + + public float DrawText(string text, float x, float y, float height, bool draw = true) + { + float originalX = x; + + // Skip out of bounds draw + if (y < height * -2 || y > ScreenHeight + height * 2) + { + draw = false; + } + + if (draw) + { + // Use font map texture + GL.BindTexture(TextureTarget.Texture2D, CharacterTextureSheet); + + // Enable blending and textures + GL.Enable(EnableCap.Texture2D); + GL.Enable(EnableCap.Blend); + GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha); + + // Draw all characters + GL.Begin(PrimitiveType.Triangles); + GL.Color4(fontColor); + } + + for (int i = 0; i < text.Length; i++) + { + if (text[i] == ' ') + { + x += height / 4; + continue; + } + + CharacterInfo charInfo = characters[text[i] - 33]; + float width = (charInfo.AspectRatio * height); + x += (charInfo.BearingX * charInfo.AspectRatio) * width; + float right = x + width; + if (draw) + { + DrawChar(charInfo, x, right, y + height * (charInfo.Height - charInfo.BearingY), y - height * charInfo.BearingY); + } + x = right + charInfo.Advance * charInfo.AspectRatio + 1; + } + + if (draw) + { + GL.End(); + + // Cleanup for caller + GL.BindTexture(TextureTarget.Texture2D, 0); + GL.Disable(EnableCap.Texture2D); + GL.Disable(EnableCap.Blend); + } + + // Return width of rendered text + return x - originalX; + } + + private void DrawChar(CharacterInfo charInfo, float left, float right, float top, float bottom) + { + GL.TexCoord2(charInfo.Left, charInfo.Bottom); GL.Vertex2(left, bottom); + GL.TexCoord2(charInfo.Left, charInfo.Top); GL.Vertex2(left, top); + GL.TexCoord2(charInfo.Right, charInfo.Top); GL.Vertex2(right, top); + + GL.TexCoord2(charInfo.Right, charInfo.Top); GL.Vertex2(right, top); + GL.TexCoord2(charInfo.Right, charInfo.Bottom); GL.Vertex2(right, bottom); + GL.TexCoord2(charInfo.Left, charInfo.Bottom); GL.Vertex2(left, bottom); + } + + public unsafe Surface RenderSurface(char c, FontFace font, out float xBearing, out float yBearing, out float advance) + { + var glyph = font.GetGlyph(c, 64); + xBearing = glyph.HorizontalMetrics.Bearing.X; + yBearing = glyph.RenderHeight - glyph.HorizontalMetrics.Bearing.Y; + advance = glyph.HorizontalMetrics.Advance; + + var surface = new Surface + { + Bits = Marshal.AllocHGlobal(glyph.RenderWidth * glyph.RenderHeight), + Width = glyph.RenderWidth, + Height = glyph.RenderHeight, + Pitch = glyph.RenderWidth + }; + + var stuff = (byte*)surface.Bits; + for (int i = 0; i < surface.Width * surface.Height; i++) + *stuff++ = 0; + + glyph.RenderTo(surface); + + return surface; + } + + private CharacterInfo UpdateTexture(Surface surface, ref uint[] rawCharMap, ref int posX, ref int posY, ref int lineOffset) + { + int width = surface.Width; + int height = surface.Height; + int len = width * height; + byte[] data = new byte[len]; + + // Get character bitmap + Marshal.Copy(surface.Bits, data, 0, len); + + // Find a slot + if (posX + width > SheetWidth) + { + posX = 0; + posY += lineOffset; + lineOffset = 0; + } + + // Update lineoffset + if (lineOffset < height) + { + lineOffset = height + 1; + } + + // Copy char to sheet + for (int y = 0; y < height; y++) + { + int destOffset = (y + posY) * SheetWidth + posX; + int sourceOffset = y * width; + + for (int x = 0; x < width; x++) + { + rawCharMap[destOffset + x] = (uint)((0xFFFFFF << 8) | data[sourceOffset + x]); + } + } + + // Generate character info + CharacterInfo charInfo = new CharacterInfo() + { + Left = (float)posX / SheetWidth, + Right = (float)(posX + width) / SheetWidth, + Top = (float)(posY - 1) / SheetHeight, + Bottom = (float)(posY + height) / SheetHeight, + Width = width, + Height = height, + }; + + // Update x + posX += width + 1; + + // Give the memory back + Marshal.FreeHGlobal(surface.Bits); + return charInfo; + } + } +}
\ No newline at end of file |
