Fix atlas page overflow: merge same-size pairs, evict as last resort
TextureAtlas._createNewPage only merged exactly-4 same-size page sets; with a heterogeneous size mix (e.g. 1024+512+512+512) it pushed past maxAtlasPages instead. GlyphRenderer's GL texture array and fragment-shader sampler array are sized to exactly maxAtlasPages, so the next render threw TypeError: Cannot read properties of undefined (reading 'version') and rendering froze (single-terminal breakage under long heavy-CLI sessions, vscode#322756). - Generalize merge selection to the largest same-size group of 2-4 pages (preferring 4, then larger sizes); AtlasPage already supported non-quad merges. Net page count never exceeds the cap. - Derive merging page indexes via _pages.indexOf instead of glyphs[0] .texturePage, which is empty-page safe. - Last resort when no same-size pair exists below max texture size (only possible when almost all pages are at max size): evict all pages; glyphs re-rasterize on demand and models rebuild via pagesVersion. - Defensive clamp in GlyphRenderer.render so any future overflow renders untextured glyphs and logs a warning instead of throwing mid-frame. WebglAtlasOverflow now passes (was 4x TypeError); full webgl suite 75/75.
This commit is contained in:
@@ -101,6 +101,7 @@ export class GlyphRenderer extends Disposable {
|
||||
|
||||
private _atlas: ITextureAtlas | undefined;
|
||||
private _lastAtlasPagesVersion: number = -1;
|
||||
private _pageOverflowWarned: boolean = false;
|
||||
private _activeBuffer: number = 0;
|
||||
private readonly _vertices: IVertices = {
|
||||
count: 0,
|
||||
@@ -376,8 +377,15 @@ export class GlyphRenderer extends Disposable {
|
||||
|
||||
// Bind the atlas page texture if they have changed. AtlasPage.version is globally
|
||||
// monotonic, so a page object swap at the same index (which happens after a page merge)
|
||||
// is detected by the same comparison.
|
||||
for (let i = 0; i < this._atlas.pages.length; i++) {
|
||||
// is detected by the same comparison. The page count is clamped to the fixed texture
|
||||
// array size so an atlas page overflow degrades (untextured glyphs) instead of throwing
|
||||
// mid-frame; the atlas is responsible for never exceeding maxAtlasPages.
|
||||
const pageCount = Math.min(this._atlas.pages.length, this._atlasTextures.length);
|
||||
if (this._atlas.pages.length > this._atlasTextures.length && !this._pageOverflowWarned) {
|
||||
this._pageOverflowWarned = true;
|
||||
this._logService.warn(`Atlas page count (${this._atlas.pages.length}) exceeds the renderer's texture capacity (${this._atlasTextures.length}), some glyphs will not render correctly`);
|
||||
}
|
||||
for (let i = 0; i < pageCount; i++) {
|
||||
if (this._atlas.pages[i].version !== this._atlasTextures[i].version) {
|
||||
this._bindAtlasPageTexture(gl, this._atlas, i);
|
||||
}
|
||||
|
||||
@@ -161,7 +161,7 @@ export class TextureAtlas implements ITextureAtlas {
|
||||
// animation frame which would result in blank rendered areas. This is actually not that
|
||||
// expensive relative to drawing the glyphs, so there is no need to wait for an idle callback.
|
||||
if (TextureAtlas.maxAtlasPages && this._pages.length >= Math.max(4, TextureAtlas.maxAtlasPages)) {
|
||||
// Find the set of the largest 4 images, below the maximum size, with the highest
|
||||
// Find the set of the largest images, below the maximum size, with the highest
|
||||
// percentages used
|
||||
const pagesBySize = this._pages.filter(e => {
|
||||
return e.canvas.width * 2 <= (TextureAtlas.maxTextureSize || Constants.FORCED_MAX_TEXTURE_SIZE);
|
||||
@@ -171,48 +171,55 @@ export class TextureAtlas implements ITextureAtlas {
|
||||
}
|
||||
return b.percentageUsed - a.percentageUsed;
|
||||
});
|
||||
let sameSizeI = -1;
|
||||
let size = 0;
|
||||
for (let i = 0; i < pagesBySize.length; i++) {
|
||||
if (pagesBySize[i].canvas.width !== size) {
|
||||
sameSizeI = i;
|
||||
size = pagesBySize[i].canvas.width;
|
||||
} else if (i - sameSizeI === 3) {
|
||||
break;
|
||||
|
||||
// Find the largest group (up to 4) of same-sized pages, preferring larger page sizes. A
|
||||
// group of 2 or 3 still reduces (or at worst maintains) the page count net of the new page
|
||||
// pushed below; growing past the cap is never an option as GlyphRenderer's texture and
|
||||
// shader sampler arrays are sized to exactly maxAtlasPages (exceeding them crashes render).
|
||||
let bestGroupStart = -1;
|
||||
let bestGroupSize = 0;
|
||||
let groupStart = 0;
|
||||
for (let i = 1; i <= pagesBySize.length; i++) {
|
||||
if (i === pagesBySize.length || pagesBySize[i].canvas.width !== pagesBySize[groupStart].canvas.width) {
|
||||
const groupSize = Math.min(4, i - groupStart);
|
||||
if (groupSize > bestGroupSize) {
|
||||
bestGroupSize = groupSize;
|
||||
bestGroupStart = groupStart;
|
||||
if (groupSize === 4) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
groupStart = i;
|
||||
}
|
||||
}
|
||||
|
||||
// Gather details of the merge
|
||||
const mergingPages = pagesBySize.slice(sameSizeI, sameSizeI + 4);
|
||||
if (bestGroupSize >= 2) {
|
||||
// Gather details of the merge
|
||||
const mergingPages = pagesBySize.slice(bestGroupStart, bestGroupStart + bestGroupSize);
|
||||
const sortedMergingPagesIndexes = mergingPages.map(e => this._pages.indexOf(e)).sort((a, b) => a - b);
|
||||
const mergedPageIndex = this.pages.length - mergingPages.length;
|
||||
|
||||
// Only proceed with merge if we have exactly 4 same-sized pages. If not, we cannot
|
||||
// effectively reduce page count and merging would cause issues.
|
||||
if (mergingPages.length < 4 || mergingPages.some(p => p.canvas.width !== mergingPages[0].canvas.width)) {
|
||||
const newPage = new AtlasPage(this._document, this._textureSize);
|
||||
this._pages.push(newPage);
|
||||
this._activePages.push(newPage);
|
||||
this._onAddTextureAtlasCanvas.fire(newPage.canvas);
|
||||
return newPage;
|
||||
// Merge into the new page
|
||||
const mergedPage = this._mergePages(mergingPages, mergedPageIndex);
|
||||
mergedPage.version = ++AtlasPage.nextVersion;
|
||||
|
||||
// Delete the pages, shifting glyph texture pages as needed
|
||||
for (let i = sortedMergingPagesIndexes.length - 1; i >= 0; i--) {
|
||||
this._deletePage(sortedMergingPagesIndexes[i]);
|
||||
}
|
||||
|
||||
// Add the new merged page to the end
|
||||
this.pages.push(mergedPage);
|
||||
|
||||
// Invalidate models so all texture pages are refreshed.
|
||||
this._pagesVersion++;
|
||||
this._onAddTextureAtlasCanvas.fire(mergedPage.canvas);
|
||||
} else {
|
||||
// No two same-sized pages below the maximum size exist, so merging cannot reduce the
|
||||
// page count. Evict all pages rather than exceed the renderers' fixed texture/sampler
|
||||
// capacity; glyphs re-rasterize on demand and models rebuild via pagesVersion.
|
||||
this._evictAllPages();
|
||||
}
|
||||
|
||||
const sortedMergingPagesIndexes = mergingPages.map(e => e.glyphs[0].texturePage).sort((a, b) => a - b);
|
||||
const mergedPageIndex = this.pages.length - mergingPages.length;
|
||||
|
||||
// Merge into the new page
|
||||
const mergedPage = this._mergePages(mergingPages, mergedPageIndex);
|
||||
mergedPage.version = ++AtlasPage.nextVersion;
|
||||
|
||||
// Delete the pages, shifting glyph texture pages as needed
|
||||
for (let i = sortedMergingPagesIndexes.length - 1; i >= 0; i--) {
|
||||
this._deletePage(sortedMergingPagesIndexes[i]);
|
||||
}
|
||||
|
||||
// Add the new merged page to the end
|
||||
this.pages.push(mergedPage);
|
||||
|
||||
// Invalidate models so all texture pages are refreshed.
|
||||
this._pagesVersion++;
|
||||
this._onAddTextureAtlasCanvas.fire(mergedPage.canvas);
|
||||
}
|
||||
|
||||
// All new atlas pages are created small as they are highly dynamic
|
||||
@@ -262,6 +269,27 @@ export class TextureAtlas implements ITextureAtlas {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes every page and clears the glyph caches. This is the last resort when the page cap is
|
||||
* reached but no same-sized pages exist to merge: the page count must never exceed
|
||||
* maxAtlasPages because renderers size their texture and shader sampler arrays to exactly
|
||||
* that. Glyphs re-rasterize on demand and renderers rebuild their models via
|
||||
* {@link pagesVersion}.
|
||||
*/
|
||||
private _evictAllPages(): void {
|
||||
for (const page of this._pages) {
|
||||
this._onRemoveTextureAtlasCanvas.fire(page.canvas);
|
||||
page.canvas.remove();
|
||||
}
|
||||
this._pages.length = 0;
|
||||
this._activePages.length = 0;
|
||||
this._overflowSizePage = undefined;
|
||||
this._cacheMap.clear();
|
||||
this._cacheMapCombined.clear();
|
||||
this._didWarmUp = false;
|
||||
this._pagesVersion++;
|
||||
}
|
||||
|
||||
public getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number, ext: number, restrictToCellHeight: boolean, domContainer: HTMLElement | undefined): IRasterizedGlyph {
|
||||
return this._getFromCacheMap(this._cacheMapCombined, chars, bg, fg, ext, restrictToCellHeight, domContainer);
|
||||
}
|
||||
@@ -1103,7 +1131,7 @@ class AtlasPage {
|
||||
sourcePages[2]._usedPixels +
|
||||
sourcePages[3]._usedPixels;
|
||||
} else {
|
||||
// fallback for non quadmerges (should never be used)
|
||||
// 2- and 3-page merges, used when no 4-page same-size set exists at the page cap
|
||||
for (let i = 0; i < sourcePages.length; ++i) {
|
||||
this._glyphs = this._glyphs.concat(sourcePages[i].glyphs);
|
||||
this._usedPixels += sourcePages[i]._usedPixels;
|
||||
|
||||
Reference in New Issue
Block a user