diff --git a/osu.Framework.Benchmarks/BenchmarkRectanglePacking.cs b/osu.Framework.Benchmarks/BenchmarkRectanglePacking.cs new file mode 100644 index 0000000000..3f2dec06ca --- /dev/null +++ b/osu.Framework.Benchmarks/BenchmarkRectanglePacking.cs @@ -0,0 +1,65 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using BenchmarkDotNet.Attributes; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Utils; +using osu.Framework.Utils.RectanglePacking; + +namespace osu.Framework.Benchmarks +{ + [MemoryDiagnoser] + public class BenchmarkRectanglePacking + { + private ShelfRectanglePacker shelf = null!; + private ShelfWithRemainderRectanglePacker shelfRemainder = null!; + private MaximalRectanglePacker maximal = null!; + private GuillotineRectanglePacker guillotine = null!; + + private readonly List rects = new List(); + + [GlobalSetup] + public void GlobalSetup() + { + for (int i = 0; i < 30; i++) + rects.Add(new Vector2I(RNG.Next(20, 30), RNG.Next(20, 30))); + } + + [Benchmark] + public void PopulateShelf() + { + shelf = new ShelfRectanglePacker(new Vector2I(1024)); + + for (int i = 0; i < rects.Count; i++) + shelf.TryAdd(rects[i].X, rects[i].Y); + } + + [Benchmark] + public void PopulateShelfRemainder() + { + shelfRemainder = new ShelfWithRemainderRectanglePacker(new Vector2I(1024)); + + for (int i = 0; i < rects.Count; i++) + shelfRemainder.TryAdd(rects[i].X, rects[i].Y); + } + + [Benchmark] + public void PopulateMaximal() + { + maximal = new MaximalRectanglePacker(new Vector2I(1024), FitStrategy.BestShortSide); + + for (int i = 0; i < rects.Count; i++) + maximal.TryAdd(rects[i].X, rects[i].Y); + } + + [Benchmark] + public void PopulateGuillotine() + { + guillotine = new GuillotineRectanglePacker(new Vector2I(1024), FitStrategy.SmallestArea, SplitStrategy.ShorterLeftoverAxis); + + for (int i = 0; i < rects.Count; i++) + guillotine.TryAdd(rects[i].X, rects[i].Y); + } + } +} diff --git a/osu.Framework.Tests/Visual/Layout/TestSceneRectanglePacking.cs b/osu.Framework.Tests/Visual/Layout/TestSceneRectanglePacking.cs new file mode 100644 index 0000000000..1b31d203b3 --- /dev/null +++ b/osu.Framework.Tests/Visual/Layout/TestSceneRectanglePacking.cs @@ -0,0 +1,241 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics.Primitives; +using System.Collections.Generic; +using osuTK; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; +using osuTK.Graphics; +using osu.Framework.Utils; +using NUnit.Framework; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Testing; +using osu.Framework.Utils.RectanglePacking; +using osu.Framework.Bindables; + +namespace osu.Framework.Tests.Visual.Layout +{ + public partial class TestSceneRectanglePacking : FrameworkTestScene + { + private const int size = 170; + + public TestSceneRectanglePacking() + { + Add(new GridContainer + { + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Relative, 0.2f), + new Dimension(GridSizeMode.Relative, 0.2f), + new Dimension(GridSizeMode.Relative, 0.2f), + new Dimension(GridSizeMode.Relative, 0.2f), + new Dimension(GridSizeMode.Relative, 0.2f) + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.Relative, 0.33f), + new Dimension(GridSizeMode.Relative, 0.33f), + new Dimension(GridSizeMode.Relative, 0.33f), + }, + Content = new[] + { + new Drawable[] + { + new DrawableBin(new ShelfRectanglePacker(new Vector2I(size))), + new DrawableBin(new ShelfWithRemainderRectanglePacker(new Vector2I(size))), + new DrawableBin(new MaximalRectanglePacker(new Vector2I(size), FitStrategy.First)), + new DrawableBin(new MaximalRectanglePacker(new Vector2I(size), FitStrategy.TopLeft)), + new DrawableBin(new MaximalRectanglePacker(new Vector2I(size), FitStrategy.BestLongSide)), + }, + new Drawable[] + { + new DrawableBin(new MaximalRectanglePacker(new Vector2I(size), FitStrategy.BestShortSide)), + new DrawableBin(new MaximalRectanglePacker(new Vector2I(size), FitStrategy.SmallestArea)), + new DrawableBin(new GuillotineRectanglePacker(new Vector2I(size), FitStrategy.First, SplitStrategy.ShorterAxis)), + new DrawableBin(new GuillotineRectanglePacker(new Vector2I(size), FitStrategy.First, SplitStrategy.ShorterLeftoverAxis)), + new DrawableBin(new GuillotineRectanglePacker(new Vector2I(size), FitStrategy.BestShortSide, SplitStrategy.ShorterAxis)) + }, + new Drawable[] + { + new DrawableBin(new GuillotineRectanglePacker(new Vector2I(size), FitStrategy.BestShortSide, SplitStrategy.ShorterLeftoverAxis)), + new DrawableBin(new GuillotineRectanglePacker(new Vector2I(size), FitStrategy.SmallestArea, SplitStrategy.ShorterAxis)), + new DrawableBin(new GuillotineRectanglePacker(new Vector2I(size), FitStrategy.SmallestArea, SplitStrategy.ShorterLeftoverAxis)), + new DrawableBin(new GuillotineRectanglePacker(new Vector2I(size), FitStrategy.TopLeft, SplitStrategy.ShorterAxis)), + new DrawableBin(new GuillotineRectanglePacker(new Vector2I(size), FitStrategy.TopLeft, SplitStrategy.ShorterLeftoverAxis)) + } + } + }); + } + + private int minWidth = 1; + private int maxWidth = size / 4; + private int minHeight = 1; + private int maxHeight = size / 4; + private int singleWidth = size / 4; + private int singleHeight = size / 4; + + [Test] + public void TestSingle() + { + AddSliderStep("Width", 1, size, size / 4, w => singleWidth = w); + AddSliderStep("Height", 1, size, size / 4, h => singleHeight = h); + + AddStep("Reset", resetAll); + AddStep("Add", () => tryAddToAll(singleWidth, singleHeight)); + } + + [Test] + public void TestMany() + { + AddSliderStep("Min width", 1, size, 1, w => minWidth = w); + AddSliderStep("Max width", 2, size, size / 4, h => maxWidth = h); + AddSliderStep("Min height", 1, size, 1, w => minHeight = w); + AddSliderStep("Max height", 2, size, size / 4, h => maxHeight = h); + + AddStep("Reset", resetAll); + AddUntilStep("Add until all filled", () => tryAddToAll(RNG.Next(minWidth, maxWidth), RNG.Next(minHeight, maxHeight))); + } + + private void resetAll() + { + foreach (var b in bins) + b.Reset(); + } + + private bool tryAddToAll(int width, int height) + { + bool canAddMore = false; + + foreach (var b in bins) + canAddMore |= b.TryAdd(width, height); + + return !canAddMore; + } + + private IEnumerable bins => this.ChildrenOfType(); + + private partial class DrawableBin : Container + { + private readonly IRectanglePacker packer; + private readonly BindableInt counter = new BindableInt(); + private readonly Container placed; + private readonly SpriteText info; + private bool blocked; + + public DrawableBin(IRectanglePacker packer) + { + this.packer = packer; + + Size = packer.BinSize; + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + Masking = true; + BorderColour = Color4.White; + BorderThickness = 3; + + AddRange(new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true + }, + placed = new Container + { + RelativeSizeAxes = Axes.Both, + }, + new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0.5f, + Colour = Color4.Black + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 5), + Children = new Drawable[] + { + new TextFlowContainer + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + TextAnchor = Anchor.Centre, + Text = packer.ToString() ?? string.Empty + }, + info = new SpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre + } + } + } + } + } + }); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + counter.BindValueChanged(c => info.Text = $"Count: {c.NewValue}"); + } + + public void Reset() + { + blocked = false; + BorderColour = Color4.White; + placed.Clear(); + packer.Reset(); + + counter.Value = 0; + } + + public bool TryAdd(int width, int height) + { + if (blocked) + return false; + + Vector2I? positionToPlace = packer.TryAdd(width, height); + + if (!positionToPlace.HasValue) + { + blocked = true; + BorderColour = Color4.Red; + return false; + } + + placed.Add(new Box + { + Size = new Vector2(width, height), + Position = positionToPlace.Value, + Colour = getRandomColour() + }); + + counter.Value++; + return true; + } + + private static Color4 getRandomColour() + { + return new Color4(RNG.NextSingle(0.5f, 1f), RNG.NextSingle(0.5f, 1f), RNG.NextSingle(0.5f, 1f), 1f); + } + } + } +} diff --git a/osu.Framework.Tests/Visual/Layout/TestSceneRectanglePackingCompare.cs b/osu.Framework.Tests/Visual/Layout/TestSceneRectanglePackingCompare.cs new file mode 100644 index 0000000000..d1ce37b31e --- /dev/null +++ b/osu.Framework.Tests/Visual/Layout/TestSceneRectanglePackingCompare.cs @@ -0,0 +1,401 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Utils; +using osu.Framework.Utils.RectanglePacking; +using osuTK; +using osuTK.Graphics; + +namespace osu.Framework.Tests.Visual.Layout +{ + public partial class TestSceneRectanglePackingCompare : FrameworkTestScene + { + private int minWidth = 1; + private int maxWidth = 1000; + private int minHeight = 1; + private int maxHeight = 1000; + private int binSize = 1000; + private int runCount = 10; + + private List packers = null!; + private Results results = null!; + + protected override void LoadComplete() + { + base.LoadComplete(); + + AddSliderStep("Bin Size", 1, 2000, 1000, s => binSize = s); + AddSliderStep("Min width", 1, 1000, 1, w => minWidth = w); + AddSliderStep("Max width", 2, 1000, 250, h => maxWidth = h); + AddSliderStep("Min height", 1, 1000, 1, w => minHeight = w); + AddSliderStep("Max height", 2, 1000, 250, h => maxHeight = h); + AddSliderStep("Run Count", 1, 100, 10, c => runCount = c); + + AddStep("Run", run); + } + + private void run() + { + packers = new List + { + new ShelfRectanglePacker(new Vector2I(binSize)), + new ShelfWithRemainderRectanglePacker(new Vector2I(binSize)), + new GuillotineRectanglePacker(new Vector2I(binSize), FitStrategy.First, SplitStrategy.ShorterAxis), + new GuillotineRectanglePacker(new Vector2I(binSize), FitStrategy.First, SplitStrategy.ShorterLeftoverAxis), + new GuillotineRectanglePacker(new Vector2I(binSize), FitStrategy.SmallestArea, SplitStrategy.ShorterAxis), + new GuillotineRectanglePacker(new Vector2I(binSize), FitStrategy.SmallestArea, SplitStrategy.ShorterLeftoverAxis), + new GuillotineRectanglePacker(new Vector2I(binSize), FitStrategy.TopLeft, SplitStrategy.ShorterAxis), + new GuillotineRectanglePacker(new Vector2I(binSize), FitStrategy.TopLeft, SplitStrategy.ShorterLeftoverAxis), + new GuillotineRectanglePacker(new Vector2I(binSize), FitStrategy.BestShortSide, SplitStrategy.ShorterAxis), + new GuillotineRectanglePacker(new Vector2I(binSize), FitStrategy.BestShortSide, SplitStrategy.ShorterLeftoverAxis), + new MaximalRectanglePacker(new Vector2I(binSize), FitStrategy.First), + new MaximalRectanglePacker(new Vector2I(binSize), FitStrategy.TopLeft), + new MaximalRectanglePacker(new Vector2I(binSize), FitStrategy.BestLongSide), + new MaximalRectanglePacker(new Vector2I(binSize), FitStrategy.BestShortSide), + new MaximalRectanglePacker(new Vector2I(binSize), FitStrategy.SmallestArea), + }; + + results = new Results(packers); + + for (int i = 0; i < runCount; i++) + { + var currentRun = new Dictionary(); + + foreach (var packer in packers) + currentRun.Add(packer, new RunInfo()); + + addUntilAllFull(currentRun); + results.LoadRun(currentRun); + + foreach (var packer in packers) + packer.Reset(); + } + + Child = new ResultsTable(results); + } + + private void addUntilAllFull(Dictionary currentRun) + { + bool allFull = false; + + while (!allFull) + allFull = addToAll(new Vector2I(RNG.Next(minWidth, maxWidth), RNG.Next(minHeight, maxHeight)), currentRun); + } + + private bool addToAll(Vector2I toAdd, Dictionary currentRun) + { + bool added = false; + + foreach (var packer in packers) + { + Stopwatch stopwatch = new Stopwatch(); + + stopwatch.Start(); + Vector2I? pos = packer.TryAdd(toAdd.X, toAdd.Y); + stopwatch.Stop(); + + if (pos.HasValue) + currentRun[packer].OnRectangleAdded(stopwatch.Elapsed.Ticks); + + added |= pos.HasValue; + } + + return !added; + } + + private partial class ResultsTable : CompositeDrawable + { + private readonly Results results; + + public ResultsTable(Results results) + { + this.results = results; + } + + [BackgroundDependencyLoader] + private void load() + { + float maxCount = results.BestFit; + double bestAverage = results.BestAverageTime; + + RelativeSizeAxes = Axes.Both; + InternalChild = new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, 30), + new Dimension() + }, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Relative, 1f) + }, + Content = new[] + { + new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(GridSizeMode.Relative, size: 1) + }, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, 400), + new Dimension(GridSizeMode.Distributed, 0.5f), + new Dimension(GridSizeMode.Distributed, 0.5f), + }, + Content = new[] + { + new Drawable[] + { + new SpriteText + { + Text = "Name", + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }, + new SpriteText + { + Text = "Packed rectangles (avg)", + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }, + new SpriteText + { + Text = "Time to add (avg)", + Anchor = Anchor.Centre, + Origin = Anchor.Centre + } + } + } + } + }, + new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + Child = new BasicScrollContainer + { + RelativeSizeAxes = Axes.Both, + Child = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 5), + Children = results.Dictionary.Values.OrderByDescending(r => r.AverageCount()).Select(r => new DrawableResult(r, maxCount, bestAverage)).ToArray() + } + } + } + } + } + }; + } + + private partial class DrawableResult : CompositeDrawable + { + private readonly RunsInfo info; + private readonly float maxCount; + private readonly double bestAverage; + + public DrawableResult(RunsInfo info, float maxCount, double bestAverage) + { + this.info = info; + this.maxCount = maxCount; + this.bestAverage = bestAverage; + } + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.X; + Height = 30; + InternalChild = new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(GridSizeMode.Relative, size: 1) + }, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, 400), + new Dimension(GridSizeMode.Distributed, 0.5f), + new Dimension(GridSizeMode.Distributed, 0.5f), + }, + Content = new[] + { + new Drawable[] + { + new SpriteText + { + Text = info.Name, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.X, + Truncate = true + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Horizontal = 5 }, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Width = info.AverageCount() / maxCount, + Colour = Color4.Gray + }, + new SpriteText + { + Text = info.AverageCount().ToString(CultureInfo.InvariantCulture), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Shadow = true + } + } + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Horizontal = 5 }, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Width = (float)(1.0 - bestAverage / info.AverageTime()), + Colour = Color4.Gray + }, + new SpriteText + { + Text = info.AverageTime().ToString(CultureInfo.InvariantCulture), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Shadow = true + } + } + } + } + } + }; + } + } + } + + private class Results + { + public readonly Dictionary Dictionary = new Dictionary(); + + public Results(IEnumerable packers) + { + foreach (var packer in packers) + { + Dictionary.Add(packer, new RunsInfo(packer.ToString() ?? string.Empty)); + } + } + + public void LoadRun(Dictionary runResults) + { + foreach (var r in runResults) + Dictionary[r.Key].AddRunInfo(r.Value); + } + + public float BestFit + { + get + { + float max = 0; + + foreach (var info in Dictionary.Values) + max = Math.Max(max, info.AverageCount()); + + return max; + } + } + + public double BestAverageTime + { + get + { + double min = double.MaxValue; + + foreach (var info in Dictionary.Values) + min = Math.Min(min, info.AverageTime()); + + return min; + } + } + } + + private class RunsInfo + { + public string Name { get; } + + private List runInfos { get; } = new List(); + + public RunsInfo(string name) + { + Name = name; + } + + public void AddRunInfo(RunInfo info) + { + runInfos.Add(info); + } + + public float AverageCount() + { + int sum = 0; + + foreach (var info in runInfos) + sum += info.RectangleCount; + + return sum / (float)runInfos.Count; + } + + public double AverageTime() + { + double sum = 0; + + foreach (var info in runInfos) + sum += info.AverageTime(); + + return sum / runInfos.Count; + } + } + + private class RunInfo + { + public int RectangleCount { get; private set; } + + private readonly List timings = new List(); + + public void OnRectangleAdded(long time) + { + RectangleCount++; + timings.Add(time); + } + + public double AverageTime() => timings.Average(); + } + } +} diff --git a/osu.Framework/Graphics/Textures/TextureAtlas.cs b/osu.Framework/Graphics/Textures/TextureAtlas.cs index a379dc577e..4f1e660977 100644 --- a/osu.Framework/Graphics/Textures/TextureAtlas.cs +++ b/osu.Framework/Graphics/Textures/TextureAtlas.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using System.Diagnostics; using System.Numerics; using osu.Framework.Graphics.Sprites; @@ -11,6 +9,7 @@ using osu.Framework.Logging; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; +using osu.Framework.Utils.RectanglePacking; namespace osu.Framework.Graphics.Textures { @@ -21,10 +20,10 @@ public partial class TextureAtlas // inflating texture rectangles. internal const int PADDING = (1 << IRenderer.MAX_MIPMAP_LEVELS) * Sprite.MAX_EDGE_SMOOTHNESS; internal const int WHITE_PIXEL_SIZE = 1; - - private readonly List subTextureBounds = new List(); private Texture? atlasTexture; + private readonly IRectanglePacker rectanglePacker; + private readonly IRenderer renderer; private readonly int atlasWidth; private readonly int atlasHeight; @@ -32,8 +31,6 @@ public partial class TextureAtlas private int maxFittableWidth => atlasWidth - PADDING * 2; private int maxFittableHeight => atlasHeight - PADDING * 2; - private Vector2I currentPosition; - internal TextureWhitePixel WhitePixel { get @@ -56,6 +53,7 @@ public TextureAtlas(IRenderer renderer, int width, int height, bool manualMipmap this.renderer = renderer; atlasWidth = width; atlasHeight = height; + rectanglePacker = new ShelfRectanglePacker(new Vector2I(width - PADDING, height - PADDING)); this.manualMipmaps = manualMipmaps; this.filteringMode = filteringMode; } @@ -72,21 +70,17 @@ public void Reset() { lock (textureRetrievalLock) { - subTextureBounds.Clear(); - currentPosition = Vector2I.Zero; + rectanglePacker.Reset(); // We pass PADDING/2 as opposed to PADDING such that the padded region of each individual texture // occupies half of the padded space. atlasTexture = new BackingAtlasTexture(renderer, atlasWidth, atlasHeight, manualMipmaps, filteringMode, PADDING / 2); - RectangleI bounds = new RectangleI(0, 0, WHITE_PIXEL_SIZE, WHITE_PIXEL_SIZE); - subTextureBounds.Add(bounds); + rectanglePacker.TryAdd(WHITE_PIXEL_SIZE, WHITE_PIXEL_SIZE); - using (var whiteTex = new TextureRegion(atlasTexture, bounds, WrapMode.Repeat, WrapMode.Repeat)) + using (var whiteTex = new TextureRegion(atlasTexture, new RectangleI(0, 0, WHITE_PIXEL_SIZE, WHITE_PIXEL_SIZE), WrapMode.Repeat, WrapMode.Repeat)) // Generate white padding as if the white texture was wrapped, even though it isn't whiteTex.SetData(new TextureUpload(new Image(SixLabors.ImageSharp.Configuration.Default, whiteTex.Width, whiteTex.Height, new Rgba32(Vector4.One)))); - - currentPosition = new Vector2I(PADDING + WHITE_PIXEL_SIZE, PADDING); } } @@ -108,10 +102,7 @@ public void Reset() Vector2I position = findPosition(width, height); Debug.Assert(atlasTexture != null, "Atlas texture should not be null after findPosition()."); - RectangleI bounds = new RectangleI(position.X, position.Y, width, height); - subTextureBounds.Add(bounds); - - return new TextureRegion(atlasTexture, bounds, wrapModeS, wrapModeT); + return new TextureRegion(atlasTexture, new RectangleI(position.X, position.Y, width, height), wrapModeS, wrapModeT); } } @@ -149,29 +140,14 @@ private Vector2I findPosition(int width, int height) Reset(); } - if (currentPosition.Y + height + PADDING > atlasHeight) - { - Logger.Log($"TextureAtlas size exceeded {++exceedCount} time(s); generating new texture ({atlasWidth}x{atlasHeight})", LoggingTarget.Performance); - Reset(); - } - - if (currentPosition.X + width + PADDING > atlasWidth) - { - int maxY = 0; - - foreach (RectangleI bounds in subTextureBounds) - maxY = Math.Max(maxY, bounds.Bottom + PADDING); - - subTextureBounds.Clear(); - currentPosition = new Vector2I(PADDING, maxY); - - return findPosition(width, height); - } + Vector2I? result = rectanglePacker.TryAdd(width + PADDING, height + PADDING); - var result = currentPosition; - currentPosition.X += width + PADDING; + if (result.HasValue) + return new Vector2I(result.Value.X + PADDING, result.Value.Y + PADDING); - return result; + Logger.Log($"TextureAtlas size exceeded {++exceedCount} time(s); generating new texture ({atlasWidth}x{atlasHeight})", LoggingTarget.Performance); + Reset(); + return findPosition(width, height); } } } diff --git a/osu.Framework/Utils/RectanglePacking/FreeSpaceTrackingRectanglePacker.cs b/osu.Framework/Utils/RectanglePacking/FreeSpaceTrackingRectanglePacker.cs new file mode 100644 index 0000000000..b8e4bb84cb --- /dev/null +++ b/osu.Framework/Utils/RectanglePacking/FreeSpaceTrackingRectanglePacker.cs @@ -0,0 +1,91 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using osu.Framework.Graphics.Primitives; + +namespace osu.Framework.Utils.RectanglePacking +{ + public abstract class FreeSpaceTrackingRectanglePacker : RectanglePacker + { + protected readonly List FreeSpaces = new List(); + protected readonly FitStrategy Strategy; + + protected FreeSpaceTrackingRectanglePacker(Vector2I binSize, FitStrategy strategy) + : base(binSize) + { + Strategy = strategy; + } + + public override void Reset() + { + FreeSpaces.Clear(); + FreeSpaces.Add(new RectangleI(0, 0, BinSize.X, BinSize.Y)); + } + + public override Vector2I? TryAdd(int width, int height) + { + RectangleI? bestFit = null; + int bestFitIndex = 0; + + for (int i = 0; i < FreeSpaces.Count; i++) + { + if (FreeSpaces[i].Width < width || FreeSpaces[i].Height < height) + continue; + + if (Strategy == FitStrategy.First) + { + bestFit = FreeSpaces[i]; + bestFitIndex = i; + break; + } + + if (bestFit.HasValue && !isBetterFit(bestFit.Value, FreeSpaces[i])) + continue; + + bestFit = FreeSpaces[i]; + bestFitIndex = i; + } + + if (!bestFit.HasValue) + return null; + + UpdateFreeSpaces(new RectangleI(bestFit.Value.X, bestFit.Value.Y, width, height), bestFitIndex); + return bestFit.Value.TopLeft; + } + + protected abstract void UpdateFreeSpaces(RectangleI newlyPlaced, int placeIndex); + + private bool isBetterFit(RectangleI currentBest, RectangleI potentialBest) + { + switch (Strategy) + { + default: + case FitStrategy.First: + return false; + + case FitStrategy.TopLeft: + return potentialBest.Y < currentBest.Y || (potentialBest.Y == currentBest.Y && potentialBest.X < currentBest.X); + + case FitStrategy.BestLongSide: + return Math.Max(potentialBest.Height, potentialBest.Width) < Math.Max(currentBest.Height, currentBest.Width); + + case FitStrategy.BestShortSide: + return Math.Min(potentialBest.Height, potentialBest.Width) < Math.Min(currentBest.Height, currentBest.Width); + + case FitStrategy.SmallestArea: + return potentialBest.Area < currentBest.Area; + } + } + } + + public enum FitStrategy + { + First, + TopLeft, + SmallestArea, + BestShortSide, + BestLongSide + } +} \ No newline at end of file diff --git a/osu.Framework/Utils/RectanglePacking/GuillotineRectanglePacker.cs b/osu.Framework/Utils/RectanglePacking/GuillotineRectanglePacker.cs new file mode 100644 index 0000000000..11540e5a16 --- /dev/null +++ b/osu.Framework/Utils/RectanglePacking/GuillotineRectanglePacker.cs @@ -0,0 +1,69 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics.Primitives; + +namespace osu.Framework.Utils.RectanglePacking +{ + public class GuillotineRectanglePacker : FreeSpaceTrackingRectanglePacker + { + private readonly SplitStrategy splitStrategy; + + public GuillotineRectanglePacker(Vector2I binSize, FitStrategy strategy, SplitStrategy splitStrategy) + : base(binSize, strategy) + { + this.splitStrategy = splitStrategy; + } + + protected override void UpdateFreeSpaces(RectangleI newlyPlaced, int placeIndex) + { + RectangleI spaceToDivide = FreeSpaces[placeIndex]; + + switch (splitStrategy) + { + default: + case SplitStrategy.ShorterAxis: + if (spaceToDivide.Width < spaceToDivide.Height) + splitHorizontally(spaceToDivide, newlyPlaced); + else + splitVertically(spaceToDivide, newlyPlaced); + break; + + case SplitStrategy.ShorterLeftoverAxis: + if (spaceToDivide.Width - newlyPlaced.Width < spaceToDivide.Height - newlyPlaced.Height) + splitHorizontally(spaceToDivide, newlyPlaced); + else + splitVertically(spaceToDivide, newlyPlaced); + break; + } + + FreeSpaces.RemoveAt(placeIndex); + } + + private void splitHorizontally(RectangleI spaceToDivide, RectangleI newlyPlaced) + { + if (spaceToDivide.Width > newlyPlaced.Width) + FreeSpaces.Add(new RectangleI(newlyPlaced.Right, spaceToDivide.Y, spaceToDivide.Width - newlyPlaced.Width, newlyPlaced.Height)); + + if (spaceToDivide.Height > newlyPlaced.Height) + FreeSpaces.Add(new RectangleI(spaceToDivide.X, newlyPlaced.Bottom, spaceToDivide.Width, spaceToDivide.Height - newlyPlaced.Height)); + } + + private void splitVertically(RectangleI spaceToDivide, RectangleI newlyPlaced) + { + if (spaceToDivide.Height > newlyPlaced.Height) + FreeSpaces.Add(new RectangleI(spaceToDivide.X, newlyPlaced.Bottom, newlyPlaced.Width, spaceToDivide.Height - newlyPlaced.Height)); + + if (spaceToDivide.Width > newlyPlaced.Width) + FreeSpaces.Add(new RectangleI(newlyPlaced.Right, spaceToDivide.Y, spaceToDivide.Width - newlyPlaced.Width, spaceToDivide.Height)); + } + + public override string ToString() => $"Guillotine ({Strategy}, {splitStrategy})"; + } + + public enum SplitStrategy + { + ShorterAxis, + ShorterLeftoverAxis + } +} \ No newline at end of file diff --git a/osu.Framework/Utils/RectanglePacking/IRectanglePacker.cs b/osu.Framework/Utils/RectanglePacking/IRectanglePacker.cs new file mode 100644 index 0000000000..c1decd3d9e --- /dev/null +++ b/osu.Framework/Utils/RectanglePacking/IRectanglePacker.cs @@ -0,0 +1,28 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics.Primitives; + +namespace osu.Framework.Utils.RectanglePacking +{ + public interface IRectanglePacker + { + /// + /// The size of the bin to place rectangles into. + /// + public Vector2I BinSize { get; } + + /// + /// Adds a new rectangle into the bin. + /// + /// Width of rectangle to be added. + /// Height of rectangle to be added. + /// Position of added rectangle. Null if no space available. + public Vector2I? TryAdd(int width, int height); + + /// + /// Removes all the rectangles from the bin. + /// + public void Reset(); + } +} \ No newline at end of file diff --git a/osu.Framework/Utils/RectanglePacking/MaximalRectanglePacker.cs b/osu.Framework/Utils/RectanglePacking/MaximalRectanglePacker.cs new file mode 100644 index 0000000000..4b75d67e45 --- /dev/null +++ b/osu.Framework/Utils/RectanglePacking/MaximalRectanglePacker.cs @@ -0,0 +1,52 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics.Primitives; + +namespace osu.Framework.Utils.RectanglePacking +{ + public class MaximalRectanglePacker : FreeSpaceTrackingRectanglePacker + { + public MaximalRectanglePacker(Vector2I binSize, FitStrategy strategy) + : base(binSize, strategy) + { + } + + protected override void UpdateFreeSpaces(RectangleI newlyPlaced, int placeIndex) + { + for (int i = FreeSpaces.Count - 1; i >= 0; i--) + { + if (!newlyPlaced.IntersectsWith(FreeSpaces[i])) + continue; + + if (newlyPlaced.Top > FreeSpaces[i].Top && newlyPlaced.Top < FreeSpaces[i].Bottom) + FreeSpaces.Add(new RectangleI(FreeSpaces[i].X, FreeSpaces[i].Y, FreeSpaces[i].Width, newlyPlaced.Top - FreeSpaces[i].Top)); + + if (newlyPlaced.Bottom >= FreeSpaces[i].Top && newlyPlaced.Bottom < FreeSpaces[i].Bottom) + FreeSpaces.Add(new RectangleI(FreeSpaces[i].X, newlyPlaced.Bottom, FreeSpaces[i].Width, FreeSpaces[i].Bottom - newlyPlaced.Bottom)); + + if (newlyPlaced.Left > FreeSpaces[i].Left && newlyPlaced.Left < FreeSpaces[i].Right) + FreeSpaces.Add(new RectangleI(FreeSpaces[i].X, FreeSpaces[i].Y, newlyPlaced.Left - FreeSpaces[i].Left, FreeSpaces[i].Height)); + + if (newlyPlaced.Right >= FreeSpaces[i].Left && newlyPlaced.Right < FreeSpaces[i].Right) + FreeSpaces.Add(new RectangleI(newlyPlaced.Right, FreeSpaces[i].Y, FreeSpaces[i].Right - newlyPlaced.Right, FreeSpaces[i].Height)); + + FreeSpaces.RemoveAt(i); + } + + for (int i = FreeSpaces.Count - 1; i >= 0; i--) + { + for (int j = FreeSpaces.Count - 1; j >= 0; j--) + { + if (i == j || !FreeSpaces[j].Contains(FreeSpaces[i])) + continue; + + FreeSpaces.RemoveAt(i); + break; + } + } + } + + public override string ToString() => $"Maximal ({Strategy})"; + } +} \ No newline at end of file diff --git a/osu.Framework/Utils/RectanglePacking/RectanglePacker.cs b/osu.Framework/Utils/RectanglePacking/RectanglePacker.cs new file mode 100644 index 0000000000..8b62cc1c83 --- /dev/null +++ b/osu.Framework/Utils/RectanglePacking/RectanglePacker.cs @@ -0,0 +1,22 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics.Primitives; + +namespace osu.Framework.Utils.RectanglePacking +{ + public abstract class RectanglePacker : IRectanglePacker + { + public Vector2I BinSize { get; protected set; } + + protected RectanglePacker(Vector2I binSize) + { + BinSize = binSize; + Reset(); + } + + public abstract Vector2I? TryAdd(int width, int height); + + public abstract void Reset(); + } +} \ No newline at end of file diff --git a/osu.Framework/Utils/RectanglePacking/ShelfRectanglePacker.cs b/osu.Framework/Utils/RectanglePacking/ShelfRectanglePacker.cs new file mode 100644 index 0000000000..3d022ec173 --- /dev/null +++ b/osu.Framework/Utils/RectanglePacking/ShelfRectanglePacker.cs @@ -0,0 +1,47 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Graphics.Primitives; + +namespace osu.Framework.Utils.RectanglePacking +{ + public class ShelfRectanglePacker : RectanglePacker + { + public ShelfRectanglePacker(Vector2I binSize) + : base(binSize) + { + } + + private int x, y, currentShelfHeight; + + public override void Reset() + { + x = y = currentShelfHeight = 0; + } + + public override Vector2I? TryAdd(int width, int height) + { + if (y + height > BinSize.Y) + return null; + + if (x + width > BinSize.X) + { + x = 0; + y += currentShelfHeight; + currentShelfHeight = 0; + + return TryAdd(width, height); + } + + Vector2I result = new Vector2I(x, y); + + x += width; + currentShelfHeight = Math.Max(currentShelfHeight, height); + + return result; + } + + public override string ToString() => "Shelf"; + } +} \ No newline at end of file diff --git a/osu.Framework/Utils/RectanglePacking/ShelfWithRemainderRectanglePacker.cs b/osu.Framework/Utils/RectanglePacking/ShelfWithRemainderRectanglePacker.cs new file mode 100644 index 0000000000..98f2b76149 --- /dev/null +++ b/osu.Framework/Utils/RectanglePacking/ShelfWithRemainderRectanglePacker.cs @@ -0,0 +1,69 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using osu.Framework.Graphics.Primitives; + +namespace osu.Framework.Utils.RectanglePacking +{ + public class ShelfWithRemainderRectanglePacker : RectanglePacker + { + public ShelfWithRemainderRectanglePacker(Vector2I binSize) + : base(binSize) + { + } + + private int x, y, currentShelfHeight; + private readonly List freeSpaces = new List(); + + public override void Reset() + { + freeSpaces.Clear(); + x = y = currentShelfHeight = 0; + } + + public override Vector2I? TryAdd(int width, int height) + { + Vector2I? result; + + for (int i = 0; i < freeSpaces.Count; i++) + { + if (height > freeSpaces[i].Height || width > freeSpaces[i].Width) + continue; + + result = freeSpaces[i].TopLeft; + + if (width < freeSpaces[i].Width) + freeSpaces[i] = new RectangleI(freeSpaces[i].X + width, freeSpaces[i].Y, freeSpaces[i].Width - width, freeSpaces[i].Height); + else + freeSpaces.RemoveAt(i); + + return result; + } + + if (y + height > BinSize.Y) + return null; + + if (x + width > BinSize.X) + { + freeSpaces.Add(new RectangleI(x, y, BinSize.X - x, currentShelfHeight)); + + x = 0; + y += currentShelfHeight; + currentShelfHeight = 0; + + return TryAdd(width, height); + } + + result = new Vector2I(x, y); + + x += width; + currentShelfHeight = Math.Max(currentShelfHeight, height); + + return result; + } + + public override string ToString() => "Shelf (with remainder)"; + } +} \ No newline at end of file