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));
}
}
}