Skip to content

Commit

Permalink
Handle WebGL context loss/restore events in WebGL addon.
Browse files Browse the repository at this point in the history
Define a new WebglAddon.onNeedsRedraw event which signals to embedders
that the terminal needs to be refreshed. Fire this event upon a
webglcontextlost / webglcontextrestored event pair internally, first
reinitializing WebglRenderer's internal state.

Respond to the event in the demo's client by refreshing the terminal.

Tested in Chrome Canary on macOS by running the demo in one window,
and visiting the internal URL about:gpucrash in another window. The
terminal now recovers and redraws properly.
  • Loading branch information
kenrussell committed Aug 29, 2022
1 parent 7dae4bc commit 23e0f3a
Show file tree
Hide file tree
Showing 4 changed files with 62 additions and 14 deletions.
3 changes: 3 additions & 0 deletions addons/xterm-addon-webgl/src/WebglAddon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export class WebglAddon implements ITerminalAddon {
public get onChangeTextureAtlas(): IEvent<HTMLElement> { return this._onChangeTextureAtlas.event; }
private _onContextLoss = new EventEmitter<void>();
public get onContextLoss(): IEvent<void> { return this._onContextLoss.event; }
private _onNeedsRedraw = new EventEmitter<void>();
public get onNeedsRedraw(): IEvent<void> { return this._onNeedsRedraw.event; }

constructor(
private _preserveDrawingBuffer?: boolean
Expand All @@ -40,6 +42,7 @@ export class WebglAddon implements ITerminalAddon {
const colors: IColorSet = (terminal as any)._core._colorManager.colors;
this._renderer = new WebglRenderer(terminal, colors, characterJoinerService, coreBrowserService, coreService, decorationService, this._preserveDrawingBuffer);
forwardEvent(this._renderer.onContextLoss, this._onContextLoss);
forwardEvent(this._renderer.onNeedsRedraw, this._onNeedsRedraw);
forwardEvent(this._renderer.onChangeTextureAtlas, this._onChangeTextureAtlas);
renderService.setRenderer(this._renderer);
}
Expand Down
51 changes: 43 additions & 8 deletions addons/xterm-addon-webgl/src/WebglRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ export class WebglRenderer extends Disposable implements IRenderer {

private _canvas: HTMLCanvasElement;
private _gl: IWebGL2RenderingContext;
private _rectangleRenderer: RectangleRenderer;
private _glyphRenderer: GlyphRenderer;
private _rectangleRenderer!: RectangleRenderer;
private _glyphRenderer!: GlyphRenderer;

public dimensions: IRenderDimensions;

Expand All @@ -62,6 +62,11 @@ export class WebglRenderer extends Disposable implements IRenderer {
private _onContextLoss = new EventEmitter<void>();
public get onContextLoss(): IEvent<void> { return this._onContextLoss.event; }

private _onNeedsRedraw = new EventEmitter<void>();
public get onNeedsRedraw(): IEvent<void> { return this._onNeedsRedraw.event; }

private _contextRestorationTimeout: number = 0;

constructor(
private _terminal: Terminal,
private _colors: IColorSet,
Expand Down Expand Up @@ -108,16 +113,35 @@ export class WebglRenderer extends Disposable implements IRenderer {
throw new Error('WebGL2 not supported ' + this._gl);
}

this.register(addDisposableDomListener(this._canvas, 'webglcontextlost', (e) => { this._onContextLoss.fire(e); }));
this.register(addDisposableDomListener(this._canvas, 'webglcontextlost', (e) => {
console.log('webglcontextlost event received');
// Prevent the default behavior in order to enable WebGL context restoration.
e.preventDefault();
// Wait a few seconds to see if the 'webglcontextrestored' event is fired.
// If not, dispatch the onContextLoss notification to observers.
this._contextRestorationTimeout = setTimeout(() => {
if (this._contextRestorationTimeout !== 0) {
console.log('webgl context not restored; firing onContextLoss');
this._onContextLoss.fire(e);
}
}, 3000 /* ms */);
}));
this.register(addDisposableDomListener(this._canvas, 'webglcontextrestored', (e) => {
console.log('webglcontextrestored event received');
clearTimeout(this._contextRestorationTimeout);
this._contextRestorationTimeout = 0;
// The texture atlas and glyph renderer must be fully reinitialized
// because their contents have been lost.
removeTerminalFromCache(this._terminal);
this._initializeWebGLState();
this._onNeedsRedraw.fire(e);
}));

this.register(observeDevicePixelDimensions(this._canvas, (w, h) => this._setCanvasDevicePixelDimensions(w, h)));

this._core.screenElement!.appendChild(this._canvas);

this._rectangleRenderer = this.register(new RectangleRenderer(this._terminal, this._colors, this._gl, this.dimensions));
this._glyphRenderer = this.register(new GlyphRenderer(this._terminal, this._colors, this._gl, this.dimensions));

// Update dimensions and acquire char atlas
this.onCharSizeChanged();
this._initializeWebGLState();

this._isAttached = document.body.contains(this._core.screenElement!);
}
Expand Down Expand Up @@ -235,6 +259,17 @@ export class WebglRenderer extends Disposable implements IRenderer {
this._refreshCharAtlas();
}

/**
* Initializes members dependent on WebGL context state.
*/
private _initializeWebGLState(): void {
this._rectangleRenderer = new RectangleRenderer(this._terminal, this._colors, this._gl, this.dimensions);
this._glyphRenderer = new GlyphRenderer(this._terminal, this._colors, this._gl, this.dimensions);

// Update dimensions and acquire char atlas
this.onCharSizeChanged();
}

/**
* Refreshes the char atlas, aquiring a new one if necessary.
* @param terminal The terminal.
Expand Down
8 changes: 7 additions & 1 deletion addons/xterm-addon-webgl/typings/xterm-addon-webgl.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,16 @@ declare module 'xterm-addon-webgl' {
public textureAtlas?: HTMLCanvasElement;

/**
* An event that is fired when the renderer loses its canvas context.
* An event that is fired when the renderer loses its canvas context irrevocably.
*/
public get onContextLoss(): IEvent<void>;

/**
* Fired when the WebglRenderer requires a redraw; happens when the context is
* lost and rapidly restored.
*/
public get onNeedsRedraw(): IEvent<void>;

/**
* An event that is fired when the texture atlas of the renderer changes.
*/
Expand Down
14 changes: 9 additions & 5 deletions demo/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,8 +241,12 @@ function createTerminal(): void {
try {
typedTerm.loadAddon(addons.webgl.instance);
setTimeout(() => {
addTextureAtlas(addons.webgl.instance.textureAtlas);
addons.webgl.instance.onChangeTextureAtlas(e => addTextureAtlas(e));
showTextureAtlas(addons.webgl.instance.textureAtlas);
addons.webgl.instance.onChangeTextureAtlas(e => showTextureAtlas(e));
addons.webgl.instance.onNeedsRedraw(() => {
console.log('Redrawing WebGL renderer');
typedTerm.refresh(0, typedTerm.rows - 1);
});
}, 0);
}
catch {
Expand Down Expand Up @@ -514,7 +518,7 @@ function initAddons(term: TerminalType): void {
try {
term.loadAddon(addon.instance);
if (name === 'webgl') {
(addon.instance as WebglAddon).onChangeTextureAtlas(e => addTextureAtlas(e));
(addon.instance as WebglAddon).onChangeTextureAtlas(e => showTextureAtlas(e));
} else if (name === 'unicode11') {
term.unicode.activeVersion = '11';
} else if (name === 'search') {
Expand Down Expand Up @@ -604,8 +608,8 @@ function htmlSerializeButtonHandler(): void {
document.getElementById("htmlserialize-output-result").innerText = "Copied to clipboard";
}

function addTextureAtlas(e: HTMLCanvasElement) {
document.querySelector('#texture-atlas').appendChild(e);
function showTextureAtlas(e: HTMLCanvasElement) {
document.querySelector('#texture-atlas').replaceChildren(e);
}

function writeCustomGlyphHandler() {
Expand Down

0 comments on commit 23e0f3a

Please sign in to comment.