From 9459553665e94713bb8dd3415b8638c0f61eabdb Mon Sep 17 00:00:00 2001 From: Kenneth Russell Date: Fri, 26 Aug 2022 16:50:42 -0700 Subject: [PATCH 1/2] Handle WebGL context loss/restore events in WebGL renderer. If the context is restored within a brief period of time (3 seconds), recreate members dependent on WebGL state, and fire onRequestRedraw to the renderer's listeners. Handle changes to the char atlas better in the demo client. 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. --- addons/xterm-addon-webgl/src/WebglRenderer.ts | 52 ++++++++++++++++--- demo/client.ts | 2 +- 2 files changed, 45 insertions(+), 9 deletions(-) diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index 43de1c98b2..23c763a20e 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -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; @@ -62,6 +62,8 @@ export class WebglRenderer extends Disposable implements IRenderer { private _onContextLoss = new EventEmitter(); public get onContextLoss(): IEvent { return this._onContextLoss.event; } + private _contextRestorationTimeout: number | undefined; + constructor( private _terminal: Terminal, private _colors: IColorSet, @@ -108,16 +110,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._requestRedrawViewport(); + })); + 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!); } @@ -235,6 +256,21 @@ export class WebglRenderer extends Disposable implements IRenderer { this._refreshCharAtlas(); } + /** + * Initializes members dependent on WebGL context state. + */ + private _initializeWebGLState(): void { + // Dispose any previous rectangle and glyph renderers before creating new ones. + this._rectangleRenderer?.dispose(); + this._glyphRenderer?.dispose(); + + 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. diff --git a/demo/client.ts b/demo/client.ts index 3593004a35..2557fb44ea 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -606,7 +606,7 @@ function htmlSerializeButtonHandler(): void { } function addTextureAtlas(e: HTMLCanvasElement) { - document.querySelector('#texture-atlas').appendChild(e); + document.querySelector('#texture-atlas').replaceChildren(e); } function writeCustomGlyphHandler() { From f309affff2d7482188a67cd3c56cabdcb69fe543 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 31 Aug 2022 05:44:48 -0700 Subject: [PATCH 2/2] Tweak handling of _contextRestorationTimeout --- addons/xterm-addon-webgl/src/WebglRenderer.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index 23c763a20e..6dcd306141 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -53,6 +53,7 @@ export class WebglRenderer extends Disposable implements IRenderer { private _core: ITerminal; private _isAttached: boolean; + private _contextRestorationTimeout: number | undefined; private _onChangeTextureAtlas = new EventEmitter(); public get onChangeTextureAtlas(): IEvent { return this._onChangeTextureAtlas.event; } @@ -62,8 +63,6 @@ export class WebglRenderer extends Disposable implements IRenderer { private _onContextLoss = new EventEmitter(); public get onContextLoss(): IEvent { return this._onContextLoss.event; } - private _contextRestorationTimeout: number | undefined; - constructor( private _terminal: Terminal, private _colors: IColorSet, @@ -117,16 +116,15 @@ export class WebglRenderer extends Disposable implements IRenderer { // 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); - } + this._contextRestorationTimeout = undefined; + console.warn('webgl context not restored; firing onContextLoss'); + this._onContextLoss.fire(e); }, 3000 /* ms */); })); this.register(addDisposableDomListener(this._canvas, 'webglcontextrestored', (e) => { - console.log('webglcontextrestored event received'); + console.warn('webglcontextrestored event received'); clearTimeout(this._contextRestorationTimeout); - this._contextRestorationTimeout = 0; + this._contextRestorationTimeout = undefined; // The texture atlas and glyph renderer must be fully reinitialized // because their contents have been lost. removeTerminalFromCache(this._terminal);