Skip to content

Commit c126306

Browse files
authored
fallback to canvas renderer if webgl is not available, debug toggle for testing (#3081)
1 parent 134b388 commit c126306

File tree

8 files changed

+142
-20
lines changed

8 files changed

+142
-20
lines changed

frontend/app/view/term/term-model.ts

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ import * as jotai from "jotai";
4141
import * as React from "react";
4242
import { getBlockingCommand } from "./shellblocking";
4343
import { computeTheme, DefaultTermTheme } from "./termutil";
44-
import { TermWrap } from "./termwrap";
44+
import { TermWrap, WebGLSupported } from "./termwrap";
4545

4646
export class TermViewModel implements ViewModel {
4747
viewType: string;
@@ -155,7 +155,7 @@ export class TermViewModel implements ViewModel {
155155
if (isCmd) {
156156
const blockMeta = get(this.blockAtom)?.meta;
157157
let cmdText = blockMeta?.["cmd"];
158-
let cmdArgs = blockMeta?.["cmd:args"];
158+
const cmdArgs = blockMeta?.["cmd:args"];
159159
if (cmdArgs != null && Array.isArray(cmdArgs) && cmdArgs.length > 0) {
160160
cmdText += " " + cmdArgs.join(" ");
161161
}
@@ -242,7 +242,7 @@ export class TermViewModel implements ViewModel {
242242
});
243243
this.termTransparencyAtom = useBlockAtom(blockId, "termtransparencyatom", () => {
244244
return jotai.atom<number>((get) => {
245-
let value = get(getOverrideConfigAtom(this.blockId, "term:transparency")) ?? 0.5;
245+
const value = get(getOverrideConfigAtom(this.blockId, "term:transparency")) ?? 0.5;
246246
return boundNumber(value, 0, 1);
247247
});
248248
});
@@ -293,6 +293,13 @@ export class TermViewModel implements ViewModel {
293293
}
294294
}
295295

296+
if (get(getSettingsKeyAtom("debug:webglstatus"))) {
297+
const webglButton = this.getWebGlIconButton(get);
298+
if (webglButton) {
299+
rtn.push(webglButton);
300+
}
301+
}
302+
296303
if (blockData?.meta?.["controller"] != "cmd" && shellProcStatus != "done") {
297304
return rtn;
298305
}
@@ -438,6 +445,38 @@ export class TermViewModel implements ViewModel {
438445
return null;
439446
}
440447

448+
getWebGlIconButton(get: jotai.Getter): IconButtonDecl | null {
449+
if (!WebGLSupported) {
450+
return {
451+
elemtype: "iconbutton",
452+
icon: "microchip",
453+
iconColor: "var(--error-color)",
454+
title: "WebGL not supported",
455+
noAction: true,
456+
};
457+
}
458+
if (!this.termRef.current?.webglEnabledAtom) {
459+
return null;
460+
}
461+
const webglEnabled = get(this.termRef.current.webglEnabledAtom);
462+
if (webglEnabled) {
463+
return {
464+
elemtype: "iconbutton",
465+
icon: "microchip",
466+
iconColor: "var(--success-color)",
467+
title: "WebGL enabled (click to disable)",
468+
click: () => this.toggleWebGl(),
469+
};
470+
}
471+
return {
472+
elemtype: "iconbutton",
473+
icon: "microchip",
474+
iconColor: "var(--secondary-text-color)",
475+
title: "WebGL disabled (click to enable)",
476+
click: () => this.toggleWebGl(),
477+
};
478+
}
479+
441480
get viewComponent(): ViewComponent {
442481
return TerminalView as ViewComponent;
443482
}
@@ -478,6 +517,22 @@ export class TermViewModel implements ViewModel {
478517
});
479518
}
480519

520+
getTermRenderer(): "webgl" | "canvas" {
521+
return this.termRef.current?.getTermRenderer() ?? "canvas";
522+
}
523+
524+
isWebGlEnabled(): boolean {
525+
return this.termRef.current?.isWebGlEnabled() ?? false;
526+
}
527+
528+
toggleWebGl() {
529+
if (!this.termRef.current) {
530+
return;
531+
}
532+
const renderer = this.termRef.current.getTermRenderer() === "webgl" ? "canvas" : "webgl";
533+
this.termRef.current.setTermRenderer(renderer);
534+
}
535+
481536
triggerRestartAtom() {
482537
globalStore.set(this.isRestarting, true);
483538
setTimeout(() => {
@@ -544,7 +599,7 @@ export class TermViewModel implements ViewModel {
544599
console.log("search is open, not giving focus");
545600
return true;
546601
}
547-
let termMode = globalStore.get(this.termMode);
602+
const termMode = globalStore.get(this.termMode);
548603
if (termMode == "term") {
549604
if (this.termRef?.current?.terminal) {
550605
this.termRef.current.terminal.focus();

frontend/app/view/term/termwrap.ts

Lines changed: 66 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
import * as services from "@/store/services";
1919
import { PLATFORM, PlatformMacOS } from "@/util/platformutil";
2020
import { base64ToArray, fireAndForget } from "@/util/util";
21+
import { CanvasAddon } from "@xterm/addon-canvas";
2122
import { SearchAddon } from "@xterm/addon-search";
2223
import { SerializeAddon } from "@xterm/addon-serialize";
2324
import { WebLinksAddon } from "@xterm/addon-web-links";
@@ -49,14 +50,14 @@ const MaxRepaintTransactionMs = 2000;
4950
function detectWebGLSupport(): boolean {
5051
try {
5152
const canvas = document.createElement("canvas");
52-
const ctx = canvas.getContext("webgl");
53+
const ctx = canvas.getContext("webgl2");
5354
return !!ctx;
5455
} catch (e) {
5556
return false;
5657
}
5758
}
5859

59-
const WebGLSupported = detectWebGLSupport();
60+
export const WebGLSupported = detectWebGLSupport();
6061
let loggedWebGL = false;
6162

6263
type TermWrapOptions = {
@@ -84,7 +85,11 @@ export class TermWrap {
8485
multiInputCallback: (data: string) => void;
8586
sendDataHandler: (data: string) => void;
8687
onSearchResultsDidChange?: (result: { resultIndex: number; resultCount: number }) => void;
87-
private toDispose: TermTypes.IDisposable[] = [];
88+
toDispose: TermTypes.IDisposable[] = [];
89+
webglAddon: WebglAddon | null = null;
90+
canvasAddon: CanvasAddon | null = null;
91+
webglContextLossDisposable: TermTypes.IDisposable | null = null;
92+
webglEnabledAtom: jotai.PrimitiveAtom<boolean>;
8893
pasteActive: boolean = false;
8994
lastUpdated: number;
9095
promptMarkers: TermTypes.IMarker[] = [];
@@ -142,6 +147,7 @@ export class TermWrap {
142147
this.promptMarkers = [];
143148
this.shellIntegrationStatusAtom = jotai.atom(null) as jotai.PrimitiveAtom<ShellIntegrationStatus | null>;
144149
this.lastCommandAtom = jotai.atom(null) as jotai.PrimitiveAtom<string | null>;
150+
this.webglEnabledAtom = jotai.atom(false) as jotai.PrimitiveAtom<boolean>;
145151
this.terminal = new Terminal(options);
146152
this.fitAddon = new FitAddon();
147153
this.fitAddon.scrollbarWidth = 6; // this needs to match scrollbar width in term.scss
@@ -179,19 +185,7 @@ export class TermWrap {
179185
}
180186
)
181187
);
182-
if (WebGLSupported && waveOptions.useWebGl) {
183-
const webglAddon = new WebglAddon();
184-
this.toDispose.push(
185-
webglAddon.onContextLoss(() => {
186-
webglAddon.dispose();
187-
})
188-
);
189-
this.terminal.loadAddon(webglAddon);
190-
if (!loggedWebGL) {
191-
console.log("loaded webgl!");
192-
loggedWebGL = true;
193-
}
194-
}
188+
this.setTermRenderer(WebGLSupported && waveOptions.useWebGl ? "webgl" : "canvas");
195189
// Register OSC handlers
196190
this.terminal.parser.registerOscHandler(7, (data: string) => {
197191
return handleOsc7Command(data, this.blockId, this.loaded);
@@ -307,6 +301,60 @@ export class TermWrap {
307301
this.terminal.options.cursorBlink = cursorBlink ?? false;
308302
}
309303

304+
setTermRenderer(renderer: "webgl" | "canvas") {
305+
if (renderer === "webgl") {
306+
if (this.webglAddon != null) {
307+
return;
308+
}
309+
if (!WebGLSupported) {
310+
renderer = "canvas";
311+
if (this.canvasAddon != null) {
312+
return;
313+
}
314+
}
315+
} else {
316+
if (this.canvasAddon != null) {
317+
return;
318+
}
319+
}
320+
if (this.webglAddon != null) {
321+
this.webglContextLossDisposable?.dispose();
322+
this.webglContextLossDisposable = null;
323+
this.webglAddon.dispose();
324+
this.webglAddon = null;
325+
globalStore.set(this.webglEnabledAtom, false);
326+
}
327+
if (this.canvasAddon != null) {
328+
this.canvasAddon.dispose();
329+
this.canvasAddon = null;
330+
}
331+
if (renderer === "webgl") {
332+
const addon = new WebglAddon();
333+
this.webglContextLossDisposable = addon.onContextLoss(() => {
334+
this.setTermRenderer("canvas");
335+
});
336+
this.terminal.loadAddon(addon);
337+
this.webglAddon = addon;
338+
globalStore.set(this.webglEnabledAtom, true);
339+
if (!loggedWebGL) {
340+
console.log("loaded webgl!");
341+
loggedWebGL = true;
342+
}
343+
} else {
344+
const addon = new CanvasAddon();
345+
this.terminal.loadAddon(addon);
346+
this.canvasAddon = addon;
347+
}
348+
}
349+
350+
getTermRenderer(): "webgl" | "canvas" {
351+
return this.webglAddon != null ? "webgl" : "canvas";
352+
}
353+
354+
isWebGlEnabled(): boolean {
355+
return this.webglAddon != null;
356+
}
357+
310358
resetCompositionState() {
311359
this.isComposing = false;
312360
this.composingData = "";
@@ -422,6 +470,8 @@ export class TermWrap {
422470
} catch (_) {}
423471
});
424472
this.promptMarkers = [];
473+
this.webglContextLossDisposable?.dispose();
474+
this.webglContextLossDisposable = null;
425475
this.terminal.dispose();
426476
this.toDispose.forEach((d) => {
427477
try {

frontend/types/gotypes.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1395,6 +1395,7 @@ declare global {
13951395
"debug:*"?: boolean;
13961396
"debug:pprofport"?: number;
13971397
"debug:pprofmemprofilerate"?: number;
1398+
"debug:webglstatus"?: boolean;
13981399
"tsunami:*"?: boolean;
13991400
"tsunami:scaffoldpath"?: string;
14001401
"tsunami:sdkreplacepath"?: string;

package-lock.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
"@table-nav/react": "^0.0.7",
8383
"@tanstack/react-table": "^8.21.3",
8484
"@tanstack/react-virtual": "^3.13.19",
85+
"@xterm/addon-canvas": "^0.7.0",
8586
"@xterm/addon-fit": "^0.10.0",
8687
"@xterm/addon-search": "^0.15.0",
8788
"@xterm/addon-serialize": "^0.13.0",

pkg/wconfig/metaconsts.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ const (
121121
ConfigKey_DebugClear = "debug:*"
122122
ConfigKey_DebugPprofPort = "debug:pprofport"
123123
ConfigKey_DebugPprofMemProfileRate = "debug:pprofmemprofilerate"
124+
ConfigKey_DebugWebGlStatus = "debug:webglstatus"
124125

125126
ConfigKey_TsunamiClear = "tsunami:*"
126127
ConfigKey_TsunamiScaffoldPath = "tsunami:scaffoldpath"

pkg/wconfig/settingsconfig.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ type SettingsType struct {
172172
DebugClear bool `json:"debug:*,omitempty"`
173173
DebugPprofPort *int `json:"debug:pprofport,omitempty"`
174174
DebugPprofMemProfileRate *int `json:"debug:pprofmemprofilerate,omitempty"`
175+
DebugWebGlStatus bool `json:"debug:webglstatus,omitempty"`
175176

176177
TsunamiClear bool `json:"tsunami:*,omitempty"`
177178
TsunamiScaffoldPath string `json:"tsunami:scaffoldpath,omitempty"`

schema/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,9 @@
325325
"debug:pprofmemprofilerate": {
326326
"type": "integer"
327327
},
328+
"debug:webglstatus": {
329+
"type": "boolean"
330+
},
328331
"tsunami:*": {
329332
"type": "boolean"
330333
},

0 commit comments

Comments
 (0)