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