Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/browser/gpu/atlas/textureAtlasSlabAllocator.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { getActiveWindow } from '../../../../base/browser/dom.js';
7
import { BugIndicatingError } from '../../../../base/common/errors.js';
8
import { NKeyMap } from '../../../../base/common/map.js';
9
import { ensureNonNullable } from '../gpuUtils.js';
10
import type { IRasterizedGlyph } from '../raster/raster.js';
11
import { UsagePreviewColors, type ITextureAtlasAllocator, type ITextureAtlasPageGlyph } from './atlas.js';
12
13
export interface TextureAtlasSlabAllocatorOptions {
14
slabW?: number;
15
slabH?: number;
16
}
17
18
/**
19
* The slab allocator is a more complex allocator that places glyphs in square slabs of a fixed
20
* size. Slabs are defined by a small range of glyphs sizes they can house, this places like-sized
21
* glyphs in the same slab which reduces wasted space.
22
*
23
* Slabs also may contain "unused" regions on the left and bottom depending on the size of the
24
* glyphs they include. This space is used to place very thin or short glyphs, which would otherwise
25
* waste a lot of space in their own slab.
26
*/
27
export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator {
28
29
private readonly _ctx: OffscreenCanvasRenderingContext2D;
30
31
private readonly _slabs: ITextureAtlasSlab[] = [];
32
private readonly _activeSlabsByDims: NKeyMap<ITextureAtlasSlab, [number, number]> = new NKeyMap();
33
34
private readonly _unusedRects: ITextureAtlasSlabUnusedRect[] = [];
35
36
private readonly _openRegionsByHeight: Map<number, ITextureAtlasSlabUnusedRect[]> = new Map();
37
private readonly _openRegionsByWidth: Map<number, ITextureAtlasSlabUnusedRect[]> = new Map();
38
39
/** A set of all glyphs allocated, this is only tracked to enable debug related functionality */
40
private readonly _allocatedGlyphs: Set<Readonly<ITextureAtlasPageGlyph>> = new Set();
41
42
private _slabW: number;
43
private _slabH: number;
44
private _slabsPerRow: number;
45
private _slabsPerColumn: number;
46
private _nextIndex = 0;
47
48
constructor(
49
private readonly _canvas: OffscreenCanvas,
50
private readonly _textureIndex: number,
51
options?: TextureAtlasSlabAllocatorOptions
52
) {
53
this._ctx = ensureNonNullable(this._canvas.getContext('2d', {
54
willReadFrequently: true
55
}));
56
57
this._slabW = Math.min(
58
options?.slabW ?? (64 << Math.max(Math.floor(getActiveWindow().devicePixelRatio) - 1, 0)),
59
this._canvas.width
60
);
61
this._slabH = Math.min(
62
options?.slabH ?? this._slabW,
63
this._canvas.height
64
);
65
this._slabsPerRow = Math.floor(this._canvas.width / this._slabW);
66
this._slabsPerColumn = Math.floor(this._canvas.height / this._slabH);
67
}
68
69
public allocate(rasterizedGlyph: IRasterizedGlyph): ITextureAtlasPageGlyph | undefined {
70
// Find ideal slab, creating it if there is none suitable
71
const glyphWidth = rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left + 1;
72
const glyphHeight = rasterizedGlyph.boundingBox.bottom - rasterizedGlyph.boundingBox.top + 1;
73
74
// The glyph does not fit into the atlas page, glyphs should never be this large in practice
75
if (glyphWidth > this._canvas.width || glyphHeight > this._canvas.height) {
76
throw new BugIndicatingError('Glyph is too large for the atlas page');
77
}
78
79
// The glyph does not fit into a slab
80
if (glyphWidth > this._slabW || glyphHeight > this._slabH) {
81
// Only if this is the allocator's first glyph, resize the slab size to fit the glyph.
82
if (this._allocatedGlyphs.size > 0) {
83
return undefined;
84
}
85
// Find the largest power of 2 devisor that the glyph fits into, this ensure there is no
86
// wasted space outside the allocated slabs.
87
let sizeCandidate = this._canvas.width;
88
while (glyphWidth < sizeCandidate / 2 && glyphHeight < sizeCandidate / 2) {
89
sizeCandidate /= 2;
90
}
91
this._slabW = sizeCandidate;
92
this._slabH = sizeCandidate;
93
this._slabsPerRow = Math.floor(this._canvas.width / this._slabW);
94
this._slabsPerColumn = Math.floor(this._canvas.height / this._slabH);
95
}
96
97
// const dpr = getActiveWindow().devicePixelRatio;
98
99
// TODO: Include font size as well as DPR in nearestXPixels calculation
100
101
// Round slab glyph dimensions to the nearest x pixels, where x scaled with device pixel ratio
102
// const nearestXPixels = Math.max(1, Math.floor(dpr / 0.5));
103
// const nearestXPixels = Math.max(1, Math.floor(dpr));
104
const desiredSlabSize = {
105
// Nearest square number
106
// TODO: This can probably be optimized
107
// w: 1 << Math.ceil(Math.sqrt(glyphWidth)),
108
// h: 1 << Math.ceil(Math.sqrt(glyphHeight)),
109
110
// Nearest x px
111
// w: Math.ceil(glyphWidth / nearestXPixels) * nearestXPixels,
112
// h: Math.ceil(glyphHeight / nearestXPixels) * nearestXPixels,
113
114
// Round odd numbers up
115
// w: glyphWidth % 0 === 1 ? glyphWidth + 1 : glyphWidth,
116
// h: glyphHeight % 0 === 1 ? glyphHeight + 1 : glyphHeight,
117
118
// Exact number only
119
w: glyphWidth,
120
h: glyphHeight,
121
};
122
123
// Get any existing slab
124
let slab = this._activeSlabsByDims.get(desiredSlabSize.w, desiredSlabSize.h);
125
126
// Check if the slab is full
127
if (slab) {
128
const glyphsPerSlab = Math.floor(this._slabW / slab.entryW) * Math.floor(this._slabH / slab.entryH);
129
if (slab.count >= glyphsPerSlab) {
130
slab = undefined;
131
}
132
}
133
134
let dx: number | undefined;
135
let dy: number | undefined;
136
137
// Search for suitable space in unused rectangles
138
if (!slab) {
139
// Only check availability for the smallest side
140
if (glyphWidth < glyphHeight) {
141
const openRegions = this._openRegionsByWidth.get(glyphWidth);
142
if (openRegions?.length) {
143
// TODO: Don't search everything?
144
// Search from the end so we can typically pop it off the stack
145
for (let i = openRegions.length - 1; i >= 0; i--) {
146
const r = openRegions[i];
147
if (r.w >= glyphWidth && r.h >= glyphHeight) {
148
dx = r.x;
149
dy = r.y;
150
if (glyphWidth < r.w) {
151
this._unusedRects.push({
152
x: r.x + glyphWidth,
153
y: r.y,
154
w: r.w - glyphWidth,
155
h: glyphHeight
156
});
157
}
158
r.y += glyphHeight;
159
r.h -= glyphHeight;
160
if (r.h === 0) {
161
if (i === openRegions.length - 1) {
162
openRegions.pop();
163
} else {
164
this._unusedRects.splice(i, 1);
165
}
166
}
167
break;
168
}
169
}
170
}
171
} else {
172
const openRegions = this._openRegionsByHeight.get(glyphHeight);
173
if (openRegions?.length) {
174
// TODO: Don't search everything?
175
// Search from the end so we can typically pop it off the stack
176
for (let i = openRegions.length - 1; i >= 0; i--) {
177
const r = openRegions[i];
178
if (r.w >= glyphWidth && r.h >= glyphHeight) {
179
dx = r.x;
180
dy = r.y;
181
if (glyphHeight < r.h) {
182
this._unusedRects.push({
183
x: r.x,
184
y: r.y + glyphHeight,
185
w: glyphWidth,
186
h: r.h - glyphHeight
187
});
188
}
189
r.x += glyphWidth;
190
r.w -= glyphWidth;
191
if (r.h === 0) {
192
if (i === openRegions.length - 1) {
193
openRegions.pop();
194
} else {
195
this._unusedRects.splice(i, 1);
196
}
197
}
198
break;
199
}
200
}
201
}
202
}
203
}
204
205
// Create a new slab
206
if (dx === undefined || dy === undefined) {
207
if (!slab) {
208
if (this._slabs.length >= this._slabsPerRow * this._slabsPerColumn) {
209
return undefined;
210
}
211
212
slab = {
213
x: Math.floor(this._slabs.length % this._slabsPerRow) * this._slabW,
214
y: Math.floor(this._slabs.length / this._slabsPerRow) * this._slabH,
215
entryW: desiredSlabSize.w,
216
entryH: desiredSlabSize.h,
217
count: 0
218
};
219
// Track unused regions to use for small glyphs
220
// +-------------+----+
221
// | | |
222
// | | | <- Unused W region
223
// | | |
224
// |-------------+----+
225
// | | <- Unused H region
226
// +------------------+
227
const unusedW = this._slabW % slab.entryW;
228
const unusedH = this._slabH % slab.entryH;
229
if (unusedW) {
230
addEntryToMapArray(this._openRegionsByWidth, unusedW, {
231
x: slab.x + this._slabW - unusedW,
232
w: unusedW,
233
y: slab.y,
234
h: this._slabH - (unusedH ?? 0)
235
});
236
}
237
if (unusedH) {
238
addEntryToMapArray(this._openRegionsByHeight, unusedH, {
239
x: slab.x,
240
w: this._slabW,
241
y: slab.y + this._slabH - unusedH,
242
h: unusedH
243
});
244
}
245
this._slabs.push(slab);
246
this._activeSlabsByDims.set(slab, desiredSlabSize.w, desiredSlabSize.h);
247
}
248
249
const glyphsPerRow = Math.floor(this._slabW / slab.entryW);
250
dx = slab.x + Math.floor(slab.count % glyphsPerRow) * slab.entryW;
251
dy = slab.y + Math.floor(slab.count / glyphsPerRow) * slab.entryH;
252
253
// Shift current row
254
slab.count++;
255
}
256
257
// Draw glyph
258
this._ctx.drawImage(
259
rasterizedGlyph.source,
260
// source
261
rasterizedGlyph.boundingBox.left,
262
rasterizedGlyph.boundingBox.top,
263
glyphWidth,
264
glyphHeight,
265
// destination
266
dx,
267
dy,
268
glyphWidth,
269
glyphHeight
270
);
271
272
// Create glyph object
273
const glyph: ITextureAtlasPageGlyph = {
274
pageIndex: this._textureIndex,
275
glyphIndex: this._nextIndex++,
276
x: dx,
277
y: dy,
278
w: glyphWidth,
279
h: glyphHeight,
280
originOffsetX: rasterizedGlyph.originOffset.x,
281
originOffsetY: rasterizedGlyph.originOffset.y,
282
fontBoundingBoxAscent: rasterizedGlyph.fontBoundingBoxAscent,
283
fontBoundingBoxDescent: rasterizedGlyph.fontBoundingBoxDescent,
284
};
285
286
// Set the glyph
287
this._allocatedGlyphs.add(glyph);
288
289
return glyph;
290
}
291
292
public getUsagePreview(): Promise<Blob> {
293
const w = this._canvas.width;
294
const h = this._canvas.height;
295
const canvas = new OffscreenCanvas(w, h);
296
const ctx = ensureNonNullable(canvas.getContext('2d'));
297
298
ctx.fillStyle = UsagePreviewColors.Unused;
299
ctx.fillRect(0, 0, w, h);
300
301
let slabEntryPixels = 0;
302
let usedPixels = 0;
303
let slabEdgePixels = 0;
304
let restrictedPixels = 0;
305
const slabW = 64 << (Math.floor(getActiveWindow().devicePixelRatio) - 1);
306
const slabH = slabW;
307
308
// Draw wasted underneath glyphs first
309
for (const slab of this._slabs) {
310
let x = 0;
311
let y = 0;
312
for (let i = 0; i < slab.count; i++) {
313
if (x + slab.entryW > slabW) {
314
x = 0;
315
y += slab.entryH;
316
}
317
ctx.fillStyle = UsagePreviewColors.Wasted;
318
ctx.fillRect(slab.x + x, slab.y + y, slab.entryW, slab.entryH);
319
320
slabEntryPixels += slab.entryW * slab.entryH;
321
x += slab.entryW;
322
}
323
const entriesPerRow = Math.floor(slabW / slab.entryW);
324
const entriesPerCol = Math.floor(slabH / slab.entryH);
325
const thisSlabPixels = slab.entryW * entriesPerRow * slab.entryH * entriesPerCol;
326
slabEdgePixels += (slabW * slabH) - thisSlabPixels;
327
}
328
329
// Draw glyphs
330
for (const g of this._allocatedGlyphs) {
331
usedPixels += g.w * g.h;
332
ctx.fillStyle = UsagePreviewColors.Used;
333
ctx.fillRect(g.x, g.y, g.w, g.h);
334
}
335
336
// Draw unused space on side
337
const unusedRegions = Array.from(this._openRegionsByWidth.values()).flat().concat(Array.from(this._openRegionsByHeight.values()).flat());
338
for (const r of unusedRegions) {
339
ctx.fillStyle = UsagePreviewColors.Restricted;
340
ctx.fillRect(r.x, r.y, r.w, r.h);
341
restrictedPixels += r.w * r.h;
342
}
343
344
345
// Overlay actual glyphs on top
346
ctx.globalAlpha = 0.5;
347
ctx.drawImage(this._canvas, 0, 0);
348
ctx.globalAlpha = 1;
349
350
return canvas.convertToBlob();
351
}
352
353
public getStats(): string {
354
const w = this._canvas.width;
355
const h = this._canvas.height;
356
357
let slabEntryPixels = 0;
358
let usedPixels = 0;
359
let slabEdgePixels = 0;
360
let wastedPixels = 0;
361
let restrictedPixels = 0;
362
const totalPixels = w * h;
363
const slabW = 64 << (Math.floor(getActiveWindow().devicePixelRatio) - 1);
364
const slabH = slabW;
365
366
// Draw wasted underneath glyphs first
367
for (const slab of this._slabs) {
368
let x = 0;
369
let y = 0;
370
for (let i = 0; i < slab.count; i++) {
371
if (x + slab.entryW > slabW) {
372
x = 0;
373
y += slab.entryH;
374
}
375
slabEntryPixels += slab.entryW * slab.entryH;
376
x += slab.entryW;
377
}
378
const entriesPerRow = Math.floor(slabW / slab.entryW);
379
const entriesPerCol = Math.floor(slabH / slab.entryH);
380
const thisSlabPixels = slab.entryW * entriesPerRow * slab.entryH * entriesPerCol;
381
slabEdgePixels += (slabW * slabH) - thisSlabPixels;
382
}
383
384
// Draw glyphs
385
for (const g of this._allocatedGlyphs) {
386
usedPixels += g.w * g.h;
387
}
388
389
// Draw unused space on side
390
const unusedRegions = Array.from(this._openRegionsByWidth.values()).flat().concat(Array.from(this._openRegionsByHeight.values()).flat());
391
for (const r of unusedRegions) {
392
restrictedPixels += r.w * r.h;
393
}
394
395
const edgeUsedPixels = slabEdgePixels - restrictedPixels;
396
wastedPixels = slabEntryPixels - (usedPixels - edgeUsedPixels);
397
398
// usedPixels += slabEdgePixels - restrictedPixels;
399
const efficiency = usedPixels / (usedPixels + wastedPixels + restrictedPixels);
400
401
return [
402
`page[${this._textureIndex}]:`,
403
` Total: ${totalPixels}px (${w}x${h})`,
404
` Used: ${usedPixels}px (${((usedPixels / totalPixels) * 100).toFixed(2)}%)`,
405
` Wasted: ${wastedPixels}px (${((wastedPixels / totalPixels) * 100).toFixed(2)}%)`,
406
`Restricted: ${restrictedPixels}px (${((restrictedPixels / totalPixels) * 100).toFixed(2)}%) (hard to allocate)`,
407
`Efficiency: ${efficiency === 1 ? '100' : (efficiency * 100).toFixed(2)}%`,
408
` Slabs: ${this._slabs.length} of ${Math.floor(this._canvas.width / slabW) * Math.floor(this._canvas.height / slabH)}`
409
].join('\n');
410
}
411
}
412
413
interface ITextureAtlasSlab {
414
x: number;
415
y: number;
416
entryH: number;
417
entryW: number;
418
count: number;
419
}
420
421
interface ITextureAtlasSlabUnusedRect {
422
x: number;
423
y: number;
424
w: number;
425
h: number;
426
}
427
428
function addEntryToMapArray<K, V>(map: Map<K, V[]>, key: K, entry: V) {
429
let list = map.get(key);
430
if (!list) {
431
list = [];
432
map.set(key, list);
433
}
434
list.push(entry);
435
}
436
437