UNPKG

@xterm/addon-webgl

Version:

An addon for [xterm.js](https://github.com/xtermjs/xterm.js) that enables a WebGL2-based renderer. This addon requires xterm.js v4+.

383 lines (331 loc) 13.8 kB
/** * Copyright (c) 2018 The xterm.js authors. All rights reserved. * @license MIT */ import { IRenderDimensions } from 'browser/renderer/shared/Types'; import { IThemeService } from 'browser/services/Services'; import { ReadonlyColorSet } from 'browser/Types'; import { Attributes, FgFlags } from 'common/buffer/Constants'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { IColor } from 'common/Types'; import { Terminal } from '@xterm/xterm'; import { RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel'; import { IRenderModel, IWebGL2RenderingContext, IWebGLVertexArrayObject } from './Types'; import { createProgram, expandFloat32Array, PROJECTION_MATRIX } from './WebglUtils'; import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils'; 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 vec4 a_color; layout (location = ${VertexAttribLocations.UNIT_QUAD}) in vec2 a_unitquad; uniform mat4 u_projection; out vec4 v_color; void main() { vec2 zeroToOne = a_position + (a_unitquad * a_size); gl_Position = u_projection * vec4(zeroToOne, 0.0, 1.0); v_color = a_color; }`; const fragmentShaderSource = `#version 300 es precision lowp float; in vec4 v_color; out vec4 outColor; void main() { outColor = v_color; }`; 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; class Vertices { public attributes: Float32Array; public count: number; constructor() { this.attributes = new Float32Array(INITIAL_BUFFER_RECTANGLE_CAPACITY); this.count = 0; } } // Work variables to avoid garbage collection let $rgba = 0; let $x1 = 0; let $y1 = 0; let $r = 0; let $g = 0; let $b = 0; let $a = 0; export class RectangleRenderer extends Disposable { private _program: WebGLProgram; private _vertexArrayObject: IWebGLVertexArrayObject; private _attributesBuffer: WebGLBuffer; private _projectionLocation: WebGLUniformLocation; private _bgFloat!: Float32Array; private _cursorFloat!: Float32Array; private _vertices: Vertices = new Vertices(); private _verticesCursor: Vertices = new Vertices(); constructor( private _terminal: Terminal, private _gl: IWebGL2RenderingContext, private _dimensions: IRenderDimensions, private readonly _themeService: IThemeService ) { super(); const gl = this._gl; this._program = throwIfFalsy(createProgram(gl, vertexShaderSource, fragmentShaderSource)); this._register(toDisposable(() => gl.deleteProgram(this._program))); // Uniform locations this._projectionLocation = throwIfFalsy(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(); this._register(toDisposable(() => gl.deleteBuffer(unitQuadVerticesBuffer))); 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 // unitQuadVertices to allow is to draw 2 triangles from the vertices via a // triangle strip const unitQuadElementIndices = new Uint8Array([0, 1, 2, 3]); const elementIndicesBuffer = gl.createBuffer(); this._register(toDisposable(() => gl.deleteBuffer(elementIndicesBuffer))); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, elementIndicesBuffer); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, unitQuadElementIndices, gl.STATIC_DRAW); // Setup attributes this._attributesBuffer = throwIfFalsy(gl.createBuffer()); this._register(toDisposable(() => gl.deleteBuffer(this._attributesBuffer))); 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(_themeService.colors); this._register(this._themeService.onChangeColors(e => { this._updateCachedColors(e); this._updateViewportRectangle(); })); } public renderBackgrounds(): void { this._renderVertices(this._vertices); } public renderCursor(): void { this._renderVertices(this._verticesCursor); } private _renderVertices(vertices: Vertices): void { const gl = this._gl; gl.useProgram(this._program); gl.bindVertexArray(this._vertexArrayObject); gl.uniformMatrix4fv(this._projectionLocation, false, PROJECTION_MATRIX); // Bind attributes buffer and draw gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer); gl.bufferData(gl.ARRAY_BUFFER, vertices.attributes, gl.DYNAMIC_DRAW); gl.drawElementsInstanced(this._gl.TRIANGLE_STRIP, 4, gl.UNSIGNED_BYTE, 0, vertices.count); } public handleResize(): void { this._updateViewportRectangle(); } public setDimensions(dimensions: IRenderDimensions): void { this._dimensions = dimensions; } private _updateCachedColors(colors: ReadonlyColorSet): void { this._bgFloat = this._colorToFloat32Array(colors.background); this._cursorFloat = this._colorToFloat32Array(colors.cursor); } private _updateViewportRectangle(): void { // Set first rectangle that clears the screen this._addRectangleFloat( this._vertices.attributes, 0, 0, 0, this._terminal.cols * this._dimensions.device.cell.width, this._terminal.rows * this._dimensions.device.cell.height, this._bgFloat ); } public updateBackgrounds(model: IRenderModel): void { const terminal = this._terminal; const vertices = this._vertices; // Declare variable ahead of time to avoid garbage collection let rectangleCount = 1; let y: number; let x: number; let currentStartX: number; let currentBg: number; let currentFg: number; let currentInverse: boolean; let modelIndex: number; let bg: number; let fg: number; let inverse: boolean; let offset: number; for (y = 0; y < terminal.rows; y++) { currentStartX = -1; currentBg = 0; currentFg = 0; currentInverse = false; for (x = 0; x < terminal.cols; x++) { modelIndex = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL; bg = model.cells[modelIndex + RENDER_MODEL_BG_OFFSET]; fg = model.cells[modelIndex + RENDER_MODEL_FG_OFFSET]; inverse = !!(fg & FgFlags.INVERSE); if (bg !== currentBg || (fg !== currentFg && (currentInverse || inverse))) { // A rectangle needs to be drawn if going from non-default to another color if (currentBg !== 0 || (currentInverse && currentFg !== 0)) { offset = rectangleCount++ * INDICES_PER_RECTANGLE; this._updateRectangle(vertices, offset, currentFg, currentBg, currentStartX, x, y); } currentStartX = x; currentBg = bg; currentFg = fg; currentInverse = inverse; } } // Finish rectangle if it's still going if (currentBg !== 0 || (currentInverse && currentFg !== 0)) { offset = rectangleCount++ * INDICES_PER_RECTANGLE; this._updateRectangle(vertices, offset, currentFg, currentBg, currentStartX, terminal.cols, y); } } vertices.count = rectangleCount; } public updateCursor(model: IRenderModel): void { const vertices = this._verticesCursor; const cursor = model.cursor; if (!cursor || cursor.style === 'block') { vertices.count = 0; return; } let offset: number; let rectangleCount = 0; if (cursor.style === 'bar' || cursor.style === 'outline') { // Left edge offset = rectangleCount++ * INDICES_PER_RECTANGLE; this._addRectangleFloat( vertices.attributes, offset, cursor.x * this._dimensions.device.cell.width, cursor.y * this._dimensions.device.cell.height, cursor.style === 'bar' ? cursor.dpr * cursor.cursorWidth : cursor.dpr, this._dimensions.device.cell.height, this._cursorFloat ); } if (cursor.style === 'underline' || cursor.style === 'outline') { // Bottom edge offset = rectangleCount++ * INDICES_PER_RECTANGLE; this._addRectangleFloat( vertices.attributes, offset, cursor.x * this._dimensions.device.cell.width, (cursor.y + 1) * this._dimensions.device.cell.height - cursor.dpr, cursor.width * this._dimensions.device.cell.width, cursor.dpr, this._cursorFloat ); } if (cursor.style === 'outline') { // Top edge offset = rectangleCount++ * INDICES_PER_RECTANGLE; this._addRectangleFloat( vertices.attributes, offset, cursor.x * this._dimensions.device.cell.width, cursor.y * this._dimensions.device.cell.height, cursor.width * this._dimensions.device.cell.width, cursor.dpr, this._cursorFloat ); // Right edge offset = rectangleCount++ * INDICES_PER_RECTANGLE; this._addRectangleFloat( vertices.attributes, offset, (cursor.x + cursor.width) * this._dimensions.device.cell.width - cursor.dpr, cursor.y * this._dimensions.device.cell.height, cursor.dpr, this._dimensions.device.cell.height, this._cursorFloat ); } vertices.count = rectangleCount; } private _updateRectangle(vertices: Vertices, offset: number, fg: number, bg: number, startX: number, endX: number, y: number): void { if (fg & FgFlags.INVERSE) { switch (fg & Attributes.CM_MASK) { case Attributes.CM_P16: case Attributes.CM_P256: $rgba = this._themeService.colors.ansi[fg & Attributes.PCOLOR_MASK].rgba; break; case Attributes.CM_RGB: $rgba = (fg & Attributes.RGB_MASK) << 8; break; case Attributes.CM_DEFAULT: default: $rgba = this._themeService.colors.foreground.rgba; } } else { switch (bg & Attributes.CM_MASK) { case Attributes.CM_P16: case Attributes.CM_P256: $rgba = this._themeService.colors.ansi[bg & Attributes.PCOLOR_MASK].rgba; break; case Attributes.CM_RGB: $rgba = (bg & Attributes.RGB_MASK) << 8; break; case Attributes.CM_DEFAULT: default: $rgba = this._themeService.colors.background.rgba; } } if (vertices.attributes.length < offset + 4) { vertices.attributes = expandFloat32Array(vertices.attributes, this._terminal.rows * this._terminal.cols * INDICES_PER_RECTANGLE); } $x1 = startX * this._dimensions.device.cell.width; $y1 = y * this._dimensions.device.cell.height; $r = (($rgba >> 24) & 0xFF) / 255; $g = (($rgba >> 16) & 0xFF) / 255; $b = (($rgba >> 8 ) & 0xFF) / 255; $a = 1; this._addRectangle(vertices.attributes, offset, $x1, $y1, (endX - startX) * this._dimensions.device.cell.width, this._dimensions.device.cell.height, $r, $g, $b, $a); } 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 / this._dimensions.device.canvas.width; array[offset + 1] = y1 / this._dimensions.device.canvas.height; array[offset + 2] = width / this._dimensions.device.canvas.width; array[offset + 3] = height / this._dimensions.device.canvas.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 / this._dimensions.device.canvas.width; array[offset + 1] = y1 / this._dimensions.device.canvas.height; array[offset + 2] = width / this._dimensions.device.canvas.width; array[offset + 3] = height / this._dimensions.device.canvas.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 ]); } }