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