- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 158
- 159
- 160
- 161
- 162
- 163
- 164
- 165
- 166
- 167
- 168
- 169
- 170
- 171
- 172
- 173
- 174
- 175
- 176
- 177
- 178
- 179
- 180
- 181
- 182
- 183
- 184
- 185
- 186
- 187
- 188
- 189
- 190
- 191
- 192
- 193
- 194
- 195
- 196
- 197
- 198
- 199
- 200
- 201
- 202
- 203
- 204
- 205
- 206
- 207
- 208
- 209
- 210
- 211
- 212
- 213
- 214
- 215
- 216
- 217
import { BaseTexture, Texture } from '@pixi/core';
import { GuilloteneAllocator } from '@pixi-essentials/area-allocator';
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<T extends Texture = Texture>
{
/**
* 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;
}
}