Path: blob/main/src/vs/editor/test/browser/gpu/atlas/textureAtlas.test.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 { strictEqual, throws } from 'assert';6import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';7import type { IGlyphRasterizer, IRasterizedGlyph } from '../../../../browser/gpu/raster/raster.js';8import { ensureNonNullable } from '../../../../browser/gpu/gpuUtils.js';9import type { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';10import { TextureAtlas } from '../../../../browser/gpu/atlas/textureAtlas.js';11import { createCodeEditorServices } from '../../testCodeEditor.js';12import { assertIsValidGlyph } from './testUtil.js';13import { TextureAtlasSlabAllocator } from '../../../../browser/gpu/atlas/textureAtlasSlabAllocator.js';14import { DecorationStyleCache } from '../../../../browser/gpu/css/decorationStyleCache.js';1516const blackInt = 0x000000FF;17const nullCharMetadata = 0x0;1819let lastUniqueGlyph: string | undefined;20function getUniqueGlyphId(): [chars: string, tokenMetadata: number, charMetadata: number, x: number] {21if (!lastUniqueGlyph) {22lastUniqueGlyph = 'a';23} else {24lastUniqueGlyph = String.fromCharCode(lastUniqueGlyph.charCodeAt(0) + 1);25}26return [lastUniqueGlyph, blackInt, nullCharMetadata, 0];27}2829class TestGlyphRasterizer implements IGlyphRasterizer {30readonly id = 0;31readonly cacheKey = '';32nextGlyphColor: [number, number, number, number] = [0, 0, 0, 0];33nextGlyphDimensions: [number, number] = [0, 0];34rasterizeGlyph(chars: string, tokenMetadata: number, charMetadata: number, colorMap: string[]): Readonly<IRasterizedGlyph> {35const w = this.nextGlyphDimensions[0];36const h = this.nextGlyphDimensions[1];37if (w === 0 || h === 0) {38throw new Error('TestGlyphRasterizer.nextGlyphDimensions must be set to a non-zero value before calling rasterizeGlyph');39}40const imageData = new ImageData(w, h);41let i = 0;42for (let y = 0; y < h; y++) {43for (let x = 0; x < w; x++) {44const [r, g, b, a] = this.nextGlyphColor;45i = (y * w + x) * 4;46imageData.data[i + 0] = r;47imageData.data[i + 1] = g;48imageData.data[i + 2] = b;49imageData.data[i + 3] = a;50}51}52const canvas = new OffscreenCanvas(w, h);53const ctx = ensureNonNullable(canvas.getContext('2d'));54ctx.putImageData(imageData, 0, 0);55return {56source: canvas,57boundingBox: { top: 0, left: 0, bottom: h - 1, right: w - 1 },58originOffset: { x: 0, y: 0 },59fontBoundingBoxAscent: 0,60fontBoundingBoxDescent: 0,61};62}63getTextMetrics(text: string): TextMetrics {64return null!;65}66}6768suite('TextureAtlas', () => {69const store = ensureNoDisposablesAreLeakedInTestSuite();7071suiteSetup(() => {72lastUniqueGlyph = undefined;73});7475let instantiationService: IInstantiationService;7677let atlas: TextureAtlas;78let glyphRasterizer: TestGlyphRasterizer;7980setup(() => {81instantiationService = createCodeEditorServices(store);82atlas = store.add(instantiationService.createInstance(TextureAtlas, 2, undefined, new DecorationStyleCache()));83glyphRasterizer = new TestGlyphRasterizer();84glyphRasterizer.nextGlyphDimensions = [1, 1];85glyphRasterizer.nextGlyphColor = [0, 0, 0, 0xFF];86});8788test('get single glyph', () => {89assertIsValidGlyph(atlas.getGlyph(glyphRasterizer, ...getUniqueGlyphId()), atlas);90});9192test('get multiple glyphs', () => {93atlas = store.add(instantiationService.createInstance(TextureAtlas, 32, undefined, new DecorationStyleCache()));94for (let i = 0; i < 10; i++) {95assertIsValidGlyph(atlas.getGlyph(glyphRasterizer, ...getUniqueGlyphId()), atlas);96}97});9899test('adding glyph to full page creates new page', () => {100let pageCount: number | undefined;101for (let i = 0; i < 4; i++) {102assertIsValidGlyph(atlas.getGlyph(glyphRasterizer, ...getUniqueGlyphId()), atlas);103if (pageCount === undefined) {104pageCount = atlas.pages.length;105} else {106strictEqual(atlas.pages.length, pageCount, 'the number of pages should not change when the page is being filled');107}108}109assertIsValidGlyph(atlas.getGlyph(glyphRasterizer, ...getUniqueGlyphId()), atlas);110strictEqual(atlas.pages.length, pageCount! + 1, 'the 5th glyph should overflow to a new page');111});112113test('adding a glyph larger than the atlas', () => {114glyphRasterizer.nextGlyphDimensions = [3, 2];115throws(() => atlas.getGlyph(glyphRasterizer, ...getUniqueGlyphId()), 'should throw when the glyph is too large, this should not happen in practice');116});117118test('adding a glyph larger than the standard slab size', () => {119glyphRasterizer.nextGlyphDimensions = [2, 2];120atlas = store.add(instantiationService.createInstance(TextureAtlas, 32, {121allocatorType: (canvas, textureIndex) => new TextureAtlasSlabAllocator(canvas, textureIndex, { slabW: 1, slabH: 1 })122}, new DecorationStyleCache()));123assertIsValidGlyph(atlas.getGlyph(glyphRasterizer, ...getUniqueGlyphId()), atlas);124});125126test('adding a non-first glyph larger than the standard slab size, causing an overflow to a new page', () => {127atlas = store.add(instantiationService.createInstance(TextureAtlas, 2, {128allocatorType: (canvas, textureIndex) => new TextureAtlasSlabAllocator(canvas, textureIndex, { slabW: 1, slabH: 1 })129}, new DecorationStyleCache()));130assertIsValidGlyph(atlas.getGlyph(glyphRasterizer, ...getUniqueGlyphId()), atlas);131strictEqual(atlas.pages.length, 1);132glyphRasterizer.nextGlyphDimensions = [2, 2];133assertIsValidGlyph(atlas.getGlyph(glyphRasterizer, ...getUniqueGlyphId()), atlas);134strictEqual(atlas.pages.length, 2, 'the 2nd glyph should overflow to a new page with a larger slab size');135});136});137138139