Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/browser/gpu/atlas/textureAtlasShelfAllocator.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 { BugIndicatingError } from '../../../../base/common/errors.js';
7
import { ensureNonNullable } from '../gpuUtils.js';
8
import type { IRasterizedGlyph } from '../raster/raster.js';
9
import { UsagePreviewColors, type ITextureAtlasAllocator, type ITextureAtlasPageGlyph } from './atlas.js';
10
11
/**
12
* The shelf allocator is a simple allocator that places glyphs in rows, starting a new row when the
13
* current row is full. Due to its simplicity, it can waste space but it is very fast.
14
*/
15
export class TextureAtlasShelfAllocator implements ITextureAtlasAllocator {
16
17
private readonly _ctx: OffscreenCanvasRenderingContext2D;
18
19
private _currentRow: ITextureAtlasShelf = {
20
x: 0,
21
y: 0,
22
h: 0
23
};
24
25
/** A set of all glyphs allocated, this is only tracked to enable debug related functionality */
26
private readonly _allocatedGlyphs: Set<Readonly<ITextureAtlasPageGlyph>> = new Set();
27
28
private _nextIndex = 0;
29
30
constructor(
31
private readonly _canvas: OffscreenCanvas,
32
private readonly _textureIndex: number,
33
) {
34
this._ctx = ensureNonNullable(this._canvas.getContext('2d', {
35
willReadFrequently: true
36
}));
37
}
38
39
public allocate(rasterizedGlyph: IRasterizedGlyph): ITextureAtlasPageGlyph | undefined {
40
// The glyph does not fit into the atlas page
41
const glyphWidth = rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left + 1;
42
const glyphHeight = rasterizedGlyph.boundingBox.bottom - rasterizedGlyph.boundingBox.top + 1;
43
if (glyphWidth > this._canvas.width || glyphHeight > this._canvas.height) {
44
throw new BugIndicatingError('Glyph is too large for the atlas page');
45
}
46
47
// Finalize and increment row if it doesn't fix horizontally
48
if (rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left + 1 > this._canvas.width - this._currentRow.x) {
49
this._currentRow.x = 0;
50
this._currentRow.y += this._currentRow.h;
51
this._currentRow.h = 1;
52
}
53
54
// Return undefined if there isn't any room left
55
if (this._currentRow.y + rasterizedGlyph.boundingBox.bottom - rasterizedGlyph.boundingBox.top + 1 > this._canvas.height) {
56
return undefined;
57
}
58
59
// Draw glyph
60
this._ctx.drawImage(
61
rasterizedGlyph.source,
62
// source
63
rasterizedGlyph.boundingBox.left,
64
rasterizedGlyph.boundingBox.top,
65
glyphWidth,
66
glyphHeight,
67
// destination
68
this._currentRow.x,
69
this._currentRow.y,
70
glyphWidth,
71
glyphHeight
72
);
73
74
// Create glyph object
75
const glyph: ITextureAtlasPageGlyph = {
76
pageIndex: this._textureIndex,
77
glyphIndex: this._nextIndex++,
78
x: this._currentRow.x,
79
y: this._currentRow.y,
80
w: glyphWidth,
81
h: glyphHeight,
82
originOffsetX: rasterizedGlyph.originOffset.x,
83
originOffsetY: rasterizedGlyph.originOffset.y,
84
fontBoundingBoxAscent: rasterizedGlyph.fontBoundingBoxAscent,
85
fontBoundingBoxDescent: rasterizedGlyph.fontBoundingBoxDescent,
86
};
87
88
// Shift current row
89
this._currentRow.x += glyphWidth;
90
this._currentRow.h = Math.max(this._currentRow.h, glyphHeight);
91
92
// Set the glyph
93
this._allocatedGlyphs.add(glyph);
94
95
return glyph;
96
}
97
98
public getUsagePreview(): Promise<Blob> {
99
const w = this._canvas.width;
100
const h = this._canvas.height;
101
const canvas = new OffscreenCanvas(w, h);
102
const ctx = ensureNonNullable(canvas.getContext('2d'));
103
ctx.fillStyle = UsagePreviewColors.Unused;
104
ctx.fillRect(0, 0, w, h);
105
106
const rowHeight: Map<number, number> = new Map(); // y -> h
107
const rowWidth: Map<number, number> = new Map(); // y -> w
108
for (const g of this._allocatedGlyphs) {
109
rowHeight.set(g.y, Math.max(rowHeight.get(g.y) ?? 0, g.h));
110
rowWidth.set(g.y, Math.max(rowWidth.get(g.y) ?? 0, g.x + g.w));
111
}
112
for (const g of this._allocatedGlyphs) {
113
ctx.fillStyle = UsagePreviewColors.Used;
114
ctx.fillRect(g.x, g.y, g.w, g.h);
115
ctx.fillStyle = UsagePreviewColors.Wasted;
116
ctx.fillRect(g.x, g.y + g.h, g.w, rowHeight.get(g.y)! - g.h);
117
}
118
for (const [rowY, rowW] of rowWidth.entries()) {
119
if (rowY !== this._currentRow.y) {
120
ctx.fillStyle = UsagePreviewColors.Wasted;
121
ctx.fillRect(rowW, rowY, w - rowW, rowHeight.get(rowY)!);
122
}
123
}
124
return canvas.convertToBlob();
125
}
126
127
getStats(): string {
128
const w = this._canvas.width;
129
const h = this._canvas.height;
130
131
let usedPixels = 0;
132
let wastedPixels = 0;
133
const totalPixels = w * h;
134
135
const rowHeight: Map<number, number> = new Map(); // y -> h
136
const rowWidth: Map<number, number> = new Map(); // y -> w
137
for (const g of this._allocatedGlyphs) {
138
rowHeight.set(g.y, Math.max(rowHeight.get(g.y) ?? 0, g.h));
139
rowWidth.set(g.y, Math.max(rowWidth.get(g.y) ?? 0, g.x + g.w));
140
}
141
for (const g of this._allocatedGlyphs) {
142
usedPixels += g.w * g.h;
143
wastedPixels += g.w * (rowHeight.get(g.y)! - g.h);
144
}
145
for (const [rowY, rowW] of rowWidth.entries()) {
146
if (rowY !== this._currentRow.y) {
147
wastedPixels += (w - rowW) * rowHeight.get(rowY)!;
148
}
149
}
150
return [
151
`page${this._textureIndex}:`,
152
` Total: ${totalPixels} (${w}x${h})`,
153
` Used: ${usedPixels} (${((usedPixels / totalPixels) * 100).toPrecision(2)}%)`,
154
` Wasted: ${wastedPixels} (${((wastedPixels / totalPixels) * 100).toPrecision(2)}%)`,
155
`Efficiency: ${((usedPixels / (usedPixels + wastedPixels)) * 100).toPrecision(2)}%`,
156
].join('\n');
157
}
158
}
159
160
interface ITextureAtlasShelf {
161
x: number;
162
y: number;
163
h: number;
164
}
165
166