using System;
using System.Collections.Generic;
using Annulus;
using Azimuth;
using Blacklight.Core;
using Blacklight.Core.Drawables;
using Prism;
namespace Blacklight.SharpDX9.PerformanceTests
{
///
/// What a scene needs to build itself.
///
public sealed class SceneContext
{
public SharpDX9WindowHandle Handle;
///
/// Eight 32 pixel frames, four across, each a different colour with a gradient that shows which way it faces.
///
public SpriteSheet Sheet;
///
/// The same frames with their colour already multiplied by alpha.
///
public SpriteSheet PremultipliedSheet;
///
/// A small checker pattern for screen space fills.
///
public Texture FillTexture;
}
///
/// A scene drawn for its pixels, which two builds of the shader are compared on.
///
public sealed class ImageScene
{
public string Name;
public Func Build;
public const int FrameSide = 32;
public const int FramesAcross = 4;
public const int FramesDown = 2;
public static DecodedImage BuildFrames(bool premultiplied)
{
int width = FrameSide * FramesAcross;
int height = FrameSide * FramesDown;
var rgba = new byte[width * height * DecodedImage.BytesPerPixel];
for (int y = 0; y < height; ++y)
{
for (int x = 0; x < width; ++x)
{
int frame = x / FrameSide + FramesAcross * (y / FrameSide);
int frameX = x % FrameSide;
int frameY = y % FrameSide;
bool edge = frameX == 0 || frameY == 0 || frameX == FrameSide - 1 || frameY == FrameSide - 1;
double red = edge ? 255 : frameX * 8;
double green = edge ? 0 : frameY * 8;
double blue = (frame * 37 + (((frameX / 4) + (frameY / 4)) % 2) * 90) % 256;
double alpha = 255;
if (frame == 5)
{
alpha = 40 + frameX * 6;
}
if (premultiplied)
{
red = red * alpha / 255;
green = green * alpha / 255;
blue = blue * alpha / 255;
}
int index = (y * width + x) * DecodedImage.BytesPerPixel;
rgba[index + 0] = (byte)red;
rgba[index + 1] = (byte)green;
rgba[index + 2] = (byte)blue;
rgba[index + 3] = (byte)alpha;
}
}
return new DecodedImage(rgba, new System.Drawing.Size(width, height),
premultiplied ? DecodedImage.AlphaMode.Premultiplied : DecodedImage.AlphaMode.Straight);
}
public static DecodedImage BuildChecker()
{
const int side = 8;
var rgba = new byte[side * side * DecodedImage.BytesPerPixel];
for (int y = 0; y < side; ++y)
{
for (int x = 0; x < side; ++x)
{
byte value = (byte)(((x / 2 + y / 2) % 2 == 0) ? 255 : 120);
int index = (y * side + x) * DecodedImage.BytesPerPixel;
rgba[index + 0] = value;
rgba[index + 1] = (byte)(value / 2 + 60);
rgba[index + 2] = 255;
rgba[index + 3] = 255;
}
}
return new DecodedImage(rgba, new System.Drawing.Size(side, side));
}
private static SpriteBatch NewBatch(SpriteSheet sheet, int capacity,
SpriteBatch.Varying varying = SpriteBatch.Varying.None)
{
return new SpriteBatch(capacity, varying) { Texture = sheet.Texture };
}
private static SpriteSource FrameOf(SpriteSheet sheet, int frame)
{
return sheet.Frame(new System.Drawing.Point(frame % FramesAcross, frame / FramesAcross % FramesDown));
}
private static void Add(SpriteBatch batch, SpriteSource source, double x, double y, double width,
double height)
{
batch.Add(new SpriteBatch.Entry(source, SpritePlacement.Corners(
new Vector(x, y), new Vector(x + width, y + height))));
}
private static Scene SceneOf(params (SpriteBatch batch, AffineMatrix transform, Rect? clip)[] entities)
{
var cel = new Cel();
foreach ((SpriteBatch batch, AffineMatrix transform, Rect? clip) in entities)
{
cel.AddEntity(new Entity(batch, transform, clip));
}
var scene = new Scene();
scene.Cels.Add(cel);
return scene;
}
private static (SpriteBatch, AffineMatrix, Rect?) At(SpriteBatch batch, AffineMatrix? transform = null,
Rect? clip = null)
{
return (batch, transform ?? AffineMatrix.Identity, clip);
}
public static List All()
{
var returnMe = new List();
returnMe.Add(new ImageScene
{
Name = "scales",
Build = context =>
{
var batch = NewBatch(context.Sheet, 32);
double[] scales = { 2.0, 1.5, 1.0, 0.75, 0.5, 0.33, 0.2, 0.1 };
double x = 4;
for (int i = 0; i < scales.Length; ++i)
{
Add(batch, FrameOf(context.Sheet, i), x, 4, 32 * scales[i], 32 * scales[i]);
Add(batch, FrameOf(context.Sheet, i + 3), x, 80, 32 * scales[i], 32 * scales[i] * 0.6);
Add(batch, FrameOf(context.Sheet, i + 5), x, 130, 32 * scales[i] * 0.45, 32 * scales[i]);
x += 32 * scales[i] + 6;
}
return SceneOf(At(batch));
},
});
returnMe.Add(new ImageScene
{
Name = "transforms",
Build = context =>
{
var batch = NewBatch(context.Sheet, 2);
Add(batch, FrameOf(context.Sheet, 1), -24, -24, 48, 48);
Add(batch, FrameOf(context.Sheet, 4), 30, -10, 40, 20);
return SceneOf(
At(batch, new AffineMatrix(0.4, new Vector(80, 80))),
At(batch, new AffineMatrix(0, new Vector(220, 80), new Vector(0.5, 2))),
At(batch, new AffineMatrix(0, new Vector(360, 80), new Vector(-1, 1))),
At(batch, new AffineMatrix(0, new Vector(500, 80), new Vector(1, -1))),
At(batch, new AffineMatrix(1, 0.5, 80, 0, 1, 220)),
At(batch, new AffineMatrix(0.7, new Vector(250, 230), 0.3)),
At(batch, new AffineMatrix(2.1, new Vector(400, 230), new Vector(1.7, 0.4))));
},
});
// The left half is one batch with a transform per entry. The right half draws the same sprites as separate
// entities, so the halves should match pixel for pixel.
returnMe.Add(new ImageScene
{
Name = "entry-transforms",
Build = context =>
{
const int count = 6;
var rotated = NewBatch(context.Sheet, count, SpriteBatch.Varying.Transform);
var entities = new List<(SpriteBatch, AffineMatrix, Rect?)>();
for (int i = 0; i < count; ++i)
{
Add(rotated, FrameOf(context.Sheet, i), -20, -20, 40, 40);
var entryTransform = new AffineMatrix(
0.07 * i, new Vector(40 + 60 * (i % 3), 40 + 70 * (i / 3)), new Vector(1 + 0.2 * i, 1));
rotated.TransformColumn[rotated.StorageOffset + i] = entryTransform;
var single = NewBatch(context.Sheet, 1);
Add(single, FrameOf(context.Sheet, i), -20, -20, 40, 40);
entities.Add(At(single, AffineMatrix.Multiply(
new AffineMatrix(0, new Vector(310, 5)), entryTransform)));
}
entities.Insert(0, At(rotated, new AffineMatrix(0, new Vector(10, 5))));
return SceneOf(entities.ToArray());
},
});
returnMe.Add(new ImageScene
{
Name = "tiling",
Build = context =>
{
var entities = new List<(SpriteBatch, AffineMatrix, Rect?)>();
TilingBehavior[] modes =
{
TilingBehavior.None, TilingBehavior.Tile, TilingBehavior.Stretch,
TilingBehavior.StretchAndTile,
};
for (int row = 0; row < 4; ++row)
{
for (int column = 0; column < 4; ++column)
{
var batch = NewBatch(context.Sheet, 1);
batch.TilingBehaviorX = modes[column];
batch.TilingBehaviorY = modes[row];
Add(batch, FrameOf(context.Sheet, row * 2 + column % 2), 0, 0, 83, 67);
var position = new Vector(10 + column * 100, 10 + row * 80);
entities.Add(At(batch, new AffineMatrix(0, position)));
}
}
return SceneOf(entities.ToArray());
},
});
returnMe.Add(new ImageScene
{
Name = "swizzle",
Build = context =>
{
var batch = NewBatch(context.Sheet, 8);
ColorChannel[][] channels =
{
new[] { ColorChannel.R, ColorChannel.G, ColorChannel.B, ColorChannel.A },
new[]
{
ColorChannel.Default, ColorChannel.Default, ColorChannel.Default, ColorChannel.Default,
},
new[] { ColorChannel.G, ColorChannel.R, ColorChannel.B, ColorChannel.A },
new[] { ColorChannel.R, ColorChannel.R, ColorChannel.R, ColorChannel.One },
new[] { ColorChannel.One, ColorChannel.One, ColorChannel.One, ColorChannel.R },
new[] { ColorChannel.Zero, ColorChannel.G, ColorChannel.Zero, ColorChannel.One },
new[] { ColorChannel.A, ColorChannel.A, ColorChannel.A, ColorChannel.A },
new[] { ColorChannel.R, ColorChannel.G, ColorChannel.B, ColorChannel.Default },
};
for (int i = 0; i < channels.Length; ++i)
{
SpriteSource source = FrameOf(context.Sheet, 5 + i % 3);
source.RedChannel = channels[i][0];
source.GreenChannel = channels[i][1];
source.BlueChannel = channels[i][2];
source.AlphaChannel = channels[i][3];
Add(batch, source, 10 + (i % 4) * 90, 10 + (i / 4) * 90, 80, 80);
}
return SceneOf(At(batch));
},
});
returnMe.Add(new ImageScene
{
Name = "tint-and-blend",
Build = context =>
{
var tinted = NewBatch(context.Sheet, 4);
tinted.TintColor = new TintColor(0.5, 0.8, 1, 0.7);
for (int i = 0; i < 4; ++i)
{
Add(tinted, FrameOf(context.Sheet, i), 10 + i * 30, 10 + i * 10, 60, 60);
}
var additive = NewBatch(context.Sheet, 4);
additive.Additiveness = 0.5;
for (int i = 0; i < 4; ++i)
{
Add(additive, FrameOf(context.Sheet, i + 4), 200 + i * 30, 10 + i * 10, 60, 60);
}
var columns = NewBatch(context.Sheet, 6,
SpriteBatch.Varying.TintColor | SpriteBatch.Varying.Additiveness);
for (int i = 0; i < 6; ++i)
{
Add(columns, FrameOf(context.Sheet, i), 10 + i * 40, 150, 60, 60);
columns.TintColorColumn[columns.StorageOffset + i] =
new TintColor(1, 1 - i * 0.15, 0.4 + i * 0.1, 0.4 + i * 0.1);
columns.AdditivenessColumn[columns.StorageOffset + i] = i * 0.2;
}
return SceneOf(At(tinted), At(additive), At(columns));
},
});
// Each row draws one sprite five times over a backdrop, fading its tint alpha from 1 to 0. Every fade
// should land on the backdrop at 0.
returnMe.Add(new ImageScene
{
Name = "additive-fade",
Build = context =>
{
// One texel each, so every sprite is a flat colour and the fades can be read off exactly.
SpriteSource backdropTexel = FrameOf(context.Sheet, 6);
backdropTexel.Origin += new System.Drawing.Size(10, 10);
backdropTexel.Size = new System.Drawing.Size(1, 1);
SpriteSource spriteTexel = FrameOf(context.Sheet, 3);
spriteTexel.Origin += new System.Drawing.Size(12, 12);
spriteTexel.Size = new System.Drawing.Size(1, 1);
var backdrop = NewBatch(context.Sheet, 1);
Add(backdrop, backdropTexel, 0, 0, 400, 240);
var rows = new List<(SpriteBatch, AffineMatrix, Rect?)> { At(backdrop) };
double[] additiveness = { 1, 0.5, 0 };
for (int row = 0; row < additiveness.Length; ++row)
{
var fading = NewBatch(context.Sheet, 5, SpriteBatch.Varying.TintColor);
fading.Additiveness = additiveness[row];
for (int i = 0; i < 5; ++i)
{
Add(fading, spriteTexel, 10 + i * 75, 10 + row * 75, 60, 60);
fading.TintColorColumn[fading.StorageOffset + i] = new TintColor(1, 1, 1, 1 - 0.25 * i);
}
rows.Add(At(fading));
}
return SceneOf(rows.ToArray());
},
});
returnMe.Add(new ImageScene
{
Name = "tint-texture",
Build = context =>
{
var batch = NewBatch(context.Sheet, 3);
batch.TintTexture = context.FillTexture;
for (int i = 0; i < 3; ++i)
{
Add(batch, FrameOf(context.Sheet, i), 10 + i * 100, 10, 90, 90);
}
var minified = NewBatch(context.Sheet, 1);
minified.TintTexture = context.FillTexture;
Add(minified, FrameOf(context.Sheet, 3), 10, 130, 20, 20);
return SceneOf(At(batch), At(minified));
},
});
returnMe.Add(new ImageScene
{
Name = "gamma",
Build = context =>
{
var entities = new List<(SpriteBatch, AffineMatrix, Rect?)>();
double[] gammas = { 1, 1.4, 0.7, 2.5, 1.0005, 0.9995, 5, 0.01 };
for (int i = 0; i < gammas.Length; ++i)
{
var batch = NewBatch(context.Sheet, 1);
batch.CoverageGamma = gammas[i];
Add(batch, FrameOf(context.Sheet, 5), 0, 0, 64, 64);
var position = new Vector(10 + (i % 4) * 80, 10 + (i / 4) * 80);
entities.Add(At(batch, new AffineMatrix(0, position)));
}
return SceneOf(entities.ToArray());
},
});
returnMe.Add(new ImageScene
{
Name = "premultiplied",
Build = context =>
{
var batch = NewBatch(context.PremultipliedSheet, 8);
for (int i = 0; i < 8; ++i)
{
Add(batch, FrameOf(context.PremultipliedSheet, i),
10 + (i % 4) * 90, 10 + (i / 4) * 90, 80, 80);
}
var minified = NewBatch(context.PremultipliedSheet, 3);
for (int i = 0; i < 3; ++i)
{
Add(minified, FrameOf(context.PremultipliedSheet, 5), 10 + i * 40, 200, 30 - i * 8, 30 - i * 8);
}
return SceneOf(At(batch), At(minified));
},
});
returnMe.Add(new ImageScene
{
Name = "clipped-sprites",
Build = context =>
{
var batch = NewBatch(context.Sheet, 1);
Add(batch, FrameOf(context.Sheet, 6), 0, 0, 64, 64);
Rect window = Rect.FromPositionAndSize(new Vector(10.5, 7.25), new Vector(30.3, 40.7));
return SceneOf(
At(batch, new AffineMatrix(0, new Vector(10, 10)), window),
At(batch, new AffineMatrix(0, new Vector(120, 10), 2), window),
At(batch, new AffineMatrix(0, new Vector(300, 10), new Vector(-1.5, 1)), window),
At(batch, new AffineMatrix(0, new Vector(10, 150), 0.5),
Rect.FromPositionAndSize(new Vector(-5, 20), new Vector(100, 22))),
At(batch, new AffineMatrix(0, new Vector(120, 200)),
Rect.FromPositionAndSize(new Vector(500, 500), new Vector(10, 10))),
At(batch, new AffineMatrix(0, new Vector(200, 200)),
Rect.FromPositionAndSize(new Vector(-1000, -1000), new Vector(5000, 5000))));
},
});
returnMe.Add(new ImageScene
{
Name = "shapes",
Build = context =>
{
var cel = new Cel();
var filled = new Core.Drawables.Rectangle
{
ShellColor = Colors.Orange,
InsideColor = Colors.Cyan,
ShellPixelThickness = 6,
ShellFillTexture = context.FillTexture,
InsideFillTexture = context.FillTexture,
};
var plain = new Core.Drawables.Rectangle
{
ShellColor = Colors.Red,
InsideColor = new Color(0.2, 0.6, 0.3, 0.8),
ShellPixelThickness = 3,
};
var ellipse = new Ellipse
{
ShellColor = Colors.Yellow,
InsideColor = new Color(0.1, 0.1, 0.8, 0.9),
ShellPixelThickness = 4,
InsideFillTexture = context.FillTexture,
};
Rect window = Rect.FromPositionAndSize(new Vector(-0.4, -0.7), new Vector(1.2, 1.1));
cel.AddEntity(new Entity(filled,
Core.Drawables.Rectangle.ModelToWorld(new Vector(100, 80), new Vector(80, 60)), null));
cel.AddEntity(new Entity(plain,
Core.Drawables.Rectangle.ModelToWorld(new Vector(100, 80), new Vector(220, 60), 0.3), null));
cel.AddEntity(new Entity(filled,
Core.Drawables.Rectangle.ModelToWorld(new Vector(100, 80), new Vector(360, 60)), window));
cel.AddEntity(new Entity(ellipse, new AffineMatrix(0, new Vector(80, 180), 40), null));
cel.AddEntity(new Entity(ellipse, new AffineMatrix(0, new Vector(220, 180), 40), window));
cel.AddEntity(new Entity(plain,
Core.Drawables.Rectangle.ModelToWorld(new Vector(100, 80), new Vector(360, 180)), window));
// Windows that hold the whole shape, which the shader can tell without testing a pixel.
Rect roomy = Rect.FromPositionAndSize(new Vector(-2, -2), new Vector(4, 4));
cel.AddEntity(new Entity(ellipse, new AffineMatrix(0, new Vector(500, 180), 40), roomy));
cel.AddEntity(new Entity(ellipse, new AffineMatrix(0, new Vector(580, 60), 30),
Rect.FromPositionAndSize(new Vector(-1, -1), new Vector(2, 2))));
cel.AddEntity(new Entity(ellipse, new AffineMatrix(0, new Vector(580, 180), 30),
Rect.FromPositionAndSize(new Vector(-0.99, -1), new Vector(2, 2))));
var scene = new Scene();
scene.Cels.Add(cel);
return scene;
},
});
// One band per transform. A screen space fill doesn't depend on where its shape is, so every band's row
// can be checked against the first band's.
returnMe.Add(new ImageScene
{
Name = "fill-transforms",
Build = context =>
{
var placements = new AffineMatrix[]
{
AffineMatrix.Identity,
new AffineMatrix(0, Vector.Zero, 2),
new AffineMatrix(0, new Vector(1, 0)),
new AffineMatrix(0, Vector.Zero, new Vector(4, 1)),
new AffineMatrix(0, new Vector(800000, 0)),
new AffineMatrix(0, new Vector(800001, 0)),
new AffineMatrix(Math.PI / 2, Vector.Zero, new Vector(4, 1)),
new AffineMatrix(1, 0.5, 3, 0.25, 2, 5),
};
var cel = new Cel();
for (int i = 0; i < placements.Length; ++i)
{
var band = new Core.Drawables.Rectangle
{
InsideColor = Colors.White,
InsideFillTexture = context.FillTexture,
InsideFillTransform = placements[i],
};
cel.AddEntity(new Entity(band,
Core.Drawables.Rectangle.ModelToWorld(new Vector(600, 30), new Vector(320, 25 + 40 * i)),
null));
}
// A thick shell with its own transform around an interior that has none.
var framed = new Core.Drawables.Rectangle
{
ShellColor = Colors.White,
InsideColor = Colors.White,
ShellPixelThickness = 12,
ShellFillTexture = context.FillTexture,
ShellFillTransform = new AffineMatrix(0, Vector.Zero, 2),
InsideFillTexture = context.FillTexture,
};
cel.AddEntity(new Entity(framed,
Core.Drawables.Rectangle.ModelToWorld(new Vector(600, 60), new Vector(320, 25 + 40 * 8 + 15)),
null));
var scene = new Scene();
scene.Cels.Add(cel);
return scene;
},
});
// A model space fill spans the same four texels across a rectangle of any size. The screen space control
// makes the same placement a quarter of a pixel per texel.
returnMe.Add(new ImageScene
{
Name = "fill-spaces",
Build = context =>
{
var quarter = new AffineMatrix(0, Vector.Zero, 0.25);
var cel = new Cel();
var screen = new Core.Drawables.Rectangle
{
InsideColor = Colors.White,
InsideFillTexture = context.FillTexture,
InsideFillTransform = quarter,
};
var model = screen;
model.InsideFillSpace = FillSpace.Model;
var modelShell = new Core.Drawables.Rectangle
{
ShellColor = Colors.White,
InsideColor = Colors.White,
ShellPixelThickness = 12,
ShellFillTexture = context.FillTexture,
ShellFillTransform = quarter,
ShellFillSpace = FillSpace.Model,
};
cel.AddEntity(new Entity(model,
Core.Drawables.Rectangle.ModelToWorld(new Vector(160, 80), new Vector(100, 60)), null));
cel.AddEntity(new Entity(model,
Core.Drawables.Rectangle.ModelToWorld(new Vector(320, 80), new Vector(400, 60)), null));
cel.AddEntity(new Entity(screen,
Core.Drawables.Rectangle.ModelToWorld(new Vector(160, 80), new Vector(100, 180)), null));
cel.AddEntity(new Entity(modelShell,
Core.Drawables.Rectangle.ModelToWorld(new Vector(160, 80), new Vector(400, 180)), null));
var scene = new Scene();
scene.Cels.Add(cel);
return scene;
},
});
return returnMe;
}
}
}