Path: blob/main/src/vs/editor/browser/gpu/atlas/textureAtlas.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { getActiveWindow } from '../../../../base/browser/dom.js';6import { CharCode } from '../../../../base/common/charCode.js';7import { BugIndicatingError } from '../../../../base/common/errors.js';8import { Emitter, Event } from '../../../../base/common/event.js';9import { Disposable, dispose, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js';10import { NKeyMap } from '../../../../base/common/map.js';11import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';12import { IThemeService } from '../../../../platform/theme/common/themeService.js';13import { MetadataConsts } from '../../../common/encodedTokenAttributes.js';14import type { DecorationStyleCache } from '../css/decorationStyleCache.js';15import { GlyphRasterizer } from '../raster/glyphRasterizer.js';16import type { IGlyphRasterizer } from '../raster/raster.js';17import { IdleTaskQueue, type ITaskQueue } from '../taskQueue.js';18import type { IReadableTextureAtlasPage, ITextureAtlasPageGlyph, GlyphMap } from './atlas.js';19import { AllocatorType, TextureAtlasPage } from './textureAtlasPage.js';2021export interface ITextureAtlasOptions {22allocatorType?: AllocatorType;23}2425export class TextureAtlas extends Disposable {26private _colorMap?: string[];27private readonly _warmUpTask: MutableDisposable<ITaskQueue> = this._register(new MutableDisposable());28private readonly _warmedUpRasterizers = new Set<number>();29private readonly _allocatorType: AllocatorType;3031/**32* The maximum number of texture atlas pages. This is currently a hard static cap that must not33* be reached.34*/35static readonly maximumPageCount = 16;3637/**38* The main texture atlas pages which are both larger textures and more efficiently packed39* relative to the scratch page. The idea is the main pages are drawn to and uploaded to the GPU40* much less frequently so as to not drop frames.41*/42private readonly _pages: TextureAtlasPage[] = [];43get pages(): IReadableTextureAtlasPage[] { return this._pages; }4445readonly pageSize: number;4647/**48* A maps of glyph keys to the page to start searching for the glyph. This is set before49* searching to have as little runtime overhead (branching, intermediate variables) as possible,50* so it is not guaranteed to be the actual page the glyph is on. But it is guaranteed that all51* pages with a lower index do not contain the glyph.52*/53private readonly _glyphPageIndex: GlyphMap<number> = new NKeyMap();5455private readonly _onDidDeleteGlyphs = this._register(new Emitter<void>());56readonly onDidDeleteGlyphs = this._onDidDeleteGlyphs.event;5758constructor(59/** The maximum texture size supported by the GPU. */60private readonly _maxTextureSize: number,61options: ITextureAtlasOptions | undefined,62private readonly _decorationStyleCache: DecorationStyleCache,63@IThemeService private readonly _themeService: IThemeService,64@IInstantiationService private readonly _instantiationService: IInstantiationService65) {66super();6768this._allocatorType = options?.allocatorType ?? 'slab';6970this._register(Event.runAndSubscribe(this._themeService.onDidColorThemeChange, () => {71if (this._colorMap) {72this.clear();73}74this._colorMap = this._themeService.getColorTheme().tokenColorMap;75}));7677const dprFactor = Math.max(1, Math.floor(getActiveWindow().devicePixelRatio));7879this.pageSize = Math.min(1024 * dprFactor, this._maxTextureSize);80this._initFirstPage();8182this._register(toDisposable(() => dispose(this._pages)));83}8485private _initFirstPage() {86const firstPage = this._instantiationService.createInstance(TextureAtlasPage, 0, this.pageSize, this._allocatorType);87this._pages.push(firstPage);8889// IMPORTANT: The first glyph on the first page must be an empty glyph such that zeroed out90// cells end up rendering nothing91// TODO: This currently means the first slab is for 0x0 glyphs and is wasted92const nullRasterizer = new GlyphRasterizer(1, '', 1, this._decorationStyleCache);93firstPage.getGlyph(nullRasterizer, '', 0, 0);94nullRasterizer.dispose();95}9697clear() {98// Clear all pages99for (const page of this._pages) {100page.dispose();101}102this._pages.length = 0;103this._glyphPageIndex.clear();104this._warmedUpRasterizers.clear();105this._warmUpTask.clear();106107// Recreate first108this._initFirstPage();109110// Tell listeners111this._onDidDeleteGlyphs.fire();112}113114getGlyph(rasterizer: IGlyphRasterizer, chars: string, tokenMetadata: number, decorationStyleSetId: number, x: number): Readonly<ITextureAtlasPageGlyph> {115// TODO: Encode font size and family into key116// Ignore metadata that doesn't affect the glyph117tokenMetadata &= ~(MetadataConsts.LANGUAGEID_MASK | MetadataConsts.TOKEN_TYPE_MASK | MetadataConsts.BALANCED_BRACKETS_MASK);118119// Add x offset for sub-pixel rendering to the unused portion or tokenMetadata. This120// converts the decimal part of the x to a range from 0 to 9, where 0 = 0.0px x offset,121// 9 = 0.9px x offset122tokenMetadata |= Math.floor((x % 1) * 10);123124// Warm up common glyphs125if (!this._warmedUpRasterizers.has(rasterizer.id)) {126this._warmUpAtlas(rasterizer);127this._warmedUpRasterizers.add(rasterizer.id);128}129130// Try get the glyph, overflowing to a new page if necessary131return this._tryGetGlyph(this._glyphPageIndex.get(chars, tokenMetadata, decorationStyleSetId, rasterizer.cacheKey) ?? 0, rasterizer, chars, tokenMetadata, decorationStyleSetId);132}133134private _tryGetGlyph(pageIndex: number, rasterizer: IGlyphRasterizer, chars: string, tokenMetadata: number, decorationStyleSetId: number): Readonly<ITextureAtlasPageGlyph> {135this._glyphPageIndex.set(pageIndex, chars, tokenMetadata, decorationStyleSetId, rasterizer.cacheKey);136return (137this._pages[pageIndex].getGlyph(rasterizer, chars, tokenMetadata, decorationStyleSetId)138?? (pageIndex + 1 < this._pages.length139? this._tryGetGlyph(pageIndex + 1, rasterizer, chars, tokenMetadata, decorationStyleSetId)140: undefined)141?? this._getGlyphFromNewPage(rasterizer, chars, tokenMetadata, decorationStyleSetId)142);143}144145private _getGlyphFromNewPage(rasterizer: IGlyphRasterizer, chars: string, tokenMetadata: number, decorationStyleSetId: number): Readonly<ITextureAtlasPageGlyph> {146if (this._pages.length >= TextureAtlas.maximumPageCount) {147throw new Error(`Attempt to create a texture atlas page past the limit ${TextureAtlas.maximumPageCount}`);148}149this._pages.push(this._instantiationService.createInstance(TextureAtlasPage, this._pages.length, this.pageSize, this._allocatorType));150this._glyphPageIndex.set(this._pages.length - 1, chars, tokenMetadata, decorationStyleSetId, rasterizer.cacheKey);151return this._pages[this._pages.length - 1].getGlyph(rasterizer, chars, tokenMetadata, decorationStyleSetId)!;152}153154public getUsagePreview(): Promise<Blob[]> {155return Promise.all(this._pages.map(e => e.getUsagePreview()));156}157158public getStats(): string[] {159return this._pages.map(e => e.getStats());160}161162/**163* Warms up the atlas by rasterizing all printable ASCII characters for each token color. This164* is distrubuted over multiple idle callbacks to avoid blocking the main thread.165*/166private _warmUpAtlas(rasterizer: IGlyphRasterizer): void {167const colorMap = this._colorMap;168if (!colorMap) {169throw new BugIndicatingError('Cannot warm atlas without color map');170}171this._warmUpTask.value?.clear();172const taskQueue = this._warmUpTask.value = this._instantiationService.createInstance(IdleTaskQueue);173// Warm up using roughly the larger glyphs first to help optimize atlas allocation174// A-Z175for (let code = CharCode.A; code <= CharCode.Z; code++) {176for (const fgColor of colorMap.keys()) {177taskQueue.enqueue(() => {178for (let x = 0; x < 1; x += 0.1) {179this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << MetadataConsts.FOREGROUND_OFFSET) & MetadataConsts.FOREGROUND_MASK, 0, x);180}181});182}183}184// a-z185for (let code = CharCode.a; code <= CharCode.z; code++) {186for (const fgColor of colorMap.keys()) {187taskQueue.enqueue(() => {188for (let x = 0; x < 1; x += 0.1) {189this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << MetadataConsts.FOREGROUND_OFFSET) & MetadataConsts.FOREGROUND_MASK, 0, x);190}191});192}193}194// Remaining ascii195for (let code = CharCode.ExclamationMark; code <= CharCode.Tilde; code++) {196for (const fgColor of colorMap.keys()) {197taskQueue.enqueue(() => {198for (let x = 0; x < 1; x += 0.1) {199this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << MetadataConsts.FOREGROUND_OFFSET) & MetadataConsts.FOREGROUND_MASK, 0, x);200}201});202}203}204}205}206207208209