From 7e4c781d929f54aca9c85ad3016747dcb793debf Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 18 Mar 2026 12:34:13 -0700 Subject: [PATCH 1/4] webgl status icon/toggle for debugging --- frontend/app/view/term/term-model.ts | 56 +++++++++++++++++++++++++++- frontend/app/view/term/termwrap.ts | 55 +++++++++++++++++++++------ frontend/types/gotypes.d.ts | 1 + pkg/wconfig/metaconsts.go | 1 + pkg/wconfig/settingsconfig.go | 1 + schema/settings.json | 3 ++ 6 files changed, 104 insertions(+), 13 deletions(-) diff --git a/frontend/app/view/term/term-model.ts b/frontend/app/view/term/term-model.ts index 9cb1c58720..1adcb2e8b3 100644 --- a/frontend/app/view/term/term-model.ts +++ b/frontend/app/view/term/term-model.ts @@ -41,7 +41,7 @@ import * as jotai from "jotai"; import * as React from "react"; import { getBlockingCommand } from "./shellblocking"; import { computeTheme, DefaultTermTheme } from "./termutil"; -import { TermWrap } from "./termwrap"; +import { TermWrap, WebGLSupported } from "./termwrap"; export class TermViewModel implements ViewModel { viewType: string; @@ -293,6 +293,13 @@ export class TermViewModel implements ViewModel { } } + if (get(getSettingsKeyAtom("debug:webglstatus"))) { + const webglButton = this.getWebGlIconButton(get); + if (webglButton) { + rtn.push(webglButton); + } + } + if (blockData?.meta?.["controller"] != "cmd" && shellProcStatus != "done") { return rtn; } @@ -438,6 +445,38 @@ export class TermViewModel implements ViewModel { return null; } + getWebGlIconButton(get: jotai.Getter): IconButtonDecl | null { + if (!WebGLSupported) { + return { + elemtype: "iconbutton", + icon: "microchip", + iconColor: "var(--error-color)", + title: "WebGL not supported", + noAction: true, + }; + } + if (!this.termRef.current?.webglEnabledAtom) { + return null; + } + const webglEnabled = get(this.termRef.current.webglEnabledAtom); + if (webglEnabled) { + return { + elemtype: "iconbutton", + icon: "microchip", + iconColor: "var(--success-color)", + title: "WebGL enabled (click to disable)", + click: () => this.toggleWebGl(), + }; + } + return { + elemtype: "iconbutton", + icon: "microchip", + iconColor: "var(--secondary-text-color)", + title: "WebGL disabled (click to enable)", + click: () => this.toggleWebGl(), + }; + } + get viewComponent(): ViewComponent { return TerminalView as ViewComponent; } @@ -478,6 +517,21 @@ export class TermViewModel implements ViewModel { }); } + isWebGlEnabled(): boolean { + return this.termRef.current?.isWebGlEnabled() ?? false; + } + + toggleWebGl() { + if (!this.termRef.current) { + return; + } + if (this.termRef.current.isWebGlEnabled()) { + this.termRef.current.disableWebGl(); + } else { + this.termRef.current.enableWebGl(); + } + } + triggerRestartAtom() { globalStore.set(this.isRestarting, true); setTimeout(() => { diff --git a/frontend/app/view/term/termwrap.ts b/frontend/app/view/term/termwrap.ts index 1cd167c800..cb5769b4f9 100644 --- a/frontend/app/view/term/termwrap.ts +++ b/frontend/app/view/term/termwrap.ts @@ -56,7 +56,7 @@ function detectWebGLSupport(): boolean { } } -const WebGLSupported = detectWebGLSupport(); +export const WebGLSupported = detectWebGLSupport(); let loggedWebGL = false; type TermWrapOptions = { @@ -85,6 +85,8 @@ export class TermWrap { sendDataHandler: (data: string) => void; onSearchResultsDidChange?: (result: { resultIndex: number; resultCount: number }) => void; private toDispose: TermTypes.IDisposable[] = []; + webglAddon: WebglAddon | null = null; + webglEnabledAtom: jotai.PrimitiveAtom; pasteActive: boolean = false; lastUpdated: number; promptMarkers: TermTypes.IMarker[] = []; @@ -142,6 +144,7 @@ export class TermWrap { this.promptMarkers = []; this.shellIntegrationStatusAtom = jotai.atom(null) as jotai.PrimitiveAtom; this.lastCommandAtom = jotai.atom(null) as jotai.PrimitiveAtom; + this.webglEnabledAtom = jotai.atom(false) as jotai.PrimitiveAtom; this.terminal = new Terminal(options); this.fitAddon = new FitAddon(); this.fitAddon.scrollbarWidth = 6; // this needs to match scrollbar width in term.scss @@ -180,17 +183,7 @@ export class TermWrap { ) ); if (WebGLSupported && waveOptions.useWebGl) { - const webglAddon = new WebglAddon(); - this.toDispose.push( - webglAddon.onContextLoss(() => { - webglAddon.dispose(); - }) - ); - this.terminal.loadAddon(webglAddon); - if (!loggedWebGL) { - console.log("loaded webgl!"); - loggedWebGL = true; - } + this.loadWebGlAddon(); } // Register OSC handlers this.terminal.parser.registerOscHandler(7, (data: string) => { @@ -307,6 +300,44 @@ export class TermWrap { this.terminal.options.cursorBlink = cursorBlink ?? false; } + loadWebGlAddon() { + const addon = new WebglAddon(); + this.toDispose.push( + addon.onContextLoss(() => { + if (addon === this.webglAddon) { + this.disableWebGl(); + } + }) + ); + this.terminal.loadAddon(addon); + this.webglAddon = addon; + globalStore.set(this.webglEnabledAtom, true); + if (!loggedWebGL) { + console.log("loaded webgl!"); + loggedWebGL = true; + } + } + + isWebGlEnabled(): boolean { + return this.webglAddon != null; + } + + enableWebGl() { + if (!WebGLSupported || this.webglAddon != null) { + return; + } + this.loadWebGlAddon(); + } + + disableWebGl() { + if (this.webglAddon == null) { + return; + } + this.webglAddon.dispose(); + this.webglAddon = null; + globalStore.set(this.webglEnabledAtom, false); + } + resetCompositionState() { this.isComposing = false; this.composingData = ""; diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index 3dede74071..f2ec1d2c64 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -1395,6 +1395,7 @@ declare global { "debug:*"?: boolean; "debug:pprofport"?: number; "debug:pprofmemprofilerate"?: number; + "debug:webglstatus"?: boolean; "tsunami:*"?: boolean; "tsunami:scaffoldpath"?: string; "tsunami:sdkreplacepath"?: string; diff --git a/pkg/wconfig/metaconsts.go b/pkg/wconfig/metaconsts.go index 8195495ad2..28f481ac16 100644 --- a/pkg/wconfig/metaconsts.go +++ b/pkg/wconfig/metaconsts.go @@ -121,6 +121,7 @@ const ( ConfigKey_DebugClear = "debug:*" ConfigKey_DebugPprofPort = "debug:pprofport" ConfigKey_DebugPprofMemProfileRate = "debug:pprofmemprofilerate" + ConfigKey_DebugWebGlStatus = "debug:webglstatus" ConfigKey_TsunamiClear = "tsunami:*" ConfigKey_TsunamiScaffoldPath = "tsunami:scaffoldpath" diff --git a/pkg/wconfig/settingsconfig.go b/pkg/wconfig/settingsconfig.go index b1b10d977f..c52012803d 100644 --- a/pkg/wconfig/settingsconfig.go +++ b/pkg/wconfig/settingsconfig.go @@ -172,6 +172,7 @@ type SettingsType struct { DebugClear bool `json:"debug:*,omitempty"` DebugPprofPort *int `json:"debug:pprofport,omitempty"` DebugPprofMemProfileRate *int `json:"debug:pprofmemprofilerate,omitempty"` + DebugWebGlStatus bool `json:"debug:webglstatus,omitempty"` TsunamiClear bool `json:"tsunami:*,omitempty"` TsunamiScaffoldPath string `json:"tsunami:scaffoldpath,omitempty"` diff --git a/schema/settings.json b/schema/settings.json index 5213fed365..eda7dc3326 100644 --- a/schema/settings.json +++ b/schema/settings.json @@ -325,6 +325,9 @@ "debug:pprofmemprofilerate": { "type": "integer" }, + "debug:webglstatus": { + "type": "boolean" + }, "tsunami:*": { "type": "boolean" }, From 5956c25f1256de143f2e2657a351b50c5fafc5a5 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 18 Mar 2026 12:57:06 -0700 Subject: [PATCH 2/4] webgl, fallback to canvas renderer --- frontend/app/view/term/term-model.ts | 17 +++--- frontend/app/view/term/termwrap.ts | 83 ++++++++++++++++------------ package-lock.json | 10 ++++ package.json | 1 + 4 files changed, 69 insertions(+), 42 deletions(-) diff --git a/frontend/app/view/term/term-model.ts b/frontend/app/view/term/term-model.ts index 1adcb2e8b3..eedc3296b3 100644 --- a/frontend/app/view/term/term-model.ts +++ b/frontend/app/view/term/term-model.ts @@ -155,7 +155,7 @@ export class TermViewModel implements ViewModel { if (isCmd) { const blockMeta = get(this.blockAtom)?.meta; let cmdText = blockMeta?.["cmd"]; - let cmdArgs = blockMeta?.["cmd:args"]; + const cmdArgs = blockMeta?.["cmd:args"]; if (cmdArgs != null && Array.isArray(cmdArgs) && cmdArgs.length > 0) { cmdText += " " + cmdArgs.join(" "); } @@ -242,7 +242,7 @@ export class TermViewModel implements ViewModel { }); this.termTransparencyAtom = useBlockAtom(blockId, "termtransparencyatom", () => { return jotai.atom((get) => { - let value = get(getOverrideConfigAtom(this.blockId, "term:transparency")) ?? 0.5; + const value = get(getOverrideConfigAtom(this.blockId, "term:transparency")) ?? 0.5; return boundNumber(value, 0, 1); }); }); @@ -517,6 +517,10 @@ export class TermViewModel implements ViewModel { }); } + getTermRenderer(): "webgl" | "canvas" { + return this.termRef.current?.getTermRenderer() ?? "canvas"; + } + isWebGlEnabled(): boolean { return this.termRef.current?.isWebGlEnabled() ?? false; } @@ -525,11 +529,8 @@ export class TermViewModel implements ViewModel { if (!this.termRef.current) { return; } - if (this.termRef.current.isWebGlEnabled()) { - this.termRef.current.disableWebGl(); - } else { - this.termRef.current.enableWebGl(); - } + const renderer = this.termRef.current.getTermRenderer() === "webgl" ? "canvas" : "webgl"; + this.termRef.current.setTermRenderer(renderer); } triggerRestartAtom() { @@ -598,7 +599,7 @@ export class TermViewModel implements ViewModel { console.log("search is open, not giving focus"); return true; } - let termMode = globalStore.get(this.termMode); + const termMode = globalStore.get(this.termMode); if (termMode == "term") { if (this.termRef?.current?.terminal) { this.termRef.current.terminal.focus(); diff --git a/frontend/app/view/term/termwrap.ts b/frontend/app/view/term/termwrap.ts index cb5769b4f9..1a6baa353a 100644 --- a/frontend/app/view/term/termwrap.ts +++ b/frontend/app/view/term/termwrap.ts @@ -18,6 +18,7 @@ import { import * as services from "@/store/services"; import { PLATFORM, PlatformMacOS } from "@/util/platformutil"; import { base64ToArray, fireAndForget } from "@/util/util"; +import { CanvasAddon } from "@xterm/addon-canvas"; import { SearchAddon } from "@xterm/addon-search"; import { SerializeAddon } from "@xterm/addon-serialize"; import { WebLinksAddon } from "@xterm/addon-web-links"; @@ -86,6 +87,7 @@ export class TermWrap { onSearchResultsDidChange?: (result: { resultIndex: number; resultCount: number }) => void; private toDispose: TermTypes.IDisposable[] = []; webglAddon: WebglAddon | null = null; + canvasAddon: CanvasAddon | null = null; webglEnabledAtom: jotai.PrimitiveAtom; pasteActive: boolean = false; lastUpdated: number; @@ -182,9 +184,7 @@ export class TermWrap { } ) ); - if (WebGLSupported && waveOptions.useWebGl) { - this.loadWebGlAddon(); - } + this.setTermRenderer(WebGLSupported && waveOptions.useWebGl ? "webgl" : "canvas"); // Register OSC handlers this.terminal.parser.registerOscHandler(7, (data: string) => { return handleOsc7Command(data, this.blockId, this.loaded); @@ -300,42 +300,57 @@ export class TermWrap { this.terminal.options.cursorBlink = cursorBlink ?? false; } - loadWebGlAddon() { - const addon = new WebglAddon(); - this.toDispose.push( - addon.onContextLoss(() => { - if (addon === this.webglAddon) { - this.disableWebGl(); - } - }) - ); - this.terminal.loadAddon(addon); - this.webglAddon = addon; - globalStore.set(this.webglEnabledAtom, true); - if (!loggedWebGL) { - console.log("loaded webgl!"); - loggedWebGL = true; + setTermRenderer(renderer: "webgl" | "canvas") { + if (renderer === "webgl") { + if (this.webglAddon != null) { + return; + } + if (!WebGLSupported) { + renderer = "canvas"; + } + } else { + if (this.canvasAddon != null) { + return; + } + } + if (this.webglAddon != null) { + this.webglAddon.dispose(); + this.webglAddon = null; + globalStore.set(this.webglEnabledAtom, false); + } + if (this.canvasAddon != null) { + this.canvasAddon.dispose(); + this.canvasAddon = null; + } + if (renderer === "webgl") { + const addon = new WebglAddon(); + this.toDispose.push( + addon.onContextLoss(() => { + if (addon === this.webglAddon) { + this.setTermRenderer("canvas"); + } + }) + ); + this.terminal.loadAddon(addon); + this.webglAddon = addon; + globalStore.set(this.webglEnabledAtom, true); + if (!loggedWebGL) { + console.log("loaded webgl!"); + loggedWebGL = true; + } + } else { + const addon = new CanvasAddon(); + this.terminal.loadAddon(addon); + this.canvasAddon = addon; } } - isWebGlEnabled(): boolean { - return this.webglAddon != null; - } - - enableWebGl() { - if (!WebGLSupported || this.webglAddon != null) { - return; - } - this.loadWebGlAddon(); + getTermRenderer(): "webgl" | "canvas" { + return this.webglAddon != null ? "webgl" : "canvas"; } - disableWebGl() { - if (this.webglAddon == null) { - return; - } - this.webglAddon.dispose(); - this.webglAddon = null; - globalStore.set(this.webglEnabledAtom, false); + isWebGlEnabled(): boolean { + return this.webglAddon != null; } resetCompositionState() { diff --git a/package-lock.json b/package-lock.json index fbf3fa1c69..468dbdae22 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "@table-nav/react": "^0.0.7", "@tanstack/react-table": "^8.21.3", "@tanstack/react-virtual": "^3.13.19", + "@xterm/addon-canvas": "^0.7.0", "@xterm/addon-fit": "^0.10.0", "@xterm/addon-search": "^0.15.0", "@xterm/addon-serialize": "^0.13.0", @@ -10664,6 +10665,15 @@ "node": ">=10.0.0" } }, + "node_modules/@xterm/addon-canvas": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-canvas/-/addon-canvas-0.7.0.tgz", + "integrity": "sha512-LF5LYcfvefJuJ7QotNRdRSPc9YASAVDeoT5uyXS/nZshZXjYplGXRECBGiznwvhNL2I8bq1Lf5MzRwstsYQ2Iw==", + "license": "MIT", + "peerDependencies": { + "@xterm/xterm": "^5.0.0" + } + }, "node_modules/@xterm/addon-fit": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz", diff --git a/package.json b/package.json index eeb75930ed..18d04ac3df 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,7 @@ "@table-nav/react": "^0.0.7", "@tanstack/react-table": "^8.21.3", "@tanstack/react-virtual": "^3.13.19", + "@xterm/addon-canvas": "^0.7.0", "@xterm/addon-fit": "^0.10.0", "@xterm/addon-search": "^0.15.0", "@xterm/addon-serialize": "^0.13.0", From dbd2e8dfe421e06e9c794b2f3b7e80687d1daeb7 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 18 Mar 2026 13:58:23 -0700 Subject: [PATCH 3/4] fix small todispose bug --- frontend/app/view/term/termwrap.ts | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/frontend/app/view/term/termwrap.ts b/frontend/app/view/term/termwrap.ts index 1a6baa353a..b9b9462c1f 100644 --- a/frontend/app/view/term/termwrap.ts +++ b/frontend/app/view/term/termwrap.ts @@ -85,9 +85,10 @@ export class TermWrap { multiInputCallback: (data: string) => void; sendDataHandler: (data: string) => void; onSearchResultsDidChange?: (result: { resultIndex: number; resultCount: number }) => void; - private toDispose: TermTypes.IDisposable[] = []; + toDispose: TermTypes.IDisposable[] = []; webglAddon: WebglAddon | null = null; canvasAddon: CanvasAddon | null = null; + webglContextLossDisposable: TermTypes.IDisposable | null = null; webglEnabledAtom: jotai.PrimitiveAtom; pasteActive: boolean = false; lastUpdated: number; @@ -307,6 +308,9 @@ export class TermWrap { } if (!WebGLSupported) { renderer = "canvas"; + if (this.canvasAddon != null) { + return; + } } } else { if (this.canvasAddon != null) { @@ -314,6 +318,8 @@ export class TermWrap { } } if (this.webglAddon != null) { + this.webglContextLossDisposable?.dispose(); + this.webglContextLossDisposable = null; this.webglAddon.dispose(); this.webglAddon = null; globalStore.set(this.webglEnabledAtom, false); @@ -324,13 +330,9 @@ export class TermWrap { } if (renderer === "webgl") { const addon = new WebglAddon(); - this.toDispose.push( - addon.onContextLoss(() => { - if (addon === this.webglAddon) { - this.setTermRenderer("canvas"); - } - }) - ); + this.webglContextLossDisposable = addon.onContextLoss(() => { + this.setTermRenderer("canvas"); + }); this.terminal.loadAddon(addon); this.webglAddon = addon; globalStore.set(this.webglEnabledAtom, true); @@ -468,6 +470,8 @@ export class TermWrap { } catch (_) {} }); this.promptMarkers = []; + this.webglContextLossDisposable?.dispose(); + this.webglContextLossDisposable = null; this.terminal.dispose(); this.toDispose.forEach((d) => { try { From f23ca8e8dd696d0fc5d25d285705b39323c1445d Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 18 Mar 2026 15:30:50 -0700 Subject: [PATCH 4/4] check for webgl2 --- frontend/app/view/term/termwrap.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app/view/term/termwrap.ts b/frontend/app/view/term/termwrap.ts index b9b9462c1f..05490ad466 100644 --- a/frontend/app/view/term/termwrap.ts +++ b/frontend/app/view/term/termwrap.ts @@ -50,7 +50,7 @@ const MaxRepaintTransactionMs = 2000; function detectWebGLSupport(): boolean { try { const canvas = document.createElement("canvas"); - const ctx = canvas.getContext("webgl"); + const ctx = canvas.getContext("webgl2"); return !!ctx; } catch (e) { return false;