A game framework written with osu! in mind.
at master 614 lines 21 kB view raw
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}