From ca809e45021a9bfe41fd9bad44157fce90a0631a Mon Sep 17 00:00:00 2001 From: Labhansh Agrawal Date: Sun, 29 Jan 2023 01:46:06 +0530 Subject: [PATCH 1/6] split extremely wide ligatures to fit atlas --- .../xterm-addon-canvas/src/BaseRenderLayer.ts | 58 ++-- addons/xterm-addon-webgl/src/GlyphRenderer.ts | 6 +- src/browser/renderer/shared/TextureAtlas.ts | 297 ++++++++++-------- src/browser/renderer/shared/Types.d.ts | 4 +- 4 files changed, 198 insertions(+), 167 deletions(-) diff --git a/addons/xterm-addon-canvas/src/BaseRenderLayer.ts b/addons/xterm-addon-canvas/src/BaseRenderLayer.ts index 88d929f1d9..04b40cca97 100644 --- a/addons/xterm-addon-canvas/src/BaseRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/BaseRenderLayer.ts @@ -368,37 +368,43 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer protected _drawChars(cell: ICellData, x: number, y: number): void { const chars = cell.getChars(); this._cellColorResolver.resolve(cell, x, this._bufferService.buffer.ydisp + y); - let glyph: IRasterizedGlyph; + let glyphs: IRasterizedGlyph[]; if (chars && chars.length > 1) { - glyph = this._charAtlas.getRasterizedGlyphCombinedChar(chars, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext); + glyphs = this._charAtlas.getRasterizedGlyphCombinedChar(chars, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext); } else { - glyph = this._charAtlas.getRasterizedGlyph(cell.getCode() || WHITESPACE_CELL_CODE, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext); + glyphs = this._charAtlas.getRasterizedGlyph(cell.getCode() || WHITESPACE_CELL_CODE, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext); } - if (!glyph.size.x || !glyph.size.y) { - return; - } - this._ctx.save(); - this._clipRow(y); - // Draw the image, use the bitmap if it's available - if (this._charAtlas.pages[glyph.texturePage].version !== this._bitmapGenerator[glyph.texturePage]?.version) { - if (!this._bitmapGenerator[glyph.texturePage]) { - this._bitmapGenerator[glyph.texturePage] = new BitmapGenerator(this._charAtlas.pages[glyph.texturePage].canvas); + + let glyphsOffsetX = 0; + for (let i = 0; i < glyphs.length; i++) { + const glyph = glyphs[i]; + if (!glyph.size.x || !glyph.size.y) { + continue; } - this._bitmapGenerator[glyph.texturePage]!.refresh(); - this._bitmapGenerator[glyph.texturePage]!.version = this._charAtlas.pages[glyph.texturePage].version; + this._ctx.save(); + this._clipRow(y); + // Draw the image, use the bitmap if it's available + if (this._charAtlas.pages[glyph.texturePage].version !== this._bitmapGenerator[glyph.texturePage]?.version) { + if (!this._bitmapGenerator[glyph.texturePage]) { + this._bitmapGenerator[glyph.texturePage] = new BitmapGenerator(this._charAtlas.pages[glyph.texturePage].canvas); + } + this._bitmapGenerator[glyph.texturePage]!.refresh(); + this._bitmapGenerator[glyph.texturePage]!.version = this._charAtlas.pages[glyph.texturePage].version; + } + this._ctx.drawImage( + this._bitmapGenerator[glyph.texturePage]?.bitmap || this._charAtlas!.pages[glyph.texturePage].canvas, + glyph.texturePosition.x, + glyph.texturePosition.y, + glyph.size.x, + glyph.size.y, + x * this._deviceCellWidth + this._deviceCharLeft - glyph.offset.x + glyphsOffsetX, + y * this._deviceCellHeight + this._deviceCharTop - glyph.offset.y, + glyph.size.x, + glyph.size.y + ); + glyphsOffsetX += glyph.size.x; + this._ctx.restore(); } - this._ctx.drawImage( - this._bitmapGenerator[glyph.texturePage]?.bitmap || this._charAtlas!.pages[glyph.texturePage].canvas, - glyph.texturePosition.x, - glyph.texturePosition.y, - glyph.size.x, - glyph.size.y, - x * this._deviceCellWidth + this._deviceCharLeft - glyph.offset.x, - y * this._deviceCellHeight + this._deviceCharTop - glyph.offset.y, - glyph.size.x, - glyph.size.y - ); - this._ctx.restore(); } /** diff --git a/addons/xterm-addon-webgl/src/GlyphRenderer.ts b/addons/xterm-addon-webgl/src/GlyphRenderer.ts index 202d4deba0..91f2dc4fe6 100644 --- a/addons/xterm-addon-webgl/src/GlyphRenderer.ts +++ b/addons/xterm-addon-webgl/src/GlyphRenderer.ts @@ -85,6 +85,7 @@ const CELL_POSITION_INDICES = 2; // Work variables to avoid garbage collection let $i = 0; let $glyph: IRasterizedGlyph | undefined = undefined; +let $glyphs: IRasterizedGlyph[] | undefined = undefined; let $leftCellPadding = 0; let $clippedPixels = 0; @@ -236,10 +237,11 @@ export class GlyphRenderer extends Disposable { // Get the glyph if (chars && chars.length > 1) { - $glyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg, ext); + $glyphs = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg, ext); } else { - $glyph = this._atlas.getRasterizedGlyph(code, bg, fg, ext); + $glyphs = this._atlas.getRasterizedGlyph(code, bg, fg, ext); } + $glyph = $glyphs[0]; $leftCellPadding = Math.floor((this._dimensions.device.cell.width - this._dimensions.device.char.width) / 2); if (bg !== lastBg && $glyph.offset.x > $leftCellPadding) { diff --git a/src/browser/renderer/shared/TextureAtlas.ts b/src/browser/renderer/shared/TextureAtlas.ts index ce1684653b..87422dd609 100644 --- a/src/browser/renderer/shared/TextureAtlas.ts +++ b/src/browser/renderer/shared/TextureAtlas.ts @@ -56,8 +56,8 @@ let $glyph = undefined; export class TextureAtlas implements ITextureAtlas { private _didWarmUp: boolean = false; - private _cacheMap: FourKeyMap = new FourKeyMap(); - private _cacheMapCombined: FourKeyMap = new FourKeyMap(); + private _cacheMap: FourKeyMap = new FourKeyMap(); + private _cacheMapCombined: FourKeyMap = new FourKeyMap(); // The texture that the atlas is drawn to private _pages: AtlasPage[] = []; @@ -242,11 +242,11 @@ export class TextureAtlas implements ITextureAtlas { } } - public getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number, ext: number): IRasterizedGlyph { + public getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number, ext: number): IRasterizedGlyph[] { return this._getFromCacheMap(this._cacheMapCombined, chars, bg, fg, ext); } - public getRasterizedGlyph(code: number, bg: number, fg: number, ext: number): IRasterizedGlyph { + public getRasterizedGlyph(code: number, bg: number, fg: number, ext: number): IRasterizedGlyph[] { return this._getFromCacheMap(this._cacheMap, code, bg, fg, ext); } @@ -254,12 +254,12 @@ export class TextureAtlas implements ITextureAtlas { * Gets the glyphs texture coords, drawing the texture if it's not already */ private _getFromCacheMap( - cacheMap: FourKeyMap, + cacheMap: FourKeyMap, key: string | number, bg: number, fg: number, ext: number - ): IRasterizedGlyph { + ): IRasterizedGlyph[] { $glyph = cacheMap.get(key, bg, fg, ext); if (!$glyph) { $glyph = this._drawToCache(key, bg, fg, ext); @@ -420,7 +420,7 @@ export class TextureAtlas implements ITextureAtlas { return color; } - private _drawToCache(codeOrChars: number | string, bg: number, fg: number, ext: number): IRasterizedGlyph { + private _drawToCache(codeOrChars: number | string, bg: number, fg: number, ext: number): IRasterizedGlyph[] { const chars = typeof codeOrChars === 'number' ? String.fromCharCode(codeOrChars) : codeOrChars; // Uncomment for debugging @@ -432,6 +432,9 @@ export class TextureAtlas implements ITextureAtlas { const allowedWidth = this._config.deviceCellWidth * Math.max(chars.length, 2) + TMP_CANVAS_GLYPH_PADDING * 2; if (this._tmpCanvas.width < allowedWidth) { this._tmpCanvas.width = allowedWidth; + } else if (this._tmpCanvas.width > allowedWidth * 2) { + // Shrink the canvas if it's too large + this._tmpCanvas.width = allowedWidth; } // Include line height when drawing glyphs const allowedHeight = this._config.deviceCellHeight + TMP_CANVAS_GLYPH_PADDING * 4; @@ -446,7 +449,7 @@ export class TextureAtlas implements ITextureAtlas { const invisible = !!this._workAttributeData.isInvisible(); if (invisible) { - return NULL_RASTERIZED_GLYPH; + return [NULL_RASTERIZED_GLYPH]; } const bold = !!this._workAttributeData.isBold(); @@ -678,157 +681,177 @@ export class TextureAtlas implements ITextureAtlas { // clear the background from the character to avoid issues with drawing over the previous // character if it extends past it's bounds - const imageData = this._tmpCtx.getImageData( - 0, 0, this._tmpCanvas.width, this._tmpCanvas.height - ); - // Clear out the background color and determine if the glyph is empty. - let isEmpty: boolean; - if (!this._config.allowTransparency) { - isEmpty = clearColor(imageData, backgroundColor, foregroundColor, enableClearThresholdCheck); + // split the image into multiple images to fit into the texture + const images: ImageData[] = []; + let glyphWidth = allowedWidth; + if (this._tmpCanvas.width > this._textureSize) { + const step = this._textureSize-(2*padding); + glyphWidth = step; + for (let i = 0; i < this._tmpCanvas.width; i += step) { + const width = Math.min(step, this._tmpCanvas.width - i); + const image = this._tmpCtx.getImageData(i, 0, width, this._tmpCanvas.height); + images.push(image); + } } else { - isEmpty = checkCompletelyTransparent(imageData); + images.push(this._tmpCtx.getImageData(0, 0, this._tmpCanvas.width, this._tmpCanvas.height)); } - // Handle empty glyphs - if (isEmpty) { - return NULL_RASTERIZED_GLYPH; - } + const rasterizedGlyphs: IRasterizedGlyph[] = []; + for (let i=0; i= 0; i--) { - for (const row of this._activePages[i].fixedRows) { - if (row.height <= activeRow.height && rasterizedGlyph.size.y <= row.height) { - activePage = this._activePages[i]; - activeRow = row; + const rasterizedGlyph = this._findGlyphBoundingBox(imageData, this._workBoundingBox, Math.min(glyphWidth, imageData.width), restrictedPowerlineGlyph, customGlyph, padding); + + // Find the best atlas row to use + let activePage: AtlasPage; + let activeRow: ICharAtlasActiveRow; + while (true) { + // If there are no active pages (the last smallest 4 were merged), create a new one + if (this._activePages.length === 0) { + const newPage = this._createNewPage(); + activePage = newPage; + activeRow = newPage.currentRow; + activeRow.height = rasterizedGlyph.size.y; + break; + } + + // Get the best current row from all active pages + activePage = this._activePages[this._activePages.length - 1]; + activeRow = activePage.currentRow; + for (const p of this._activePages) { + if (rasterizedGlyph.size.y <= p.currentRow.height) { + activePage = p; + activeRow = p.currentRow; + } + } + + // TODO: This algorithm could be simplified: + // - Search for the page with ROW_PIXEL_THRESHOLD in mind + // - Keep track of current/fixed rows in a Map + + // Replace the best current row with a fixed row if there is one at least as good as the + // current row. Search in reverse to prioritize filling in older pages. + for (let i = this._activePages.length - 1; i >= 0; i--) { + for (const row of this._activePages[i].fixedRows) { + if (row.height <= activeRow.height && rasterizedGlyph.size.y <= row.height) { + activePage = this._activePages[i]; + activeRow = row; + } } } - } - // Create a new one if too much vertical space would be wasted or there is not enough room - // left in the page. The previous active row will become fixed in the process as it now has a - // fixed height - if (activeRow.y + rasterizedGlyph.size.y >= activePage.canvas.height || activeRow.height > rasterizedGlyph.size.y + Constants.ROW_PIXEL_THRESHOLD) { + // Create a new one if too much vertical space would be wasted or there is not enough room + // left in the page. The previous active row will become fixed in the process as it now has a + // fixed height + if (activeRow.y + rasterizedGlyph.size.y >= activePage.canvas.height || activeRow.height > rasterizedGlyph.size.y + Constants.ROW_PIXEL_THRESHOLD) { // Create the new fixed height row, creating a new page if there isn't enough room on the // current page - let wasNewPageCreated = false; - if (activePage.currentRow.y + activePage.currentRow.height + rasterizedGlyph.size.y >= activePage.canvas.height) { + let wasNewPageCreated = false; + if (activePage.currentRow.y + activePage.currentRow.height + rasterizedGlyph.size.y >= activePage.canvas.height) { // Find the first page with room to create the new row on - let candidatePage: AtlasPage | undefined; - for (const p of this._activePages) { - if (p.currentRow.y + p.currentRow.height + rasterizedGlyph.size.y < p.canvas.height) { - candidatePage = p; - break; + let candidatePage: AtlasPage | undefined; + for (const p of this._activePages) { + if (p.currentRow.y + p.currentRow.height + rasterizedGlyph.size.y < p.canvas.height) { + candidatePage = p; + break; + } } - } - if (candidatePage) { - activePage = candidatePage; - } else { + if (candidatePage) { + activePage = candidatePage; + } else { // Create a new page if there is no room - const newPage = this._createNewPage(); - activePage = newPage; - activeRow = newPage.currentRow; - activeRow.height = rasterizedGlyph.size.y; - wasNewPageCreated = true; + const newPage = this._createNewPage(); + activePage = newPage; + activeRow = newPage.currentRow; + activeRow.height = rasterizedGlyph.size.y; + wasNewPageCreated = true; + } } - } - if (!wasNewPageCreated) { + if (!wasNewPageCreated) { // Fix the current row as the new row is being added below - if (activePage.currentRow.height > 0) { - activePage.fixedRows.push(activePage.currentRow); + if (activePage.currentRow.height > 0) { + activePage.fixedRows.push(activePage.currentRow); + } + activeRow = { + x: 0, + y: activePage.currentRow.y + activePage.currentRow.height, + height: rasterizedGlyph.size.y + }; + activePage.fixedRows.push(activeRow); + + // Create the new current row below the new fixed height row + activePage.currentRow = { + x: 0, + y: activeRow.y + activeRow.height, + height: 0 + }; } - activeRow = { - x: 0, - y: activePage.currentRow.y + activePage.currentRow.height, - height: rasterizedGlyph.size.y - }; - activePage.fixedRows.push(activeRow); - - // Create the new current row below the new fixed height row - activePage.currentRow = { - x: 0, - y: activeRow.y + activeRow.height, - height: 0 - }; - } // TODO: Remove pages from _activePages when all rows are filled - } + } - // Exit the loop if there is enough room in the row - if (activeRow.x + rasterizedGlyph.size.x <= activePage.canvas.width) { - break; - } + // Exit the loop if there is enough room in the row + if (activeRow.x + rasterizedGlyph.size.x <= activePage.canvas.width) { + break; + } - // If there is not enough room in the current row, finish it and try again - if (activeRow === activePage.currentRow) { - activeRow.x = 0; - activeRow.y += activeRow.height; - activeRow.height = 0; - } else { - activePage.fixedRows.splice(activePage.fixedRows.indexOf(activeRow), 1); + // If there is not enough room in the current row, finish it and try again + if (activeRow === activePage.currentRow) { + activeRow.x = 0; + activeRow.y += activeRow.height; + activeRow.height = 0; + } else { + activePage.fixedRows.splice(activePage.fixedRows.indexOf(activeRow), 1); + } } - } - // Record texture position - rasterizedGlyph.texturePage = this._pages.indexOf(activePage); - rasterizedGlyph.texturePosition.x = activeRow.x; - rasterizedGlyph.texturePosition.y = activeRow.y; - rasterizedGlyph.texturePositionClipSpace.x = activeRow.x / activePage.canvas.width; - rasterizedGlyph.texturePositionClipSpace.y = activeRow.y / activePage.canvas.height; - - // Fix the clipspace position as pages may be of differing size - rasterizedGlyph.sizeClipSpace.x /= activePage.canvas.width; - rasterizedGlyph.sizeClipSpace.y /= activePage.canvas.height; - - // Update atlas current row, for fixed rows the glyph height will never be larger than the row - // height - activeRow.height = Math.max(activeRow.height, rasterizedGlyph.size.y); - activeRow.x += rasterizedGlyph.size.x; - - // putImageData doesn't do any blending, so it will overwrite any existing cache entry for us - activePage.ctx.putImageData( - imageData, - rasterizedGlyph.texturePosition.x - this._workBoundingBox.left, - rasterizedGlyph.texturePosition.y - this._workBoundingBox.top, - this._workBoundingBox.left, - this._workBoundingBox.top, - rasterizedGlyph.size.x, - rasterizedGlyph.size.y - ); - activePage.addGlyph(rasterizedGlyph); - activePage.version++; + // Record texture position + rasterizedGlyph.texturePage = this._pages.indexOf(activePage); + rasterizedGlyph.texturePosition.x = activeRow.x; + rasterizedGlyph.texturePosition.y = activeRow.y; + rasterizedGlyph.texturePositionClipSpace.x = activeRow.x / activePage.canvas.width; + rasterizedGlyph.texturePositionClipSpace.y = activeRow.y / activePage.canvas.height; + + // Fix the clipspace position as pages may be of differing size + rasterizedGlyph.sizeClipSpace.x /= activePage.canvas.width; + rasterizedGlyph.sizeClipSpace.y /= activePage.canvas.height; + + // Update atlas current row, for fixed rows the glyph height will never be larger than the row + // height + activeRow.height = Math.max(activeRow.height, rasterizedGlyph.size.y); + activeRow.x += rasterizedGlyph.size.x; + + // putImageData doesn't do any blending, so it will overwrite any existing cache entry for us + activePage.ctx.putImageData( + imageData, + rasterizedGlyph.texturePosition.x - this._workBoundingBox.left, + rasterizedGlyph.texturePosition.y - this._workBoundingBox.top, + this._workBoundingBox.left, + this._workBoundingBox.top, + rasterizedGlyph.size.x, + rasterizedGlyph.size.y + ); + activePage.addGlyph(rasterizedGlyph); + activePage.version++; + + rasterizedGlyphs.push(rasterizedGlyph); + } - return rasterizedGlyph; + return rasterizedGlyphs; } /** @@ -845,7 +868,7 @@ export class TextureAtlas implements ITextureAtlas { let found = false; for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { - const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3; + const alphaOffset = y * imageData.width * 4 + x * 4 + 3; if (imageData.data[alphaOffset] !== 0) { boundingBox.top = y; found = true; @@ -860,7 +883,7 @@ export class TextureAtlas implements ITextureAtlas { found = false; for (let x = 0; x < padding + width; x++) { for (let y = 0; y < height; y++) { - const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3; + const alphaOffset = y * imageData.width * 4 + x * 4 + 3; if (imageData.data[alphaOffset] !== 0) { boundingBox.left = x; found = true; @@ -875,7 +898,7 @@ export class TextureAtlas implements ITextureAtlas { found = false; for (let x = padding + width - 1; x >= padding; x--) { for (let y = 0; y < height; y++) { - const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3; + const alphaOffset = y * imageData.width * 4 + x * 4 + 3; if (imageData.data[alphaOffset] !== 0) { boundingBox.right = x; found = true; @@ -890,7 +913,7 @@ export class TextureAtlas implements ITextureAtlas { found = false; for (let y = height - 1; y >= 0; y--) { for (let x = 0; x < width; x++) { - const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3; + const alphaOffset = y * imageData.width * 4 + x * 4 + 3; if (imageData.data[alphaOffset] !== 0) { boundingBox.bottom = y; found = true; diff --git a/src/browser/renderer/shared/Types.d.ts b/src/browser/renderer/shared/Types.d.ts index cf4513c26a..24f276dd0a 100644 --- a/src/browser/renderer/shared/Types.d.ts +++ b/src/browser/renderer/shared/Types.d.ts @@ -107,8 +107,8 @@ export interface ITextureAtlas extends IDisposable { * Clear all glyphs from the texture atlas. */ clearTexture(): void; - getRasterizedGlyph(code: number, bg: number, fg: number, ext: number): IRasterizedGlyph; - getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number, ext: number): IRasterizedGlyph; + getRasterizedGlyph(code: number, bg: number, fg: number, ext: number): IRasterizedGlyph[]; + getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number, ext: number): IRasterizedGlyph[]; } /** From 50bccb4a4ad420562d97d621a138a20eebd9cd97 Mon Sep 17 00:00:00 2001 From: Labhansh Agrawal Date: Sun, 29 Jan 2023 12:54:30 +0530 Subject: [PATCH 2/6] fix offset --- addons/xterm-addon-canvas/src/BaseRenderLayer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/xterm-addon-canvas/src/BaseRenderLayer.ts b/addons/xterm-addon-canvas/src/BaseRenderLayer.ts index 04b40cca97..94889a65da 100644 --- a/addons/xterm-addon-canvas/src/BaseRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/BaseRenderLayer.ts @@ -402,7 +402,7 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer glyph.size.x, glyph.size.y ); - glyphsOffsetX += glyph.size.x; + glyphsOffsetX += glyph.size.x - glyph.offset.x; this._ctx.restore(); } } From 0bf072c9c06c227072b72f623858729a2ff5cecd Mon Sep 17 00:00:00 2001 From: Labhansh Agrawal Date: Fri, 24 Feb 2023 00:47:05 +0530 Subject: [PATCH 3/6] set max width long ligatures render wider than the cells --- src/browser/renderer/shared/TextureAtlas.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/renderer/shared/TextureAtlas.ts b/src/browser/renderer/shared/TextureAtlas.ts index 87422dd609..8635d07852 100644 --- a/src/browser/renderer/shared/TextureAtlas.ts +++ b/src/browser/renderer/shared/TextureAtlas.ts @@ -643,7 +643,7 @@ export class TextureAtlas implements ITextureAtlas { // Draw the character if (!customGlyph) { - this._tmpCtx.fillText(chars, padding, padding + this._config.deviceCharHeight); + this._tmpCtx.fillText(chars, padding, padding + this._config.deviceCharHeight, allowedWidth - padding); } // If this charcater is underscore and beyond the cell bounds, shift it up until it is visible From 2d2db018d43cff019c8f723311a8f05688bbca25 Mon Sep 17 00:00:00 2001 From: Labhansh Agrawal Date: Sat, 25 Feb 2023 19:20:46 +0530 Subject: [PATCH 4/6] fix empty and unnecessarily wide glyphs --- src/browser/renderer/shared/TextureAtlas.ts | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/browser/renderer/shared/TextureAtlas.ts b/src/browser/renderer/shared/TextureAtlas.ts index 8635d07852..6a89d30a66 100644 --- a/src/browser/renderer/shared/TextureAtlas.ts +++ b/src/browser/renderer/shared/TextureAtlas.ts @@ -684,12 +684,18 @@ export class TextureAtlas implements ITextureAtlas { // split the image into multiple images to fit into the texture const images: ImageData[] = []; - let glyphWidth = allowedWidth; if (this._tmpCanvas.width > this._textureSize) { const step = this._textureSize-(2*padding); - glyphWidth = step; - for (let i = 0; i < this._tmpCanvas.width; i += step) { - const width = Math.min(step, this._tmpCanvas.width - i); + + const fullImageData = this._tmpCtx.getImageData(0, 0, this._tmpCanvas.width, this._tmpCanvas.height); + if (!this._config.allowTransparency) { + clearColor(fullImageData, backgroundColor, foregroundColor, enableClearThresholdCheck); + } + + this._findGlyphBoundingBox(fullImageData, this._workBoundingBox, fullImageData.width, restrictedPowerlineGlyph, customGlyph, padding); + const end = this._workBoundingBox.right; + for (let i = 0; i <= end; i += step) { + const width = Math.min(step, end - i + 1); const image = this._tmpCtx.getImageData(i, 0, width, this._tmpCanvas.height); images.push(image); } @@ -715,7 +721,7 @@ export class TextureAtlas implements ITextureAtlas { continue; } - const rasterizedGlyph = this._findGlyphBoundingBox(imageData, this._workBoundingBox, Math.min(glyphWidth, imageData.width), restrictedPowerlineGlyph, customGlyph, padding); + const rasterizedGlyph = this._findGlyphBoundingBox(imageData, this._workBoundingBox, imageData.width, restrictedPowerlineGlyph, customGlyph, padding); // Find the best atlas row to use let activePage: AtlasPage; @@ -881,7 +887,7 @@ export class TextureAtlas implements ITextureAtlas { } boundingBox.left = 0; found = false; - for (let x = 0; x < padding + width; x++) { + for (let x = 0; x < width; x++) { for (let y = 0; y < height; y++) { const alphaOffset = y * imageData.width * 4 + x * 4 + 3; if (imageData.data[alphaOffset] !== 0) { @@ -896,7 +902,7 @@ export class TextureAtlas implements ITextureAtlas { } boundingBox.right = width; found = false; - for (let x = padding + width - 1; x >= padding; x--) { + for (let x = width - 1; x >= 0; x--) { for (let y = 0; y < height; y++) { const alphaOffset = y * imageData.width * 4 + x * 4 + 3; if (imageData.data[alphaOffset] !== 0) { From a3321a617596355229bdbd9eaacc4411f634a645 Mon Sep 17 00:00:00 2001 From: Labhansh Agrawal Date: Sat, 25 Feb 2023 19:21:45 +0530 Subject: [PATCH 5/6] webgl --- addons/xterm-addon-webgl/src/GlyphRenderer.ts | 73 ++++++++++--------- addons/xterm-addon-webgl/src/WebglRenderer.ts | 11 +-- src/browser/renderer/shared/TextureAtlas.ts | 2 +- 3 files changed, 46 insertions(+), 40 deletions(-) diff --git a/addons/xterm-addon-webgl/src/GlyphRenderer.ts b/addons/xterm-addon-webgl/src/GlyphRenderer.ts index 91f2dc4fe6..caeb33c68e 100644 --- a/addons/xterm-addon-webgl/src/GlyphRenderer.ts +++ b/addons/xterm-addon-webgl/src/GlyphRenderer.ts @@ -84,6 +84,7 @@ const CELL_POSITION_INDICES = 2; // Work variables to avoid garbage collection let $i = 0; +let $j = 0; let $glyph: IRasterizedGlyph | undefined = undefined; let $glyphs: IRasterizedGlyph[] | undefined = undefined; let $leftCellPadding = 0; @@ -241,40 +242,44 @@ export class GlyphRenderer extends Disposable { } else { $glyphs = this._atlas.getRasterizedGlyph(code, bg, fg, ext); } - $glyph = $glyphs[0]; - - $leftCellPadding = Math.floor((this._dimensions.device.cell.width - this._dimensions.device.char.width) / 2); - if (bg !== lastBg && $glyph.offset.x > $leftCellPadding) { - $clippedPixels = $glyph.offset.x - $leftCellPadding; - // a_origin - array[$i ] = -($glyph.offset.x - $clippedPixels) + this._dimensions.device.char.left; - array[$i + 1] = -$glyph.offset.y + this._dimensions.device.char.top; - // a_size - array[$i + 2] = ($glyph.size.x - $clippedPixels) / this._dimensions.device.canvas.width; - array[$i + 3] = $glyph.size.y / this._dimensions.device.canvas.height; - // a_texpage - array[$i + 4] = $glyph.texturePage; - // a_texcoord - array[$i + 5] = $glyph.texturePositionClipSpace.x + $clippedPixels / this._atlas.pages[$glyph.texturePage].canvas.width; - array[$i + 6] = $glyph.texturePositionClipSpace.y; - // a_texsize - array[$i + 7] = $glyph.sizeClipSpace.x - $clippedPixels / this._atlas.pages[$glyph.texturePage].canvas.width; - array[$i + 8] = $glyph.sizeClipSpace.y; - } else { - // a_origin - array[$i ] = -$glyph.offset.x + this._dimensions.device.char.left; - array[$i + 1] = -$glyph.offset.y + this._dimensions.device.char.top; - // a_size - array[$i + 2] = $glyph.size.x / this._dimensions.device.canvas.width; - array[$i + 3] = $glyph.size.y / this._dimensions.device.canvas.height; - // a_texpage - array[$i + 4] = $glyph.texturePage; - // a_texcoord - array[$i + 5] = $glyph.texturePositionClipSpace.x; - array[$i + 6] = $glyph.texturePositionClipSpace.y; - // a_texsize - array[$i + 7] = $glyph.sizeClipSpace.x; - array[$i + 8] = $glyph.sizeClipSpace.y; + + for ($j = 0; $j < $glyphs.length; $j++) { + $i = (y * this._terminal.cols + x + $j) * INDICES_PER_CELL; + $glyph = $glyphs[$j]; + + $leftCellPadding = Math.floor((this._dimensions.device.cell.width - this._dimensions.device.char.width) / 2); + if (bg !== lastBg && $glyph.offset.x > $leftCellPadding) { + $clippedPixels = $glyph.offset.x - $leftCellPadding; + // a_origin + array[$i ] = -($glyph.offset.x - $clippedPixels) + this._dimensions.device.char.left; + array[$i + 1] = -$glyph.offset.y + this._dimensions.device.char.top; + // a_size + array[$i + 2] = ($glyph.size.x - $clippedPixels) / this._dimensions.device.canvas.width; + array[$i + 3] = $glyph.size.y / this._dimensions.device.canvas.height; + // a_texpage + array[$i + 4] = $glyph.texturePage; + // a_texcoord + array[$i + 5] = $glyph.texturePositionClipSpace.x + $clippedPixels / this._atlas.pages[$glyph.texturePage].canvas.width; + array[$i + 6] = $glyph.texturePositionClipSpace.y; + // a_texsize + array[$i + 7] = $glyph.sizeClipSpace.x - $clippedPixels / this._atlas.pages[$glyph.texturePage].canvas.width; + array[$i + 8] = $glyph.sizeClipSpace.y; + } else { + // a_origin + array[$i ] = -$glyph.offset.x + this._dimensions.device.char.left; + array[$i + 1] = -$glyph.offset.y + this._dimensions.device.char.top; + // a_size + array[$i + 2] = $glyph.size.x / this._dimensions.device.canvas.width; + array[$i + 3] = $glyph.size.y / this._dimensions.device.canvas.height; + // a_texpage + array[$i + 4] = $glyph.texturePage; + // a_texcoord + array[$i + 5] = $glyph.texturePositionClipSpace.x; + array[$i + 6] = $glyph.texturePositionClipSpace.y; + // a_texsize + array[$i + 7] = $glyph.sizeClipSpace.x; + array[$i + 8] = $glyph.sizeClipSpace.y; + } } // a_cellpos only changes on resize } diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index fcb0dd8096..2f29181ea4 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -366,6 +366,7 @@ export class WebglRenderer extends Disposable implements IRenderer { let i: number; let x: number; let j: number; + let k: number; start = clamp(start, terminal.rows - 1, 0); end = clamp(end, terminal.rows - 1, 0); for (y = start; y <= end; y++) { @@ -434,22 +435,22 @@ export class WebglRenderer extends Disposable implements IRenderer { this._model.cells[i + RENDER_MODEL_FG_OFFSET] = this._cellColorResolver.result.fg; this._model.cells[i + RENDER_MODEL_EXT_OFFSET] = this._cellColorResolver.result.ext; - this._glyphRenderer!.updateCell(x, y, code, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext, chars, lastBg); - if (isJoined) { // Restore work cell cell = this._workCell; // Null out non-first cells - for (x++; x < lastCharX; x++) { - j = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL; - this._glyphRenderer!.updateCell(x, y, NULL_CELL_CODE, 0, 0, 0, NULL_CELL_CHAR, 0); + for (k = x + 1; k < lastCharX; k++) { + j = ((y * terminal.cols) + k) * RENDER_MODEL_INDICIES_PER_CELL; + this._glyphRenderer!.updateCell(k, y, NULL_CELL_CODE, 0, 0, 0, NULL_CELL_CHAR, 0); this._model.cells[j] = NULL_CELL_CODE; this._model.cells[j + RENDER_MODEL_BG_OFFSET] = this._cellColorResolver.result.bg; this._model.cells[j + RENDER_MODEL_FG_OFFSET] = this._cellColorResolver.result.fg; this._model.cells[j + RENDER_MODEL_EXT_OFFSET] = this._cellColorResolver.result.ext; } } + this._glyphRenderer!.updateCell(x, y, code, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext, chars, lastBg); + x = lastCharX; } } this._rectangleRenderer!.updateBackgrounds(this._model); diff --git a/src/browser/renderer/shared/TextureAtlas.ts b/src/browser/renderer/shared/TextureAtlas.ts index 6a89d30a66..c06f270b9e 100644 --- a/src/browser/renderer/shared/TextureAtlas.ts +++ b/src/browser/renderer/shared/TextureAtlas.ts @@ -685,7 +685,7 @@ export class TextureAtlas implements ITextureAtlas { // split the image into multiple images to fit into the texture const images: ImageData[] = []; if (this._tmpCanvas.width > this._textureSize) { - const step = this._textureSize-(2*padding); + const step = this._config.deviceCellWidth; const fullImageData = this._tmpCtx.getImageData(0, 0, this._tmpCanvas.width, this._tmpCanvas.height); if (!this._config.allowTransparency) { From ff69d7cc1fa3bdcfaddc9976a537d4498277f241 Mon Sep 17 00:00:00 2001 From: Labhansh Agrawal Date: Mon, 27 Feb 2023 21:45:12 +0530 Subject: [PATCH 6/6] split glyph into bigger pieces --- addons/xterm-addon-webgl/src/GlyphRenderer.ts | 12 ++++++++---- src/browser/renderer/shared/TextureAtlas.ts | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/addons/xterm-addon-webgl/src/GlyphRenderer.ts b/addons/xterm-addon-webgl/src/GlyphRenderer.ts index caeb33c68e..04a548b74b 100644 --- a/addons/xterm-addon-webgl/src/GlyphRenderer.ts +++ b/addons/xterm-addon-webgl/src/GlyphRenderer.ts @@ -89,6 +89,7 @@ let $glyph: IRasterizedGlyph | undefined = undefined; let $glyphs: IRasterizedGlyph[] | undefined = undefined; let $leftCellPadding = 0; let $clippedPixels = 0; +let $offset = 0; export class GlyphRenderer extends Disposable { private readonly _program: WebGLProgram; @@ -243,15 +244,17 @@ export class GlyphRenderer extends Disposable { $glyphs = this._atlas.getRasterizedGlyph(code, bg, fg, ext); } + $offset = $glyphs[0].offset.x; + for ($j = 0; $j < $glyphs.length; $j++) { $i = (y * this._terminal.cols + x + $j) * INDICES_PER_CELL; $glyph = $glyphs[$j]; $leftCellPadding = Math.floor((this._dimensions.device.cell.width - this._dimensions.device.char.width) / 2); - if (bg !== lastBg && $glyph.offset.x > $leftCellPadding) { - $clippedPixels = $glyph.offset.x - $leftCellPadding; + if (bg !== lastBg && $offset > $leftCellPadding) { + $clippedPixels = $offset - $leftCellPadding; // a_origin - array[$i ] = -($glyph.offset.x - $clippedPixels) + this._dimensions.device.char.left; + array[$i ] = -($offset - $clippedPixels) + this._dimensions.device.char.left; array[$i + 1] = -$glyph.offset.y + this._dimensions.device.char.top; // a_size array[$i + 2] = ($glyph.size.x - $clippedPixels) / this._dimensions.device.canvas.width; @@ -266,7 +269,7 @@ export class GlyphRenderer extends Disposable { array[$i + 8] = $glyph.sizeClipSpace.y; } else { // a_origin - array[$i ] = -$glyph.offset.x + this._dimensions.device.char.left; + array[$i ] = -$offset + this._dimensions.device.char.left; array[$i + 1] = -$glyph.offset.y + this._dimensions.device.char.top; // a_size array[$i + 2] = $glyph.size.x / this._dimensions.device.canvas.width; @@ -280,6 +283,7 @@ export class GlyphRenderer extends Disposable { array[$i + 7] = $glyph.sizeClipSpace.x; array[$i + 8] = $glyph.sizeClipSpace.y; } + $offset -= $glyph.size.x - this._dimensions.device.cell.width; } // a_cellpos only changes on resize } diff --git a/src/browser/renderer/shared/TextureAtlas.ts b/src/browser/renderer/shared/TextureAtlas.ts index c06f270b9e..16d0bba75c 100644 --- a/src/browser/renderer/shared/TextureAtlas.ts +++ b/src/browser/renderer/shared/TextureAtlas.ts @@ -685,7 +685,7 @@ export class TextureAtlas implements ITextureAtlas { // split the image into multiple images to fit into the texture const images: ImageData[] = []; if (this._tmpCanvas.width > this._textureSize) { - const step = this._config.deviceCellWidth; + const step = this._textureSize - 2 * padding; const fullImageData = this._tmpCtx.getImageData(0, 0, this._tmpCanvas.width, this._tmpCanvas.height); if (!this._config.allowTransparency) {