aboutsummaryrefslogtreecommitdiffziptar.gz
path: root/src/Godosa.Desktop/FrameTimeGraph.cs
blob: 7e59a170d727a4506830bc7f13914a63155fee08 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using Godosa.Core.Diagnostics;

namespace Godosa.Desktop;

/// <summary>
/// Frame-time bar graph (game-loop spec §5): one 1-px bar per frame in the
/// stats window, newest at the right, 0–50 ms scale, dashed guides at 16.7 ms
/// (60 fps) and 33.3 ms (30 fps). Bars are colored relative to the window
/// median (green ≤ 1.5×, yellow ≤ 2×, red = hitch) so ordinary vsync jitter
/// stays green whatever the refresh rate. Drawn by Avalonia, not GL, so it
/// doesn't perturb the GL frames it measures. Presentation only.
/// </summary>
public sealed class FrameTimeGraph : Control
{
    private const float ScaleMs = 50f;
    private const float Ms60 = 1000f / 60f;
    private const float Ms30 = 1000f / 30f;

    private static readonly IBrush Backdrop = new SolidColorBrush(Color.FromArgb(90, 255, 255, 255));
    private static readonly IBrush Good = new SolidColorBrush(Color.FromRgb(80, 200, 120));
    private static readonly IBrush Slow = new SolidColorBrush(Color.FromRgb(230, 200, 60));
    private static readonly IBrush Bad = new SolidColorBrush(Color.FromRgb(230, 70, 60));
    private static readonly IPen Guide = new Pen(new SolidColorBrush(Color.FromArgb(170, 255, 255, 255)), 1, DashStyle.Dash);

    private readonly FrameTimeStats _stats;
    private readonly float[] _buffer;

    public FrameTimeGraph(FrameTimeStats stats)
    {
        _stats = stats;
        _buffer = new float[stats.Capacity];
        Width = stats.Capacity;
        Height = 60;
    }

    public override void Render(DrawingContext context)
    {
        double w = Bounds.Width, h = Bounds.Height;
        context.FillRectangle(Backdrop, new Rect(0, 0, w, h));

        int n = _stats.CopyIntervalsMs(_buffer);
        float median = (float)_stats.Summarize().P50Ms;
        double x0 = w - n;
        for (int i = 0; i < n; i++)
        {
            float ms = _buffer[i];
            double barHeight = Math.Min(ms / ScaleMs, 1f) * h;
            var brush = ms > 2f * median ? Bad : ms > 1.5f * median ? Slow : Good;
            context.FillRectangle(brush, new Rect(x0 + i, h - barHeight, 1, barHeight));
        }

        DrawGuide(context, Ms60, w, h);
        DrawGuide(context, Ms30, w, h);
    }

    private static void DrawGuide(DrawingContext context, float ms, double w, double h)
    {
        double y = h - ms / ScaleMs * h;
        context.DrawLine(Guide, new Point(0, y), new Point(w, y));
    }
}