diff --git a/src/Terminal.ts b/src/Terminal.ts index 7083c0f0..8517fabc 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -47,7 +47,6 @@ import { removeTerminalFromCache } from './renderer/atlas/CharAtlasCache'; import { DomRenderer } from './renderer/dom/DomRenderer'; import { IKeyboardEvent } from './common/Types'; import { evaluateKeyboardEvent } from './core/input/Keyboard'; -import { WebglRenderer } from './renderer/webgl/WebglRenderer'; import { KeyboardResultType, ICharset, IBufferLine, IAttributeData } from './core/Types'; import { clone } from './common/Clone'; import { EventEmitter2, IEvent } from './common/EventEmitter2'; @@ -818,11 +817,16 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II } + public setRenderer(renderer: IRenderer): void { + this._renderCoordinator.setRenderer(renderer); + // this._renderCoordinator.onOptionsChanged(); + this.refresh(0, this.rows - 1); + } + private _createRenderer(): IRenderer { switch (this.options.rendererType) { - case 'canvas': return new Renderer(this, this._colorManager.colors); break; - case 'dom': return new DomRenderer(this, this._colorManager.colors); break; - case 'webgl': return new WebglRenderer(this, this._colorManager.colors); break; + case 'canvas': return new Renderer(this, this._colorManager.colors); + case 'dom': return new DomRenderer(this, this._colorManager.colors); default: throw new Error(`Unrecognized rendererType "${this.options.rendererType}"`); } } diff --git a/src/TestUtils.test.ts b/src/TestUtils.test.ts index f83e7d22..e95d4741 100644 --- a/src/TestUtils.test.ts +++ b/src/TestUtils.test.ts @@ -22,6 +22,9 @@ export class TestTerminal extends Terminal { } export class MockTerminal implements ITerminal { + setRenderer(renderer: any): void { + throw new Error('Method not implemented.'); + } onCursorMove: IEvent; onLineFeed: IEvent; onSelectionChange: IEvent; diff --git a/src/Types.ts b/src/Types.ts index 91bee283..8db46901 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -270,6 +270,7 @@ export interface IPublicTerminal extends IDisposable, IEventEmitter { setOption(key: string, value: any): void; refresh(start: number, end: number): void; reset(): void; + setRenderer(renderer: any): void; } export interface IBufferAccessor { diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index f1874a3f..ce469b97 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -10,6 +10,7 @@ import { Terminal as TerminalCore } from '../Terminal'; import * as Strings from '../Strings'; import { IEvent } from '../common/EventEmitter2'; import { AddonManager } from './AddonManager'; +import { WebglRendererAddon } from '../renderer/webgl/WebglRendererAddon'; export class Terminal implements ITerminalApi { private _core: ITerminal; @@ -183,6 +184,12 @@ export class Terminal implements ITerminalApi { public loadAddon(addon: ITerminalAddon): void { return this._addonManager.loadAddon(this, addon); } + public setRenderer(renderer: any): void { + this._core.setRenderer(renderer); + } + public loadWebgl(): void { + this.loadAddon(new WebglRendererAddon()); + } public static get strings(): ILocalizableStrings { return Strings; } diff --git a/src/renderer/atlas/CharAtlasCache.ts b/src/renderer/atlas/CharAtlasCache.ts index c5dcdc6b..30826d5b 100644 --- a/src/renderer/atlas/CharAtlasCache.ts +++ b/src/renderer/atlas/CharAtlasCache.ts @@ -9,15 +9,13 @@ import BaseCharAtlas from './BaseCharAtlas'; import DynamicCharAtlas from './DynamicCharAtlas'; import NoneCharAtlas from './NoneCharAtlas'; import StaticCharAtlas from './StaticCharAtlas'; -import WebglCharAtlas from '../webgl/WebglCharAtlas'; import { ICharAtlasConfig } from './Types'; import { IColorSet } from '../../ui/Types'; const charAtlasImplementations = { 'none': NoneCharAtlas, 'static': StaticCharAtlas, - 'dynamic': DynamicCharAtlas, - 'webgl': WebglCharAtlas + 'dynamic': DynamicCharAtlas }; interface ICharAtlasCacheEntry { diff --git a/src/renderer/webgl/GlyphRenderer.ts b/src/renderer/webgl/GlyphRenderer.ts index 698a2f19..95b1e325 100644 --- a/src/renderer/webgl/GlyphRenderer.ts +++ b/src/renderer/webgl/GlyphRenderer.ts @@ -6,7 +6,7 @@ import { createProgram, PROJECTION_MATRIX } from './WebglUtils'; import { IRenderDimensions } from '../Types'; import { ITerminal } from '../../Types'; -import WebglCharAtlas from './WebglCharAtlas'; +import WebglCharAtlas from './atlas/WebglCharAtlas'; import { IWebGL2RenderingContext, IWebGLVertexArrayObject, IRenderModel, IRasterizedGlyph } from './Types'; import { INDICIES_PER_CELL } from './WebglRenderer'; import { COMBINED_CHAR_BIT_MASK } from './RenderModel'; diff --git a/src/renderer/webgl/RectangleRenderer.ts b/src/renderer/webgl/RectangleRenderer.ts index 7aeb5880..62731bc5 100644 --- a/src/renderer/webgl/RectangleRenderer.ts +++ b/src/renderer/webgl/RectangleRenderer.ts @@ -8,8 +8,8 @@ import { IRenderDimensions } from '../Types'; import { createProgram, expandFloat32Array, PROJECTION_MATRIX } from './WebglUtils'; import { IRenderModel, IWebGLVertexArrayObject, IWebGL2RenderingContext, ISelectionRenderModel } from './Types'; import { fill } from '../../common/TypedArrayUtils'; -import { INVERTED_DEFAULT_COLOR } from '../atlas/Types'; -import { is256Color } from '../atlas/CharAtlasUtils'; +import { INVERTED_DEFAULT_COLOR } from './atlas/Types'; +import { is256Color } from './atlas/CharAtlasUtils'; import { DEFAULT_COLOR } from '../../common/Types'; import { IColorSet, IColor } from '../../ui/Types'; diff --git a/src/renderer/webgl/WebglRenderer.ts b/src/renderer/webgl/WebglRenderer.ts index b0d32e28..d93080d7 100644 --- a/src/renderer/webgl/WebglRenderer.ts +++ b/src/renderer/webgl/WebglRenderer.ts @@ -8,11 +8,11 @@ import { CharacterJoinerHandler, ITerminal } from '../../Types'; import { GlyphRenderer } from './GlyphRenderer'; import { LinkRenderLayer } from '../LinkRenderLayer'; import { CursorRenderLayer } from '../CursorRenderLayer'; -import { acquireCharAtlas } from '../atlas/CharAtlasCache'; -import WebglCharAtlas from './WebglCharAtlas'; +import { acquireCharAtlas } from './atlas/CharAtlasCache'; +import WebglCharAtlas from './atlas/WebglCharAtlas'; import { RectangleRenderer } from './RectangleRenderer'; import { IWebGL2RenderingContext } from './Types'; -import { INVERTED_DEFAULT_COLOR } from '../atlas/Types'; +import { INVERTED_DEFAULT_COLOR } from './atlas/Types'; import { RenderModel, COMBINED_CHAR_BIT_MASK } from './RenderModel'; import { Disposable } from '../../common/Lifecycle'; import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_ATTR_INDEX, NULL_CELL_CODE } from '../../core/buffer/BufferLine'; diff --git a/src/renderer/webgl/WebglRendererAddon.ts b/src/renderer/webgl/WebglRendererAddon.ts new file mode 100644 index 00000000..764caa0f --- /dev/null +++ b/src/renderer/webgl/WebglRendererAddon.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { Terminal, ITerminalAddon } from 'xterm'; +import { WebglRenderer } from './WebglRenderer'; + +export class WebglRendererAddon implements ITerminalAddon { + private _terminal: Terminal | undefined; + + constructor() {} + + public activate(terminal: Terminal): void { + if (!terminal.element) { + throw new Error('Cannot activate WebglRendererAddon before Terminal.open'); + } + this._terminal = terminal; + const core = (terminal as any)._core; + this._terminal.setRenderer(new WebglRenderer(core, core._colorManager.colors)); + } + + public dispose(): void { + throw new Error('WebglRendererAddon.dispose Not yet implemented'); + } +} diff --git a/src/renderer/webgl/atlas/BaseCharAtlas.ts b/src/renderer/webgl/atlas/BaseCharAtlas.ts new file mode 100644 index 00000000..ee69b381 --- /dev/null +++ b/src/renderer/webgl/atlas/BaseCharAtlas.ts @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IGlyphIdentifier } from './Types'; +import { IDisposable } from 'xterm'; + +export default abstract class BaseCharAtlas implements IDisposable { + private _didWarmUp: boolean = false; + + public dispose(): void { } + + /** + * Perform any work needed to warm the cache before it can be used. May be called multiple times. + * Implement _doWarmUp instead if you only want to get called once. + */ + public warmUp(): void { + if (!this._didWarmUp) { + this._doWarmUp(); + this._didWarmUp = true; + } + } + + /** + * Perform any work needed to warm the cache before it can be used. Used by the default + * implementation of warmUp(), and will only be called once. + */ + protected _doWarmUp(): void { } + + /** + * Called when we start drawing a new frame. + * + * TODO: We rely on this getting called by TextRenderLayer. This should really be called by + * Renderer instead, but we need to make Renderer the source-of-truth for the char atlas, instead + * of BaseRenderLayer. + */ + public beginFrame(): void { } + + /** + * May be called before warmUp finishes, however it is okay for the implementation to + * do nothing and return false in that case. + * + * @param ctx Where to draw the character onto. + * @param glyph Information about what to draw + * @param x The position on the context to start drawing at + * @param y The position on the context to start drawing at + * @returns The success state. True if we drew the character. + */ + public abstract draw( + ctx: CanvasRenderingContext2D, + glyph: IGlyphIdentifier, + x: number, + y: number + ): boolean; +} diff --git a/src/renderer/webgl/atlas/CharAtlasCache.ts b/src/renderer/webgl/atlas/CharAtlasCache.ts new file mode 100644 index 00000000..9d16893b --- /dev/null +++ b/src/renderer/webgl/atlas/CharAtlasCache.ts @@ -0,0 +1,94 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { ITerminal } from '../../../Types'; +import { generateConfig, configEquals } from './CharAtlasUtils'; +import BaseCharAtlas from './BaseCharAtlas'; +import WebglCharAtlas from './WebglCharAtlas'; +import { ICharAtlasConfig } from './Types'; +import { IColorSet } from '../../../ui/Types'; + +interface ICharAtlasCacheEntry { + atlas: BaseCharAtlas; + config: ICharAtlasConfig; + // N.B. This implementation potentially holds onto copies of the terminal forever, so + // this may cause memory leaks. + ownedBy: ITerminal[]; +} + +const charAtlasCache: ICharAtlasCacheEntry[] = []; + +/** + * Acquires a char atlas, either generating a new one or returning an existing + * one that is in use by another terminal. + * @param terminal The terminal. + * @param colors The colors to use. + */ +export function acquireCharAtlas( + terminal: ITerminal, + colors: IColorSet, + scaledCharWidth: number, + scaledCharHeight: number +): BaseCharAtlas { + const newConfig = generateConfig(scaledCharWidth, scaledCharHeight, terminal, colors); + + // Check to see if the terminal already owns this config + for (let i = 0; i < charAtlasCache.length; i++) { + const entry = charAtlasCache[i]; + const ownedByIndex = entry.ownedBy.indexOf(terminal); + if (ownedByIndex >= 0) { + if (configEquals(entry.config, newConfig)) { + return entry.atlas; + } + // 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); + } + break; + } + } + + // Try match a char atlas from the cache + for (let i = 0; i < charAtlasCache.length; i++) { + const entry = charAtlasCache[i]; + if (configEquals(entry.config, newConfig)) { + // Add the terminal to the cache entry and return + entry.ownedBy.push(terminal); + return entry.atlas; + } + } + + const newEntry: ICharAtlasCacheEntry = { + atlas: new WebglCharAtlas(document, newConfig), + config: newConfig, + ownedBy: [terminal] + }; + charAtlasCache.push(newEntry); + return newEntry.atlas; +} + +/** + * Removes a terminal reference from the cache, allowing its memory to be freed. + * @param terminal The terminal to remove. + */ +export function removeTerminalFromCache(terminal: ITerminal): void { + for (let i = 0; i < charAtlasCache.length; i++) { + const index = charAtlasCache[i].ownedBy.indexOf(terminal); + 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 + charAtlasCache[i].ownedBy.splice(index, 1); + } + break; + } + } +} diff --git a/src/renderer/webgl/atlas/CharAtlasGenerator.ts b/src/renderer/webgl/atlas/CharAtlasGenerator.ts new file mode 100644 index 00000000..e844f37a --- /dev/null +++ b/src/renderer/webgl/atlas/CharAtlasGenerator.ts @@ -0,0 +1,129 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { FontWeight } from 'xterm'; +import { isFirefox, isSafari } from '../../../common/Platform'; +import { ICharAtlasConfig, CHAR_ATLAS_CELL_SPACING } from './Types'; +import { IColor } from '../../../ui/Types'; + +/** + * Generates a char atlas. + * @param context The window or worker context. + * @param canvasFactory A function to generate a canvas with a width or height. + * @param config The config for the new char atlas. + */ +export function generateStaticCharAtlasTexture(context: Window, canvasFactory: (width: number, height: number) => HTMLCanvasElement, config: ICharAtlasConfig): HTMLCanvasElement | Promise { + const cellWidth = config.scaledCharWidth + CHAR_ATLAS_CELL_SPACING; + const cellHeight = config.scaledCharHeight + CHAR_ATLAS_CELL_SPACING; + const canvas = canvasFactory( + /*255 ascii chars*/255 * cellWidth, + (/*default+default bold*/2 + /*0-15*/16 + /*0-15 bold*/16) * cellHeight + ); + const ctx = canvas.getContext('2d', {alpha: config.allowTransparency}); + + ctx.fillStyle = config.colors.background.css; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + ctx.save(); + ctx.fillStyle = config.colors.foreground.css; + ctx.font = getFont(config.fontWeight, config); + ctx.textBaseline = 'middle'; + + // Default color + for (let i = 0; i < 256; i++) { + ctx.save(); + ctx.beginPath(); + ctx.rect(i * cellWidth, 0, cellWidth, cellHeight); + ctx.clip(); + ctx.fillText(String.fromCharCode(i), i * cellWidth, cellHeight / 2); + ctx.restore(); + } + // Default color bold + ctx.save(); + ctx.font = getFont(config.fontWeightBold, config); + for (let i = 0; i < 256; i++) { + ctx.save(); + ctx.beginPath(); + ctx.rect(i * cellWidth, cellHeight, cellWidth, cellHeight); + ctx.clip(); + ctx.fillText(String.fromCharCode(i), i * cellWidth, cellHeight * 1.5); + ctx.restore(); + } + ctx.restore(); + + // Colors 0-15 + ctx.font = getFont(config.fontWeight, config); + for (let colorIndex = 0; colorIndex < 16; colorIndex++) { + const y = (colorIndex + 2) * cellHeight; + // Draw ascii characters + for (let i = 0; i < 256; i++) { + ctx.save(); + ctx.beginPath(); + ctx.rect(i * cellWidth, y, cellWidth, cellHeight); + ctx.clip(); + ctx.fillStyle = config.colors.ansi[colorIndex].css; + ctx.fillText(String.fromCharCode(i), i * cellWidth, y + cellHeight / 2); + ctx.restore(); + } + } + + // Colors 0-15 bold + ctx.font = getFont(config.fontWeightBold, config); + for (let colorIndex = 0; colorIndex < 16; colorIndex++) { + const y = (colorIndex + 2 + 16) * cellHeight; + // Draw ascii characters + for (let i = 0; i < 256; i++) { + ctx.save(); + ctx.beginPath(); + ctx.rect(i * cellWidth, y, cellWidth, cellHeight); + ctx.clip(); + ctx.fillStyle = config.colors.ansi[colorIndex].css; + ctx.fillText(String.fromCharCode(i), i * cellWidth, y + cellHeight / 2); + ctx.restore(); + } + } + ctx.restore(); + + // Support is patchy for createImageBitmap at the moment, pass a canvas back + // if support is lacking as drawImage works there too. Firefox is also + // included here as ImageBitmap appears both buggy and has horrible + // performance (tested on v55). + if (!('createImageBitmap' in context) || isFirefox || isSafari) { + // Don't attempt to clear background colors if createImageBitmap is not supported + return canvas; + } + + const charAtlasImageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + + // Remove the background color from the image so characters may overlap + clearColor(charAtlasImageData, config.colors.background); + + return context.createImageBitmap(charAtlasImageData); +} + +/** + * Makes a partiicular rgb color in an ImageData completely transparent. + * @returns True if the result is "empty", meaning all pixels are fully transparent. + */ +export function clearColor(imageData: ImageData, color: IColor): boolean { + let isEmpty = true; + const r = color.rgba >>> 24; + const g = color.rgba >>> 16 & 0xFF; + const b = color.rgba >>> 8 & 0xFF; + for (let offset = 0; offset < imageData.data.length; offset += 4) { + if (imageData.data[offset] === r && + imageData.data[offset + 1] === g && + imageData.data[offset + 2] === b) { + imageData.data[offset + 3] = 0; + } else { + isEmpty = false; + } + } + return isEmpty; +} + +function getFont(fontWeight: FontWeight, config: ICharAtlasConfig): string { + return `${fontWeight} ${config.fontSize * config.devicePixelRatio}px ${config.fontFamily}`; +} diff --git a/src/renderer/webgl/atlas/CharAtlasUtils.ts b/src/renderer/webgl/atlas/CharAtlasUtils.ts new file mode 100644 index 00000000..b11ca085 --- /dev/null +++ b/src/renderer/webgl/atlas/CharAtlasUtils.ts @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { ITerminal } from '../../../Types'; +import { ICharAtlasConfig } from './Types'; +import { DEFAULT_COLOR } from '../../../common/Types'; +import { IColorSet } from '../../../ui/Types'; + +export function generateConfig(scaledCharWidth: number, scaledCharHeight: number, terminal: ITerminal, colors: IColorSet): ICharAtlasConfig { + // null out some fields that don't matter + const clonedColors = { + foreground: colors.foreground, + background: colors.background, + cursor: null, + cursorAccent: null, + selection: null, + // For the static char atlas, we only use the first 16 colors, but we need all 256 for the + // dynamic character atlas. + ansi: colors.ansi.slice(0, 16) + }; + return { + type: terminal.options.experimentalCharAtlas, + devicePixelRatio: window.devicePixelRatio, + scaledCharWidth, + scaledCharHeight, + fontFamily: terminal.options.fontFamily, + fontSize: terminal.options.fontSize, + fontWeight: terminal.options.fontWeight, + fontWeightBold: terminal.options.fontWeightBold, + allowTransparency: terminal.options.allowTransparency, + colors: clonedColors + }; +} + +export function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean { + for (let i = 0; i < a.colors.ansi.length; i++) { + if (a.colors.ansi[i].rgba !== b.colors.ansi[i].rgba) { + return false; + } + } + return a.type === b.type && + a.devicePixelRatio === b.devicePixelRatio && + a.fontFamily === b.fontFamily && + a.fontSize === b.fontSize && + a.fontWeight === b.fontWeight && + a.fontWeightBold === b.fontWeightBold && + a.allowTransparency === b.allowTransparency && + a.scaledCharWidth === b.scaledCharWidth && + a.scaledCharHeight === b.scaledCharHeight && + a.colors.foreground === b.colors.foreground && + a.colors.background === b.colors.background; +} + +export function is256Color(colorCode: number): boolean { + return colorCode < DEFAULT_COLOR; +} diff --git a/src/renderer/webgl/atlas/LRUMap.test.ts b/src/renderer/webgl/atlas/LRUMap.test.ts new file mode 100644 index 00000000..197d1159 --- /dev/null +++ b/src/renderer/webgl/atlas/LRUMap.test.ts @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { assert } from 'chai'; +import LRUMap from './LRUMap'; + +describe('LRUMap', () => { + it('can be used to store and retrieve values', () => { + const map = new LRUMap(10); + map.set(1, 'valuea'); + map.set(2, 'valueb'); + map.set(3, 'valuec'); + assert.strictEqual(map.get(1), 'valuea'); + assert.strictEqual(map.get(2), 'valueb'); + assert.strictEqual(map.get(3), 'valuec'); + }); + + it('maintains a size from insertions', () => { + const map = new LRUMap(10); + assert.strictEqual(map.size, 0); + map.set(1, 'value'); + assert.strictEqual(map.size, 1); + map.set(2, 'value'); + assert.strictEqual(map.size, 2); + }); + + it('deletes the oldest entry when the capacity is exceeded', () => { + const map = new LRUMap(4); + map.set(1, 'value'); + map.set(2, 'value'); + map.set(3, 'value'); + map.set(4, 'value'); + map.set(5, 'value'); + assert.isNull(map.get(1)); + assert.isNotNull(map.get(2)); + assert.isNotNull(map.get(3)); + assert.isNotNull(map.get(4)); + assert.isNotNull(map.get(5)); + assert.strictEqual(map.size, 4); + }); + + it('prevents a recently accessed entry from getting deleted', () => { + const map = new LRUMap(2); + map.set(1, 'value'); + map.set(2, 'value'); + map.get(1); + // a would normally get deleted here, except that we called get() + map.set(3, 'value'); + assert.isNotNull(map.get(1)); + // b got deleted instead of a + assert.isNull(map.get(2)); + assert.isNotNull(map.get(3)); + }); + + it('supports mutation', () => { + const map = new LRUMap(10); + map.set(1, 'oldvalue'); + map.set(1, 'newvalue'); + // mutation doesn't change the size + assert.strictEqual(map.size, 1); + assert.strictEqual(map.get(1), 'newvalue'); + }); +}); diff --git a/src/renderer/webgl/atlas/LRUMap.ts b/src/renderer/webgl/atlas/LRUMap.ts new file mode 100644 index 00000000..d7e01ec6 --- /dev/null +++ b/src/renderer/webgl/atlas/LRUMap.ts @@ -0,0 +1,136 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +interface ILinkedListNode { + prev: ILinkedListNode; + next: ILinkedListNode; + key: number; + value: T; +} + +export default class LRUMap { + private _map: { [key: number]: ILinkedListNode } = {}; + private _head: ILinkedListNode = null; + private _tail: ILinkedListNode = null; + private _nodePool: ILinkedListNode[] = []; + public size: number = 0; + + constructor(public capacity: number) { } + + private _unlinkNode(node: ILinkedListNode): void { + const prev = node.prev; + const next = node.next; + if (node === this._head) { + this._head = next; + } + if (node === this._tail) { + this._tail = prev; + } + if (prev !== null) { + prev.next = next; + } + if (next !== null) { + next.prev = prev; + } + } + + private _appendNode(node: ILinkedListNode): void { + const tail = this._tail; + if (tail !== null) { + tail.next = node; + } + node.prev = tail; + node.next = null; + this._tail = node; + if (this._head === null) { + this._head = node; + } + } + + /** + * Preallocate a bunch of linked-list nodes. Allocating these nodes ahead of time means that + * they're more likely to live next to each other in memory, which seems to improve performance. + * + * Each empty object only consumes about 60 bytes of memory, so this is pretty cheap, even for + * large maps. + */ + public prealloc(count: number): void { + const nodePool = this._nodePool; + for (let i = 0; i < count; i++) { + nodePool.push({ + prev: null, + next: null, + key: null, + value: null + }); + } + } + + public get(key: number): T | null { + // This is unsafe: We're assuming our keyspace doesn't overlap with Object.prototype. However, + // it's faster than calling hasOwnProperty, and in our case, it would never overlap. + const node = this._map[key]; + if (node !== undefined) { + this._unlinkNode(node); + this._appendNode(node); + return node.value; + } + return null; + } + + /** + * Gets a value from a key without marking it as the most recently used item. + */ + public peekValue(key: number): T | null { + const node = this._map[key]; + if (node !== undefined) { + return node.value; + } + return null; + } + + public peek(): T | null { + const head = this._head; + return head === null ? null : head.value; + } + + public set(key: number, value: T): void { + // This is unsafe: See note above. + let node = this._map[key]; + if (node !== undefined) { + // already exists, we just need to mutate it and move it to the end of the list + node = this._map[key]; + this._unlinkNode(node); + node.value = value; + } else if (this.size >= this.capacity) { + // we're out of space: recycle the head node, move it to the tail + node = this._head; + this._unlinkNode(node); + delete this._map[node.key]; + node.key = key; + node.value = value; + this._map[key] = node; + } else { + // make a new element + const nodePool = this._nodePool; + if (nodePool.length > 0) { + // use a preallocated node if we can + node = nodePool.pop(); + node.key = key; + node.value = value; + } else { + node = { + prev: null, + next: null, + key, + value + }; + } + this._map[key] = node; + this.size++; + } + this._appendNode(node); + } +} diff --git a/src/renderer/webgl/atlas/Types.ts b/src/renderer/webgl/atlas/Types.ts new file mode 100644 index 00000000..1bdaf3b9 --- /dev/null +++ b/src/renderer/webgl/atlas/Types.ts @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { FontWeight } from 'xterm'; +import { IColorSet } from '../../../ui/Types'; + +export const INVERTED_DEFAULT_COLOR = 257; +export const DIM_OPACITY = 0.5; + +export const CHAR_ATLAS_CELL_SPACING = 1; + +export interface IGlyphIdentifier { + chars: string; + code: number; + bg: number; + fg: number; + bold: boolean; + dim: boolean; + italic: boolean; +} + +export interface ICharAtlasConfig { + type: 'none' | 'static' | 'dynamic' | 'webgl'; + devicePixelRatio: number; + fontSize: number; + fontFamily: string; + fontWeight: FontWeight; + fontWeightBold: FontWeight; + scaledCharWidth: number; + scaledCharHeight: number; + allowTransparency: boolean; + colors: IColorSet; +} diff --git a/src/renderer/webgl/WebglCharAtlas.ts b/src/renderer/webgl/atlas/WebglCharAtlas.ts similarity index 96% rename from src/renderer/webgl/WebglCharAtlas.ts rename to src/renderer/webgl/atlas/WebglCharAtlas.ts index a6bd4013..5da955bc 100644 --- a/src/renderer/webgl/WebglCharAtlas.ts +++ b/src/renderer/webgl/atlas/WebglCharAtlas.ts @@ -3,16 +3,16 @@ * @license MIT */ -import { DIM_OPACITY, IGlyphIdentifier, INVERTED_DEFAULT_COLOR, ICharAtlasConfig } from '../atlas/Types'; -import BaseCharAtlas from '../atlas/BaseCharAtlas'; -import { IRasterizedGlyph, IBoundingBox, IRasterizedGlyphSet } from './Types'; -import { FLAGS } from '../Types'; -import { is256Color } from '../atlas/CharAtlasUtils'; -import { clearColor } from '../atlas/CharAtlasGenerator'; -import { DEFAULT_ATTR } from '../../core/buffer/BufferLine'; -import { DEFAULT_COLOR } from '../../common/Types'; -import { IColor } from '../../ui/Types'; -import { DEFAULT_ANSI_COLORS } from '../../ui/ColorManager'; +import { DIM_OPACITY, IGlyphIdentifier, INVERTED_DEFAULT_COLOR, ICharAtlasConfig } from './Types'; +import BaseCharAtlas from './BaseCharAtlas'; +import { IRasterizedGlyph, IBoundingBox, IRasterizedGlyphSet } from '../Types'; +import { FLAGS } from '../../Types'; +import { is256Color } from './CharAtlasUtils'; +import { clearColor } from './CharAtlasGenerator'; +import { DEFAULT_ATTR } from '../../../core/buffer/BufferLine'; +import { DEFAULT_COLOR } from '../../../common/Types'; +import { IColor } from '../../../ui/Types'; +import { DEFAULT_ANSI_COLORS } from '../../../ui/ColorManager'; // 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. diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index a0fb74ce..de4051ed 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' | 'webgl'; + export type RendererType = 'dom' | 'canvas'; /** * 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' | 'webgl'; + experimentalCharAtlas?: 'none' | 'static' | 'dynamic'; /** * The font size used to render text. @@ -890,6 +890,11 @@ declare module 'xterm' { * @param addon The addon to load. */ loadAddon(addon: ITerminalAddon): void; + + /** + * (EXPERIMENTAL) + */ + setRenderer(renderer: any): void; } /**