using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Reflection;
using System.Windows.Forms;
using Annulus;
using Azimuth;
using Blacklight.Core;
using Blacklight.Core.Cameras;
using Blacklight.Core.Drawables;
using Prism;
namespace Blacklight.SharpDX9.PerformanceTests
{
///
/// Times whole frames of sprites through the real D3D9 backend on a window that is never shown, for the numbers
/// deciding whether particles need a faster sprite path.
///
public static class Program
{
public const int DefaultWidth = 1920;
public const int DefaultHeight = 1080;
public const int WarmUpFrames = 30;
public const int MeasuredFrames = 121;
public const int ControlFrames = 61;
public const int TextureSide = 64;
public const double DriftWarningMilliseconds = 0.5;
public static bool IsJitOptimizerDisabled(Assembly assembly)
{
var debuggable = assembly.GetCustomAttribute();
return debuggable != null && debuggable.IsJITOptimizerDisabled;
}
///
/// Why timings taken in this process wouldn't mean anything, or null when they would.
///
public static string ReasonTimingsAreMeaningless()
{
if (Debugger.IsAttached)
{
return "a debugger is attached";
}
foreach (Assembly assembly in new[]
{
typeof(Program).Assembly,
typeof(WindowHandle).Assembly,
typeof(Module).Assembly,
typeof(GPUBridge.PrimitiveBatch).Assembly,
})
{
if (IsJitOptimizerDisabled(assembly))
{
return $"{assembly.GetName().Name} is a Debug build";
}
}
return null;
}
///
/// Gets a soft-edged disc, white at the centre and fading to nothing at the rim, as straight alpha.
///
public static DecodedImage BuildSoftDisc()
{
var rgba = new byte[TextureSide * TextureSide * DecodedImage.BytesPerPixel];
double radius = TextureSide / 2.0;
for (int y = 0; y < TextureSide; ++y)
{
for (int x = 0; x < TextureSide; ++x)
{
double distance = Math.Sqrt(
(x + 0.5 - radius) * (x + 0.5 - radius) + (y + 0.5 - radius) * (y + 0.5 - radius));
double alpha = Math.Max(0, Math.Min(1, 1 - distance / radius));
int index = (y * TextureSide + x) * DecodedImage.BytesPerPixel;
rgba[index + 0] = 255;
rgba[index + 1] = 200;
rgba[index + 2] = 120;
rgba[index + 3] = (byte)(alpha * 255);
}
}
return new DecodedImage(rgba, new System.Drawing.Size(TextureSide, TextureSide));
}
///
/// Gets a scene drawing nothing, which costs the clear and the resolve pass.
///
public static Scene BuildEmptyScene()
{
var scene = new Scene();
scene.Cels.Add(new Cel());
return scene;
}
///
/// Gets a scene of one sprite batch holding the scenario's sprites, spread evenly over the screen.
///
public static Scene BuildScene(Texture texture, SpriteScenario scenario, int width, int height)
{
SpriteBatch.Varying varying = SpriteBatch.Varying.None;
if (scenario.VaryingColumns)
{
varying = SpriteBatch.Varying.TintColor | SpriteBatch.Varying.Additiveness;
}
if (scenario.EntryTransforms)
{
varying |= SpriteBatch.Varying.Transform;
}
var batch = new SpriteBatch(scenario.Count, varying) { Texture = texture };
if (scenario.Additive)
{
batch.Additiveness = 1;
}
if (scenario.Tiled)
{
batch.TilingBehaviorX = TilingBehavior.Tile;
batch.TilingBehaviorY = TilingBehavior.Tile;
}
if (scenario.TintTextured)
{
batch.TintTexture = texture;
}
batch.CoverageGamma = scenario.Gamma;
SpriteSource source = texture.Subregion(
new System.Drawing.Point(0, 0), new System.Drawing.Size(TextureSide, TextureSide));
if (scenario.Swizzled)
{
source.RedChannel = ColorChannel.G;
source.GreenChannel = ColorChannel.R;
}
int spanWidth = width;
int spanHeight = height;
if (scenario.Region > 0)
{
spanWidth = Math.Min(width, scenario.Region);
spanHeight = Math.Min(height, scenario.Region);
}
// A fixed multiplier generator, so every run draws the same scene.
uint state = 12345;
for (int i = 0; i < scenario.Count; ++i)
{
state = state * 1664525u + 1013904223u;
double x = (state >> 8) % (uint)spanWidth;
state = state * 1664525u + 1013904223u;
double y = (state >> 8) % (uint)spanHeight;
SpriteDestination destination = SpritePlacement.Corners(
new Vector(x, y), new Vector(x + scenario.Side, y + scenario.Side));
if (scenario.EntryTransforms)
{
// Centred on the origin so the entry transform spins it in place.
double half = 0.5 * scenario.Side;
destination = SpritePlacement.Corners(new Vector(-half, -half), new Vector(half, half));
batch.Add(new SpriteBatch.Entry(source, destination));
batch.TransformColumn[batch.StorageOffset + i] =
new AffineMatrix(0.01 * (i % 100), new Vector(x + half, y + half));
}
else
{
batch.Add(new SpriteBatch.Entry(source, destination));
}
if (scenario.VaryingColumns)
{
batch.TintColorColumn[batch.StorageOffset + i] = new TintColor(1, 1, 1, 0.5 + 0.5 * (i % 2));
batch.AdditivenessColumn[batch.StorageOffset + i] = 0.5 * (i % 3);
}
}
var cel = new Cel();
// Past every sprite, so it costs the pixel stage its test and cuts nothing.
Rect? clip = null;
if (scenario.Clipped)
{
clip = Rect.FromPositionAndSize(
new Vector(-1000, -1000), new Vector(width + 2000, height + 2000));
}
cel.AddEntity(new Entity(batch, AffineMatrix.Identity, clip));
var scene = new Scene();
scene.Cels.Add(cel);
return scene;
}
public static double Median(double[] samples)
{
var sorted = (double[])samples.Clone();
Array.Sort(sorted);
return sorted[sorted.Length / 2];
}
public static double MillisecondsFor(long ticks)
{
return ticks * 1000.0 / Stopwatch.Frequency;
}
///
/// Draws `scene` for a warm-up and then `frames` timed frames, and returns the median of each cost.
///
public static FrameTimes TimeFrames(SharpDX9WindowHandle handle, Scene scene, Camera camera, int frames)
{
for (int i = 0; i < WarmUpFrames; ++i)
{
handle.FrameReset();
handle.DrawScene(scene, camera);
handle.SubmitDrawCalls();
}
var queue = new double[frames];
var submit = new double[frames];
var gpuWait = new double[frames];
var whole = new double[frames];
for (int i = 0; i < frames; ++i)
{
long start = Stopwatch.GetTimestamp();
handle.FrameReset();
handle.DrawScene(scene, camera);
long queued = Stopwatch.GetTimestamp();
handle.SubmitDrawCalls();
long finished = Stopwatch.GetTimestamp();
FrameCost cost = handle.LastFrameCost;
queue[i] = MillisecondsFor(queued - start);
submit[i] = cost.Submit.TotalMilliseconds;
gpuWait[i] = cost.GpuWait.TotalMilliseconds;
whole[i] = MillisecondsFor(finished - start);
}
return new FrameTimes
{
QueueMilliseconds = Median(queue),
SubmitMilliseconds = Median(submit),
GpuWaitMilliseconds = Median(gpuWait),
FrameMilliseconds = Median(whole),
};
}
public static string Cell(double value, int width)
{
return value.ToString("0.00", CultureInfo.InvariantCulture).PadLeft(width);
}
public static void PrintRow(SpriteScenario scenario, FrameTimes times, double controlMilliseconds,
int pixels)
{
double net = times.FrameMilliseconds - controlMilliseconds;
double spritePixels = (double)scenario.Count * scenario.Side * scenario.Side;
double nanosecondsPerPixel = net * 1e6 / spritePixels;
double overlap = spritePixels / pixels;
Console.WriteLine($"{scenario.Name,-34} {scenario.Count,7} {scenario.Side,5} {Cell(overlap, 7)}x" +
$" {Cell(times.QueueMilliseconds, 9)} {Cell(times.SubmitMilliseconds, 9)}" +
$" {Cell(times.GpuWaitMilliseconds, 9)} {Cell(times.FrameMilliseconds, 9)}" +
$" {Cell(net, 9)} {Cell(nanosecondsPerPixel, 8)}");
}
public static string AdapterDescription()
{
using (var direct3D = new SharpDX.Direct3D9.Direct3D())
{
return direct3D.Adapters[0].Details.Description.Trim();
}
}
public static int Main(string[] args)
{
string reason = ReasonTimingsAreMeaningless();
if (reason != null)
{
Console.Error.WriteLine($"Refusing to benchmark: {reason}. Run a Release build without a debugger.");
return 1;
}
if (args.Length >= 2 && args[0] == ImageDump.Argument)
{
return ImageDump.Run(args[1]);
}
if (args.Length >= 1 && args[0] == ShaderStats.Argument)
{
return args.Length >= 2 ? ShaderStats.Dump(args[1]) : ShaderStats.Run();
}
string filter = args.Length >= 1 ? args[0] : "";
Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.High;
using (var window = new Form())
using (var module = new Module { VerticalSync = VerticalSyncMode.Off })
{
// Never shown. D3D9 needs a window to own the device, and the frames go to an off-screen back buffer.
window.ClientSize = new System.Drawing.Size(DefaultWidth, DefaultHeight);
var handle = (SharpDX9WindowHandle)module.CreateDrawableSurfaceOnWindow(window.Handle);
try
{
handle.MeasureGpuCompletion = true;
int width = handle.Width;
int height = handle.Height;
var camera = new ScreenSpaceCamera(handle);
Texture texture = handle.CreateTexture(BuildSoftDisc());
Scene empty = BuildEmptyScene();
Console.WriteLine($"{AdapterDescription()}, {width}x{height}, {IntPtr.Size * 8}-bit, " +
$"medians of {MeasuredFrames} frames after {WarmUpFrames} warm-up frames.");
Console.WriteLine("Every row is the median frame minus an empty frame measured before and " +
"after it, so \"net\" is what the sprites cost.");
Console.WriteLine("ns/px is net time per sprite pixel drawn (count x side x side), the fill " +
"cost when the GPU is the limit.");
Console.WriteLine();
Console.WriteLine($"{"scenario",-34} {"sprites",7} {"side",5} {"overlap",8}" +
$" {"queue ms",9} {"submit ms",9} {"gpu ms",9} {"frame ms",9} {"net ms",9} {"ns/px",8}");
foreach (SpriteScenario scenario in SpriteScenario.All)
{
if (scenario.Name.IndexOf(filter, StringComparison.OrdinalIgnoreCase) < 0)
{
continue;
}
FrameTimes before = TimeFrames(handle, empty, camera, ControlFrames);
FrameTimes times = TimeFrames(handle, BuildScene(texture, scenario, width, height), camera,
MeasuredFrames);
FrameTimes after = TimeFrames(handle, empty, camera, ControlFrames);
double control = (before.FrameMilliseconds + after.FrameMilliseconds) / 2;
PrintRow(scenario, times, control, width * height);
double drift = Math.Abs(before.FrameMilliseconds - after.FrameMilliseconds);
if (drift > Math.Max(DriftWarningMilliseconds, 0.25 * control))
{
Console.WriteLine(
$" WARNING: the empty frame drifted from {before.FrameMilliseconds:0.00} to " +
$"{after.FrameMilliseconds:0.00} ms around that row. The machine was busy.");
}
}
}
finally
{
handle.Dispose();
}
}
return 0;
}
}
///
/// The median cost of each part of a frame, in milliseconds.
///
public struct FrameTimes
{
public double QueueMilliseconds;
public double SubmitMilliseconds;
public double GpuWaitMilliseconds;
public double FrameMilliseconds;
}
///
/// One sprite batch to draw: how many sprites, how big on screen, and which per-entry columns it uses.
///
public sealed class SpriteScenario
{
public string Name;
public int Count;
public int Side;
public bool Additive;
public bool VaryingColumns;
///
/// Gives each sprite its own rotation and position through the transform column.
///
public bool EntryTransforms;
///
/// Sprite features that take the pixel stage off its short path, one at a time.
///
public bool Tiled;
public bool Swizzled;
public bool Clipped;
public bool TintTextured;
public double Gamma = 1;
///
/// The side of the square in the screen's corner that the sprites are confined to, or 0 to spread them over the
/// whole screen. A small square stays in the GPU's cache, so it tells fill apart from memory bandwidth.
///
public int Region;
public static readonly SpriteScenario[] All =
{
new SpriteScenario { Name = "1k sprites, small", Count = 1000, Side = 16 },
new SpriteScenario { Name = "20k sprites, tiny", Count = 20000, Side = 8 },
new SpriteScenario { Name = "20k sprites, small", Count = 20000, Side = 16 },
new SpriteScenario { Name = "20k sprites, medium", Count = 20000, Side = 32 },
new SpriteScenario { Name = "20k sprites, large", Count = 20000, Side = 64 },
new SpriteScenario { Name = "2k sprites, large", Count = 2000, Side = 64 },
new SpriteScenario { Name = "20k medium, in a 256px square", Count = 20000, Side = 32, Region = 256 },
new SpriteScenario { Name = "20k large, in a 256px square", Count = 20000, Side = 64, Region = 256 },
new SpriteScenario { Name = "20k small, additive", Count = 20000, Side = 16, Additive = true },
new SpriteScenario
{
Name = "20k small, tint+additive columns", Count = 20000, Side = 16, VaryingColumns = true,
},
new SpriteScenario
{
Name = "20k medium, tint+additive columns", Count = 20000, Side = 32, VaryingColumns = true,
},
new SpriteScenario
{
Name = "20k small, entry transforms", Count = 20000, Side = 16, EntryTransforms = true,
},
new SpriteScenario { Name = "feature: none", Count = 20000, Side = 64 },
new SpriteScenario { Name = "feature: clip window", Count = 20000, Side = 64, Clipped = true },
new SpriteScenario { Name = "feature: tiling", Count = 20000, Side = 64, Tiled = true },
new SpriteScenario { Name = "feature: channel swap", Count = 20000, Side = 64, Swizzled = true },
new SpriteScenario { Name = "feature: tint texture", Count = 20000, Side = 64, TintTextured = true },
new SpriteScenario { Name = "feature: coverage gamma", Count = 20000, Side = 64, Gamma = 1.4 },
new SpriteScenario
{
Name = "feature: text (swap, gamma, clip)", Count = 20000, Side = 64, Swizzled = true, Gamma = 1.4,
Clipped = true,
},
new SpriteScenario
{
Name = "feature: all", Count = 20000, Side = 64, Swizzled = true, Gamma = 1.4, Clipped = true,
Tiled = true, TintTextured = true,
},
};
}
}