mirror of
https://github.com/Ellpeck/MLEM.git
synced 2024-11-24 05:38:33 +01:00
Improve the runtime texture packer algorithm (#31)
* use a random size for testing pack times to show how truly bad the current algo is * expand overlap test to include padding and pixel padding * fixed TestPackTimes on KNI * also test for whether the padding intersects * Use a binary tree algorithm for the runtime texture packer * update changelog and bump version * fixed fna compile errors * save TestPackTimes textures separately
This commit is contained in:
parent
a58470de9f
commit
6f64661817
4 changed files with 185 additions and 159 deletions
|
@ -2,7 +2,7 @@
|
||||||
MLEM tries to adhere to [semantic versioning](https://semver.org/). Potentially breaking changes are written in **bold**.
|
MLEM tries to adhere to [semantic versioning](https://semver.org/). Potentially breaking changes are written in **bold**.
|
||||||
|
|
||||||
Jump to version:
|
Jump to version:
|
||||||
- [7.2.0](#720-in-development)
|
- [8.0.0](#800-in-development)
|
||||||
- [7.1.1](#711)
|
- [7.1.1](#711)
|
||||||
- [7.1.0](#710)
|
- [7.1.0](#710)
|
||||||
- [7.0.0](#700)
|
- [7.0.0](#700)
|
||||||
|
@ -16,7 +16,7 @@ Jump to version:
|
||||||
- [5.1.0](#510)
|
- [5.1.0](#510)
|
||||||
- [5.0.0](#500)
|
- [5.0.0](#500)
|
||||||
|
|
||||||
## 7.2.0 (In Development)
|
## 8.0.0 (In Development)
|
||||||
|
|
||||||
### MLEM
|
### MLEM
|
||||||
Fixes
|
Fixes
|
||||||
|
@ -35,6 +35,7 @@ Fixes
|
||||||
|
|
||||||
### MLEM.Data
|
### MLEM.Data
|
||||||
Improvements
|
Improvements
|
||||||
|
- **Use a binary tree algorithm for RuntimeTexturePacker to vastly increase packing speed**
|
||||||
- Made fields and methods in StaticJsonConverter protected to allow extending it
|
- Made fields and methods in StaticJsonConverter protected to allow extending it
|
||||||
|
|
||||||
## 7.1.1
|
## 7.1.1
|
||||||
|
|
|
@ -4,7 +4,6 @@ using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Microsoft.Xna.Framework;
|
using Microsoft.Xna.Framework;
|
||||||
using Microsoft.Xna.Framework.Graphics;
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
using MLEM.Maths;
|
|
||||||
using MLEM.Textures;
|
using MLEM.Textures;
|
||||||
using static MLEM.Textures.TextureExtensions;
|
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.
|
/// Packing textures in this manner allows for faster rendering, as fewer texture swaps are required.
|
||||||
/// The resulting texture segments are returned as <see cref="TextureRegion"/> instances.
|
/// The resulting texture segments are returned as <see cref="TextureRegion"/> instances.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 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/.
|
||||||
|
/// </remarks>
|
||||||
public class RuntimeTexturePacker : IDisposable {
|
public class RuntimeTexturePacker : IDisposable {
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -36,31 +38,21 @@ namespace MLEM.Data {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The amount of currently packed texture regions.
|
/// The amount of currently packed texture regions.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int PackedTextures => this.packedTextures.Count;
|
public int PackedTextures => this.PackedTexture != null ? this.requests.Count : 0;
|
||||||
|
|
||||||
private readonly List<Request> texturesToPack = new List<Request>();
|
private readonly List<Request> requests = new List<Request>();
|
||||||
private readonly List<Request> packedTextures = new List<Request>();
|
|
||||||
private readonly Dictionary<Point, Request> occupiedPositions = new Dictionary<Point, Request>();
|
|
||||||
private readonly Dictionary<Point, Point> initialPositions = new Dictionary<Point, Point>();
|
|
||||||
private readonly Dictionary<Texture2D, TextureData> dataCache = new Dictionary<Texture2D, TextureData>();
|
private readonly Dictionary<Texture2D, TextureData> dataCache = new Dictionary<Texture2D, TextureData>();
|
||||||
private readonly bool autoIncreaseMaxWidth;
|
|
||||||
private readonly bool forcePowerOfTwo;
|
private readonly bool forcePowerOfTwo;
|
||||||
private readonly bool forceSquare;
|
private readonly bool forceSquare;
|
||||||
private readonly bool disposeTextures;
|
private readonly bool disposeTextures;
|
||||||
|
|
||||||
private int maxWidth;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new runtime texture packer with the given settings.
|
/// Creates a new runtime texture packer with the given settings.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="maxWidth">The maximum width that the packed texture can have. Defaults to 2048.</param>
|
|
||||||
/// <param name="autoIncreaseMaxWidth">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.</param>
|
|
||||||
/// <param name="forcePowerOfTwo">Whether the resulting <see cref="PackedTexture"/> should have a width and height that is a power of two.</param>
|
/// <param name="forcePowerOfTwo">Whether the resulting <see cref="PackedTexture"/> should have a width and height that is a power of two.</param>
|
||||||
/// <param name="forceSquare">Whether the resulting <see cref="PackedTexture"/> should be square regardless of required size.</param>
|
/// <param name="forceSquare">Whether the resulting <see cref="PackedTexture"/> should be square regardless of required size.</param>
|
||||||
/// <param name="disposeTextures">Whether the original textures submitted to this texture packer should be disposed after packing.</param>
|
/// <param name="disposeTextures">Whether the original textures submitted to this texture packer should be disposed after packing.</param>
|
||||||
public RuntimeTexturePacker(int maxWidth = 2048, bool autoIncreaseMaxWidth = false, bool forcePowerOfTwo = false, bool forceSquare = false, bool disposeTextures = false) {
|
public RuntimeTexturePacker(bool forcePowerOfTwo = false, bool forceSquare = false, bool disposeTextures = false) {
|
||||||
this.maxWidth = maxWidth;
|
|
||||||
this.autoIncreaseMaxWidth = autoIncreaseMaxWidth;
|
|
||||||
this.forcePowerOfTwo = forcePowerOfTwo;
|
this.forcePowerOfTwo = forcePowerOfTwo;
|
||||||
this.forceSquare = forceSquare;
|
this.forceSquare = forceSquare;
|
||||||
this.disposeTextures = disposeTextures;
|
this.disposeTextures = disposeTextures;
|
||||||
|
@ -145,15 +137,7 @@ namespace MLEM.Data {
|
||||||
/// <param name="padWithPixels">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 <paramref name="padding"/> is greater than 0.</param>
|
/// <param name="padWithPixels">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 <paramref name="padding"/> is greater than 0.</param>
|
||||||
/// <exception cref="InvalidOperationException">Thrown when trying to add a texture width a width greater than the defined max width.</exception>
|
/// <exception cref="InvalidOperationException">Thrown when trying to add a texture width a width greater than the defined max width.</exception>
|
||||||
public void Add(TextureRegion texture, Action<TextureRegion> result, int padding = 0, bool padWithPixels = false) {
|
public void Add(TextureRegion texture, Action<TextureRegion> result, int padding = 0, bool padWithPixels = false) {
|
||||||
var paddedWidth = texture.Width + 2 * padding;
|
this.requests.Add(new Request(texture, result, padding, padWithPixels));
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -163,19 +147,32 @@ namespace MLEM.Data {
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="device">The graphics device to use for texture generation</param>
|
/// <param name="device">The graphics device to use for texture generation</param>
|
||||||
public void Pack(GraphicsDevice device) {
|
public void Pack(GraphicsDevice device) {
|
||||||
// set pack areas for each request
|
// set pack areas for each request based on the algo in https://codeincomplete.com/articles/bin-packing/
|
||||||
// we pack larger textures first, so that smaller textures can fit in the gaps that larger ones leave
|
|
||||||
var stopwatch = Stopwatch.StartNew();
|
var stopwatch = Stopwatch.StartNew();
|
||||||
foreach (var request in this.texturesToPack.OrderByDescending(t => t.Texture.Width * t.Texture.Height)) {
|
RequestNode root = null;
|
||||||
request.PackedArea = this.OccupyFreeArea(request);
|
foreach (var request in this.requests.OrderByDescending(t => Math.Max(t.Texture.Width, t.Texture.Height) + t.Padding * 2)) {
|
||||||
this.packedTextures.Add(request);
|
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();
|
stopwatch.Stop();
|
||||||
this.LastCalculationTime = stopwatch.Elapsed;
|
this.LastCalculationTime = stopwatch.Elapsed;
|
||||||
|
|
||||||
// figure out texture size and regenerate texture if necessary
|
// figure out texture size and regenerate texture if necessary
|
||||||
var width = this.packedTextures.Max(t => t.PackedArea.Right);
|
var width = root.Area.Width;
|
||||||
var height = this.packedTextures.Max(t => t.PackedArea.Bottom);
|
var height = root.Area.Height;
|
||||||
if (this.forcePowerOfTwo) {
|
if (this.forcePowerOfTwo) {
|
||||||
width = RuntimeTexturePacker.ToPowerOfTwo(width);
|
width = RuntimeTexturePacker.ToPowerOfTwo(width);
|
||||||
height = RuntimeTexturePacker.ToPowerOfTwo(height);
|
height = RuntimeTexturePacker.ToPowerOfTwo(height);
|
||||||
|
@ -184,26 +181,24 @@ namespace MLEM.Data {
|
||||||
width = height = Math.Max(width, height);
|
width = height = Math.Max(width, height);
|
||||||
|
|
||||||
// if we don't need to regenerate, we only need to add newly added regions
|
// if we don't need to regenerate, we only need to add newly added regions
|
||||||
IEnumerable<Request> texturesToCopy = this.texturesToPack;
|
|
||||||
if (this.PackedTexture == null || this.PackedTexture.Width != width || this.PackedTexture.Height != height) {
|
if (this.PackedTexture == null || this.PackedTexture.Width != width || this.PackedTexture.Height != height) {
|
||||||
this.PackedTexture?.Dispose();
|
this.PackedTexture?.Dispose();
|
||||||
this.PackedTexture = new Texture2D(device, width, height);
|
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
|
// copy texture data onto the packed texture
|
||||||
stopwatch.Restart();
|
stopwatch.Restart();
|
||||||
using (var data = this.PackedTexture.GetTextureData()) {
|
using (var data = this.PackedTexture.GetTextureData()) {
|
||||||
foreach (var request in texturesToCopy)
|
foreach (var request in this.requests)
|
||||||
this.CopyRegion(data, request);
|
this.CopyRegion(data, request);
|
||||||
}
|
}
|
||||||
stopwatch.Stop();
|
stopwatch.Stop();
|
||||||
this.LastPackTime = stopwatch.Elapsed;
|
this.LastPackTime = stopwatch.Elapsed;
|
||||||
|
|
||||||
// invoke callbacks for textures we copied
|
// invoke callbacks for textures we copied
|
||||||
foreach (var request in texturesToCopy) {
|
foreach (var request in this.requests) {
|
||||||
var packedArea = request.PackedArea.Shrink(new Point(request.Padding, request.Padding));
|
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) {
|
request.Result.Invoke(new TextureRegion(this.PackedTexture, packedArea) {
|
||||||
Pivot = request.Texture.Pivot,
|
Pivot = request.Texture.Pivot,
|
||||||
Name = request.Texture.Name,
|
Name = request.Texture.Name,
|
||||||
|
@ -213,7 +208,6 @@ namespace MLEM.Data {
|
||||||
request.Texture.Texture.Dispose();
|
request.Texture.Texture.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.texturesToPack.Clear();
|
|
||||||
this.dataCache.Clear();
|
this.dataCache.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -225,10 +219,7 @@ namespace MLEM.Data {
|
||||||
this.PackedTexture = null;
|
this.PackedTexture = null;
|
||||||
this.LastCalculationTime = TimeSpan.Zero;
|
this.LastCalculationTime = TimeSpan.Zero;
|
||||||
this.LastPackTime = TimeSpan.Zero;
|
this.LastPackTime = TimeSpan.Zero;
|
||||||
this.texturesToPack.Clear();
|
this.requests.Clear();
|
||||||
this.packedTextures.Clear();
|
|
||||||
this.initialPositions.Clear();
|
|
||||||
this.occupiedPositions.Clear();
|
|
||||||
this.dataCache.Clear();
|
this.dataCache.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -237,53 +228,9 @@ namespace MLEM.Data {
|
||||||
this.Reset();
|
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) {
|
private void CopyRegion(TextureData destination, Request request) {
|
||||||
var data = this.GetCachedTextureData(request.Texture.Texture);
|
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 x = -request.Padding; x < request.Texture.Width + request.Padding; x++) {
|
||||||
for (var y = -request.Padding; y < request.Texture.Height + request.Padding; y++) {
|
for (var y = -request.Padding; y < request.Texture.Height + request.Padding; y++) {
|
||||||
Color srcColor;
|
Color srcColor;
|
||||||
|
@ -328,13 +275,57 @@ namespace MLEM.Data {
|
||||||
return ret;
|
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 {
|
private class Request {
|
||||||
|
|
||||||
public readonly TextureRegion Texture;
|
public readonly TextureRegion Texture;
|
||||||
public readonly Action<TextureRegion> Result;
|
public readonly Action<TextureRegion> Result;
|
||||||
public readonly int Padding;
|
public readonly int Padding;
|
||||||
public readonly bool PadWithPixels;
|
public readonly bool PadWithPixels;
|
||||||
public Rectangle PackedArea;
|
public RequestNode Node;
|
||||||
|
|
||||||
public Request(TextureRegion texture, Action<TextureRegion> result, int padding, bool padWithPixels) {
|
public Request(TextureRegion texture, Action<TextureRegion> result, int padding, bool padWithPixels) {
|
||||||
this.Texture = texture;
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,27 +1,25 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
using Microsoft.Xna.Framework.Graphics;
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
using MLEM.Data;
|
using MLEM.Data;
|
||||||
|
using MLEM.Graphics;
|
||||||
|
using MLEM.Maths;
|
||||||
using MLEM.Textures;
|
using MLEM.Textures;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using Color = Microsoft.Xna.Framework.Color;
|
||||||
|
|
||||||
namespace Tests;
|
namespace Tests;
|
||||||
|
|
||||||
public class TexturePackerTests : GameTestFixture {
|
public class TexturePackerTests : GameTestFixture {
|
||||||
|
|
||||||
private Texture2D testTexture;
|
private readonly List<TextureRegion> generatedTextures = [];
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TearDown]
|
[TearDown]
|
||||||
public void TearDown() {
|
public void TearDown() {
|
||||||
this.testTexture?.Dispose();
|
foreach (var tex in this.generatedTextures)
|
||||||
this.disposedTestTexture?.Dispose();
|
tex.Texture.Dispose();
|
||||||
|
this.generatedTextures.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
@ -29,69 +27,67 @@ public class TexturePackerTests : GameTestFixture {
|
||||||
using var packer = new RuntimeTexturePacker();
|
using var packer = new RuntimeTexturePacker();
|
||||||
for (var i = 0; i < 5; i++) {
|
for (var i = 0; i < 5; i++) {
|
||||||
var width = 16 * (i + 1);
|
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.Width, width);
|
||||||
Assert.AreEqual(r.Height, 64);
|
Assert.AreEqual(r.Height, 64);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
packer.Pack(this.Game.GraphicsDevice);
|
packer.Pack(this.Game.GraphicsDevice);
|
||||||
Assert.AreEqual(packer.PackedTexture.Width, 16 + 32 + 48 + 64 + 80);
|
TexturePackerTests.SaveTexture(packer);
|
||||||
Assert.AreEqual(packer.PackedTexture.Height, 64);
|
Assert.AreEqual(packer.PackedTexture.Width, 128);
|
||||||
|
Assert.AreEqual(packer.PackedTexture.Height, 128);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestOverlap() {
|
public void TestOverlap([Values(0, 1, 5, 10)] int padding) {
|
||||||
var packed = new List<TextureRegion>();
|
var packed = new List<TextureRegion>();
|
||||||
using (var packer = new RuntimeTexturePacker(8192)) {
|
using var packer = new RuntimeTexturePacker();
|
||||||
for (var i = 1; i <= 1000; i++)
|
for (var i = 1; i <= 1000; i++)
|
||||||
packer.Add(new TextureRegion(this.testTexture, 0, 0, i % 239, i % 673), packed.Add);
|
packer.Add(this.MakeTextureRegion(i % 239, i % 673), packed.Add, padding);
|
||||||
packer.Pack(this.Game.GraphicsDevice);
|
packer.Pack(this.Game.GraphicsDevice);
|
||||||
}
|
|
||||||
|
TexturePackerTests.SaveTexture(packer, padding.ToString());
|
||||||
|
|
||||||
foreach (var r1 in packed) {
|
foreach (var r1 in packed) {
|
||||||
|
var r1Padded = r1.Area;
|
||||||
|
r1Padded.Inflate(padding, padding);
|
||||||
|
|
||||||
foreach (var r2 in packed) {
|
foreach (var r2 in packed) {
|
||||||
if (r1 == r2)
|
if (r1 == r2)
|
||||||
continue;
|
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]
|
[Test]
|
||||||
public void TestDisposal() {
|
public void TestDisposal() {
|
||||||
using var packer = new RuntimeTexturePacker(128, disposeTextures: true);
|
using var packer = new RuntimeTexturePacker(disposeTextures: true);
|
||||||
packer.Add(new TextureRegion(this.disposedTestTexture), TexturePackerTests.StubResult);
|
var disposeLater = this.MakeTextureRegion(16, 16);
|
||||||
packer.Add(new TextureRegion(this.disposedTestTexture, 0, 0, 8, 8), TexturePackerTests.StubResult);
|
packer.Add(disposeLater, TexturePackerTests.StubResult);
|
||||||
|
packer.Add(new TextureRegion(disposeLater, 0, 0, 8, 8), TexturePackerTests.StubResult);
|
||||||
packer.Pack(this.Game.GraphicsDevice);
|
packer.Pack(this.Game.GraphicsDevice);
|
||||||
Assert.True(this.disposedTestTexture.IsDisposed);
|
Assert.True(disposeLater.Texture.IsDisposed);
|
||||||
Assert.False(packer.PackedTexture.IsDisposed);
|
Assert.False(packer.PackedTexture.IsDisposed);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestBounds() {
|
public void TestBounds() {
|
||||||
// test forced max width
|
|
||||||
using var packer = new RuntimeTexturePacker(128);
|
|
||||||
Assert.Throws<InvalidOperationException>(() => {
|
|
||||||
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
|
// test power of two forcing
|
||||||
using var packer3 = new RuntimeTexturePacker(128, forcePowerOfTwo: true);
|
using var packer3 = new RuntimeTexturePacker(true);
|
||||||
packer3.Add(new TextureRegion(this.testTexture, 0, 0, 37, 170), TexturePackerTests.StubResult);
|
packer3.Add(this.MakeTextureRegion(37, 170), TexturePackerTests.StubResult);
|
||||||
packer3.Pack(this.Game.GraphicsDevice);
|
packer3.Pack(this.Game.GraphicsDevice);
|
||||||
Assert.AreEqual(64, packer3.PackedTexture.Width);
|
Assert.AreEqual(64, packer3.PackedTexture.Width);
|
||||||
Assert.AreEqual(256, packer3.PackedTexture.Height);
|
Assert.AreEqual(256, packer3.PackedTexture.Height);
|
||||||
|
|
||||||
// test square forcing
|
// test square forcing
|
||||||
using var packer4 = new RuntimeTexturePacker(128, forceSquare: true);
|
using var packer4 = new RuntimeTexturePacker(forceSquare: true);
|
||||||
packer4.Add(new TextureRegion(this.testTexture, 0, 0, 37, 170), TexturePackerTests.StubResult);
|
packer4.Add(this.MakeTextureRegion(37, 170), TexturePackerTests.StubResult);
|
||||||
packer4.Pack(this.Game.GraphicsDevice);
|
packer4.Pack(this.Game.GraphicsDevice);
|
||||||
Assert.AreEqual(170, packer4.PackedTexture.Width);
|
Assert.AreEqual(170, packer4.PackedTexture.Width);
|
||||||
Assert.AreEqual(170, packer4.PackedTexture.Height);
|
Assert.AreEqual(170, packer4.PackedTexture.Height);
|
||||||
|
@ -99,45 +95,66 @@ public class TexturePackerTests : GameTestFixture {
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestPackMultipleTimes() {
|
public void TestPackMultipleTimes() {
|
||||||
using var packer = new RuntimeTexturePacker(1024);
|
using var packer = new RuntimeTexturePacker();
|
||||||
|
|
||||||
// pack the first time
|
// pack the first time
|
||||||
var results = 0;
|
var results = 0;
|
||||||
for (var i = 0; i < 10; i++)
|
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);
|
packer.Pack(this.Game.GraphicsDevice);
|
||||||
Assert.AreEqual(10, results);
|
Assert.AreEqual(10, results);
|
||||||
|
|
||||||
// pack without resizing
|
// pack again
|
||||||
packer.Add(new TextureRegion(this.testTexture, 0, 0, 0, 0), _ => results++);
|
packer.Add(this.MakeTextureRegion(64, 64), _ => results++);
|
||||||
packer.Pack(this.Game.GraphicsDevice);
|
packer.Pack(this.Game.GraphicsDevice);
|
||||||
Assert.AreEqual(11, 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);
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestPackTimes() {
|
public void TestPackTimes([Values(1, 100, 1000, 5000, 10000)] int total) {
|
||||||
for (var total = 1; total <= 10001; total += 1000) {
|
var random = new Random(1238492384);
|
||||||
using var sameSizePacker = new RuntimeTexturePacker();
|
using var sameSizePacker = new RuntimeTexturePacker();
|
||||||
using var diffSizePacker = new RuntimeTexturePacker();
|
using var diffSizePacker = new RuntimeTexturePacker();
|
||||||
for (var i = 0; i < total; i++) {
|
for (var i = 0; i < total; i++) {
|
||||||
sameSizePacker.Add(new TextureRegion(this.testTexture, 0, 0, 10, 10), TexturePackerTests.StubResult);
|
sameSizePacker.Add(this.MakeTextureRegion(10, 10), TexturePackerTests.StubResult);
|
||||||
diffSizePacker.Add(new TextureRegion(this.testTexture, 0, 0, 10 + i % 129, 10 * (i % 5 + 1)), TexturePackerTests.StubResult);
|
diffSizePacker.Add(this.MakeTextureRegion(random.Next(10, 200), random.Next(10, 200)), TexturePackerTests.StubResult);
|
||||||
}
|
}
|
||||||
sameSizePacker.Pack(this.Game.GraphicsDevice);
|
sameSizePacker.Pack(this.Game.GraphicsDevice);
|
||||||
diffSizePacker.Pack(this.Game.GraphicsDevice);
|
diffSizePacker.Pack(this.Game.GraphicsDevice);
|
||||||
|
|
||||||
|
TexturePackerTests.SaveTexture(sameSizePacker, $"SameSize{total}");
|
||||||
|
TexturePackerTests.SaveTexture(diffSizePacker, $"DiffSize{total}");
|
||||||
|
|
||||||
TestContext.WriteLine($"""
|
TestContext.WriteLine($"""
|
||||||
{total} regions,
|
{total} regions,
|
||||||
same-size {sameSizePacker.LastCalculationTime.TotalMilliseconds} calc, {sameSizePacker.LastPackTime.TotalMilliseconds} pack, {sameSizePacker.LastTotalTime.TotalMilliseconds} total,
|
same-size {sameSizePacker.LastCalculationTime.TotalMilliseconds}ms calc, {sameSizePacker.LastPackTime.TotalMilliseconds}ms pack, {sameSizePacker.LastTotalTime.TotalMilliseconds}ms total,
|
||||||
diff-size {diffSizePacker.LastCalculationTime.TotalMilliseconds} calc, {diffSizePacker.LastPackTime.TotalMilliseconds} pack, {diffSizePacker.LastTotalTime.TotalMilliseconds} 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) {}
|
private static void StubResult(TextureRegion region) {}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
#tool dotnet:?package=docfx&version=2.75.3
|
#tool dotnet:?package=docfx&version=2.75.3
|
||||||
|
|
||||||
// this is the upcoming version, for prereleases
|
// 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 target = Argument("target", "Default");
|
||||||
var gitRef = Argument("ref", "refs/heads/main");
|
var gitRef = Argument("ref", "refs/heads/main");
|
||||||
var buildNum = Argument("buildNum", "");
|
var buildNum = Argument("buildNum", "");
|
||||||
|
@ -40,7 +40,7 @@ Task("Test").IsDependentOn("Prepare").Does(() => {
|
||||||
var settings = new DotNetTestSettings {
|
var settings = new DotNetTestSettings {
|
||||||
Configuration = config,
|
Configuration = config,
|
||||||
Collectors = {"XPlat Code Coverage"},
|
Collectors = {"XPlat Code Coverage"},
|
||||||
Loggers = {"console;verbosity=normal"}
|
Loggers = {"console;verbosity=normal", "nunit"}
|
||||||
};
|
};
|
||||||
DotNetTest("MLEM.sln", settings);
|
DotNetTest("MLEM.sln", settings);
|
||||||
DotNetTest("MLEM.FNA.sln", settings);
|
DotNetTest("MLEM.FNA.sln", settings);
|
||||||
|
|
Loading…
Reference in a new issue