using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; using SharpDX.Direct3D9; namespace Blacklight.Bezier.D3D9 { public sealed class Options { public int Side = 512; public int TimingSide = 1024; public int Passes = 10; public int Repeats = 9; public int Seed = 1; public bool Timing = true; public string[] VariantFilter; public string OutputDirectory; public string RelativeTo = "HalleyTwo"; public static Options Parse(string[] args) { var returnMe = new Options(); for (int i = 0; i < args.Length; i++) { string value = i + 1 < args.Length ? args[i + 1] : null; switch (args[i]) { case "--side": returnMe.Side = int.Parse(value); i++; break; case "--timing-side": returnMe.TimingSide = int.Parse(value); i++; break; case "--passes": returnMe.Passes = int.Parse(value); i++; break; case "--repeats": returnMe.Repeats = int.Parse(value); i++; break; case "--seed": returnMe.Seed = int.Parse(value); i++; break; case "--no-timing": returnMe.Timing = false; break; case "--variants": returnMe.VariantFilter = value.Split(','); i++; break; case "--output": returnMe.OutputDirectory = value; i++; break; case "--relative-to": returnMe.RelativeTo = value; i++; break; default: throw new ArgumentException("Unknown option " + args[i]); } } return returnMe; } } /// /// The worst sample a variant saw across every set, with enough context to reproduce it. /// public sealed class WorstCase { public string SetName; public ShellSample Sample; public ReferenceResult Reference; public float Measured; public double Ratio = -1; } public static class Program { public static string Format(double value) { if (double.IsNaN(value) || double.IsInfinity(value)) { return value.ToString(CultureInfo.InvariantCulture); } if (value != 0 && (Math.Abs(value) >= 1e4 || Math.Abs(value) < 1e-2)) { return value.ToString("0.0e+0", CultureInfo.InvariantCulture); } return value.ToString("0.##", CultureInfo.InvariantCulture); } public const int ReportWidth = 120; /// /// text after prefix, word-wrapped to ReportWidth with continuation lines indented under its start. /// public static void AppendWrapped(StringBuilder report, string prefix, string text) { var line = new StringBuilder(prefix); foreach (string word in text.Split(' ')) { if (line.Length > prefix.Length && line.Length + 1 + word.Length > ReportWidth) { report.AppendLine(line.ToString()); line.Clear().Append(' ', prefix.Length); } if (line.Length > prefix.Length) { line.Append(' '); } line.Append(word); } report.AppendLine(line.ToString()); } /// /// Rows (the first being the header) with every column as wide as its widest entry, split into blocks that /// fit ReportWidth; each block repeats the first leadingColumns, which are left-aligned labels. /// public static void AppendTable(StringBuilder report, List rows, int leadingColumns) { const int gap = 2; int columns = rows[0].Length; var widths = new int[columns]; for (int column = 0; column < columns; column++) { widths[column] = rows.Max(row => row[column].Length); } int leadingWidth = 0; for (int column = 0; column < leadingColumns; column++) { leadingWidth += widths[column] + gap; } int blockStart = leadingColumns; while (blockStart < columns) { // At least one column per block, even one too wide to fit on its own. int blockEnd = blockStart + 1; int width = leadingWidth + widths[blockStart] + gap; while (blockEnd < columns && width + widths[blockEnd] + gap <= ReportWidth) { width += widths[blockEnd] + gap; blockEnd++; } foreach (string[] row in rows) { var line = new StringBuilder(); for (int column = 0; column < leadingColumns; column++) { line.Append(row[column].PadRight(widths[column] + gap)); } for (int column = blockStart; column < blockEnd; column++) { line.Append(row[column].PadLeft(widths[column] + gap)); } report.AppendLine(line.ToString().TrimEnd()); } report.AppendLine(); blockStart = blockEnd; } } public static ReferenceResult[] SolveAll(ShellSample[] samples) { var returnMe = new ReferenceResult[samples.Length]; Parallel.For(0, samples.Length, i => returnMe[i] = ReferenceDistance.Solve(samples[i])); return returnMe; } /// /// Samples whose GPU output isn't bit-for-bit their own x: nonzero means the texel addressing is off and /// every other number is comparing the wrong samples. /// public static int AddressingMismatches(float[] measured, ShellSample[] samples) { int returnMe = 0; for (int i = 0; i < samples.Length; i++) { if (measured[i] != (float)samples[i].X) { returnMe++; } } return returnMe; } /// /// Every variant's shell answers on pixels crowded against their shell edge, scored per set. /// public static void AppendShellAccuracy(StringBuilder report, D3D9Bench bench, List shellVariants, List shellSets, int side) { var shaders = shellVariants.ToDictionary(variant => variant.Name, variant => bench.CompilePixelShader(variant, "ShellPS")); var statistics = shellVariants.ToDictionary(variant => variant.Name, variant => new List()); foreach (ShellTestSet set in shellSets) { Console.WriteLine("Shell-testing {0}...", set.Name); if (set.Samples.Length == 0) { foreach (Variant variant in shellVariants) { statistics[variant.Name].Add(new ShellStatistics(0, 0, 0, 0, -1)); } continue; } using (Texture texture = bench.UploadSamples(set.Samples, side, set.G)) { foreach (Variant variant in shellVariants) { float[] measured = bench.Measure(shaders[variant.Name], texture, side); statistics[variant.Name].Add(ShellStatistics.Measure(measured, set)); } } } foreach (PixelShader shader in shaders.Values) { shader.Dispose(); } report.AppendLine("Shell tests on owned pixels, thickness within 1e-9..1e-1 of each pixel's distance:"); report.AppendLine("worst miss / input-rounding floor (misses over 10 floors)"); report.AppendLine(); var rows = new List(); rows.Add(new[] { "Set", "Owned" }.Concat(shellVariants.Select(variant => variant.Name)).ToArray()); for (int setIndex = 0; setIndex < shellSets.Count; setIndex++) { int index = setIndex; rows.Add(new[] { shellSets[setIndex].Name, shellSets[setIndex].Samples.Length.ToString(CultureInfo.InvariantCulture) } .Concat(shellVariants.Select(variant => { ShellStatistics result = statistics[variant.Name][index]; return Format(result.WorstSeverity) + " (" + result.SeriousMisses + ")"; })).ToArray()); } rows.Add(new[] { "Worst anywhere", "" }.Concat(shellVariants.Select(variant => Format(statistics[variant.Name].Max(result => result.WorstSeverity)))).ToArray()); rows.Add(new[] { "Misses over 10 floors", "" }.Concat(shellVariants.Select(variant => statistics[variant.Name].Sum(result => result.SeriousMisses).ToString(CultureInfo.InvariantCulture))) .ToArray()); rows.Add(new[] { "All misses", "" }.Concat(shellVariants.Select(variant => statistics[variant.Name].Sum(result => result.Misclassified).ToString(CultureInfo.InvariantCulture))) .ToArray()); AppendTable(report, rows, leadingColumns: 2); report.AppendLine("Worst shell miss per variant:"); foreach (Variant variant in shellVariants) { int worstSet = -1; for (int setIndex = 0; setIndex < shellSets.Count; setIndex++) { ShellStatistics result = statistics[variant.Name][setIndex]; if (result.WorstIndex >= 0 && (worstSet < 0 || result.WorstSeverity > statistics[variant.Name][worstSet].WorstSeverity)) { worstSet = setIndex; } } if (worstSet < 0) { report.AppendLine(" " + variant.Name + ": no misses"); continue; } ShellTestSet set = shellSets[worstSet]; int index = statistics[variant.Name][worstSet].WorstIndex; ShellSample sample = set.Samples[index]; double distance = set.References[index].Distance; report.AppendLine(string.Format(CultureInfo.InvariantCulture, " {0}: {1} in \"{2}\"", variant.Name, Format(statistics[variant.Name][worstSet].WorstSeverity), set.Name)); report.AppendLine(string.Format(CultureInfo.InvariantCulture, " x = {0:R}", sample.X)); report.AppendLine(string.Format(CultureInfo.InvariantCulture, " y = {0:R}", sample.Y)); report.AppendLine(string.Format(CultureInfo.InvariantCulture, " phi = {0:R}", sample.Phi)); report.AppendLine(string.Format(CultureInfo.InvariantCulture, " g = {0:R}", set.G[index])); report.AppendLine(string.Format(CultureInfo.InvariantCulture, " distance = {0:R}, truly {1}", distance, distance * distance < set.G[index] ? "inside" : "outside")); } report.AppendLine(); } public static string Duration(TimeSpan span) { return ((int)span.TotalMinutes).ToString(CultureInfo.InvariantCulture) + ":" + span.Seconds.ToString("00", CultureInfo.InvariantCulture); } /// /// Rewrites one console line with how far a long measurement has got and roughly how long it has left. /// public static Action ProgressLine(string label) { Stopwatch clock = Stopwatch.StartNew(); return (done, total) => { TimeSpan elapsed = clock.Elapsed; TimeSpan left = TimeSpan.FromTicks(elapsed.Ticks * (total - done) / Math.Max(done, 1)); Console.Write("\r {0}: {1}/{2} measurements, {3} elapsed, about {4} left ", label, done, total, Duration(elapsed), Duration(left)); if (done == total) { Console.WriteLine(); } }; } /// /// A timing table: every variant through entryPoint over the sample textures, net of the Null baseline when /// there is one, relative to relativeTo. /// public static void AppendTiming(StringBuilder report, D3D9Bench bench, List variants, string entryPoint, Texture[] textures, Options options, string title, string relativeTo) { PixelShader[] timingShaders = variants .Select(variant => bench.CompilePixelShader(variant, entryPoint)).ToArray(); double[] perPixel = bench.NanosecondsPerPixel(timingShaders, textures, options.TimingSide, options.Passes, options.Repeats, ProgressLine(title.Split(':')[0])); foreach (PixelShader shader in timingShaders) { shader.Dispose(); } if (perPixel == null) { report.AppendLine("Timing: this driver doesn't support timestamp queries."); return; } var nanoseconds = new Dictionary(); for (int i = 0; i < variants.Count; i++) { nanoseconds[variants[i].Name] = perPixel[i] / D3D9Bench.EvaluationsPerTimedPixel; } double baseline = 0; if (nanoseconds.ContainsKey(VariantCatalog.BaselineName)) { baseline = nanoseconds[VariantCatalog.BaselineName]; } double reference = double.NaN; if (nanoseconds.ContainsKey(relativeTo)) { reference = nanoseconds[relativeTo] - baseline; } report.AppendLine(title); report.AppendLine(string.Format(CultureInfo.InvariantCulture, " median of {0} rounds, each timing every variant once over {1} passes of {2}x{2} samples,", options.Repeats, options.Passes, options.TimingSide)); report.AppendLine(string.Format(CultureInfo.InvariantCulture, " at {0} evaluations per pixel", D3D9Bench.EvaluationsPerTimedPixel)); string subtracted = ""; if (nanoseconds.ContainsKey(VariantCatalog.BaselineName)) { subtracted = ", after subtracting Null"; } report.AppendLine(" (relative is to " + relativeTo + subtracted + ")"); report.AppendLine(); var rows = new List { new[] { "Variant", "ns/eval", "minus Null", "relative" } }; foreach (Variant variant in variants) { double net = nanoseconds[variant.Name] - baseline; string relative = ""; if (variant.Name != VariantCatalog.BaselineName) { relative = Format(net / reference); } rows.Add(new[] { variant.Name, Format(nanoseconds[variant.Name]), Format(net), relative }); } AppendTable(report, rows, leadingColumns: 1); } public const string PieceControlName = "PieceHalleyTwo"; /// /// Piece shell tests: checks every variant's answers against the reference and against the control, then /// timed where they skip much and where they skip nothing. /// public static void AppendPieceShell(StringBuilder report, D3D9Bench bench, List pieceVariants, List sets, List references, Options options) { Console.WriteLine("Building piece shell tests..."); List nearEdge = PieceShellSets.FromSampleSets(sets, references, ShellTestSets.NearEdge, options.Seed); ShellTestSet rendered = PieceShellSets.Rendered(options.Side, options.Seed); List pieceSets = nearEdge.Concat(new[] { rendered }).ToList(); var shaders = pieceVariants.ToDictionary(variant => variant.Name, variant => bench.CompilePixelShader(variant, "PieceShellPS")); var statistics = pieceVariants.ToDictionary(variant => variant.Name, variant => new List()); var disagreements = pieceVariants.ToDictionary(variant => variant.Name, variant => 0); foreach (ShellTestSet set in pieceSets) { Console.WriteLine("Piece-testing {0}...", set.Name); double[] thickness = set.G.Select(Math.Sqrt).ToArray(); using (Texture samples = bench.UploadSamples(set.Samples, options.Side)) using (Texture thicknesses = bench.UploadValues(thickness, options.Side)) { var measured = new Dictionary(); foreach (Variant variant in pieceVariants) { measured[variant.Name] = bench.Measure(shaders[variant.Name], new[] { samples, thicknesses }, options.Side); statistics[variant.Name].Add(ShellStatistics.Measure(measured[variant.Name], set)); } if (measured.ContainsKey(PieceControlName)) { foreach (Variant variant in pieceVariants) { disagreements[variant.Name] += PieceShellSets.Disagreements(measured[variant.Name], measured[PieceControlName], set.Samples.Length); } } } } foreach (PixelShader shader in shaders.Values) { shader.Dispose(); } AppendWrapped(report, "", "Piece shell tests, ends included. The near-edge sets put each pixel's " + "thickness within 1e-9..1e-1 of its distance; Rendered spreads pixels around everyday pieces with " + "thin shells. Skipped counts the pixels the fat line settles without a distance."); report.AppendLine("worst miss / input-rounding floor (misses over 10 floors)"); report.AppendLine(); var rows = new List(); rows.Add(new[] { "Set", "Pixels", "Skipped" }.Concat(pieceVariants.Select(variant => variant.Name)) .ToArray()); for (int setIndex = 0; setIndex < pieceSets.Count; setIndex++) { int index = setIndex; ShellTestSet set = pieceSets[setIndex]; double skipped = 100.0 * PieceShellSets.FatLineSkips(set) / Math.Max(set.Samples.Length, 1); rows.Add(new[] { set.Name, set.Samples.Length.ToString(CultureInfo.InvariantCulture), Format(skipped) + "%" } .Concat(pieceVariants.Select(variant => { ShellStatistics result = statistics[variant.Name][index]; return Format(result.WorstSeverity) + " (" + result.SeriousMisses + ")"; })).ToArray()); } rows.Add(new[] { "Worst anywhere", "", "" }.Concat(pieceVariants.Select(variant => Format(statistics[variant.Name].Max(result => result.WorstSeverity)))).ToArray()); rows.Add(new[] { "Misses over 10 floors", "", "" }.Concat(pieceVariants.Select(variant => statistics[variant.Name].Sum(result => result.SeriousMisses).ToString(CultureInfo.InvariantCulture))) .ToArray()); rows.Add(new[] { "All misses", "", "" }.Concat(pieceVariants.Select(variant => statistics[variant.Name].Sum(result => result.Misclassified).ToString(CultureInfo.InvariantCulture))) .ToArray()); rows.Add(new[] { "Answers unlike " + PieceControlName, "", "" }.Concat(pieceVariants.Select(variant => disagreements[variant.Name].ToString(CultureInfo.InvariantCulture))).ToArray()); AppendTable(report, rows, leadingColumns: 3); if (!options.Timing) { return; } var timed = new[] { new KeyValuePair( "Timing, piece shell test: Rendered, where the fat line settles most pixels", new[] { PieceShellSets.Rendered(options.TimingSide, options.Seed) }), new KeyValuePair( "Timing, piece shell test: near edge, where it settles almost none", nearEdge.ToArray()), }; foreach (KeyValuePair timing in timed) { ShellSample[] samples = timing.Value.SelectMany(set => set.Samples).ToArray(); double[] thickness = timing.Value.SelectMany(set => set.G).Select(Math.Sqrt).ToArray(); using (Texture sampleTexture = bench.UploadSamples(samples, options.TimingSide)) using (Texture thicknessTexture = bench.UploadValues(thickness, options.TimingSide)) { AppendTiming(report, bench, pieceVariants, "PieceShellTimingPS", new[] { sampleTexture, thicknessTexture }, options, timing.Key, PieceControlName); } } } public const string TwoCurveControlName = "TwoHalley"; /// /// Every two-curve variant's answers on one set, uploaded as the first piece, the second, and the thicknesses. /// public static Dictionary MeasureTwoCurve(D3D9Bench bench, Dictionary shaders, TwoCurveSet set, int side) { var returnMe = new Dictionary(); using (Texture first = bench.UploadSamples(set.First, side)) using (Texture second = bench.UploadSamples(set.Second, side)) using (Texture thicknesses = bench.UploadValues(set.FirstThickness, side, set.SecondThickness)) { foreach (KeyValuePair shader in shaders) { // TwoCurvePS reads the second piece from s2, past the thicknesses in s1. returnMe[shader.Key] = bench.Measure(shader.Value, new[] { first, thicknesses, second }, side); } } return returnMe; } /// /// Two-curve tests: every variant's answers against the reference and against the control, which an exact /// optimization never changes, then timed on coherent tiles and on edge-crowded pixels. /// public static void AppendTwoCurve(StringBuilder report, D3D9Bench bench, List twoCurveVariants, Options options) { Console.WriteLine("Building two-curve tests..."); int count = options.Side * options.Side; int seed = options.Seed * 1000 + 700; var twoCurveSets = new List { TwoCurveSets.NearEdge("Corner, near edge", TwoCurveSets.CornerPlacer, count, seed), TwoCurveSets.NearEdge("Neck, near edge", TwoCurveSets.NeckPlacer, count, seed + 1), TwoCurveSets.Rendered("Corner, rendered", TwoCurveSets.CornerPlacer, options.Side, seed + 2), TwoCurveSets.Rendered("Neck, rendered", TwoCurveSets.NeckPlacer, options.Side, seed + 3), }; var shaders = twoCurveVariants.ToDictionary(variant => variant.Name, variant => bench.CompilePixelShader(variant, "TwoCurvePS")); var statistics = twoCurveVariants.ToDictionary(variant => variant.Name, variant => new List()); var disagreements = twoCurveVariants.ToDictionary(variant => variant.Name, variant => 0); foreach (TwoCurveSet set in twoCurveSets) { Console.WriteLine("Two-curve-testing {0}...", set.Name); Dictionary measured = MeasureTwoCurve(bench, shaders, set, options.Side); foreach (Variant variant in twoCurveVariants) { statistics[variant.Name].Add(TwoCurveSets.Measure(measured[variant.Name], set)); if (measured.ContainsKey(TwoCurveControlName)) { disagreements[variant.Name] += PieceShellSets.Disagreements(measured[variant.Name], measured[TwoCurveControlName], set.Count); } } } foreach (PixelShader shader in shaders.Values) { shader.Dispose(); } AppendWrapped(report, "", "Two-curve tests: inside when within the thickness of either piece. Corner " + "pieces meet end to end at any angle; Neck pieces bulge toward each other across a narrow gap. " + "Near-edge sets put each pixel's thickness within 1e-9..1e-1 of its distance to the nearer piece; " + "rendered sets lay pixels out in coherent tiles with thin shells. Skipped counts the per-piece " + "distances the fat line settles, out of two per pixel."); report.AppendLine("worst miss / input-rounding floor (misses over 10 floors)"); report.AppendLine(); var rows = new List(); rows.Add(new[] { "Set", "Pixels", "Skipped" }.Concat(twoCurveVariants.Select(variant => variant.Name)) .ToArray()); for (int setIndex = 0; setIndex < twoCurveSets.Count; setIndex++) { int index = setIndex; TwoCurveSet set = twoCurveSets[setIndex]; double skipped = 100.0 * TwoCurveSets.FatLineSkips(set) / Math.Max(2 * set.Count, 1); rows.Add(new[] { set.Name, set.Count.ToString(CultureInfo.InvariantCulture), Format(skipped) + "%" } .Concat(twoCurveVariants.Select(variant => { ShellStatistics result = statistics[variant.Name][index]; return Format(result.WorstSeverity) + " (" + result.SeriousMisses + ")"; })).ToArray()); } rows.Add(new[] { "Worst anywhere", "", "" }.Concat(twoCurveVariants.Select(variant => Format(statistics[variant.Name].Max(result => result.WorstSeverity)))).ToArray()); rows.Add(new[] { "Misses over 10 floors", "", "" }.Concat(twoCurveVariants.Select(variant => statistics[variant.Name].Sum(result => result.SeriousMisses).ToString(CultureInfo.InvariantCulture))) .ToArray()); rows.Add(new[] { "All misses", "", "" }.Concat(twoCurveVariants.Select(variant => statistics[variant.Name].Sum(result => result.Misclassified).ToString(CultureInfo.InvariantCulture))) .ToArray()); rows.Add(new[] { "Answers unlike " + TwoCurveControlName, "", "" }.Concat(twoCurveVariants.Select( variant => disagreements[variant.Name].ToString(CultureInfo.InvariantCulture))).ToArray()); AppendTable(report, rows, leadingColumns: 3); if (!options.Timing) { return; } int timingSeed = options.Seed * 1000 + 710; var timed = new[] { new KeyValuePair("Timing, two-curve test: rendered corners", TwoCurveSets.Rendered("Corner", TwoCurveSets.CornerPlacer, options.TimingSide, timingSeed)), new KeyValuePair("Timing, two-curve test: rendered necks", TwoCurveSets.Rendered("Neck", TwoCurveSets.NeckPlacer, options.TimingSide, timingSeed + 1)), new KeyValuePair("Timing, two-curve test: corners, near edge", TwoCurveSets.NearEdge("Corner", TwoCurveSets.CornerPlacer, options.TimingSide * options.TimingSide, timingSeed + 2)), }; foreach (KeyValuePair timing in timed) { using (Texture first = bench.UploadSamples(timing.Value.First, options.TimingSide)) using (Texture second = bench.UploadSamples(timing.Value.Second, options.TimingSide)) using (Texture thicknesses = bench.UploadValues(timing.Value.FirstThickness, options.TimingSide, timing.Value.SecondThickness)) { AppendTiming(report, bench, twoCurveVariants, "TwoCurveTimingPS", new[] { first, thicknesses, second }, options, timing.Key, TwoCurveControlName); } } } public static int Main(string[] args) { Options options = Options.Parse(args); string baseDirectory = AppDomain.CurrentDomain.BaseDirectory; string outputDirectory = options.OutputDirectory ?? Path.GetFullPath(Path.Combine(baseDirectory, "..", "Results")); Directory.CreateDirectory(outputDirectory); List variants = VariantCatalog.Load(VariantCatalog.FindShaderDirectory(baseDirectory)); if (options.VariantFilter != null) { variants = variants.Where(variant => variant.Name == VariantCatalog.BaselineName || options.VariantFilter.Contains(variant.Name)).ToList(); } List distanceVariants = variants.Where(variant => variant.Kind == VariantKind.Distance).ToList(); List measuredVariants = distanceVariants .Where(variant => variant.Name != VariantCatalog.BaselineName).ToList(); // Distance variants answer the shell question too, by comparing with the thickness. List shellVariants = variants.Where(variant => variant.Name != VariantCatalog.BaselineName && (variant.Kind == VariantKind.Distance || variant.Kind == VariantKind.ShellTest)).ToList(); List pieceVariants = variants.Where(variant => variant.Kind == VariantKind.PieceShellTest) .ToList(); List twoCurveVariants = variants.Where(variant => variant.Kind == VariantKind.TwoCurveTest) .ToList(); Console.WriteLine("Generating and solving {0} samples per set...", options.Side * options.Side); List sets = SampleSets.Create(options.Side * options.Side, options.Seed); List references = sets.Select(set => SolveAll(set.Samples)).ToList(); var report = new StringBuilder(); int exitCode = 0; using (var bench = new D3D9Bench(variants[0].Source)) { report.AppendLine("Blacklight.Bezier D3D9 benchmark"); report.AppendLine("Adapter: " + bench.AdapterDescription); report.AppendLine("Date: " + DateTime.Now.ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture)); report.AppendLine(string.Format(CultureInfo.InvariantCulture, "Samples: {0} per set, seed {1}", options.Side * options.Side, options.Seed)); report.AppendLine(); report.AppendLine("Variants:"); int nameWidth = variants.Max(variant => variant.Name.Length) + 2; foreach (Variant variant in variants) { AppendWrapped(report, " " + variant.Name.PadRight(nameWidth), variant.Description); } report.AppendLine(); var shaders = new Dictionary(); foreach (Variant variant in distanceVariants) { Console.WriteLine("Compiling {0}...", variant.Name); shaders[variant.Name] = bench.CompilePixelShader(variant, "DistancePS"); } var statistics = new Dictionary>(); var worstCases = measuredVariants.ToDictionary(variant => variant.Name, variant => new WorstCase()); int addressingMismatches = 0; for (int setIndex = 0; setIndex < sets.Count; setIndex++) { SampleSet set = sets[setIndex]; Console.WriteLine("Measuring {0}...", set.Name); using (Texture texture = bench.UploadSamples(set.Samples, options.Side)) { if (shaders.ContainsKey(VariantCatalog.BaselineName)) { addressingMismatches += AddressingMismatches( bench.Measure(shaders[VariantCatalog.BaselineName], texture, options.Side), set.Samples); } foreach (Variant variant in measuredVariants) { float[] measured = bench.Measure(shaders[variant.Name], texture, options.Side); AccuracyStatistics result = AccuracyStatistics.Measure(measured, set.Samples, references[setIndex]); if (!statistics.ContainsKey(variant.Name)) { statistics[variant.Name] = new List(); } statistics[variant.Name].Add(result); WorstCase worst = worstCases[variant.Name]; if (result.WorstIndex >= 0 && result.WorstRatio > worst.Ratio) { worst.SetName = set.Name; worst.Sample = set.Samples[result.WorstIndex]; worst.Reference = references[setIndex][result.WorstIndex]; worst.Measured = measured[result.WorstIndex]; worst.Ratio = result.WorstRatio; } } } } if (addressingMismatches > 0) { report.AppendLine(string.Format(CultureInfo.InvariantCulture, "ADDRESSING CHECK FAILED: {0} samples came back as a different sample's x. The accuracy " + "numbers below compare mismatched samples and mean nothing.", addressingMismatches)); report.AppendLine(); exitCode = 1; } else if (shaders.ContainsKey(VariantCatalog.BaselineName)) { report.AppendLine("Addressing check: every sample read back as its own x."); report.AppendLine(); } report.AppendLine("Accuracy: worst error / input-rounding floor [median] " + "(1 = as good as the inputs allow)"); report.AppendLine(); var rows = new List(); rows.Add(new[] { "Set" }.Concat(measuredVariants.Select(variant => variant.Name)).ToArray()); for (int setIndex = 0; setIndex < sets.Count; setIndex++) { int index = setIndex; rows.Add(new[] { sets[setIndex].Name }.Concat(measuredVariants.Select(variant => { AccuracyStatistics result = statistics[variant.Name][index]; return Format(result.WorstRatio) + " [" + Format(result.MedianRatio) + "]"; })).ToArray()); } rows.Add(new[] { "Worst anywhere" }.Concat(measuredVariants.Select(variant => Format(statistics[variant.Name].Max(result => result.WorstRatio)))).ToArray()); rows.Add(new[] { "Share above 10" }.Concat(measuredVariants.Select(variant => Format(100 * statistics[variant.Name].Average(result => result.ShareAboveTen)) + "%")).ToArray()); AppendTable(report, rows, leadingColumns: 1); report.AppendLine("Worst sample per variant:"); foreach (Variant variant in measuredVariants) { WorstCase worst = worstCases[variant.Name]; report.AppendLine(string.Format(CultureInfo.InvariantCulture, " {0}: {1} in \"{2}\"", variant.Name, Format(worst.Ratio), worst.SetName)); report.AppendLine(string.Format(CultureInfo.InvariantCulture, " x = {0:R}", worst.Sample.X)); report.AppendLine(string.Format(CultureInfo.InvariantCulture, " y = {0:R}", worst.Sample.Y)); report.AppendLine(string.Format(CultureInfo.InvariantCulture, " phi = {0:R}", worst.Sample.Phi)); report.AppendLine(string.Format(CultureInfo.InvariantCulture, " end = {0:R}", worst.Sample.End)); report.AppendLine(string.Format(CultureInfo.InvariantCulture, " reference = {0:R}", worst.Reference.Distance)); report.AppendLine(string.Format(CultureInfo.InvariantCulture, " measured = {0:R}", worst.Measured)); } report.AppendLine(); Console.WriteLine("Building shell tests..."); List shellSets = ShellTestSets.Create(sets, ShellTestSets.NearEdge, options.Seed); AppendShellAccuracy(report, bench, shellVariants, shellSets, options.Side); if (options.Timing) { Console.WriteLine("Timing..."); ShellSample[] mixed = sets.SelectMany(set => set.Samples).ToArray(); using (Texture texture = bench.UploadSamples(mixed, options.TimingSide)) { AppendTiming(report, bench, distanceVariants, "TimingPS", new[] { texture }, options, "Timing, distance: mixed samples from every set", options.RelativeTo); } // Real shell pixels mostly sit well clear of the edge, where the cascade's cheap first stage // answers; the edge-crowded accuracy samples would time its worst case instead. List typicalSets = ShellTestSets.Create(sets, ShellTestSets.Typical, options.Seed); ShellSample[] typicalSamples = typicalSets.SelectMany(set => set.Samples).ToArray(); double[] typicalG = typicalSets.SelectMany(set => set.G).ToArray(); using (Texture texture = bench.UploadSamples(typicalSamples, options.TimingSide, typicalG)) { AppendTiming(report, bench, variants.Where(variant => variant.Kind == VariantKind.Distance || variant.Kind == VariantKind.ShellTest).ToList(), "ShellTimingPS", new[] { texture }, options, "Timing, shell test: owned pixels, thickness up to e times either side of the distance", options.RelativeTo); } } if (pieceVariants.Count > 0) { AppendPieceShell(report, bench, pieceVariants, sets, references, options); } if (twoCurveVariants.Count > 0) { AppendTwoCurve(report, bench, twoCurveVariants, options); } foreach (PixelShader shader in shaders.Values) { shader.Dispose(); } } string text = report.ToString(); string reportPath = Path.Combine(outputDirectory, "report-" + DateTime.Now.ToString("yyyyMMdd-HHmmss", CultureInfo.InvariantCulture) + ".txt"); File.WriteAllText(reportPath, text); Console.WriteLine(); Console.WriteLine(text); Console.WriteLine("Written to " + reportPath); return exitCode; } } }