import { BaseTexture, Texture } from '@pixi/core'; import { GuilloteneAllocator } from './GuilloteneAllocator'; import { Rectangle } from '@pixi/math'; import type { TextureSlab } from './TextureSlab'; const tempRect = new Rectangle(); /** * The texture allocator dynamically manages space on base-texture slabs. It can be used to generate * atlases on demand, which improve batching efficiency. * * @public */ export class TextureAllocator { /** * The width of texture slabs. */ public readonly slabWidth: number; /** * The height of texture slabs. */ public readonly slabHeight: number; /** * The list of base-textures that are used to allocate texture space. */ protected textureSlabs: TextureSlab[]; /** * @param slabWidth - The width of base-texture slabs. This should be at most 2048. * @param slabHeight - The height of base-texture slabs. This should be at most 2048. */ constructor(slabWidth = 2048, slabHeight = 2048) { this.slabWidth = slabWidth; this.slabHeight = slabHeight; this.textureSlabs = []; } get maxWidth() { return this.slabWidth - (2 * this.calculatePadding(this.slabWidth, this.slabHeight)); } get maxHeight() { return this.slabHeight - (2 * this.calculatePadding(this.slabWidth, this.slabHeight)); } /** * Allocates a texture from this allocator. * * If its existing slab pool has enough space, the texture is issued from one. Otherwise, * a new slab is created and the texture is issued from it. However, if the requested * dimensions are larger than slabs themselves, then `null` is always returned. * * To upload a texture source, you will have to create an atlas-managing {@link Resource} * yourself on the base-texture. The {@link AtlasAllocator} does this for you, while the * {@link CanvasTextureAllocator} can be used to draw on a canvas-based atlas. * * @param width - The width of the requested texture. * @param height - The height of the requested texture. * @param padding - The padding requested around the texture, to prevent bleeding. * @return The allocated texture, if successful; otherwise, `null`. */ allocate(width: number, height: number, padding = this.calculatePadding(width, height)): T { // Cannot allocate a texture larger than a texture-slab. if (width + 2 * padding > this.slabWidth || height + 2 * padding > this.slabHeight) { return null; } const slabs = this.textureSlabs; // Loop through the slabs and find one with enough space, if any. for (let i = 0, j = slabs.length; i < j; i++) { const slab = slabs[i]; const texture = this.issueTexture(slab, width, height, padding); if (texture) { return texture; } } // Issue a new slab. const slab = this.createSlab(); // Append this slab to the head of the list. this.textureSlabs.unshift(slab); // Issue the texture from this blank slab. return this.issueTexture(slab, width, height, padding); } /** * Frees the texture and reclaims its space. It is assumed you will not use it again, and have * destroyed any resource uploading its data. * * @param texture * @throws When the texture was not located in this allocator. */ free(texture: T): void { const baseTexture = (texture as Texture).baseTexture; const slab = this.textureSlabs.find(sl => sl.slab === baseTexture); if (!slab) { throw new Error("The texture cannot be freed because " + "its base-texture is not pooled by this allocator. " + "This is either a bug in TextureAllocator or you tried to free a " + "texture that was never allocated by one."); } const textureEntry = slab.managedTextures.find(entry => entry.texture === texture); if (!textureEntry) { throw new Error("The texture cannot be freed because it was not found " + "in the managed list of issued textures on its slab. This may be because you " + "duplicated this texture or a bug in TextureAllocator"); } slab.managedArea.free(textureEntry.area); slab.managedTextures.splice(slab.managedTextures.indexOf(textureEntry), 1); } protected calculatePadding(width: number, height: number): number { const dimen = Math.max(width, height); if (dimen < 64) { return 2; } else if (dimen < 128) { return 4; } else if (dimen < 1024) { return 8; } else { return 16; } } /** * Creates a texture slab. The slab's base-texture is not backed by any resource. You * will have to manage that yourself. See {@link AtlasAllocator} or {@link CanvasTextureAllocator} * for better resource semantics. */ protected createSlab(): TextureSlab { return { managedArea: new GuilloteneAllocator(this.slabWidth, this.slabHeight), managedTextures: [], slab: new BaseTexture(null, { width: this.slabWidth, height: this.slabHeight, }), }; } /** * Creates a texture on the given base-texture at {@code frame}. * * @param baseTexture - The base texture that will hold the texture's space. * @param frame - The frame in which the texture will be stored. */ protected createTexture(baseTexture: BaseTexture, frame: Rectangle): T { // Override this method to return correct texture type T. return new Texture(baseTexture, frame) as T; } /** * Issues a texture from the given texture slab, if possible. * * @param slab - The texture slab to allocate frame. * @param width - The width of the requested texture. * @param height - The height of the requested texture. * @param padding - Padding required around the texture. * @return The issued texture, if successful; otherwise, `null`. */ protected issueTexture(slab: TextureSlab, width: number, height: number, padding = 0): T { const area = slab.managedArea.allocate(width + 2 * padding, height + 2 * padding); if (!area) { return null; } tempRect.copyFrom(area); tempRect.pad(-padding); const baseTexture = slab.slab; const issuedTexture = this.createTexture(baseTexture, tempRect.clone()); slab.managedTextures.push({ area, texture: issuedTexture, }); return issuedTexture; } }