diff --git a/CHANGELOG.md b/CHANGELOG.md
index 133752d..cd56cd9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,7 +2,7 @@
MLEM tries to adhere to [semantic versioning](https://semver.org/). Potentially breaking changes are written in **bold**.
Jump to version:
-- [7.2.0](#720-in-development)
+- [8.0.0](#800-in-development)
- [7.1.1](#711)
- [7.1.0](#710)
- [7.0.0](#700)
@@ -16,7 +16,7 @@ Jump to version:
- [5.1.0](#510)
- [5.0.0](#500)
-## 7.2.0 (In Development)
+## 8.0.0 (In Development)
### MLEM
Fixes
@@ -35,6 +35,7 @@ Fixes
### MLEM.Data
Improvements
+- **Use a binary tree algorithm for RuntimeTexturePacker to vastly increase packing speed**
- Made fields and methods in StaticJsonConverter protected to allow extending it
## 7.1.1
diff --git a/MLEM.Data/RuntimeTexturePacker.cs b/MLEM.Data/RuntimeTexturePacker.cs
index 9d2f66f..0b147e9 100644
--- a/MLEM.Data/RuntimeTexturePacker.cs
+++ b/MLEM.Data/RuntimeTexturePacker.cs
@@ -4,7 +4,6 @@ using System.Diagnostics;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
-using MLEM.Maths;
using MLEM.Textures;
using static MLEM.Textures.TextureExtensions;
@@ -14,6 +13,9 @@ namespace MLEM.Data {
/// Packing textures in this manner allows for faster rendering, as fewer texture swaps are required.
/// The resulting texture segments are returned as instances.
///
+ ///
+ /// The algorithm used by this implementation is based on the blog post "Binary Tree Bin Packing Algorithm", which can be found at https://codeincomplete.com/articles/bin-packing/.
+ ///
public class RuntimeTexturePacker : IDisposable {
///
@@ -36,31 +38,21 @@ namespace MLEM.Data {
///
/// The amount of currently packed texture regions.
///
- public int PackedTextures => this.packedTextures.Count;
+ public int PackedTextures => this.PackedTexture != null ? this.requests.Count : 0;
- private readonly List texturesToPack = new List();
- private readonly List packedTextures = new List();
- private readonly Dictionary occupiedPositions = new Dictionary();
- private readonly Dictionary initialPositions = new Dictionary();
+ private readonly List requests = new List();
private readonly Dictionary dataCache = new Dictionary();
- private readonly bool autoIncreaseMaxWidth;
private readonly bool forcePowerOfTwo;
private readonly bool forceSquare;
private readonly bool disposeTextures;
- private int maxWidth;
-
///
/// Creates a new runtime texture packer with the given settings.
///
- /// The maximum width that the packed texture can have. Defaults to 2048.
- /// Whether the maximum width should be increased if there is a texture to be packed that is wider than the maximum width specified in the constructor. Defaults to false.
/// Whether the resulting should have a width and height that is a power of two.
/// Whether the resulting should be square regardless of required size.
/// Whether the original textures submitted to this texture packer should be disposed after packing.
- public RuntimeTexturePacker(int maxWidth = 2048, bool autoIncreaseMaxWidth = false, bool forcePowerOfTwo = false, bool forceSquare = false, bool disposeTextures = false) {
- this.maxWidth = maxWidth;
- this.autoIncreaseMaxWidth = autoIncreaseMaxWidth;
+ public RuntimeTexturePacker(bool forcePowerOfTwo = false, bool forceSquare = false, bool disposeTextures = false) {
this.forcePowerOfTwo = forcePowerOfTwo;
this.forceSquare = forceSquare;
this.disposeTextures = disposeTextures;
@@ -145,15 +137,7 @@ namespace MLEM.Data {
/// Whether the texture's padding should be filled with a copy of the texture's border, rather than transparent pixels. This value only has an effect if is greater than 0.
/// Thrown when trying to add a texture width a width greater than the defined max width.
public void Add(TextureRegion texture, Action result, int padding = 0, bool padWithPixels = false) {
- var paddedWidth = texture.Width + 2 * padding;
- if (paddedWidth > this.maxWidth) {
- if (this.autoIncreaseMaxWidth) {
- this.maxWidth = paddedWidth;
- } else {
- throw new InvalidOperationException($"Cannot add texture with width {texture.Width} to a texture packer with max width {this.maxWidth}");
- }
- }
- this.texturesToPack.Add(new Request(texture, result, padding, padWithPixels));
+ this.requests.Add(new Request(texture, result, padding, padWithPixels));
}
///
@@ -163,19 +147,32 @@ namespace MLEM.Data {
///
/// The graphics device to use for texture generation
public void Pack(GraphicsDevice device) {
- // set pack areas for each request
- // we pack larger textures first, so that smaller textures can fit in the gaps that larger ones leave
+ // set pack areas for each request based on the algo in https://codeincomplete.com/articles/bin-packing/
var stopwatch = Stopwatch.StartNew();
- foreach (var request in this.texturesToPack.OrderByDescending(t => t.Texture.Width * t.Texture.Height)) {
- request.PackedArea = this.OccupyFreeArea(request);
- this.packedTextures.Add(request);
+ RequestNode root = null;
+ foreach (var request in this.requests.OrderByDescending(t => Math.Max(t.Texture.Width, t.Texture.Height) + t.Padding * 2)) {
+ var size = new Point(request.Texture.Width, request.Texture.Height);
+ size.X += request.Padding * 2;
+ size.Y += request.Padding * 2;
+
+ if (root == null)
+ root = new RequestNode(0, 0, size.X, size.Y);
+
+ var node = RuntimeTexturePacker.FindNode(size, root);
+ if (node == null) {
+ root = RuntimeTexturePacker.GrowNode(size, root);
+ node = RuntimeTexturePacker.FindNode(size, root);
+ }
+
+ request.Node = node;
+ node.Split(size);
}
stopwatch.Stop();
this.LastCalculationTime = stopwatch.Elapsed;
// figure out texture size and regenerate texture if necessary
- var width = this.packedTextures.Max(t => t.PackedArea.Right);
- var height = this.packedTextures.Max(t => t.PackedArea.Bottom);
+ var width = root.Area.Width;
+ var height = root.Area.Height;
if (this.forcePowerOfTwo) {
width = RuntimeTexturePacker.ToPowerOfTwo(width);
height = RuntimeTexturePacker.ToPowerOfTwo(height);
@@ -184,26 +181,24 @@ namespace MLEM.Data {
width = height = Math.Max(width, height);
// if we don't need to regenerate, we only need to add newly added regions
- IEnumerable texturesToCopy = this.texturesToPack;
if (this.PackedTexture == null || this.PackedTexture.Width != width || this.PackedTexture.Height != height) {
this.PackedTexture?.Dispose();
this.PackedTexture = new Texture2D(device, width, height);
- // if we need to regenerate, we need to copy all regions since the old ones were deleted
- texturesToCopy = this.packedTextures;
}
// copy texture data onto the packed texture
stopwatch.Restart();
using (var data = this.PackedTexture.GetTextureData()) {
- foreach (var request in texturesToCopy)
+ foreach (var request in this.requests)
this.CopyRegion(data, request);
}
stopwatch.Stop();
this.LastPackTime = stopwatch.Elapsed;
// invoke callbacks for textures we copied
- foreach (var request in texturesToCopy) {
- var packedArea = request.PackedArea.Shrink(new Point(request.Padding, request.Padding));
+ foreach (var request in this.requests) {
+ var packedLoc = request.Node.Area.Location + new Point(request.Padding, request.Padding);
+ var packedArea = new Rectangle(packedLoc.X, packedLoc.Y, request.Texture.Width, request.Texture.Height);
request.Result.Invoke(new TextureRegion(this.PackedTexture, packedArea) {
Pivot = request.Texture.Pivot,
Name = request.Texture.Name,
@@ -213,7 +208,6 @@ namespace MLEM.Data {
request.Texture.Texture.Dispose();
}
- this.texturesToPack.Clear();
this.dataCache.Clear();
}
@@ -225,10 +219,7 @@ namespace MLEM.Data {
this.PackedTexture = null;
this.LastCalculationTime = TimeSpan.Zero;
this.LastPackTime = TimeSpan.Zero;
- this.texturesToPack.Clear();
- this.packedTextures.Clear();
- this.initialPositions.Clear();
- this.occupiedPositions.Clear();
+ this.requests.Clear();
this.dataCache.Clear();
}
@@ -237,53 +228,9 @@ namespace MLEM.Data {
this.Reset();
}
- private Rectangle OccupyFreeArea(Request request) {
- var size = new Point(request.Texture.Width, request.Texture.Height);
- size.X += request.Padding * 2;
- size.Y += request.Padding * 2;
-
- // exit early if the texture doesn't need to find a free location
- if (size.X <= 0 || size.Y <= 0)
- return Rectangle.Empty;
-
- var pos = this.initialPositions.TryGetValue(size, out var first) ? first : Point.Zero;
- var area = new Rectangle(pos.X, pos.Y, size.X, size.Y);
- var lowestY = int.MaxValue;
- while (true) {
- // check if the current area is already occupied
- if (!this.occupiedPositions.TryGetValue(area.Location, out var existing)) {
- existing = this.packedTextures.FirstOrDefault(t => t.PackedArea.Intersects(area));
- // if no texture is occupying this space, we have found a free area
- if (existing == null) {
- // if this is the first position that this request fit in, no other requests of the same size will find a position before it
- this.initialPositions[new Point(area.Width, area.Height)] = area.Location;
- this.occupiedPositions.Add(area.Location, request);
- return area;
- }
-
- // also cache the existing texture for this position, in case we check it again in the future
- this.occupiedPositions.Add(area.Location, existing);
- }
-
- // move to the right by the existing texture's width
- area.X = existing.PackedArea.Right;
-
- // remember the smallest intersecting texture's height for when we move down
- if (lowestY > existing.PackedArea.Bottom)
- lowestY = existing.PackedArea.Bottom;
-
- // move down a row if we exceed our maximum width
- if (area.Right > this.maxWidth) {
- area.X = 0;
- area.Y = lowestY;
- lowestY = int.MaxValue;
- }
- }
- }
-
private void CopyRegion(TextureData destination, Request request) {
var data = this.GetCachedTextureData(request.Texture.Texture);
- var location = request.PackedArea.Location + new Point(request.Padding, request.Padding);
+ var location = request.Node.Area.Location + new Point(request.Padding, request.Padding);
for (var x = -request.Padding; x < request.Texture.Width + request.Padding; x++) {
for (var y = -request.Padding; y < request.Texture.Height + request.Padding; y++) {
Color srcColor;
@@ -328,13 +275,57 @@ namespace MLEM.Data {
return ret;
}
+ private static RequestNode FindNode(Point requestSize, RequestNode node) {
+ if (node.Down != null && node.Right != null) {
+ return RuntimeTexturePacker.FindNode(requestSize, node.Right) ?? RuntimeTexturePacker.FindNode(requestSize, node.Down);
+ } else if (requestSize.X <= node.Area.Width && requestSize.Y <= node.Area.Height) {
+ return node;
+ } else {
+ return null;
+ }
+ }
+
+ private static RequestNode GrowNode(Point requestSize, RequestNode node) {
+ var canGrowDown = requestSize.X <= node.Area.Width;
+ var canGrowRight = requestSize.Y <= node.Area.Height;
+
+ var shouldGrowRight = canGrowRight && node.Area.Height >= node.Area.Width + requestSize.X;
+ var shouldGrowDown = canGrowDown && node.Area.Width >= node.Area.Height + requestSize.Y;
+
+ if (shouldGrowRight) {
+ return RuntimeTexturePacker.GrowNodeRight(requestSize, node);
+ } else if (shouldGrowDown) {
+ return RuntimeTexturePacker.GrowNodeDown(requestSize, node);
+ } else if (canGrowRight) {
+ return RuntimeTexturePacker.GrowNodeRight(requestSize, node);
+ } else if (canGrowDown) {
+ return RuntimeTexturePacker.GrowNodeDown(requestSize, node);
+ } else {
+ return null;
+ }
+ }
+
+ private static RequestNode GrowNodeRight(Point requestSize, RequestNode node) {
+ return new RequestNode(0, 0, node.Area.Width + requestSize.X, node.Area.Height) {
+ Right = new RequestNode(node.Area.Width, 0, requestSize.X, node.Area.Height),
+ Down = node
+ };
+ }
+
+ private static RequestNode GrowNodeDown(Point requestSize, RequestNode node) {
+ return new RequestNode(0, 0, node.Area.Width, node.Area.Height + requestSize.Y) {
+ Right = node,
+ Down = new RequestNode(0, node.Area.Height, node.Area.Width, requestSize.Y)
+ };
+ }
+
private class Request {
public readonly TextureRegion Texture;
public readonly Action Result;
public readonly int Padding;
public readonly bool PadWithPixels;
- public Rectangle PackedArea;
+ public RequestNode Node;
public Request(TextureRegion texture, Action result, int padding, bool padWithPixels) {
this.Texture = texture;
@@ -345,5 +336,22 @@ namespace MLEM.Data {
}
+ private class RequestNode {
+
+ public readonly Rectangle Area;
+ public RequestNode Down;
+ public RequestNode Right;
+
+ public RequestNode(int x, int y, int width, int height) {
+ this.Area = new Rectangle(x, y, width, height);
+ }
+
+ public void Split(Point requestSize) {
+ this.Down = new RequestNode(this.Area.X, this.Area.Y + requestSize.Y, this.Area.Width, this.Area.Height - requestSize.Y);
+ this.Right = new RequestNode(this.Area.X + requestSize.X, this.Area.Y, this.Area.Width - requestSize.X, requestSize.Y);
+ }
+
+ }
+
}
}
diff --git a/Tests/TexturePackerTests.cs b/Tests/TexturePackerTests.cs
index e95cea7..38f4e51 100644
--- a/Tests/TexturePackerTests.cs
+++ b/Tests/TexturePackerTests.cs
@@ -1,27 +1,25 @@
using System;
using System.Collections.Generic;
+using System.IO;
using Microsoft.Xna.Framework.Graphics;
using MLEM.Data;
+using MLEM.Graphics;
+using MLEM.Maths;
using MLEM.Textures;
using NUnit.Framework;
+using Color = Microsoft.Xna.Framework.Color;
namespace Tests;
public class TexturePackerTests : GameTestFixture {
- private Texture2D testTexture;
- private Texture2D disposedTestTexture;
-
- [SetUp]
- public void SetUp() {
- this.testTexture = new Texture2D(this.Game.GraphicsDevice, 2048, 2048);
- this.disposedTestTexture = new Texture2D(this.Game.GraphicsDevice, 16, 16);
- }
+ private readonly List generatedTextures = [];
[TearDown]
public void TearDown() {
- this.testTexture?.Dispose();
- this.disposedTestTexture?.Dispose();
+ foreach (var tex in this.generatedTextures)
+ tex.Texture.Dispose();
+ this.generatedTextures.Clear();
}
[Test]
@@ -29,69 +27,67 @@ public class TexturePackerTests : GameTestFixture {
using var packer = new RuntimeTexturePacker();
for (var i = 0; i < 5; i++) {
var width = 16 * (i + 1);
- packer.Add(new TextureRegion(this.testTexture, 0, 0, width, 64), r => {
+ packer.Add(this.MakeTextureRegion(width, 64), r => {
Assert.AreEqual(r.Width, width);
Assert.AreEqual(r.Height, 64);
});
}
packer.Pack(this.Game.GraphicsDevice);
- Assert.AreEqual(packer.PackedTexture.Width, 16 + 32 + 48 + 64 + 80);
- Assert.AreEqual(packer.PackedTexture.Height, 64);
+ TexturePackerTests.SaveTexture(packer);
+ Assert.AreEqual(packer.PackedTexture.Width, 128);
+ Assert.AreEqual(packer.PackedTexture.Height, 128);
}
[Test]
- public void TestOverlap() {
+ public void TestOverlap([Values(0, 1, 5, 10)] int padding) {
var packed = new List();
- using (var packer = new RuntimeTexturePacker(8192)) {
- for (var i = 1; i <= 1000; i++)
- packer.Add(new TextureRegion(this.testTexture, 0, 0, i % 239, i % 673), packed.Add);
- packer.Pack(this.Game.GraphicsDevice);
- }
+ using var packer = new RuntimeTexturePacker();
+ for (var i = 1; i <= 1000; i++)
+ packer.Add(this.MakeTextureRegion(i % 239, i % 673), packed.Add, padding);
+ packer.Pack(this.Game.GraphicsDevice);
+
+ TexturePackerTests.SaveTexture(packer, padding.ToString());
foreach (var r1 in packed) {
+ var r1Padded = r1.Area;
+ r1Padded.Inflate(padding, padding);
+
foreach (var r2 in packed) {
if (r1 == r2)
continue;
- Assert.False(r1.Area.Intersects(r2.Area));
+
+ Assert.False(r1.Area.Intersects(r2.Area), $"Regions {r1.Area} and {r2.Area} intersect");
+
+ var r2Padded = r2.Area;
+ r2Padded.Inflate(padding, padding);
+ Assert.False(r1Padded.Intersects(r2Padded), $"Padded regions {r1Padded} and {r2Padded} intersect");
}
}
}
[Test]
public void TestDisposal() {
- using var packer = new RuntimeTexturePacker(128, disposeTextures: true);
- packer.Add(new TextureRegion(this.disposedTestTexture), TexturePackerTests.StubResult);
- packer.Add(new TextureRegion(this.disposedTestTexture, 0, 0, 8, 8), TexturePackerTests.StubResult);
+ using var packer = new RuntimeTexturePacker(disposeTextures: true);
+ var disposeLater = this.MakeTextureRegion(16, 16);
+ packer.Add(disposeLater, TexturePackerTests.StubResult);
+ packer.Add(new TextureRegion(disposeLater, 0, 0, 8, 8), TexturePackerTests.StubResult);
packer.Pack(this.Game.GraphicsDevice);
- Assert.True(this.disposedTestTexture.IsDisposed);
+ Assert.True(disposeLater.Texture.IsDisposed);
Assert.False(packer.PackedTexture.IsDisposed);
}
[Test]
public void TestBounds() {
- // test forced max width
- using var packer = new RuntimeTexturePacker(128);
- Assert.Throws(() => {
- packer.Add(new TextureRegion(this.testTexture, 0, 0, 256, 128), TexturePackerTests.StubResult);
- });
-
- // test auto-expanding width
- using var packer2 = new RuntimeTexturePacker(128, true);
- Assert.DoesNotThrow(() => {
- packer2.Add(new TextureRegion(this.testTexture, 0, 0, 256, 128), TexturePackerTests.StubResult);
- });
- packer2.Pack(this.Game.GraphicsDevice);
-
// test power of two forcing
- using var packer3 = new RuntimeTexturePacker(128, forcePowerOfTwo: true);
- packer3.Add(new TextureRegion(this.testTexture, 0, 0, 37, 170), TexturePackerTests.StubResult);
+ using var packer3 = new RuntimeTexturePacker(true);
+ packer3.Add(this.MakeTextureRegion(37, 170), TexturePackerTests.StubResult);
packer3.Pack(this.Game.GraphicsDevice);
Assert.AreEqual(64, packer3.PackedTexture.Width);
Assert.AreEqual(256, packer3.PackedTexture.Height);
// test square forcing
- using var packer4 = new RuntimeTexturePacker(128, forceSquare: true);
- packer4.Add(new TextureRegion(this.testTexture, 0, 0, 37, 170), TexturePackerTests.StubResult);
+ using var packer4 = new RuntimeTexturePacker(forceSquare: true);
+ packer4.Add(this.MakeTextureRegion(37, 170), TexturePackerTests.StubResult);
packer4.Pack(this.Game.GraphicsDevice);
Assert.AreEqual(170, packer4.PackedTexture.Width);
Assert.AreEqual(170, packer4.PackedTexture.Height);
@@ -99,45 +95,66 @@ public class TexturePackerTests : GameTestFixture {
[Test]
public void TestPackMultipleTimes() {
- using var packer = new RuntimeTexturePacker(1024);
+ using var packer = new RuntimeTexturePacker();
// pack the first time
var results = 0;
for (var i = 0; i < 10; i++)
- packer.Add(new TextureRegion(this.testTexture, 0, 0, 64, 64), _ => results++);
+ packer.Add(this.MakeTextureRegion(64, 64), _ => results++);
packer.Pack(this.Game.GraphicsDevice);
Assert.AreEqual(10, results);
- // pack without resizing
- packer.Add(new TextureRegion(this.testTexture, 0, 0, 0, 0), _ => results++);
+ // pack again
+ packer.Add(this.MakeTextureRegion(64, 64), _ => results++);
packer.Pack(this.Game.GraphicsDevice);
- Assert.AreEqual(11, results);
-
- // pack and force a resize
- packer.Add(new TextureRegion(this.testTexture, 0, 0, 64, 64), _ => results++);
- packer.Pack(this.Game.GraphicsDevice);
- // all callbacks are called again, so we add 11 again, as well as the callback we just added
- Assert.AreEqual(2 * 11 + 1, results);
+ // all callbacks are called again, so we add 10 again, as well as the callback we just added
+ Assert.AreEqual(10 + 10 + 1, results);
}
[Test]
- public void TestPackTimes() {
- for (var total = 1; total <= 10001; total += 1000) {
- using var sameSizePacker = new RuntimeTexturePacker();
- using var diffSizePacker = new RuntimeTexturePacker();
- for (var i = 0; i < total; i++) {
- sameSizePacker.Add(new TextureRegion(this.testTexture, 0, 0, 10, 10), TexturePackerTests.StubResult);
- diffSizePacker.Add(new TextureRegion(this.testTexture, 0, 0, 10 + i % 129, 10 * (i % 5 + 1)), TexturePackerTests.StubResult);
- }
- sameSizePacker.Pack(this.Game.GraphicsDevice);
- diffSizePacker.Pack(this.Game.GraphicsDevice);
-
- TestContext.WriteLine($"""
- {total} regions,
- same-size {sameSizePacker.LastCalculationTime.TotalMilliseconds} calc, {sameSizePacker.LastPackTime.TotalMilliseconds} pack, {sameSizePacker.LastTotalTime.TotalMilliseconds} total,
- diff-size {diffSizePacker.LastCalculationTime.TotalMilliseconds} calc, {diffSizePacker.LastPackTime.TotalMilliseconds} pack, {diffSizePacker.LastTotalTime.TotalMilliseconds} total
- """);
+ public void TestPackTimes([Values(1, 100, 1000, 5000, 10000)] int total) {
+ var random = new Random(1238492384);
+ using var sameSizePacker = new RuntimeTexturePacker();
+ using var diffSizePacker = new RuntimeTexturePacker();
+ for (var i = 0; i < total; i++) {
+ sameSizePacker.Add(this.MakeTextureRegion(10, 10), TexturePackerTests.StubResult);
+ diffSizePacker.Add(this.MakeTextureRegion(random.Next(10, 200), random.Next(10, 200)), TexturePackerTests.StubResult);
}
+ sameSizePacker.Pack(this.Game.GraphicsDevice);
+ diffSizePacker.Pack(this.Game.GraphicsDevice);
+
+ TexturePackerTests.SaveTexture(sameSizePacker, $"SameSize{total}");
+ TexturePackerTests.SaveTexture(diffSizePacker, $"DiffSize{total}");
+
+ TestContext.WriteLine($"""
+ {total} regions,
+ same-size {sameSizePacker.LastCalculationTime.TotalMilliseconds}ms calc, {sameSizePacker.LastPackTime.TotalMilliseconds}ms pack, {sameSizePacker.LastTotalTime.TotalMilliseconds}ms total,
+ diff-size {diffSizePacker.LastCalculationTime.TotalMilliseconds}ms calc, {diffSizePacker.LastPackTime.TotalMilliseconds}ms pack, {diffSizePacker.LastTotalTime.TotalMilliseconds}ms total
+ """);
+ }
+
+ private TextureRegion MakeTextureRegion(int width, int height) {
+ var color = ColorHelper.FromHexRgb(SingleRandom.Int(this.generatedTextures.Count));
+ var texture = new Texture2D(this.Game.GraphicsDevice, Math.Max(width, 1), Math.Max(height, 1));
+ using (var data = texture.GetTextureData()) {
+ for (var x = 0; x < texture.Width; x++) {
+ for (var y = 0; y < texture.Height; y++)
+ data[x, y] = color;
+ }
+ }
+ var region = new TextureRegion(texture, 0, 0, width, height);
+ this.generatedTextures.Add(region);
+ return region;
+ }
+
+ private static void SaveTexture(RuntimeTexturePacker packer, string append = "") {
+ var caller = new System.Diagnostics.StackTrace(1).GetFrame(0).GetMethod().Name + append;
+ var file = Path.GetFullPath(Path.Combine(TestContext.CurrentContext.WorkDirectory, "PackedTextures", caller + ".png"));
+ Directory.CreateDirectory(Path.GetDirectoryName(file)!);
+ using (var stream = File.Create(file))
+ packer.PackedTexture.SaveAsPng(stream, packer.PackedTexture.Width, packer.PackedTexture.Height);
+ TestContext.WriteLine($"Saving texture generated by {caller} to {file}");
+ TestContext.AddTestAttachment(file);
}
private static void StubResult(TextureRegion region) {}
diff --git a/build.cake b/build.cake
index fad0686..a932083 100644
--- a/build.cake
+++ b/build.cake
@@ -2,7 +2,7 @@
#tool dotnet:?package=docfx&version=2.75.3
// this is the upcoming version, for prereleases
-var version = Argument("version", "7.2.0");
+var version = Argument("version", "8.0.0");
var target = Argument("target", "Default");
var gitRef = Argument("ref", "refs/heads/main");
var buildNum = Argument("buildNum", "");
@@ -40,7 +40,7 @@ Task("Test").IsDependentOn("Prepare").Does(() => {
var settings = new DotNetTestSettings {
Configuration = config,
Collectors = {"XPlat Code Coverage"},
- Loggers = {"console;verbosity=normal"}
+ Loggers = {"console;verbosity=normal", "nunit"}
};
DotNetTest("MLEM.sln", settings);
DotNetTest("MLEM.FNA.sln", settings);