diff --git a/demo/client.ts b/demo/client.ts index d5196d37..23b3a502 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -203,11 +203,11 @@ function initOptions(term: TerminalType): void { bellSound: null, bellStyle: ['none', 'sound'], cursorStyle: ['block', 'underline', 'bar'], - experimentalCharAtlas: ['none', 'static', 'dynamic'], + experimentalCharAtlas: ['none', 'static', 'dynamic', 'webgl'], fontFamily: null, fontWeight: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], fontWeightBold: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], - rendererType: ['dom', 'canvas'], + rendererType: ['dom', 'canvas', 'webgl'], experimentalBufferLineImpl: ['JsArray', 'TypedArray'] }; const options = Object.keys((term)._core.options); @@ -235,7 +235,7 @@ function initOptions(term: TerminalType): void { }); html += '
'; numberOptions.forEach(o => { - html += `
`; + html += `
`; }); html += '
'; Object.keys(stringOptions).forEach(o => { @@ -265,7 +265,7 @@ function initOptions(term: TerminalType): void { if (o === 'cols' || o === 'rows') { updateTerminalSize(); } else { - term.setOption(o, parseInt(input.value, 10)); + term.setOption(o, o === 'lineHeight' ? parseFloat(input.value) : parseInt(input.value, 10)); } }); }); diff --git a/src/Terminal.ts b/src/Terminal.ts index c9bc98ff..84fd192b 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -52,6 +52,7 @@ import { DomRenderer } from './renderer/dom/DomRenderer'; import { IKeyboardEvent } from './common/Types'; import { evaluateKeyboardEvent } from './core/input/Keyboard'; import { KeyboardResultType, ICharset } from './core/Types'; +import { WebglRenderer } from './renderer/webgl/WebglRenderer'; // Let it work inside Node.js for automated testing purposes. const document = (typeof window !== 'undefined') ? window.document : null; @@ -462,6 +463,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.renderer.onResize(this.cols, this.rows); this.refresh(0, this.rows - 1); } + break; case 'rendererType': if (this.renderer) { this.unregister(this.renderer); @@ -761,6 +763,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II switch (this.options.rendererType) { case 'canvas': this.renderer = new Renderer(this, this.options.theme); break; case 'dom': this.renderer = new DomRenderer(this, this.options.theme); break; + case 'webgl': this.renderer = new WebglRenderer(this, this.options.theme); break; default: throw new Error(`Unrecognized rendererType "${this.options.rendererType}"`); } this.register(this.renderer); diff --git a/src/Types.ts b/src/Types.ts index e8578426..7f2f8816 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -311,6 +311,7 @@ export interface IBufferSet extends IEventEmitter { export interface ISelectionManager { selectionText: string; + hasSelection: boolean; selectionStart: [number, number]; selectionEnd: [number, number]; diff --git a/src/renderer/atlas/CharAtlasCache.ts b/src/renderer/atlas/CharAtlasCache.ts index eee93d6c..61b2ed13 100644 --- a/src/renderer/atlas/CharAtlasCache.ts +++ b/src/renderer/atlas/CharAtlasCache.ts @@ -11,11 +11,13 @@ import BaseCharAtlas from './BaseCharAtlas'; import DynamicCharAtlas from './DynamicCharAtlas'; import NoneCharAtlas from './NoneCharAtlas'; import StaticCharAtlas from './StaticCharAtlas'; +import WebglCharAtlas from '../webgl/WebglCharAtlas'; const charAtlasImplementations = { 'none': NoneCharAtlas, 'static': StaticCharAtlas, - 'dynamic': DynamicCharAtlas + 'dynamic': DynamicCharAtlas, + 'webgl': WebglCharAtlas }; interface ICharAtlasCacheEntry { @@ -38,9 +40,10 @@ export function acquireCharAtlas( terminal: ITerminal, colors: IColorSet, scaledCharWidth: number, - scaledCharHeight: number + scaledCharHeight: number, + devicePixelRatio?: number ): BaseCharAtlas { - const newConfig = generateConfig(scaledCharWidth, scaledCharHeight, terminal, colors); + const newConfig = generateConfig(scaledCharWidth, scaledCharHeight, terminal, colors, devicePixelRatio); // TODO: Currently if a terminal changes configs it will not free the entry reference (until it's disposed) @@ -54,6 +57,7 @@ export function acquireCharAtlas( } // The configs differ, release the terminal from the entry if (entry.ownedBy.length === 1) { + entry.atlas.dispose(); charAtlasCache.splice(i, 1); } else { entry.ownedBy.splice(ownedByIndex, 1); @@ -94,6 +98,7 @@ export function removeTerminalFromCache(terminal: ITerminal): void { if (index !== -1) { if (charAtlasCache[i].ownedBy.length === 1) { // Remove the cache entry if it's the only terminal + charAtlasCache[i].atlas.dispose(); charAtlasCache.splice(i, 1); } else { // Remove the reference from the cache entry diff --git a/src/renderer/atlas/CharAtlasUtils.ts b/src/renderer/atlas/CharAtlasUtils.ts index 59ac07df..47cdf45a 100644 --- a/src/renderer/atlas/CharAtlasUtils.ts +++ b/src/renderer/atlas/CharAtlasUtils.ts @@ -7,7 +7,7 @@ import { ITerminal } from '../../Types'; import { IColorSet } from '../Types'; import { ICharAtlasConfig } from '../../shared/atlas/Types'; -export function generateConfig(scaledCharWidth: number, scaledCharHeight: number, terminal: ITerminal, colors: IColorSet): ICharAtlasConfig { +export function generateConfig(scaledCharWidth: number, scaledCharHeight: number, terminal: ITerminal, colors: IColorSet, devicePixelRatio: number = window.devicePixelRatio): ICharAtlasConfig { // null out some fields that don't matter const clonedColors = { foreground: colors.foreground, @@ -21,7 +21,7 @@ export function generateConfig(scaledCharWidth: number, scaledCharHeight: number }; return { type: terminal.options.experimentalCharAtlas, - devicePixelRatio: window.devicePixelRatio, + devicePixelRatio, scaledCharWidth, scaledCharHeight, fontFamily: terminal.options.fontFamily, diff --git a/src/renderer/webgl/GlyphRenderer.ts b/src/renderer/webgl/GlyphRenderer.ts new file mode 100644 index 00000000..0600f758 --- /dev/null +++ b/src/renderer/webgl/GlyphRenderer.ts @@ -0,0 +1,343 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { createProgram, PROJECTION_MATRIX } from './WebglUtils'; +import { IRenderDimensions } from '../Types'; +import { ITerminal, IBufferLine } from '../../Types'; +import { NULL_CELL_CODE, CHAR_DATA_CHAR_INDEX } from '../../Buffer'; +import WebglCharAtlas from './WebglCharAtlas'; +import { IWebGL2RenderingContext, IWebGLVertexArrayObject, IRenderModel, IRasterizedGlyph } from './Types'; +import { INDICIES_PER_CELL } from './WebglRenderer'; +import { COMBINED_CHAR_BIT_MASK } from './RenderModel'; + +interface IVertices { + attributes: Float32Array; + selectionAttributes: Float32Array; + cellPosition: Float32Array; + count: number; +} + +const enum VertexAttribLocations { + UNIT_QUAD = 0, + CELL_POSITION = 1, + OFFSET = 2, + SIZE = 3, + TEXCOORD = 4, + TEXSIZE = 5 +} + +const vertexShaderSource = `#version 300 es +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.TEXCOORD}) in vec2 a_texcoord; +layout (location = ${VertexAttribLocations.TEXSIZE}) in vec2 a_texsize; + +uniform mat4 u_projection; +uniform vec2 u_resolution; + +out vec2 v_texcoord; + +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_texcoord = a_texcoord + a_unitquad * a_texsize; +}`; + +const fragmentShaderSource = `#version 300 es +precision mediump float; + +in vec2 v_texcoord; + +uniform sampler2D u_texture; + +out vec4 outColor; + +void main() { + outColor = texture(u_texture, v_texcoord); +}`; + +const INDICES_PER_CELL = 8; +const BYTES_PER_CELL = INDICES_PER_CELL * Float32Array.BYTES_PER_ELEMENT; + +export class GlyphRenderer { + private _atlas: WebglCharAtlas; + + private _program: WebGLProgram; + private _vertexArrayObject: IWebGLVertexArrayObject; + private _projectionLocation: WebGLUniformLocation; + private _resolutionLocation: WebGLUniformLocation; + private _textureLocation: WebGLUniformLocation; + private _atlasTexture: WebGLTexture; + private _attributesBuffer: WebGLBuffer; + private _cellPositionBuffer: WebGLBuffer; + + private _lineLengths: Int16Array = new Int16Array(0); + private _vertices: IVertices = { + count: 0, + attributes: new Float32Array(0), + selectionAttributes: new Float32Array(0), + cellPosition: new Float32Array(0) + }; + + constructor( + private _terminal: ITerminal, + private _gl: IWebGL2RenderingContext, + private _dimensions: IRenderDimensions + ) { + const gl = this._gl; + + this._program = createProgram(gl, vertexShaderSource, fragmentShaderSource); + + // Uniform locations + this._projectionLocation = gl.getUniformLocation(this._program, 'u_projection'); + this._resolutionLocation = gl.getUniformLocation(this._program, 'u_resolution'); + this._textureLocation = gl.getUniformLocation(this._program, 'u_texture'); + + // Create and set the vertex array object + this._vertexArrayObject = gl.createVertexArray(); + gl.bindVertexArray(this._vertexArrayObject); + + // Setup a_unitquad, this defines the 4 vertices of a rectangle + const unitQuadVertices = new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]); + const unitQuadVerticesBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, unitQuadVerticesBuffer); + gl.bufferData(gl.ARRAY_BUFFER, unitQuadVertices, gl.STATIC_DRAW); + gl.enableVertexAttribArray(VertexAttribLocations.UNIT_QUAD); + gl.vertexAttribPointer(VertexAttribLocations.UNIT_QUAD, 2, this._gl.FLOAT, false, 0, 0); + + // Setup the unit quad element array buffer, this points to indices in + // unitQuadVertuces to allow is to draw 2 triangles from the vertices + const unitQuadElementIndices = new Uint8Array([0, 1, 3, 0, 2, 3]); + const elementIndicesBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, elementIndicesBuffer); + gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, unitQuadElementIndices, gl.STATIC_DRAW); + + // Setup a_cellpos, this is separate as it rarely changed + this._cellPositionBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, this._cellPositionBuffer); + gl.enableVertexAttribArray(VertexAttribLocations.CELL_POSITION); + gl.vertexAttribPointer(VertexAttribLocations.CELL_POSITION, 2, gl.FLOAT, false, 0, 0); + gl.vertexAttribDivisor(VertexAttribLocations.CELL_POSITION, 1); + + // Setup attributes + this._attributesBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer); + gl.enableVertexAttribArray(VertexAttribLocations.OFFSET); + gl.vertexAttribPointer(VertexAttribLocations.OFFSET, 2, gl.FLOAT, false, BYTES_PER_CELL, 0); + gl.vertexAttribDivisor(VertexAttribLocations.OFFSET, 1); + 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.TEXCOORD); + gl.vertexAttribPointer(VertexAttribLocations.TEXCOORD, 2, gl.FLOAT, false, BYTES_PER_CELL, 4 * 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.vertexAttribDivisor(VertexAttribLocations.TEXSIZE, 1); + + // Setup empty texture atlas + this._atlasTexture = gl.createTexture(); + 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); + + // Allow drawing of transparent texture + gl.enable(gl.BLEND); + gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); + + // Set viewport + this.onResize(); + } + + public beginFrame(): boolean { + return this._atlas.beginFrame(); + } + + public updateCell(x: number, y: number, code: number, attr: number, bg: number, fg: number, chars: string): void { + this._updateCell(this._vertices.attributes, x, y, code, attr, bg, fg, chars); + } + + private _updateCell(array: Float32Array, x: number, y: number, code: number, attr: number, bg: number, fg: number, chars?: string): void { + const terminal = this._terminal; + + const i = ((y * terminal.cols) + x) * INDICES_PER_CELL; + + // Exit early if this is a null/space character + if (code === NULL_CELL_CODE) { + array.fill(0, i, i + INDICES_PER_CELL - 1); + return; + } + + let rasterizedGlyph: IRasterizedGlyph; + if (chars && chars.length > 1) { + rasterizedGlyph = this._atlas.getRasterizedGlyphCombinedChar(chars, attr, bg, fg, this._terminal.options.enableBold); + } else { + rasterizedGlyph = this._atlas.getRasterizedGlyph(code, attr, bg, fg, this._terminal.options.enableBold); + } + + // Fill empty if no glyph was found + if (!rasterizedGlyph) { + array.fill(0, i, i + INDICES_PER_CELL - 1); + return; + } + + // a_origin + array[i ] = -rasterizedGlyph.offset.x + this._dimensions.scaledCharLeft; + array[i + 1] = -rasterizedGlyph.offset.y + this._dimensions.scaledCharTop; + // a_size + array[i + 2] = rasterizedGlyph.size.x / this._dimensions.scaledCanvasWidth; + array[i + 3] = rasterizedGlyph.size.y / this._dimensions.scaledCanvasHeight; + // a_texcoord + array[i + 4] = rasterizedGlyph.texturePositionClipSpace.x; + array[i + 5] = rasterizedGlyph.texturePositionClipSpace.y; + // a_texsize + array[i + 6] = rasterizedGlyph.sizeClipSpace.x; + array[i + 7] = rasterizedGlyph.sizeClipSpace.y; + } + + public updateLineEnd(x: number, y: number): void { + // Clears all cells to the right of the line end + const i = (y * this._terminal.cols + x + 1) * INDICES_PER_CELL; + this._vertices.attributes.fill(0, i, i + (this._terminal.cols - this._lineLengths[y]) * INDICES_PER_CELL - 1); + } + + public updateSelection(model: IRenderModel, columnSelectMode: boolean): void { + const terminal = this._terminal; + + this._vertices.selectionAttributes = this._vertices.attributes.slice(0); + + // TODO: Make fg and bg configurable, currently since the buffer doesn't + // support truecolor the char atlas cannot store it. + const fg = 0; + const bg = 7; + + if (columnSelectMode) { + const startCol = model.selection.startCol; + const width = model.selection.endCol - startCol; + const height = model.selection.viewportCappedEndRow - model.selection.viewportCappedStartRow + 1; + for (let y = model.selection.viewportCappedStartRow; y < model.selection.viewportCappedStartRow + height; y++) { + this._updateSelectionRange(startCol, startCol + width, y, model, bg, fg); + } + } else { + // Draw first row + const startCol = model.selection.viewportStartRow === model.selection.viewportCappedStartRow ? model.selection.startCol : 0; + const startRowEndCol = model.selection.viewportCappedStartRow === model.selection.viewportCappedEndRow ? model.selection.endCol : terminal.cols; + this._updateSelectionRange(startCol, startRowEndCol, model.selection.viewportCappedStartRow, model, bg, fg); + + // Draw middle rows + const middleRowsCount = Math.max(model.selection.viewportCappedEndRow - model.selection.viewportCappedStartRow - 1, 0); + for (let y = (model.selection.viewportCappedStartRow + 1); y <= model.selection.viewportCappedStartRow + middleRowsCount; y++) { + this._updateSelectionRange(0, startRowEndCol, y, model, bg, fg); + } + + // Draw final row + if (model.selection.viewportCappedStartRow !== model.selection.viewportCappedEndRow) { + // Only draw viewportEndRow if it's not the same as viewportStartRow + const endCol = model.selection.viewportEndRow === model.selection.viewportCappedEndRow ? model.selection.endCol : terminal.cols; + this._updateSelectionRange(0, endCol, model.selection.viewportCappedEndRow, model, bg, fg); + } + } + } + + private _updateSelectionRange(startCol: number, endCol: number, y: number, model: IRenderModel, bg: number, fg: number): void { + const terminal = this._terminal; + const row = y + terminal.buffer.ydisp; + let line: IBufferLine; + for (let x = startCol; x < endCol; x++) { + const offset = (y * this._terminal.cols + x) * INDICIES_PER_CELL; + // Because the cache uses attr as a lookup key it needs to contain the selection colors as well + let attr = model.cells[offset + 1]; + attr = attr & ~0x3ffff | bg << 9 | fg; + const code = model.cells[offset]; + if (code & COMBINED_CHAR_BIT_MASK) { + if (!line) { + line = terminal.buffer.lines.get(row); + } + const charData = line.get(x); + const chars = charData[CHAR_DATA_CHAR_INDEX]; + this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], attr, bg, fg, chars); + } else { + this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], attr, bg, fg); + } + } + } + + public onResize(): void { + const terminal = this._terminal; + const gl = this._gl; + + gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); + + // Update vertices + const newCount = terminal.cols * terminal.rows * INDICES_PER_CELL; + if (this._vertices.count !== newCount) { + this._vertices.count = newCount; + this._vertices.attributes = new Float32Array(newCount); + this._lineLengths = new Int16Array(terminal.rows); + + this._vertices.cellPosition = new Float32Array(terminal.cols * terminal.rows * 2); + + let i = 0; + for (let y = 0; y < terminal.rows; y++) { + for (let x = 0; x < terminal.cols; x++) { + this._vertices.cellPosition[i++] = x / terminal.cols; + this._vertices.cellPosition[i++] = y / terminal.rows; + } + } + } + } + + public onThemeChanged(): void { + } + + public render(isSelectionVisible: boolean): void { + if (!this._atlas) { + return; + } + + const gl = this._gl; + + gl.useProgram(this._program); + gl.bindVertexArray(this._vertexArrayObject); + + gl.bindBuffer(gl.ARRAY_BUFFER, this._cellPositionBuffer); + gl.bufferData(gl.ARRAY_BUFFER, this._vertices.cellPosition, gl.STATIC_DRAW); + gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer); + gl.bufferData(gl.ARRAY_BUFFER, isSelectionVisible ? this._vertices.selectionAttributes : this._vertices.attributes, gl.DYNAMIC_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); + } + + // 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.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0, this._vertices.count / INDICES_PER_CELL); + } + + public setAtlas(atlas: WebglCharAtlas): void { + const gl = this._gl; + this._atlas = atlas; + + gl.bindTexture(gl.TEXTURE_2D, this._atlasTexture); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, atlas.cacheCanvas); + gl.generateMipmap(gl.TEXTURE_2D); + } + + public setDimensions(dimensions: IRenderDimensions): void { + this._dimensions = dimensions; + } +} diff --git a/src/renderer/webgl/RectangleRenderer.ts b/src/renderer/webgl/RectangleRenderer.ts new file mode 100644 index 00000000..eca89b3a --- /dev/null +++ b/src/renderer/webgl/RectangleRenderer.ts @@ -0,0 +1,320 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { ITerminal } from '../../Types'; +import { IColorManager, IRenderDimensions } from '../Types'; +import { createProgram, expandFloat32Array, PROJECTION_MATRIX } from './WebglUtils'; +import { IColor } from '../../shared/Types'; +import { IRenderModel, IWebGLVertexArrayObject, IWebGL2RenderingContext, ISelectionRenderModel } from './Types'; +import { RENDER_INVERTED_DEFAULT_COLOR } from './RenderModel'; + +const enum VertexAttribLocations { + POSITION = 0, + SIZE = 1, + COLOR = 2, + UNIT_QUAD = 3 +} + +const vertexShaderSource = `#version 300 es +layout (location = ${VertexAttribLocations.POSITION}) in vec2 a_position; +layout (location = ${VertexAttribLocations.SIZE}) in vec2 a_size; +layout (location = ${VertexAttribLocations.COLOR}) in vec3 a_color; +layout (location = ${VertexAttribLocations.UNIT_QUAD}) in vec2 a_unitquad; + +uniform mat4 u_projection; +uniform vec2 u_resolution; + +out vec3 v_color; + +void main() { + vec2 zeroToOne = (a_position + (a_unitquad * a_size)) / u_resolution; + gl_Position = u_projection * vec4(zeroToOne, 0.0, 1.0); + v_color = a_color; +}`; + +const fragmentShaderSource = `#version 300 es +precision mediump float; + +in vec3 v_color; + +out vec4 outColor; + +void main() { + outColor = vec4(v_color, 1); +}`; + +interface IVertices { + attributes: Float32Array; + selection: Float32Array; + count: number; +} + +const INDICES_PER_RECTANGLE = 8; +const BYTES_PER_RECTANGLE = INDICES_PER_RECTANGLE * Float32Array.BYTES_PER_ELEMENT; + +const INITIAL_BUFFER_RECTANGLE_CAPACITY = 20 * INDICES_PER_RECTANGLE; + +export class RectangleRenderer { + + private _program: WebGLProgram; + private _vertexArrayObject: IWebGLVertexArrayObject; + private _resolutionLocation: WebGLUniformLocation; + private _attributesBuffer: WebGLBuffer; + private _projectionLocation: WebGLUniformLocation; + private _bgFloat: Float32Array; + private _selectionFloat: Float32Array; + + private _vertices: IVertices = { + count: 0, + attributes: new Float32Array(INITIAL_BUFFER_RECTANGLE_CAPACITY), + selection: new Float32Array(3 * INDICES_PER_RECTANGLE) + }; + + constructor( + private _terminal: ITerminal, + private _colorManager: IColorManager, + private _gl: IWebGL2RenderingContext, + private _dimensions: IRenderDimensions + ) { + const gl = this._gl; + + this._program = createProgram(gl, vertexShaderSource, fragmentShaderSource); + + // Uniform locations + this._resolutionLocation = gl.getUniformLocation(this._program, 'u_resolution'); + this._projectionLocation = gl.getUniformLocation(this._program, 'u_projection'); + + // Create and set the vertex array object + this._vertexArrayObject = gl.createVertexArray(); + gl.bindVertexArray(this._vertexArrayObject); + + // Setup a_unitquad, this defines the 4 vertices of a rectangle + const unitQuadVertices = new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]); + const unitQuadVerticesBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, unitQuadVerticesBuffer); + gl.bufferData(gl.ARRAY_BUFFER, unitQuadVertices, gl.STATIC_DRAW); + gl.enableVertexAttribArray(VertexAttribLocations.UNIT_QUAD); + gl.vertexAttribPointer(VertexAttribLocations.UNIT_QUAD, 2, this._gl.FLOAT, false, 0, 0); + + // Setup the unit quad element array buffer, this points to indices in + // unitQuadVertuces to allow is to draw 2 triangles from the vertices + const unitQuadElementIndices = new Uint8Array([0, 1, 3, 0, 2, 3]); + const elementIndicesBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, elementIndicesBuffer); + gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, unitQuadElementIndices, gl.STATIC_DRAW); + + // Setup attributes + this._attributesBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer); + gl.enableVertexAttribArray(VertexAttribLocations.POSITION); + gl.vertexAttribPointer(VertexAttribLocations.POSITION, 2, gl.FLOAT, false, BYTES_PER_RECTANGLE, 0); + gl.vertexAttribDivisor(VertexAttribLocations.POSITION, 1); + gl.enableVertexAttribArray(VertexAttribLocations.SIZE); + gl.vertexAttribPointer(VertexAttribLocations.SIZE, 2, gl.FLOAT, false, BYTES_PER_RECTANGLE, 2 * Float32Array.BYTES_PER_ELEMENT); + gl.vertexAttribDivisor(VertexAttribLocations.SIZE, 1); + gl.enableVertexAttribArray(VertexAttribLocations.COLOR); + gl.vertexAttribPointer(VertexAttribLocations.COLOR, 4, gl.FLOAT, false, BYTES_PER_RECTANGLE, 4 * Float32Array.BYTES_PER_ELEMENT); + gl.vertexAttribDivisor(VertexAttribLocations.COLOR, 1); + + this._updateCachedColors(); + } + + public render(): void { + const gl = this._gl; + + gl.useProgram(this._program); + + gl.bindVertexArray(this._vertexArrayObject); + + gl.uniformMatrix4fv(this._projectionLocation, false, PROJECTION_MATRIX); + gl.uniform2f(this._resolutionLocation, gl.canvas.width, gl.canvas.height); + + // Bind attributes buffer and draw + gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer); + gl.bufferData(gl.ARRAY_BUFFER, this._vertices.attributes, gl.DYNAMIC_DRAW); + gl.drawElementsInstanced(this._gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0, this._vertices.count); + + // Bind selection buffer and draw + gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer); + gl.bufferData(gl.ARRAY_BUFFER, this._vertices.selection, gl.DYNAMIC_DRAW); + gl.drawElementsInstanced(this._gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0, 3); + } + + public onResize(): void { + this._updateViewportRectangle(); + } + + public onThemeChanged(): void { + this._updateCachedColors(); + this._updateViewportRectangle(); + } + + private _updateCachedColors(): void { + this._bgFloat = this._colorToFloat32Array(this._colorManager.colors.background); + this._selectionFloat = this._colorToFloat32Array(this._colorManager.colors.selection); + } + + private _updateViewportRectangle(): void { + // Set first rectangle that clears the screen + this._addRectangleFloat( + this._vertices.attributes, + 0, + 0, + 0, + this._terminal.cols * this._dimensions.scaledCellWidth, + this._terminal.rows * this._dimensions.scaledCellHeight, + this._bgFloat + ); + } + + public updateSelection(model: ISelectionRenderModel, columnSelectMode: boolean): void { + const terminal = this._terminal; + + if (!model.hasSelection) { + this._vertices.selection.fill(0, 0); + return; + } + + if (columnSelectMode) { + const startCol = model.startCol; + const width = model.endCol - startCol; + const height = model.viewportCappedEndRow - model.viewportCappedStartRow + 1; + this._addRectangleFloat( + this._vertices.selection, + 0, + startCol * this._dimensions.scaledCellWidth, + model.viewportCappedStartRow * this._dimensions.scaledCellHeight, + width * this._dimensions.scaledCellWidth, + height * this._dimensions.scaledCellHeight, + this._selectionFloat + ); + this._vertices.selection.fill(0, INDICES_PER_RECTANGLE); + } else { + // Draw first row + const startCol = model.viewportStartRow === model.viewportCappedStartRow ? model.startCol : 0; + const startRowEndCol = model.viewportCappedStartRow === model.viewportCappedEndRow ? model.endCol : terminal.cols; + this._addRectangleFloat( + this._vertices.selection, + 0, + startCol * this._dimensions.scaledCellWidth, + model.viewportCappedStartRow * this._dimensions.scaledCellHeight, + (startRowEndCol - startCol) * this._dimensions.scaledCellWidth, + this._dimensions.scaledCellHeight, + this._selectionFloat + ); + + // Draw middle rows + const middleRowsCount = Math.max(model.viewportCappedEndRow - model.viewportCappedStartRow - 1, 0); + this._addRectangleFloat( + this._vertices.selection, + INDICES_PER_RECTANGLE, + 0, + (model.viewportCappedStartRow + 1) * this._dimensions.scaledCellHeight, + terminal.cols * this._dimensions.scaledCellWidth, + middleRowsCount * this._dimensions.scaledCellHeight, + this._selectionFloat + ); + + // Draw final row + if (model.viewportCappedStartRow !== model.viewportCappedEndRow) { + // Only draw viewportEndRow if it's not the same as viewportStartRow + const endCol = model.viewportEndRow === model.viewportCappedEndRow ? model.endCol : terminal.cols; + this._addRectangleFloat( + this._vertices.selection, + INDICES_PER_RECTANGLE * 2, + 0, + model.viewportCappedEndRow * this._dimensions.scaledCellHeight, + endCol * this._dimensions.scaledCellWidth, + this._dimensions.scaledCellHeight, + this._selectionFloat + ); + } else { + this._vertices.selection.fill(0, INDICES_PER_RECTANGLE * 2); + } + } + } + + public updateBackgrounds(model: IRenderModel): void { + const terminal = this._terminal; + const vertices = this._vertices; + + let rectangleCount = 1; + + const DEFAULT_BACKGROUND_COLOR = 256; + for (let y = 0; y < terminal.rows; y++) { + let currentStartX = -1; + let currentBg = DEFAULT_BACKGROUND_COLOR; + for (let x = 0; x < terminal.cols; x++) { + const modelIndex = ((y * terminal.cols) + x) * 4; + const bg = model.cells[modelIndex + 2]; + if (bg !== currentBg) { + // A rectangle needs to be drawn if going from non-default to another color + if (currentBg !== DEFAULT_BACKGROUND_COLOR) { + const offset = rectangleCount++ * INDICES_PER_RECTANGLE; + this._updateRectangle(vertices, offset, currentBg, currentStartX, x, y); + } + currentStartX = x; + currentBg = bg; + } + } + // Finish rectangle if it's still going + if (currentBg !== DEFAULT_BACKGROUND_COLOR) { + const offset = rectangleCount++ * INDICES_PER_RECTANGLE; + this._updateRectangle(vertices, offset, currentBg, currentStartX, terminal.cols, y); + } + } + vertices.count = rectangleCount; + } + + private _updateRectangle(vertices: IVertices, offset: number, bg: number, startX: number, endX: number, y: number): void { + let color: IColor | null = null; + if (bg === RENDER_INVERTED_DEFAULT_COLOR) { + color = this._colorManager.colors.foreground; + } else if (bg < 256) { + color = this._colorManager.colors.ansi[bg]; + } + if (vertices.attributes.length < offset + 4) { + vertices.attributes = expandFloat32Array(vertices.attributes, this._terminal.rows * this._terminal.cols * INDICES_PER_RECTANGLE); + } + const x1 = startX * this._dimensions.scaledCellWidth; + const y1 = y * this._dimensions.scaledCellHeight; + const r = ((color.rgba >> 24) & 0xFF) / 255; + const g = ((color.rgba >> 16) & 0xFF) / 255; + const b = ((color.rgba >> 8 ) & 0xFF) / 255; + + this._addRectangle(vertices.attributes, offset, x1, y1, (endX - startX) * this._dimensions.scaledCellWidth, this._dimensions.scaledCellHeight, r, g, b, 1); + } + + private _addRectangle(array: Float32Array, offset: number, x1: number, y1: number, width: number, height: number, r: number, g: number, b: number, a: number): void { + array[offset ] = x1; + array[offset + 1] = y1; + array[offset + 2] = width; + array[offset + 3] = height; + array[offset + 4] = r; + array[offset + 5] = g; + array[offset + 6] = b; + array[offset + 7] = a; + } + + private _addRectangleFloat(array: Float32Array, offset: number, x1: number, y1: number, width: number, height: number, color: Float32Array): void { + array[offset ] = x1; + array[offset + 1] = y1; + array[offset + 2] = width; + array[offset + 3] = height; + array[offset + 4] = color[0]; + array[offset + 5] = color[1]; + array[offset + 6] = color[2]; + array[offset + 7] = color[3]; + } + + private _colorToFloat32Array(color: IColor): Float32Array { + return new Float32Array([ + ((color.rgba >> 24) & 0xFF) / 255, + ((color.rgba >> 16) & 0xFF) / 255, + ((color.rgba >> 8 ) & 0xFF) / 255, + ((color.rgba ) & 0xFF) / 255 + ]); + } +} diff --git a/src/renderer/webgl/RenderModel.ts b/src/renderer/webgl/RenderModel.ts new file mode 100644 index 00000000..a14616c2 --- /dev/null +++ b/src/renderer/webgl/RenderModel.ts @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IRenderModel, ISelectionRenderModel } from './Types'; + +export const RENDER_MODEL_INDICIES_PER_CELL = 4; + +// HACK: Cannot use INVERTED_DEFAULT_COLOR (-1) here because _model.cells is a +// Uint32Array. This should be changed when true color is introduced to whatever +// the mechanism is for the buffer. +export const RENDER_INVERTED_DEFAULT_COLOR = 258; + +export const COMBINED_CHAR_BIT_MASK = 0x80000000; + +export class RenderModel implements IRenderModel { + public cells: Uint32Array; + public lineLengths: Uint32Array; + public selection: ISelectionRenderModel; + + constructor() { + this.cells = new Uint32Array(0); + this.lineLengths = new Uint32Array(0); + this.selection = { + hasSelection: false, + viewportStartRow: 0, + viewportEndRow: 0, + viewportCappedStartRow: 0, + viewportCappedEndRow: 0, + startCol: 0, + endCol: 0 + }; + } + + public resize(cols: number, rows: number): void { + const indexCount = cols * rows * RENDER_MODEL_INDICIES_PER_CELL; + if (indexCount !== this.cells.length) { + this.cells = new Uint32Array(indexCount); + this.lineLengths = new Uint32Array(rows); + } + } + + public clear(): void { + this.cells.fill(0, 0); + this.lineLengths.fill(0, 0); + this.clearSelection(); + } + + public clearSelection(): void { + this.selection.hasSelection = false; + this.selection.viewportStartRow = 0; + this.selection.viewportEndRow = 0; + this.selection.viewportCappedStartRow = 0; + this.selection.viewportCappedEndRow = 0; + this.selection.startCol = 0; + this.selection.endCol = 0; + } +} diff --git a/src/renderer/webgl/Types.ts b/src/renderer/webgl/Types.ts new file mode 100644 index 00000000..6ebc8d64 --- /dev/null +++ b/src/renderer/webgl/Types.ts @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +export interface IRasterizedGlyphSet { + [flags: number]: IRasterizedGlyph; +} + +/** + * Represents a rasterized glyph within a texture atlas. Some numbers are + * tracked in CSS pixels as well in order to reduce calculations during the + * render loop. + */ +export interface IRasterizedGlyph { + /** + * The x and y offset between the glyph's top/left and the top/left of a cell + * in pixels. + */ + offset: IVector; + /** + * the x and y position of the glyph in the texture in pixels. + */ + texturePosition: IVector; + /** + * the x and y position of the glyph in the texture in clip space coordinates. + */ + texturePositionClipSpace: IVector; + /** + * The width and height of the glyph in the texture in pixels. + */ + size: IVector; + /** + * The width and height of the glyph in the texture in clip space coordinates. + */ + sizeClipSpace: IVector; +} + +export interface IVector { + x: number; + y: number; +} + +export interface IBoundingBox { + top: number; + left: number; + right: number; + bottom: number; +} + +export interface IRenderModel { + cells: Uint32Array; + lineLengths: Uint32Array; + selection: ISelectionRenderModel; +} + +export interface ISelectionRenderModel { + hasSelection: boolean; + viewportStartRow: number; + viewportEndRow: number; + viewportCappedStartRow: number; + viewportCappedEndRow: number; + startCol: number; + endCol: number; +} + +export interface IWebGL2RenderingContext extends WebGLRenderingContext { + vertexAttribDivisor(index: number, divisor: number): void; + createVertexArray(): IWebGLVertexArrayObject; + bindVertexArray(vao: IWebGLVertexArrayObject): void; + drawElementsInstanced(mode: number, count: number, type: number, offset: number, instanceCount: number): void; +} + +export interface IWebGLVertexArrayObject { +} diff --git a/src/renderer/webgl/WebglCharAtlas.ts b/src/renderer/webgl/WebglCharAtlas.ts new file mode 100644 index 00000000..86a60d07 --- /dev/null +++ b/src/renderer/webgl/WebglCharAtlas.ts @@ -0,0 +1,386 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { DIM_OPACITY, IGlyphIdentifier } from '../atlas/Types'; +import { ICharAtlasConfig } from '../../shared/atlas/Types'; +import { IColor } from '../../shared/Types'; +import BaseCharAtlas from '../atlas/BaseCharAtlas'; +import { DEFAULT_ANSI_COLORS } from '../ColorManager'; +import { clearColor } from '../../shared/atlas/CharAtlasGenerator'; +import { IRasterizedGlyph, IBoundingBox, IRasterizedGlyphSet } from './Types'; +import { DEFAULT_ATTR } from '../../Buffer'; +import { FLAGS } from '../Types'; +import { RENDER_INVERTED_DEFAULT_COLOR } from './RenderModel'; + +// In practice we're probably never going to exhaust a texture this large. For debugging purposes, +// however, 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; + +/** + * 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); + +const TRANSPARENT_COLOR = { + css: 'rgba(0, 0, 0, 0)', + rgba: 0 +}; + +/** + * A shared object which is used to draw nothing for a particular cell. + */ +const NULL_RASTERIZED_GLYPH: IRasterizedGlyph = { + offset: { x: 0, y: 0 }, + texturePosition: { x: 0, y: 0 }, + texturePositionClipSpace: { x: 0, y: 0 }, + size: { x: 0, y: 0 }, + sizeClipSpace: { x: 0, y: 0 } +}; + +const TMP_CANVAS_GLYPH_PADDING = 2; + +export default class WebglCharAtlas extends BaseCharAtlas { + private _cacheMap: { [code: number]: IRasterizedGlyphSet } = {}; + private _cacheMapCombined: { [chars: string]: IRasterizedGlyphSet } = {}; + + // The texture that the atlas is drawn to + public cacheCanvas: HTMLCanvasElement; + private _cacheCtx: CanvasRenderingContext2D; + + private _tmpCanvas: HTMLCanvasElement; + // A temporary context that glyphs are drawn to before being transfered to the atlas. + private _tmpCtx: CanvasRenderingContext2D; + + // Since glyphs are expected to be around the same height, the packing + // strategy used it to fill a row with glyphs while keeping track of the + // tallest glyph in the row. Once the row is full a new row is started at + // (0,lastRow+lastRowTallestGlyph). + private _currentRowY: number = 0; + private _currentRowX: number = 0; + private _currentRowHeight: number = 0; + + public hasCanvasChanged = false; + + private _workBoundingBox: IBoundingBox = { top: 0, left: 0, bottom: 0, right: 0 }; + + constructor(document: Document, private _config: ICharAtlasConfig) { + super(); + + this.cacheCanvas = document.createElement('canvas'); + this.cacheCanvas.width = TEXTURE_WIDTH; + this.cacheCanvas.height = 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 = this.cacheCanvas.getContext('2d', {alpha: true}); + + this._tmpCanvas = document.createElement('canvas'); + this._tmpCanvas.width = this._config.scaledCharWidth * 2 + TMP_CANVAS_GLYPH_PADDING * 2; + this._tmpCanvas.height = this._config.scaledCharHeight + TMP_CANVAS_GLYPH_PADDING * 2; + this._tmpCtx = this._tmpCanvas.getContext('2d', {alpha: this._config.allowTransparency}); + + // This is useful for debugging + document.body.appendChild(this.cacheCanvas); + } + + public dispose(): void { + if (this.cacheCanvas.parentElement) { + this.cacheCanvas.parentElement.removeChild(this.cacheCanvas); + } + } + + protected _doWarmUp(): void { + // Pre-fill with ASCII 33-126 + for (let i = 33; i < 126; i++) { + const rasterizedGlyph = this._drawToCache(i, DEFAULT_ATTR, 256, 257, true); + this._cacheMap[i] = { + [DEFAULT_ATTR]: rasterizedGlyph + }; + } + } + + public beginFrame(): boolean { + if (this._currentRowY > TEXTURE_CAPACITY) { + this._cacheCtx.clearRect(0, 0, TEXTURE_WIDTH, TEXTURE_HEIGHT); + this._cacheMap = {}; + this._currentRowHeight = 0; + this._currentRowX = 0; + this._currentRowY = 0; + this._doWarmUp(); + return true; + } + return false; + } + + public getRasterizedGlyphCombinedChar(chars: string, attr: number, bg: number, fg: number, enableBold: boolean): IRasterizedGlyph { + let rasterizedGlyphSet = this._cacheMapCombined[chars]; + if (!rasterizedGlyphSet) { + rasterizedGlyphSet = {}; + this._cacheMapCombined[chars] = rasterizedGlyphSet; + } + let rasterizedGlyph = rasterizedGlyphSet[attr]; + if (!rasterizedGlyph) { + rasterizedGlyph = this._drawToCache(chars, attr, bg, fg, enableBold); + rasterizedGlyphSet[attr] = rasterizedGlyph; + } + return rasterizedGlyph; + } + + /** + * Gets the glyphs texture coords, drawing the texture if it's not already + */ + public getRasterizedGlyph(code: number, attr: number, bg: number, fg: number, enableBold: boolean): IRasterizedGlyph { + // Space is always an empty cell, special case this as it's so common + if (code === 32) { + return; + } + + let rasterizedGlyphSet = this._cacheMap[code]; + if (!rasterizedGlyphSet) { + rasterizedGlyphSet = {}; + this._cacheMap[code] = rasterizedGlyphSet; + } + let rasterizedGlyph = rasterizedGlyphSet[attr]; + if (!rasterizedGlyph) { + rasterizedGlyph = this._drawToCache(code, attr, bg, fg, enableBold); + rasterizedGlyphSet[attr] = rasterizedGlyph; + } + return rasterizedGlyph; + } + + public draw( + ctx: CanvasRenderingContext2D, + glyph: IGlyphIdentifier, + x: number, + y: number + ): boolean { + throw new Error('WebglCharAtlas is only compatible with the webgl renderer'); + } + + private _getColorFromAnsiIndex(idx: number): IColor { + if (idx < this._config.colors.ansi.length) { + return this._config.colors.ansi[idx]; + } + return DEFAULT_ANSI_COLORS[idx]; + } + + private _getBackgroundColor(bg: number): IColor { + if (this._config.allowTransparency) { + // The background color might have some transparency, so we need to render it as fully + // transparent in the atlas. Otherwise we'd end up drawing the transparent background twice + // around the anti-aliased edges of the glyph, and it would look too dark. + return TRANSPARENT_COLOR; + } else if (bg === RENDER_INVERTED_DEFAULT_COLOR) { + return this._config.colors.foreground; + } else if (bg < 256) { + return this._getColorFromAnsiIndex(bg); + } + return this._config.colors.background; + } + + private _getForegroundColor(fg: number): IColor { + if (fg === RENDER_INVERTED_DEFAULT_COLOR) { + return this._config.colors.background; + } else if (fg < 256) { + // 256 color support + return this._getColorFromAnsiIndex(fg); + } + return this._config.colors.foreground; + } + + private _drawToCache(code: number, attr: number, bg: number, fg: number, enableBold: boolean): IRasterizedGlyph; + private _drawToCache(chars: string, attr: number, bg: number, fg: number, enableBold: boolean): IRasterizedGlyph; + private _drawToCache(codeOrChars: number | string, attr: number, bg: number, fg: number, enableBold: boolean): IRasterizedGlyph { + const chars = typeof codeOrChars === 'number' ? String.fromCharCode(codeOrChars) : codeOrChars; + + this.hasCanvasChanged = true; + + const flags = attr >> 18; + + const bold = !!(flags & FLAGS.BOLD) && enableBold; + const dim = !!(flags & FLAGS.DIM); + const italic = !!(flags & FLAGS.ITALIC); + + this._tmpCtx.save(); + + // draw the background + const backgroundColor = this._getBackgroundColor(bg); + // Use a 'copy' composite operation to clear any existing glyph out of _tmpCtxWithAlpha, regardless of + // transparency in backgroundColor + this._tmpCtx.globalCompositeOperation = 'copy'; + this._tmpCtx.fillStyle = backgroundColor.css; + this._tmpCtx.fillRect(0, 0, this._tmpCanvas.width, this._tmpCanvas.height); + this._tmpCtx.globalCompositeOperation = 'source-over'; + + // draw the foreground/glyph + const fontWeight = bold ? this._config.fontWeightBold : this._config.fontWeight; + const fontStyle = italic ? 'italic' : ''; + this._tmpCtx.font = + `${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`; + this._tmpCtx.textBaseline = 'top'; + + this._tmpCtx.fillStyle = this._getForegroundColor(fg).css; + + // Apply alpha to dim the character + if (dim) { + this._tmpCtx.globalAlpha = DIM_OPACITY; + } + + // Draw the character + this._tmpCtx.fillText(chars, TMP_CANVAS_GLYPH_PADDING, TMP_CANVAS_GLYPH_PADDING); + this._tmpCtx.restore(); + + // clear the background from the character to avoid issues with drawing over the previous + // character if it extends past it's bounds + const imageData = this._tmpCtx.getImageData( + 0, 0, this._tmpCanvas.width, this._tmpCanvas.height + ); + + // TODO: Support transparency + // let isEmpty = false; + // if (!this._config.allowTransparency) { + // isEmpty = clearColor(imageData, backgroundColor); + // } + + // Clear out the background color and determine if the glyph is empty. + const isEmpty = clearColor(imageData, backgroundColor); + + // Handle empty glyphs + if (isEmpty) { + return NULL_RASTERIZED_GLYPH; + } + + const rasterizedGlyph = this._findGlyphBoundingBox(imageData, this._workBoundingBox); + const clippedImageData = this._clipImageData(imageData, this._workBoundingBox); + + // Check if there is enough room in the current row and go to next if needed + if (this._currentRowX + this._config.scaledCharWidth > TEXTURE_WIDTH) { + this._currentRowX = 0; + this._currentRowY += this._currentRowHeight; + this._currentRowHeight = 0; + } + + // Record texture position + rasterizedGlyph.texturePosition.x = this._currentRowX; + rasterizedGlyph.texturePosition.y = this._currentRowY; + rasterizedGlyph.texturePositionClipSpace.x = this._currentRowX / TEXTURE_WIDTH; + rasterizedGlyph.texturePositionClipSpace.y = this._currentRowY / TEXTURE_HEIGHT; + + // Update atlas current row + this._currentRowHeight = Math.max(this._currentRowHeight, rasterizedGlyph.size.y); + this._currentRowX += rasterizedGlyph.size.x; + + // putImageData doesn't do any blending, so it will overwrite any existing cache entry for us + this._cacheCtx.putImageData(clippedImageData, rasterizedGlyph.texturePosition.x, rasterizedGlyph.texturePosition.y); + + return rasterizedGlyph; + } + + /** + * Given an ImageData object, find the bounding box of the non-transparent + * portion of the texture and return an IRasterizedGlyph with these + * dimensions. + * @param imageData The image data to read. + * @param boundingBox An IBoundingBox to put the clipped bounding box values. + */ + private _findGlyphBoundingBox(imageData: ImageData, boundingBox: IBoundingBox): IRasterizedGlyph { + boundingBox.top = 0; + let found = false; + for (let y = 0; y < this._tmpCanvas.height; y++) { + for (let x = 0; x < this._tmpCanvas.width; x++) { + const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3; + if (imageData.data[alphaOffset] !== 0) { + boundingBox.top = y; + found = true; + break; + } + } + if (found) { + break; + } + } + boundingBox.left = 0; + found = false; + for (let x = 0; x < this._tmpCanvas.width; x++) { + for (let y = 0; y < this._tmpCanvas.height; y++) { + const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3; + if (imageData.data[alphaOffset] !== 0) { + boundingBox.left = x; + found = true; + break; + } + } + if (found) { + break; + } + } + boundingBox.right = this._tmpCanvas.width; + found = false; + for (let x = this._tmpCanvas.width - 1; x >= 0; x--) { + for (let y = 0; y < this._tmpCanvas.height; y++) { + const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3; + if (imageData.data[alphaOffset] !== 0) { + boundingBox.right = x; + found = true; + break; + } + } + if (found) { + break; + } + } + boundingBox.bottom = this._tmpCanvas.height; + found = false; + for (let y = this._tmpCanvas.height - 1; y >= 0; y--) { + for (let x = 0; x < this._tmpCanvas.width; x++) { + const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3; + if (imageData.data[alphaOffset] !== 0) { + boundingBox.bottom = y; + found = true; + break; + } + } + if (found) { + break; + } + } + return { + texturePosition: { x: 0, y: 0 }, + texturePositionClipSpace: { x: 0, y: 0 }, + size: { + x: boundingBox.right - boundingBox.left + 1, + y: boundingBox.bottom - boundingBox.top + 1 + }, + sizeClipSpace: { + x: (boundingBox.right - boundingBox.left + 1) / TEXTURE_WIDTH, + y: (boundingBox.bottom - boundingBox.top + 1) / TEXTURE_HEIGHT + }, + offset: { + x: -boundingBox.left + TMP_CANVAS_GLYPH_PADDING, + y: -boundingBox.top + TMP_CANVAS_GLYPH_PADDING + } + }; + } + + private _clipImageData(imageData: ImageData, boundingBox: IBoundingBox): ImageData { + const width = boundingBox.right - boundingBox.left + 1; + const height = boundingBox.bottom - boundingBox.top + 1; + const clippedData = new Uint8ClampedArray(width * height * 4); + for (let y = boundingBox.top; y <= boundingBox.bottom; y++) { + for (let x = boundingBox.left; x <= boundingBox.right; x++) { + const oldOffset = y * this._tmpCanvas.width * 4 + x * 4; + const newOffset = (y - boundingBox.top) * width * 4 + (x - boundingBox.left) * 4; + clippedData[newOffset] = imageData.data[oldOffset]; + clippedData[newOffset + 1] = imageData.data[oldOffset + 1]; + clippedData[newOffset + 2] = imageData.data[oldOffset + 2]; + clippedData[newOffset + 3] = imageData.data[oldOffset + 3]; + } + } + return new ImageData(clippedData, width, height); + } +} diff --git a/src/renderer/webgl/WebglRenderer.ts b/src/renderer/webgl/WebglRenderer.ts new file mode 100644 index 00000000..25030319 --- /dev/null +++ b/src/renderer/webgl/WebglRenderer.ts @@ -0,0 +1,427 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { EventEmitter } from '../../common/EventEmitter'; +import { IRenderer, IRenderDimensions, IColorSet, IRenderLayer, FLAGS } from '../Types'; +import { ITheme } from 'xterm'; +import { CharacterJoinerHandler, ITerminal } from '../../Types'; +import { ColorManager } from '../ColorManager'; +import { RenderDebouncer } from '../../ui/RenderDebouncer'; +import { GlyphRenderer } from './GlyphRenderer'; +import { LinkRenderLayer } from '../LinkRenderLayer'; +import { CursorRenderLayer } from '../CursorRenderLayer'; +import { acquireCharAtlas } from '../atlas/CharAtlasCache'; +import WebglCharAtlas from './WebglCharAtlas'; +import { ScreenDprMonitor } from '../../ui/ScreenDprMonitor'; +import { RectangleRenderer } from './RectangleRenderer'; +import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX } from '../../Buffer'; +import { IWebGL2RenderingContext } from './Types'; +import { INVERTED_DEFAULT_COLOR } from '../atlas/Types'; +import { RenderModel, RENDER_INVERTED_DEFAULT_COLOR, COMBINED_CHAR_BIT_MASK } from './RenderModel'; + +export const INDICIES_PER_CELL = 4; + +export class WebglRenderer extends EventEmitter implements IRenderer { + private _renderDebouncer: RenderDebouncer; + private _renderLayers: IRenderLayer[]; + private _charAtlas: WebglCharAtlas; + private _screenDprMonitor: ScreenDprMonitor; + private _devicePixelRatio: number; + + private _model: RenderModel = new RenderModel(); + + private _canvas: HTMLCanvasElement; + private _gl: IWebGL2RenderingContext; + private _rectangleRenderer: RectangleRenderer; + private _glyphRenderer: GlyphRenderer; + + private _isPaused: boolean = false; + private _needsFullRefresh: boolean = false; + + public dimensions: IRenderDimensions; + public colorManager: ColorManager; + + constructor( + private _terminal: ITerminal, + theme: ITheme + ) { + super(); + const allowTransparency = this._terminal.options.allowTransparency; + this.colorManager = new ColorManager(document, allowTransparency); + + this._renderLayers = [ + new LinkRenderLayer(this._terminal.screenElement, 2, this.colorManager.colors, this._terminal), + new CursorRenderLayer(this._terminal.screenElement, 3, this.colorManager.colors) + ]; + this.dimensions = { + scaledCharWidth: null, + scaledCharHeight: null, + scaledCellWidth: null, + scaledCellHeight: null, + scaledCharLeft: null, + scaledCharTop: null, + scaledCanvasWidth: null, + scaledCanvasHeight: null, + canvasWidth: null, + canvasHeight: null, + actualCellWidth: null, + actualCellHeight: null + }; + this._devicePixelRatio = window.devicePixelRatio; + this._updateDimensions(); + + this._screenDprMonitor = new ScreenDprMonitor(); + this._screenDprMonitor.setListener(() => this.onWindowResize(window.devicePixelRatio)); + this.register(this._screenDprMonitor); + + this._renderDebouncer = new RenderDebouncer(this._terminal, this._renderRows.bind(this)); + + this._canvas = document.createElement('canvas'); + this._gl = this._canvas.getContext('webgl2') as IWebGL2RenderingContext; + if (!this._gl) { + throw new Error('WebGL2 not supported'); + } + this._terminal.screenElement.appendChild(this._canvas); + + this._rectangleRenderer = new RectangleRenderer(this._terminal, this.colorManager, this._gl, this.dimensions); + this._glyphRenderer = new GlyphRenderer(this._terminal, this._gl, this.dimensions); + + // Detect whether IntersectionObserver is detected and enable renderer pause + // and resume based on terminal visibility if so + if ('IntersectionObserver' in window) { + const observer = new IntersectionObserver(e => this.onIntersectionChange(e[0]), { threshold: 0 }); + observer.observe(this._terminal.element); + this.register({ dispose: () => observer.disconnect() }); + } + } + + public dispose(): void { + this._renderLayers.forEach(l => l.dispose()); + this._terminal.screenElement.removeChild(this._canvas); + } + + public onIntersectionChange(entry: IntersectionObserverEntry): void { + this._isPaused = entry.intersectionRatio === 0; + if (!this._isPaused && this._needsFullRefresh) { + this._terminal.refresh(0, this._terminal.rows - 1); + } + } + + private _refreshViewport(): void { + // Force a refresh + this._model.clear(); + if (this._isPaused) { + this._needsFullRefresh = true; + } else { + this._terminal.refresh(0, this._terminal.rows - 1); + } + } + + public setTheme(theme: ITheme | undefined): IColorSet { + if (theme) { + this.colorManager.setTheme(theme); + } + + // Clear layers and force a full render + this._renderLayers.forEach(l => { + l.onThemeChanged(this._terminal, this.colorManager.colors); + l.reset(this._terminal); + }); + + this._rectangleRenderer.onThemeChanged(); + this._glyphRenderer.onThemeChanged(); + + this._refreshCharAtlas(); + this._refreshViewport(); + + return this.colorManager.colors; + } + + public onWindowResize(devicePixelRatio: number): void { + // If the device pixel ratio changed, the char atlas needs to be regenerated + // and the terminal needs to refreshed + if (this._devicePixelRatio !== devicePixelRatio) { + this._devicePixelRatio = devicePixelRatio; + this.onResize(this._terminal.cols, this._terminal.rows, devicePixelRatio); + } + } + + public onResize(cols: number, rows: number, devicePixelRatio: number = window.devicePixelRatio): void { + // Update character and canvas dimensions + this._updateDimensions(devicePixelRatio); + + this._model.resize(this._terminal.cols, this._terminal.rows); + this._rectangleRenderer.onResize(); + + // Resize all render layers + this._renderLayers.forEach(l => l.resize(this._terminal, this.dimensions)); + + // Resize the canvas + this._canvas.width = this.dimensions.scaledCanvasWidth; + this._canvas.height = this.dimensions.scaledCanvasHeight; + this._canvas.style.width = `${this.dimensions.canvasWidth}px`; + this._canvas.style.height = `${this.dimensions.canvasHeight}px`; + + // Resize the screen + this._terminal.screenElement.style.width = `${this.dimensions.canvasWidth}px`; + this._terminal.screenElement.style.height = `${this.dimensions.canvasHeight}px`; + this._glyphRenderer.setDimensions(this.dimensions); + this._glyphRenderer.onResize(); + + this._refreshCharAtlas(devicePixelRatio); + this._refreshViewport(); + + this.emit('resize', { + width: this.dimensions.canvasWidth, + height: this.dimensions.canvasHeight + }); + } + + public onCharSizeChanged(): void { + this.onResize(this._terminal.cols, this._terminal.rows); + } + + public onBlur(): void { + this._renderLayers.forEach(l => l.onBlur(this._terminal)); + } + + public onFocus(): void { + this._renderLayers.forEach(l => l.onFocus(this._terminal)); + } + + public onSelectionChanged(start: [number, number], end: [number, number], columnSelectMode: boolean): void { + this._renderLayers.forEach(l => l.onSelectionChanged(this._terminal, start, end, columnSelectMode)); + + this._updateSelectionModel(start, end); + + this._rectangleRenderer.updateSelection(this._model.selection, columnSelectMode); + this._glyphRenderer.updateSelection(this._model, columnSelectMode); + this.refreshRows(0, this._terminal.rows - 1); + } + + public onCursorMove(): void { + this._renderLayers.forEach(l => l.onCursorMove(this._terminal)); + } + + public onOptionsChanged(): void { + this._renderLayers.forEach(l => l.onOptionsChanged(this._terminal)); + this._updateDimensions(); + this._refreshCharAtlas(); + } + + /** + * Refreshes the char atlas, aquiring a new one if necessary. + * @param terminal The terminal. + * @param colorSet The color set to use for the char atlas. + */ + private _refreshCharAtlas(devicePixelRatio: number = window.devicePixelRatio): void { + if (this.dimensions.scaledCharWidth <= 0 && this.dimensions.scaledCharHeight <= 0) { + return; + } + + const atlas = acquireCharAtlas(this._terminal, this.colorManager.colors, this.dimensions.scaledCharWidth, this.dimensions.scaledCharHeight, devicePixelRatio); + if (!('getRasterizedGlyph' in atlas)) { + throw new Error('The webgl renderer only works with the webgl char atlas'); + } + this._charAtlas = atlas as WebglCharAtlas; + this._charAtlas.warmUp(); + this._glyphRenderer.setAtlas(this._charAtlas); + } + + public clear(): void { + this._renderLayers.forEach(l => l.reset(this._terminal)); + } + + public refreshRows(start: number, end: number): void { + if (this._isPaused) { + this._needsFullRefresh = true; + return; + } + this._renderDebouncer.refresh(start, end); + } + + public registerCharacterJoiner(handler: CharacterJoinerHandler): number { + return -1; + } + + public deregisterCharacterJoiner(joinerId: number): boolean { + return false; + } + + private _renderRows(start: number, end: number): void { + // Update render layers + this._renderLayers.forEach(l => l.onGridChanged(this._terminal, start, end)); + + // Tell renderer the frame is beginning + if (this._glyphRenderer.beginFrame()) { + this._model.clear(); + } + + // Update model to reflect what's drawn + this._updateModel(start, end); + + // Render + this._rectangleRenderer.render(); + this._glyphRenderer.render(this._model.selection.hasSelection); + + // Emit event + this._terminal.emit('refresh', { start, end }); + } + + private _updateModel(start: number, end: number): void { + const terminal = this._terminal; + + for (let y = start; y <= end; y++) { + const row = y + terminal.buffer.ydisp; + const line = terminal.buffer.lines.get(row); + this._model.lineLengths[y] = 0; + for (let x = 0; x < terminal.cols; x++) { + const charData = line.get(x); + const chars = charData[CHAR_DATA_CHAR_INDEX]; + let code = charData[CHAR_DATA_CODE_INDEX]; + const attr = charData[CHAR_DATA_ATTR_INDEX]; + const i = ((y * terminal.cols) + x) * INDICIES_PER_CELL; + + // Nothing has changed, no updates needed + if (this._model.cells[i] === code && this._model.cells[i + 1] === attr) { + continue; + } + + // Resolve bg and fg and cache in the model + const flags = attr >> 18; + let bg = attr & 0x1ff; + let fg = (attr >> 9) & 0x1ff; + + // If inverse flag is on, the foreground should become the background. + if (flags & FLAGS.INVERSE) { + const temp = bg; + bg = fg; + fg = temp; + if (fg === 256) { + fg = RENDER_INVERTED_DEFAULT_COLOR; + } + if (bg === 257) { + bg = RENDER_INVERTED_DEFAULT_COLOR; + } + } + const drawInBrightColor = terminal.options.drawBoldTextInBrightColors && !!(flags & FLAGS.BOLD) && fg < 8 && fg !== INVERTED_DEFAULT_COLOR; + fg += drawInBrightColor ? 8 : 0; + + // Flag combined chars with a bit mask so they're easily identifiable + if (chars.length > 1) { + code = code | COMBINED_CHAR_BIT_MASK; + } + + this._model.cells[i ] = code; + this._model.cells[i + 1] = attr; + this._model.cells[i + 2] = bg; + this._model.cells[i + 3] = fg; + + this._glyphRenderer.updateCell(x, y, code, attr, bg, fg, chars); + } + } + this._rectangleRenderer.updateBackgrounds(this._model); + } + + private _updateSelectionModel(start: [number, number], end: [number, number]): void { + const terminal = this._terminal; + + // Selection does not exist + if (!start || !end || (start[0] === end[0] && start[1] === end[1])) { + this._model.clearSelection(); + return; + } + + // Translate from buffer position to viewport position + const viewportStartRow = start[1] - terminal.buffer.ydisp; + const viewportEndRow = end[1] - terminal.buffer.ydisp; + const viewportCappedStartRow = Math.max(viewportStartRow, 0); + const viewportCappedEndRow = Math.min(viewportEndRow, terminal.rows - 1); + + // No need to draw the selection + if (viewportCappedStartRow >= terminal.rows || viewportCappedEndRow < 0) { + this._model.clearSelection(); + return; + } + + this._model.selection.hasSelection = true; + this._model.selection.viewportStartRow = viewportStartRow; + this._model.selection.viewportEndRow = viewportEndRow; + this._model.selection.viewportCappedStartRow = viewportCappedStartRow; + this._model.selection.viewportCappedEndRow = viewportCappedEndRow; + this._model.selection.startCol = start[0]; + this._model.selection.endCol = end[0]; + } + + /** + * Recalculates the character and canvas dimensions. + */ + private _updateDimensions(devicePixelRatio: number = window.devicePixelRatio): void { + // Perform a new measure if the CharMeasure dimensions are not yet available + if (!this._terminal.charMeasure.width || !this._terminal.charMeasure.height) { + return; + } + + // Calculate the scaled character width. Width is floored as it must be + // drawn to an integer grid in order for the CharAtlas "stamps" to not be + // blurry. When text is drawn to the grid not using the CharAtlas, it is + // clipped to ensure there is no overlap with the next cell. + + // NOTE: ceil fixes sometime, floor does others :s + + this.dimensions.scaledCharWidth = Math.floor(this._terminal.charMeasure.width * devicePixelRatio); + + // Calculate the scaled character height. Height is ceiled in case + // devicePixelRatio is a floating point number in order to ensure there is + // enough space to draw the character to the cell. + this.dimensions.scaledCharHeight = Math.ceil(this._terminal.charMeasure.height * devicePixelRatio); + + // Calculate the scaled cell height, if lineHeight is not 1 then the value + // will be floored because since lineHeight can never be lower then 1, there + // is a guarentee that the scaled line height will always be larger than + // scaled char height. + this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._terminal.options.lineHeight); + + // Calculate the y coordinate within a cell that text should draw from in + // order to draw in the center of a cell. + this.dimensions.scaledCharTop = this._terminal.options.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2); + + // Calculate the scaled cell width, taking the letterSpacing into account. + this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._terminal.options.letterSpacing); + + // Calculate the x coordinate with a cell that text should draw from in + // order to draw in the center of a cell. + this.dimensions.scaledCharLeft = Math.floor(this._terminal.options.letterSpacing / 2); + + // Recalculate the canvas dimensions; scaled* define the actual number of + // pixel in the canvas + this.dimensions.scaledCanvasHeight = this._terminal.rows * this.dimensions.scaledCellHeight; + this.dimensions.scaledCanvasWidth = this._terminal.cols * this.dimensions.scaledCellWidth; + + // The the size of the canvas on the page. It's very important that this + // rounds to nearest integer and not ceils as browsers often set + // window.devicePixelRatio as something like 1.100000023841858, when it's + // actually 1.1. Ceiling causes blurriness as the backing canvas image is 1 + // pixel too large for the canvas element size. + this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / devicePixelRatio); + this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / devicePixelRatio); + + // this.dimensions.scaledCanvasHeight = this.dimensions.canvasHeight * devicePixelRatio; + // this.dimensions.scaledCanvasWidth = this.dimensions.canvasWidth * devicePixelRatio; + + // Get the _actual_ dimensions of an individual cell. This needs to be + // derived from the canvasWidth/Height calculated above which takes into + // account window.devicePixelRatio. CharMeasure.width/height by itself is + // insufficient when the page is not at 100% zoom level as CharMeasure is + // measured in CSS pixels, but the actual char size on the canvas can + // differ. + // this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._terminal.rows; + // this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._terminal.cols; + + // This fixes 110% and 125%, not 150% or 175% though + this.dimensions.actualCellHeight = this.dimensions.scaledCellHeight / devicePixelRatio; + this.dimensions.actualCellWidth = this.dimensions.scaledCellWidth / devicePixelRatio; + } +} diff --git a/src/renderer/webgl/WebglUtils.ts b/src/renderer/webgl/WebglUtils.ts new file mode 100644 index 00000000..8f166a23 --- /dev/null +++ b/src/renderer/webgl/WebglUtils.ts @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +/** + * A matrix that when multiplies will translate 0-1 coordinates (left to right, + * top to bottom) to clip space. + */ +export const PROJECTION_MATRIX = new Float32Array([ + 2, 0, 0, 0, + 0, -2, 0, 0, + 0, 0, 1, 0, + -1, 1, 0, 1 +]); + +export function createProgram(gl: WebGLRenderingContext, vertexSource: string, fragmentSource: string): WebGLProgram | undefined { + const program = gl.createProgram(); + gl.attachShader(program, createShader(gl, gl.VERTEX_SHADER, vertexSource)); + gl.attachShader(program, createShader(gl, gl.FRAGMENT_SHADER, fragmentSource)); + gl.linkProgram(program); + const success = gl.getProgramParameter(program, gl.LINK_STATUS); + if (success) { + return program; + } + + console.log(gl.getProgramInfoLog(program)); + gl.deleteProgram(program); +} + +export function createShader(gl: WebGLRenderingContext, type: number, source: string): WebGLShader | undefined { + const shader = gl.createShader(type); + gl.shaderSource(shader, source); + gl.compileShader(shader); + const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS); + if (success) { + return shader; + } + + console.log(gl.getShaderInfoLog(shader)); + gl.deleteShader(shader); +} + +export function expandFloat32Array(source: Float32Array, max: number): Float32Array { + const newLength = Math.min(source.length * 2, max); + const newArray = new Float32Array(newLength); + for (let i = 0; i < source.length; i++) { + newArray[i] = source[i]; + } + return newArray; +} diff --git a/src/shared/atlas/Types.ts b/src/shared/atlas/Types.ts index 25eaa716..7a69f78b 100644 --- a/src/shared/atlas/Types.ts +++ b/src/shared/atlas/Types.ts @@ -9,7 +9,7 @@ import { IColorSet } from '../Types'; export const CHAR_ATLAS_CELL_SPACING = 1; export interface ICharAtlasConfig { - type: 'none' | 'static' | 'dynamic'; + type: 'none' | 'static' | 'dynamic' | 'webgl'; devicePixelRatio: number; fontSize: number; fontFamily: string; diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index c6b6b1e5..881ea292 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -18,7 +18,7 @@ declare module 'xterm' { /** * A string representing a renderer type. */ - export type RendererType = 'dom' | 'canvas'; + export type RendererType = 'dom' | 'canvas' | 'webgl'; /** * An object containing start up options for the terminal. @@ -99,7 +99,7 @@ declare module 'xterm' { * Currently defaults to 'static'. This option may be removed in the future. If it is, passed * parameters will be ignored. */ - experimentalCharAtlas?: 'none' | 'static' | 'dynamic'; + experimentalCharAtlas?: 'none' | 'static' | 'dynamic' | 'webgl'; /** * (EXPERIMENTAL) Defines which implementation to use for buffer lines. @@ -213,7 +213,7 @@ declare module 'xterm' { cursor?: string, /** The accent color of the cursor (used as the foreground color for a block cursor) */ cursorAccent?: string, - /** The selection color (can be transparent) */ + /** The selection background color (can be transparent) */ selection?: string, /** ANSI black (eg. `\x1b[30m`) */ black?: string,