using Amphora; using Annulus; using Azimuth; using Blacklight.Core; using Blacklight.Core.Drawables; using System; using System.Collections.Generic; using System.Diagnostics; namespace Glass.Testbed { /// /// Makes frames miss the display on purpose, by stalling the cpu or loading the gpu, so the readout's dropped /// count can be checked against a known cause. /// public class FrameDropsTestCase : TestCase { // Big enough to cover the window at any ordinary zoom, so every quad shades every pixel. private const double LoadQuadSize = 1e5; // Invisible, but nothing skips a transparent pixel, so each one is still shaded and blended. private static readonly Rectangle LoadQuad = new Rectangle { InsideColor = new Color(0, 0, 0, 0), ShellColor = new Color(0, 0, 0, 0), }; private static readonly AffineMatrix LoadQuadTransform = Rectangle.ModelToWorld(new Vector(LoadQuadSize, LoadQuadSize)); private readonly World world; private readonly List loadQuads = new List(); private NumberField stallField; private NumberField loadField; private bool stallPending; public FrameDropsTestCase(string name, World world) : base(name) { this.world = world; } private static NumberField BuildField(string debugName, NumberField.Data data) { var field = new NumberField(new NumberFieldBehavior(), data) { DebugName = debugName, NumberFieldMaterial = Widgets.NumberField, }; field.Bounds = Rect.FromPositionAndSize(Vector.Zero, field.PreferredBoundsSize); return field; } private void SetLoad(int quadCount) { while (loadQuads.Count < quadCount) { loadQuads.Add(world.cel.AddEntity(LoadQuad, LoadQuadTransform)); } while (loadQuads.Count > quadCount) { world.cel.RemoveEntity(loadQuads[loadQuads.Count - 1]); loadQuads.RemoveAt(loadQuads.Count - 1); } } // Spins rather than sleeps, since Sleep can overshoot by a whole scheduler quantum. private static void Spin(TimeSpan duration) { long until = Stopwatch.GetTimestamp() + (long)(duration.TotalSeconds * Stopwatch.Frequency); while (Stopwatch.GetTimestamp() < until) { } } private void AddStallRow() { stallField = BuildField("stall", new NumberField.Data { Grid = new NumberGrid { Max = 500, Step = 5 }, Value = 50, Format = (value, culture) => value.ToString("N0", culture) + " ms", }); Vector buttonSize = Widgets.ButtonLabel("Stall once").PreferredSize(Constraints.Unbounded); Rect row = ContentAggregator.NewRow(Math.Max(stallField.Bounds.Height, buttonSize.Y)); stallField.Bounds = row.NewCol(stallField.Bounds.Width, margin: Gutter); Add(stallField); Button stallButton = new Label(Widgets.ButtonLabelStyle, "Stall once").FillParent( new Button(ButtonBehavior.Default) { DebugName = "stall once", ButtonMaterial = UiPacksheetMaterials.Button, Bounds = row.NewCol(buttonSize.X), }); stallButton.OnActivate += delegate (ButtonBase _) { stallPending = true; }; Add(stallButton); } private void AddLoadRow() { loadField = BuildField("gpu load", new NumberField.Data { // Kept low because a gpu frame past two seconds makes Windows reset the driver, and a quad can cost // milliseconds of fill. Grid = new NumberGrid { Max = 100 }, Format = (value, culture) => value.ToString("N0", culture) + " quads", }); loadField.OnValueChanged += (_, __, valueNow) => SetLoad((int)valueNow); var caption = new Label(Widgets.SmallCaptionStyle, "window-sized quads drawn every frame", Constraints.Unbounded); Vector captionSize = caption.Bounds.Size; Rect row = ContentAggregator.NewRow(Math.Max(loadField.Bounds.Height, captionSize.Y)); loadField.Bounds = row.NewCol(loadField.Bounds.Width, margin: Gutter); Add(loadField); caption.Bounds = caption.Bounds.WithPosition( row.TopLeft + new Vector(0, (row.Height - captionSize.Y) / 2)); Add(caption); } protected override void Populate() { AddCaption("Stall the cpu for one frame, or load the gpu every frame, and watch dropped in the readout."); AddCaption("A short stall can hide behind the frame already queued for the display."); AddStallRow(); AddLoadRow(); } protected override void Setup() { stallPending = false; SetLoad((int)loadField.Value); } // The quads live in the shared game scene, so they'd keep loading the gpu under every other case. protected override void Teardown() { SetLoad(0); } public override void Update(Duration elapsed) { if (!stallPending) { return; } stallPending = false; Spin(TimeSpan.FromMilliseconds((double)stallField.Value)); } } }