diff --git a/src/Terminal.ts b/src/Terminal.ts index 48792ac5..ddb5893c 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -38,7 +38,6 @@ import { SoundService } from 'browser/services/SoundService'; import { MouseZoneManager } from 'browser/MouseZoneManager'; import { AccessibilityManager } from './AccessibilityManager'; import { ITheme, IMarker, IDisposable, ISelectionPosition } from 'xterm'; -import { removeTerminalFromCache } from 'browser/renderer/atlas/CharAtlasCache'; import { DomRenderer } from './renderer/dom/DomRenderer'; import { IKeyboardEvent, KeyboardResultType, ICharset, IBufferLine, IAttributeData } from 'common/Types'; import { evaluateKeyboardEvent } from 'common/input/Keyboard'; @@ -264,8 +263,8 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._windowsMode.dispose(); this._windowsMode = undefined; } + this._renderService.dispose(); this._customKeyEventHandler = null; - removeTerminalFromCache(this); this.write = () => {}; if (this.element && this.element.parentNode) { this.element.parentNode.removeChild(this.element); diff --git a/src/browser/renderer/atlas/CharAtlasCache.ts b/src/browser/renderer/atlas/CharAtlasCache.ts index 92c970f2..257835ba 100644 --- a/src/browser/renderer/atlas/CharAtlasCache.ts +++ b/src/browser/renderer/atlas/CharAtlasCache.ts @@ -8,13 +8,14 @@ import { BaseCharAtlas } from 'browser/renderer/atlas/BaseCharAtlas'; import { DynamicCharAtlas } from 'browser/renderer/atlas/DynamicCharAtlas'; import { ICharAtlasConfig } from 'browser/renderer/atlas/Types'; import { IColorSet } from 'browser/Types'; +import { ITerminalOptions } from 'common/services/Services'; 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: any[]; + ownedBy: number[]; } const charAtlasCache: ICharAtlasCacheEntry[] = []; @@ -22,26 +23,25 @@ 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: any, + options: ITerminalOptions, + rendererId: number, colors: IColorSet, scaledCharWidth: number, scaledCharHeight: number ): BaseCharAtlas { - const newConfig = generateConfig(scaledCharWidth, scaledCharHeight, terminal.optionsService.options, colors); + const newConfig = generateConfig(scaledCharWidth, scaledCharHeight, options, colors); - // Check to see if the terminal already owns this config + // Check to see if the renderer already owns this config for (let i = 0; i < charAtlasCache.length; i++) { const entry = charAtlasCache[i]; - const ownedByIndex = entry.ownedBy.indexOf(terminal); + const ownedByIndex = entry.ownedBy.indexOf(rendererId); if (ownedByIndex >= 0) { if (configEquals(entry.config, newConfig)) { return entry.atlas; } - // The configs differ, release the terminal from the entry + // The configs differ, release the renderer from the entry if (entry.ownedBy.length === 1) { entry.atlas.dispose(); charAtlasCache.splice(i, 1); @@ -56,8 +56,8 @@ export function acquireCharAtlas( 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); + // Add the renderer to the cache entry and return + entry.ownedBy.push(rendererId); return entry.atlas; } } @@ -68,7 +68,7 @@ export function acquireCharAtlas( newConfig ), config: newConfig, - ownedBy: [terminal] + ownedBy: [rendererId] }; charAtlasCache.push(newEntry); return newEntry.atlas; @@ -76,14 +76,13 @@ export function acquireCharAtlas( /** * Removes a terminal reference from the cache, allowing its memory to be freed. - * @param terminal The terminal to remove. */ -export function removeTerminalFromCache(terminal: any): void { +export function removeTerminalFromCache(rendererId: number): void { for (let i = 0; i < charAtlasCache.length; i++) { - const index = charAtlasCache[i].ownedBy.indexOf(terminal); + const index = charAtlasCache[i].ownedBy.indexOf(rendererId); if (index !== -1) { if (charAtlasCache[i].ownedBy.length === 1) { - // Remove the cache entry if it's the only terminal + // Remove the cache entry if it's the only renderer charAtlasCache[i].atlas.dispose(); charAtlasCache.splice(i, 1); } else { diff --git a/src/browser/services/RenderService.ts b/src/browser/services/RenderService.ts index ecf20e28..c0b3c0e5 100644 --- a/src/browser/services/RenderService.ts +++ b/src/browser/services/RenderService.ts @@ -103,6 +103,10 @@ export class RenderService extends Disposable implements IRenderService { this._onDimensionsChange.fire(this._renderer.dimensions); } + public dispose(): void { + this._renderer.dispose(); + } + public setRenderer(renderer: IRenderer): void { // TODO: RenderCoordinator should be the only one to dispose the renderer this._renderer.dispose(); diff --git a/src/browser/services/Services.ts b/src/browser/services/Services.ts index 863f3a9e..2170a04e 100644 --- a/src/browser/services/Services.ts +++ b/src/browser/services/Services.ts @@ -8,6 +8,7 @@ import { IRenderDimensions, IRenderer, CharacterJoinerHandler } from 'browser/re import { IColorSet } from 'browser/Types'; import { ISelectionRedrawRequestEvent } from 'browser/selection/Types'; import { createDecorator } from 'common/services/ServiceRegistry'; +import { IDisposable } from 'common/Types'; export const ICharSizeService = createDecorator('CharSizeService'); export interface ICharSizeService { @@ -31,7 +32,7 @@ export interface IMouseService { } export const IRenderService = createDecorator('RenderService'); -export interface IRenderService { +export interface IRenderService extends IDisposable { serviceBrand: any; onDimensionsChange: IEvent; diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 929651fa..4fd45e30 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -5,7 +5,6 @@ import { IRenderLayer } from './Types'; import { IRenderDimensions } from 'browser/renderer/Types'; -import { ITerminal } from '../Types'; import { ICellData } from 'common/Types'; import { DEFAULT_COLOR, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE } from 'common/buffer/Constants'; import { IGlyphIdentifier } from 'browser/renderer/atlas/Types'; @@ -48,7 +47,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { zIndex: number, private _alpha: boolean, protected _colors: IColorSet, - protected _terminal: ITerminal, + private _rendererId: number, protected readonly _bufferService: IBufferService, protected readonly _optionsService: IOptionsService ) { @@ -112,7 +111,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { if (this._scaledCharWidth <= 0 && this._scaledCharHeight <= 0) { return; } - this._charAtlas = acquireCharAtlas(this._terminal, colorSet, this._scaledCharWidth, this._scaledCharHeight); + this._charAtlas = acquireCharAtlas(this._optionsService.options, this._rendererId, colorSet, this._scaledCharWidth, this._scaledCharHeight); this._charAtlas.warmUp(); } diff --git a/src/renderer/CursorRenderLayer.ts b/src/renderer/CursorRenderLayer.ts index 57f7bcd5..9d00864a 100644 --- a/src/renderer/CursorRenderLayer.ts +++ b/src/renderer/CursorRenderLayer.ts @@ -34,11 +34,12 @@ export class CursorRenderLayer extends BaseRenderLayer { container: HTMLElement, zIndex: number, colors: IColorSet, - terminal: ITerminal, + private _terminal: ITerminal, + rendererId: number, readonly bufferService: IBufferService, readonly optionsService: IOptionsService ) { - super(container, 'cursor', zIndex, true, colors, terminal, bufferService, optionsService); + super(container, 'cursor', zIndex, true, colors, rendererId, bufferService, optionsService); this._state = { x: null, y: null, diff --git a/src/renderer/LinkRenderLayer.ts b/src/renderer/LinkRenderLayer.ts index f225f745..51b77396 100644 --- a/src/renderer/LinkRenderLayer.ts +++ b/src/renderer/LinkRenderLayer.ts @@ -3,7 +3,6 @@ * @license MIT */ -import { ITerminal } from '../Types'; import { IRenderDimensions } from 'browser/renderer/Types'; import { BaseRenderLayer } from './BaseRenderLayer'; import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants'; @@ -18,12 +17,12 @@ export class LinkRenderLayer extends BaseRenderLayer { container: HTMLElement, zIndex: number, colors: IColorSet, - terminal: ITerminal, + rendererId: number, linkifier: ILinkifier, readonly bufferService: IBufferService, readonly optionsService: IOptionsService ) { - super(container, 'link', zIndex, true, colors, terminal, bufferService, optionsService); + super(container, 'link', zIndex, true, colors, rendererId, bufferService, optionsService); linkifier.onLinkHover(e => this._onLinkHover(e)); linkifier.onLinkLeave(e => this._onLinkLeave(e)); } diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index 03c44817..c7154c02 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -15,8 +15,13 @@ import { Disposable } from 'common/Lifecycle'; import { IColorSet } from 'browser/Types'; import { ICharSizeService } from 'browser/services/Services'; import { IBufferService, IOptionsService } from 'common/services/Services'; +import { removeTerminalFromCache } from 'browser/renderer/atlas/CharAtlasCache'; + +let nextRendererId = 1; export class Renderer extends Disposable implements IRenderer { + private _id = nextRendererId++; + private _renderLayers: IRenderLayer[]; private _devicePixelRatio: number; private _characterJoinerRegistry: ICharacterJoinerRegistry; @@ -35,10 +40,10 @@ export class Renderer extends Disposable implements IRenderer { this._characterJoinerRegistry = new CharacterJoinerRegistry(bufferService); this._renderLayers = [ - new TextRenderLayer(this._terminal.screenElement, 0, this._colors, this._characterJoinerRegistry, allowTransparency, this._terminal, bufferService, optionsService), - new SelectionRenderLayer(this._terminal.screenElement, 1, this._colors, this._terminal, bufferService, optionsService), - new LinkRenderLayer(this._terminal.screenElement, 2, this._colors, this._terminal, this._terminal.linkifier, bufferService, optionsService), - new CursorRenderLayer(this._terminal.screenElement, 3, this._colors, this._terminal, bufferService, optionsService) + new TextRenderLayer(this._terminal.screenElement, 0, this._colors, this._characterJoinerRegistry, allowTransparency, this._id, bufferService, optionsService), + new SelectionRenderLayer(this._terminal.screenElement, 1, this._colors, this._id, bufferService, optionsService), + new LinkRenderLayer(this._terminal.screenElement, 2, this._colors, this._id, this._terminal.linkifier, bufferService, optionsService), + new CursorRenderLayer(this._terminal.screenElement, 3, this._colors, this._terminal, this._id, bufferService, optionsService) ]; this.dimensions = { scaledCharWidth: null, @@ -62,6 +67,7 @@ export class Renderer extends Disposable implements IRenderer { public dispose(): void { super.dispose(); this._renderLayers.forEach(l => l.dispose()); + removeTerminalFromCache(this._id); } public onDevicePixelRatioChange(): void { diff --git a/src/renderer/SelectionRenderLayer.ts b/src/renderer/SelectionRenderLayer.ts index 97a38645..4c59594e 100644 --- a/src/renderer/SelectionRenderLayer.ts +++ b/src/renderer/SelectionRenderLayer.ts @@ -3,7 +3,6 @@ * @license MIT */ -import { ITerminal } from '../Types'; import { IRenderDimensions } from 'browser/renderer/Types'; import { BaseRenderLayer } from './BaseRenderLayer'; import { IColorSet } from 'browser/Types'; @@ -23,11 +22,11 @@ export class SelectionRenderLayer extends BaseRenderLayer { container: HTMLElement, zIndex: number, colors: IColorSet, - terminal: ITerminal, + rendererId: number, readonly bufferService: IBufferService, readonly optionsService: IOptionsService ) { - super(container, 'selection', zIndex, true, colors, terminal, bufferService, optionsService); + super(container, 'selection', zIndex, true, colors, rendererId, bufferService, optionsService); this._clearState(); } @@ -72,10 +71,10 @@ export class SelectionRenderLayer extends BaseRenderLayer { const viewportStartRow = start[1] - this._bufferService.buffer.ydisp; const viewportEndRow = end[1] - this._bufferService.buffer.ydisp; const viewportCappedStartRow = Math.max(viewportStartRow, 0); - const viewportCappedEndRow = Math.min(viewportEndRow, this._terminal.rows - 1); + const viewportCappedEndRow = Math.min(viewportEndRow, this._bufferService.rows - 1); // No need to draw the selection - if (viewportCappedStartRow >= this._terminal.rows || viewportCappedEndRow < 0) { + if (viewportCappedStartRow >= this._bufferService.rows || viewportCappedEndRow < 0) { return; } @@ -89,17 +88,17 @@ export class SelectionRenderLayer extends BaseRenderLayer { } else { // Draw first row const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0; - const startRowEndCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : this._terminal.cols; + const startRowEndCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : this._bufferService.cols; this._fillCells(startCol, viewportCappedStartRow, startRowEndCol - startCol, 1); // Draw middle rows const middleRowsCount = Math.max(viewportCappedEndRow - viewportCappedStartRow - 1, 0); - this._fillCells(0, viewportCappedStartRow + 1, this._terminal.cols, middleRowsCount); + this._fillCells(0, viewportCappedStartRow + 1, this._bufferService.cols, middleRowsCount); // Draw final row if (viewportCappedStartRow !== viewportCappedEndRow) { // Only draw viewportEndRow if it's not the same as viewportStartRow - const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : this._terminal.cols; + const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : this._bufferService.cols; this._fillCells(0, viewportCappedEndRow, endCol, 1); } } diff --git a/src/renderer/TextRenderLayer.ts b/src/renderer/TextRenderLayer.ts index e3035e97..06e334ef 100644 --- a/src/renderer/TextRenderLayer.ts +++ b/src/renderer/TextRenderLayer.ts @@ -4,7 +4,6 @@ */ import { ICharacterJoinerRegistry, IRenderDimensions } from 'browser/renderer/Types'; -import { ITerminal } from '../Types'; import { CharData, ICellData } from 'common/Types'; import { GridCache } from 'browser/renderer/GridCache'; import { BaseRenderLayer } from './BaseRenderLayer'; @@ -36,11 +35,11 @@ export class TextRenderLayer extends BaseRenderLayer { colors: IColorSet, characterJoinerRegistry: ICharacterJoinerRegistry, alpha: boolean, - terminal: ITerminal, + rendererId: number, readonly bufferService: IBufferService, readonly optionsService: IOptionsService ) { - super(container, 'text', zIndex, alpha, colors, terminal, bufferService, optionsService); + super(container, 'text', zIndex, alpha, colors, rendererId, bufferService, optionsService); this._state = new GridCache(); this._characterJoinerRegistry = characterJoinerRegistry; } @@ -57,7 +56,7 @@ export class TextRenderLayer extends BaseRenderLayer { } // Resizing the canvas discards the contents of the canvas so clear state this._state.clear(); - this._state.resize(this._terminal.cols, this._terminal.rows); + this._state.resize(this._bufferService.cols, this._bufferService.rows); } public reset(): void { @@ -76,10 +75,10 @@ export class TextRenderLayer extends BaseRenderLayer { ) => void ): void { for (let y = firstRow; y <= lastRow; y++) { - const row = y + this._terminal.buffer.ydisp; - const line = this._terminal.buffer.lines.get(row); + const row = y + this._bufferService.buffer.ydisp; + const line = this._bufferService.buffer.lines.get(row); const joinedRanges = joinerRegistry ? joinerRegistry.getJoinedCharacters(row) : []; - for (let x = 0; x < this._terminal.cols; x++) { + for (let x = 0; x < this._bufferService.cols; x++) { line.loadCell(x, this._workCell); let cell = this._workCell; @@ -154,7 +153,7 @@ export class TextRenderLayer extends BaseRenderLayer { */ private _drawBackground(firstRow: number, lastRow: number): void { const ctx = this._ctx; - const cols = this._terminal.cols; + const cols = this._bufferService.cols; let startX: number = 0; let startY: number = 0; let prevFillStyle: string | null = null; @@ -235,7 +234,7 @@ export class TextRenderLayer extends BaseRenderLayer { this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`; } else { let fg = cell.getFgColor(); - if (this._terminal.options.drawBoldTextInBrightColors && cell.isBold() && fg < 8) { + if (this._optionsService.options.drawBoldTextInBrightColors && cell.isBold() && fg < 8) { fg += 8; } this._ctx.fillStyle = this._colors.ansi[fg].css; @@ -258,13 +257,13 @@ export class TextRenderLayer extends BaseRenderLayer { this._charAtlas.beginFrame(); } - this._clearCells(0, firstRow, this._terminal.cols, lastRow - firstRow + 1); + this._clearCells(0, firstRow, this._bufferService.cols, lastRow - firstRow + 1); this._drawBackground(firstRow, lastRow); this._drawForeground(firstRow, lastRow); } public onOptionsChanged(): void { - this._setTransparency(this._terminal.options.allowTransparency); + this._setTransparency(this._optionsService.options.allowTransparency); } /**