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