Skip to content

Commit

Permalink
Render to Texture refactors (#441)
Browse files Browse the repository at this point in the history
Fixes #440

Todo:

- [x] Investigate why source -> destination texture relation adheres to
original RTT node render state
- [x] Cache Parent RTT node for faster update behaviour
  • Loading branch information
wouterlucas authored Nov 13, 2024
2 parents efa9ed9 + b43e90a commit eb54ccc
Show file tree
Hide file tree
Showing 9 changed files with 253 additions and 85 deletions.
4 changes: 3 additions & 1 deletion examples/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand Down
49 changes: 47 additions & 2 deletions examples/tests/rtt-dimension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
}
198 changes: 125 additions & 73 deletions src/core/CoreNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
});
}

Expand Down Expand Up @@ -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', {
Expand All @@ -839,13 +850,23 @@ 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,
} satisfies NodeTextureFailedPayload);
};

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);
Expand All @@ -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() {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;

Expand All @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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();
Expand All @@ -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 {
Expand Down Expand Up @@ -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<CoreNodeAnimateProps>,
settings: Partial<AnimationSettings>,
Expand Down
Loading

0 comments on commit eb54ccc

Please sign in to comment.