Path: blob/main/src/vs/editor/browser/gpu/atlas/textureAtlasShelfAllocator.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 { BugIndicatingError } from '../../../../base/common/errors.js';6import { ensureNonNullable } from '../gpuUtils.js';7import type { IRasterizedGlyph } from '../raster/raster.js';8import { UsagePreviewColors, type ITextureAtlasAllocator, type ITextureAtlasPageGlyph } from './atlas.js';910/**11* The shelf allocator is a simple allocator that places glyphs in rows, starting a new row when the12* current row is full. Due to its simplicity, it can waste space but it is very fast.13*/14export class TextureAtlasShelfAllocator implements ITextureAtlasAllocator {1516private readonly _ctx: OffscreenCanvasRenderingContext2D;1718private _currentRow: ITextureAtlasShelf = {19x: 0,20y: 0,21h: 022};2324/** A set of all glyphs allocated, this is only tracked to enable debug related functionality */25private readonly _allocatedGlyphs: Set<Readonly<ITextureAtlasPageGlyph>> = new Set();2627private _nextIndex = 0;2829constructor(30private readonly _canvas: OffscreenCanvas,31private readonly _textureIndex: number,32) {33this._ctx = ensureNonNullable(this._canvas.getContext('2d', {34willReadFrequently: true35}));36}3738public allocate(rasterizedGlyph: IRasterizedGlyph): ITextureAtlasPageGlyph | undefined {39// The glyph does not fit into the atlas page40const glyphWidth = rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left + 1;41const glyphHeight = rasterizedGlyph.boundingBox.bottom - rasterizedGlyph.boundingBox.top + 1;42if (glyphWidth > this._canvas.width || glyphHeight > this._canvas.height) {43throw new BugIndicatingError('Glyph is too large for the atlas page');44}4546// Finalize and increment row if it doesn't fix horizontally47if (rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left + 1 > this._canvas.width - this._currentRow.x) {48this._currentRow.x = 0;49this._currentRow.y += this._currentRow.h;50this._currentRow.h = 1;51}5253// Return undefined if there isn't any room left54if (this._currentRow.y + rasterizedGlyph.boundingBox.bottom - rasterizedGlyph.boundingBox.top + 1 > this._canvas.height) {55return undefined;56}5758// Draw glyph59this._ctx.drawImage(60rasterizedGlyph.source,61// source62rasterizedGlyph.boundingBox.left,63rasterizedGlyph.boundingBox.top,64glyphWidth,65glyphHeight,66// destination67this._currentRow.x,68this._currentRow.y,69glyphWidth,70glyphHeight71);7273// Create glyph object74const glyph: ITextureAtlasPageGlyph = {75pageIndex: this._textureIndex,76glyphIndex: this._nextIndex++,77x: this._currentRow.x,78y: this._currentRow.y,79w: glyphWidth,80h: glyphHeight,81originOffsetX: rasterizedGlyph.originOffset.x,82originOffsetY: rasterizedGlyph.originOffset.y,83fontBoundingBoxAscent: rasterizedGlyph.fontBoundingBoxAscent,84fontBoundingBoxDescent: rasterizedGlyph.fontBoundingBoxDescent,85};8687// Shift current row88this._currentRow.x += glyphWidth;89this._currentRow.h = Math.max(this._currentRow.h, glyphHeight);9091// Set the glyph92this._allocatedGlyphs.add(glyph);9394return glyph;95}9697public getUsagePreview(): Promise<Blob> {98const w = this._canvas.width;99const h = this._canvas.height;100const canvas = new OffscreenCanvas(w, h);101const ctx = ensureNonNullable(canvas.getContext('2d'));102ctx.fillStyle = UsagePreviewColors.Unused;103ctx.fillRect(0, 0, w, h);104105const rowHeight: Map<number, number> = new Map(); // y -> h106const rowWidth: Map<number, number> = new Map(); // y -> w107for (const g of this._allocatedGlyphs) {108rowHeight.set(g.y, Math.max(rowHeight.get(g.y) ?? 0, g.h));109rowWidth.set(g.y, Math.max(rowWidth.get(g.y) ?? 0, g.x + g.w));110}111for (const g of this._allocatedGlyphs) {112ctx.fillStyle = UsagePreviewColors.Used;113ctx.fillRect(g.x, g.y, g.w, g.h);114ctx.fillStyle = UsagePreviewColors.Wasted;115ctx.fillRect(g.x, g.y + g.h, g.w, rowHeight.get(g.y)! - g.h);116}117for (const [rowY, rowW] of rowWidth.entries()) {118if (rowY !== this._currentRow.y) {119ctx.fillStyle = UsagePreviewColors.Wasted;120ctx.fillRect(rowW, rowY, w - rowW, rowHeight.get(rowY)!);121}122}123return canvas.convertToBlob();124}125126getStats(): string {127const w = this._canvas.width;128const h = this._canvas.height;129130let usedPixels = 0;131let wastedPixels = 0;132const totalPixels = w * h;133134const rowHeight: Map<number, number> = new Map(); // y -> h135const rowWidth: Map<number, number> = new Map(); // y -> w136for (const g of this._allocatedGlyphs) {137rowHeight.set(g.y, Math.max(rowHeight.get(g.y) ?? 0, g.h));138rowWidth.set(g.y, Math.max(rowWidth.get(g.y) ?? 0, g.x + g.w));139}140for (const g of this._allocatedGlyphs) {141usedPixels += g.w * g.h;142wastedPixels += g.w * (rowHeight.get(g.y)! - g.h);143}144for (const [rowY, rowW] of rowWidth.entries()) {145if (rowY !== this._currentRow.y) {146wastedPixels += (w - rowW) * rowHeight.get(rowY)!;147}148}149return [150`page${this._textureIndex}:`,151` Total: ${totalPixels} (${w}x${h})`,152` Used: ${usedPixels} (${((usedPixels / totalPixels) * 100).toPrecision(2)}%)`,153` Wasted: ${wastedPixels} (${((wastedPixels / totalPixels) * 100).toPrecision(2)}%)`,154`Efficiency: ${((usedPixels / (usedPixels + wastedPixels)) * 100).toPrecision(2)}%`,155].join('\n');156}157}158159interface ITextureAtlasShelf {160x: number;161y: number;162h: number;163}164165166