Problem
When running multiple opencodes (xterm.js-based terminals) side-by-side in kolu, the terminal that loses focus exhibits visual render artifacts — text overlap, corrupted glyphs, or stale content that is never repainted.
Root Cause
Kolu uses a focus-gated renderer policy (Terminal.tsx:221): WebGL on the focused tile, DOM renderer on all others. When focus moves from tile A → tile B:
- Tile A:
unloadWebgl() disposes the WebGL addon and calls loseContext() on the GPU context (Terminal.tsx:269-296). xterm falls back to its DOM renderer — but no terminal.refresh() is queued, so the DOM renderer never repaints the now-stale framebuffer.
- Tile B:
loadWebgl() creates a fresh WebGL addon and renders normally.
The result: the formerly-focused tile shows corrupted rendering because the DOM renderer was never told to repaint after the WebGL teardown.
Evidence in the code
| Location |
Issue |
Terminal.tsx:269-296 |
unloadWebgl() calls w.dispose() but never calls terminal.refresh(0, terminal.rows - 1) afterwards. |
Terminal.tsx:349-359 |
The shouldUseWebgl effect is the only rendering-pipeline effect that does not call debouncedFit() or clearTextureAtlas() after a renderer switch. Every other effect (font size, theme, tab visibility) does. |
Terminal.tsx:213 |
clearTextureAtlas() is a silent no-op when webgl is null (DOM-rendered tiles), so the refitOnTabVisible and theme effects never trigger a DOM repaint either. |
SettingsPopover.tsx:51 |
The DOM renderer option is labeled "stable font on focus", tacitly acknowledging that the default auto mode has focus-related rendering instability. |
Why this is a kolu bug (not an xterm.js bug)
xterm.js does not promise to auto-repaint when a renderer addon is disposed mid-session — that is the host app's responsibility. Kolu actively switches renderers based on focus, but the transition code is incomplete.
Remediation
Proposed change 1: Force DOM repaint after WebGL unload
In Terminal.tsx, inside unloadWebgl(), after w.dispose():
terminal.refresh(0, terminal.rows - 1);
This forces the DOM renderer to repaint the entire visible area after the WebGL addon is disposed — a known xterm.js pattern for clean renderer transitions.
Proposed change 2: Add fit + atlas clear to the shouldUseWebgl effect
In the shouldUseWebgl effect (Terminal.tsx:349-359), call debouncedFit() and clearTextureAtlas() after the load/unload, mirroring what every other rendering-pipeline effect does:
createEffect(
on(
shouldUseWebgl,
(should) => {
if (!terminal) return;
if (should) loadWebgl();
else unloadWebgl();
debouncedFit();
clearTextureAtlas();
},
{ defer: true },
),
);
Proposed change 3 (optional): Defer the cross-tile WebGL handoff
Unloading tile A's WebGL and loading tile B's WebGL in the same reactive cycle stresses Chrome's ~16 WebGL context budget (see the #575 references in Terminal.tsx:219). Consider deferring the load to the next frame:
createEffect(
on(
shouldUseWebgl,
(should) => {
if (!terminal) return;
if (should) requestAnimationFrame(() => loadWebgl());
else unloadWebgl();
},
{ defer: true },
),
);
Immediate workaround for users
Switch the renderer preference from Auto to DOM in settings. This eliminates the WebGL↔DOM transition entirely, at the cost of lower GPU throughput.
Related
- Issue references in code:
#239 (font rendering corruption), #575 (context exhaustion), #595 (WebGL lifecycle)
- See also
docs/perf-investigations/memory-learnings.md Chapter 3 (IntersectionObserver interference)
Problem
When running multiple opencodes (xterm.js-based terminals) side-by-side in kolu, the terminal that loses focus exhibits visual render artifacts — text overlap, corrupted glyphs, or stale content that is never repainted.
Root Cause
Kolu uses a focus-gated renderer policy (
Terminal.tsx:221): WebGL on the focused tile, DOM renderer on all others. When focus moves from tile A → tile B:unloadWebgl()disposes the WebGL addon and callsloseContext()on the GPU context (Terminal.tsx:269-296). xterm falls back to its DOM renderer — but noterminal.refresh()is queued, so the DOM renderer never repaints the now-stale framebuffer.loadWebgl()creates a fresh WebGL addon and renders normally.The result: the formerly-focused tile shows corrupted rendering because the DOM renderer was never told to repaint after the WebGL teardown.
Evidence in the code
Terminal.tsx:269-296unloadWebgl()callsw.dispose()but never callsterminal.refresh(0, terminal.rows - 1)afterwards.Terminal.tsx:349-359shouldUseWebgleffect is the only rendering-pipeline effect that does not calldebouncedFit()orclearTextureAtlas()after a renderer switch. Every other effect (font size, theme, tab visibility) does.Terminal.tsx:213clearTextureAtlas()is a silent no-op whenwebglis null (DOM-rendered tiles), so therefitOnTabVisibleand theme effects never trigger a DOM repaint either.SettingsPopover.tsx:51Why this is a kolu bug (not an xterm.js bug)
xterm.js does not promise to auto-repaint when a renderer addon is disposed mid-session — that is the host app's responsibility. Kolu actively switches renderers based on focus, but the transition code is incomplete.
Remediation
Proposed change 1: Force DOM repaint after WebGL unload
In
Terminal.tsx, insideunloadWebgl(), afterw.dispose():This forces the DOM renderer to repaint the entire visible area after the WebGL addon is disposed — a known xterm.js pattern for clean renderer transitions.
Proposed change 2: Add fit + atlas clear to the shouldUseWebgl effect
In the
shouldUseWebgleffect (Terminal.tsx:349-359), calldebouncedFit()andclearTextureAtlas()after the load/unload, mirroring what every other rendering-pipeline effect does:Proposed change 3 (optional): Defer the cross-tile WebGL handoff
Unloading tile A's WebGL and loading tile B's WebGL in the same reactive cycle stresses Chrome's ~16 WebGL context budget (see the
#575references inTerminal.tsx:219). Consider deferring the load to the next frame:Immediate workaround for users
Switch the renderer preference from Auto to DOM in settings. This eliminates the WebGL↔DOM transition entirely, at the cost of lower GPU throughput.
Related
#239(font rendering corruption),#575(context exhaustion),#595(WebGL lifecycle)docs/perf-investigations/memory-learnings.mdChapter 3 (IntersectionObserver interference)