using System; using System.Reflection; using System.Runtime.InteropServices; using System.Threading; using System.Windows.Forms; using SharpDX; using SharpDX.Direct3D9; namespace Blacklight.Bezier.D3D9 { /// /// Runs one pixel per sample through a variant on the default adapter, for readback or for timing. /// public sealed class D3D9Bench : IDisposable { private static readonly Vector4[] FullScreenTriangle = { new Vector4(-1, -1, 0, 1), new Vector4(3, -1, 0, 1), new Vector4(-1, 3, 0, 1), }; // D3DGETDATA_FLUSH, so a poll can't spin on a query the driver never submitted. private const int GetDataFlush = 1; // Query.GetData can't tell "not ready yet" from a result, this can. private static readonly Func RawGetData = (Func)Delegate.CreateDelegate( typeof(Func), typeof(Query).GetMethod(nameof(Query.GetData), BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.DeclaredOnly, binder: null, types: new[] { typeof(IntPtr), typeof(int), typeof(int) }, modifiers: null)); private readonly Form window; private readonly Direct3D direct3D; private readonly Device device; private readonly VertexDeclaration declaration; private readonly VertexShader vertexShader; public readonly string AdapterDescription; public D3D9Bench(string vertexShaderSource) { // Never shown: D3D9 needs a window to own the device, and nothing here is presented. window = new Form(); direct3D = new Direct3D(); AdapterDescription = direct3D.Adapters[0].Details.Description.Trim(); if (!direct3D.CheckDeviceFormat(0, DeviceType.Hardware, Format.X8R8G8B8, Usage.RenderTarget, ResourceType.Texture, Format.R32F)) { throw new NotSupportedException(AdapterDescription + " can't render to R32F."); } device = new Device(direct3D, 0, DeviceType.Hardware, window.Handle, // FpuPreserve: otherwise D3D9 drops the x87 FPU to single precision under the double reference. CreateFlags.HardwareVertexProcessing | CreateFlags.FpuPreserve, new PresentParameters(1, 1)); declaration = new VertexDeclaration(device, new[] { new VertexElement(0, 0, DeclarationType.Float4, DeclarationMethod.Default, DeclarationUsage.Position, 0), VertexElement.VertexDeclarationEnd, }); // D3DX9's compiler, the one production D3D9 shaders go through. vertexShader = new VertexShader(device, ShaderBytecode.Compile(vertexShaderSource, "FullScreenVS", "vs_3_0", ShaderFlags.OptimizationLevel3)); } /// /// Distance evaluations TimingPS runs per pixel; matches EvaluationsPerTimedPixel in Harness.hlsl. /// public const int EvaluationsPerTimedPixel = 16; /// DistancePS to read distances back, TimingPS to time them. public PixelShader CompilePixelShader(Variant variant, string entryPoint) { return new PixelShader(device, ShaderBytecode.Compile(variant.Source, entryPoint, "ps_3_0", ShaderFlags.OptimizationLevel3)); } /// /// A side x side texture of (x, y, phi, end), each rounded to float32 on its own; samples repeat to fill it. /// /// Replaces end in the last channel when given, as g does for shell tests. public Texture UploadSamples(ShellSample[] samples, int side, double[] fourth = null) { var returnMe = new Texture(device, side, side, 1, Usage.None, Format.A32B32G32R32F, Pool.Managed); DataRectangle rectangle = returnMe.LockRectangle(0, LockFlags.None); var row = new float[side * 4]; for (int rowIndex = 0; rowIndex < side; rowIndex++) { for (int column = 0; column < side; column++) { int index = (rowIndex * side + column) % samples.Length; ShellSample sample = samples[index]; row[column * 4] = (float)sample.X; row[column * 4 + 1] = (float)sample.Y; row[column * 4 + 2] = (float)sample.Phi; row[column * 4 + 3] = (float)(fourth == null ? sample.End : fourth[index]); } Marshal.Copy(row, 0, IntPtr.Add(rectangle.DataPointer, rowIndex * rectangle.Pitch), row.Length); } returnMe.UnlockRectangle(0); return returnMe; } /// /// A side x side texture with one value per texel in .x, and optionally a second in .y, repeating to fill it. /// public Texture UploadValues(double[] values, int side, double[] secondValues = null) { var returnMe = new Texture(device, side, side, 1, Usage.None, Format.A32B32G32R32F, Pool.Managed); DataRectangle rectangle = returnMe.LockRectangle(0, LockFlags.None); var row = new float[side * 4]; for (int rowIndex = 0; rowIndex < side; rowIndex++) { for (int column = 0; column < side; column++) { int index = (rowIndex * side + column) % values.Length; row[column * 4] = (float)values[index]; if (secondValues != null) { row[column * 4 + 1] = (float)secondValues[index]; } } Marshal.Copy(row, 0, IntPtr.Add(rectangle.DataPointer, rowIndex * rectangle.Pitch), row.Length); } returnMe.UnlockRectangle(0); return returnMe; } private void PrepareDraw(PixelShader shader, Texture[] samples, int side, Surface target) { device.SetRenderTarget(0, target); // The device's own depth buffer is 1x1; one smaller than the target makes every draw invalid. device.DepthStencilSurface = null; device.SetRenderState(RenderState.ZEnable, false); device.SetRenderState(RenderState.AlphaBlendEnable, false); device.SetRenderState(RenderState.CullMode, Cull.None); device.VertexDeclaration = declaration; device.VertexShader = vertexShader; device.PixelShader = shader; for (int stage = 0; stage < samples.Length; stage++) { device.SetTexture(stage, samples[stage]); device.SetSamplerState(stage, SamplerState.MinFilter, TextureFilter.Point); device.SetSamplerState(stage, SamplerState.MagFilter, TextureFilter.Point); device.SetSamplerState(stage, SamplerState.MipFilter, TextureFilter.None); device.SetSamplerState(stage, SamplerState.AddressU, TextureAddress.Clamp); device.SetSamplerState(stage, SamplerState.AddressV, TextureAddress.Clamp); } device.SetPixelShaderConstant(0, new[] { new Vector4(side, side, 1.0f / side, 1.0f / side) }); } private void DrawPass() { device.DrawUserPrimitives(PrimitiveType.TriangleList, 1, FullScreenTriangle); } /// /// Every sample's distance, as the variant computed it, in sample order. /// public float[] Measure(PixelShader shader, Texture samples, int side) { return Measure(shader, new[] { samples }, side); } /// Bound to samplers s0, s1, ... in order. public float[] Measure(PixelShader shader, Texture[] samples, int side) { using (var target = new Texture(device, side, side, 1, Usage.RenderTarget, Format.R32F, Pool.Default)) using (Surface targetSurface = target.GetSurfaceLevel(0)) using (Surface readback = Surface.CreateOffscreenPlain(device, side, side, Format.R32F, Pool.SystemMemory)) { PrepareDraw(shader, samples, side, targetSurface); device.BeginScene(); DrawPass(); device.EndScene(); device.GetRenderTargetData(targetSurface, readback); var returnMe = new float[side * side]; DataRectangle rectangle = readback.LockRectangle(LockFlags.ReadOnly); for (int row = 0; row < side; row++) { Marshal.Copy(IntPtr.Add(rectangle.DataPointer, row * rectangle.Pitch), returnMe, row * side, side); } readback.UnlockRectangle(); return returnMe; } } private static long WaitForQuery(Query query, IntPtr buffer, int size) { Result result; while ((result = RawGetData(query, buffer, size, GetDataFlush)) != Result.Ok) { result.CheckError(); Thread.SpinWait(64); } if (size == sizeof(int)) { return Marshal.ReadInt32(buffer); } return Marshal.ReadInt64(buffer); } /// /// Seconds the GPU spent on passes full-screen draws, or null if the clock jumped mid-measurement. /// private double? TimeOnce(PixelShader shader, Texture[] samples, int side, Surface target, int passes, Query disjoint, Query frequency, Query start, Query finish, IntPtr buffer) { PrepareDraw(shader, samples, side, target); disjoint.Issue(Issue.Begin); start.Issue(Issue.End); device.BeginScene(); for (int pass = 0; pass < passes; pass++) { DrawPass(); } device.EndScene(); finish.Issue(Issue.End); disjoint.Issue(Issue.End); frequency.Issue(Issue.End); long startTicks = WaitForQuery(start, buffer, sizeof(long)); long finishTicks = WaitForQuery(finish, buffer, sizeof(long)); long ticksPerSecond = WaitForQuery(frequency, buffer, sizeof(long)); if (WaitForQuery(disjoint, buffer, sizeof(int)) != 0) { return null; } return (double)(finishTicks - startTicks) / ticksPerSecond; } /// /// Median GPU nanoseconds per pixel for each shader, or null if the driver can't time. /// /// Called after each measurement with how many are done and how many there are. public double[] NanosecondsPerPixel(PixelShader[] shaders, Texture samples, int side, int passes, int repeats, Action progress) { return NanosecondsPerPixel(shaders, new[] { samples }, side, passes, repeats, progress); } /// Bound to samplers s0, s1, ... in order. public double[] NanosecondsPerPixel(PixelShader[] shaders, Texture[] samples, int side, int passes, int repeats, Action progress) { Query disjoint; Query frequency; Query start; Query finish; try { disjoint = new Query(device, QueryType.TimestampDisjoint); frequency = new Query(device, QueryType.TimestampFreq); start = new Query(device, QueryType.Timestamp); finish = new Query(device, QueryType.Timestamp); } catch (SharpDXException) { return null; } IntPtr buffer = Marshal.AllocHGlobal(sizeof(long)); try { using (disjoint) using (frequency) using (start) using (finish) using (var target = new Texture(device, side, side, 1, Usage.RenderTarget, Format.R32F, Pool.Default)) using (Surface targetSurface = target.GetSurfaceLevel(0)) { var perPixel = new double[shaders.Length][]; for (int shaderIndex = 0; shaderIndex < shaders.Length; shaderIndex++) { perPixel[shaderIndex] = new double[repeats]; } // Shaders take turns within each repeat, to make comparisons apples to apples. int measurements = (repeats + 1) * shaders.Length; int measured = 0; for (int repeat = -1; repeat < repeats; repeat++) { for (int shaderIndex = 0; shaderIndex < shaders.Length; shaderIndex++) { double? seconds = null; for (int attempt = 0; attempt < 10 && seconds == null; attempt++) { seconds = TimeOnce(shaders[shaderIndex], samples, side, targetSurface, passes, disjoint, frequency, start, finish, buffer); } if (repeat >= 0) { perPixel[shaderIndex][repeat] = (seconds ?? double.NaN) * 1e9 / ((double)passes * side * side); } measured++; progress(measured, measurements); } } var returnMe = new double[shaders.Length]; for (int shaderIndex = 0; shaderIndex < shaders.Length; shaderIndex++) { Array.Sort(perPixel[shaderIndex]); returnMe[shaderIndex] = perPixel[shaderIndex][repeats / 2]; } return returnMe; } } finally { Marshal.FreeHGlobal(buffer); } } public void Dispose() { vertexShader.Dispose(); declaration.Dispose(); device.Dispose(); direct3D.Dispose(); window.Dispose(); } } }