A game framework written with osu! in mind.
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2// See the LICENCE file in the repository root for full licence text.
3
4using osuTK.Graphics;
5using osuTK.Input;
6using osu.Framework.Allocation;
7using osu.Framework.Graphics.Containers;
8using osu.Framework.Graphics.Primitives;
9using osu.Framework.Graphics.Shapes;
10using osu.Framework.Graphics.Sprites;
11using osu.Framework.Graphics.Textures;
12using osu.Framework.Utils;
13using osu.Framework.Statistics;
14using osu.Framework.Threading;
15using System;
16using System.Buffers;
17using System.Collections.Generic;
18using System.Linq;
19using osu.Framework.Input.Events;
20using osuTK;
21using SixLabors.ImageSharp;
22using SixLabors.ImageSharp.PixelFormats;
23
24namespace osu.Framework.Graphics.Performance
25{
26 internal class FrameStatisticsDisplay : Container, IStateful<FrameStatisticsMode>
27 {
28 internal const int HEIGHT = 100;
29
30 protected const int WIDTH = 800;
31
32 private const int amount_count_steps = 5;
33
34 private const int amount_ms_steps = 5;
35 private const float visible_ms_range = 20;
36 private const float scale = HEIGHT / visible_ms_range;
37
38 private const float alpha_when_active = 0.75f;
39
40 private readonly TimeBar[] timeBars;
41
42 private static readonly Color4[] garbage_collect_colors = { Color4.Green, Color4.Yellow, Color4.Red };
43 private readonly PerformanceMonitor monitor;
44
45 private int currentX;
46
47 private int timeBarIndex => currentX / WIDTH;
48 private int timeBarX => currentX % WIDTH;
49
50 private readonly Container overlayContainer;
51 private readonly Drawable labelText;
52 private readonly Sprite counterBarBackground;
53
54 private readonly Container mainContainer;
55 private readonly Container timeBarsContainer;
56
57 private readonly ArrayPool<Rgba32> uploadPool;
58
59 private readonly Drawable[] legendMapping = new Drawable[FrameStatistics.NUM_PERFORMANCE_COLLECTION_TYPES];
60 private readonly Dictionary<StatisticsCounterType, CounterBar> counterBars = new Dictionary<StatisticsCounterType, CounterBar>();
61
62 private readonly FrameTimeDisplay frameTimeDisplay;
63
64 private FrameStatisticsMode state;
65
66 public event Action<FrameStatisticsMode> StateChanged;
67
68 public FrameStatisticsMode State
69 {
70 get => state;
71 set
72 {
73 if (state == value) return;
74
75 state = value;
76
77 switch (state)
78 {
79 case FrameStatisticsMode.Minimal:
80 mainContainer.AutoSizeAxes = Axes.Both;
81
82 timeBarsContainer.Hide();
83
84 labelText.Origin = Anchor.CentreRight;
85 labelText.Rotation = 0;
86 break;
87
88 case FrameStatisticsMode.Full:
89 mainContainer.AutoSizeAxes = Axes.None;
90 mainContainer.Size = new Vector2(WIDTH, HEIGHT);
91
92 timeBarsContainer.Show();
93
94 labelText.Origin = Anchor.BottomCentre;
95 labelText.Rotation = -90;
96 break;
97 }
98
99 Running = state != FrameStatisticsMode.None;
100 Expanded = false;
101
102 StateChanged?.Invoke(State);
103 }
104 }
105
106 public FrameStatisticsDisplay(GameThread thread, ArrayPool<Rgba32> uploadPool)
107 {
108 Name = thread.Name;
109 monitor = thread.Monitor;
110 this.uploadPool = uploadPool;
111
112 Origin = Anchor.TopRight;
113 AutoSizeAxes = Axes.Both;
114 Alpha = alpha_when_active;
115
116 int colour = 0;
117
118 bool hasCounters = monitor.ActiveCounters.Any(b => b);
119 Child = new Container
120 {
121 AutoSizeAxes = Axes.Both,
122 Children = new[]
123 {
124 new Container
125 {
126 Origin = Anchor.TopRight,
127 AutoSizeAxes = Axes.X,
128 RelativeSizeAxes = Axes.Y,
129 Children = new[]
130 {
131 labelText = new SpriteText
132 {
133 Text = Name,
134 Origin = Anchor.BottomCentre,
135 Anchor = Anchor.CentreLeft,
136 Rotation = -90,
137 Font = FrameworkFont.Regular,
138 },
139 !hasCounters
140 ? new Container { Width = 2 }
141 : new Container
142 {
143 Masking = true,
144 CornerRadius = 5,
145 AutoSizeAxes = Axes.X,
146 RelativeSizeAxes = Axes.Y,
147 Margin = new MarginPadding { Right = 2, Left = 2 },
148 Children = new Drawable[]
149 {
150 counterBarBackground = new Sprite
151 {
152 Texture = new Texture(1, HEIGHT, true),
153 RelativeSizeAxes = Axes.Both,
154 Size = new Vector2(1, 1),
155 },
156 new FillFlowContainer
157 {
158 Direction = FillDirection.Horizontal,
159 AutoSizeAxes = Axes.X,
160 RelativeSizeAxes = Axes.Y,
161 ChildrenEnumerable =
162 from StatisticsCounterType t in Enum.GetValues(typeof(StatisticsCounterType))
163 where monitor.ActiveCounters[(int)t]
164 select counterBars[t] = new CounterBar
165 {
166 Colour = getColour(colour++),
167 Label = t.ToString(),
168 },
169 },
170 }
171 }
172 }
173 },
174 mainContainer = new Container
175 {
176 Size = new Vector2(WIDTH, HEIGHT),
177 Children = new[]
178 {
179 timeBarsContainer = new Container
180 {
181 Masking = true,
182 CornerRadius = 5,
183 RelativeSizeAxes = Axes.Both,
184 Children = timeBars = new[]
185 {
186 new TimeBar(),
187 new TimeBar(),
188 },
189 },
190 frameTimeDisplay = new FrameTimeDisplay(monitor.Clock)
191 {
192 Anchor = Anchor.BottomRight,
193 Origin = Anchor.BottomRight,
194 },
195 overlayContainer = new Container
196 {
197 RelativeSizeAxes = Axes.Both,
198 Alpha = 0,
199 Children = new Drawable[]
200 {
201 new FillFlowContainer
202 {
203 Anchor = Anchor.TopRight,
204 Origin = Anchor.TopRight,
205 AutoSizeAxes = Axes.Both,
206 Spacing = new Vector2(5, 1),
207 Padding = new MarginPadding { Right = 5 },
208 ChildrenEnumerable =
209 from PerformanceCollectionType t in Enum.GetValues(typeof(PerformanceCollectionType))
210 select legendMapping[(int)t] = new SpriteText
211 {
212 Colour = getColour(t),
213 Text = t.ToString(),
214 Alpha = 0,
215 Font = FrameworkFont.Regular,
216 },
217 },
218 new SpriteText
219 {
220 Padding = new MarginPadding { Left = 4 },
221 Text = $@"{visible_ms_range}ms",
222 Font = FrameworkFont.Regular,
223 },
224 new SpriteText
225 {
226 Padding = new MarginPadding { Left = 4 },
227 Text = @"0ms",
228 Anchor = Anchor.BottomLeft,
229 Origin = Anchor.BottomLeft,
230 Font = FrameworkFont.Regular,
231 }
232 }
233 }
234 }
235 }
236 }
237 };
238 }
239
240 [BackgroundDependencyLoader]
241 private void load()
242 {
243 //initialise background
244 var columnUpload = new ArrayPoolTextureUpload(1, HEIGHT);
245 var fullBackground = new Image<Rgba32>(WIDTH, HEIGHT);
246
247 addArea(null, null, HEIGHT, amount_ms_steps, columnUpload);
248
249 for (int i = 0; i < HEIGHT; i++)
250 {
251 for (int k = 0; k < WIDTH; k++)
252 fullBackground[k, i] = columnUpload.RawData[i];
253 }
254
255 addArea(null, null, HEIGHT, amount_count_steps, columnUpload);
256
257 counterBarBackground?.Texture.SetData(columnUpload);
258 Schedule(() =>
259 {
260 foreach (var t in timeBars)
261 t.Sprite.Texture.SetData(new TextureUpload(fullBackground.Clone()));
262 });
263 }
264
265 private void addEvent(int type)
266 {
267 Box b = new Box
268 {
269 Origin = Anchor.TopCentre,
270 Position = new Vector2(timeBarX, type * 3),
271 Colour = garbage_collect_colors[type],
272 Size = new Vector2(3, 3)
273 };
274
275 timeBars[timeBarIndex].Add(b);
276 }
277
278 private bool running = true;
279
280 public bool Running
281 {
282 get => running;
283 set
284 {
285 if (running == value) return;
286
287 running = value;
288
289 frameTimeDisplay.Counting = running;
290
291 // clear all pending frames on state change.
292 monitor.PendingFrames.Clear();
293 }
294 }
295
296 private bool expanded;
297
298 public bool Expanded
299 {
300 get => expanded;
301 set
302 {
303 value &= state == FrameStatisticsMode.Full;
304
305 if (expanded == value) return;
306
307 expanded = value;
308
309 overlayContainer.FadeTo(expanded ? 1 : 0, 100);
310 this.FadeTo(expanded ? 1 : alpha_when_active, 100);
311
312 foreach (CounterBar bar in counterBars.Values)
313 bar.Expanded = expanded;
314 }
315 }
316
317 protected override bool OnKeyDown(KeyDownEvent e)
318 {
319 switch (e.Key)
320 {
321 case Key.ControlLeft:
322 Expanded = true;
323 break;
324
325 case Key.ShiftLeft:
326 Running = false;
327 break;
328 }
329
330 return base.OnKeyDown(e);
331 }
332
333 protected override void OnKeyUp(KeyUpEvent e)
334 {
335 switch (e.Key)
336 {
337 case Key.ControlLeft:
338 Expanded = false;
339 break;
340
341 case Key.ShiftLeft:
342 Running = true;
343 break;
344 }
345
346 base.OnKeyUp(e);
347 }
348
349 private void applyFrameGC(FrameStatistics frame)
350 {
351 foreach (int gcLevel in frame.GarbageCollections)
352 addEvent(gcLevel);
353 }
354
355 private void applyFrameTime(FrameStatistics frame)
356 {
357 TimeBar timeBar = timeBars[timeBarIndex];
358 var upload = new ArrayPoolTextureUpload(1, HEIGHT, uploadPool)
359 {
360 Bounds = new RectangleI(timeBarX, 0, 1, HEIGHT)
361 };
362
363 int currentHeight = HEIGHT;
364
365 for (int i = 0; i < FrameStatistics.NUM_PERFORMANCE_COLLECTION_TYPES; i++)
366 currentHeight = addArea(frame, (PerformanceCollectionType)i, currentHeight, amount_ms_steps, upload);
367 addArea(frame, null, currentHeight, amount_ms_steps, upload);
368
369 timeBar.Sprite.Texture.SetData(upload);
370
371 timeBars[timeBarIndex].X = WIDTH - timeBarX;
372 timeBars[(timeBarIndex + 1) % timeBars.Length].X = -timeBarX;
373 currentX = (currentX + 1) % (timeBars.Length * WIDTH);
374
375 foreach (Drawable e in timeBars[(timeBarIndex + 1) % timeBars.Length])
376 {
377 if (e is Box && e.DrawPosition.X <= timeBarX)
378 e.Expire();
379 }
380 }
381
382 private void applyFrameCounts(FrameStatistics frame)
383 {
384 foreach (var pair in frame.Counts)
385 counterBars[pair.Key].Value = pair.Value;
386 }
387
388 private void applyFrame(FrameStatistics frame)
389 {
390 if (state == FrameStatisticsMode.Full)
391 {
392 applyFrameGC(frame);
393 applyFrameTime(frame);
394 }
395
396 applyFrameCounts(frame);
397 }
398
399 protected override void Update()
400 {
401 base.Update();
402
403 if (running)
404 {
405 while (monitor.PendingFrames.TryDequeue(out FrameStatistics frame))
406 {
407 applyFrame(frame);
408 frameTimeDisplay.NewFrame(frame);
409 monitor.FramesPool.Return(frame);
410 }
411 }
412 }
413
414 private Color4 getColour(PerformanceCollectionType type)
415 {
416 switch (type)
417 {
418 default:
419 return Color4.YellowGreen;
420
421 case PerformanceCollectionType.SwapBuffer:
422 return Color4.Red;
423#if DEBUG
424 case PerformanceCollectionType.Debug:
425 return Color4.Yellow;
426#endif
427 case PerformanceCollectionType.Sleep:
428 return Color4.DarkBlue;
429
430 case PerformanceCollectionType.Scheduler:
431 return Color4.HotPink;
432
433 case PerformanceCollectionType.WndProc:
434 return Color4.GhostWhite;
435
436 case PerformanceCollectionType.GLReset:
437 return Color4.Cyan;
438 }
439 }
440
441 private Color4 getColour(int index)
442 {
443 const int colour_count = 7;
444
445 switch (index % colour_count)
446 {
447 default:
448 return Color4.BlueViolet;
449
450 case 1:
451 return Color4.YellowGreen;
452
453 case 2:
454 return Color4.HotPink;
455
456 case 3:
457 return Color4.Red;
458
459 case 4:
460 return Color4.Cyan;
461
462 case 5:
463 return Color4.Yellow;
464
465 case 6:
466 return Color4.SkyBlue;
467 }
468 }
469
470 private int addArea(FrameStatistics frame, PerformanceCollectionType? frameTimeType, int currentHeight, int amountSteps, ArrayPoolTextureUpload columnUpload)
471 {
472 int drawHeight;
473
474 if (!frameTimeType.HasValue)
475 drawHeight = currentHeight;
476 else if (frame.CollectedTimes.TryGetValue(frameTimeType.Value, out double elapsedMilliseconds))
477 {
478 legendMapping[(int)frameTimeType].Alpha = 1;
479 drawHeight = (int)(elapsedMilliseconds * scale);
480 }
481 else
482 return currentHeight;
483
484 Color4 col = frameTimeType.HasValue ? getColour(frameTimeType.Value) : new Color4(0.1f, 0.1f, 0.1f, 1);
485
486 for (int i = currentHeight - 1; i >= 0; --i)
487 {
488 if (drawHeight-- == 0) break;
489
490 bool acceptableRange = (float)currentHeight / HEIGHT > 1 - monitor.FrameAimTime / visible_ms_range;
491
492 float brightnessAdjust = 1;
493
494 if (!frameTimeType.HasValue)
495 {
496 int step = amountSteps / HEIGHT;
497 brightnessAdjust *= 1 - i * step / 8f;
498 }
499 else if (acceptableRange)
500 brightnessAdjust *= 0.8f;
501
502 columnUpload.RawData[i] = new Rgba32(col.R * brightnessAdjust, col.G * brightnessAdjust, col.B * brightnessAdjust, col.A);
503
504 currentHeight--;
505 }
506
507 return currentHeight;
508 }
509
510 private class TimeBar : Container
511 {
512 public readonly Sprite Sprite;
513
514 public TimeBar()
515 {
516 Size = new Vector2(WIDTH, HEIGHT);
517 Child = Sprite = new Sprite();
518
519 Sprite.Texture = new Texture(WIDTH, HEIGHT, true) { TextureGL = { BypassTextureUploadQueueing = true } };
520 }
521 }
522
523 private class CounterBar : Container
524 {
525 private readonly Box box;
526 private readonly SpriteText text;
527
528 public string Label;
529
530 private bool expanded;
531
532 public bool Expanded
533 {
534 get => expanded;
535 set
536 {
537 if (expanded == value) return;
538
539 expanded = value;
540
541 if (expanded)
542 {
543 this.ResizeTo(new Vector2(bar_width + text.Font.Size + 2, 1), 100);
544 text.FadeIn(100);
545 }
546 else
547 {
548 this.ResizeTo(new Vector2(bar_width, 1), 100);
549 text.FadeOut(100);
550 }
551 }
552 }
553
554 private double height;
555 private double velocity;
556 private const double acceleration = 0.000001;
557 private const float bar_width = 6;
558
559 private long value;
560
561 public long Value
562 {
563 set
564 {
565 this.value = value;
566 height = Math.Log10(value + 1) / amount_count_steps;
567 }
568 }
569
570 public CounterBar()
571 {
572 Size = new Vector2(bar_width, 1);
573 RelativeSizeAxes = Axes.Y;
574
575 Children = new Drawable[]
576 {
577 text = new SpriteText
578 {
579 Origin = Anchor.BottomLeft,
580 Anchor = Anchor.BottomRight,
581 Rotation = -90,
582 Position = new Vector2(-bar_width - 1, 0),
583 Font = FrameworkFont.Regular.With(size: 16),
584 },
585 box = new Box
586 {
587 RelativeSizeAxes = Axes.Y,
588 Size = new Vector2(bar_width, 0),
589 Anchor = Anchor.BottomRight,
590 Origin = Anchor.BottomRight,
591 }
592 };
593 }
594
595 protected override void Update()
596 {
597 base.Update();
598
599 double elapsedTime = Time.Elapsed;
600 double movement = velocity * Time.Elapsed + 0.5 * acceleration * elapsedTime * elapsedTime;
601 double newHeight = Math.Max(height, box.Height - movement);
602 box.Height = (float)newHeight;
603
604 if (newHeight <= height)
605 velocity = 0;
606 else
607 velocity += Time.Elapsed * acceleration;
608
609 if (expanded)
610 text.Text = $@"{Label}: {NumberFormatter.PrintWithSiSuffix(value)}";
611 }
612 }
613 }
614}