generateColoredAsciiFlood emitted a trailing \r\n after its final cell, so a flood whose last line lands on the bottom row scrolled the screen. In the alt-buffer repro this destroyed the pinned header row in the BUFFER, making the 255/255 signature diff a content change, not renderer corruption. The single-terminal merge path is actually healthy: with the scroll removed the same 30-chunk merge-heavy workload renders the header with zero drift. - generateColoredAsciiFlood: no linefeed after the final cell - WebglReproExperiment: now a green guard; asserts row-1 buffer text first so a content bug can never masquerade as corruption again - WebglVisual: accurate comments; erase rewritten lines so leftover prefill text doesn't look like corruption - WebglSingleTermStress: replace stale EXPECTED-RED comment (test passes)
101 lines
4.2 KiB
TypeScript
101 lines
4.2 KiB
TypeScript
/**
|
|
* Copyright (c) 2025 The xterm.js authors. All rights reserved.
|
|
* @license MIT
|
|
*
|
|
* Single-terminal WebGL atlas stress: try to reproduce the "garbled glyph"
|
|
* corruption from microsoft/vscode#322756 in ONE terminal (no shared atlas),
|
|
* via heavy unique-glyph churn that forces atlas page merges, both with content
|
|
* pinned on screen (TUI-style) and mid-scroll. Chromium/SwiftShader only.
|
|
*/
|
|
|
|
import test, { expect } from '@playwright/test';
|
|
import { ITestContext, createTestContext, openTerminal } from '../../../test/playwright/TestUtils';
|
|
import {
|
|
captureRowSignatures,
|
|
expectSignatureMatches,
|
|
floodDistinctGlyphs,
|
|
getAtlasPageCount,
|
|
loadWebglStrict,
|
|
setMaxAtlasPages,
|
|
waitForRender,
|
|
writeAndWaitForRender
|
|
} from '../../../test/playwright/RendererTestUtils';
|
|
import { generateUniqueGlyphFlood } from '../../../test/playwright/SyntheticTui';
|
|
|
|
let ctx: ITestContext;
|
|
test.beforeAll(async ({ browser }) => {
|
|
ctx = await createTestContext(browser);
|
|
});
|
|
test.afterAll(async () => await ctx.page.close());
|
|
|
|
test.describe('WebGL single-terminal heavy render + merge', () => {
|
|
test.skip(({ browserName }) => browserName !== 'chromium', 'WebGL atlas tests run on Chromium/SwiftShader only');
|
|
test.describe.configure({ timeout: 30000 });
|
|
|
|
test.beforeEach(async () => {
|
|
await openTerminal(ctx, { cols: 80, rows: 24, scrollback: 5000 });
|
|
await loadWebglStrict(ctx);
|
|
});
|
|
|
|
test('heavy merge churn must not crash the GlyphRenderer (vscode#322756)', async () => {
|
|
// Guard: heavy merges must not throw from GlyphRenderer.render. Note the
|
|
// page cap here is lowered AFTER the renderer was built, so _atlasTextures
|
|
// keeps its original (large) length and this test cannot hit the
|
|
// page-overflow crash; that scenario needs the cap lowered BEFORE loading
|
|
// the addon and is covered separately (WebglAtlasOverflow.test.ts).
|
|
const errors: string[] = [];
|
|
const onError = (e: Error): void => { errors.push(e.message); };
|
|
ctx.page.on('pageerror', onError);
|
|
try {
|
|
await setMaxAtlasPages(ctx, 4);
|
|
await floodDistinctGlyphs(ctx, 10);
|
|
// Force a GPU read (screenshot) mid-churn so a render lands during/after a
|
|
// merge, then churn more — this reliably exposes the version crash.
|
|
await captureRowSignatures(ctx, 1, [1, 2, 3]);
|
|
const pages = await floodDistinctGlyphs(ctx, 10);
|
|
expect(pages, 'should have allocated/merged pages').toBeGreaterThan(1);
|
|
await ctx.page.waitForTimeout(300);
|
|
expect(errors, `renderer crashed during atlas merges: ${errors[0] ?? ''}`).toEqual([]);
|
|
} finally {
|
|
ctx.page.off('pageerror', onError);
|
|
}
|
|
});
|
|
|
|
test('pinned header stays correct while body churns many pages', async () => {
|
|
const cols = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
|
await writeAndWaitForRender(ctx, 'HEADER_REF_0123456789');
|
|
const reference = await captureRowSignatures(ctx, 1, cols);
|
|
|
|
// Body churns unique glyphs (forces merges) while header line stays on screen.
|
|
const pages = await floodDistinctGlyphs(ctx, 8);
|
|
expect(pages, 'should have allocated/merged pages').toBeGreaterThan(1);
|
|
|
|
await ctx.proxy.scrollToTop();
|
|
await waitForRender(ctx);
|
|
const after = await captureRowSignatures(ctx, 1, cols);
|
|
for (let i = 0; i < cols.length; i++) {
|
|
expectSignatureMatches(after[i], reference[i], 14, `header glyph col ${cols[i]} corrupted by single-terminal merges`);
|
|
}
|
|
});
|
|
|
|
test('visible rows match content while scrolling (no merge)', async () => {
|
|
// Big scrollback of distinct glyphs across multiple pages (no tiny page cap,
|
|
// so no merge), then scroll up/down repeatedly.
|
|
for (let c = 0; c < 8; c++) {
|
|
await writeAndWaitForRender(ctx, generateUniqueGlyphFlood(24 * 40, 80, c * 24 * 40));
|
|
}
|
|
expect(await getAtlasPageCount(ctx)).toBeGreaterThan(1);
|
|
|
|
await ctx.proxy.scrollToTop();
|
|
await waitForRender(ctx);
|
|
const top = await captureRowSignatures(ctx, 1, [1, 2, 3, 4, 5]);
|
|
await ctx.proxy.scrollToBottom();
|
|
await ctx.proxy.scrollToTop();
|
|
await waitForRender(ctx);
|
|
const topAgain = await captureRowSignatures(ctx, 1, [1, 2, 3, 4, 5]);
|
|
for (let i = 0; i < top.length; i++) {
|
|
expectSignatureMatches(topAgain[i], top[i], 14, `row1 col${i + 1} changed after scroll round-trip`);
|
|
}
|
|
});
|
|
});
|