mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
* Upgrade xterm to 6.1.0-beta.303 and generate the addon patches
Takes the current xterm beta line: xterm 287 -> 303, addon-webgl 286 -> 299,
addon-serialize 287 -> 300, headless 302, the remaining addons -> 300, and the
same set on mobile. All four packages stamp upstream commit d3e32b3.
The reasons are upstream #6042/#6043/#6055 (a shared glyph atlas no longer
garbles sibling panes on a page merge, clear, or sampler-budget overflow) and
Note that core 303 is not image-addon-only over 302: it carries the buffer perf
work, including the new BufferLineStringCache.
addon-webgl and addon-serialize move into the patch generator
--------------------------------------------------------------
Both were hand-edited minified bundles, which is what the Known Gaps section of
docs/reference/xterm-patch-regeneration.md described. Both reproduce byte for
byte from the pinned commit, so they are now manifest entries generated from a
source patch like @xterm/xterm already was. Their sourcemaps now move with their
bundles; before this they shipped maps whose offsets did not match the code
beside them.
The webgl patch shrinks from a 1.06 MB hand-edited bundle to a 6.6 KB source
patch, because upstream took the invalidation half Orca had backported. What is
left is only what upstream still lacks: the fragment-shader else branch for a
v_texpage past the sampler budget, the clearTexture guard that no-ops once a
merged page holds index 0, spending the merge retry budget before beginFrame
latches the version it saw, and Orca's font-weight probe.
The serialize source patch is byte-for-byte the same fixes as before; upstream
changed nothing in that addon between 287 and 300.
Generator fixes, each of which failed silently
----------------------------------------------
- `--relative` was appended after the `--` separator in CHECKOUT_DIFF_FLAGS, so
git read it as a pathspec and kept repo-root-relative paths, dropping every
source hunk from an addon's patch.
- `git apply` run from a package subdirectory still resolves patch paths from
the repo root, skips every hunk and exits 0. It now runs from the root with
`--directory=<packageDir>`, and a source patch that leaves the checkout
unchanged is a hard failure rather than an empty patch.
- An addon's own `tsgo -p .` has empty files/include and only project
references, so it emits nothing and the addon webpack then fails on a missing
./out/. The root build now runs first.
- versionStampFile is optional; publish.js stamps an addon's package.json, which
overlayBuildOutput never patches.
- On a version bump the lockfile has no entry under the new key yet, so --write
reports the gap instead of aborting mid-run. --check still fails on it.
Adding the two addons pushed the generator and the Electron packaging contract
test over max-lines, so the patch-text helpers move to xterm-patch-text.mjs
(pure text: no checkout, no build) and the vendored-xterm assertions move out of
the packaging contract into xterm-webgl-runtime-contract.test.mjs.
Tests
-----
Four tests asserted upstream bugs that are now fixed, not Orca behaviour:
- xterm-user-scrolling-contract pinned headless and core by version string.
Upstream bumps each package only when its own output changes, so headless 302
and core 303 are the same source. It now asserts they share a commit.
- Five CSI 3 J assertions expected a reader stranded at the top after an erase.
Upstream #6081 clears isUserScrolling there, so the erase releases them to the
bottom instead. Orca's pin still lands them correctly, because its parser
handler observes the erase before xterm's own handler runs.
- The IME transaction test hard-coded the xterm version; it now reads the
installed package, since the point is that bundle, map and version agree.
- The Electron runtime contract asserted Orca's old clearModelGeneration. Shared
atlas invalidation is upstream's now, so it asserts pageLayoutVersion on the
resolved dependency, plus the Orca-only hunks on the patch.
Verified: 66,008 unit tests, mobile's 3,863, the four WebGL atlas e2e specs, and
`regenerate-xterm-patches.mjs --check` in sync on all three packages.
Left alone deliberately: resetAllTerminalWebglAtlases still fans out globally
even though clearTexture now self-heals siblings, and upstream #6068
(WebglAddon.dispose leaks the GL context) is still open.
* Drop the two unused WebGL atlas fan-out exports
resetAllTerminalWebglAtlases and presentAllTerminalPanesWithoutAtlasClear have
no callers, and had none at cadfc55102 either — the last call site went in
#6949, which routed reveal recovery through
resetAndRefreshAllTerminalWebglAtlases instead. Only a comment in
pane-manager.ts still named the first one; it now points at the live entry
point. scheduleRevealPresent leaves the registry's structural type with them,
though the manager method stays: terminal-visibility-resume.ts calls it
directly.
This is dead-code removal, not a consequence of the xterm bump. The live
recovery path is unchanged.
resetAndRefreshAllTerminalWebglAtlases stays, and so does the reveal-time
escalation in pane-reveal-repaint.ts. Upstream 299 does make a pane-local
clearTexture bump pageLayoutVersion so siblings rebuild on their next frame,
which is the bug the escalation was written for, but I could not demonstrate
that removing it is safe: with the escalation removed,
floating-workspace-shared-glyph-atlas.spec.ts still passed headful, and it also
passed with upstream's mechanism deliberately disabled (pageLayoutVersion
pinned to 0 in the installed bundle, verified present in the built renderer).
A guard that passes with the fix disabled cannot license removing the
workaround, so the escalation stays until that spec can reproduce the garbling.
Verified: pane-manager and terminal-pane suites (4,713 tests), typecheck, the
headful shared-atlas spec, and the three headless WebGL specs.
* Give the shared glyph atlas spec a trigger that can fail
floating-workspace-shared-glyph-atlas.spec.ts guards the corruption where one
terminal wiping the module-global atlas leaves sibling terminals drawing from
stale texture coordinates. Both of its tests drive that through a floating
panel reveal, and Orca's reveal paths escalate to a registry-wide atlas reset
that repaints every pane — so the recovery under test heals the damage before
the assertion runs, and the tests pass whether or not xterm propagates the
invalidation at all.
The new test clears the shared atlas straight through the floating manager with
the panel closed, so nothing else repaints the workspace terminal, then repaints
it with terminal.refresh(). That is the load-bearing detail: _updateModel skips
cells whose content is unchanged, so the refresh reuses vertices baked against
the pages that were just wiped, which is exactly the state the fix has to
recover from.
Verified as a discriminator rather than assumed. Pinning ITextureAtlas's
pageLayoutVersion getter to 0 in the installed bundle, which disables the
per-renderer invalidation upstream added in addon-webgl 0.20.0-beta.299, and
confirming that reached the built renderer:
fix intact: siblingClearIntact=true 1 passed
fix disabled: siblingClearIntact=false 1 failed
The failure renders the workspace terminal completely blank — stale coordinates
into a wiped atlas sample nothing. The two reveal tests pass unchanged in both
configurations, which is the gap this closes.
* Compare shared-atlas screenshots with tolerance instead of byte equality
Byte equality fails on sub-pixel antialiasing noise that leaves every glyph
legible, so the headful spec flaked under xterm 303. Reuse the existing
compareTerminalScreenshots helper: real stale-model corruption blanks the
terminal at ~3% of pixels, twice the helper's 1.5% threshold, so the looser
oracle keeps its teeth. Log the ratio so failures are diagnosable.
* fix(xterm): cancel empty deferred IME compositions
* test(xterm): strengthen runtime patch contracts
132 lines
6.4 KiB
Diff
132 lines
6.4 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 4977ad741065e26bbfd35bc50e7558a033b13340..f55805ddc3f266415f7f8e81e5b8c318e7db194c 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';
|
|
@@ -135,21 +136,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 {
|
|
@@ -465,6 +468,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;
|
|
|
|
@@ -536,6 +569,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/WebglRenderer.ts b/src/WebglRenderer.ts
|
|
index a951efba5c75e82735cd39b6b22c4b5d5fad5928..e7f80c3a8c14dd65a16a97f1d17e3da1d65ed8bf 100644
|
|
--- a/src/WebglRenderer.ts
|
|
+++ b/src/WebglRenderer.ts
|
|
@@ -386,7 +386,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);
|