From b853aa492914bbaef91a73eeae0c904b621bfd9e Mon Sep 17 00:00:00 2001 From: Benjamin Woodruff Date: Wed, 14 Mar 2018 10:05:29 -0700 Subject: [PATCH 01/10] Separate foreground & background rendering passes I'm primarily interested in doing this because it'll allow me to optimize the background rendering in later diffs, but this may also make it possible to fix some minor rendering issues. Right now, if we draw a single-width character that overflows its cell's bounds, and the character to the right of it has a background, that background may cover up the first character's foreground. By drawing the background in a separate pass, we can avoid those cases. There's still some issues with how dirty regions are computed that makes rendering stuff like that flaky, but this at least gets us closer to "correct" rendering. Other terminal emulators (e.g. alacritty) render the foreground and background in separate passes: https://github.com/jwilm/alacritty/blob/1b7ffea/src/renderer/mod.rs#L766 --- src/renderer/TextRenderLayer.ts | 155 +++++++++++++++----------------- 1 file changed, 72 insertions(+), 83 deletions(-) diff --git a/src/renderer/TextRenderLayer.ts b/src/renderer/TextRenderLayer.ts index 2487a294..8b061511 100644 --- a/src/renderer/TextRenderLayer.ts +++ b/src/renderer/TextRenderLayer.ts @@ -48,21 +48,24 @@ export class TextRenderLayer extends BaseRenderLayer { this.clearAll(); } - public onGridChanged(terminal: ITerminal, startRow: number, endRow: number): void { - // Resize has not been called yet - if (this._state.cache.length === 0) { - return; - } - + private _forEachCell( + terminal: ITerminal, + startRow: number, + endRow: number, + callback: ( + code: number, + char: string, + width: number, + x: number, + y: number, + fg: number, + bg: number, + flags: number + ) => void + ): void { for (let y = startRow; y <= endRow; y++) { const row = y + terminal.buffer.ydisp; const line = terminal.buffer.lines.get(row); - - this.clearCells(0, y, terminal.cols, 1); - // for (let x = 0; x < terminal.cols; x++) { - // this._state.cache[x][y] = null; - // } - for (let x = 0; x < terminal.cols; x++) { const charData = line[x]; const code: number = charData[CHAR_DATA_CODE_INDEX]; @@ -73,51 +76,12 @@ export class TextRenderLayer extends BaseRenderLayer { // The character to the left is a wide character, drawing is owned by // the char at x-1 if (width === 0) { - // this._state.cache[x][y] = null; - continue; - } - - // If the character is a space and the character to the left is an - // overlapping character, skip the character and allow the overlapping - // char to take full control over this character's cell. - if (code === 32 /*' '*/) { - if (x > 0) { - const previousChar: CharData = line[x - 1]; - if (this._isOverlapping(previousChar)) { - continue; - } - } - } - - // Skip rendering if the character is identical - // const state = this._state.cache[x][y]; - // if (state && state[CHAR_DATA_CHAR_INDEX] === char && state[CHAR_DATA_ATTR_INDEX] === attr) { - // // Skip render, contents are identical - // this._state.cache[x][y] = charData; - // continue; - // } - - // Clear the old character was not a space with the default background - // const wasInverted = !!(state && state[CHAR_DATA_ATTR_INDEX] && state[CHAR_DATA_ATTR_INDEX] >> 18 & FLAGS.INVERSE); - // if (state && !(state[CHAR_DATA_CODE_INDEX] === 32 /*' '*/ && (state[CHAR_DATA_ATTR_INDEX] & 0x1ff) >= 256 && !wasInverted)) { - // this._clearChar(x, y); - // } - // this._state.cache[x][y] = charData; - - const flags = attr >> 18; - let bg = attr & 0x1ff; - - // Skip rendering if the character is invisible - const isDefaultBackground = bg >= 256; - const isInvisible = flags & FLAGS.INVISIBLE; - const isInverted = flags & FLAGS.INVERSE; - if (!code || (code === 32 /*' '*/ && isDefaultBackground && !isInverted) || isInvisible) { continue; } // If the character is an overlapping char and the character to the right is a // space, take ownership of the cell to the right. - if (width !== 0 && this._isOverlapping(charData)) { + if (this._isOverlapping(charData)) { // If the character is overlapping, we want to force a re-render on every // frame. This is specifically to work around the case where two // overlaping chars `a` and `b` are adjacent, the cursor is moved to b and a @@ -135,10 +99,12 @@ export class TextRenderLayer extends BaseRenderLayer { } } + 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 (isInverted) { + if (flags & FLAGS.INVERSE) { const temp = bg; bg = fg; fg = temp; @@ -150,47 +116,70 @@ export class TextRenderLayer extends BaseRenderLayer { } } - // Clear the cell next to this character if it's wide - if (width === 2) { - // this.clearCells(x + 1, y, 1, 1); - } - - // Draw background - if (bg < 256) { - this._ctx.save(); - this._ctx.fillStyle = (bg === INVERTED_DEFAULT_COLOR ? this._colors.foreground.css : this._colors.ansi[bg].css); - this.fillCells(x, y, width, 1); - this._ctx.restore(); - } - - this._ctx.save(); if (flags & FLAGS.BOLD) { - this._ctx.font = this._getFont(terminal, true); // Convert the FG color to the bold variant if (fg < 8) { fg += 8; } } - if (flags & FLAGS.UNDERLINE) { - if (fg === INVERTED_DEFAULT_COLOR) { - this._ctx.fillStyle = this._colors.background.css; - } else if (fg < 256) { - // 256 color support - this._ctx.fillStyle = this._colors.ansi[fg].css; - } else { - this._ctx.fillStyle = this._colors.foreground.css; - } - this.fillBottomLineAtCells(x, y); - } - - this.drawChar(terminal, char, code, width, x, y, fg, bg, !!(flags & FLAGS.BOLD), !!(flags & FLAGS.DIM)); - - this._ctx.restore(); + callback(code, char, width, x, y, fg, bg, flags); } } } + private _drawBackground(terminal: ITerminal, startRow: number, endRow: number): void { + this._forEachCell(terminal, startRow, endRow, (code, char, width, x, y, fg, bg, flags) => { + // libvte and xterm both draw the background (but not foreground) of invisible characters, + // so we should too. + const isDefaultBackground = bg >= 256; + if (!isDefaultBackground) { + this._ctx.save(); + this._ctx.fillStyle = (bg === INVERTED_DEFAULT_COLOR ? this._colors.foreground.css : this._colors.ansi[bg].css); + this.fillCells(x, y, width, 1); + this._ctx.restore(); + } + }); + } + + private _drawForeground(terminal: ITerminal, startRow: number, endRow: number): void { + this._forEachCell(terminal, startRow, endRow, (code, char, width, x, y, fg, bg, flags) => { + if (flags & FLAGS.INVISIBLE) { + return; + } + if (flags & FLAGS.UNDERLINE) { + this._ctx.save(); + if (fg === INVERTED_DEFAULT_COLOR) { + this._ctx.fillStyle = this._colors.background.css; + } else if (fg < 256) { + // 256 color support + this._ctx.fillStyle = this._colors.ansi[fg].css; + } else { + this._ctx.fillStyle = this._colors.foreground.css; + } + this.fillBottomLineAtCells(x, y); + this._ctx.restore(); + } + this.drawChar( + terminal, char, code, + width, x, y, + fg, bg, + !!(flags & FLAGS.BOLD), !!(flags & FLAGS.DIM) + ); + }); + } + + public onGridChanged(terminal: ITerminal, startRow: number, endRow: number): void { + // Resize has not been called yet + if (this._state.cache.length === 0) { + return; + } + + this.clearCells(0, startRow, terminal.cols, endRow - startRow + 1); // endRow is inclusive + this._drawBackground(terminal, startRow, endRow); + this._drawForeground(terminal, startRow, endRow); + } + public onOptionsChanged(terminal: ITerminal): void { this.setTransparency(terminal, terminal.options.allowTransparency); } From 0fe59df2e91e766f5bcec2e7e2479cb32a32bd3b Mon Sep 17 00:00:00 2001 From: Benjamin Woodruff Date: Wed, 18 Apr 2018 20:22:55 -0700 Subject: [PATCH 02/10] Rename startRow/endRow in TextRenderLayer Renaming these to firstRow/lastRow makes the inclusivity of the range clearer. Addresses this comment: https://github.com/xtermjs/xterm.js/pull/1393#discussion_r182471582 --- src/renderer/TextRenderLayer.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/renderer/TextRenderLayer.ts b/src/renderer/TextRenderLayer.ts index 8b061511..58d0d790 100644 --- a/src/renderer/TextRenderLayer.ts +++ b/src/renderer/TextRenderLayer.ts @@ -50,8 +50,8 @@ export class TextRenderLayer extends BaseRenderLayer { private _forEachCell( terminal: ITerminal, - startRow: number, - endRow: number, + firstRow: number, + lastRow: number, callback: ( code: number, char: string, @@ -63,7 +63,7 @@ export class TextRenderLayer extends BaseRenderLayer { flags: number ) => void ): void { - for (let y = startRow; y <= endRow; y++) { + for (let y = firstRow; y <= lastRow; y++) { const row = y + terminal.buffer.ydisp; const line = terminal.buffer.lines.get(row); for (let x = 0; x < terminal.cols; x++) { @@ -128,8 +128,8 @@ export class TextRenderLayer extends BaseRenderLayer { } } - private _drawBackground(terminal: ITerminal, startRow: number, endRow: number): void { - this._forEachCell(terminal, startRow, endRow, (code, char, width, x, y, fg, bg, flags) => { + private _drawBackground(terminal: ITerminal, firstRow: number, lastRow: number): void { + this._forEachCell(terminal, firstRow, lastRow, (code, char, width, x, y, fg, bg, flags) => { // libvte and xterm both draw the background (but not foreground) of invisible characters, // so we should too. const isDefaultBackground = bg >= 256; @@ -142,8 +142,8 @@ export class TextRenderLayer extends BaseRenderLayer { }); } - private _drawForeground(terminal: ITerminal, startRow: number, endRow: number): void { - this._forEachCell(terminal, startRow, endRow, (code, char, width, x, y, fg, bg, flags) => { + private _drawForeground(terminal: ITerminal, firstRow: number, lastRow: number): void { + this._forEachCell(terminal, firstRow, lastRow, (code, char, width, x, y, fg, bg, flags) => { if (flags & FLAGS.INVISIBLE) { return; } @@ -169,15 +169,15 @@ export class TextRenderLayer extends BaseRenderLayer { }); } - public onGridChanged(terminal: ITerminal, startRow: number, endRow: number): void { + public onGridChanged(terminal: ITerminal, firstRow: number, lastRow: number): void { // Resize has not been called yet if (this._state.cache.length === 0) { return; } - this.clearCells(0, startRow, terminal.cols, endRow - startRow + 1); // endRow is inclusive - this._drawBackground(terminal, startRow, endRow); - this._drawForeground(terminal, startRow, endRow); + this.clearCells(0, firstRow, terminal.cols, lastRow - firstRow + 1); + this._drawBackground(terminal, firstRow, lastRow); + this._drawForeground(terminal, firstRow, lastRow); } public onOptionsChanged(terminal: ITerminal): void { From 2bfe74490a706f089a223d36d79e1a041f8c4984 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Mon, 23 Apr 2018 16:29:19 +0200 Subject: [PATCH 03/10] make .buffer local where appropriate --- src/InputHandler.ts | 180 +++++++++++++++++++++++++++----------------- 1 file changed, 110 insertions(+), 70 deletions(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index acf7af1f..3936b33c 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -23,6 +23,10 @@ export class InputHandler implements IInputHandler { public addChar(char: string, code: number): void { if (char >= ' ') { + + // make buffer local for faster access + let buffer = this._terminal.buffer; + // calculate print space // expensive call, therefore we save width in line buffer const chWidth = wcwidth(code); @@ -35,42 +39,42 @@ export class InputHandler implements IInputHandler { this._terminal.emit('a11y.char', char); } - let row = this._terminal.buffer.y + this._terminal.buffer.ybase; + let row = buffer.y + buffer.ybase; // insert combining char in last cell // FIXME: needs handling after cursor jumps - if (!chWidth && this._terminal.buffer.x) { + if (!chWidth && buffer.x) { // dont overflow left - if (this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1]) { - if (!this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1][CHAR_DATA_WIDTH_INDEX]) { + if (buffer.lines.get(row)[buffer.x - 1]) { + if (!buffer.lines.get(row)[buffer.x - 1][CHAR_DATA_WIDTH_INDEX]) { // found empty cell after fullwidth, need to go 2 cells back - if (this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 2]) { - this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 2][CHAR_DATA_CHAR_INDEX] += char; - this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 2][3] = char.charCodeAt(0); + if (buffer.lines.get(row)[buffer.x - 2]) { + buffer.lines.get(row)[buffer.x - 2][CHAR_DATA_CHAR_INDEX] += char; + buffer.lines.get(row)[buffer.x - 2][3] = char.charCodeAt(0); } } else { - this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1][CHAR_DATA_CHAR_INDEX] += char; - this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1][3] = char.charCodeAt(0); + buffer.lines.get(row)[buffer.x - 1][CHAR_DATA_CHAR_INDEX] += char; + buffer.lines.get(row)[buffer.x - 1][3] = char.charCodeAt(0); } - this._terminal.updateRange(this._terminal.buffer.y); + this._terminal.updateRange(buffer.y); } return; } // goto next line if ch would overflow // TODO: needs a global min terminal width of 2 - if (this._terminal.buffer.x + chWidth - 1 >= this._terminal.cols) { + if (buffer.x + chWidth - 1 >= this._terminal.cols) { // autowrap - DECAWM if (this._terminal.wraparoundMode) { - this._terminal.buffer.x = 0; - this._terminal.buffer.y++; - if (this._terminal.buffer.y > this._terminal.buffer.scrollBottom) { - this._terminal.buffer.y--; + buffer.x = 0; + buffer.y++; + if (buffer.y > buffer.scrollBottom) { + buffer.y--; this._terminal.scroll(true); } else { // The line already exists (eg. the initial viewport), mark it as a // wrapped line - (this._terminal.buffer.lines.get(this._terminal.buffer.y)).isWrapped = true; + (buffer.lines.get(buffer.y)).isWrapped = true; } } else { if (chWidth === 2) { // FIXME: check for xterm behavior @@ -78,7 +82,7 @@ export class InputHandler implements IInputHandler { } } } - row = this._terminal.buffer.y + this._terminal.buffer.ybase; + row = buffer.y + buffer.ybase; // insert mode: move characters to right if (this._terminal.insertMode) { @@ -86,26 +90,26 @@ export class InputHandler implements IInputHandler { for (let moves = 0; moves < chWidth; ++moves) { // remove last cell, if it's width is 0 // we have to adjust the second last cell as well - const removed = this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).pop(); + const removed = buffer.lines.get(buffer.y + buffer.ybase).pop(); if (removed[CHAR_DATA_WIDTH_INDEX] === 0 - && this._terminal.buffer.lines.get(row)[this._terminal.cols - 2] - && this._terminal.buffer.lines.get(row)[this._terminal.cols - 2][CHAR_DATA_WIDTH_INDEX] === 2) { - this._terminal.buffer.lines.get(row)[this._terminal.cols - 2] = [this._terminal.curAttr, ' ', 1, ' '.charCodeAt(0)]; + && buffer.lines.get(row)[this._terminal.cols - 2] + && buffer.lines.get(row)[this._terminal.cols - 2][CHAR_DATA_WIDTH_INDEX] === 2) { + buffer.lines.get(row)[this._terminal.cols - 2] = [this._terminal.curAttr, ' ', 1, ' '.charCodeAt(0)]; } // insert empty cell at cursor - this._terminal.buffer.lines.get(row).splice(this._terminal.buffer.x, 0, [this._terminal.curAttr, ' ', 1, ' '.charCodeAt(0)]); + buffer.lines.get(row).splice(buffer.x, 0, [this._terminal.curAttr, ' ', 1, ' '.charCodeAt(0)]); } } - this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = [this._terminal.curAttr, char, chWidth, char.charCodeAt(0)]; - this._terminal.buffer.x++; - this._terminal.updateRange(this._terminal.buffer.y); + buffer.lines.get(row)[buffer.x] = [this._terminal.curAttr, char, chWidth, char.charCodeAt(0)]; + buffer.x++; + this._terminal.updateRange(buffer.y); // fullwidth char - set next cell width to zero and advance cursor if (chWidth === 2) { - this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = [this._terminal.curAttr, '', 0, undefined]; - this._terminal.buffer.x++; + buffer.lines.get(row)[buffer.x] = [this._terminal.curAttr, '', 0, undefined]; + buffer.x++; } } } @@ -123,17 +127,20 @@ export class InputHandler implements IInputHandler { * Line Feed or New Line (NL). (LF is Ctrl-J). */ public lineFeed(): void { + // make buffer local for faster access + let buffer = this._terminal.buffer; + if (this._terminal.convertEol) { - this._terminal.buffer.x = 0; + buffer.x = 0; } - this._terminal.buffer.y++; - if (this._terminal.buffer.y > this._terminal.buffer.scrollBottom) { - this._terminal.buffer.y--; + buffer.y++; + if (buffer.y > buffer.scrollBottom) { + buffer.y--; this._terminal.scroll(); } // If the end of the line is hit, prevent this action from wrapping around to the next line. - if (this._terminal.buffer.x >= this._terminal.cols) { - this._terminal.buffer.x--; + if (buffer.x >= this._terminal.cols) { + buffer.x--; } /** * This event is emitted whenever the terminal outputs a LF or NL. @@ -199,13 +206,16 @@ export class InputHandler implements IInputHandler { let param = params[0]; if (param < 1) param = 1; - const row = this._terminal.buffer.y + this._terminal.buffer.ybase; - let j = this._terminal.buffer.x; + // make buffer local for faster access + let buffer = this._terminal.buffer; + + const row = buffer.y + buffer.ybase; + let j = buffer.x; const ch: CharData = [this._terminal.eraseAttr(), ' ', 1, 32]; // xterm while (param-- && j < this._terminal.cols) { - this._terminal.buffer.lines.get(row).splice(j++, 0, ch); - this._terminal.buffer.lines.get(row).pop(); + buffer.lines.get(row).splice(j++, 0, ch); + buffer.lines.get(row).pop(); } } @@ -447,20 +457,24 @@ export class InputHandler implements IInputHandler { if (param < 1) { param = 1; } - let row: number = this._terminal.buffer.y + this._terminal.buffer.ybase; - let scrollBottomRowsOffset = this._terminal.rows - 1 - this._terminal.buffer.scrollBottom; - let scrollBottomAbsolute = this._terminal.rows - 1 + this._terminal.buffer.ybase - scrollBottomRowsOffset + 1; + // make buffer local for faster access + let buffer = this._terminal.buffer; + + let row: number = buffer.y + buffer.ybase; + + let scrollBottomRowsOffset = this._terminal.rows - 1 - buffer.scrollBottom; + let scrollBottomAbsolute = this._terminal.rows - 1 + buffer.ybase - scrollBottomRowsOffset + 1; while (param--) { // test: echo -e '\e[44m\e[1L\e[0m' // blankLine(true) - xterm/linux behavior - this._terminal.buffer.lines.splice(scrollBottomAbsolute - 1, 1); - this._terminal.buffer.lines.splice(row, 0, this._terminal.blankLine(true)); + buffer.lines.splice(scrollBottomAbsolute - 1, 1); + buffer.lines.splice(row, 0, this._terminal.blankLine(true)); } // this.maxRange(); - this._terminal.updateRange(this._terminal.buffer.y); - this._terminal.updateRange(this._terminal.buffer.scrollBottom); + this._terminal.updateRange(buffer.y); + this._terminal.updateRange(buffer.scrollBottom); } /** @@ -472,21 +486,25 @@ export class InputHandler implements IInputHandler { if (param < 1) { param = 1; } - const row: number = this._terminal.buffer.y + this._terminal.buffer.ybase; + + // make buffer local for faster access + let buffer = this._terminal.buffer; + + const row: number = buffer.y + buffer.ybase; let j: number; - j = this._terminal.rows - 1 - this._terminal.buffer.scrollBottom; - j = this._terminal.rows - 1 + this._terminal.buffer.ybase - j; + j = this._terminal.rows - 1 - buffer.scrollBottom; + j = this._terminal.rows - 1 + buffer.ybase - j; while (param--) { // test: echo -e '\e[44m\e[1M\e[0m' // blankLine(true) - xterm/linux behavior - this._terminal.buffer.lines.splice(row, 1); - this._terminal.buffer.lines.splice(j, 0, this._terminal.blankLine(true)); + buffer.lines.splice(row, 1); + buffer.lines.splice(j, 0, this._terminal.blankLine(true)); } // this.maxRange(); - this._terminal.updateRange(this._terminal.buffer.y); - this._terminal.updateRange(this._terminal.buffer.scrollBottom); + this._terminal.updateRange(buffer.y); + this._terminal.updateRange(buffer.scrollBottom); } /** @@ -499,14 +517,17 @@ export class InputHandler implements IInputHandler { param = 1; } - const row = this._terminal.buffer.y + this._terminal.buffer.ybase; + // make buffer local for faster access + let buffer = this._terminal.buffer; + + const row = buffer.y + buffer.ybase; const ch: CharData = [this._terminal.eraseAttr(), ' ', 1, 32]; // xterm while (param--) { - this._terminal.buffer.lines.get(row).splice(this._terminal.buffer.x, 1); - this._terminal.buffer.lines.get(row).push(ch); + buffer.lines.get(row).splice(buffer.x, 1); + buffer.lines.get(row).push(ch); } - this._terminal.updateRange(this._terminal.buffer.y); + this._terminal.updateRange(buffer.y); } /** @@ -514,13 +535,17 @@ export class InputHandler implements IInputHandler { */ public scrollUp(params: number[]): void { let param = params[0] || 1; + + // make buffer local for faster access + let buffer = this._terminal.buffer; + while (param--) { - this._terminal.buffer.lines.splice(this._terminal.buffer.ybase + this._terminal.buffer.scrollTop, 1); - this._terminal.buffer.lines.splice(this._terminal.buffer.ybase + this._terminal.buffer.scrollBottom, 0, this._terminal.blankLine()); + buffer.lines.splice(buffer.ybase + buffer.scrollTop, 1); + buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, this._terminal.blankLine()); } // this.maxRange(); - this._terminal.updateRange(this._terminal.buffer.scrollTop); - this._terminal.updateRange(this._terminal.buffer.scrollBottom); + this._terminal.updateRange(buffer.scrollTop); + this._terminal.updateRange(buffer.scrollBottom); } /** @@ -528,13 +553,17 @@ export class InputHandler implements IInputHandler { */ public scrollDown(params: number[]): void { let param = params[0] || 1; + + // make buffer local for faster access + let buffer = this._terminal.buffer; + while (param--) { - this._terminal.buffer.lines.splice(this._terminal.buffer.ybase + this._terminal.buffer.scrollBottom, 1); - this._terminal.buffer.lines.splice(this._terminal.buffer.ybase + this._terminal.buffer.scrollTop, 0, this._terminal.blankLine()); + buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 1); + buffer.lines.splice(buffer.ybase + buffer.scrollTop, 0, this._terminal.blankLine()); } // this.maxRange(); - this._terminal.updateRange(this._terminal.buffer.scrollTop); - this._terminal.updateRange(this._terminal.buffer.scrollBottom); + this._terminal.updateRange(buffer.scrollTop); + this._terminal.updateRange(buffer.scrollBottom); } /** @@ -547,12 +576,15 @@ export class InputHandler implements IInputHandler { param = 1; } - const row = this._terminal.buffer.y + this._terminal.buffer.ybase; - let j = this._terminal.buffer.x; + // make buffer local for faster access + let buffer = this._terminal.buffer; + + const row = buffer.y + buffer.ybase; + let j = buffer.x; const ch: CharData = [this._terminal.eraseAttr(), ' ', 1, 32]; // xterm while (param-- && j < this._terminal.cols) { - this._terminal.buffer.lines.get(row)[j++] = ch; + buffer.lines.get(row)[j++] = ch; } } @@ -561,8 +593,12 @@ export class InputHandler implements IInputHandler { */ public cursorBackwardTab(params: number[]): void { let param = params[0] || 1; + + // make buffer local for faster access + let buffer = this._terminal.buffer; + while (param--) { - this._terminal.buffer.x = this._terminal.buffer.prevStop(); + buffer.x = buffer.prevStop(); } } @@ -602,11 +638,15 @@ export class InputHandler implements IInputHandler { */ public repeatPrecedingCharacter(params: number[]): void { let param = params[0] || 1; - const line = this._terminal.buffer.lines.get(this._terminal.buffer.ybase + this._terminal.buffer.y); - const ch = line[this._terminal.buffer.x - 1] || [this._terminal.defAttr, ' ', 1, 32]; + + // make buffer local for faster access + let buffer = this._terminal.buffer; + + const line = buffer.lines.get(buffer.ybase + buffer.y); + const ch = line[buffer.x - 1] || [this._terminal.defAttr, ' ', 1, 32]; while (param--) { - line[this._terminal.buffer.x++] = ch; + line[buffer.x++] = ch; } } From 0decd77253202c2559805833b1ecc5e5df82b5cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Mon, 23 Apr 2018 16:59:55 +0200 Subject: [PATCH 04/10] map Buffer.lines directly to Buffer._lines --- src/Buffer.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index c1cf3cc7..0689c4cf 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -34,6 +34,7 @@ export class Buffer implements IBuffer { public savedY: number; public savedX: number; public markers: Marker[] = []; + public lines: CircularList; /** * Create a new Buffer. @@ -48,10 +49,6 @@ export class Buffer implements IBuffer { this.clear(); } - public get lines(): CircularList { - return this._lines; - } - public get hasScrollback(): boolean { return this._hasScrollback && this.lines.maxLength > this._terminal.rows; } @@ -98,6 +95,7 @@ export class Buffer implements IBuffer { this.y = 0; this.x = 0; this._lines = new CircularList(this._getCorrectBufferLength(this._terminal.rows)); + this.lines = this._lines; this.scrollTop = 0; this.scrollBottom = this._terminal.rows - 1; this.setupTabStops(); From dd096c2e8b681b49007124302d3b39fe6dcf9d93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Mon, 23 Apr 2018 17:19:42 +0200 Subject: [PATCH 05/10] const buffer --- src/InputHandler.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 3936b33c..4df3e695 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -25,7 +25,7 @@ export class InputHandler implements IInputHandler { if (char >= ' ') { // make buffer local for faster access - let buffer = this._terminal.buffer; + const buffer = this._terminal.buffer; // calculate print space // expensive call, therefore we save width in line buffer @@ -128,7 +128,7 @@ export class InputHandler implements IInputHandler { */ public lineFeed(): void { // make buffer local for faster access - let buffer = this._terminal.buffer; + const buffer = this._terminal.buffer; if (this._terminal.convertEol) { buffer.x = 0; @@ -207,7 +207,7 @@ export class InputHandler implements IInputHandler { if (param < 1) param = 1; // make buffer local for faster access - let buffer = this._terminal.buffer; + const buffer = this._terminal.buffer; const row = buffer.y + buffer.ybase; let j = buffer.x; @@ -459,7 +459,7 @@ export class InputHandler implements IInputHandler { } // make buffer local for faster access - let buffer = this._terminal.buffer; + const buffer = this._terminal.buffer; let row: number = buffer.y + buffer.ybase; @@ -488,7 +488,7 @@ export class InputHandler implements IInputHandler { } // make buffer local for faster access - let buffer = this._terminal.buffer; + const buffer = this._terminal.buffer; const row: number = buffer.y + buffer.ybase; @@ -518,7 +518,7 @@ export class InputHandler implements IInputHandler { } // make buffer local for faster access - let buffer = this._terminal.buffer; + const buffer = this._terminal.buffer; const row = buffer.y + buffer.ybase; const ch: CharData = [this._terminal.eraseAttr(), ' ', 1, 32]; // xterm @@ -537,7 +537,7 @@ export class InputHandler implements IInputHandler { let param = params[0] || 1; // make buffer local for faster access - let buffer = this._terminal.buffer; + const buffer = this._terminal.buffer; while (param--) { buffer.lines.splice(buffer.ybase + buffer.scrollTop, 1); @@ -555,7 +555,7 @@ export class InputHandler implements IInputHandler { let param = params[0] || 1; // make buffer local for faster access - let buffer = this._terminal.buffer; + const buffer = this._terminal.buffer; while (param--) { buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 1); @@ -577,7 +577,7 @@ export class InputHandler implements IInputHandler { } // make buffer local for faster access - let buffer = this._terminal.buffer; + const buffer = this._terminal.buffer; const row = buffer.y + buffer.ybase; let j = buffer.x; @@ -595,7 +595,7 @@ export class InputHandler implements IInputHandler { let param = params[0] || 1; // make buffer local for faster access - let buffer = this._terminal.buffer; + const buffer = this._terminal.buffer; while (param--) { buffer.x = buffer.prevStop(); @@ -640,7 +640,7 @@ export class InputHandler implements IInputHandler { let param = params[0] || 1; // make buffer local for faster access - let buffer = this._terminal.buffer; + const buffer = this._terminal.buffer; const line = buffer.lines.get(buffer.ybase + buffer.y); const ch = line[buffer.x - 1] || [this._terminal.defAttr, ' ', 1, 32]; From 0d7ecd912939cb699ad0fa2e48fc0b25e75f1101 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Mon, 23 Apr 2018 18:26:06 +0200 Subject: [PATCH 06/10] prefetch row in addChar --- src/InputHandler.ts | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 4df3e695..db5032f9 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -39,22 +39,23 @@ export class InputHandler implements IInputHandler { this._terminal.emit('a11y.char', char); } - let row = buffer.y + buffer.ybase; + //let row = buffer.y + buffer.ybase; + let row = buffer.lines.get(buffer.y + buffer.ybase); // insert combining char in last cell // FIXME: needs handling after cursor jumps if (!chWidth && buffer.x) { // dont overflow left - if (buffer.lines.get(row)[buffer.x - 1]) { - if (!buffer.lines.get(row)[buffer.x - 1][CHAR_DATA_WIDTH_INDEX]) { + if (row[buffer.x - 1]) { + if (!row[buffer.x - 1][CHAR_DATA_WIDTH_INDEX]) { // found empty cell after fullwidth, need to go 2 cells back - if (buffer.lines.get(row)[buffer.x - 2]) { - buffer.lines.get(row)[buffer.x - 2][CHAR_DATA_CHAR_INDEX] += char; - buffer.lines.get(row)[buffer.x - 2][3] = char.charCodeAt(0); + if (row[buffer.x - 2]) { + row[buffer.x - 2][CHAR_DATA_CHAR_INDEX] += char; + row[buffer.x - 2][3] = char.charCodeAt(0); } } else { - buffer.lines.get(row)[buffer.x - 1][CHAR_DATA_CHAR_INDEX] += char; - buffer.lines.get(row)[buffer.x - 1][3] = char.charCodeAt(0); + row[buffer.x - 1][CHAR_DATA_CHAR_INDEX] += char; + row[buffer.x - 1][3] = char.charCodeAt(0); } this._terminal.updateRange(buffer.y); } @@ -82,7 +83,7 @@ export class InputHandler implements IInputHandler { } } } - row = buffer.y + buffer.ybase; + row = buffer.lines.get(buffer.y + buffer.ybase); // insert mode: move characters to right if (this._terminal.insertMode) { @@ -92,23 +93,23 @@ export class InputHandler implements IInputHandler { // we have to adjust the second last cell as well const removed = buffer.lines.get(buffer.y + buffer.ybase).pop(); if (removed[CHAR_DATA_WIDTH_INDEX] === 0 - && buffer.lines.get(row)[this._terminal.cols - 2] - && buffer.lines.get(row)[this._terminal.cols - 2][CHAR_DATA_WIDTH_INDEX] === 2) { - buffer.lines.get(row)[this._terminal.cols - 2] = [this._terminal.curAttr, ' ', 1, ' '.charCodeAt(0)]; + && row[this._terminal.cols - 2] + && row[this._terminal.cols - 2][CHAR_DATA_WIDTH_INDEX] === 2) { + row[this._terminal.cols - 2] = [this._terminal.curAttr, ' ', 1, ' '.charCodeAt(0)]; } // insert empty cell at cursor - buffer.lines.get(row).splice(buffer.x, 0, [this._terminal.curAttr, ' ', 1, ' '.charCodeAt(0)]); + row.splice(buffer.x, 0, [this._terminal.curAttr, ' ', 1, ' '.charCodeAt(0)]); } } - buffer.lines.get(row)[buffer.x] = [this._terminal.curAttr, char, chWidth, char.charCodeAt(0)]; + row[buffer.x] = [this._terminal.curAttr, char, chWidth, char.charCodeAt(0)]; buffer.x++; this._terminal.updateRange(buffer.y); // fullwidth char - set next cell width to zero and advance cursor if (chWidth === 2) { - buffer.lines.get(row)[buffer.x] = [this._terminal.curAttr, '', 0, undefined]; + row[buffer.x] = [this._terminal.curAttr, '', 0, undefined]; buffer.x++; } } From 13d4ab6f8dbd06564e7f479c906beea0956202f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Mon, 23 Apr 2018 18:32:32 +0200 Subject: [PATCH 07/10] remove remnant --- src/InputHandler.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index db5032f9..3d39e0c0 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -39,7 +39,6 @@ export class InputHandler implements IInputHandler { this._terminal.emit('a11y.char', char); } - //let row = buffer.y + buffer.ybase; let row = buffer.lines.get(buffer.y + buffer.ybase); // insert combining char in last cell From 6ff4ae7194b266ce0a6fb29bfac645be76f3b1c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Mon, 23 Apr 2018 22:06:24 +0200 Subject: [PATCH 08/10] revert to local const buffer --- src/Buffer.ts | 6 ++++-- src/InputHandler.ts | 30 +++++++++++++++--------------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 0689c4cf..c1cf3cc7 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -34,7 +34,6 @@ export class Buffer implements IBuffer { public savedY: number; public savedX: number; public markers: Marker[] = []; - public lines: CircularList; /** * Create a new Buffer. @@ -49,6 +48,10 @@ export class Buffer implements IBuffer { this.clear(); } + public get lines(): CircularList { + return this._lines; + } + public get hasScrollback(): boolean { return this._hasScrollback && this.lines.maxLength > this._terminal.rows; } @@ -95,7 +98,6 @@ export class Buffer implements IBuffer { this.y = 0; this.x = 0; this._lines = new CircularList(this._getCorrectBufferLength(this._terminal.rows)); - this.lines = this._lines; this.scrollTop = 0; this.scrollBottom = this._terminal.rows - 1; this.setupTabStops(); diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 3d39e0c0..4df3e695 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -39,22 +39,22 @@ export class InputHandler implements IInputHandler { this._terminal.emit('a11y.char', char); } - let row = buffer.lines.get(buffer.y + buffer.ybase); + let row = buffer.y + buffer.ybase; // insert combining char in last cell // FIXME: needs handling after cursor jumps if (!chWidth && buffer.x) { // dont overflow left - if (row[buffer.x - 1]) { - if (!row[buffer.x - 1][CHAR_DATA_WIDTH_INDEX]) { + if (buffer.lines.get(row)[buffer.x - 1]) { + if (!buffer.lines.get(row)[buffer.x - 1][CHAR_DATA_WIDTH_INDEX]) { // found empty cell after fullwidth, need to go 2 cells back - if (row[buffer.x - 2]) { - row[buffer.x - 2][CHAR_DATA_CHAR_INDEX] += char; - row[buffer.x - 2][3] = char.charCodeAt(0); + if (buffer.lines.get(row)[buffer.x - 2]) { + buffer.lines.get(row)[buffer.x - 2][CHAR_DATA_CHAR_INDEX] += char; + buffer.lines.get(row)[buffer.x - 2][3] = char.charCodeAt(0); } } else { - row[buffer.x - 1][CHAR_DATA_CHAR_INDEX] += char; - row[buffer.x - 1][3] = char.charCodeAt(0); + buffer.lines.get(row)[buffer.x - 1][CHAR_DATA_CHAR_INDEX] += char; + buffer.lines.get(row)[buffer.x - 1][3] = char.charCodeAt(0); } this._terminal.updateRange(buffer.y); } @@ -82,7 +82,7 @@ export class InputHandler implements IInputHandler { } } } - row = buffer.lines.get(buffer.y + buffer.ybase); + row = buffer.y + buffer.ybase; // insert mode: move characters to right if (this._terminal.insertMode) { @@ -92,23 +92,23 @@ export class InputHandler implements IInputHandler { // we have to adjust the second last cell as well const removed = buffer.lines.get(buffer.y + buffer.ybase).pop(); if (removed[CHAR_DATA_WIDTH_INDEX] === 0 - && row[this._terminal.cols - 2] - && row[this._terminal.cols - 2][CHAR_DATA_WIDTH_INDEX] === 2) { - row[this._terminal.cols - 2] = [this._terminal.curAttr, ' ', 1, ' '.charCodeAt(0)]; + && buffer.lines.get(row)[this._terminal.cols - 2] + && buffer.lines.get(row)[this._terminal.cols - 2][CHAR_DATA_WIDTH_INDEX] === 2) { + buffer.lines.get(row)[this._terminal.cols - 2] = [this._terminal.curAttr, ' ', 1, ' '.charCodeAt(0)]; } // insert empty cell at cursor - row.splice(buffer.x, 0, [this._terminal.curAttr, ' ', 1, ' '.charCodeAt(0)]); + buffer.lines.get(row).splice(buffer.x, 0, [this._terminal.curAttr, ' ', 1, ' '.charCodeAt(0)]); } } - row[buffer.x] = [this._terminal.curAttr, char, chWidth, char.charCodeAt(0)]; + buffer.lines.get(row)[buffer.x] = [this._terminal.curAttr, char, chWidth, char.charCodeAt(0)]; buffer.x++; this._terminal.updateRange(buffer.y); // fullwidth char - set next cell width to zero and advance cursor if (chWidth === 2) { - row[buffer.x] = [this._terminal.curAttr, '', 0, undefined]; + buffer.lines.get(row)[buffer.x] = [this._terminal.curAttr, '', 0, undefined]; buffer.x++; } } From 7ed7505ce47c91210c7c68cba7284d0de4f27a5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Mon, 23 Apr 2018 22:18:05 +0200 Subject: [PATCH 09/10] change _lines to lines in Buffer --- src/Buffer.ts | 45 ++++++++++++++++++++------------------------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index c1cf3cc7..1ea303b2 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -22,8 +22,7 @@ export const MAX_BUFFER_SIZE = 4294967295; // 2^32 - 1 * - scroll position */ export class Buffer implements IBuffer { - private _lines: CircularList; - + public lines: CircularList; public ydisp: number; public ybase: number; public y: number; @@ -48,10 +47,6 @@ export class Buffer implements IBuffer { this.clear(); } - public get lines(): CircularList { - return this._lines; - } - public get hasScrollback(): boolean { return this._hasScrollback && this.lines.maxLength > this._terminal.rows; } @@ -81,7 +76,7 @@ export class Buffer implements IBuffer { * Fills the buffer's viewport with blank lines. */ public fillViewportRows(): void { - if (this._lines.length === 0) { + if (this.lines.length === 0) { let i = this._terminal.rows; while (i--) { this.lines.push(this._terminal.blankLine()); @@ -97,7 +92,7 @@ export class Buffer implements IBuffer { this.ybase = 0; this.y = 0; this.x = 0; - this._lines = new CircularList(this._getCorrectBufferLength(this._terminal.rows)); + this.lines = new CircularList(this._getCorrectBufferLength(this._terminal.rows)); this.scrollTop = 0; this.scrollBottom = this._terminal.rows - 1; this.setupTabStops(); @@ -112,19 +107,19 @@ export class Buffer implements IBuffer { // Increase max length if needed before adjustments to allow space to fill // as required. const newMaxLength = this._getCorrectBufferLength(newRows); - if (newMaxLength > this._lines.maxLength) { - this._lines.maxLength = newMaxLength; + if (newMaxLength > this.lines.maxLength) { + this.lines.maxLength = newMaxLength; } // The following adjustments should only happen if the buffer has been // initialized/filled. - if (this._lines.length > 0) { + if (this.lines.length > 0) { // Deal with columns increasing (we don't do anything when columns reduce) if (this._terminal.cols < newCols) { const ch: CharData = [this._terminal.defAttr, ' ', 1, 32]; // does xterm use the default attr? - for (let i = 0; i < this._lines.length; i++) { - while (this._lines.get(i).length < newCols) { - this._lines.get(i).push(ch); + for (let i = 0; i < this.lines.length; i++) { + while (this.lines.get(i).length < newCols) { + this.lines.get(i).push(ch); } } } @@ -133,8 +128,8 @@ export class Buffer implements IBuffer { let addToY = 0; if (this._terminal.rows < newRows) { for (let y = this._terminal.rows; y < newRows; y++) { - if (this._lines.length < newRows + this.ybase) { - if (this.ybase > 0 && this._lines.length <= this.ybase + this.y + addToY + 1) { + if (this.lines.length < newRows + this.ybase) { + if (this.ybase > 0 && this.lines.length <= this.ybase + this.y + addToY + 1) { // There is room above the buffer and there are no empty elements below the line, // scroll up this.ybase--; @@ -146,16 +141,16 @@ export class Buffer implements IBuffer { } else { // Add a blank line if there is no buffer left at the top to scroll to, or if there // are blank lines after the cursor - this._lines.push(this._terminal.blankLine(undefined, undefined, newCols)); + this.lines.push(this._terminal.blankLine(undefined, undefined, newCols)); } } } } else { // (this._terminal.rows >= newRows) for (let y = this._terminal.rows; y > newRows; y--) { - if (this._lines.length > newRows + this.ybase) { - if (this._lines.length > this.ybase + this.y + 1) { + if (this.lines.length > newRows + this.ybase) { + if (this.lines.length > this.ybase + this.y + 1) { // The line is a blank line below the cursor, remove it - this._lines.pop(); + this.lines.pop(); } else { // The line is the cursor, scroll down this.ybase++; @@ -167,15 +162,15 @@ export class Buffer implements IBuffer { // Reduce max length if needed after adjustments, this is done after as it // would otherwise cut data from the bottom of the buffer. - if (newMaxLength < this._lines.maxLength) { + if (newMaxLength < this.lines.maxLength) { // Trim from the top of the buffer and adjust ybase and ydisp. - const amountToTrim = this._lines.length - newMaxLength; + const amountToTrim = this.lines.length - newMaxLength; if (amountToTrim > 0) { - this._lines.trimStart(amountToTrim); + this.lines.trimStart(amountToTrim); this.ybase = Math.max(this.ybase - amountToTrim, 0); this.ydisp = Math.max(this.ydisp - amountToTrim, 0); } - this._lines.maxLength = newMaxLength; + this.lines.maxLength = newMaxLength; } // Make sure that the cursor stays on screen @@ -310,7 +305,7 @@ export class Buffer implements IBuffer { public addMarker(y: number): Marker { const marker = new Marker(y); this.markers.push(marker); - marker.disposables.push(this._lines.addDisposableListener('trim', amount => { + marker.disposables.push(this.lines.addDisposableListener('trim', amount => { marker.line -= amount; // The marker should be disposed when the line is trimmed from the buffer if (marker.line < 0) { From 9593cb0cc81eefcae9d0f5fbe546768d46073a0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Mon, 23 Apr 2018 22:40:24 +0200 Subject: [PATCH 10/10] readonly lines; fix for MockBuffer --- src/Linkifier.test.ts | 2 +- src/Types.ts | 2 +- src/utils/TestUtils.test.ts | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Linkifier.test.ts b/src/Linkifier.test.ts index 0e67403c..61245296 100644 --- a/src/Linkifier.test.ts +++ b/src/Linkifier.test.ts @@ -40,7 +40,7 @@ describe('Linkifier', () => { terminal = new MockTerminal(); terminal.cols = 100; terminal.buffer = new MockBuffer(); - terminal.buffer.lines = new CircularList(20); + (terminal.buffer).setLines(new CircularList(20)); terminal.buffer.ydisp = 0; linkifier = new TestLinkifier(terminal); mouseZoneManager = new TestMouseZoneManager(); diff --git a/src/Types.ts b/src/Types.ts index 84355e1f..2442b027 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -248,7 +248,7 @@ export interface ITerminalOptions extends IPublicTerminalOptions { } export interface IBuffer { - lines: ICircularList; + readonly lines: ICircularList; ydisp: number; ybase: number; y: number; diff --git a/src/utils/TestUtils.test.ts b/src/utils/TestUtils.test.ts index 4436c526..51aafb50 100644 --- a/src/utils/TestUtils.test.ts +++ b/src/utils/TestUtils.test.ts @@ -309,6 +309,9 @@ export class MockBuffer implements IBuffer { prevStop(x?: number): number { throw new Error('Method not implemented.'); } + setLines(lines: ICircularList<[number, string, number, number][]>): void { + this.lines = lines; + } } export class MockRenderer implements IRenderer {