Merge pull request #4244 from Tyriar/multi_page_atlas

Support multiple texture atlas pages
This commit is contained in:
Daniel Imms
2022-10-30 18:30:11 -07:00
committed by GitHub
15 changed files with 402 additions and 184 deletions
@@ -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
View File
@@ -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();
/**
+101 -58
View File
@@ -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);
}
+4 -1
View File
@@ -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(() => {
+12 -5
View File
@@ -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
View File
@@ -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);
+3
View File
@@ -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
View File
@@ -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;
}
+197 -94
View File
@@ -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
View File
@@ -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.
*/
+1 -1
View File
@@ -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;