From 54335ef54ae052063cb384148e1655a8a0749e60 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 28 Oct 2022 18:16:55 -0700 Subject: [PATCH 01/28] Create texture page attribute --- addons/xterm-addon-webgl/src/GlyphRenderer.ts | 69 ++++++++++++++----- 1 file changed, 50 insertions(+), 19 deletions(-) diff --git a/addons/xterm-addon-webgl/src/GlyphRenderer.ts b/addons/xterm-addon-webgl/src/GlyphRenderer.ts index 4bce546ac6..a6c2f74c9e 100644 --- a/addons/xterm-addon-webgl/src/GlyphRenderer.ts +++ b/addons/xterm-addon-webgl/src/GlyphRenderer.ts @@ -8,7 +8,6 @@ import { IWebGL2RenderingContext, IWebGLVertexArrayObject, IRenderModel } from ' import { fill } from 'common/TypedArrayUtils'; import { NULL_CELL_CODE } from 'common/buffer/Constants'; import { Terminal } from 'xterm'; -import { IColorSet } from 'browser/Types'; import { IRasterizedGlyph, IRenderDimensions, ITextureAtlas } from 'browser/renderer/shared/Types'; import { Disposable, toDisposable } from 'common/Lifecycle'; import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils'; @@ -30,8 +29,9 @@ const enum VertexAttribLocations { CELL_POSITION = 1, OFFSET = 2, SIZE = 3, - TEXCOORD = 4, - TEXSIZE = 5 + TEXPAGE = 4, + TEXCOORD = 5, + TEXSIZE = 6 } const vertexShaderSource = `#version 300 es @@ -39,6 +39,7 @@ layout (location = ${VertexAttribLocations.UNIT_QUAD}) in vec2 a_unitquad; layout (location = ${VertexAttribLocations.CELL_POSITION}) in vec2 a_cellpos; layout (location = ${VertexAttribLocations.OFFSET}) in vec2 a_offset; layout (location = ${VertexAttribLocations.SIZE}) in vec2 a_size; +layout (location = ${VertexAttribLocations.TEXPAGE}) in float a_texpage; layout (location = ${VertexAttribLocations.TEXCOORD}) in vec2 a_texcoord; layout (location = ${VertexAttribLocations.TEXSIZE}) in vec2 a_texsize; @@ -46,10 +47,12 @@ uniform mat4 u_projection; uniform vec2 u_resolution; out vec2 v_texcoord; +flat out int v_texpage; void main() { vec2 zeroToOne = (a_offset / u_resolution) + a_cellpos + (a_unitquad * a_size); gl_Position = u_projection * vec4(zeroToOne, 0.0, 1.0); + v_texpage = int(a_texpage); v_texcoord = a_texcoord + a_unitquad * a_texsize; }`; @@ -57,16 +60,21 @@ const fragmentShaderSource = `#version 300 es precision lowp float; in vec2 v_texcoord; +flat in int v_texpage; -uniform sampler2D u_texture; +uniform sampler2D u_texture[2]; out vec4 outColor; void main() { - outColor = texture(u_texture, v_texcoord); + if (v_texpage == 0) { + outColor = texture(u_texture[0], v_texcoord); + } else if (v_texpage == 1) { + outColor = texture(u_texture[1], v_texcoord); + } }`; -const INDICES_PER_CELL = 10; +const INDICES_PER_CELL = 11; const BYTES_PER_CELL = INDICES_PER_CELL * Float32Array.BYTES_PER_ELEMENT; const CELL_POSITION_INDICES = 2; @@ -84,6 +92,7 @@ export class GlyphRenderer extends Disposable { private _projectionLocation: WebGLUniformLocation; private _resolutionLocation: WebGLUniformLocation; private _textureLocation: WebGLUniformLocation; + private readonly _nullTexture: WebGLTexture; private _atlasTexture: WebGLTexture; private _attributesBuffer: WebGLBuffer; private _activeBuffer: number = 0; @@ -145,24 +154,35 @@ export class GlyphRenderer extends Disposable { gl.enableVertexAttribArray(VertexAttribLocations.SIZE); gl.vertexAttribPointer(VertexAttribLocations.SIZE, 2, gl.FLOAT, false, BYTES_PER_CELL, 2 * Float32Array.BYTES_PER_ELEMENT); gl.vertexAttribDivisor(VertexAttribLocations.SIZE, 1); + gl.enableVertexAttribArray(VertexAttribLocations.TEXPAGE); + gl.vertexAttribPointer(VertexAttribLocations.TEXPAGE, 1, gl.FLOAT, false, BYTES_PER_CELL, 4 * Float32Array.BYTES_PER_ELEMENT); + gl.vertexAttribDivisor(VertexAttribLocations.TEXPAGE, 1); gl.enableVertexAttribArray(VertexAttribLocations.TEXCOORD); - gl.vertexAttribPointer(VertexAttribLocations.TEXCOORD, 2, gl.FLOAT, false, BYTES_PER_CELL, 4 * Float32Array.BYTES_PER_ELEMENT); + gl.vertexAttribPointer(VertexAttribLocations.TEXCOORD, 2, gl.FLOAT, false, BYTES_PER_CELL, 5 * Float32Array.BYTES_PER_ELEMENT); gl.vertexAttribDivisor(VertexAttribLocations.TEXCOORD, 1); gl.enableVertexAttribArray(VertexAttribLocations.TEXSIZE); - gl.vertexAttribPointer(VertexAttribLocations.TEXSIZE, 2, gl.FLOAT, false, BYTES_PER_CELL, 6 * Float32Array.BYTES_PER_ELEMENT); + gl.vertexAttribPointer(VertexAttribLocations.TEXSIZE, 2, gl.FLOAT, false, BYTES_PER_CELL, 7 * Float32Array.BYTES_PER_ELEMENT); gl.vertexAttribDivisor(VertexAttribLocations.TEXSIZE, 1); gl.enableVertexAttribArray(VertexAttribLocations.CELL_POSITION); - gl.vertexAttribPointer(VertexAttribLocations.CELL_POSITION, 2, gl.FLOAT, false, BYTES_PER_CELL, 8 * Float32Array.BYTES_PER_ELEMENT); + gl.vertexAttribPointer(VertexAttribLocations.CELL_POSITION, 2, gl.FLOAT, false, BYTES_PER_CELL, 9 * Float32Array.BYTES_PER_ELEMENT); gl.vertexAttribDivisor(VertexAttribLocations.CELL_POSITION, 1); // Setup empty texture atlas this._atlasTexture = throwIfFalsy(gl.createTexture()); this.register(toDisposable(() => gl.deleteTexture(this._atlasTexture))); + gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, this._atlasTexture); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([0, 0, 255, 255])); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + this._nullTexture = throwIfFalsy(gl.createTexture()); + gl.activeTexture(gl.TEXTURE0 + 1); + gl.bindTexture(gl.TEXTURE_2D, this._nullTexture); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([255, 0, 0, 255])); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + // Allow drawing of transparent texture gl.enable(gl.BLEND); gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); @@ -213,12 +233,14 @@ export class GlyphRenderer extends Disposable { // 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] = 0; // a_texcoord - array[$i + 4] = $glyph.texturePositionClipSpace.x + $clippedPixels / this._atlas.cacheCanvas.width; - array[$i + 5] = $glyph.texturePositionClipSpace.y; + array[$i + 5] = $glyph.texturePositionClipSpace.x + $clippedPixels / this._atlas.cacheCanvas.width; + array[$i + 6] = $glyph.texturePositionClipSpace.y; // a_texsize - array[$i + 6] = $glyph.sizeClipSpace.x - $clippedPixels / this._atlas.cacheCanvas.width; - array[$i + 7] = $glyph.sizeClipSpace.y; + array[$i + 7] = $glyph.sizeClipSpace.x - $clippedPixels / this._atlas.cacheCanvas.width; + array[$i + 8] = $glyph.sizeClipSpace.y; } else { // a_origin array[$i ] = -$glyph.offset.x + this._dimensions.device.char.left; @@ -226,12 +248,14 @@ export class GlyphRenderer extends Disposable { // 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] = 0; // a_texcoord - array[$i + 4] = $glyph.texturePositionClipSpace.x; - array[$i + 5] = $glyph.texturePositionClipSpace.y; + array[$i + 5] = $glyph.texturePositionClipSpace.x; + array[$i + 6] = $glyph.texturePositionClipSpace.y; // a_texsize - array[$i + 6] = $glyph.sizeClipSpace.x; - array[$i + 7] = $glyph.sizeClipSpace.y; + array[$i + 7] = $glyph.sizeClipSpace.x; + array[$i + 8] = $glyph.sizeClipSpace.y; } // a_cellpos only changes on resize } @@ -257,8 +281,8 @@ export class GlyphRenderer extends Disposable { let i = 0; for (let y = 0; y < terminal.rows; y++) { for (let x = 0; x < terminal.cols; x++) { - this._vertices.attributes[i + 8] = x / terminal.cols; - this._vertices.attributes[i + 9] = y / terminal.rows; + this._vertices.attributes[i + 9] = x / terminal.cols; + this._vertices.attributes[i + 10] = y / terminal.rows; i += INDICES_PER_CELL; } } @@ -306,13 +330,20 @@ export class GlyphRenderer extends Disposable { // Bind the texture atlas if it's changed if (this._atlas.hasCanvasChanged) { this._atlas.hasCanvasChanged = false; + // TODO: Make nicer + const layerTextureUnits = new Int32Array([0, 1]); + gl.uniform1iv(this._textureLocation, layerTextureUnits); gl.uniform1i(this._textureLocation, 0); gl.activeTexture(gl.TEXTURE0 + 0); gl.bindTexture(gl.TEXTURE_2D, this._atlasTexture); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this._atlas.cacheCanvas); + // TODO: Why is mipmap here? gl.generateMipmap(gl.TEXTURE_2D); } + gl.activeTexture(gl.TEXTURE0 + 1); + gl.bindTexture(gl.TEXTURE_2D, this._nullTexture); + // Set uniforms gl.uniformMatrix4fv(this._projectionLocation, false, PROJECTION_MATRIX); gl.uniform2f(this._resolutionLocation, gl.canvas.width, gl.canvas.height); From 84ab6a49c4cf717027251a4c118552a7ed0472ea Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 28 Oct 2022 18:56:49 -0700 Subject: [PATCH 02/28] Bring texturePage partially to atlas --- addons/xterm-addon-webgl/src/GlyphRenderer.ts | 52 ++++++++++++------- src/browser/renderer/shared/TextureAtlas.ts | 29 ++++++++--- src/browser/renderer/shared/Types.d.ts | 6 +++ 3 files changed, 60 insertions(+), 27 deletions(-) diff --git a/addons/xterm-addon-webgl/src/GlyphRenderer.ts b/addons/xterm-addon-webgl/src/GlyphRenderer.ts index a6c2f74c9e..58df22dd23 100644 --- a/addons/xterm-addon-webgl/src/GlyphRenderer.ts +++ b/addons/xterm-addon-webgl/src/GlyphRenderer.ts @@ -85,18 +85,17 @@ let $leftCellPadding = 0; let $clippedPixels = 0; export class GlyphRenderer extends Disposable { - private _atlas: ITextureAtlas | undefined; + private readonly _program: WebGLProgram; + private readonly _vertexArrayObject: IWebGLVertexArrayObject; + private readonly _projectionLocation: WebGLUniformLocation; + private readonly _resolutionLocation: WebGLUniformLocation; + private readonly _textureLocation: WebGLUniformLocation; + private readonly _atlasTexture: WebGLTexture; + private readonly _atlasTexture1: WebGLTexture; + private readonly _attributesBuffer: WebGLBuffer; - private _program: WebGLProgram; - private _vertexArrayObject: IWebGLVertexArrayObject; - private _projectionLocation: WebGLUniformLocation; - private _resolutionLocation: WebGLUniformLocation; - private _textureLocation: WebGLUniformLocation; - private readonly _nullTexture: WebGLTexture; - private _atlasTexture: WebGLTexture; - private _attributesBuffer: WebGLBuffer; + private _atlas: ITextureAtlas | undefined; private _activeBuffer: number = 0; - private _vertices: IVertices = { count: 0, attributes: new Float32Array(0), @@ -172,16 +171,17 @@ export class GlyphRenderer extends Disposable { this.register(toDisposable(() => gl.deleteTexture(this._atlasTexture))); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, this._atlasTexture); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([0, 0, 255, 255])); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([0, 0, 255, 255])); - this._nullTexture = throwIfFalsy(gl.createTexture()); + this._atlasTexture1 = throwIfFalsy(gl.createTexture()); + this.register(toDisposable(() => gl.deleteTexture(this._atlasTexture1))); gl.activeTexture(gl.TEXTURE0 + 1); - gl.bindTexture(gl.TEXTURE_2D, this._nullTexture); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([255, 0, 0, 255])); + gl.bindTexture(gl.TEXTURE_2D, this._atlasTexture1); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([255, 0, 0, 255])); // Allow drawing of transparent texture gl.enable(gl.BLEND); @@ -234,7 +234,7 @@ export class GlyphRenderer extends Disposable { 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] = 0; + array[$i + 4] = $glyph.texturePage; // a_texcoord array[$i + 5] = $glyph.texturePositionClipSpace.x + $clippedPixels / this._atlas.cacheCanvas.width; array[$i + 6] = $glyph.texturePositionClipSpace.y; @@ -249,7 +249,7 @@ export class GlyphRenderer extends Disposable { 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] = 0; + array[$i + 4] = $glyph.texturePage; // a_texcoord array[$i + 5] = $glyph.texturePositionClipSpace.x; array[$i + 6] = $glyph.texturePositionClipSpace.y; @@ -333,16 +333,18 @@ export class GlyphRenderer extends Disposable { // TODO: Make nicer const layerTextureUnits = new Int32Array([0, 1]); gl.uniform1iv(this._textureLocation, layerTextureUnits); - gl.uniform1i(this._textureLocation, 0); gl.activeTexture(gl.TEXTURE0 + 0); gl.bindTexture(gl.TEXTURE_2D, this._atlasTexture); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this._atlas.cacheCanvas); - // TODO: Why is mipmap here? + gl.generateMipmap(gl.TEXTURE_2D); + + // TODO: Check if the particular texture page changed + gl.activeTexture(gl.TEXTURE0 + 1); + gl.bindTexture(gl.TEXTURE_2D, this._atlasTexture1); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this._atlas.cacheCanvas1!); gl.generateMipmap(gl.TEXTURE_2D); } - gl.activeTexture(gl.TEXTURE0 + 1); - gl.bindTexture(gl.TEXTURE_2D, this._nullTexture); // Set uniforms gl.uniformMatrix4fv(this._projectionLocation, false, PROJECTION_MATRIX); @@ -356,9 +358,19 @@ export class GlyphRenderer extends Disposable { const gl = this._gl; this._atlas = atlas; + gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, this._atlasTexture); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, atlas.cacheCanvas); gl.generateMipmap(gl.TEXTURE_2D); + + gl.activeTexture(gl.TEXTURE0 + 1); + gl.bindTexture(gl.TEXTURE_2D, this._atlasTexture1); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, atlas.cacheCanvas1!); + gl.generateMipmap(gl.TEXTURE_2D); } public setDimensions(dimensions: IRenderDimensions): void { diff --git a/src/browser/renderer/shared/TextureAtlas.ts b/src/browser/renderer/shared/TextureAtlas.ts index 4edebf5c8e..25ecf3cb33 100644 --- a/src/browser/renderer/shared/TextureAtlas.ts +++ b/src/browser/renderer/shared/TextureAtlas.ts @@ -30,9 +30,10 @@ const TEXTURE_CAPACITY = Math.floor(TEXTURE_HEIGHT * 0.8); * A shared object which is used to draw nothing for a particular cell. */ const NULL_RASTERIZED_GLYPH: IRasterizedGlyph = { - offset: { x: 0, y: 0 }, + texturePage: 0, texturePosition: { x: 0, y: 0 }, texturePositionClipSpace: { x: 0, y: 0 }, + offset: { x: 0, y: 0 }, size: { x: 0, y: 0 }, sizeClipSpace: { x: 0, y: 0 } }; @@ -56,7 +57,9 @@ export class TextureAtlas implements ITextureAtlas { // The texture that the atlas is drawn to public cacheCanvas: HTMLCanvasElement; + public cacheCanvas1: HTMLCanvasElement | undefined; private _cacheCtx: CanvasRenderingContext2D; + private _cacheCtx1: CanvasRenderingContext2D | undefined; private _tmpCanvas: HTMLCanvasElement; // A temporary context that glyphs are drawn to before being transfered to the atlas. @@ -89,23 +92,34 @@ export class TextureAtlas implements ITextureAtlas { private readonly _config: ICharAtlasConfig, private readonly _unicodeService: IUnicodeService ) { - this.cacheCanvas = document.createElement('canvas'); - this.cacheCanvas.width = TEXTURE_WIDTH; - this.cacheCanvas.height = TEXTURE_HEIGHT; + this.cacheCanvas = this._createCanvas(TEXTURE_WIDTH, TEXTURE_HEIGHT); // The canvas needs alpha because we use clearColor to convert the background color to alpha. // It might also contain some characters with transparent backgrounds if allowTransparency is // set. this._cacheCtx = throwIfFalsy(this.cacheCanvas.getContext('2d', { alpha: true })); - this._tmpCanvas = document.createElement('canvas'); - this._tmpCanvas.width = this._config.deviceCellWidth * 4 + TMP_CANVAS_GLYPH_PADDING * 2; - this._tmpCanvas.height = this._config.deviceCellHeight + TMP_CANVAS_GLYPH_PADDING * 2; + this.cacheCanvas1 = this._createCanvas(TEXTURE_WIDTH, TEXTURE_HEIGHT); + this._cacheCtx1 = throwIfFalsy(this.cacheCanvas1.getContext('2d', { alpha: true })); + this._cacheCtx1.fillStyle = 'rgb(255, 255, 0)'; + this._cacheCtx1.fillRect(0, 0, TEXTURE_WIDTH, TEXTURE_HEIGHT); + + this._tmpCanvas = this._createCanvas( + this._config.deviceCellWidth * 4 + TMP_CANVAS_GLYPH_PADDING * 2, + this._config.deviceCellHeight + TMP_CANVAS_GLYPH_PADDING * 2 + ); this._tmpCtx = throwIfFalsy(this._tmpCanvas.getContext('2d', { alpha: this._config.allowTransparency, willReadFrequently: true })); } + private _createCanvas(width: number, height: number): HTMLCanvasElement { + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + return canvas; + } + public dispose(): void { if (this.cacheCanvas.parentElement) { this.cacheCanvas.parentElement.removeChild(this.cacheCanvas); @@ -759,6 +773,7 @@ export class TextureAtlas implements ITextureAtlas { } } return { + texturePage: 1, texturePosition: { x: 0, y: 0 }, texturePositionClipSpace: { x: 0, y: 0 }, size: { diff --git a/src/browser/renderer/shared/Types.d.ts b/src/browser/renderer/shared/Types.d.ts index 433d48a221..b5220e2c51 100644 --- a/src/browser/renderer/shared/Types.d.ts +++ b/src/browser/renderer/shared/Types.d.ts @@ -88,6 +88,8 @@ export interface IRenderer extends IDisposable { export interface ITextureAtlas extends IDisposable { readonly cacheCanvas: HTMLCanvasElement; + readonly cacheCanvas1: HTMLCanvasElement | undefined; + hasCanvasChanged: boolean; /** @@ -120,6 +122,10 @@ export interface IRasterizedGlyph { * in pixels. */ offset: IVector; + /** + * The index of the texture page that the glyph is on. + */ + texturePage: number; /** * the x and y position of the glyph in the texture in pixels. */ From 6b37187b4d3c42e8a258439096fb85ee8971f46f Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 28 Oct 2022 19:08:33 -0700 Subject: [PATCH 03/28] Create AtlasPage --- addons/xterm-addon-webgl/src/GlyphRenderer.ts | 62 +++++++++---------- src/browser/renderer/shared/TextureAtlas.ts | 60 ++++++++++-------- src/browser/renderer/shared/Types.d.ts | 4 +- 3 files changed, 69 insertions(+), 57 deletions(-) diff --git a/addons/xterm-addon-webgl/src/GlyphRenderer.ts b/addons/xterm-addon-webgl/src/GlyphRenderer.ts index 58df22dd23..2096c35a63 100644 --- a/addons/xterm-addon-webgl/src/GlyphRenderer.ts +++ b/addons/xterm-addon-webgl/src/GlyphRenderer.ts @@ -77,6 +77,7 @@ void main() { const INDICES_PER_CELL = 11; const BYTES_PER_CELL = INDICES_PER_CELL * Float32Array.BYTES_PER_ELEMENT; const CELL_POSITION_INDICES = 2; +const MAX_ATLAS_PAGES = 8; // Work variables to avoid garbage collection let $i = 0; @@ -90,8 +91,7 @@ export class GlyphRenderer extends Disposable { private readonly _projectionLocation: WebGLUniformLocation; private readonly _resolutionLocation: WebGLUniformLocation; private readonly _textureLocation: WebGLUniformLocation; - private readonly _atlasTexture: WebGLTexture; - private readonly _atlasTexture1: WebGLTexture; + private readonly _atlasTextures: WebGLTexture[]; private readonly _attributesBuffer: WebGLBuffer; private _atlas: ITextureAtlas | undefined; @@ -166,22 +166,18 @@ export class GlyphRenderer extends Disposable { gl.vertexAttribPointer(VertexAttribLocations.CELL_POSITION, 2, gl.FLOAT, false, BYTES_PER_CELL, 9 * Float32Array.BYTES_PER_ELEMENT); gl.vertexAttribDivisor(VertexAttribLocations.CELL_POSITION, 1); - // Setup empty texture atlas - this._atlasTexture = throwIfFalsy(gl.createTexture()); - this.register(toDisposable(() => gl.deleteTexture(this._atlasTexture))); - gl.activeTexture(gl.TEXTURE0); - gl.bindTexture(gl.TEXTURE_2D, this._atlasTexture); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([0, 0, 255, 255])); - - this._atlasTexture1 = throwIfFalsy(gl.createTexture()); - this.register(toDisposable(() => gl.deleteTexture(this._atlasTexture1))); - gl.activeTexture(gl.TEXTURE0 + 1); - gl.bindTexture(gl.TEXTURE_2D, this._atlasTexture1); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([255, 0, 0, 255])); + // Setup empty textures for all potential atlas pages + this._atlasTextures = []; + for (let i = 0; i < MAX_ATLAS_PAGES; i++) { + const texture = throwIfFalsy(gl.createTexture()); + this.register(toDisposable(() => gl.deleteTexture(texture))); + gl.activeTexture(gl.TEXTURE0 + i); + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([255, 0, 0, 255])); + this._atlasTextures[i] = texture; + } // Allow drawing of transparent texture gl.enable(gl.BLEND); @@ -334,15 +330,17 @@ export class GlyphRenderer extends Disposable { const layerTextureUnits = new Int32Array([0, 1]); gl.uniform1iv(this._textureLocation, layerTextureUnits); gl.activeTexture(gl.TEXTURE0 + 0); - gl.bindTexture(gl.TEXTURE_2D, this._atlasTexture); + gl.bindTexture(gl.TEXTURE_2D, this._atlasTextures[0]); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this._atlas.cacheCanvas); gl.generateMipmap(gl.TEXTURE_2D); - // TODO: Check if the particular texture page changed - gl.activeTexture(gl.TEXTURE0 + 1); - gl.bindTexture(gl.TEXTURE_2D, this._atlasTexture1); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this._atlas.cacheCanvas1!); - gl.generateMipmap(gl.TEXTURE_2D); + if (this._atlas.pages.length > 1) { + // TODO: Check if the particular texture page changed + gl.activeTexture(gl.TEXTURE0 + 1); + gl.bindTexture(gl.TEXTURE_2D, this._atlasTextures[1]); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this._atlas.pages[1]); + gl.generateMipmap(gl.TEXTURE_2D); + } } @@ -359,18 +357,20 @@ export class GlyphRenderer extends Disposable { this._atlas = atlas; gl.activeTexture(gl.TEXTURE0); - gl.bindTexture(gl.TEXTURE_2D, this._atlasTexture); + gl.bindTexture(gl.TEXTURE_2D, this._atlasTextures[0]); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, atlas.cacheCanvas); gl.generateMipmap(gl.TEXTURE_2D); - gl.activeTexture(gl.TEXTURE0 + 1); - gl.bindTexture(gl.TEXTURE_2D, this._atlasTexture1); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, atlas.cacheCanvas1!); - gl.generateMipmap(gl.TEXTURE_2D); + if (atlas.pages.length > 1) { + gl.activeTexture(gl.TEXTURE0 + 1); + gl.bindTexture(gl.TEXTURE_2D, this._atlasTextures[1]); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, atlas.pages[1]); + gl.generateMipmap(gl.TEXTURE_2D); + } } public setDimensions(dimensions: IRenderDimensions): void { diff --git a/src/browser/renderer/shared/TextureAtlas.ts b/src/browser/renderer/shared/TextureAtlas.ts index 25ecf3cb33..0d291ff60a 100644 --- a/src/browser/renderer/shared/TextureAtlas.ts +++ b/src/browser/renderer/shared/TextureAtlas.ts @@ -56,10 +56,10 @@ export class TextureAtlas implements ITextureAtlas { private _cacheMapCombined: FourKeyMap = new FourKeyMap(); // The texture that the atlas is drawn to - public cacheCanvas: HTMLCanvasElement; - public cacheCanvas1: HTMLCanvasElement | undefined; - private _cacheCtx: CanvasRenderingContext2D; - private _cacheCtx1: CanvasRenderingContext2D | undefined; + private _pages: AtlasPage[] = []; + public get pages(): HTMLCanvasElement[] { return this._pages.map(e => e.canvas); } + + public get cacheCanvas(): HTMLCanvasElement { return this._pages[0].canvas; } private _tmpCanvas: HTMLCanvasElement; // A temporary context that glyphs are drawn to before being transfered to the atlas. @@ -92,18 +92,9 @@ export class TextureAtlas implements ITextureAtlas { private readonly _config: ICharAtlasConfig, private readonly _unicodeService: IUnicodeService ) { - this.cacheCanvas = this._createCanvas(TEXTURE_WIDTH, TEXTURE_HEIGHT); - // The canvas needs alpha because we use clearColor to convert the background color to alpha. - // It might also contain some characters with transparent backgrounds if allowTransparency is - // set. - this._cacheCtx = throwIfFalsy(this.cacheCanvas.getContext('2d', { alpha: true })); - - this.cacheCanvas1 = this._createCanvas(TEXTURE_WIDTH, TEXTURE_HEIGHT); - this._cacheCtx1 = throwIfFalsy(this.cacheCanvas1.getContext('2d', { alpha: true })); - this._cacheCtx1.fillStyle = 'rgb(255, 255, 0)'; - this._cacheCtx1.fillRect(0, 0, TEXTURE_WIDTH, TEXTURE_HEIGHT); - - this._tmpCanvas = this._createCanvas( + this._pages.push(new AtlasPage(document)); + this._tmpCanvas = createCanvas( + document, this._config.deviceCellWidth * 4 + TMP_CANVAS_GLYPH_PADDING * 2, this._config.deviceCellHeight + TMP_CANVAS_GLYPH_PADDING * 2 ); @@ -113,13 +104,6 @@ export class TextureAtlas implements ITextureAtlas { })); } - private _createCanvas(width: number, height: number): HTMLCanvasElement { - const canvas = document.createElement('canvas'); - canvas.width = width; - canvas.height = height; - return canvas; - } - public dispose(): void { if (this.cacheCanvas.parentElement) { this.cacheCanvas.parentElement.removeChild(this.cacheCanvas); @@ -159,7 +143,9 @@ export class TextureAtlas implements ITextureAtlas { if (this._currentRow.x === 0 && this._currentRow.y === 0) { return; } - this._cacheCtx.clearRect(0, 0, TEXTURE_WIDTH, TEXTURE_HEIGHT); + for (const page of this._pages) { + page.clear(); + } this._cacheMap.clear(); this._cacheMapCombined.clear(); this._currentRow.x = 0; @@ -689,7 +675,7 @@ export class TextureAtlas implements ITextureAtlas { activeRow.x += rasterizedGlyph.size.x; // putImageData doesn't do any blending, so it will overwrite any existing cache entry for us - this._cacheCtx.putImageData( + this._pages[0].ctx.putImageData( imageData, rasterizedGlyph.texturePosition.x - this._workBoundingBox.left, rasterizedGlyph.texturePosition.y - this._workBoundingBox.top, @@ -792,6 +778,23 @@ export class TextureAtlas implements ITextureAtlas { } } +class AtlasPage { + public readonly canvas: HTMLCanvasElement; + public readonly ctx: CanvasRenderingContext2D; + + constructor(document: Document) { + this.canvas = createCanvas(document, TEXTURE_WIDTH, TEXTURE_HEIGHT); + // The canvas needs alpha because we use clearColor to convert the background color to alpha. + // It might also contain some characters with transparent backgrounds if allowTransparency is + // set. + this.ctx = throwIfFalsy(this.canvas.getContext('2d', { alpha: true })); + } + + public clear(): void { + this.ctx.clearRect(0, 0, TEXTURE_WIDTH, TEXTURE_HEIGHT); + } +} + /** * Makes a particular rgb color and colors that are nearly the same in an ImageData completely * transparent. @@ -846,3 +849,10 @@ function checkCompletelyTransparent(imageData: ImageData): boolean { } return true; } + +function createCanvas(document: Document, width: number, height: number): HTMLCanvasElement { + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + return canvas; +} diff --git a/src/browser/renderer/shared/Types.d.ts b/src/browser/renderer/shared/Types.d.ts index b5220e2c51..f7ee968d1b 100644 --- a/src/browser/renderer/shared/Types.d.ts +++ b/src/browser/renderer/shared/Types.d.ts @@ -87,8 +87,10 @@ export interface IRenderer extends IDisposable { } export interface ITextureAtlas extends IDisposable { + /** @deprecated */ readonly cacheCanvas: HTMLCanvasElement; - readonly cacheCanvas1: HTMLCanvasElement | undefined; + + readonly pages: HTMLCanvasElement[]; hasCanvasChanged: boolean; From 975d52fab9b79fd3351cc18b13602e70b0f3d703 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 28 Oct 2022 19:12:01 -0700 Subject: [PATCH 04/28] Remove deprecated prop --- addons/xterm-addon-canvas/src/BaseRenderLayer.ts | 7 ++++--- addons/xterm-addon-webgl/src/GlyphRenderer.ts | 12 ++++++------ addons/xterm-addon-webgl/src/WebglRenderer.ts | 4 ++-- src/browser/renderer/shared/TextureAtlas.ts | 8 +++----- src/browser/renderer/shared/Types.d.ts | 5 +---- 5 files changed, 16 insertions(+), 20 deletions(-) diff --git a/addons/xterm-addon-canvas/src/BaseRenderLayer.ts b/addons/xterm-addon-canvas/src/BaseRenderLayer.ts index b037d61e63..1db5c81da5 100644 --- a/addons/xterm-addon-canvas/src/BaseRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/BaseRenderLayer.ts @@ -39,7 +39,8 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer protected _charAtlas!: ITextureAtlas; public get canvas(): HTMLCanvasElement { return this._canvas; } - public get cacheCanvas(): HTMLCanvasElement { return this._charAtlas?.cacheCanvas!; } + // TODO: Support multiple pages + public get cacheCanvas(): HTMLCanvasElement { return this._charAtlas?.pages[0].canvas!; } constructor( private readonly _terminal: Terminal, @@ -118,7 +119,7 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer } this._charAtlas = acquireTextureAtlas(this._terminal, colorSet, this._deviceCellWidth, this._deviceCellHeight, this._deviceCharWidth, this._deviceCharHeight, this._coreBrowserService.dpr); this._charAtlas.warmUp(); - this._bitmapGenerator = new BitmapGenerator(this._charAtlas.cacheCanvas); + this._bitmapGenerator = new BitmapGenerator(this._charAtlas.pages[0].canvas); } public resize(dim: IRenderDimensions): void { @@ -372,7 +373,7 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer this._charAtlas.hasCanvasChanged = false; } this._ctx.drawImage( - this._bitmapGenerator?.bitmap || this._charAtlas!.cacheCanvas, + this._bitmapGenerator?.bitmap || this._charAtlas!.pages[0].canvas, glyph.texturePosition.x, glyph.texturePosition.y, glyph.size.x, diff --git a/addons/xterm-addon-webgl/src/GlyphRenderer.ts b/addons/xterm-addon-webgl/src/GlyphRenderer.ts index 2096c35a63..73fa1af232 100644 --- a/addons/xterm-addon-webgl/src/GlyphRenderer.ts +++ b/addons/xterm-addon-webgl/src/GlyphRenderer.ts @@ -232,10 +232,10 @@ export class GlyphRenderer extends Disposable { // a_texpage array[$i + 4] = $glyph.texturePage; // a_texcoord - array[$i + 5] = $glyph.texturePositionClipSpace.x + $clippedPixels / this._atlas.cacheCanvas.width; + array[$i + 5] = $glyph.texturePositionClipSpace.x + $clippedPixels / this._atlas.pages[0].canvas.width; array[$i + 6] = $glyph.texturePositionClipSpace.y; // a_texsize - array[$i + 7] = $glyph.sizeClipSpace.x - $clippedPixels / this._atlas.cacheCanvas.width; + array[$i + 7] = $glyph.sizeClipSpace.x - $clippedPixels / this._atlas.pages[0].canvas.width; array[$i + 8] = $glyph.sizeClipSpace.y; } else { // a_origin @@ -331,14 +331,14 @@ export class GlyphRenderer extends Disposable { gl.uniform1iv(this._textureLocation, layerTextureUnits); gl.activeTexture(gl.TEXTURE0 + 0); gl.bindTexture(gl.TEXTURE_2D, this._atlasTextures[0]); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this._atlas.cacheCanvas); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this._atlas.pages[0].canvas); gl.generateMipmap(gl.TEXTURE_2D); if (this._atlas.pages.length > 1) { // TODO: Check if the particular texture page changed gl.activeTexture(gl.TEXTURE0 + 1); gl.bindTexture(gl.TEXTURE_2D, this._atlasTextures[1]); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this._atlas.pages[1]); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this._atlas.pages[1].canvas); gl.generateMipmap(gl.TEXTURE_2D); } } @@ -360,7 +360,7 @@ export class GlyphRenderer extends Disposable { gl.bindTexture(gl.TEXTURE_2D, this._atlasTextures[0]); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, atlas.cacheCanvas); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, atlas.pages[0].canvas); gl.generateMipmap(gl.TEXTURE_2D); if (atlas.pages.length > 1) { @@ -368,7 +368,7 @@ export class GlyphRenderer extends Disposable { gl.bindTexture(gl.TEXTURE_2D, this._atlasTextures[1]); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, atlas.pages[1]); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, atlas.pages[1].canvas); gl.generateMipmap(gl.TEXTURE_2D); } } diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index f366c37945..35542bcf7a 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -135,7 +135,7 @@ export class WebglRenderer extends Disposable implements IRenderer { } public get textureAtlas(): HTMLCanvasElement | undefined { - return this._charAtlas?.cacheCanvas; + return this._charAtlas?.pages[0].canvas; } private _handleColorChange(): void { @@ -261,7 +261,7 @@ export class WebglRenderer extends Disposable implements IRenderer { this._coreBrowserService.dpr ); if (this._charAtlas !== atlas) { - this._onChangeTextureAtlas.fire(atlas.cacheCanvas); + this._onChangeTextureAtlas.fire(atlas.pages[0].canvas); } this._charAtlas = atlas; this._charAtlas.warmUp(); diff --git a/src/browser/renderer/shared/TextureAtlas.ts b/src/browser/renderer/shared/TextureAtlas.ts index 0d291ff60a..7e5332262b 100644 --- a/src/browser/renderer/shared/TextureAtlas.ts +++ b/src/browser/renderer/shared/TextureAtlas.ts @@ -57,9 +57,7 @@ export class TextureAtlas implements ITextureAtlas { // The texture that the atlas is drawn to private _pages: AtlasPage[] = []; - public get pages(): HTMLCanvasElement[] { return this._pages.map(e => e.canvas); } - - public get cacheCanvas(): HTMLCanvasElement { return this._pages[0].canvas; } + public get pages(): { canvas: HTMLCanvasElement }[] { return this._pages; } private _tmpCanvas: HTMLCanvasElement; // A temporary context that glyphs are drawn to before being transfered to the atlas. @@ -105,8 +103,8 @@ export class TextureAtlas implements ITextureAtlas { } public dispose(): void { - if (this.cacheCanvas.parentElement) { - this.cacheCanvas.parentElement.removeChild(this.cacheCanvas); + for (const page of this.pages) { + page.canvas.remove(); } } diff --git a/src/browser/renderer/shared/Types.d.ts b/src/browser/renderer/shared/Types.d.ts index f7ee968d1b..2eaac493d8 100644 --- a/src/browser/renderer/shared/Types.d.ts +++ b/src/browser/renderer/shared/Types.d.ts @@ -87,10 +87,7 @@ export interface IRenderer extends IDisposable { } export interface ITextureAtlas extends IDisposable { - /** @deprecated */ - readonly cacheCanvas: HTMLCanvasElement; - - readonly pages: HTMLCanvasElement[]; + readonly pages: { canvas: HTMLCanvasElement }[]; hasCanvasChanged: boolean; From 67ef4555154c0938f0df5fa3c03213f4159f92a9 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 28 Oct 2022 19:20:26 -0700 Subject: [PATCH 05/28] Get a second page kind of working --- addons/xterm-addon-webgl/src/GlyphRenderer.ts | 4 +- src/browser/renderer/shared/TextureAtlas.ts | 90 ++++++++++--------- 2 files changed, 48 insertions(+), 46 deletions(-) diff --git a/addons/xterm-addon-webgl/src/GlyphRenderer.ts b/addons/xterm-addon-webgl/src/GlyphRenderer.ts index 73fa1af232..823a0db118 100644 --- a/addons/xterm-addon-webgl/src/GlyphRenderer.ts +++ b/addons/xterm-addon-webgl/src/GlyphRenderer.ts @@ -232,10 +232,10 @@ export class GlyphRenderer extends Disposable { // a_texpage array[$i + 4] = $glyph.texturePage; // a_texcoord - array[$i + 5] = $glyph.texturePositionClipSpace.x + $clippedPixels / this._atlas.pages[0].canvas.width; + 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[0].canvas.width; + array[$i + 7] = $glyph.sizeClipSpace.x - $clippedPixels / this._atlas.pages[$glyph.texturePage].canvas.width; array[$i + 8] = $glyph.sizeClipSpace.y; } else { // a_origin diff --git a/src/browser/renderer/shared/TextureAtlas.ts b/src/browser/renderer/shared/TextureAtlas.ts index 7e5332262b..e2d5d6170b 100644 --- a/src/browser/renderer/shared/TextureAtlas.ts +++ b/src/browser/renderer/shared/TextureAtlas.ts @@ -15,10 +15,9 @@ import { FourKeyMap } from 'common/MultiKeyMap'; import { IdleTaskQueue } from 'common/TaskQueue'; import { IBoundingBox, ICharAtlasConfig, IRasterizedGlyph, ITextureAtlas } from 'browser/renderer/shared/Types'; -// For debugging purposes, it can be useful to set this to a really tiny value, -// to verify that LRU eviction works. -const TEXTURE_WIDTH = 1024; -const TEXTURE_HEIGHT = 1024; +// For debugging purposes, it can be useful to set this to a really tiny value. +const TEXTURE_WIDTH = 512; +const TEXTURE_HEIGHT = 512; /** * The amount of the texture to be filled before throwing it away and starting @@ -63,36 +62,19 @@ export class TextureAtlas implements ITextureAtlas { // A temporary context that glyphs are drawn to before being transfered to the atlas. private _tmpCtx: CanvasRenderingContext2D; - // Texture atlas current positioning data. The texture packing strategy used is to fill from - // left-to-right and top-to-bottom. When the glyph being written is less than half of the current - // row's height, the following happens: - // - // - The current row becomes the fixed height row A - // - A new fixed height row B the exact size of the glyph is created below the current row - // - A new dynamic height current row is created below B - // - // This strategy does a good job preventing space being wasted for very short glyphs such as - // underscores, hyphens etc. or those with underlines rendered. - private _currentRow: ICharAtlasActiveRow = { - x: 0, - y: 0, - height: 0 - }; - private readonly _fixedRows: ICharAtlasActiveRow[] = []; - public hasCanvasChanged = false; private _workBoundingBox: IBoundingBox = { top: 0, left: 0, bottom: 0, right: 0 }; private _workAttributeData: AttributeData = new AttributeData(); constructor( - document: Document, + private readonly _document: Document, private readonly _config: ICharAtlasConfig, private readonly _unicodeService: IUnicodeService ) { - this._pages.push(new AtlasPage(document)); + this._pages.push(new AtlasPage(_document)); this._tmpCanvas = createCanvas( - document, + _document, this._config.deviceCellWidth * 4 + TMP_CANVAS_GLYPH_PADDING * 2, this._config.deviceCellHeight + TMP_CANVAS_GLYPH_PADDING * 2 ); @@ -129,16 +111,17 @@ export class TextureAtlas implements ITextureAtlas { } public beginFrame(): boolean { - if (this._currentRow.y > TEXTURE_CAPACITY) { - this.clearTexture(); - this.warmUp(); + if (this._pages[this._pages.length - 1].currentRow.y > TEXTURE_CAPACITY) { + // TODO: Support drawing to multiple pages at once + console.log(`Add page #${this._pages.length + 1}`); + this._pages.push(new AtlasPage(this._document)); return true; } return false; } public clearTexture(): void { - if (this._currentRow.x === 0 && this._currentRow.y === 0) { + if (this._pages[0].currentRow.x === 0 && this._pages[0].currentRow.y === 0) { return; } for (const page of this._pages) { @@ -146,10 +129,6 @@ export class TextureAtlas implements ITextureAtlas { } this._cacheMap.clear(); this._cacheMapCombined.clear(); - this._currentRow.x = 0; - this._currentRow.y = 0; - this._currentRow.height = 0; - this._fixedRows.length = 0; this._didWarmUp = false; this.hasCanvasChanged = true; } @@ -613,11 +592,12 @@ export class TextureAtlas implements ITextureAtlas { // Find the best atlas row to use let activeRow: ICharAtlasActiveRow; + const page = this._pages[this._pages.length - 1]; while (true) { // Select the ideal existing row, preferring fixed rows over the current row - activeRow = this._currentRow; - for (const row of this._fixedRows) { - if ((activeRow === this._currentRow || row.height < activeRow.height) && rasterizedGlyph.size.y <= row.height) { + activeRow = page.currentRow; + for (const row of page.fixedRows) { + if ((activeRow === page.currentRow || row.height < activeRow.height) && rasterizedGlyph.size.y <= row.height) { activeRow = row; } } @@ -626,20 +606,20 @@ export class TextureAtlas implements ITextureAtlas { // process as it now has a fixed height if (activeRow.height > rasterizedGlyph.size.y * 2) { // Fix the current row as the new row is being added below - if (this._currentRow.height > 0) { - this._fixedRows.push(this._currentRow); + if (page.currentRow.height > 0) { + page.fixedRows.push(page.currentRow); } // Create the new fixed height row activeRow = { x: 0, - y: this._currentRow.y + this._currentRow.height, + y: page.currentRow.y + page.currentRow.height, height: rasterizedGlyph.size.y }; - this._fixedRows.push(activeRow); + page.fixedRows.push(activeRow); // Create the new current row below the new fixed height row - this._currentRow = { + page.currentRow = { x: 0, y: activeRow.y + activeRow.height, height: 0 @@ -652,16 +632,17 @@ export class TextureAtlas implements ITextureAtlas { } // If there is enough room in the current row, finish it and try again - if (activeRow === this._currentRow) { + if (activeRow === page.currentRow) { activeRow.x = 0; activeRow.y += activeRow.height; activeRow.height = 0; } else { - this._fixedRows.splice(this._fixedRows.indexOf(activeRow), 1); + page.fixedRows.splice(page.fixedRows.indexOf(activeRow), 1); } } // Record texture position + rasterizedGlyph.texturePage = this._pages.length - 1; rasterizedGlyph.texturePosition.x = activeRow.x; rasterizedGlyph.texturePosition.y = activeRow.y; rasterizedGlyph.texturePositionClipSpace.x = activeRow.x / TEXTURE_WIDTH; @@ -673,7 +654,7 @@ export class TextureAtlas implements ITextureAtlas { activeRow.x += rasterizedGlyph.size.x; // putImageData doesn't do any blending, so it will overwrite any existing cache entry for us - this._pages[0].ctx.putImageData( + page.ctx.putImageData( imageData, rasterizedGlyph.texturePosition.x - this._workBoundingBox.left, rasterizedGlyph.texturePosition.y - this._workBoundingBox.top, @@ -757,7 +738,7 @@ export class TextureAtlas implements ITextureAtlas { } } return { - texturePage: 1, + texturePage: 0, texturePosition: { x: 0, y: 0 }, texturePositionClipSpace: { x: 0, y: 0 }, size: { @@ -780,6 +761,23 @@ class AtlasPage { public readonly canvas: HTMLCanvasElement; public readonly ctx: CanvasRenderingContext2D; + // Texture atlas current positioning data. The texture packing strategy used is to fill from + // left-to-right and top-to-bottom. When the glyph being written is less than half of the current + // row's height, the following happens: + // + // - The current row becomes the fixed height row A + // - A new fixed height row B the exact size of the glyph is created below the current row + // - A new dynamic height current row is created below B + // + // This strategy does a good job preventing space being wasted for very short glyphs such as + // underscores, hyphens etc. or those with underlines rendered. + public currentRow: ICharAtlasActiveRow = { + x: 0, + y: 0, + height: 0 + }; + public readonly fixedRows: ICharAtlasActiveRow[] = []; + constructor(document: Document) { this.canvas = createCanvas(document, TEXTURE_WIDTH, TEXTURE_HEIGHT); // The canvas needs alpha because we use clearColor to convert the background color to alpha. @@ -790,6 +788,10 @@ class AtlasPage { public clear(): void { this.ctx.clearRect(0, 0, TEXTURE_WIDTH, TEXTURE_HEIGHT); + this.currentRow.x = 0; + this.currentRow.y = 0; + this.currentRow.height = 0; + this.fixedRows.length = 0; } } From efba2c9040c1ce0490f65c7bc3d474ea46fdba42 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 28 Oct 2022 19:30:42 -0700 Subject: [PATCH 06/28] Get pages working, add to demo --- addons/xterm-addon-webgl/src/GlyphRenderer.ts | 48 +++++++++---------- addons/xterm-addon-webgl/src/WebglAddon.ts | 5 +- addons/xterm-addon-webgl/src/WebglRenderer.ts | 6 ++- .../typings/xterm-addon-webgl.d.ts | 3 ++ demo/client.ts | 18 ++++--- src/browser/renderer/shared/TextureAtlas.ts | 9 +++- src/browser/renderer/shared/Types.d.ts | 2 + 7 files changed, 57 insertions(+), 34 deletions(-) diff --git a/addons/xterm-addon-webgl/src/GlyphRenderer.ts b/addons/xterm-addon-webgl/src/GlyphRenderer.ts index 823a0db118..54afb7a772 100644 --- a/addons/xterm-addon-webgl/src/GlyphRenderer.ts +++ b/addons/xterm-addon-webgl/src/GlyphRenderer.ts @@ -62,7 +62,7 @@ precision lowp float; in vec2 v_texcoord; flat in int v_texpage; -uniform sampler2D u_texture[2]; +uniform sampler2D u_texture[8]; out vec4 outColor; @@ -71,6 +71,18 @@ void main() { outColor = texture(u_texture[0], v_texcoord); } else if (v_texpage == 1) { outColor = texture(u_texture[1], v_texcoord); + } else if (v_texpage == 2) { + outColor = texture(u_texture[2], v_texcoord); + } else if (v_texpage == 3) { + outColor = texture(u_texture[3], v_texcoord); + } else if (v_texpage == 4) { + outColor = texture(u_texture[4], v_texcoord); + } else if (v_texpage == 5) { + outColor = texture(u_texture[5], v_texcoord); + } else if (v_texpage == 6) { + outColor = texture(u_texture[6], v_texcoord); + } else if (v_texpage == 7) { + outColor = texture(u_texture[7], v_texcoord); } }`; @@ -327,23 +339,17 @@ export class GlyphRenderer extends Disposable { if (this._atlas.hasCanvasChanged) { this._atlas.hasCanvasChanged = false; // TODO: Make nicer - const layerTextureUnits = new Int32Array([0, 1]); + const layerTextureUnits = new Int32Array([0, 1, 2, 3, 4, 5, 6, 7]); gl.uniform1iv(this._textureLocation, layerTextureUnits); - gl.activeTexture(gl.TEXTURE0 + 0); - gl.bindTexture(gl.TEXTURE_2D, this._atlasTextures[0]); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this._atlas.pages[0].canvas); - gl.generateMipmap(gl.TEXTURE_2D); - - if (this._atlas.pages.length > 1) { - // TODO: Check if the particular texture page changed - gl.activeTexture(gl.TEXTURE0 + 1); - gl.bindTexture(gl.TEXTURE_2D, this._atlasTextures[1]); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this._atlas.pages[1].canvas); + // TODO: Only upload the texture(s) that changed + for (let i = 0; i < this._atlas.pages.length; i++) { + gl.activeTexture(gl.TEXTURE0 + i); + gl.bindTexture(gl.TEXTURE_2D, this._atlasTextures[i]); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this._atlas.pages[i].canvas); gl.generateMipmap(gl.TEXTURE_2D); } } - // Set uniforms gl.uniformMatrix4fv(this._projectionLocation, false, PROJECTION_MATRIX); gl.uniform2f(this._resolutionLocation, gl.canvas.width, gl.canvas.height); @@ -356,19 +362,13 @@ export class GlyphRenderer extends Disposable { const gl = this._gl; this._atlas = atlas; - gl.activeTexture(gl.TEXTURE0); - gl.bindTexture(gl.TEXTURE_2D, this._atlasTextures[0]); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, atlas.pages[0].canvas); - gl.generateMipmap(gl.TEXTURE_2D); - - if (atlas.pages.length > 1) { - gl.activeTexture(gl.TEXTURE0 + 1); - gl.bindTexture(gl.TEXTURE_2D, this._atlasTextures[1]); + // TODO: Share code + for (let i = 0; i < this._atlas.pages.length; i++) { + gl.activeTexture(gl.TEXTURE0 + i); + gl.bindTexture(gl.TEXTURE_2D, this._atlasTextures[i]); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, atlas.pages[1].canvas); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this._atlas.pages[i].canvas); gl.generateMipmap(gl.TEXTURE_2D); } } diff --git a/addons/xterm-addon-webgl/src/WebglAddon.ts b/addons/xterm-addon-webgl/src/WebglAddon.ts index 4f4c9dcc97..5262da8e58 100644 --- a/addons/xterm-addon-webgl/src/WebglAddon.ts +++ b/addons/xterm-addon-webgl/src/WebglAddon.ts @@ -17,8 +17,10 @@ export class WebglAddon extends Disposable implements ITerminalAddon { private _terminal?: Terminal; private _renderer?: WebglRenderer; - private readonly _onChangeTextureAtlas = this.register(new EventEmitter()); + private readonly _onChangeTextureAtlas = this.register(new EventEmitter()); public readonly onChangeTextureAtlas = this._onChangeTextureAtlas.event; + private readonly _onAddTextureAtlasCanvas = this.register(new EventEmitter()); + public readonly onAddTextureAtlasCanvas = this._onAddTextureAtlasCanvas.event; private readonly _onContextLoss = this.register(new EventEmitter()); public readonly onContextLoss = this._onContextLoss.event; @@ -64,6 +66,7 @@ export class WebglAddon extends Disposable implements ITerminalAddon { )); this.register(forwardEvent(this._renderer.onContextLoss, this._onContextLoss)); this.register(forwardEvent(this._renderer.onChangeTextureAtlas, this._onChangeTextureAtlas)); + this.register(forwardEvent(this._renderer.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas)); renderService.setRenderer(this._renderer); this.register(toDisposable(() => { diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index 35542bcf7a..636be672d4 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -14,7 +14,7 @@ import { ITerminal } from 'browser/Types'; import { AttributeData } from 'common/buffer/AttributeData'; import { CellData } from 'common/buffer/CellData'; import { Content, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants'; -import { EventEmitter } from 'common/EventEmitter'; +import { EventEmitter, forwardEvent } from 'common/EventEmitter'; import { Disposable, toDisposable } from 'common/Lifecycle'; import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services'; import { CharData, IBufferLine, ICellData } from 'common/Types'; @@ -49,6 +49,8 @@ export class WebglRenderer extends Disposable implements IRenderer { private readonly _onChangeTextureAtlas = this.register(new EventEmitter()); public readonly onChangeTextureAtlas = this._onChangeTextureAtlas.event; + private readonly _onAddTextureAtlasCanvas = this.register(new EventEmitter()); + public readonly onAddTextureAtlasCanvas = this._onAddTextureAtlasCanvas.event; private readonly _onRequestRedraw = this.register(new EventEmitter()); public readonly onRequestRedraw = this._onRequestRedraw.event; private readonly _onContextLoss = this.register(new EventEmitter()); @@ -262,6 +264,8 @@ export class WebglRenderer extends Disposable implements IRenderer { ); if (this._charAtlas !== atlas) { this._onChangeTextureAtlas.fire(atlas.pages[0].canvas); + // TODO: Dispose this when there's a new atlas + forwardEvent(atlas.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas); } this._charAtlas = atlas; this._charAtlas.warmUp(); diff --git a/addons/xterm-addon-webgl/typings/xterm-addon-webgl.d.ts b/addons/xterm-addon-webgl/typings/xterm-addon-webgl.d.ts index 6865b6dbed..a6afd2ac69 100644 --- a/addons/xterm-addon-webgl/typings/xterm-addon-webgl.d.ts +++ b/addons/xterm-addon-webgl/typings/xterm-addon-webgl.d.ts @@ -22,6 +22,9 @@ declare module 'xterm-addon-webgl' { */ public readonly onChangeTextureAtlas: IEvent; + // TODO: Doc + public readonly onAddTextureAtlasCanvas: IEvent; + constructor(preserveDrawingBuffer?: boolean); /** diff --git a/demo/client.ts b/demo/client.ts index 5b561a8c97..3829b6dd44 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -273,8 +273,9 @@ function createTerminal(): void { typedTerm.loadAddon(addons.webgl.instance); setTimeout(() => { if (addons.webgl.instance !== undefined) { - addTextureAtlas(addons.webgl.instance.textureAtlas); - addons.webgl.instance.onChangeTextureAtlas(e => addTextureAtlas(e)); + setTextureAtlas(addons.webgl.instance.textureAtlas); + addons.webgl.instance.onChangeTextureAtlas(e => setTextureAtlas(e)); + addons.webgl.instance.onAddTextureAtlasCanvas(e => appendTextureAtlas(e)); } }, 0); @@ -551,13 +552,13 @@ function initAddons(term: TerminalType): void { term.loadAddon(addon.instance); if (name === 'webgl') { setTimeout(() => { - addTextureAtlas(addons.webgl.instance.textureAtlas); - addons.webgl.instance.onChangeTextureAtlas(e => addTextureAtlas(e)); + setTextureAtlas(addons.webgl.instance.textureAtlas); + addons.webgl.instance.onChangeTextureAtlas(e => setTextureAtlas(e)); }, 0); } else if (name === 'canvas') { setTimeout(() => { - addTextureAtlas(addons.canvas.instance.textureAtlas); - addons.canvas.instance.onChangeTextureAtlas(e => addTextureAtlas(e)); + setTextureAtlas(addons.canvas.instance.textureAtlas); + addons.canvas.instance.onChangeTextureAtlas(e => setTextureAtlas(e)); }, 0); } else if (name === 'unicode11') { term.unicode.activeVersion = '11'; @@ -648,9 +649,12 @@ function htmlSerializeButtonHandler(): void { document.getElementById('htmlserialize-output-result').innerText = 'Copied to clipboard'; } -function addTextureAtlas(e: HTMLCanvasElement): void { +function setTextureAtlas(e: HTMLCanvasElement): void { document.querySelector('#texture-atlas').replaceChildren(e); } +function appendTextureAtlas(e: HTMLCanvasElement): void { + document.querySelector('#texture-atlas').appendChild(e); +} function writeCustomGlyphHandler(): void { term.write('\n\r'); diff --git a/src/browser/renderer/shared/TextureAtlas.ts b/src/browser/renderer/shared/TextureAtlas.ts index e2d5d6170b..5b7f7eb28e 100644 --- a/src/browser/renderer/shared/TextureAtlas.ts +++ b/src/browser/renderer/shared/TextureAtlas.ts @@ -14,6 +14,7 @@ import { IUnicodeService } from 'common/services/Services'; import { FourKeyMap } from 'common/MultiKeyMap'; import { IdleTaskQueue } from 'common/TaskQueue'; import { IBoundingBox, ICharAtlasConfig, IRasterizedGlyph, ITextureAtlas } from 'browser/renderer/shared/Types'; +import { EventEmitter } from 'common/EventEmitter'; // For debugging purposes, it can be useful to set this to a really tiny value. const TEXTURE_WIDTH = 512; @@ -67,6 +68,10 @@ export class TextureAtlas implements ITextureAtlas { private _workBoundingBox: IBoundingBox = { top: 0, left: 0, bottom: 0, right: 0 }; private _workAttributeData: AttributeData = new AttributeData(); + // TODO: Register + private readonly _onAddTextureAtlasCanvas = new EventEmitter(); + public readonly onAddTextureAtlasCanvas = this._onAddTextureAtlasCanvas.event; + constructor( private readonly _document: Document, private readonly _config: ICharAtlasConfig, @@ -114,7 +119,9 @@ export class TextureAtlas implements ITextureAtlas { if (this._pages[this._pages.length - 1].currentRow.y > TEXTURE_CAPACITY) { // TODO: Support drawing to multiple pages at once console.log(`Add page #${this._pages.length + 1}`); - this._pages.push(new AtlasPage(this._document)); + const newPage = new AtlasPage(this._document); + this._pages.push(newPage); + this._onAddTextureAtlasCanvas.fire(newPage.canvas); return true; } return false; diff --git a/src/browser/renderer/shared/Types.d.ts b/src/browser/renderer/shared/Types.d.ts index 2eaac493d8..a622438baa 100644 --- a/src/browser/renderer/shared/Types.d.ts +++ b/src/browser/renderer/shared/Types.d.ts @@ -91,6 +91,8 @@ export interface ITextureAtlas extends IDisposable { hasCanvasChanged: boolean; + onAddTextureAtlasCanvas: IEvent; + /** * Warm up the texture atlas, adding common glyphs to avoid slowing early frame. */ From 713baeb64db3a0a098f4cbe0202cb46783a0d165 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 29 Oct 2022 12:45:20 -0700 Subject: [PATCH 07/28] Base atlas page size on device pixel ratio --- src/browser/renderer/shared/TextureAtlas.ts | 44 ++++++++++----------- 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/src/browser/renderer/shared/TextureAtlas.ts b/src/browser/renderer/shared/TextureAtlas.ts index 5b7f7eb28e..1bb72c3e4f 100644 --- a/src/browser/renderer/shared/TextureAtlas.ts +++ b/src/browser/renderer/shared/TextureAtlas.ts @@ -16,16 +16,6 @@ import { IdleTaskQueue } from 'common/TaskQueue'; import { IBoundingBox, ICharAtlasConfig, IRasterizedGlyph, ITextureAtlas } from 'browser/renderer/shared/Types'; import { EventEmitter } from 'common/EventEmitter'; -// For debugging purposes, it can be useful to set this to a really tiny value. -const TEXTURE_WIDTH = 512; -const TEXTURE_HEIGHT = 512; - -/** - * The amount of the texture to be filled before throwing it away and starting - * again. Since the throw away and individual glyph draws don't cost too much, - * this prevent juggling multiple textures in the GL context. - */ -const TEXTURE_CAPACITY = Math.floor(TEXTURE_HEIGHT * 0.8); /** * A shared object which is used to draw nothing for a particular cell. */ @@ -77,7 +67,7 @@ export class TextureAtlas implements ITextureAtlas { private readonly _config: ICharAtlasConfig, private readonly _unicodeService: IUnicodeService ) { - this._pages.push(new AtlasPage(_document)); + this._pages.push(new AtlasPage(_document, _config.devicePixelRatio)); this._tmpCanvas = createCanvas( _document, this._config.deviceCellWidth * 4 + TMP_CANVAS_GLYPH_PADDING * 2, @@ -116,10 +106,12 @@ export class TextureAtlas implements ITextureAtlas { } public beginFrame(): boolean { - if (this._pages[this._pages.length - 1].currentRow.y > TEXTURE_CAPACITY) { + const page = this._pages[this._pages.length - 1]; + // TODO: Fill the page completely + if (page.currentRow.y > Math.floor(page.canvas.height * 0.8)) { // TODO: Support drawing to multiple pages at once console.log(`Add page #${this._pages.length + 1}`); - const newPage = new AtlasPage(this._document); + const newPage = new AtlasPage(this._document, this._config.devicePixelRatio); this._pages.push(newPage); this._onAddTextureAtlasCanvas.fire(newPage.canvas); return true; @@ -595,11 +587,11 @@ export class TextureAtlas implements ITextureAtlas { return NULL_RASTERIZED_GLYPH; } - const rasterizedGlyph = this._findGlyphBoundingBox(imageData, this._workBoundingBox, allowedWidth, restrictedPowerlineGlyph, customGlyph, padding); + const page = this._pages[this._pages.length - 1]; + const rasterizedGlyph = this._findGlyphBoundingBox(imageData, this._workBoundingBox, allowedWidth, restrictedPowerlineGlyph, customGlyph, padding, page.canvas.width, page.canvas.height); // Find the best atlas row to use let activeRow: ICharAtlasActiveRow; - const page = this._pages[this._pages.length - 1]; while (true) { // Select the ideal existing row, preferring fixed rows over the current row activeRow = page.currentRow; @@ -634,7 +626,7 @@ export class TextureAtlas implements ITextureAtlas { } // Exit the loop if there is enough room in the row - if (activeRow.x + rasterizedGlyph.size.x <= TEXTURE_WIDTH) { + if (activeRow.x + rasterizedGlyph.size.x <= page.canvas.width) { break; } @@ -652,8 +644,8 @@ export class TextureAtlas implements ITextureAtlas { rasterizedGlyph.texturePage = this._pages.length - 1; rasterizedGlyph.texturePosition.x = activeRow.x; rasterizedGlyph.texturePosition.y = activeRow.y; - rasterizedGlyph.texturePositionClipSpace.x = activeRow.x / TEXTURE_WIDTH; - rasterizedGlyph.texturePositionClipSpace.y = activeRow.y / TEXTURE_HEIGHT; + rasterizedGlyph.texturePositionClipSpace.x = activeRow.x / page.canvas.width; + rasterizedGlyph.texturePositionClipSpace.y = activeRow.y / page.canvas.height; // Update atlas current row, for fixed rows the glyph height will never be larger than the row // height @@ -681,7 +673,7 @@ export class TextureAtlas implements ITextureAtlas { * @param imageData The image data to read. * @param boundingBox An IBoundingBox to put the clipped bounding box values. */ - private _findGlyphBoundingBox(imageData: ImageData, boundingBox: IBoundingBox, allowedWidth: number, restrictedGlyph: boolean, customGlyph: boolean, padding: number): IRasterizedGlyph { + private _findGlyphBoundingBox(imageData: ImageData, boundingBox: IBoundingBox, allowedWidth: number, restrictedGlyph: boolean, customGlyph: boolean, padding: number, pageWidth: number, pageHeight: number): IRasterizedGlyph { boundingBox.top = 0; const height = restrictedGlyph ? this._config.deviceCellHeight : this._tmpCanvas.height; const width = restrictedGlyph ? this._config.deviceCellWidth : allowedWidth; @@ -753,8 +745,8 @@ export class TextureAtlas implements ITextureAtlas { y: boundingBox.bottom - boundingBox.top + 1 }, sizeClipSpace: { - x: (boundingBox.right - boundingBox.left + 1) / TEXTURE_WIDTH, - y: (boundingBox.bottom - boundingBox.top + 1) / TEXTURE_HEIGHT + x: (boundingBox.right - boundingBox.left + 1) / pageWidth, + y: (boundingBox.bottom - boundingBox.top + 1) / pageHeight }, offset: { x: -boundingBox.left + padding + ((restrictedGlyph || customGlyph) ? Math.floor((this._config.deviceCellWidth - this._config.deviceCharWidth) / 2) : 0), @@ -785,8 +777,12 @@ class AtlasPage { }; public readonly fixedRows: ICharAtlasActiveRow[] = []; - constructor(document: Document) { - this.canvas = createCanvas(document, TEXTURE_WIDTH, TEXTURE_HEIGHT); + constructor( + document: Document, + dpr: number + ) { + const size = Math.pow(2, 8 + Math.max(1, dpr)); + this.canvas = createCanvas(document, size, size); // The canvas needs alpha because we use clearColor to convert the background color to alpha. // It might also contain some characters with transparent backgrounds if allowTransparency is // set. @@ -794,7 +790,7 @@ class AtlasPage { } public clear(): void { - this.ctx.clearRect(0, 0, TEXTURE_WIDTH, TEXTURE_HEIGHT); + this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); this.currentRow.x = 0; this.currentRow.y = 0; this.currentRow.height = 0; From c6e3cc007cd024506321def1523d65ef265025eb Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 29 Oct 2022 12:49:11 -0700 Subject: [PATCH 08/28] Stop atlas flickering on demo --- demo/client.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/demo/client.ts b/demo/client.ts index 3829b6dd44..3c8a291988 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-restricted-syntax */ /** * Copyright (c) 2018 The xterm.js authors. All rights reserved. * @license MIT @@ -650,11 +651,17 @@ function htmlSerializeButtonHandler(): void { } function setTextureAtlas(e: HTMLCanvasElement): void { + styleAtlasPage(e); document.querySelector('#texture-atlas').replaceChildren(e); } function appendTextureAtlas(e: HTMLCanvasElement): void { + styleAtlasPage(e); document.querySelector('#texture-atlas').appendChild(e); } +function styleAtlasPage(e: HTMLCanvasElement): void { + e.style.width = `${e.width / window.devicePixelRatio}px`; + e.style.height = `${e.height / window.devicePixelRatio}px`; +} function writeCustomGlyphHandler(): void { term.write('\n\r'); From 0a49a5dde1afd433ce354de06bb6e7f54a55089f Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 29 Oct 2022 13:01:41 -0700 Subject: [PATCH 09/28] Make texture atlas presentation nicer for multiple pages --- demo/index.html | 2 ++ demo/style.css | 12 +++++------- src/browser/renderer/shared/TextureAtlas.ts | 1 + 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/demo/index.html b/demo/index.html index a65b8bbc2d..446d1d5bff 100644 --- a/demo/index.html +++ b/demo/index.html @@ -91,6 +91,8 @@

Test

+ +