mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
Merge pull request #4244 from Tyriar/multi_page_atlas
Support multiple texture atlas pages
This commit is contained in:
@@ -15,12 +15,13 @@ import { ReadonlyColorSet } from 'browser/Types';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { WHITESPACE_CELL_CODE } from 'common/buffer/Constants';
|
||||
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
|
||||
import { ICellData } from 'common/Types';
|
||||
import { ICellData, IDisposable } from 'common/Types';
|
||||
import { Terminal } from 'xterm';
|
||||
import { IRenderLayer } from './Types';
|
||||
import { CellColorResolver } from 'browser/renderer/shared/CellColorResolver';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
import { isSafari } from 'common/Platform';
|
||||
import { EventEmitter, forwardEvent } from 'common/EventEmitter';
|
||||
|
||||
export abstract class BaseRenderLayer extends Disposable implements IRenderLayer {
|
||||
private _canvas: HTMLCanvasElement;
|
||||
@@ -34,12 +35,16 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer
|
||||
|
||||
protected _selectionModel: ISelectionRenderModel = createSelectionRenderModel();
|
||||
private _cellColorResolver: CellColorResolver;
|
||||
private _bitmapGenerator?: BitmapGenerator;
|
||||
private _bitmapGenerator: (BitmapGenerator | undefined)[] = [];
|
||||
|
||||
protected _charAtlas!: ITextureAtlas;
|
||||
private _charAtlasDisposable?: IDisposable;
|
||||
|
||||
public get canvas(): HTMLCanvasElement { return this._canvas; }
|
||||
public get cacheCanvas(): HTMLCanvasElement { return this._charAtlas?.cacheCanvas!; }
|
||||
public get cacheCanvas(): HTMLCanvasElement { return this._charAtlas?.pages[0].canvas!; }
|
||||
|
||||
private readonly _onAddTextureAtlasCanvas = this.register(new EventEmitter<HTMLCanvasElement>());
|
||||
public readonly onAddTextureAtlasCanvas = this._onAddTextureAtlasCanvas.event;
|
||||
|
||||
constructor(
|
||||
private readonly _terminal: Terminal,
|
||||
@@ -116,9 +121,13 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer
|
||||
if (this._deviceCharWidth <= 0 && this._deviceCharHeight <= 0) {
|
||||
return;
|
||||
}
|
||||
this._charAtlasDisposable?.dispose();
|
||||
this._charAtlas = acquireTextureAtlas(this._terminal, colorSet, this._deviceCellWidth, this._deviceCellHeight, this._deviceCharWidth, this._deviceCharHeight, this._coreBrowserService.dpr);
|
||||
this._charAtlasDisposable = forwardEvent(this._charAtlas.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas);
|
||||
this._charAtlas.warmUp();
|
||||
this._bitmapGenerator = new BitmapGenerator(this._charAtlas.cacheCanvas);
|
||||
for (let i = 0; i < this._charAtlas.pages.length; i++) {
|
||||
this._bitmapGenerator[i] = new BitmapGenerator(this._charAtlas.pages[i].canvas);
|
||||
}
|
||||
}
|
||||
|
||||
public resize(dim: IRenderDimensions): void {
|
||||
@@ -367,12 +376,15 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer
|
||||
this._ctx.save();
|
||||
this._clipRow(y);
|
||||
// Draw the image, use the bitmap if it's available
|
||||
if (this._charAtlas.hasCanvasChanged) {
|
||||
this._bitmapGenerator?.refresh();
|
||||
this._charAtlas.hasCanvasChanged = false;
|
||||
if (this._charAtlas.pages[glyph.texturePage].hasCanvasChanged) {
|
||||
if (!this._bitmapGenerator[glyph.texturePage]) {
|
||||
this._bitmapGenerator[glyph.texturePage] = new BitmapGenerator(this._charAtlas.pages[glyph.texturePage].canvas);
|
||||
}
|
||||
this._bitmapGenerator[glyph.texturePage]?.refresh();
|
||||
this._charAtlas.pages[glyph.texturePage].hasCanvasChanged = false;
|
||||
}
|
||||
this._ctx.drawImage(
|
||||
this._bitmapGenerator?.bitmap || this._charAtlas!.cacheCanvas,
|
||||
this._bitmapGenerator[glyph.texturePage]?.bitmap || this._charAtlas!.pages[glyph.texturePage].canvas,
|
||||
glyph.texturePosition.x,
|
||||
glyph.texturePosition.y,
|
||||
glyph.size.x,
|
||||
|
||||
@@ -17,6 +17,8 @@ export class CanvasAddon extends Disposable implements ITerminalAddon {
|
||||
|
||||
private readonly _onChangeTextureAtlas = this.register(new EventEmitter<HTMLCanvasElement>());
|
||||
public readonly onChangeTextureAtlas = this._onChangeTextureAtlas.event;
|
||||
private readonly _onAddTextureAtlasCanvas = this.register(new EventEmitter<HTMLCanvasElement>());
|
||||
public readonly onAddTextureAtlasCanvas = this._onAddTextureAtlasCanvas.event;
|
||||
|
||||
public get textureAtlas(): HTMLCanvasElement | undefined {
|
||||
return this._renderer?.textureAtlas;
|
||||
@@ -46,6 +48,7 @@ export class CanvasAddon extends Disposable implements ITerminalAddon {
|
||||
|
||||
this._renderer = new CanvasRenderer(terminal, screenElement, linkifier, bufferService, charSizeService, optionsService, characterJoinerService, coreService, coreBrowserService, decorationService, themeService);
|
||||
this.register(forwardEvent(this._renderer.onChangeTextureAtlas, this._onChangeTextureAtlas));
|
||||
this.register(forwardEvent(this._renderer.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas));
|
||||
renderService.setRenderer(this._renderer);
|
||||
renderService.handleResize(bufferService.cols, bufferService.rows);
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils';
|
||||
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/shared/Types';
|
||||
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IThemeService } from 'browser/services/Services';
|
||||
import { ILinkifier2 } from 'browser/Types';
|
||||
import { EventEmitter } from 'common/EventEmitter';
|
||||
import { EventEmitter, forwardEvent } from 'common/EventEmitter';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
import { IBufferService, ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
|
||||
import { Terminal } from 'xterm';
|
||||
@@ -29,6 +29,8 @@ export class CanvasRenderer extends Disposable implements IRenderer {
|
||||
public readonly onRequestRedraw = this._onRequestRedraw.event;
|
||||
private readonly _onChangeTextureAtlas = this.register(new EventEmitter<HTMLCanvasElement>());
|
||||
public readonly onChangeTextureAtlas = this._onChangeTextureAtlas.event;
|
||||
private readonly _onAddTextureAtlasCanvas = this.register(new EventEmitter<HTMLCanvasElement>());
|
||||
public readonly onAddTextureAtlasCanvas = this._onAddTextureAtlasCanvas.event;
|
||||
|
||||
constructor(
|
||||
private readonly _terminal: Terminal,
|
||||
@@ -51,6 +53,9 @@ export class CanvasRenderer extends Disposable implements IRenderer {
|
||||
new LinkRenderLayer(this._terminal, this._screenElement, 2, linkifier2, this._bufferService, this._optionsService, decorationService, this._coreBrowserService, _themeService),
|
||||
new CursorRenderLayer(this._terminal, this._screenElement, 3, this._onRequestRedraw, this._bufferService, this._optionsService, coreService, this._coreBrowserService, decorationService, _themeService)
|
||||
];
|
||||
for (const layer of this._renderLayers) {
|
||||
forwardEvent(layer.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas);
|
||||
}
|
||||
this.dimensions = createRenderDimensions();
|
||||
this._devicePixelRatio = this._coreBrowserService.dpr;
|
||||
this._updateDimensions();
|
||||
|
||||
+1
@@ -42,6 +42,7 @@ export interface IRenderLayer extends IDisposable {
|
||||
readonly canvas: HTMLCanvasElement;
|
||||
readonly cacheCanvas: HTMLCanvasElement;
|
||||
|
||||
readonly onAddTextureAtlasCanvas: IEvent<HTMLCanvasElement>;
|
||||
/**
|
||||
* Called when the terminal loses focus.
|
||||
*/
|
||||
|
||||
@@ -17,6 +17,11 @@ declare module 'xterm-addon-canvas' {
|
||||
*/
|
||||
public readonly onChangeTextureAtlas: IEvent<HTMLCanvasElement>;
|
||||
|
||||
/**
|
||||
* An event that is fired when the a new page is added to the texture atlas.
|
||||
*/
|
||||
public readonly onAddTextureAtlasCanvas: IEvent<HTMLCanvasElement>;
|
||||
|
||||
constructor();
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,7 +8,6 @@ import { IWebGL2RenderingContext, IWebGLVertexArrayObject, IRenderModel } from '
|
||||
import { fill } from 'common/TypedArrayUtils';
|
||||
import { NULL_CELL_CODE } from 'common/buffer/Constants';
|
||||
import { Terminal } from 'xterm';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { IRasterizedGlyph, IRenderDimensions, ITextureAtlas } from 'browser/renderer/shared/Types';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
|
||||
@@ -30,8 +29,9 @@ const enum VertexAttribLocations {
|
||||
CELL_POSITION = 1,
|
||||
OFFSET = 2,
|
||||
SIZE = 3,
|
||||
TEXCOORD = 4,
|
||||
TEXSIZE = 5
|
||||
TEXPAGE = 4,
|
||||
TEXCOORD = 5,
|
||||
TEXSIZE = 6
|
||||
}
|
||||
|
||||
const vertexShaderSource = `#version 300 es
|
||||
@@ -39,6 +39,7 @@ layout (location = ${VertexAttribLocations.UNIT_QUAD}) in vec2 a_unitquad;
|
||||
layout (location = ${VertexAttribLocations.CELL_POSITION}) in vec2 a_cellpos;
|
||||
layout (location = ${VertexAttribLocations.OFFSET}) in vec2 a_offset;
|
||||
layout (location = ${VertexAttribLocations.SIZE}) in vec2 a_size;
|
||||
layout (location = ${VertexAttribLocations.TEXPAGE}) in float a_texpage;
|
||||
layout (location = ${VertexAttribLocations.TEXCOORD}) in vec2 a_texcoord;
|
||||
layout (location = ${VertexAttribLocations.TEXSIZE}) in vec2 a_texsize;
|
||||
|
||||
@@ -46,27 +47,38 @@ uniform mat4 u_projection;
|
||||
uniform vec2 u_resolution;
|
||||
|
||||
out vec2 v_texcoord;
|
||||
flat out int v_texpage;
|
||||
|
||||
void main() {
|
||||
vec2 zeroToOne = (a_offset / u_resolution) + a_cellpos + (a_unitquad * a_size);
|
||||
gl_Position = u_projection * vec4(zeroToOne, 0.0, 1.0);
|
||||
v_texpage = int(a_texpage);
|
||||
v_texcoord = a_texcoord + a_unitquad * a_texsize;
|
||||
}`;
|
||||
|
||||
const fragmentShaderSource = `#version 300 es
|
||||
function createFragmentShaderSource(maxFragmentShaderTextureUnits: number): string {
|
||||
let textureConditionals = '';
|
||||
for (let i = 1; i < maxFragmentShaderTextureUnits; i++) {
|
||||
textureConditionals += ` else if (v_texpage == ${i}) { outColor = texture(u_texture[${i}], v_texcoord); }`;
|
||||
}
|
||||
return (`#version 300 es
|
||||
precision lowp float;
|
||||
|
||||
in vec2 v_texcoord;
|
||||
flat in int v_texpage;
|
||||
|
||||
uniform sampler2D u_texture;
|
||||
uniform sampler2D u_texture[${maxFragmentShaderTextureUnits}];
|
||||
|
||||
out vec4 outColor;
|
||||
|
||||
void main() {
|
||||
outColor = texture(u_texture, v_texcoord);
|
||||
}`;
|
||||
if (v_texpage == 0) {
|
||||
outColor = texture(u_texture[0], v_texcoord);
|
||||
} ${textureConditionals}
|
||||
}`);
|
||||
}
|
||||
|
||||
const INDICES_PER_CELL = 10;
|
||||
const INDICES_PER_CELL = 11;
|
||||
const BYTES_PER_CELL = INDICES_PER_CELL * Float32Array.BYTES_PER_ELEMENT;
|
||||
const CELL_POSITION_INDICES = 2;
|
||||
|
||||
@@ -77,18 +89,17 @@ let $leftCellPadding = 0;
|
||||
let $clippedPixels = 0;
|
||||
|
||||
export class GlyphRenderer extends Disposable {
|
||||
private readonly _program: WebGLProgram;
|
||||
private readonly _vertexArrayObject: IWebGLVertexArrayObject;
|
||||
private readonly _projectionLocation: WebGLUniformLocation;
|
||||
private readonly _resolutionLocation: WebGLUniformLocation;
|
||||
private readonly _textureLocation: WebGLUniformLocation;
|
||||
private readonly _atlasTextures: WebGLTexture[];
|
||||
private readonly _attributesBuffer: WebGLBuffer;
|
||||
|
||||
private _atlas: ITextureAtlas | undefined;
|
||||
|
||||
private _program: WebGLProgram;
|
||||
private _vertexArrayObject: IWebGLVertexArrayObject;
|
||||
private _projectionLocation: WebGLUniformLocation;
|
||||
private _resolutionLocation: WebGLUniformLocation;
|
||||
private _textureLocation: WebGLUniformLocation;
|
||||
private _atlasTexture: WebGLTexture;
|
||||
private _attributesBuffer: WebGLBuffer;
|
||||
private _activeBuffer: number = 0;
|
||||
|
||||
private _vertices: IVertices = {
|
||||
private readonly _vertices: IVertices = {
|
||||
count: 0,
|
||||
attributes: new Float32Array(0),
|
||||
attributesBuffers: [
|
||||
@@ -97,15 +108,22 @@ export class GlyphRenderer extends Disposable {
|
||||
]
|
||||
};
|
||||
|
||||
private static _maxAtlasPages: number | undefined;
|
||||
|
||||
constructor(
|
||||
private _terminal: Terminal,
|
||||
private _gl: IWebGL2RenderingContext,
|
||||
private readonly _terminal: Terminal,
|
||||
private readonly _gl: IWebGL2RenderingContext,
|
||||
private _dimensions: IRenderDimensions
|
||||
) {
|
||||
super();
|
||||
|
||||
const gl = this._gl;
|
||||
this._program = throwIfFalsy(createProgram(gl, vertexShaderSource, fragmentShaderSource));
|
||||
|
||||
if (GlyphRenderer._maxAtlasPages === undefined) {
|
||||
GlyphRenderer._maxAtlasPages = throwIfFalsy(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS) as number | null);
|
||||
}
|
||||
|
||||
this._program = throwIfFalsy(createProgram(gl, vertexShaderSource, createFragmentShaderSource(GlyphRenderer._maxAtlasPages)));
|
||||
this.register(toDisposable(() => gl.deleteProgram(this._program)));
|
||||
|
||||
// Uniform locations
|
||||
@@ -145,23 +163,41 @@ export class GlyphRenderer extends Disposable {
|
||||
gl.enableVertexAttribArray(VertexAttribLocations.SIZE);
|
||||
gl.vertexAttribPointer(VertexAttribLocations.SIZE, 2, gl.FLOAT, false, BYTES_PER_CELL, 2 * Float32Array.BYTES_PER_ELEMENT);
|
||||
gl.vertexAttribDivisor(VertexAttribLocations.SIZE, 1);
|
||||
gl.enableVertexAttribArray(VertexAttribLocations.TEXPAGE);
|
||||
gl.vertexAttribPointer(VertexAttribLocations.TEXPAGE, 1, gl.FLOAT, false, BYTES_PER_CELL, 4 * Float32Array.BYTES_PER_ELEMENT);
|
||||
gl.vertexAttribDivisor(VertexAttribLocations.TEXPAGE, 1);
|
||||
gl.enableVertexAttribArray(VertexAttribLocations.TEXCOORD);
|
||||
gl.vertexAttribPointer(VertexAttribLocations.TEXCOORD, 2, gl.FLOAT, false, BYTES_PER_CELL, 4 * Float32Array.BYTES_PER_ELEMENT);
|
||||
gl.vertexAttribPointer(VertexAttribLocations.TEXCOORD, 2, gl.FLOAT, false, BYTES_PER_CELL, 5 * Float32Array.BYTES_PER_ELEMENT);
|
||||
gl.vertexAttribDivisor(VertexAttribLocations.TEXCOORD, 1);
|
||||
gl.enableVertexAttribArray(VertexAttribLocations.TEXSIZE);
|
||||
gl.vertexAttribPointer(VertexAttribLocations.TEXSIZE, 2, gl.FLOAT, false, BYTES_PER_CELL, 6 * Float32Array.BYTES_PER_ELEMENT);
|
||||
gl.vertexAttribPointer(VertexAttribLocations.TEXSIZE, 2, gl.FLOAT, false, BYTES_PER_CELL, 7 * Float32Array.BYTES_PER_ELEMENT);
|
||||
gl.vertexAttribDivisor(VertexAttribLocations.TEXSIZE, 1);
|
||||
gl.enableVertexAttribArray(VertexAttribLocations.CELL_POSITION);
|
||||
gl.vertexAttribPointer(VertexAttribLocations.CELL_POSITION, 2, gl.FLOAT, false, BYTES_PER_CELL, 8 * Float32Array.BYTES_PER_ELEMENT);
|
||||
gl.vertexAttribPointer(VertexAttribLocations.CELL_POSITION, 2, gl.FLOAT, false, BYTES_PER_CELL, 9 * Float32Array.BYTES_PER_ELEMENT);
|
||||
gl.vertexAttribDivisor(VertexAttribLocations.CELL_POSITION, 1);
|
||||
|
||||
// Setup empty texture atlas
|
||||
this._atlasTexture = throwIfFalsy(gl.createTexture());
|
||||
this.register(toDisposable(() => gl.deleteTexture(this._atlasTexture)));
|
||||
gl.bindTexture(gl.TEXTURE_2D, this._atlasTexture);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([0, 0, 255, 255]));
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||
// Setup static uniforms
|
||||
gl.useProgram(this._program);
|
||||
const textureUnits = new Int32Array(GlyphRenderer._maxAtlasPages);
|
||||
for (let i = 0; i < GlyphRenderer._maxAtlasPages; i++) {
|
||||
textureUnits[i] = i;
|
||||
}
|
||||
gl.uniform1iv(this._textureLocation, textureUnits);
|
||||
gl.uniformMatrix4fv(this._projectionLocation, false, PROJECTION_MATRIX);
|
||||
|
||||
// Setup 1x1 red pixel textures for all potential atlas pages, if one of these invalid textures
|
||||
// is ever drawn it will show characters as red rectangles.
|
||||
this._atlasTextures = [];
|
||||
for (let i = 0; i < GlyphRenderer._maxAtlasPages; i++) {
|
||||
const texture = throwIfFalsy(gl.createTexture());
|
||||
this.register(toDisposable(() => gl.deleteTexture(texture)));
|
||||
gl.activeTexture(gl.TEXTURE0 + i);
|
||||
gl.bindTexture(gl.TEXTURE_2D, texture);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([255, 0, 0, 255]));
|
||||
this._atlasTextures[i] = texture;
|
||||
}
|
||||
|
||||
// Allow drawing of transparent texture
|
||||
gl.enable(gl.BLEND);
|
||||
@@ -213,12 +249,14 @@ export class GlyphRenderer extends Disposable {
|
||||
// a_size
|
||||
array[$i + 2] = ($glyph.size.x - $clippedPixels) / this._dimensions.device.canvas.width;
|
||||
array[$i + 3] = $glyph.size.y / this._dimensions.device.canvas.height;
|
||||
// a_texpage
|
||||
array[$i + 4] = $glyph.texturePage;
|
||||
// a_texcoord
|
||||
array[$i + 4] = $glyph.texturePositionClipSpace.x + $clippedPixels / this._atlas.cacheCanvas.width;
|
||||
array[$i + 5] = $glyph.texturePositionClipSpace.y;
|
||||
array[$i + 5] = $glyph.texturePositionClipSpace.x + $clippedPixels / this._atlas.pages[$glyph.texturePage].canvas.width;
|
||||
array[$i + 6] = $glyph.texturePositionClipSpace.y;
|
||||
// a_texsize
|
||||
array[$i + 6] = $glyph.sizeClipSpace.x - $clippedPixels / this._atlas.cacheCanvas.width;
|
||||
array[$i + 7] = $glyph.sizeClipSpace.y;
|
||||
array[$i + 7] = $glyph.sizeClipSpace.x - $clippedPixels / this._atlas.pages[$glyph.texturePage].canvas.width;
|
||||
array[$i + 8] = $glyph.sizeClipSpace.y;
|
||||
} else {
|
||||
// a_origin
|
||||
array[$i ] = -$glyph.offset.x + this._dimensions.device.char.left;
|
||||
@@ -226,12 +264,14 @@ export class GlyphRenderer extends Disposable {
|
||||
// a_size
|
||||
array[$i + 2] = $glyph.size.x / this._dimensions.device.canvas.width;
|
||||
array[$i + 3] = $glyph.size.y / this._dimensions.device.canvas.height;
|
||||
// a_texpage
|
||||
array[$i + 4] = $glyph.texturePage;
|
||||
// a_texcoord
|
||||
array[$i + 4] = $glyph.texturePositionClipSpace.x;
|
||||
array[$i + 5] = $glyph.texturePositionClipSpace.y;
|
||||
array[$i + 5] = $glyph.texturePositionClipSpace.x;
|
||||
array[$i + 6] = $glyph.texturePositionClipSpace.y;
|
||||
// a_texsize
|
||||
array[$i + 6] = $glyph.sizeClipSpace.x;
|
||||
array[$i + 7] = $glyph.sizeClipSpace.y;
|
||||
array[$i + 7] = $glyph.sizeClipSpace.x;
|
||||
array[$i + 8] = $glyph.sizeClipSpace.y;
|
||||
}
|
||||
// a_cellpos only changes on resize
|
||||
}
|
||||
@@ -246,7 +286,8 @@ export class GlyphRenderer extends Disposable {
|
||||
} else {
|
||||
this._vertices.attributes.fill(0);
|
||||
}
|
||||
for (let i = 0; i < this._vertices.attributesBuffers.length; i++) {
|
||||
let i = 0;
|
||||
for (; i < this._vertices.attributesBuffers.length; i++) {
|
||||
if (this._vertices.count !== newCount) {
|
||||
this._vertices.attributesBuffers[i] = new Float32Array(newCount);
|
||||
} else {
|
||||
@@ -254,11 +295,11 @@ export class GlyphRenderer extends Disposable {
|
||||
}
|
||||
}
|
||||
this._vertices.count = newCount;
|
||||
let i = 0;
|
||||
i = 0;
|
||||
for (let y = 0; y < terminal.rows; y++) {
|
||||
for (let x = 0; x < terminal.cols; x++) {
|
||||
this._vertices.attributes[i + 8] = x / terminal.cols;
|
||||
this._vertices.attributes[i + 9] = y / terminal.rows;
|
||||
this._vertices.attributes[i + 9] = x / terminal.cols;
|
||||
this._vertices.attributes[i + 10] = y / terminal.rows;
|
||||
i += INDICES_PER_CELL;
|
||||
}
|
||||
}
|
||||
@@ -267,6 +308,7 @@ export class GlyphRenderer extends Disposable {
|
||||
public handleResize(): void {
|
||||
const gl = this._gl;
|
||||
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
|
||||
gl.uniform2f(this._resolutionLocation, gl.canvas.width, gl.canvas.height);
|
||||
this.clear();
|
||||
}
|
||||
|
||||
@@ -303,30 +345,31 @@ export class GlyphRenderer extends Disposable {
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, activeBuffer.subarray(0, bufferLength), gl.STREAM_DRAW);
|
||||
|
||||
// Bind the texture atlas if it's changed
|
||||
if (this._atlas.hasCanvasChanged) {
|
||||
this._atlas.hasCanvasChanged = false;
|
||||
gl.uniform1i(this._textureLocation, 0);
|
||||
gl.activeTexture(gl.TEXTURE0 + 0);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this._atlasTexture);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this._atlas.cacheCanvas);
|
||||
gl.generateMipmap(gl.TEXTURE_2D);
|
||||
// Bind the atlas page texture if they have changed
|
||||
for (let i = 0; i < this._atlas.pages.length; i++) {
|
||||
if (this._atlas.pages[i].hasCanvasChanged) {
|
||||
this._atlas.pages[i].hasCanvasChanged = false;
|
||||
this._bindAtlasPageTexture(gl, this._atlas, i);
|
||||
}
|
||||
}
|
||||
|
||||
// Set uniforms
|
||||
gl.uniformMatrix4fv(this._projectionLocation, false, PROJECTION_MATRIX);
|
||||
gl.uniform2f(this._resolutionLocation, gl.canvas.width, gl.canvas.height);
|
||||
|
||||
// Draw the viewport
|
||||
gl.drawElementsInstanced(gl.TRIANGLE_STRIP, 4, gl.UNSIGNED_BYTE, 0, bufferLength / INDICES_PER_CELL);
|
||||
}
|
||||
|
||||
public setAtlas(atlas: ITextureAtlas): void {
|
||||
const gl = this._gl;
|
||||
this._atlas = atlas;
|
||||
for (let i = 0; i < atlas.pages.length; i++) {
|
||||
this._bindAtlasPageTexture(this._gl, atlas, i);
|
||||
}
|
||||
}
|
||||
|
||||
gl.bindTexture(gl.TEXTURE_2D, this._atlasTexture);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, atlas.cacheCanvas);
|
||||
private _bindAtlasPageTexture(gl: IWebGL2RenderingContext, atlas: ITextureAtlas, i: number): void {
|
||||
gl.activeTexture(gl.TEXTURE0 + i);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this._atlasTextures[i]);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, atlas.pages[i].canvas);
|
||||
gl.generateMipmap(gl.TEXTURE_2D);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,8 +17,10 @@ export class WebglAddon extends Disposable implements ITerminalAddon {
|
||||
private _terminal?: Terminal;
|
||||
private _renderer?: WebglRenderer;
|
||||
|
||||
private readonly _onChangeTextureAtlas = this.register(new EventEmitter<HTMLElement>());
|
||||
private readonly _onChangeTextureAtlas = this.register(new EventEmitter<HTMLCanvasElement>());
|
||||
public readonly onChangeTextureAtlas = this._onChangeTextureAtlas.event;
|
||||
private readonly _onAddTextureAtlasCanvas = this.register(new EventEmitter<HTMLCanvasElement>());
|
||||
public readonly onAddTextureAtlasCanvas = this._onAddTextureAtlasCanvas.event;
|
||||
private readonly _onContextLoss = this.register(new EventEmitter<void>());
|
||||
public readonly onContextLoss = this._onContextLoss.event;
|
||||
|
||||
@@ -64,6 +66,7 @@ export class WebglAddon extends Disposable implements ITerminalAddon {
|
||||
));
|
||||
this.register(forwardEvent(this._renderer.onContextLoss, this._onContextLoss));
|
||||
this.register(forwardEvent(this._renderer.onChangeTextureAtlas, this._onChangeTextureAtlas));
|
||||
this.register(forwardEvent(this._renderer.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas));
|
||||
renderService.setRenderer(this._renderer);
|
||||
|
||||
this.register(toDisposable(() => {
|
||||
|
||||
@@ -7,18 +7,19 @@ import { addDisposableDomListener } from 'browser/Lifecycle';
|
||||
import { CellColorResolver } from 'browser/renderer/shared/CellColorResolver';
|
||||
import { acquireTextureAtlas, removeTerminalFromCache } from 'browser/renderer/shared/CharAtlasCache';
|
||||
import { observeDevicePixelDimensions } from 'browser/renderer/shared/DevicePixelObserver';
|
||||
import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils';
|
||||
import { createRenderDimensions, throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
|
||||
import { TextureAtlas } from 'browser/renderer/shared/TextureAtlas';
|
||||
import { IRenderDimensions, IRenderer, IRequestRedrawEvent, ITextureAtlas } from 'browser/renderer/shared/Types';
|
||||
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IThemeService } from 'browser/services/Services';
|
||||
import { ITerminal } from 'browser/Types';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { Content, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants';
|
||||
import { EventEmitter } from 'common/EventEmitter';
|
||||
import { EventEmitter, forwardEvent } from 'common/EventEmitter';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
|
||||
import { CharData, IBufferLine, ICellData } from 'common/Types';
|
||||
import { Terminal } from 'xterm';
|
||||
import { IDisposable, Terminal } from 'xterm';
|
||||
import { GlyphRenderer } from './GlyphRenderer';
|
||||
import { RectangleRenderer } from './RectangleRenderer';
|
||||
import { CursorRenderLayer } from './renderLayer/CursorRenderLayer';
|
||||
@@ -29,6 +30,7 @@ import { IWebGL2RenderingContext } from './Types';
|
||||
|
||||
export class WebglRenderer extends Disposable implements IRenderer {
|
||||
private _renderLayers: IRenderLayer[];
|
||||
private _charAtlasDisposable: IDisposable | undefined;
|
||||
private _charAtlas: ITextureAtlas | undefined;
|
||||
private _devicePixelRatio: number;
|
||||
|
||||
@@ -49,6 +51,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
|
||||
private readonly _onChangeTextureAtlas = this.register(new EventEmitter<HTMLCanvasElement>());
|
||||
public readonly onChangeTextureAtlas = this._onChangeTextureAtlas.event;
|
||||
private readonly _onAddTextureAtlasCanvas = this.register(new EventEmitter<HTMLCanvasElement>());
|
||||
public readonly onAddTextureAtlasCanvas = this._onAddTextureAtlasCanvas.event;
|
||||
private readonly _onRequestRedraw = this.register(new EventEmitter<IRequestRedrawEvent>());
|
||||
public readonly onRequestRedraw = this._onRequestRedraw.event;
|
||||
private readonly _onContextLoss = this.register(new EventEmitter<void>());
|
||||
@@ -135,7 +139,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
}
|
||||
|
||||
public get textureAtlas(): HTMLCanvasElement | undefined {
|
||||
return this._charAtlas?.cacheCanvas;
|
||||
return this._charAtlas?.pages[0].canvas;
|
||||
}
|
||||
|
||||
private _handleColorChange(): void {
|
||||
@@ -261,7 +265,10 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
this._coreBrowserService.dpr
|
||||
);
|
||||
if (this._charAtlas !== atlas) {
|
||||
this._onChangeTextureAtlas.fire(atlas.cacheCanvas);
|
||||
|
||||
this._charAtlasDisposable?.dispose();
|
||||
this._onChangeTextureAtlas.fire(atlas.pages[0].canvas);
|
||||
this._charAtlasDisposable = forwardEvent(atlas.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas);
|
||||
}
|
||||
this._charAtlas = atlas;
|
||||
this._charAtlas.warmUp();
|
||||
|
||||
@@ -22,6 +22,11 @@ declare module 'xterm-addon-webgl' {
|
||||
*/
|
||||
public readonly onChangeTextureAtlas: IEvent<HTMLCanvasElement>;
|
||||
|
||||
/**
|
||||
* An event that is fired when the a new page is added to the texture atlas.
|
||||
*/
|
||||
public readonly onAddTextureAtlasCanvas: IEvent<HTMLCanvasElement>;
|
||||
|
||||
constructor(preserveDrawingBuffer?: boolean);
|
||||
|
||||
/**
|
||||
|
||||
+31
-7
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
@@ -218,6 +219,7 @@ if (document.location.pathname === '/test') {
|
||||
document.getElementById('htmlserialize').addEventListener('click', htmlSerializeButtonHandler);
|
||||
document.getElementById('custom-glyph').addEventListener('click', writeCustomGlyphHandler);
|
||||
document.getElementById('load-test').addEventListener('click', loadTest);
|
||||
document.getElementById('print-cjk').addEventListener('click', addCjk);
|
||||
document.getElementById('powerline-symbol-test').addEventListener('click', powerlineSymbolTest);
|
||||
document.getElementById('underline-test').addEventListener('click', underlineTest);
|
||||
document.getElementById('ansi-colors').addEventListener('click', ansiColorsTest);
|
||||
@@ -273,8 +275,9 @@ function createTerminal(): void {
|
||||
typedTerm.loadAddon(addons.webgl.instance);
|
||||
setTimeout(() => {
|
||||
if (addons.webgl.instance !== undefined) {
|
||||
addTextureAtlas(addons.webgl.instance.textureAtlas);
|
||||
addons.webgl.instance.onChangeTextureAtlas(e => addTextureAtlas(e));
|
||||
setTextureAtlas(addons.webgl.instance.textureAtlas);
|
||||
addons.webgl.instance.onChangeTextureAtlas(e => setTextureAtlas(e));
|
||||
addons.webgl.instance.onAddTextureAtlasCanvas(e => appendTextureAtlas(e));
|
||||
}
|
||||
}, 0);
|
||||
|
||||
@@ -551,13 +554,15 @@ function initAddons(term: TerminalType): void {
|
||||
term.loadAddon(addon.instance);
|
||||
if (name === 'webgl') {
|
||||
setTimeout(() => {
|
||||
addTextureAtlas(addons.webgl.instance.textureAtlas);
|
||||
addons.webgl.instance.onChangeTextureAtlas(e => addTextureAtlas(e));
|
||||
setTextureAtlas(addons.webgl.instance.textureAtlas);
|
||||
addons.webgl.instance.onChangeTextureAtlas(e => setTextureAtlas(e));
|
||||
addons.webgl.instance.onAddTextureAtlasCanvas(e => appendTextureAtlas(e));
|
||||
}, 0);
|
||||
} else if (name === 'canvas') {
|
||||
setTimeout(() => {
|
||||
addTextureAtlas(addons.canvas.instance.textureAtlas);
|
||||
addons.canvas.instance.onChangeTextureAtlas(e => addTextureAtlas(e));
|
||||
setTextureAtlas(addons.canvas.instance.textureAtlas);
|
||||
addons.canvas.instance.onChangeTextureAtlas(e => setTextureAtlas(e));
|
||||
addons.canvas.instance.onAddTextureAtlasCanvas(e => appendTextureAtlas(e));
|
||||
}, 0);
|
||||
} else if (name === 'unicode11') {
|
||||
term.unicode.activeVersion = '11';
|
||||
@@ -648,9 +653,18 @@ function htmlSerializeButtonHandler(): void {
|
||||
document.getElementById('htmlserialize-output-result').innerText = 'Copied to clipboard';
|
||||
}
|
||||
|
||||
function addTextureAtlas(e: HTMLCanvasElement): void {
|
||||
function setTextureAtlas(e: HTMLCanvasElement): void {
|
||||
styleAtlasPage(e);
|
||||
document.querySelector('#texture-atlas').replaceChildren(e);
|
||||
}
|
||||
function appendTextureAtlas(e: HTMLCanvasElement): void {
|
||||
styleAtlasPage(e);
|
||||
document.querySelector('#texture-atlas').appendChild(e);
|
||||
}
|
||||
function styleAtlasPage(e: HTMLCanvasElement): void {
|
||||
e.style.width = `${e.width / window.devicePixelRatio}px`;
|
||||
e.style.height = `${e.height / window.devicePixelRatio}px`;
|
||||
}
|
||||
|
||||
function writeCustomGlyphHandler(): void {
|
||||
term.write('\n\r');
|
||||
@@ -965,6 +979,16 @@ function addAnsiHyperlink(): void {
|
||||
term.write('\x1b[3A\x1b[1C\x1b]8;;https://xtermjs.org\x07xter\x1b[B\x1b[4Dm.js\x1b]8;;\x07\x1b[2B\x1b[5D');
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the 20977 characters from the CJK Unified Ideographs unicode block.
|
||||
*/
|
||||
function addCjk(): void {
|
||||
term.write('\n\n\r');
|
||||
for (let i = 0x4E00; i < 0x9FCC; i++) {
|
||||
term.write(String.fromCharCode(i));
|
||||
}
|
||||
}
|
||||
|
||||
function addDecoration(): void {
|
||||
term.options['overviewRulerWidth'] = 15;
|
||||
const marker = term.registerMarker(1);
|
||||
|
||||
@@ -74,6 +74,7 @@
|
||||
|
||||
<dt>Performance</dt>
|
||||
<dd><button id="load-test" title="Write several MB of data to simulate a lot of data coming from the process">Load test</button></dd>
|
||||
<dd><button id="print-cjk" title="Prints the 20977 characters from the CJK Unified Ideographs unicode block">CJK Unified Ideographs</button></dd>
|
||||
|
||||
<dt>Styles</dt>
|
||||
<dd><button id="custom-glyph" title="Write custom box drawing and block element characters to the terminal">Test custom glyphs</button></dd>
|
||||
@@ -91,6 +92,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input type="checkbox" id="texture-atlas-zoom"/>
|
||||
<label for="texture-atlas-zoom">Zoom texture atlas</label>
|
||||
<div id="texture-atlas"></div>
|
||||
<script src="dist/client-bundle.js" defer ></script>
|
||||
<script>
|
||||
|
||||
+6
-7
@@ -90,16 +90,15 @@ pre {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#texture-atlas-zoom:checked + label + #texture-atlas canvas {
|
||||
/* Zoom atlas to the width of the container*/
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
}
|
||||
#texture-atlas {
|
||||
width: 100%;
|
||||
height: 600px;
|
||||
overflow: scroll;
|
||||
}
|
||||
#texture-atlas canvas {
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
#texture-atlas canvas:hover {
|
||||
/* zoom to 4x on hover */
|
||||
width: 4096px;
|
||||
height: 4096px;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
@@ -14,31 +14,30 @@ import { IUnicodeService } from 'common/services/Services';
|
||||
import { FourKeyMap } from 'common/MultiKeyMap';
|
||||
import { IdleTaskQueue } from 'common/TaskQueue';
|
||||
import { IBoundingBox, ICharAtlasConfig, IRasterizedGlyph, ITextureAtlas } from 'browser/renderer/shared/Types';
|
||||
import { EventEmitter } from 'common/EventEmitter';
|
||||
|
||||
// For debugging purposes, it can be useful to set this to a really tiny value,
|
||||
// to verify that LRU eviction works.
|
||||
const TEXTURE_WIDTH = 1024;
|
||||
const TEXTURE_HEIGHT = 1024;
|
||||
|
||||
/**
|
||||
* The amount of the texture to be filled before throwing it away and starting
|
||||
* again. Since the throw away and individual glyph draws don't cost too much,
|
||||
* this prevent juggling multiple textures in the GL context.
|
||||
*/
|
||||
const TEXTURE_CAPACITY = Math.floor(TEXTURE_HEIGHT * 0.8);
|
||||
/**
|
||||
* A shared object which is used to draw nothing for a particular cell.
|
||||
*/
|
||||
const NULL_RASTERIZED_GLYPH: IRasterizedGlyph = {
|
||||
offset: { x: 0, y: 0 },
|
||||
texturePage: 0,
|
||||
texturePosition: { x: 0, y: 0 },
|
||||
texturePositionClipSpace: { x: 0, y: 0 },
|
||||
offset: { x: 0, y: 0 },
|
||||
size: { x: 0, y: 0 },
|
||||
sizeClipSpace: { x: 0, y: 0 }
|
||||
};
|
||||
|
||||
const TMP_CANVAS_GLYPH_PADDING = 2;
|
||||
|
||||
const enum Constants {
|
||||
/**
|
||||
* The amount of pixel padding to allow in each row. Setting this to zero would make the atlas
|
||||
* page pack as tightly as possible, but more pages would end up being created as a result.
|
||||
*/
|
||||
ROW_PIXEL_THRESHOLD = 2
|
||||
}
|
||||
|
||||
interface ICharAtlasActiveRow {
|
||||
x: number;
|
||||
y: number;
|
||||
@@ -55,51 +54,35 @@ export class TextureAtlas implements ITextureAtlas {
|
||||
private _cacheMapCombined: FourKeyMap<string, number, number, number, IRasterizedGlyph> = new FourKeyMap();
|
||||
|
||||
// The texture that the atlas is drawn to
|
||||
public cacheCanvas: HTMLCanvasElement;
|
||||
private _cacheCtx: CanvasRenderingContext2D;
|
||||
private _pages: AtlasPage[] = [];
|
||||
public get pages(): { canvas: HTMLCanvasElement, hasCanvasChanged: boolean }[] { return this._pages; }
|
||||
|
||||
// The set of atlas pages that can be written to
|
||||
private _activePages: AtlasPage[] = [];
|
||||
|
||||
private _tmpCanvas: HTMLCanvasElement;
|
||||
// A temporary context that glyphs are drawn to before being transfered to the atlas.
|
||||
private _tmpCtx: CanvasRenderingContext2D;
|
||||
|
||||
// Texture atlas current positioning data. The texture packing strategy used is to fill from
|
||||
// left-to-right and top-to-bottom. When the glyph being written is less than half of the current
|
||||
// row's height, the following happens:
|
||||
//
|
||||
// - The current row becomes the fixed height row A
|
||||
// - A new fixed height row B the exact size of the glyph is created below the current row
|
||||
// - A new dynamic height current row is created below B
|
||||
//
|
||||
// This strategy does a good job preventing space being wasted for very short glyphs such as
|
||||
// underscores, hyphens etc. or those with underlines rendered.
|
||||
private _currentRow: ICharAtlasActiveRow = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
height: 0
|
||||
};
|
||||
private readonly _fixedRows: ICharAtlasActiveRow[] = [];
|
||||
|
||||
public hasCanvasChanged = false;
|
||||
|
||||
private _workBoundingBox: IBoundingBox = { top: 0, left: 0, bottom: 0, right: 0 };
|
||||
private _workAttributeData: AttributeData = new AttributeData();
|
||||
|
||||
private _textureSize: number = 512;
|
||||
|
||||
private readonly _onAddTextureAtlasCanvas = new EventEmitter<HTMLCanvasElement>();
|
||||
public readonly onAddTextureAtlasCanvas = this._onAddTextureAtlasCanvas.event;
|
||||
|
||||
constructor(
|
||||
document: Document,
|
||||
private readonly _document: Document,
|
||||
private readonly _config: ICharAtlasConfig,
|
||||
private readonly _unicodeService: IUnicodeService
|
||||
) {
|
||||
this.cacheCanvas = document.createElement('canvas');
|
||||
this.cacheCanvas.width = TEXTURE_WIDTH;
|
||||
this.cacheCanvas.height = TEXTURE_HEIGHT;
|
||||
// The canvas needs alpha because we use clearColor to convert the background color to alpha.
|
||||
// It might also contain some characters with transparent backgrounds if allowTransparency is
|
||||
// set.
|
||||
this._cacheCtx = throwIfFalsy(this.cacheCanvas.getContext('2d', { alpha: true }));
|
||||
|
||||
this._tmpCanvas = document.createElement('canvas');
|
||||
this._tmpCanvas.width = this._config.deviceCellWidth * 4 + TMP_CANVAS_GLYPH_PADDING * 2;
|
||||
this._tmpCanvas.height = this._config.deviceCellHeight + TMP_CANVAS_GLYPH_PADDING * 2;
|
||||
this._createNewPage();
|
||||
this._tmpCanvas = createCanvas(
|
||||
_document,
|
||||
this._config.deviceCellWidth * 4 + TMP_CANVAS_GLYPH_PADDING * 2,
|
||||
this._config.deviceCellHeight + TMP_CANVAS_GLYPH_PADDING * 2
|
||||
);
|
||||
this._tmpCtx = throwIfFalsy(this._tmpCanvas.getContext('2d', {
|
||||
alpha: this._config.allowTransparency,
|
||||
willReadFrequently: true
|
||||
@@ -107,9 +90,10 @@ export class TextureAtlas implements ITextureAtlas {
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this.cacheCanvas.parentElement) {
|
||||
this.cacheCanvas.parentElement.removeChild(this.cacheCanvas);
|
||||
for (const page of this.pages) {
|
||||
page.canvas.remove();
|
||||
}
|
||||
this._onAddTextureAtlasCanvas.dispose();
|
||||
}
|
||||
|
||||
public warmUp(): void {
|
||||
@@ -133,27 +117,43 @@ export class TextureAtlas implements ITextureAtlas {
|
||||
}
|
||||
|
||||
public beginFrame(): boolean {
|
||||
if (this._currentRow.y > TEXTURE_CAPACITY) {
|
||||
this.clearTexture();
|
||||
this.warmUp();
|
||||
return true;
|
||||
}
|
||||
// TODO: Something should happen to prevent reaching capacity
|
||||
return false;
|
||||
}
|
||||
|
||||
public clearTexture(): void {
|
||||
if (this._currentRow.x === 0 && this._currentRow.y === 0) {
|
||||
if (this._pages[0].currentRow.x === 0 && this._pages[0].currentRow.y === 0) {
|
||||
return;
|
||||
}
|
||||
this._cacheCtx.clearRect(0, 0, TEXTURE_WIDTH, TEXTURE_HEIGHT);
|
||||
for (const page of this._pages) {
|
||||
page.clear();
|
||||
}
|
||||
this._cacheMap.clear();
|
||||
this._cacheMapCombined.clear();
|
||||
this._currentRow.x = 0;
|
||||
this._currentRow.y = 0;
|
||||
this._currentRow.height = 0;
|
||||
this._fixedRows.length = 0;
|
||||
this._didWarmUp = false;
|
||||
this.hasCanvasChanged = true;
|
||||
}
|
||||
|
||||
private _createNewPage(): AtlasPage {
|
||||
if (this._pages.length === 4 || this._pages.length === 7) {
|
||||
this._increaseTextureSize();
|
||||
}
|
||||
// TODO: Ensure pages aren't created beyond the maximum supported
|
||||
const newPage = new AtlasPage(this._document, this._textureSize);
|
||||
this._pages.push(newPage);
|
||||
this._activePages.push(newPage);
|
||||
this._onAddTextureAtlasCanvas.fire(newPage.canvas);
|
||||
return newPage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Doubles the texture size of new atlas pages if allowed.
|
||||
*/
|
||||
private _increaseTextureSize(): void {
|
||||
// 4096 is the minimum texture size in WebGL, but we still want the texture to be reasonably fast
|
||||
// to upload. We could loosen this limit if it ever becomes a problem.
|
||||
if (this._textureSize < 2048) {
|
||||
this._textureSize *= 2;
|
||||
}
|
||||
}
|
||||
|
||||
public getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number, ext: number): IRasterizedGlyph {
|
||||
@@ -340,8 +340,6 @@ export class TextureAtlas implements ITextureAtlas {
|
||||
// Uncomment for debugging
|
||||
// console.log(`draw to cache "${chars}"`, bg, fg, ext);
|
||||
|
||||
this.hasCanvasChanged = true;
|
||||
|
||||
// Allow 1 cell width per character, with a minimum of 2 (CJK), plus some padding. This is used
|
||||
// to draw the glyph to the canvas as well as to restrict the bounding box search to ensure
|
||||
// giant ligatures (eg. =====>) don't impact overall performance.
|
||||
@@ -614,60 +612,108 @@ export class TextureAtlas implements ITextureAtlas {
|
||||
const rasterizedGlyph = this._findGlyphBoundingBox(imageData, this._workBoundingBox, allowedWidth, restrictedPowerlineGlyph, customGlyph, padding);
|
||||
|
||||
// Find the best atlas row to use
|
||||
let activePage: AtlasPage;
|
||||
let activeRow: ICharAtlasActiveRow;
|
||||
while (true) {
|
||||
// Select the ideal existing row, preferring fixed rows over the current row
|
||||
activeRow = this._currentRow;
|
||||
for (const row of this._fixedRows) {
|
||||
if ((activeRow === this._currentRow || row.height < activeRow.height) && rasterizedGlyph.size.y <= row.height) {
|
||||
activeRow = row;
|
||||
// Get the best current row from all active pages
|
||||
activePage = this._activePages[this._activePages.length - 1];
|
||||
activeRow = activePage.currentRow;
|
||||
for (const p of this._activePages) {
|
||||
if (rasterizedGlyph.size.y <= p.currentRow.height) {
|
||||
activePage = p;
|
||||
activeRow = p.currentRow;
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new one if vertical space would be wasted, fixing the previously active row in the
|
||||
// process as it now has a fixed height
|
||||
if (activeRow.height > rasterizedGlyph.size.y * 2) {
|
||||
// Fix the current row as the new row is being added below
|
||||
if (this._currentRow.height > 0) {
|
||||
this._fixedRows.push(this._currentRow);
|
||||
// TODO: This algorithm could be simplified:
|
||||
// - Search for the page with ROW_PIXEL_THRESHOLD in mind
|
||||
// - Keep track of current/fixed rows in a Map
|
||||
|
||||
// Replace the best current row with a fixed row if there is one at least as good as the
|
||||
// current row. Search in reverse to prioritize filling in older pages.
|
||||
for (let i = this._activePages.length - 1; i >= 0; i--) {
|
||||
for (const row of this._activePages[i].fixedRows) {
|
||||
if (row.height <= activeRow.height && rasterizedGlyph.size.y <= row.height) {
|
||||
activePage = this._activePages[i];
|
||||
activeRow = row;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create the new fixed height row
|
||||
activeRow = {
|
||||
x: 0,
|
||||
y: this._currentRow.y + this._currentRow.height,
|
||||
height: rasterizedGlyph.size.y
|
||||
};
|
||||
this._fixedRows.push(activeRow);
|
||||
// Create a new one if too much vertical space would be wasted or there is not enough room
|
||||
// left in the page. The previous active row will become fixed in the process as it now has a
|
||||
// fixed height
|
||||
if (activeRow.y + rasterizedGlyph.size.y >= activePage.canvas.height || activeRow.height > rasterizedGlyph.size.y + Constants.ROW_PIXEL_THRESHOLD) {
|
||||
// Create the new fixed height row, creating a new page if there isn't enough room on the
|
||||
// current page
|
||||
let wasNewPageCreated = false;
|
||||
if (activePage.currentRow.y + activePage.currentRow.height + rasterizedGlyph.size.y >= activePage.canvas.height) {
|
||||
// Find the first page with room to create the new row on
|
||||
let candidatePage: AtlasPage | undefined;
|
||||
for (const p of this._activePages) {
|
||||
if (p.currentRow.y + p.currentRow.height + rasterizedGlyph.size.y < p.canvas.height) {
|
||||
candidatePage = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (candidatePage) {
|
||||
activePage = candidatePage;
|
||||
} else {
|
||||
// Create a new page if there is no room
|
||||
const newPage = this._createNewPage();
|
||||
activePage = newPage;
|
||||
activeRow = newPage.currentRow;
|
||||
activeRow.height = rasterizedGlyph.size.y;
|
||||
wasNewPageCreated = true;
|
||||
}
|
||||
}
|
||||
if (!wasNewPageCreated) {
|
||||
// Fix the current row as the new row is being added below
|
||||
if (activePage.currentRow.height > 0) {
|
||||
activePage.fixedRows.push(activePage.currentRow);
|
||||
}
|
||||
activeRow = {
|
||||
x: 0,
|
||||
y: activePage.currentRow.y + activePage.currentRow.height,
|
||||
height: rasterizedGlyph.size.y
|
||||
};
|
||||
activePage.fixedRows.push(activeRow);
|
||||
|
||||
// Create the new current row below the new fixed height row
|
||||
this._currentRow = {
|
||||
x: 0,
|
||||
y: activeRow.y + activeRow.height,
|
||||
height: 0
|
||||
};
|
||||
// Create the new current row below the new fixed height row
|
||||
activePage.currentRow = {
|
||||
x: 0,
|
||||
y: activeRow.y + activeRow.height,
|
||||
height: 0
|
||||
};
|
||||
}
|
||||
// TODO: Remove pages from _activePages when all rows are filled
|
||||
}
|
||||
|
||||
// Exit the loop if there is enough room in the row
|
||||
if (activeRow.x + rasterizedGlyph.size.x <= TEXTURE_WIDTH) {
|
||||
if (activeRow.x + rasterizedGlyph.size.x <= activePage.canvas.width) {
|
||||
break;
|
||||
}
|
||||
|
||||
// If there is enough room in the current row, finish it and try again
|
||||
if (activeRow === this._currentRow) {
|
||||
// If there is not enough room in the current row, finish it and try again
|
||||
if (activeRow === activePage.currentRow) {
|
||||
activeRow.x = 0;
|
||||
activeRow.y += activeRow.height;
|
||||
activeRow.height = 0;
|
||||
} else {
|
||||
this._fixedRows.splice(this._fixedRows.indexOf(activeRow), 1);
|
||||
activePage.fixedRows.splice(activePage.fixedRows.indexOf(activeRow), 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Record texture position
|
||||
rasterizedGlyph.texturePage = this._pages.indexOf(activePage);
|
||||
rasterizedGlyph.texturePosition.x = activeRow.x;
|
||||
rasterizedGlyph.texturePosition.y = activeRow.y;
|
||||
rasterizedGlyph.texturePositionClipSpace.x = activeRow.x / TEXTURE_WIDTH;
|
||||
rasterizedGlyph.texturePositionClipSpace.y = activeRow.y / TEXTURE_HEIGHT;
|
||||
rasterizedGlyph.texturePositionClipSpace.x = activeRow.x / activePage.canvas.width;
|
||||
rasterizedGlyph.texturePositionClipSpace.y = activeRow.y / activePage.canvas.height;
|
||||
|
||||
// Fix the clipspace position as pages may be of differing size
|
||||
rasterizedGlyph.sizeClipSpace.x /= activePage.canvas.width;
|
||||
rasterizedGlyph.sizeClipSpace.y /= activePage.canvas.height;
|
||||
|
||||
// Update atlas current row, for fixed rows the glyph height will never be larger than the row
|
||||
// height
|
||||
@@ -675,7 +721,7 @@ export class TextureAtlas implements ITextureAtlas {
|
||||
activeRow.x += rasterizedGlyph.size.x;
|
||||
|
||||
// putImageData doesn't do any blending, so it will overwrite any existing cache entry for us
|
||||
this._cacheCtx.putImageData(
|
||||
activePage.ctx.putImageData(
|
||||
imageData,
|
||||
rasterizedGlyph.texturePosition.x - this._workBoundingBox.left,
|
||||
rasterizedGlyph.texturePosition.y - this._workBoundingBox.top,
|
||||
@@ -684,6 +730,7 @@ export class TextureAtlas implements ITextureAtlas {
|
||||
rasterizedGlyph.size.x,
|
||||
rasterizedGlyph.size.y
|
||||
);
|
||||
activePage.hasCanvasChanged = true;
|
||||
|
||||
return rasterizedGlyph;
|
||||
}
|
||||
@@ -759,6 +806,7 @@ export class TextureAtlas implements ITextureAtlas {
|
||||
}
|
||||
}
|
||||
return {
|
||||
texturePage: 0,
|
||||
texturePosition: { x: 0, y: 0 },
|
||||
texturePositionClipSpace: { x: 0, y: 0 },
|
||||
size: {
|
||||
@@ -766,8 +814,8 @@ export class TextureAtlas implements ITextureAtlas {
|
||||
y: boundingBox.bottom - boundingBox.top + 1
|
||||
},
|
||||
sizeClipSpace: {
|
||||
x: (boundingBox.right - boundingBox.left + 1) / TEXTURE_WIDTH,
|
||||
y: (boundingBox.bottom - boundingBox.top + 1) / TEXTURE_HEIGHT
|
||||
x: (boundingBox.right - boundingBox.left + 1),
|
||||
y: (boundingBox.bottom - boundingBox.top + 1)
|
||||
},
|
||||
offset: {
|
||||
x: -boundingBox.left + padding + ((restrictedGlyph || customGlyph) ? Math.floor((this._config.deviceCellWidth - this._config.deviceCharWidth) / 2) : 0),
|
||||
@@ -777,6 +825,54 @@ export class TextureAtlas implements ITextureAtlas {
|
||||
}
|
||||
}
|
||||
|
||||
class AtlasPage {
|
||||
public readonly canvas: HTMLCanvasElement;
|
||||
public readonly ctx: CanvasRenderingContext2D;
|
||||
|
||||
/**
|
||||
* Whether the canvas of the atlas page has changed, this is only set to true by the atlas, the
|
||||
* user of the boolean is required to reset its value to false.
|
||||
*/
|
||||
public hasCanvasChanged = false;
|
||||
|
||||
// Texture atlas current positioning data. The texture packing strategy used is to fill from
|
||||
// left-to-right and top-to-bottom. When the glyph being written is less than half of the current
|
||||
// row's height, the following happens:
|
||||
//
|
||||
// - The current row becomes the fixed height row A
|
||||
// - A new fixed height row B the exact size of the glyph is created below the current row
|
||||
// - A new dynamic height current row is created below B
|
||||
//
|
||||
// This strategy does a good job preventing space being wasted for very short glyphs such as
|
||||
// underscores, hyphens etc. or those with underlines rendered.
|
||||
public currentRow: ICharAtlasActiveRow = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
height: 0
|
||||
};
|
||||
public readonly fixedRows: ICharAtlasActiveRow[] = [];
|
||||
|
||||
constructor(
|
||||
document: Document,
|
||||
size: number
|
||||
) {
|
||||
this.canvas = createCanvas(document, size, size);
|
||||
// The canvas needs alpha because we use clearColor to convert the background color to alpha.
|
||||
// It might also contain some characters with transparent backgrounds if allowTransparency is
|
||||
// set.
|
||||
this.ctx = throwIfFalsy(this.canvas.getContext('2d', { alpha: true }));
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
this.currentRow.x = 0;
|
||||
this.currentRow.y = 0;
|
||||
this.currentRow.height = 0;
|
||||
this.fixedRows.length = 0;
|
||||
this.hasCanvasChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a particular rgb color and colors that are nearly the same in an ImageData completely
|
||||
* transparent.
|
||||
@@ -831,3 +927,10 @@ function checkCompletelyTransparent(imageData: ImageData): boolean {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function createCanvas(document: Document, width: number, height: number): HTMLCanvasElement {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
return canvas;
|
||||
}
|
||||
|
||||
+7
-2
@@ -87,8 +87,9 @@ export interface IRenderer extends IDisposable {
|
||||
}
|
||||
|
||||
export interface ITextureAtlas extends IDisposable {
|
||||
readonly cacheCanvas: HTMLCanvasElement;
|
||||
hasCanvasChanged: boolean;
|
||||
readonly pages: { canvas: HTMLCanvasElement, hasCanvasChanged: boolean }[];
|
||||
|
||||
onAddTextureAtlasCanvas: IEvent<HTMLCanvasElement>;
|
||||
|
||||
/**
|
||||
* Warm up the texture atlas, adding common glyphs to avoid slowing early frame.
|
||||
@@ -120,6 +121,10 @@ export interface IRasterizedGlyph {
|
||||
* in pixels.
|
||||
*/
|
||||
offset: IVector;
|
||||
/**
|
||||
* The index of the texture page that the glyph is on.
|
||||
*/
|
||||
texturePage: number;
|
||||
/**
|
||||
* the x and y position of the glyph in the texture in pixels.
|
||||
*/
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
export const DEFAULT_COLOR = 256;
|
||||
export const DEFAULT_COLOR = 0;
|
||||
export const DEFAULT_ATTR = (0 << 18) | (DEFAULT_COLOR << 9) | (256 << 0);
|
||||
export const DEFAULT_EXT = 0;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user