diff --git a/addons/xterm-addon-canvas/src/BaseRenderLayer.ts b/addons/xterm-addon-canvas/src/BaseRenderLayer.ts index b037d61e63..3d16467d95 100644 --- a/addons/xterm-addon-canvas/src/BaseRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/BaseRenderLayer.ts @@ -15,12 +15,13 @@ import { ReadonlyColorSet } from 'browser/Types'; import { CellData } from 'common/buffer/CellData'; import { WHITESPACE_CELL_CODE } from 'common/buffer/Constants'; import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services'; -import { ICellData } from 'common/Types'; +import { ICellData, IDisposable } from 'common/Types'; import { Terminal } from 'xterm'; import { IRenderLayer } from './Types'; import { CellColorResolver } from 'browser/renderer/shared/CellColorResolver'; import { Disposable, toDisposable } from 'common/Lifecycle'; import { isSafari } from 'common/Platform'; +import { EventEmitter, forwardEvent } from 'common/EventEmitter'; export abstract class BaseRenderLayer extends Disposable implements IRenderLayer { private _canvas: HTMLCanvasElement; @@ -34,12 +35,16 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer protected _selectionModel: ISelectionRenderModel = createSelectionRenderModel(); private _cellColorResolver: CellColorResolver; - private _bitmapGenerator?: BitmapGenerator; + private _bitmapGenerator: (BitmapGenerator | undefined)[] = []; protected _charAtlas!: ITextureAtlas; + private _charAtlasDisposable?: IDisposable; public get canvas(): HTMLCanvasElement { return this._canvas; } - public get cacheCanvas(): HTMLCanvasElement { return this._charAtlas?.cacheCanvas!; } + public get cacheCanvas(): HTMLCanvasElement { return this._charAtlas?.pages[0].canvas!; } + + private readonly _onAddTextureAtlasCanvas = this.register(new EventEmitter()); + public readonly onAddTextureAtlasCanvas = this._onAddTextureAtlasCanvas.event; constructor( private readonly _terminal: Terminal, @@ -116,9 +121,13 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer if (this._deviceCharWidth <= 0 && this._deviceCharHeight <= 0) { return; } + this._charAtlasDisposable?.dispose(); this._charAtlas = acquireTextureAtlas(this._terminal, colorSet, this._deviceCellWidth, this._deviceCellHeight, this._deviceCharWidth, this._deviceCharHeight, this._coreBrowserService.dpr); + this._charAtlasDisposable = forwardEvent(this._charAtlas.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas); this._charAtlas.warmUp(); - this._bitmapGenerator = new BitmapGenerator(this._charAtlas.cacheCanvas); + for (let i = 0; i < this._charAtlas.pages.length; i++) { + this._bitmapGenerator[i] = new BitmapGenerator(this._charAtlas.pages[i].canvas); + } } public resize(dim: IRenderDimensions): void { @@ -367,12 +376,15 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer this._ctx.save(); this._clipRow(y); // Draw the image, use the bitmap if it's available - if (this._charAtlas.hasCanvasChanged) { - this._bitmapGenerator?.refresh(); - this._charAtlas.hasCanvasChanged = false; + if (this._charAtlas.pages[glyph.texturePage].hasCanvasChanged) { + if (!this._bitmapGenerator[glyph.texturePage]) { + this._bitmapGenerator[glyph.texturePage] = new BitmapGenerator(this._charAtlas.pages[glyph.texturePage].canvas); + } + this._bitmapGenerator[glyph.texturePage]?.refresh(); + this._charAtlas.pages[glyph.texturePage].hasCanvasChanged = false; } this._ctx.drawImage( - this._bitmapGenerator?.bitmap || this._charAtlas!.cacheCanvas, + this._bitmapGenerator[glyph.texturePage]?.bitmap || this._charAtlas!.pages[glyph.texturePage].canvas, glyph.texturePosition.x, glyph.texturePosition.y, glyph.size.x, diff --git a/addons/xterm-addon-canvas/src/CanvasAddon.ts b/addons/xterm-addon-canvas/src/CanvasAddon.ts index e39b56f139..2a13c95eb2 100644 --- a/addons/xterm-addon-canvas/src/CanvasAddon.ts +++ b/addons/xterm-addon-canvas/src/CanvasAddon.ts @@ -17,6 +17,8 @@ export class CanvasAddon extends Disposable implements ITerminalAddon { 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; public get textureAtlas(): HTMLCanvasElement | undefined { return this._renderer?.textureAtlas; @@ -46,6 +48,7 @@ export class CanvasAddon extends Disposable implements ITerminalAddon { this._renderer = new CanvasRenderer(terminal, screenElement, linkifier, bufferService, charSizeService, optionsService, characterJoinerService, coreService, coreBrowserService, decorationService, themeService); this.register(forwardEvent(this._renderer.onChangeTextureAtlas, this._onChangeTextureAtlas)); + this.register(forwardEvent(this._renderer.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas)); renderService.setRenderer(this._renderer); renderService.handleResize(bufferService.cols, bufferService.rows); diff --git a/addons/xterm-addon-canvas/src/CanvasRenderer.ts b/addons/xterm-addon-canvas/src/CanvasRenderer.ts index d4563c5c3a..090d0e09bf 100644 --- a/addons/xterm-addon-canvas/src/CanvasRenderer.ts +++ b/addons/xterm-addon-canvas/src/CanvasRenderer.ts @@ -9,7 +9,7 @@ import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils'; import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/shared/Types'; import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IThemeService } from 'browser/services/Services'; import { ILinkifier2 } from 'browser/Types'; -import { EventEmitter } from 'common/EventEmitter'; +import { EventEmitter, forwardEvent } from 'common/EventEmitter'; import { Disposable, toDisposable } from 'common/Lifecycle'; import { IBufferService, ICoreService, IDecorationService, IOptionsService } from 'common/services/Services'; import { Terminal } from 'xterm'; @@ -29,6 +29,8 @@ export class CanvasRenderer extends Disposable implements IRenderer { public readonly onRequestRedraw = this._onRequestRedraw.event; 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; constructor( private readonly _terminal: Terminal, @@ -51,6 +53,9 @@ export class CanvasRenderer extends Disposable implements IRenderer { new LinkRenderLayer(this._terminal, this._screenElement, 2, linkifier2, this._bufferService, this._optionsService, decorationService, this._coreBrowserService, _themeService), new CursorRenderLayer(this._terminal, this._screenElement, 3, this._onRequestRedraw, this._bufferService, this._optionsService, coreService, this._coreBrowserService, decorationService, _themeService) ]; + for (const layer of this._renderLayers) { + forwardEvent(layer.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas); + } this.dimensions = createRenderDimensions(); this._devicePixelRatio = this._coreBrowserService.dpr; this._updateDimensions(); diff --git a/addons/xterm-addon-canvas/src/Types.d.ts b/addons/xterm-addon-canvas/src/Types.d.ts index 7e582535ce..73e6c836de 100644 --- a/addons/xterm-addon-canvas/src/Types.d.ts +++ b/addons/xterm-addon-canvas/src/Types.d.ts @@ -42,6 +42,7 @@ export interface IRenderLayer extends IDisposable { readonly canvas: HTMLCanvasElement; readonly cacheCanvas: HTMLCanvasElement; + readonly onAddTextureAtlasCanvas: IEvent; /** * Called when the terminal loses focus. */ diff --git a/addons/xterm-addon-canvas/typings/xterm-addon-canvas.d.ts b/addons/xterm-addon-canvas/typings/xterm-addon-canvas.d.ts index 6a2b98d4a3..c983825c27 100644 --- a/addons/xterm-addon-canvas/typings/xterm-addon-canvas.d.ts +++ b/addons/xterm-addon-canvas/typings/xterm-addon-canvas.d.ts @@ -17,6 +17,11 @@ declare module 'xterm-addon-canvas' { */ public readonly onChangeTextureAtlas: IEvent; + /** + * An event that is fired when the a new page is added to the texture atlas. + */ + public readonly onAddTextureAtlasCanvas: IEvent; + constructor(); /** diff --git a/addons/xterm-addon-webgl/src/GlyphRenderer.ts b/addons/xterm-addon-webgl/src/GlyphRenderer.ts index 4bce546ac6..f5ce46eb6b 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,27 +47,38 @@ 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; }`; -const fragmentShaderSource = `#version 300 es +function createFragmentShaderSource(maxFragmentShaderTextureUnits: number): string { + let textureConditionals = ''; + for (let i = 1; i < maxFragmentShaderTextureUnits; i++) { + textureConditionals += ` else if (v_texpage == ${i}) { outColor = texture(u_texture[${i}], v_texcoord); }`; + } + return (`#version 300 es precision lowp float; in vec2 v_texcoord; +flat in int v_texpage; -uniform sampler2D u_texture; +uniform sampler2D u_texture[${maxFragmentShaderTextureUnits}]; out vec4 outColor; void main() { - outColor = texture(u_texture, v_texcoord); -}`; + if (v_texpage == 0) { + outColor = texture(u_texture[0], v_texcoord); + } ${textureConditionals} +}`); +} -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; @@ -77,18 +89,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 _atlasTextures: WebGLTexture[]; + private readonly _attributesBuffer: WebGLBuffer; - private _program: WebGLProgram; - private _vertexArrayObject: IWebGLVertexArrayObject; - private _projectionLocation: WebGLUniformLocation; - private _resolutionLocation: WebGLUniformLocation; - private _textureLocation: WebGLUniformLocation; - private _atlasTexture: WebGLTexture; - private _attributesBuffer: WebGLBuffer; + private _atlas: ITextureAtlas | undefined; private _activeBuffer: number = 0; - - private _vertices: IVertices = { + private readonly _vertices: IVertices = { count: 0, attributes: new Float32Array(0), attributesBuffers: [ @@ -97,15 +108,22 @@ export class GlyphRenderer extends Disposable { ] }; + private static _maxAtlasPages: number | undefined; + constructor( - private _terminal: Terminal, - private _gl: IWebGL2RenderingContext, + private readonly _terminal: Terminal, + private readonly _gl: IWebGL2RenderingContext, private _dimensions: IRenderDimensions ) { super(); const gl = this._gl; - this._program = throwIfFalsy(createProgram(gl, vertexShaderSource, fragmentShaderSource)); + + if (GlyphRenderer._maxAtlasPages === undefined) { + GlyphRenderer._maxAtlasPages = throwIfFalsy(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS) as number | null); + } + + this._program = throwIfFalsy(createProgram(gl, vertexShaderSource, createFragmentShaderSource(GlyphRenderer._maxAtlasPages))); this.register(toDisposable(() => gl.deleteProgram(this._program))); // Uniform locations @@ -145,23 +163,41 @@ 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.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); + // Setup static uniforms + gl.useProgram(this._program); + const textureUnits = new Int32Array(GlyphRenderer._maxAtlasPages); + for (let i = 0; i < GlyphRenderer._maxAtlasPages; i++) { + textureUnits[i] = i; + } + gl.uniform1iv(this._textureLocation, textureUnits); + gl.uniformMatrix4fv(this._projectionLocation, false, PROJECTION_MATRIX); + + // Setup 1x1 red pixel textures for all potential atlas pages, if one of these invalid textures + // is ever drawn it will show characters as red rectangles. + this._atlasTextures = []; + for (let i = 0; i < GlyphRenderer._maxAtlasPages; 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); @@ -213,12 +249,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] = $glyph.texturePage; // 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.pages[$glyph.texturePage].canvas.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.pages[$glyph.texturePage].canvas.width; + array[$i + 8] = $glyph.sizeClipSpace.y; } else { // a_origin array[$i ] = -$glyph.offset.x + this._dimensions.device.char.left; @@ -226,12 +264,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] = $glyph.texturePage; // 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 } @@ -246,7 +286,8 @@ export class GlyphRenderer extends Disposable { } else { this._vertices.attributes.fill(0); } - for (let i = 0; i < this._vertices.attributesBuffers.length; i++) { + let i = 0; + for (; i < this._vertices.attributesBuffers.length; i++) { if (this._vertices.count !== newCount) { this._vertices.attributesBuffers[i] = new Float32Array(newCount); } else { @@ -254,11 +295,11 @@ export class GlyphRenderer extends Disposable { } } this._vertices.count = newCount; - let i = 0; + 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; } } @@ -267,6 +308,7 @@ export class GlyphRenderer extends Disposable { public handleResize(): void { const gl = this._gl; gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); + gl.uniform2f(this._resolutionLocation, gl.canvas.width, gl.canvas.height); this.clear(); } @@ -303,30 +345,31 @@ export class GlyphRenderer extends Disposable { gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer); gl.bufferData(gl.ARRAY_BUFFER, activeBuffer.subarray(0, bufferLength), gl.STREAM_DRAW); - // Bind the texture atlas if it's changed - if (this._atlas.hasCanvasChanged) { - this._atlas.hasCanvasChanged = false; - 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); - gl.generateMipmap(gl.TEXTURE_2D); + // Bind the atlas page texture if they have changed + for (let i = 0; i < this._atlas.pages.length; i++) { + if (this._atlas.pages[i].hasCanvasChanged) { + this._atlas.pages[i].hasCanvasChanged = false; + this._bindAtlasPageTexture(gl, this._atlas, i); + } } - // Set uniforms - gl.uniformMatrix4fv(this._projectionLocation, false, PROJECTION_MATRIX); - gl.uniform2f(this._resolutionLocation, gl.canvas.width, gl.canvas.height); - // Draw the viewport gl.drawElementsInstanced(gl.TRIANGLE_STRIP, 4, gl.UNSIGNED_BYTE, 0, bufferLength / INDICES_PER_CELL); } public setAtlas(atlas: ITextureAtlas): void { - const gl = this._gl; this._atlas = atlas; + for (let i = 0; i < atlas.pages.length; i++) { + this._bindAtlasPageTexture(this._gl, atlas, i); + } + } - gl.bindTexture(gl.TEXTURE_2D, this._atlasTexture); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, atlas.cacheCanvas); + private _bindAtlasPageTexture(gl: IWebGL2RenderingContext, atlas: ITextureAtlas, i: number): void { + 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[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 f366c37945..8f039033bc 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -7,18 +7,19 @@ import { addDisposableDomListener } from 'browser/Lifecycle'; import { CellColorResolver } from 'browser/renderer/shared/CellColorResolver'; import { acquireTextureAtlas, removeTerminalFromCache } from 'browser/renderer/shared/CharAtlasCache'; import { observeDevicePixelDimensions } from 'browser/renderer/shared/DevicePixelObserver'; -import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils'; +import { createRenderDimensions, throwIfFalsy } from 'browser/renderer/shared/RendererUtils'; +import { TextureAtlas } from 'browser/renderer/shared/TextureAtlas'; import { IRenderDimensions, IRenderer, IRequestRedrawEvent, ITextureAtlas } from 'browser/renderer/shared/Types'; import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IThemeService } from 'browser/services/Services'; 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'; -import { Terminal } from 'xterm'; +import { IDisposable, Terminal } from 'xterm'; import { GlyphRenderer } from './GlyphRenderer'; import { RectangleRenderer } from './RectangleRenderer'; import { CursorRenderLayer } from './renderLayer/CursorRenderLayer'; @@ -29,6 +30,7 @@ import { IWebGL2RenderingContext } from './Types'; export class WebglRenderer extends Disposable implements IRenderer { private _renderLayers: IRenderLayer[]; + private _charAtlasDisposable: IDisposable | undefined; private _charAtlas: ITextureAtlas | undefined; private _devicePixelRatio: number; @@ -49,6 +51,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()); @@ -135,7 +139,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 +265,10 @@ export class WebglRenderer extends Disposable implements IRenderer { this._coreBrowserService.dpr ); if (this._charAtlas !== atlas) { - this._onChangeTextureAtlas.fire(atlas.cacheCanvas); + + this._charAtlasDisposable?.dispose(); + this._onChangeTextureAtlas.fire(atlas.pages[0].canvas); + this._charAtlasDisposable = 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..79108e0de0 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,11 @@ declare module 'xterm-addon-webgl' { */ public readonly onChangeTextureAtlas: IEvent; + /** + * An event that is fired when the a new page is added to the texture atlas. + */ + public readonly onAddTextureAtlasCanvas: IEvent; + constructor(preserveDrawingBuffer?: boolean); /** diff --git a/demo/client.ts b/demo/client.ts index 5b561a8c97..033ea41b48 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 @@ -218,6 +219,7 @@ if (document.location.pathname === '/test') { document.getElementById('htmlserialize').addEventListener('click', htmlSerializeButtonHandler); document.getElementById('custom-glyph').addEventListener('click', writeCustomGlyphHandler); document.getElementById('load-test').addEventListener('click', loadTest); + document.getElementById('print-cjk').addEventListener('click', addCjk); document.getElementById('powerline-symbol-test').addEventListener('click', powerlineSymbolTest); document.getElementById('underline-test').addEventListener('click', underlineTest); document.getElementById('ansi-colors').addEventListener('click', ansiColorsTest); @@ -273,8 +275,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 +554,15 @@ 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)); + addons.webgl.instance.onAddTextureAtlasCanvas(e => appendTextureAtlas(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)); + addons.canvas.instance.onAddTextureAtlasCanvas(e => appendTextureAtlas(e)); }, 0); } else if (name === 'unicode11') { term.unicode.activeVersion = '11'; @@ -648,9 +653,18 @@ function htmlSerializeButtonHandler(): void { document.getElementById('htmlserialize-output-result').innerText = 'Copied to clipboard'; } -function addTextureAtlas(e: HTMLCanvasElement): 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'); @@ -965,6 +979,16 @@ function addAnsiHyperlink(): void { term.write('\x1b[3A\x1b[1C\x1b]8;;https://xtermjs.org\x07xter\x1b[B\x1b[4Dm.js\x1b]8;;\x07\x1b[2B\x1b[5D'); } +/** + * Prints the 20977 characters from the CJK Unified Ideographs unicode block. + */ +function addCjk(): void { + term.write('\n\n\r'); + for (let i = 0x4E00; i < 0x9FCC; i++) { + term.write(String.fromCharCode(i)); + } +} + function addDecoration(): void { term.options['overviewRulerWidth'] = 15; const marker = term.registerMarker(1); diff --git a/demo/index.html b/demo/index.html index a65b8bbc2d..26dcc53d2d 100644 --- a/demo/index.html +++ b/demo/index.html @@ -74,6 +74,7 @@

Test

Performance
+
Styles
@@ -91,6 +92,8 @@

Test

+ +