// 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.Numerics; using osu.Framework.Graphics.OpenGL.Textures; using osuTK.Graphics.ES30; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Primitives; using osu.Framework.Logging; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; namespace osu.Framework.Graphics.Textures { public class TextureAtlas { // We are adding an extra padding on top of the padding required by // mipmap blending in order to support smooth edges without antialiasing which requires // inflating texture rectangles. internal const int PADDING = (1 << TextureGLSingle.MAX_MIPMAP_LEVELS) * Sprite.MAX_EDGE_SMOOTHNESS; internal const int WHITE_PIXEL_SIZE = 1; private readonly List subTextureBounds = new List(); internal TextureGLSingle AtlasTexture; private readonly int atlasWidth; private readonly int atlasHeight; private int maxFittableWidth => atlasWidth - PADDING * 2; private int maxFittableHeight => atlasHeight - PADDING * 2; private Vector2I currentPosition; internal TextureWhitePixel WhitePixel { get { if (AtlasTexture == null) Reset(); return new TextureWhitePixel(new TextureGLAtlasWhite(AtlasTexture)); } } private readonly bool manualMipmaps; private readonly All filteringMode; private readonly object textureRetrievalLock = new object(); public TextureAtlas(int width, int height, bool manualMipmaps = false, All filteringMode = All.Linear) { atlasWidth = width; atlasHeight = height; this.manualMipmaps = manualMipmaps; this.filteringMode = filteringMode; } private int exceedCount; public void Reset() { subTextureBounds.Clear(); currentPosition = Vector2I.Zero; // 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 TextureGLAtlas(atlasWidth, atlasHeight, manualMipmaps, filteringMode, PADDING / 2); RectangleI bounds = new RectangleI(0, 0, WHITE_PIXEL_SIZE, WHITE_PIXEL_SIZE); subTextureBounds.Add(bounds); using (var whiteTex = new TextureGLSub(bounds, AtlasTexture, WrapMode.Repeat, WrapMode.Repeat)) // Generate white padding as if WhitePixel 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); } /// /// Add (allocate) a new texture in the atlas. /// /// The width of the requested texture. /// The height of the requested texture. /// The horizontal wrap mode of the texture. /// The vertical wrap mode of the texture. /// A texture, or null if the requested size exceeds the atlas' bounds. internal TextureGL Add(int width, int height, WrapMode wrapModeS = WrapMode.None, WrapMode wrapModeT = WrapMode.None) { if (!canFitEmptyTextureAtlas(width, height)) return null; lock (textureRetrievalLock) { Vector2I position = findPosition(width, height); RectangleI bounds = new RectangleI(position.X, position.Y, width, height); subTextureBounds.Add(bounds); return new TextureGLSub(bounds, AtlasTexture, wrapModeS, wrapModeT); } } /// /// Whether or not a texture of the given width and height could be placed into a completely empty texture atlas /// /// The width of the texture. /// The height of the texture. /// True if the texture could fit an empty texture atlas, false if it could not private bool canFitEmptyTextureAtlas(int width, int height) { // exceeds bounds in one direction if (width > maxFittableWidth || height > maxFittableHeight) return false; // exceeds bounds in both directions (in this one, we have to account for the white pixel) if (width + WHITE_PIXEL_SIZE > maxFittableWidth && height + WHITE_PIXEL_SIZE > maxFittableHeight) return false; return true; } /// /// Locates a position in the current texture atlas for a new texture of the given size, or /// creates a new texture atlas if there is not enough space in the current one. /// /// The width of the requested texture. /// The height of the requested texture. /// The position within the texture atlas to place the new texture. private Vector2I findPosition(int width, int height) { if (AtlasTexture == null) { Logger.Log($"TextureAtlas initialised ({atlasWidth}x{atlasHeight})", LoggingTarget.Performance); 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); } var result = currentPosition; currentPosition.X += width + PADDING; return result; } } }