mirror of
https://github.com/stablyai/orca.git
synced 2026-11-13 17:37:40 +00:00
Cap merged atlas pages at FORCED_MAX_TEXTURE_SIZE instead of the device MAX_TEXTURE_SIZE (16384 on Apple Silicon), stop the link render layer from acquiring the shared atlas with a mismatched 2048 texture size, and reset GL texture slots of removed atlas pages to the 1x1 placeholder. Mirrors xtermjs/xterm.js#6187, #6156 and #6306 in the addon-webgl source patch and regenerates the runtime patch and lock hash. Co-authored-by: FlamingoPg <1106310035@qq.com>
372 lines
18 KiB
Diff
372 lines
18 KiB
Diff
diff --git a/src/CellColorResolver.ts b/src/CellColorResolver.ts
|
|
index 49fdf5b1bb4d6c1f602e36470248a10a690a3479..384f4db6f22d0691f66938d42333d1b0424ee513 100644
|
|
--- a/src/CellColorResolver.ts
|
|
+++ b/src/CellColorResolver.ts
|
|
@@ -18,6 +18,15 @@ let $isSelected = false;
|
|
let $colors: ReadonlyColorSet | undefined;
|
|
let $variantOffset = 0;
|
|
|
|
+function isTranslucentDefaultBackground(bg: number, colors: ReadonlyColorSet): boolean {
|
|
+ const mode = bg & Attributes.CM_MASK;
|
|
+ if (mode === Attributes.CM_DEFAULT || (colors.background.rgba & 0xFF) === 0xFF) {
|
|
+ return false;
|
|
+ }
|
|
+ const rgb = mode === Attributes.CM_RGB ? bg & Attributes.RGB_MASK : colors.ansi[bg & Attributes.PCOLOR_MASK].rgba >>> 8;
|
|
+ return rgb === colors.background.rgba >>> 8;
|
|
+}
|
|
+
|
|
export class CellColorResolver {
|
|
/**
|
|
* The shared result of the {@link resolve} call. This is only safe to use immediately after as
|
|
@@ -59,6 +68,12 @@ export class CellColorResolver {
|
|
$colors = this._themeService.colors;
|
|
$variantOffset = 0;
|
|
|
|
+ // Orca: a translucent terminal paints its backdrop once; an explicit background equal to the
|
|
+ // default background is that backdrop, not an opaque fill. The buffer keeps the original color.
|
|
+ if (!(cell.fg & FgFlags.INVERSE) && this._optionService.rawOptions.allowTransparency && isTranslucentDefaultBackground(cell.bg, $colors)) {
|
|
+ this.result.bg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);
|
|
+ }
|
|
+
|
|
const code = cell.getCode();
|
|
if (code !== NULL_CELL_CODE && cell.extended.underlineStyle === UnderlineStyle.DOTTED) {
|
|
const lineWidth = Math.max(1, Math.floor(this._optionService.rawOptions.fontSize * this._coreBrowserService.dpr / 15));
|
|
diff --git a/src/GlyphRenderer.ts b/src/GlyphRenderer.ts
|
|
index 742f0879ff4f04509e4a07c8efdf0d5743fe8ee5..21347443255cba3011e534248557239931ab7d47 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); }
|
|
}`);
|
|
}
|
|
|
|
@@ -102,6 +104,7 @@ export class GlyphRenderer extends Disposable {
|
|
private _atlas: ITextureAtlas | undefined;
|
|
private _lastSeenPageLayoutVersion: number = -1;
|
|
private _pageOverflowWarned: boolean = false;
|
|
+ private _uploadedTextureCount: number = 0;
|
|
private _activeBuffer: number = 0;
|
|
private readonly _vertices: IVertices = {
|
|
count: 0,
|
|
@@ -387,6 +390,15 @@ export class GlyphRenderer extends Disposable {
|
|
this._bindAtlasPageTexture(gl, this._atlas, i);
|
|
}
|
|
}
|
|
+ // Merging or evicting pages shrinks the page list, release the textures of pages that no
|
|
+ // longer exist as they would otherwise keep their full size in GPU memory until reused.
|
|
+ for (let i = pageCount; i < this._uploadedTextureCount; i++) {
|
|
+ gl.activeTexture(gl.TEXTURE0 + i);
|
|
+ gl.bindTexture(gl.TEXTURE_2D, this._atlasTextures[i].texture);
|
|
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([255, 0, 0, 255]));
|
|
+ this._atlasTextures[i].version = -1;
|
|
+ }
|
|
+ this._uploadedTextureCount = pageCount;
|
|
|
|
// Draw the viewport
|
|
gl.drawElementsInstanced(gl.TRIANGLE_STRIP, 4, gl.UNSIGNED_BYTE, 0, bufferLength / Constants.INDICES_PER_CELL);
|
|
diff --git a/src/RectangleRenderer.ts b/src/RectangleRenderer.ts
|
|
index eebfda3b3df9b4c8cefcc6a64692053ed5eaeefa..75e34767bd1e12802b72371fddb5fc9028814e11 100644
|
|
--- a/src/RectangleRenderer.ts
|
|
+++ b/src/RectangleRenderer.ts
|
|
@@ -226,7 +226,9 @@ export class RectangleRenderer extends Disposable {
|
|
inverse = !!(fg & FgFlags.INVERSE);
|
|
if (bg !== currentBg || (fg !== currentFg && (currentInverse || inverse))) {
|
|
// A rectangle needs to be drawn if going from non-default to another color
|
|
- if (currentBg !== 0 || (currentInverse && currentFg !== 0)) {
|
|
+ // Orca: style-only bg flags (dim, italic, extended) keep the default color, which the
|
|
+ // viewport clear already painted; filling it again ignored a translucent background.
|
|
+ if ((currentBg & Attributes.CM_MASK) !== Attributes.CM_DEFAULT || (currentInverse && currentFg !== 0)) {
|
|
offset = rectangleCount++ * INDICES_PER_RECTANGLE;
|
|
this._updateRectangle(vertices, offset, currentFg, currentBg, currentStartX, x, y);
|
|
}
|
|
@@ -237,7 +239,7 @@ export class RectangleRenderer extends Disposable {
|
|
}
|
|
}
|
|
// Finish rectangle if it's still going
|
|
- if (currentBg !== 0 || (currentInverse && currentFg !== 0)) {
|
|
+ if ((currentBg & Attributes.CM_MASK) !== Attributes.CM_DEFAULT || (currentInverse && currentFg !== 0)) {
|
|
offset = rectangleCount++ * INDICES_PER_RECTANGLE;
|
|
this._updateRectangle(vertices, offset, currentFg, currentBg, currentStartX, terminal.cols, y);
|
|
}
|
|
diff --git a/src/TextureAtlas.ts b/src/TextureAtlas.ts
|
|
index eff0d0e1c36906b1f14b85fb8db7907bf685c8a7..1fb0b7a971e33e1dbd2dcf9841645ca464617af2 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 {
|
|
@@ -164,7 +172,7 @@ export class TextureAtlas implements ITextureAtlas {
|
|
// Find the set of the largest 4 images, below the maximum size, with the highest
|
|
// percentages used
|
|
const pagesBySize = this._pages.filter(e => {
|
|
- return e.canvas.width * 2 <= (TextureAtlas.maxTextureSize || Constants.FORCED_MAX_TEXTURE_SIZE);
|
|
+ return e.canvas.width * 2 <= Math.min(TextureAtlas.maxTextureSize || Constants.FORCED_MAX_TEXTURE_SIZE, Constants.FORCED_MAX_TEXTURE_SIZE);
|
|
}).sort((a, b) => {
|
|
if (b.canvas.width !== a.canvas.width) {
|
|
return b.canvas.width - a.canvas.width;
|
|
@@ -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..00f299659106716258ad7b02c2bfdddcc53e91fe 100644
|
|
--- a/src/renderLayer/BaseRenderLayer.ts
|
|
+++ b/src/renderLayer/BaseRenderLayer.ts
|
|
@@ -3,8 +3,6 @@
|
|
* @license MIT
|
|
*/
|
|
|
|
-import { ReadonlyColorSet } from 'browser/Types';
|
|
-import { acquireTextureAtlas } from '../CharAtlasCache';
|
|
import { IRenderDimensions } from 'browser/renderer/shared/Types';
|
|
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
|
|
import { Disposable, toDisposable } from 'common/Lifecycle';
|
|
@@ -14,7 +12,6 @@ import { Terminal } from '@xterm/xterm';
|
|
import { IRenderLayer } from './Types';
|
|
import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
|
|
import { TEXT_BASELINE } from '../Constants';
|
|
-import type { ITextureAtlas } from '../Types';
|
|
|
|
export abstract class BaseRenderLayer extends Disposable implements IRenderLayer {
|
|
private _canvas: HTMLCanvasElement;
|
|
@@ -26,8 +23,6 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer
|
|
private _deviceCharLeft: number = 0;
|
|
private _deviceCharTop: number = 0;
|
|
|
|
- protected _charAtlas: ITextureAtlas | undefined;
|
|
-
|
|
constructor(
|
|
terminal: Terminal,
|
|
private _container: HTMLElement,
|
|
@@ -44,8 +39,7 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer
|
|
this._canvas.style.zIndex = zIndex.toString();
|
|
this._initCanvas();
|
|
this._container.appendChild(this._canvas);
|
|
- this._register(this._themeService.onChangeColors(e => {
|
|
- this._refreshCharAtlas(terminal, e);
|
|
+ this._register(this._themeService.onChangeColors(() => {
|
|
this.reset(terminal);
|
|
}));
|
|
this._register(toDisposable(() => {
|
|
@@ -81,25 +75,10 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer
|
|
this._initCanvas();
|
|
this._container.replaceChild(this._canvas, oldCanvas);
|
|
|
|
- // Regenerate char atlas and force a full redraw
|
|
- this._refreshCharAtlas(terminal, this._themeService.colors);
|
|
+ // Force a full redraw
|
|
this.handleGridChanged(terminal, 0, terminal.rows - 1);
|
|
}
|
|
|
|
- /**
|
|
- * Refreshes the char atlas, aquiring a new one if necessary.
|
|
- * @param terminal The terminal.
|
|
- * @param colorSet The color set to use for the char atlas.
|
|
- */
|
|
- private _refreshCharAtlas(terminal: Terminal, colorSet: ReadonlyColorSet): void {
|
|
- if (this._deviceCharWidth <= 0 && this._deviceCharHeight <= 0) {
|
|
- return;
|
|
- }
|
|
-
|
|
- this._charAtlas = acquireTextureAtlas(terminal, this._optionsService.rawOptions, colorSet, this._deviceCellWidth, this._deviceCellHeight, this._deviceCharWidth, this._deviceCharHeight, this._coreBrowserService.dpr, 2048);
|
|
- this._charAtlas.warmUp();
|
|
- }
|
|
-
|
|
public resize(terminal: Terminal, dim: IRenderDimensions): void {
|
|
this._deviceCellWidth = dim.device.cell.width;
|
|
this._deviceCellHeight = dim.device.cell.height;
|
|
@@ -116,8 +95,6 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer
|
|
if (!this._alpha) {
|
|
this._clearAll();
|
|
}
|
|
-
|
|
- this._refreshCharAtlas(terminal, this._themeService.colors);
|
|
}
|
|
|
|
public abstract reset(terminal: Terminal): void;
|