diff --git a/CHANGELOG.md b/CHANGELOG.md index fb66776..7014ff7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,7 @@ Additions Improvements - Premultiply textures when using RawContentManager - Allow enumerating all region names of a DataTextureAtlas +- Cache RuntimeTexturePacker texture data while packing to improve performance Fixes - Fixed SoundEffectReader incorrectly claiming it could read ogg and mp3 files diff --git a/MLEM.Data/RuntimeTexturePacker.cs b/MLEM.Data/RuntimeTexturePacker.cs index d2ebe14..56de3fa 100644 --- a/MLEM.Data/RuntimeTexturePacker.cs +++ b/MLEM.Data/RuntimeTexturePacker.cs @@ -6,6 +6,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using MLEM.Extensions; using MLEM.Textures; +using static MLEM.Extensions.TextureExtensions; namespace MLEM.Data { /// @@ -34,11 +35,11 @@ namespace MLEM.Data { public TimeSpan LastTotalTime => this.LastCalculationTime + this.LastPackTime; private readonly List textures = 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 readonly int padding; private int maxWidth; @@ -50,14 +51,12 @@ namespace MLEM.Data { /// 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. - /// The padding that each texture region should have around itself. This can be useful if texture bleeding issues occur due to texture coordinate rounding. - public RuntimeTexturePacker(int maxWidth = 2048, bool autoIncreaseMaxWidth = false, bool forcePowerOfTwo = false, bool forceSquare = false, bool disposeTextures = false, int padding = default) { + public RuntimeTexturePacker(int maxWidth = 2048, bool autoIncreaseMaxWidth = false, bool forcePowerOfTwo = false, bool forceSquare = false, bool disposeTextures = false) { this.maxWidth = maxWidth; this.autoIncreaseMaxWidth = autoIncreaseMaxWidth; this.forcePowerOfTwo = forcePowerOfTwo; this.forceSquare = forceSquare; this.disposeTextures = disposeTextures; - this.padding = padding; } /// @@ -67,9 +66,10 @@ namespace MLEM.Data { /// /// The texture atlas to pack. /// The result callback which will receive the resulting texture regions. - /// Whether the texture packer's padding should be filled with a copy of the texture's border, rather than transparent pixels. This value only has an effect if the texture packer was constructed with a padding. + /// The padding that the texture should have around itself. This can be useful if texture bleeding issues occur due to texture coordinate rounding. + /// 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 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(UniformTextureAtlas atlas, Action> result, bool padWithPixels = false) { + public void Add(UniformTextureAtlas atlas, Action> result, int padding = 0, bool padWithPixels = false) { var resultRegions = new Dictionary(); for (var x = 0; x < atlas.RegionAmountX; x++) { for (var y = 0; y < atlas.RegionAmountY; y++) { @@ -78,7 +78,7 @@ namespace MLEM.Data { resultRegions.Add(pos, r); if (resultRegions.Count >= atlas.RegionAmountX * atlas.RegionAmountY) result.Invoke(resultRegions); - }, padWithPixels); + }, padding, padWithPixels); } } } @@ -90,9 +90,10 @@ namespace MLEM.Data { /// /// The texture atlas to pack. /// The result callback which will receive the resulting texture regions. - /// Whether the texture packer's padding should be filled with a copy of the texture's border, rather than transparent pixels. This value only has an effect if the texture packer was constructed with a padding. + /// The padding that the texture should have around itself. This can be useful if texture bleeding issues occur due to texture coordinate rounding. + /// 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 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(DataTextureAtlas atlas, Action> result, bool padWithPixels = false) { + public void Add(DataTextureAtlas atlas, Action> result, int padding = 0, bool padWithPixels = false) { var atlasRegions = atlas.RegionNames.ToArray(); var resultRegions = new Dictionary(); foreach (var region in atlasRegions) { @@ -100,7 +101,7 @@ namespace MLEM.Data { resultRegions.Add(region, r); if (resultRegions.Count >= atlasRegions.Length) result.Invoke(resultRegions); - }, padWithPixels); + }, padding, padWithPixels); } } @@ -110,10 +111,11 @@ namespace MLEM.Data { /// /// The texture to pack. /// The result callback which will receive the resulting texture region. - /// Whether the texture packer's padding should be filled with a copy of the texture's border, rather than transparent pixels. This value only has an effect if the texture packer was constructed with a padding. + /// The padding that the texture should have around itself. This can be useful if texture bleeding issues occur due to texture coordinate rounding. + /// 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 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(Texture2D texture, Action result, bool padWithPixels = false) { - this.Add(new TextureRegion(texture), result, padWithPixels); + public void Add(Texture2D texture, Action result, int padding = 0, bool padWithPixels = false) { + this.Add(new TextureRegion(texture), result, padding, padWithPixels); } /// @@ -122,23 +124,25 @@ namespace MLEM.Data { /// /// The texture region to pack. /// The result callback which will receive the resulting texture region. - /// Whether the texture packer's padding should be filled with a copy of the texture's border, rather than transparent pixels. This value only has an effect if the texture packer was constructed with a padding. + /// The padding that the texture should have around itself. This can be useful if texture bleeding issues occur due to texture coordinate rounding. + /// 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 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, bool padWithPixels = false) { + public void Add(TextureRegion texture, Action result, int padding = 0, bool padWithPixels = false) { if (this.PackedTexture != null) throw new InvalidOperationException("Cannot add texture to a texture packer that is already packed"); - if (texture.Width > this.maxWidth) { + var paddedWidth = texture.Width + 2 * padding; + if (paddedWidth > this.maxWidth) { if (this.autoIncreaseMaxWidth) { - this.maxWidth = texture.Width; + this.maxWidth = paddedWidth; } else { 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, padWithPixels)); + this.textures.Add(new Request(texture, result, padding, padWithPixels)); } /// - /// Packs all of the textures and texture regions added using into one texture. + /// 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. /// @@ -151,7 +155,7 @@ namespace MLEM.Data { // set pack areas for each request var stopwatch = Stopwatch.StartNew(); foreach (var request in this.textures.OrderByDescending(t => t.Texture.Width * t.Texture.Height)) - request.PackedArea = this.FindFreeArea(new Point(request.Texture.Width, request.Texture.Height)); + request.PackedArea = this.FindFreeArea(request); stopwatch.Stop(); this.LastCalculationTime = stopwatch.Elapsed; @@ -177,13 +181,14 @@ namespace MLEM.Data { // invoke callbacks foreach (var request in this.textures) { - var packedArea = request.PackedArea.Shrink(new Point(this.padding)); + var packedArea = request.PackedArea.Shrink(new Point(request.Padding)); request.Result.Invoke(new TextureRegion(this.PackedTexture, packedArea)); if (this.disposeTextures) request.Texture.Texture.Dispose(); } this.textures.Clear(); + this.dataCache.Clear(); } /// @@ -193,6 +198,7 @@ namespace MLEM.Data { this.PackedTexture?.Dispose(); this.PackedTexture = null; this.textures.Clear(); + this.dataCache.Clear(); this.LastCalculationTime = TimeSpan.Zero; this.LastPackTime = TimeSpan.Zero; } @@ -202,9 +208,10 @@ namespace MLEM.Data { this.Reset(); } - private Rectangle FindFreeArea(Point size) { - size.X += this.padding * 2; - size.Y += this.padding * 2; + private Rectangle FindFreeArea(Request request) { + var size = new Point(request.Texture.Width, request.Texture.Height); + size.X += request.Padding * 2; + size.Y += request.Padding * 2; var pos = new Point(0, 0); var lowestY = int.MaxValue; @@ -231,22 +238,27 @@ namespace MLEM.Data { } } - private void CopyRegion(TextureExtensions.TextureData destination, Request request) { - var location = request.PackedArea.Location + new Point(this.padding); - using (var data = request.Texture.Texture.GetTextureData()) { - for (var x = -this.padding; x < request.Texture.Width + this.padding; x++) { - for (var y = -this.padding; y < request.Texture.Height + this.padding; y++) { - Color srcColor; - if (!request.PadWithPixels && (x < 0 || y < 0 || x >= request.Texture.Width || y >= request.Texture.Height)) { - // if we're out of bounds and not padding with pixels, we make it transparent - srcColor = Color.Transparent; - } else { - // otherwise, we just use the closest pixel that is actually in bounds, causing the border pixels to be doubled up - var src = new Point(MathHelper.Clamp(x, 0, request.Texture.Width - 1), MathHelper.Clamp(y, 0, request.Texture.Height - 1)); - srcColor = data[request.Texture.Position + src]; - } - destination[location + new Point(x, y)] = srcColor; + private void CopyRegion(TextureData destination, Request request) { + // we cache texture data in case multiple requests use the same underlying texture + // this collection doesn't need to be disposed since we don't actually edit these textures + if (!this.dataCache.TryGetValue(request.Texture.Texture, out var data)) { + data = request.Texture.Texture.GetTextureData(); + this.dataCache.Add(request.Texture.Texture, data); + } + + var location = request.PackedArea.Location + new Point(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; + if (!request.PadWithPixels && (x < 0 || y < 0 || x >= request.Texture.Width || y >= request.Texture.Height)) { + // if we're out of bounds and not padding with pixels, we make it transparent + srcColor = Color.Transparent; + } else { + // otherwise, we just use the closest pixel that is actually in bounds, causing the border pixels to be doubled up + var src = new Point(MathHelper.Clamp(x, 0, request.Texture.Width - 1), MathHelper.Clamp(y, 0, request.Texture.Height - 1)); + srcColor = data[request.Texture.Position + src]; } + destination[location + new Point(x, y)] = srcColor; } } } @@ -262,12 +274,14 @@ namespace MLEM.Data { public readonly TextureRegion Texture; public readonly Action Result; + public readonly int Padding; public readonly bool PadWithPixels; public Rectangle PackedArea; - public Request(TextureRegion texture, Action result, bool padWithPixels) { + public Request(TextureRegion texture, Action result, int padding, bool padWithPixels) { this.Texture = texture; this.Result = result; + this.Padding = padding; this.PadWithPixels = padWithPixels; } diff --git a/Sandbox/GameImpl.cs b/Sandbox/GameImpl.cs index 8aeed78..01af04a 100644 --- a/Sandbox/GameImpl.cs +++ b/Sandbox/GameImpl.cs @@ -318,16 +318,16 @@ namespace Sandbox { } this.UiSystem.Add("Keybinds", keybindPanel); - var packer = new RuntimeTexturePacker(padding: 1); + var packer = new RuntimeTexturePacker(); var regions = new List(); packer.Add(new UniformTextureAtlas(tex, 16, 16), r => { regions.AddRange(r.Values); Console.WriteLine($"Returned {r.Count} regions: {string.Join(", ", r.Select(kv => kv.Key + ": " + kv.Value.Area))}"); - }, true); + }, 1, true); packer.Add(this.Content.LoadTextureAtlas("Textures/Furniture"), r => { regions.AddRange(r.Values); Console.WriteLine($"Returned {r.Count} regions: {string.Join(", ", r.Select(kv => kv.Key + ": " + kv.Value.Area))}"); - }, true); + }, 1, true); packer.Pack(this.GraphicsDevice); packer.PackedTexture.SaveAsPng(File.Create("_Packed.png"), packer.PackedTexture.Width, packer.PackedTexture.Height);