diff --git a/examples/index.ts b/examples/index.ts index aa50d9a9..6a2d4e50 100644 --- a/examples/index.ts +++ b/examples/index.ts @@ -382,7 +382,9 @@ async function runAutomation( // Override Math.random() as stable random number generator // - Each test gets the same sequence of random numbers // - This only is in effect when tests are run in automation mode - const rand = mt19937.factory({ seed: 1234 }); + // eslint-disable-next-line @typescript-eslint/unbound-method + const factory = mt19937.factory || mt19937.default.factory; + const rand = factory({ seed: 1234 }); Math.random = function () { return rand() / rand.MAX; }; diff --git a/examples/tests/rtt-dimension.ts b/examples/tests/rtt-dimension.ts index 6965174e..5b1c7aaf 100644 --- a/examples/tests/rtt-dimension.ts +++ b/examples/tests/rtt-dimension.ts @@ -2,8 +2,13 @@ import type { ExampleSettings } from '../common/ExampleSettings.js'; import rocko from '../assets/rocko.png'; export async function automation(settings: ExampleSettings) { - await test(settings); - await settings.snapshot(); + const page = await test(settings); + + const maxPages = 6; + for (let i = 0; i < maxPages; i++) { + page(i); + await settings.snapshot(); + } } export default async function test({ renderer, testRoot }: ExampleSettings) { @@ -251,4 +256,44 @@ export default async function test({ renderer, testRoot }: ExampleSettings) { rttNode.height = rttNode.height === 200 ? 300 : 200; } }); + + // Define the page function to configure different test scenarios + const page = (i = 0) => { + switch (i) { + case 1: + rttNode.rtt = false; + rttNode2.rtt = false; + rttNode3.rtt = false; + break; + + case 2: + rttNode.rtt = true; + rttNode2.rtt = true; + rttNode3.rtt = true; + break; + + case 4: + // Modify child texture properties in nested RTT node + rocko4.x = 0; + break; + + case 5: + nestedRTTNode1.rtt = false; + break; + + case 6: + nestedRTTNode1.rtt = true; + break; + + default: + // Reset to initial state + rttNode.rtt = true; + rttNode2.rtt = true; + rttNode3.rtt = true; + nestedRTTNode1.rtt = true; + break; + } + }; + + return page; } diff --git a/src/core/CoreNode.ts b/src/core/CoreNode.ts index e76fdd4d..189a3f0d 100644 --- a/src/core/CoreNode.ts +++ b/src/core/CoreNode.ts @@ -783,6 +783,21 @@ export class CoreNode extends EventEmitter { if (this.textureOptions.preload) { texture.ctxTexture.load(); } + + texture.on('loaded', this.onTextureLoaded); + texture.on('failed', this.onTextureFailed); + texture.on('freed', this.onTextureFreed); + + // If the parent is a render texture, the initial texture status + // will be set to freed until the texture is processed by the + // Render RTT nodes. So we only need to listen fo changes and + // no need to check the texture.state until we restructure how + // textures are being processed. + if (this.parentHasRenderTexture) { + this.notifyParentRTTOfUpdate(); + return; + } + if (texture.state === 'loaded') { assertTruthy(texture.dimensions); this.onTextureLoaded(texture, texture.dimensions); @@ -792,9 +807,6 @@ export class CoreNode extends EventEmitter { } else if (texture.state === 'freed') { this.onTextureFreed(texture); } - texture.on('loaded', this.onTextureLoaded); - texture.on('failed', this.onTextureFailed); - texture.on('freed', this.onTextureFreed); }); } @@ -822,9 +834,8 @@ export class CoreNode extends EventEmitter { this.stage.requestRender(); // If parent has a render texture, flag that we need to update - // @todo: Reserve type for RTT updates if (this.parentHasRenderTexture) { - this.setRTTUpdates(1); + this.notifyParentRTTOfUpdate(); } this.emit('loaded', { @@ -839,6 +850,11 @@ export class CoreNode extends EventEmitter { }; private onTextureFailed: TextureFailedEventHandler = (_, error) => { + // If parent has a render texture, flag that we need to update + if (this.parentHasRenderTexture) { + this.notifyParentRTTOfUpdate(); + } + this.emit('failed', { type: 'texture', error, @@ -846,6 +862,11 @@ export class CoreNode extends EventEmitter { }; private onTextureFreed: TextureFreedEventHandler = () => { + // If parent has a render texture, flag that we need to update + if (this.parentHasRenderTexture) { + this.notifyParentRTTOfUpdate(); + } + this.emit('freed', { type: 'texture', } satisfies NodeTextureFreedPayload); @@ -866,24 +887,10 @@ export class CoreNode extends EventEmitter { const parent = this.props.parent; if (!parent) return; - // Inform the parent if it doesn’t already have a child update if ((parent.updateType & UpdateType.Children) === 0) { + // Inform the parent if it doesn’t already have a child update parent.setUpdateType(UpdateType.Children); } - - if (this.parentHasRenderTexture === false) return; - - if (this.rtt === false) { - if ((parent.updateType & UpdateType.RenderTexture) === 0) { - this.setRTTUpdates(type); - parent.setUpdateType(UpdateType.RenderTexture); - } - } - - // If this node has outstanding RTT updates, propagate them - if (this.hasRTTupdates) { - this.setRTTUpdates(type); - } } sortChildren() { @@ -988,24 +995,11 @@ export class CoreNode extends EventEmitter { const parent = this.props.parent; let renderState = null; - if (this.updateType & UpdateType.ParentRenderTexture) { - let p = this.parent; - while (p) { - if (p.rtt) { - this.parentHasRenderTexture = true; - } - p = p.parent; - } - } - - // If we have render texture updates and not already running a full update - if ( - this.updateType ^ UpdateType.All && - this.updateType & UpdateType.RenderTexture - ) { - for (let i = 0, length = this.children.length; i < length; i++) { - this.children[i]?.setUpdateType(UpdateType.All); - } + // Handle specific RTT updates at this node level + if (this.updateType & UpdateType.RenderTexture && this.rtt) { + // Only the RTT node itself triggers `renderToTexture` + this.hasRTTupdates = true; + this.stage.renderer?.renderToTexture(this); } if (this.updateType & UpdateType.Global) { @@ -1130,11 +1124,7 @@ export class CoreNode extends EventEmitter { return; } - if ( - this.updateType & UpdateType.Children && - this.children.length > 0 && - this.rtt === false - ) { + if (this.updateType & UpdateType.Children && this.children.length > 0) { for (let i = 0, length = this.children.length; i < length; i++) { const child = this.children[i] as CoreNode; @@ -1148,6 +1138,13 @@ export class CoreNode extends EventEmitter { } } + // If the node has an RTT parent and requires a texture re-render, inform the RTT parent + // if (this.parentHasRenderTexture && this.updateType & UpdateType.RenderTexture) { + // @TODO have a more scoped down updateType for RTT updates + if (this.parentHasRenderTexture && this.updateType > 0) { + this.notifyParentRTTOfUpdate(); + } + // Sorting children MUST happen after children have been updated so // that they have the oppotunity to update their calculated zIndex. if (this.updateType & UpdateType.ZIndexSortedChildren) { @@ -1168,6 +1165,31 @@ export class CoreNode extends EventEmitter { this.childUpdateType = 0; } + private notifyParentRTTOfUpdate() { + if (this.parent === null) { + return; + } + + let rttNode: CoreNode | null = this.parent; + // Traverse up to find the RTT root node + while (rttNode && !rttNode.rtt) { + rttNode = rttNode.parent; + } + + if (!rttNode) { + return; + } + + // If an RTT node is found, mark it for re-rendering + rttNode.hasRTTupdates = true; + rttNode.setUpdateType(UpdateType.RenderTexture); + + // if rttNode is nested, also make it update its RTT parent + if (rttNode.parentHasRenderTexture === true) { + rttNode.notifyParentRTTOfUpdate(); + } + } + //check if CoreNode is renderable based on props hasRenderableProperties(): boolean { if (this.props.texture) { @@ -1940,8 +1962,9 @@ export class CoreNode extends EventEmitter { UpdateType.Children | UpdateType.ZIndexSortedChildren, ); + // If the new parent has an RTT enabled, apply RTT inheritance if (newParent.rtt || newParent.parentHasRenderTexture) { - this.setRTTUpdates(UpdateType.All); + this.applyRTTInheritance(newParent); } } this.updateScaleRotateTransform(); @@ -1963,43 +1986,77 @@ export class CoreNode extends EventEmitter { } set rtt(value: boolean) { - if (this.props.rtt === true) { - this.props.rtt = value; - - // unload texture if we used to have a render texture - if (value === false && this.texture !== null) { - this.unloadTexture(); - this.setUpdateType(UpdateType.All); - for (let i = 0, length = this.children.length; i < length; i++) { - this.children[i]!.parentHasRenderTexture = false; - } - this.stage.renderer?.removeRTTNode(this); - return; - } + if (this.props.rtt === value) { + return; } + this.props.rtt = value; - // if the new value is false and we didnt have rtt previously, we don't need to do anything - if (value === false) { - return; + if (value) { + this.initRenderTexture(); + this.markChildrenWithRTT(); + } else { + this.cleanupRenderTexture(); } - // load texture + this.setUpdateType(UpdateType.RenderTexture); + + if (this.parentHasRenderTexture) { + this.notifyParentRTTOfUpdate(); + } + } + private initRenderTexture() { this.texture = this.stage.txManager.loadTexture('RenderTexture', { width: this.width, height: this.height, }); this.textureOptions.preload = true; + this.stage.renderer?.renderToTexture(this); // Only this RTT node + } - this.props.rtt = true; - this.hasRTTupdates = true; - this.setUpdateType(UpdateType.All); + private cleanupRenderTexture() { + this.unloadTexture(); + this.clearRTTInheritance(); - for (let i = 0, length = this.children.length; i < length; i++) { - this.children[i]!.setUpdateType(UpdateType.All); + this.stage.renderer?.removeRTTNode(this); + this.hasRTTupdates = false; + this.texture = null; + } + + private markChildrenWithRTT(node: CoreNode | null = null) { + const parent = node || this; + + for (const child of parent.children) { + child.setUpdateType(UpdateType.All); + child.parentHasRenderTexture = true; + child.markChildrenWithRTT(); + } + } + + // Apply RTT inheritance when a node has an RTT-enabled parent + private applyRTTInheritance(parent: CoreNode) { + if (parent.rtt) { + // Only the RTT node should be added to `renderToTexture` + parent.setUpdateType(UpdateType.RenderTexture); } - // Store RTT nodes in a separate list - this.stage.renderer?.renderToTexture(this); + // Propagate `parentHasRenderTexture` downwards + this.markChildrenWithRTT(parent); + } + + // Clear RTT inheritance when detaching from an RTT chain + private clearRTTInheritance() { + // if this node is RTT itself stop the propagation important for nested RTT nodes + // for the initial RTT node this is already handled in `set rtt` + if (this.rtt) { + return; + } + + for (const child of this.children) { + // force child to update everything as the RTT inheritance has changed + child.parentHasRenderTexture = false; + child.setUpdateType(UpdateType.All); + child.clearRTTInheritance(); + } } get shader(): BaseShaderController { @@ -2158,11 +2215,6 @@ export class CoreNode extends EventEmitter { this.childUpdateType |= UpdateType.RenderBounds | UpdateType.Children; } - setRTTUpdates(type: number) { - this.hasRTTupdates = true; - this.parent?.setRTTUpdates(type); - } - animate( props: Partial, settings: Partial, diff --git a/src/core/renderers/webgl/WebGlCoreRenderer.ts b/src/core/renderers/webgl/WebGlCoreRenderer.ts index 3854eccf..515d0588 100644 --- a/src/core/renderers/webgl/WebGlCoreRenderer.ts +++ b/src/core/renderers/webgl/WebGlCoreRenderer.ts @@ -626,13 +626,88 @@ export class WebGlCoreRenderer extends CoreRenderer { } } - // @todo: Better bottom up rendering order - this.rttNodes.unshift(node); + this.insertRTTNodeInOrder(node); + } + + /** + * Inserts an RTT node into `this.rttNodes` while maintaining the correct rendering order based on hierarchy. + * + * Rendering order for RTT nodes is critical when nested RTT nodes exist in a parent-child relationship. + * Specifically: + * - Child RTT nodes must be rendered before their RTT-enabled parents to ensure proper texture composition. + * - If an RTT node is added and it has existing RTT children, it should be rendered after those children. + * + * This function addresses both cases by: + * 1. **Checking Upwards**: It traverses the node's hierarchy upwards to identify any RTT parent + * already in `rttNodes`. If an RTT parent is found, the new node is placed before this parent. + * 2. **Checking Downwards**: It traverses the node’s children recursively to find any RTT-enabled + * children that are already in `rttNodes`. If such children are found, the new node is inserted + * after the last (highest index) RTT child node. + * + * The final calculated insertion index ensures the new node is positioned in `rttNodes` to respect + * both parent-before-child and child-before-parent rendering rules, preserving the correct order + * for the WebGL renderer. + * + * @param node - The RTT-enabled CoreNode to be added to `rttNodes` in the appropriate hierarchical position. + */ + private insertRTTNodeInOrder(node: CoreNode) { + let insertIndex = this.rttNodes.length; // Default to the end of the array + + // 1. Traverse upwards to ensure the node is placed before its RTT parent (if any). + let currentNode: CoreNode = node; + while (currentNode) { + if (!currentNode.parent) { + break; + } + + const parentIndex = this.rttNodes.indexOf(currentNode.parent); + if (parentIndex !== -1) { + // Found an RTT parent in the list; set insertIndex to place node before the parent + insertIndex = parentIndex; + break; + } + + currentNode = currentNode.parent; + } + + // 2. Traverse downwards to ensure the node is placed after any RTT children. + // Look through each child recursively to see if any are already in rttNodes. + const maxChildIndex = this.findMaxChildRTTIndex(node); + if (maxChildIndex !== -1) { + // Adjust insertIndex to be after the last child RTT node + insertIndex = Math.max(insertIndex, maxChildIndex + 1); + } + + // 3. Insert the node at the calculated position + this.rttNodes.splice(insertIndex, 0, node); + } + + // Helper function to find the highest index of any RTT children of a node within rttNodes + private findMaxChildRTTIndex(node: CoreNode): number { + let maxIndex = -1; + + const traverseChildren = (currentNode: CoreNode) => { + const currentIndex = this.rttNodes.indexOf(currentNode); + if (currentIndex !== -1) { + maxIndex = Math.max(maxIndex, currentIndex); + } + + // Recursively check all children of the current node + for (const child of currentNode.children) { + traverseChildren(child); + } + }; + + // Start traversal directly with the provided node + traverseChildren(node); + + return maxIndex; } renderRTTNodes() { const { glw } = this; const { txManager } = this.stage; + // Render all associated RTT nodes to their textures for (let i = 0; i < this.rttNodes.length; i++) { const node = this.rttNodes[i]; @@ -662,16 +737,10 @@ export class WebGlCoreRenderer extends CoreRenderer { // Render all associated quads to the texture for (let i = 0; i < node.children.length; i++) { const child = node.children[i]; + if (!child) { continue; } - child.update(this.stage.deltaTime, { - x: 0, - y: 0, - width: 0, - height: 0, - valid: false, - }); this.stage.addQuads(child); child.hasRTTupdates = false; diff --git a/visual-regression/certified-snapshots/chromium-ci/rtt-dimension-2.png b/visual-regression/certified-snapshots/chromium-ci/rtt-dimension-2.png new file mode 100644 index 00000000..3f651124 Binary files /dev/null and b/visual-regression/certified-snapshots/chromium-ci/rtt-dimension-2.png differ diff --git a/visual-regression/certified-snapshots/chromium-ci/rtt-dimension-3.png b/visual-regression/certified-snapshots/chromium-ci/rtt-dimension-3.png new file mode 100644 index 00000000..305381b9 Binary files /dev/null and b/visual-regression/certified-snapshots/chromium-ci/rtt-dimension-3.png differ diff --git a/visual-regression/certified-snapshots/chromium-ci/rtt-dimension-4.png b/visual-regression/certified-snapshots/chromium-ci/rtt-dimension-4.png new file mode 100644 index 00000000..305381b9 Binary files /dev/null and b/visual-regression/certified-snapshots/chromium-ci/rtt-dimension-4.png differ diff --git a/visual-regression/certified-snapshots/chromium-ci/rtt-dimension-5.png b/visual-regression/certified-snapshots/chromium-ci/rtt-dimension-5.png new file mode 100644 index 00000000..f51a6a56 Binary files /dev/null and b/visual-regression/certified-snapshots/chromium-ci/rtt-dimension-5.png 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 new file mode 100644 index 00000000..e4aa30e7 Binary files /dev/null and b/visual-regression/certified-snapshots/chromium-ci/rtt-dimension-6.png differ