diff --git a/examples/index.ts b/examples/index.ts index 8c393e5..da88944 100644 --- a/examples/index.ts +++ b/examples/index.ts @@ -432,7 +432,11 @@ async function runAutomation( }, memMonitor: null, }; - await automation(exampleSettings); + try { + await automation(exampleSettings); + } catch (err) { + console.error(`Automation for ${testName} threw:`, err); + } testRoot.parent = null; testRoot.destroy(); } diff --git a/examples/tests/animation-events.ts b/examples/tests/animation-events.ts deleted file mode 100644 index 09116a7..0000000 --- a/examples/tests/animation-events.ts +++ /dev/null @@ -1,188 +0,0 @@ -import type { ExampleSettings } from '../common/ExampleSettings.js'; -import robotImg from '../assets/robot/robot.png'; - -export async function automation(settings: ExampleSettings) { - // Snapshot single page - await test(settings); -} - -/** - * This test verifies that the animation events are firing as expected. - * - * This test runs three seperate animations of a robot. - * - * Animation 1: - * - Starts at 0, 0 - * - Moves to 100, 100 - * - Duration: 500ms - * - Delay: 500ms - * - Tests that the 'animating' event is fired after the delay - * - Tests that the waitUntilStopped() promise resolves when the animation finishes - * - * Animation 2: - * - Starts at 100, 100 - * - Moves to 0, 0 - * - Duration: 500ms - * - No Delay - * - Tests that the 'animating' event is fired (even if no delay) - * - Tests that the 'stopped' event is fired when the animation finishes - * - * Animation 3: - * - Starts at 0, 0 - * - Moves to 100, 100 - * - Duration: 500ms - * - No Delay - * - Tests that the 'stopped' event is fired when stop() is called before the - * animation finishes - * - Robot resets to 0, 0 when stop() is called - * - * @param param0 - */ -export default async function test({ - renderer, - testRoot, - snapshot, -}: ExampleSettings) { - testRoot.w = 250; - testRoot.h = 250; - testRoot.color = 0xffffffff; - - const robot = renderer.createNode({ - x: 0, - y: 0, - w: 140, - h: 140, - zIndex: 5, - src: robotImg, - parent: testRoot, - }); - - const status = renderer.createTextNode({ - mount: 1, - x: testRoot.w, - y: testRoot.h, - fontSize: 40, - fontFamily: 'Ubuntu', - parent: testRoot, - color: 0x000000ff, - }); - - ////////////////////////// - // Animation 1 - ////////////////////////// - status.text = 'a1: init'; - await snapshot({ name: 'a1' }); - const animation1 = robot.animate( - { - x: 100, - y: 100, - }, - { - delay: 500, - duration: 500, - }, - ); - animation1.once('animating', () => { - robot.color = 0x00ff00ff; // green - // Hide the robot until after the snapshot is taken - // Hack for the VRT, since the position of the robot at this point will not - // be consistent between runs of the VRT. - robot.alpha = 0; - status.text = 'a1: animating'; - snapshot({ name: 'a1' }) - .then(() => { - robot.alpha = 1; - }) - .catch(console.error); - }); - // This will resolve right await because the animation starts out stopped. - await animation1.waitUntilStopped(); - animation1.start(); - await animation1.waitUntilStopped(); - status.text = 'a1: stopped'; - robot.color = 0xff0000ff; // red - await snapshot({ name: 'a1' }); - - ////////////////////////// - // Animation 2 - ////////////////////////// - status.text = 'a2: init'; - robot.color = 0xffffffff; // white - await snapshot({ name: 'a2' }); - const animation2 = robot.animate( - { - x: 0, - y: 0, - }, - { - duration: 500, - }, - ); - animation2.once('animating', () => { - robot.color = 0x00ff00ff; // green - // Hide the robot until after the snapshot is taken... - // Hack for the VRT, since the position of the robot at this point will not - // be consistent between runs of the VRT. - robot.alpha = 0; - status.text = 'a2: animating'; - snapshot({ name: 'a2' }) - .then(() => { - robot.alpha = 1; - }) - .catch(console.error); - }); - - const stoppedEventPromiseA2 = new Promise((resolve) => { - animation2.once('stopped', () => { - status.text = 'a2: stopped'; - robot.color = 0xff0000ff; // red - snapshot({ name: 'a2' }) - .then(() => { - robot.alpha = 1; - resolve(); - }) - .catch(console.error); - }); - }); - animation2.start(); - await stoppedEventPromiseA2; - - ////////////////////////// - // Animation 3 - ////////////////////////// - status.text = 'a3: init'; - robot.color = 0xffffffff; // white - await snapshot({ name: 'a3' }); - const animation3 = robot.animate( - { - x: 100, - y: 100, - }, - { - duration: 500, - }, - ); - - const stoppedEventPromiseA3 = new Promise((resolve) => { - animation3.once('stopped', () => { - status.text = 'a3: stopped'; - robot.color = 0xff0000ff; // red - snapshot({ name: 'a3' }) - .then(() => { - robot.alpha = 1; - resolve(); - }) - .catch(console.error); - }); - }); - animation3.start(); - await delay(100); - // Force stop the animation - animation3.stop(); - // Wait for the stopped event to be fired - await stoppedEventPromiseA3; -} - -function delay(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} diff --git a/examples/vite.config.ts b/examples/vite.config.ts index 53ad5ea..0b8dc6b 100644 --- a/examples/vite.config.ts +++ b/examples/vite.config.ts @@ -65,6 +65,8 @@ export default defineConfig(({ command, mode, isSsrBuild }) => { define: { __DEV__: true, __enableAutosize__: process.env.VRT_AUTOSIZE === 'true', + __emitBoundsEvents__: true, + __RTT__: true, }, }; }); diff --git a/src/core/CoreNode.ts b/src/core/CoreNode.ts index e03e645..5028f35 100644 --- a/src/core/CoreNode.ts +++ b/src/core/CoreNode.ts @@ -2577,10 +2577,21 @@ export class CoreNode extends EventEmitter { return; } + // If there's still an RTT ancestor higher up (nested RTT case), descendants + // should inherit from that ancestor rather than be detached from RTT + // entirely. Otherwise — when this node was the only RTT in the chain — + // fully clear inheritance. + const ancestorRTT = this.findParentRTTNode(); + for (const child of this.children) { + if (ancestorRTT !== null) { + child.parentHasRenderTexture = true; + child.rttParent = ancestorRTT; + } else { + child.parentHasRenderTexture = false; + child.rttParent = null; + } // force child to update everything as the RTT inheritance has changed - child.parentHasRenderTexture = false; - child.rttParent = null; child.setUpdateType(UpdateType.All); child.clearRTTInheritance(); } diff --git a/src/core/Stage.ts b/src/core/Stage.ts index 9fcc0cf..2ac2502 100644 --- a/src/core/Stage.ts +++ b/src/core/Stage.ts @@ -809,6 +809,12 @@ export class Stage { this.strictBound, this.boundsMargin, ); + // The frame loop walks root.children directly without running root.update + // (see this.update), so root.createRenderBounds is never invoked. Refresh + // root's bounds in lockstep with the stage so descendants pick up the new + // viewport via createRenderBounds' parent.strictBound copy. + this.root.strictBound = this.strictBound; + this.root.preloadBound = this.preloadBound; this.root.setUpdateType(UpdateType.RenderBounds | UpdateType.Children); this.root.childUpdateType |= UpdateType.RenderBounds; } diff --git a/src/core/renderers/webgl/WebGlRenderer.ts b/src/core/renderers/webgl/WebGlRenderer.ts index 790d693..5f4d34d 100644 --- a/src/core/renderers/webgl/WebGlRenderer.ts +++ b/src/core/renderers/webgl/WebGlRenderer.ts @@ -58,6 +58,16 @@ export class WebGlRenderer extends CoreRenderer { quadBuffer: ArrayBuffer; fQuadBuffer: Float32Array; uiQuadBuffer: Uint32Array; + /** + * Separate buffer for RTT quad data. Required when DIRTY_QUAD_BUFFER is on: + * main-scene nodes own permanent slots in `quadBuffer` and only rewrite when + * dirty, so if RTT wrote into the same backing storage starting at index 0 + * it would silently overwrite (and corrupt) main-scene slots whose owners + * aren't dirty this frame. Allocated lazily on first RTT. + */ + rttQuadBuffer: ArrayBuffer | null = null; + fRttQuadBuffer: Float32Array | null = null; + uiRttQuadBuffer: Uint32Array | null = null; renderOps: WebGlRenderOp[] = []; /** * Deferred queue for SDF text render ops, used when RENDER_TEXT_BATCHING is @@ -373,8 +383,17 @@ export class WebGlRenderer extends CoreRenderer { * The function updates the length and number of quads in the current render operation, and updates the current buffer index. */ addQuad(node: CoreNode) { - const f = this.fQuadBuffer; - const u = this.uiQuadBuffer; + let f = this.fQuadBuffer; + let u = this.uiQuadBuffer; + if (USE_RTT && this.renderToTextureActive === true) { + if (this.fRttQuadBuffer === null) { + this.rttQuadBuffer = new ArrayBuffer(this.stage.options.quadBufferSize); + this.fRttQuadBuffer = new Float32Array(this.rttQuadBuffer); + this.uiRttQuadBuffer = new Uint32Array(this.rttQuadBuffer); + } + f = this.fRttQuadBuffer; + u = this.uiRttQuadBuffer!; + } // Explicit zIndex on a non-text quad opts out of the text-batching // ordering: flush deferred text now so this quad lands above any text @@ -1186,12 +1205,13 @@ export class WebGlRenderer extends CoreRenderer { if (RENDER_TEXT_BATCHING === true) { this.flushTextRenderOps(); } - const { glw, quadBuffer } = this; + const { glw } = this; const buffer = this.quadBufferCollection.getBuffer('a_position') || null; // Always do a full upload for RTT — the buffer is rebuilt from scratch - // each frame with sequential slots starting at index 0. - const arr = new Float32Array(quadBuffer, 0, this.curBufferIdx); + // each frame with sequential slots starting at index 0. Upload from the + // dedicated RTT ArrayBuffer so we don't read stale main-scene data. + const arr = new Float32Array(this.rttQuadBuffer!, 0, this.curBufferIdx); glw.arrayBufferData(buffer, arr, glw.STATIC_DRAW); for (let i = 0, length = this.renderOps.length; i < length; i++) { diff --git a/src/core/text-rendering/SdfTextRenderer.ts b/src/core/text-rendering/SdfTextRenderer.ts index 3e25003..1ed0693 100644 --- a/src/core/text-rendering/SdfTextRenderer.ts +++ b/src/core/text-rendering/SdfTextRenderer.ts @@ -35,7 +35,7 @@ const font: FontHandler = SdfFontHandler; const layoutCache = new Map(); const getLayoutCacheKey = (props: CoreTextNodeProps): string => - `${props.fontFamily}-${props.fontSize}-${props.letterSpacing}-${props.lineHeight}-${props.maxHeight}-${props.maxWidth}-${props.textAlign}-${props.text}`; + `${props.fontFamily}-${props.fontSize}-${props.letterSpacing}-${props.lineHeight}-${props.maxHeight}-${props.maxWidth}-${props.maxLines}-${props.textAlign}-${props.wordBreak}-${props.overflowSuffix}-${props.text}`; /** * SDF text renderer using MSDF/SDF fonts with WebGL diff --git a/visual-regression/certified-snapshots/chromium-ci/animation-events_a1-1.png b/visual-regression/certified-snapshots/chromium-ci/animation-events_a1-1.png deleted file mode 100644 index bc98b0d..0000000 Binary files a/visual-regression/certified-snapshots/chromium-ci/animation-events_a1-1.png and /dev/null differ diff --git a/visual-regression/certified-snapshots/chromium-ci/animation-events_a1-2.png b/visual-regression/certified-snapshots/chromium-ci/animation-events_a1-2.png deleted file mode 100644 index 491e6cc..0000000 Binary files a/visual-regression/certified-snapshots/chromium-ci/animation-events_a1-2.png and /dev/null differ diff --git a/visual-regression/certified-snapshots/chromium-ci/animation-events_a1-3.png b/visual-regression/certified-snapshots/chromium-ci/animation-events_a1-3.png deleted file mode 100644 index f2e3855..0000000 Binary files a/visual-regression/certified-snapshots/chromium-ci/animation-events_a1-3.png and /dev/null differ diff --git a/visual-regression/certified-snapshots/chromium-ci/animation-events_a2-1.png b/visual-regression/certified-snapshots/chromium-ci/animation-events_a2-1.png deleted file mode 100644 index 156d00f..0000000 Binary files a/visual-regression/certified-snapshots/chromium-ci/animation-events_a2-1.png and /dev/null differ diff --git a/visual-regression/certified-snapshots/chromium-ci/animation-events_a2-2.png b/visual-regression/certified-snapshots/chromium-ci/animation-events_a2-2.png deleted file mode 100644 index 6cbe856..0000000 Binary files a/visual-regression/certified-snapshots/chromium-ci/animation-events_a2-2.png and /dev/null differ diff --git a/visual-regression/certified-snapshots/chromium-ci/animation-events_a2-3.png b/visual-regression/certified-snapshots/chromium-ci/animation-events_a2-3.png deleted file mode 100644 index 3dccaf9..0000000 Binary files a/visual-regression/certified-snapshots/chromium-ci/animation-events_a2-3.png and /dev/null differ diff --git a/visual-regression/certified-snapshots/chromium-ci/animation-events_a3-1.png b/visual-regression/certified-snapshots/chromium-ci/animation-events_a3-1.png deleted file mode 100644 index 493fbaf..0000000 Binary files a/visual-regression/certified-snapshots/chromium-ci/animation-events_a3-1.png and /dev/null differ diff --git a/visual-regression/certified-snapshots/chromium-ci/animation-events_a3-2.png b/visual-regression/certified-snapshots/chromium-ci/animation-events_a3-2.png deleted file mode 100644 index e44d438..0000000 Binary files a/visual-regression/certified-snapshots/chromium-ci/animation-events_a3-2.png and /dev/null differ diff --git a/visual-regression/certified-snapshots/chromium-ci/rtt-dimension-6.png b/visual-regression/certified-snapshots/chromium-ci/rtt-dimension-6.png index f797d91..2c84400 100644 Binary files a/visual-regression/certified-snapshots/chromium-ci/rtt-dimension-6.png and b/visual-regression/certified-snapshots/chromium-ci/rtt-dimension-6.png differ