Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/browser/gpu/atlas/textureAtlas.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 { CharCode } from '../../../../base/common/charCode.js';
8
import { BugIndicatingError } from '../../../../base/common/errors.js';
9
import { Emitter, Event } from '../../../../base/common/event.js';
10
import { Disposable, dispose, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
11
import { NKeyMap } from '../../../../base/common/map.js';
12
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
13
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
14
import { MetadataConsts } from '../../../common/encodedTokenAttributes.js';
15
import type { DecorationStyleCache } from '../css/decorationStyleCache.js';
16
import { GlyphRasterizer } from '../raster/glyphRasterizer.js';
17
import type { IGlyphRasterizer } from '../raster/raster.js';
18
import { IdleTaskQueue, type ITaskQueue } from '../taskQueue.js';
19
import type { IReadableTextureAtlasPage, ITextureAtlasPageGlyph, GlyphMap } from './atlas.js';
20
import { AllocatorType, TextureAtlasPage } from './textureAtlasPage.js';
21
22
export interface ITextureAtlasOptions {
23
allocatorType?: AllocatorType;
24
}
25
26
export class TextureAtlas extends Disposable {
27
private _colorMap?: string[];
28
private readonly _warmUpTask: MutableDisposable<ITaskQueue> = this._register(new MutableDisposable());
29
private readonly _warmedUpRasterizers = new Set<number>();
30
private readonly _allocatorType: AllocatorType;
31
32
/**
33
* The maximum number of texture atlas pages. This is currently a hard static cap that must not
34
* be reached.
35
*/
36
static readonly maximumPageCount = 16;
37
38
/**
39
* The main texture atlas pages which are both larger textures and more efficiently packed
40
* relative to the scratch page. The idea is the main pages are drawn to and uploaded to the GPU
41
* much less frequently so as to not drop frames.
42
*/
43
private readonly _pages: TextureAtlasPage[] = [];
44
get pages(): IReadableTextureAtlasPage[] { return this._pages; }
45
46
readonly pageSize: number;
47
48
/**
49
* A maps of glyph keys to the page to start searching for the glyph. This is set before
50
* searching to have as little runtime overhead (branching, intermediate variables) as possible,
51
* so it is not guaranteed to be the actual page the glyph is on. But it is guaranteed that all
52
* pages with a lower index do not contain the glyph.
53
*/
54
private readonly _glyphPageIndex: GlyphMap<number> = new NKeyMap();
55
56
private readonly _onDidDeleteGlyphs = this._register(new Emitter<void>());
57
readonly onDidDeleteGlyphs = this._onDidDeleteGlyphs.event;
58
59
constructor(
60
/** The maximum texture size supported by the GPU. */
61
private readonly _maxTextureSize: number,
62
options: ITextureAtlasOptions | undefined,
63
private readonly _decorationStyleCache: DecorationStyleCache,
64
@IThemeService private readonly _themeService: IThemeService,
65
@IInstantiationService private readonly _instantiationService: IInstantiationService
66
) {
67
super();
68
69
this._allocatorType = options?.allocatorType ?? 'slab';
70
71
this._register(Event.runAndSubscribe(this._themeService.onDidColorThemeChange, () => {
72
if (this._colorMap) {
73
this.clear();
74
}
75
this._colorMap = this._themeService.getColorTheme().tokenColorMap;
76
}));
77
78
const dprFactor = Math.max(1, Math.floor(getActiveWindow().devicePixelRatio));
79
80
this.pageSize = Math.min(1024 * dprFactor, this._maxTextureSize);
81
this._initFirstPage();
82
83
this._register(toDisposable(() => dispose(this._pages)));
84
}
85
86
private _initFirstPage() {
87
const firstPage = this._instantiationService.createInstance(TextureAtlasPage, 0, this.pageSize, this._allocatorType);
88
this._pages.push(firstPage);
89
90
// IMPORTANT: The first glyph on the first page must be an empty glyph such that zeroed out
91
// cells end up rendering nothing
92
// TODO: This currently means the first slab is for 0x0 glyphs and is wasted
93
const nullRasterizer = new GlyphRasterizer(1, '', 1, this._decorationStyleCache);
94
firstPage.getGlyph(nullRasterizer, '', 0, 0);
95
nullRasterizer.dispose();
96
}
97
98
clear() {
99
// Clear all pages
100
for (const page of this._pages) {
101
page.dispose();
102
}
103
this._pages.length = 0;
104
this._glyphPageIndex.clear();
105
this._warmedUpRasterizers.clear();
106
this._warmUpTask.clear();
107
108
// Recreate first
109
this._initFirstPage();
110
111
// Tell listeners
112
this._onDidDeleteGlyphs.fire();
113
}
114
115
getGlyph(rasterizer: IGlyphRasterizer, chars: string, tokenMetadata: number, decorationStyleSetId: number, x: number): Readonly<ITextureAtlasPageGlyph> {
116
// TODO: Encode font size and family into key
117
// Ignore metadata that doesn't affect the glyph
118
tokenMetadata &= ~(MetadataConsts.LANGUAGEID_MASK | MetadataConsts.TOKEN_TYPE_MASK | MetadataConsts.BALANCED_BRACKETS_MASK);
119
120
// Add x offset for sub-pixel rendering to the unused portion or tokenMetadata. This
121
// converts the decimal part of the x to a range from 0 to 9, where 0 = 0.0px x offset,
122
// 9 = 0.9px x offset
123
tokenMetadata |= Math.floor((x % 1) * 10);
124
125
// Warm up common glyphs
126
if (!this._warmedUpRasterizers.has(rasterizer.id)) {
127
this._warmUpAtlas(rasterizer);
128
this._warmedUpRasterizers.add(rasterizer.id);
129
}
130
131
// Try get the glyph, overflowing to a new page if necessary
132
return this._tryGetGlyph(this._glyphPageIndex.get(chars, tokenMetadata, decorationStyleSetId, rasterizer.cacheKey) ?? 0, rasterizer, chars, tokenMetadata, decorationStyleSetId);
133
}
134
135
private _tryGetGlyph(pageIndex: number, rasterizer: IGlyphRasterizer, chars: string, tokenMetadata: number, decorationStyleSetId: number): Readonly<ITextureAtlasPageGlyph> {
136
this._glyphPageIndex.set(pageIndex, chars, tokenMetadata, decorationStyleSetId, rasterizer.cacheKey);
137
return (
138
this._pages[pageIndex].getGlyph(rasterizer, chars, tokenMetadata, decorationStyleSetId)
139
?? (pageIndex + 1 < this._pages.length
140
? this._tryGetGlyph(pageIndex + 1, rasterizer, chars, tokenMetadata, decorationStyleSetId)
141
: undefined)
142
?? this._getGlyphFromNewPage(rasterizer, chars, tokenMetadata, decorationStyleSetId)
143
);
144
}
145
146
private _getGlyphFromNewPage(rasterizer: IGlyphRasterizer, chars: string, tokenMetadata: number, decorationStyleSetId: number): Readonly<ITextureAtlasPageGlyph> {
147
if (this._pages.length >= TextureAtlas.maximumPageCount) {
148
throw new Error(`Attempt to create a texture atlas page past the limit ${TextureAtlas.maximumPageCount}`);
149
}
150
this._pages.push(this._instantiationService.createInstance(TextureAtlasPage, this._pages.length, this.pageSize, this._allocatorType));
151
this._glyphPageIndex.set(this._pages.length - 1, chars, tokenMetadata, decorationStyleSetId, rasterizer.cacheKey);
152
return this._pages[this._pages.length - 1].getGlyph(rasterizer, chars, tokenMetadata, decorationStyleSetId)!;
153
}
154
155
public getUsagePreview(): Promise<Blob[]> {
156
return Promise.all(this._pages.map(e => e.getUsagePreview()));
157
}
158
159
public getStats(): string[] {
160
return this._pages.map(e => e.getStats());
161
}
162
163
/**
164
* Warms up the atlas by rasterizing all printable ASCII characters for each token color. This
165
* is distrubuted over multiple idle callbacks to avoid blocking the main thread.
166
*/
167
private _warmUpAtlas(rasterizer: IGlyphRasterizer): void {
168
const colorMap = this._colorMap;
169
if (!colorMap) {
170
throw new BugIndicatingError('Cannot warm atlas without color map');
171
}
172
this._warmUpTask.value?.clear();
173
const taskQueue = this._warmUpTask.value = this._instantiationService.createInstance(IdleTaskQueue);
174
// Warm up using roughly the larger glyphs first to help optimize atlas allocation
175
// A-Z
176
for (let code = CharCode.A; code <= CharCode.Z; code++) {
177
for (const fgColor of colorMap.keys()) {
178
taskQueue.enqueue(() => {
179
for (let x = 0; x < 1; x += 0.1) {
180
this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << MetadataConsts.FOREGROUND_OFFSET) & MetadataConsts.FOREGROUND_MASK, 0, x);
181
}
182
});
183
}
184
}
185
// a-z
186
for (let code = CharCode.a; code <= CharCode.z; code++) {
187
for (const fgColor of colorMap.keys()) {
188
taskQueue.enqueue(() => {
189
for (let x = 0; x < 1; x += 0.1) {
190
this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << MetadataConsts.FOREGROUND_OFFSET) & MetadataConsts.FOREGROUND_MASK, 0, x);
191
}
192
});
193
}
194
}
195
// Remaining ascii
196
for (let code = CharCode.ExclamationMark; code <= CharCode.Tilde; code++) {
197
for (const fgColor of colorMap.keys()) {
198
taskQueue.enqueue(() => {
199
for (let x = 0; x < 1; x += 0.1) {
200
this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << MetadataConsts.FOREGROUND_OFFSET) & MetadataConsts.FOREGROUND_MASK, 0, x);
201
}
202
});
203
}
204
}
205
}
206
}
207
208
209