diff --git a/MLEM.Data/RuntimeTexturePacker.cs b/MLEM.Data/RuntimeTexturePacker.cs
new file mode 100644
index 0000000..d75f8d4
--- /dev/null
+++ b/MLEM.Data/RuntimeTexturePacker.cs
@@ -0,0 +1,164 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+using MLEM.Extensions;
+using MLEM.Textures;
+
+namespace MLEM.Data {
+ ///
+ /// A runtime texture packer provides the user with the ability to combine multiple instances into a single texture.
+ /// Packing textures in this manner allows for faster rendering, as fewer texture swaps are required.
+ /// The resulting texture segments are returned as instances.
+ ///
+ public class RuntimeTexturePacker {
+
+ private readonly List textures = new List();
+
+ ///
+ /// The generated packed texture.
+ /// This value is null before is called.
+ ///
+ public Texture2D PackedTexture { get; private set; }
+ ///
+ /// The time that it took to calculate the required areas the last time that was called
+ ///
+ public TimeSpan LastCalculationTime { get; private set; }
+ ///
+ /// The time that it took to copy the texture data from the invidiual textures onto the the last time that was called
+ ///
+ public TimeSpan LastPackTime { get; private set; }
+ ///
+ /// The time that took the last time it was called
+ ///
+ public TimeSpan LastTotalTime => this.LastCalculationTime + this.LastPackTime;
+ private readonly int maxWidth;
+
+ ///
+ /// Creates a new runtime texture packer with the given settings
+ ///
+ /// The maximum width that the packed texture can have. Defaults to 2048.
+ public RuntimeTexturePacker(int maxWidth = 2048) {
+ this.maxWidth = maxWidth;
+ }
+
+ ///
+ /// Adds a new texture to this texture packer to be packed.
+ /// The passed is invoked in and provides the caller with the resulting texture region on the .
+ ///
+ /// The texture to pack
+ /// The result callback which will receive the resulting texture region
+ public void Add(Texture2D texture, Action result) {
+ this.Add(new TextureRegion(texture), result);
+ }
+
+ ///
+ /// Adds a new to this texture packer to be packed.
+ /// The passed is invoked in and provides the caller with the resulting texture region on the .
+ ///
+ /// The texture to pack
+ /// The result callback which will receive the resulting texture region
+ /// Thrown when trying to add data to a packer that has already been packed, or when trying to add a texture width a width greater than the defined max width
+ public void Add(TextureRegion texture, Action result) {
+ if (this.PackedTexture != null)
+ throw new InvalidOperationException("Cannot add texture to a texture packer that is already packed");
+ if (texture.Width > this.maxWidth)
+ throw new InvalidOperationException($"Cannot add texture with width {texture.Width} to a texture packer with max width {this.maxWidth}");
+ this.textures.Add(new Request(texture, result));
+ }
+
+ ///
+ /// Packs all of the textures and texture regions added using into one texture.
+ /// The resulting texture will be stored in .
+ /// All of the result callbacks that were added will also be invoked.
+ ///
+ /// The graphics device to use for texture generation
+ /// Thrown when calling this method on a texture packer that has already been packed
+ public void Pack(GraphicsDevice device) {
+ if (this.PackedTexture != null)
+ throw new InvalidOperationException("Cannot pack a texture packer that is already packed");
+
+ // set pack areas for each request
+ var stopwatch = Stopwatch.StartNew();
+ foreach (var request in this.textures.OrderByDescending(t => t.Texture.Width * t.Texture.Height)) {
+ var area = this.FindFreeArea(new Point(request.Texture.Width, request.Texture.Height));
+ request.PackedArea = area;
+ }
+ stopwatch.Stop();
+ this.LastCalculationTime = stopwatch.Elapsed;
+
+ // generate texture based on required size
+ var width = this.textures.Max(t => t.PackedArea.Right);
+ var height = this.textures.Max(t => t.PackedArea.Bottom);
+ this.PackedTexture = new Texture2D(device, width, height);
+ device.Disposing += (o, a) => this.PackedTexture.Dispose();
+
+ // copy texture data onto the packed texture
+ stopwatch.Restart();
+ using (var data = this.PackedTexture.GetTextureData()) {
+ foreach (var request in this.textures)
+ CopyRegion(data, request);
+ }
+ stopwatch.Stop();
+ this.LastPackTime = stopwatch.Elapsed;
+
+ // invoke callbacks
+ foreach (var request in this.textures)
+ request.Result.Invoke(new TextureRegion(this.PackedTexture, request.PackedArea));
+ }
+
+ private Rectangle FindFreeArea(Point size) {
+ var pos = new Point(0, 0);
+ var lowestY = int.MaxValue;
+ while (true) {
+ var intersected = false;
+ var area = new Rectangle(pos, size);
+ foreach (var tex in this.textures) {
+ if (tex.PackedArea.Intersects(area)) {
+ pos.X = tex.PackedArea.Right;
+ // when we move down, we want to move down by the smallest intersecting texture's height
+ if (lowestY > tex.PackedArea.Bottom)
+ lowestY = tex.PackedArea.Bottom;
+ intersected = true;
+ break;
+ }
+ }
+ if (!intersected)
+ return area;
+ if (pos.X + size.X > this.maxWidth) {
+ pos.X = 0;
+ pos.Y = lowestY;
+ lowestY = int.MaxValue;
+ }
+ }
+ }
+
+ private static void CopyRegion(TextureExtensions.TextureData destination, Request request) {
+ using (var data = request.Texture.Texture.GetTextureData()) {
+ for (var x = 0; x < request.Texture.Width; x++) {
+ for (var y = 0; y < request.Texture.Height; y++) {
+ var dest = request.PackedArea.Location + new Point(x, y);
+ var src = request.Texture.Position + new Point(x, y);
+ destination[dest] = data[src];
+ }
+ }
+ }
+ }
+
+ private class Request {
+
+ public readonly TextureRegion Texture;
+ public readonly Action Result;
+ public Rectangle PackedArea;
+
+ public Request(TextureRegion texture, Action result) {
+ this.Texture = texture;
+ this.Result = result;
+ }
+
+ }
+
+ }
+}
\ No newline at end of file