Files
orca/config/patches/xterm-src/@xterm__addon-webgl@0.20.0-beta.329.src.patch
T
Neil b69963356d chore(terminal): bump xterm to 6.1.0-beta.336 and regenerate patches (#37266)
Port the xterm, webgl, search, serialize and image source patches to upstream
08829c7. Where upstream fixed the same bug (#6204 mouse NaN guard, #6226 warm-up
cancel on dispose, #6235/#6237 serialize width clamp) take upstream and keep only
Orca's remaining delta (domContainer warm-up gate, clipped wide-cell blanking).
Bump mobile's webgl subset patch alongside. Drop serialize fuzz seed normal:5,
which now round-trips.
2026-11-01 18:05:58 -08:00

219 lines
10 KiB
Diff

diff --git a/src/GlyphRenderer.ts b/src/GlyphRenderer.ts
index 742f0879ff4f04509e4a07c8efdf0d5743fe8ee5..885a206a394fff783737a412c0b75bf935dd0eca 100644
--- a/src/GlyphRenderer.ts
+++ b/src/GlyphRenderer.ts
@@ -61,6 +61,8 @@ function createFragmentShaderSource(maxFragmentShaderTextureUnits: number): stri
for (let i = 1; i < maxFragmentShaderTextureUnits; i++) {
textureConditionals += ` else if (v_texpage == ${i}) { outColor = texture(u_texture[${i}], v_texcoord); }`;
}
+ // A v_texpage beyond the sampler budget matches no branch above. Leaving outColor unwritten
+ // is undefined behaviour in GLSL ES and paints garbage, so fall through to transparent.
return (`#version 300 es
precision lowp float;
@@ -74,7 +76,7 @@ out vec4 outColor;
void main() {
if (v_texpage == 0) {
outColor = texture(u_texture[0], v_texcoord);
- } ${textureConditionals}
+ } ${textureConditionals} else { outColor = vec4(0.0, 0.0, 0.0, 0.0); }
}`);
}
diff --git a/src/TextureAtlas.ts b/src/TextureAtlas.ts
index eff0d0e1c36906b1f14b85fb8db7907bf685c8a7..b4fcff8c6be49b1868a17bcabfa3bb54689fb8b1 100644
--- a/src/TextureAtlas.ts
+++ b/src/TextureAtlas.ts
@@ -3,6 +3,7 @@
* @license MIT
*/
+import { FontWeight } from '@xterm/xterm';
import { IColorContrastCache } from 'browser/Types';
import { DIM_OPACITY, TEXT_BASELINE } from './Constants';
import { tryDrawCustomGlyph } from './customGlyphs/CustomGlyphRasterizer';
@@ -114,21 +115,26 @@ export class TextureAtlas implements ITextureAtlas {
this._onRemoveTextureAtlasCanvas.dispose();
}
- public warmUp(): void {
+ public warmUp(domContainer?: HTMLElement): void {
if (!this._didWarmUp) {
- this._doWarmUp();
+ this._doWarmUp(domContainer);
this._didWarmUp = true;
}
}
- private _doWarmUp(): void {
+ private _doWarmUp(domContainer: HTMLElement | undefined): void {
// Pre-fill with ASCII 33-126, this is not urgent and done in idle callbacks
this._warmUpQueue?.clear();
const queue = this._warmUpQueue = new IdleTaskQueue(this._logService);
for (let i = 33; i < 126; i++) {
queue.enqueue(() => {
+ // Orca: a canvas outside the document ignores the page's font smoothing and rasterizes
+ // heavier glyphs that stay cached, so leave them to on-demand drawing until connected.
+ if (!domContainer?.isConnected) {
+ return;
+ }
if (!this._cacheMap.get(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT)) {
- const rasterizedGlyph = this._drawToCache(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT, false, undefined);
+ const rasterizedGlyph = this._drawToCache(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT, false, domContainer);
this._cacheMap.set(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT, rasterizedGlyph);
}
});
@@ -138,21 +144,23 @@ export class TextureAtlas implements ITextureAtlas {
private _pageLayoutVersion = 0;
public get pageLayoutVersion(): number { return this._pageLayoutVersion; }
+ // Orca diagnostics: read by terminal-render-desync-weight-probe.ts to tell a real
+ // bold-collapse from a repaint problem. Canvas silently keeps its previous font when an
+ // assignment fails to parse, which rasterizes glyphs at a stale weight.
+ public fontProbeMismatchCount = 0;
+ public fontProbeLastMismatch: { desired: string, actual: string } | undefined;
+
public clearTexture(): void {
- if (this._pages[0].currentRow.x === 0 && this._pages[0].currentRow.y === 0) {
+ // Guard on every page rather than pages[0]: a merged page is never written through
+ // currentRow, so once one lands at index 0 the old check made every later clear a no-op.
+ if (this._pages.every(page => page.glyphs.length === 0 && page.currentRow.x === 0 && page.currentRow.y === 0)) {
return;
}
- for (const page of this._pages) {
- page.clear();
- }
- this._cacheMap.clear();
- this._cacheMapCombined.clear();
- this._didWarmUp = false;
-
- // Invalidate renderer models so all texture pages are refreshed. The atlas may be shared, in
- // which case the clearing renderer has cleared only its own model and every other owner still
- // holds texture coords into the rows just wiped.
- this._pageLayoutVersion++;
+ // Return the atlas to its constructor state instead of clearing in place: page.clear() leaves
+ // page.glyphs populated, which would keep the guard above from ever firing again. Eviction
+ // also bumps _pageLayoutVersion, so every renderer sharing this atlas rebuilds its model.
+ this._evictAllPages();
+ this._createNewPage();
}
private _createNewPage(): AtlasPage {
@@ -468,6 +476,36 @@ export class TextureAtlas implements ITextureAtlas {
return this._config.colors.contrastCache;
}
+ /**
+ * Orca diagnostic. Canvas ignores a font assignment it cannot parse and silently keeps the
+ * previous value, so a bad family or weight rasterizes every glyph at a stale weight. Record
+ * the mismatch rather than correcting it: the goal is to tell that failure apart from a
+ * repaint bug when a terminal renders bold-collapsed.
+ */
+ private _probeRasterizationFontWeight(fontWeight: FontWeight): void {
+ const desired = String(fontWeight);
+ // Only numeric weights are comparable; keywords round-trip through Canvas unchanged.
+ if (!/^(?:[1-8]\d{2}|900)$/.test(desired)) {
+ return;
+ }
+ // Canvas normalizes font serialization: Chromium omits 400 and emits the keyword bold for 700.
+ const token = this._tmpCtx.font.match(
+ /(?:^|\s)(normal|bold|[1-9]\d{0,3})(?=\s+\d+(?:\.\d+)?px(?:\s|$))/
+ )?.[1] ?? '400';
+ const actual = token === 'normal' ? '400' : token === 'bold' ? '700' : token;
+ if (actual === desired) {
+ return;
+ }
+ this.fontProbeMismatchCount++;
+ this.fontProbeLastMismatch = { desired, actual: this._tmpCtx.font };
+ try {
+ (globalThis as { __orcaAtlasFontProbe?: (mismatch: { desired: string, actual: string }) => void })
+ .__orcaAtlasFontProbe?.(this.fontProbeLastMismatch);
+ } catch {
+ // Diagnostics only; a throwing listener must never break rasterization.
+ }
+ }
+
private _drawToCache(codeOrChars: number | string, bg: number, fg: number, ext: number, restrictToCellHeight: boolean, domContainer: HTMLElement | undefined): IRasterizedGlyph {
const chars = typeof codeOrChars === 'number' ? String.fromCharCode(codeOrChars) : codeOrChars;
@@ -539,6 +577,7 @@ export class TextureAtlas implements ITextureAtlas {
const fontStyle = italic ? 'italic' : '';
this._tmpCtx.font =
`${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`;
+ this._probeRasterizationFontWeight(fontWeight);
this._tmpCtx.textBaseline = TEXT_BASELINE;
const powerlineGlyph = chars.length === 1 && isPowerlineGlyph(chars.charCodeAt(0));
diff --git a/src/Types.ts b/src/Types.ts
index bfd6fd76cebe8f55dce7bfad5406d4f7cd184320..0bd283ea7b2bb72d5d3f20b6e7e11e7cc44f283f 100644
--- a/src/Types.ts
+++ b/src/Types.ts
@@ -65,7 +65,7 @@ export interface ITextureAtlas extends IDisposable {
/**
* Warm up the texture atlas, adding common glyphs to avoid slowing early frame.
*/
- warmUp(): void;
+ warmUp(domContainer?: HTMLElement): void;
/**
* Incremented whenever cached glyph texture page mappings may be stale, such as after atlas page
diff --git a/src/WebglRenderer.ts b/src/WebglRenderer.ts
index 2d0f5a3a8ee7fc0765a0179cee0102edb9376e4b..92f1c7241e887cd907d21d91aa26c0def02339f5 100644
--- a/src/WebglRenderer.ts
+++ b/src/WebglRenderer.ts
@@ -323,7 +323,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
);
}
this._charAtlas = atlas;
- this._charAtlas.warmUp();
+ this._charAtlas.warmUp(this._terminal.element);
this._glyphRenderer.value?.setAtlas(this._charAtlas);
}
@@ -393,7 +393,10 @@ export class WebglRenderer extends Disposable implements IRenderer {
// page's version, so re-run the update and force a full texture rebind.
let merged = false;
let mergeRetries = 0;
- while (this._charAtlas && this._glyphRenderer.value.beginFrame() && mergeRetries++ < Constants.MERGE_RETRY_LIMIT) {
+ // Test the retry budget before beginFrame: beginFrame latches the page layout version it
+ // observed, so tripping the limit after consuming it would strand a stale model with no
+ // later frame able to notice it needs rebuilding.
+ while (this._charAtlas && mergeRetries++ < Constants.MERGE_RETRY_LIMIT && this._glyphRenderer.value.beginFrame()) {
merged = true;
this._clearModel(true);
this._updateModel(0, this._terminal.rows - 1);
@@ -403,6 +406,20 @@ export class WebglRenderer extends Disposable implements IRenderer {
}
// Render
+ // A transparent framebuffer keeps the previous draw, so re-blending translucent
+ // backgrounds accumulated opacity on every redraw and flickered (Orca #6491).
+ if (this._terminal.options.allowTransparency) {
+ // Keep the RGB blend but write source alpha as-is; SRC_ALPHA on alpha
+ // itself would turn the configured opacity `a` into `a²`.
+ this._gl.blendFuncSeparate(
+ this._gl.SRC_ALPHA,
+ this._gl.ONE_MINUS_SRC_ALPHA,
+ this._gl.ONE,
+ this._gl.ONE_MINUS_SRC_ALPHA
+ );
+ this._gl.clearColor(0, 0, 0, 0);
+ this._gl.clear(this._gl.COLOR_BUFFER_BIT);
+ }
this._rectangleRenderer.value.renderBackgrounds();
this._glyphRenderer.value.render(this._model);
if (!this._cursorBlinkStateManager.value || this._cursorBlinkStateManager.value.isCursorVisible) {
diff --git a/src/renderLayer/BaseRenderLayer.ts b/src/renderLayer/BaseRenderLayer.ts
index 5b11615328957b6f5d41a174284c11e061910d91..1654e5ae39baf65a7243cbe7b1395c28824b269e 100644
--- a/src/renderLayer/BaseRenderLayer.ts
+++ b/src/renderLayer/BaseRenderLayer.ts
@@ -97,7 +97,7 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer
}
this._charAtlas = acquireTextureAtlas(terminal, this._optionsService.rawOptions, colorSet, this._deviceCellWidth, this._deviceCellHeight, this._deviceCharWidth, this._deviceCharHeight, this._coreBrowserService.dpr, 2048);
- this._charAtlas.warmUp();
+ this._charAtlas.warmUp(terminal.element);
}
public resize(terminal: Terminal, dim: IRenderDimensions): void {