From ed54ec70ddb75d5649b2ce806509d89cf4e62415 Mon Sep 17 00:00:00 2001 From: wouterlucas Date: Thu, 29 Aug 2024 21:05:35 +0200 Subject: [PATCH 1/8] perf: Inherit strict and preload boundaries from parent unless clipping is enabled --- src/core/CoreNode.ts | 130 +++++++++++++++++++++++++++++++++---------- 1 file changed, 101 insertions(+), 29 deletions(-) diff --git a/src/core/CoreNode.ts b/src/core/CoreNode.ts index 3b115bc5..840f5128 100644 --- a/src/core/CoreNode.ts +++ b/src/core/CoreNode.ts @@ -178,6 +178,11 @@ export enum UpdateType { */ ParentRenderTexture = 4096, + /** + * Render Bounds update + */ + RenderBounds = 8192, + /** * None */ @@ -994,6 +999,46 @@ export class CoreNode extends EventEmitter { childUpdateType |= UpdateType.Global; } + if (this.updateType & UpdateType.RenderBounds) { + this.createRenderBounds(); + this.setUpdateType(UpdateType.RenderState); + } + + if (this.updateType & UpdateType.RenderState) { + this.checkRenderBounds(); + this.updateRenderState(); + this.setUpdateType(UpdateType.IsRenderable); + } + + if (this.updateType & UpdateType.IsRenderable) { + this.updateIsRenderable(); + } + + if (this.renderState === CoreNodeRenderState.OutOfBounds) { + this.updateType = 0; + return; + } + + 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 + ) { + this.children.forEach((child) => { + child.setUpdateType(UpdateType.All); + }); + } + if (this.updateType & UpdateType.Clipping) { this.calculateClippingRect(parentClippingRect); this.setUpdateType(UpdateType.Children); @@ -1050,15 +1095,6 @@ export class CoreNode extends EventEmitter { } } - if (this.updateType & UpdateType.RenderState) { - this.updateRenderState(parentClippingRect); - this.setUpdateType(UpdateType.IsRenderable); - } - - if (this.updateType & UpdateType.IsRenderable) { - this.updateIsRenderable(); - } - // No need to update zIndex if there is no parent if (parent && this.updateType & UpdateType.CalculatedZIndex) { this.calculateZIndex(); @@ -1151,39 +1187,72 @@ export class CoreNode extends EventEmitter { return false; } - checkRenderBounds(parentClippingRect: RectWithValid): CoreNodeRenderState { + checkRenderBounds(): CoreNodeRenderState { assertTruthy(this.renderBound); - const rectW = parentClippingRect.width || this.stage.root.width; - const rectH = parentClippingRect.height || this.stage.root.height; - this.strictBound = createBound( - parentClippingRect.x, - parentClippingRect.y, - parentClippingRect.x + rectW, - parentClippingRect.y + rectH, - this.strictBound, - ); + assertTruthy(this.strictBound); + assertTruthy(this.preloadBound); if (boundInsideBound(this.renderBound, this.strictBound)) { return CoreNodeRenderState.InViewport; } + if (boundInsideBound(this.renderBound, this.preloadBound)) { + return CoreNodeRenderState.InBounds; + } + return CoreNodeRenderState.OutOfBounds; + } + + createPreloadBounds(strictBound: Bound): Bound { const renderM = this.stage.boundsMargin; - this.preloadBound = createBound( - this.strictBound.x1 - renderM[3], - this.strictBound.y1 - renderM[0], - this.strictBound.x2 + renderM[1], - this.strictBound.y2 + renderM[2], + return createBound( + strictBound.x1 - renderM[3], + strictBound.y1 - renderM[0], + strictBound.x2 + renderM[1], + strictBound.y2 + renderM[2], this.preloadBound, ); + } - if (boundInsideBound(this.renderBound, this.preloadBound)) { - return CoreNodeRenderState.InBounds; + createRenderBounds(): void { + assertTruthy(this.stage); + + // no clipping, use parent's bounds + if (this.clipping === false && this.parent !== null) { + this.strictBound = + this.parent.strictBound ?? + createBound(0, 0, this.stage.root.width, this.stage.root.height); + this.preloadBound = + this.parent.preloadBound ?? this.createPreloadBounds(this.strictBound); + return; } - return CoreNodeRenderState.OutOfBounds; + + // no parent, use stage's root bounds + if (this.parent === null) { + this.strictBound = createBound( + 0, + 0, + this.stage.root.width, + this.stage.root.height, + ); + this.preloadBound = this.createPreloadBounds(this.strictBound); + return; + } + + // clipping is enabled create our own bounds + const { x, y, width, height } = this.props; + this.strictBound = createBound( + x, + y, + x + width, + y + height, + this.strictBound, + ); + + this.preloadBound = this.createPreloadBounds(this.strictBound); } - updateRenderState(parentClippingRect: RectWithValid) { - const renderState = this.checkRenderBounds(parentClippingRect); + updateRenderState() { + const renderState = this.checkRenderBounds(); if (renderState === this.renderState) { return; @@ -1825,6 +1894,9 @@ export class CoreNode extends EventEmitter { } } this.updateScaleRotateTransform(); + + // fetch render bounds from parent + this.setUpdateType(UpdateType.RenderBounds); } get preventCleanup(): boolean { From 883558240d8b887450412646ccce1f8b8d05d2c0 Mon Sep 17 00:00:00 2001 From: wouterlucas Date: Fri, 30 Aug 2024 13:16:06 +0200 Subject: [PATCH 2/8] perf: Propagate createBounds to children on clipping/bound changes --- src/core/CoreNode.ts | 29 ++++++++++++++++++++++------- src/core/Stage.ts | 8 ++++---- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/core/CoreNode.ts b/src/core/CoreNode.ts index 840f5128..21b66690 100644 --- a/src/core/CoreNode.ts +++ b/src/core/CoreNode.ts @@ -743,6 +743,10 @@ export class CoreNode extends EventEmitter { this.rtt = props.rtt; this.updateScaleRotateTransform(); + + this.setUpdateType( + UpdateType.Global | UpdateType.RenderBounds | UpdateType.RenderState, + ); } //#region Textures @@ -993,15 +997,21 @@ export class CoreNode extends EventEmitter { this.calculateRenderCoords(); this.updateBoundingRect(); + this.setUpdateType( UpdateType.Clipping | UpdateType.RenderState | UpdateType.Children, ); + childUpdateType |= UpdateType.Global; } if (this.updateType & UpdateType.RenderBounds) { this.createRenderBounds(); this.setUpdateType(UpdateType.RenderState); + + if (this.clipping === true || parentClippingRect.valid === true) { + this.setUpdateType(UpdateType.Children); + } } if (this.updateType & UpdateType.RenderState) { @@ -1041,8 +1051,11 @@ export class CoreNode extends EventEmitter { if (this.updateType & UpdateType.Clipping) { this.calculateClippingRect(parentClippingRect); - this.setUpdateType(UpdateType.Children); - childUpdateType |= UpdateType.Clipping; + + if (this.clipping === true || parentClippingRect.valid === true) { + this.setUpdateType(UpdateType.Children); + childUpdateType |= UpdateType.Clipping; + } } if (this.updateType & UpdateType.WorldAlpha) { @@ -1369,7 +1382,7 @@ export class CoreNode extends EventEmitter { const isRotated = gt.tb !== 0 || gt.tc !== 0; - if (clipping && !isRotated) { + if (clipping === true && isRotated === false) { clippingRect.x = gt.tx; clippingRect.y = gt.ty; clippingRect.width = this.width * gt.ta; @@ -1379,10 +1392,10 @@ export class CoreNode extends EventEmitter { clippingRect.valid = false; } - if (parentClippingRect.valid && clippingRect.valid) { + if (parentClippingRect.valid === true && clippingRect.valid === true) { // Intersect parent clipping rect with node clipping rect intersectRect(parentClippingRect, clippingRect, clippingRect); - } else if (parentClippingRect.valid) { + } else if (parentClippingRect.valid === true) { // Copy parent clipping rect copyRect(parentClippingRect, clippingRect); clippingRect.valid = true; @@ -1728,7 +1741,9 @@ export class CoreNode extends EventEmitter { set clipping(value: boolean) { this.props.clipping = value; - this.setUpdateType(UpdateType.Clipping); + this.setUpdateType( + UpdateType.Clipping | UpdateType.RenderBounds | UpdateType.Children, + ); } get color(): number { @@ -1896,7 +1911,7 @@ export class CoreNode extends EventEmitter { this.updateScaleRotateTransform(); // fetch render bounds from parent - this.setUpdateType(UpdateType.RenderBounds); + this.setUpdateType(UpdateType.RenderBounds | UpdateType.Children); } get preventCleanup(): boolean { diff --git a/src/core/Stage.ts b/src/core/Stage.ts index 135c5397..504406d0 100644 --- a/src/core/Stage.ts +++ b/src/core/Stage.ts @@ -381,20 +381,20 @@ export class Stage { } addQuads(node: CoreNode) { - assertTruthy(this.renderer && node.globalTransform); + assertTruthy(this.renderer); - if (node.isRenderable) { + if (node.isRenderable === true) { node.renderQuads(this.renderer); } for (let i = 0; i < node.children.length; i++) { const child = node.children[i]; - if (!child) { + if (child === undefined) { continue; } - if (child?.worldAlpha === 0) { + if (child.worldAlpha === 0) { continue; } From e87f8387af43b9ca509a01b3d43faf725ea9443a Mon Sep 17 00:00:00 2001 From: wouterlucas Date: Fri, 30 Aug 2024 17:16:41 +0200 Subject: [PATCH 3/8] refactor: Optimize updateType handling in CoreNode.ts --- src/core/CoreNode.ts | 39 ++++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/src/core/CoreNode.ts b/src/core/CoreNode.ts index 21b66690..231fc34b 100644 --- a/src/core/CoreNode.ts +++ b/src/core/CoreNode.ts @@ -998,9 +998,11 @@ export class CoreNode extends EventEmitter { this.calculateRenderCoords(); this.updateBoundingRect(); - this.setUpdateType( - UpdateType.Clipping | UpdateType.RenderState | UpdateType.Children, - ); + this.setUpdateType(UpdateType.RenderState | UpdateType.Children); + + if (this.clipping === true) { + this.setUpdateType(UpdateType.Clipping); + } childUpdateType |= UpdateType.Global; } @@ -1008,14 +1010,10 @@ export class CoreNode extends EventEmitter { if (this.updateType & UpdateType.RenderBounds) { this.createRenderBounds(); this.setUpdateType(UpdateType.RenderState); - - if (this.clipping === true || parentClippingRect.valid === true) { - this.setUpdateType(UpdateType.Children); - } + this.setUpdateType(UpdateType.Children); } if (this.updateType & UpdateType.RenderState) { - this.checkRenderBounds(); this.updateRenderState(); this.setUpdateType(UpdateType.IsRenderable); } @@ -1051,11 +1049,10 @@ export class CoreNode extends EventEmitter { if (this.updateType & UpdateType.Clipping) { this.calculateClippingRect(parentClippingRect); + this.setUpdateType(UpdateType.Children); - if (this.clipping === true || parentClippingRect.valid === true) { - this.setUpdateType(UpdateType.Children); - childUpdateType |= UpdateType.Clipping; - } + childUpdateType |= UpdateType.Clipping; + childUpdateType |= UpdateType.RenderBounds; } if (this.updateType & UpdateType.WorldAlpha) { @@ -1109,7 +1106,7 @@ export class CoreNode extends EventEmitter { } // No need to update zIndex if there is no parent - if (parent && this.updateType & UpdateType.CalculatedZIndex) { + if (parent !== null && this.updateType & UpdateType.CalculatedZIndex) { this.calculateZIndex(); // Tell parent to re-sort children parent.setUpdateType(UpdateType.ZIndexSortedChildren); @@ -1117,8 +1114,8 @@ export class CoreNode extends EventEmitter { if ( this.updateType & UpdateType.Children && - this.children.length && - !this.rtt + this.children.length > 0 && + this.rtt === false ) { this.children.forEach((child) => { // Trigger the depenedent update types on the child @@ -1212,6 +1209,7 @@ export class CoreNode extends EventEmitter { if (boundInsideBound(this.renderBound, this.preloadBound)) { return CoreNodeRenderState.InBounds; } + return CoreNodeRenderState.OutOfBounds; } @@ -1253,11 +1251,14 @@ export class CoreNode extends EventEmitter { // clipping is enabled create our own bounds const { x, y, width, height } = this.props; + const { tx, ty } = this.globalTransform || {}; + const _x = tx ?? x; + const _y = ty ?? y; this.strictBound = createBound( - x, - y, - x + width, - y + height, + _x, + _y, + _x + width, + _y + height, this.strictBound, ); From 12b79ac5961eb2a40eb17f0ecfb3b5a04d0dede4 Mon Sep 17 00:00:00 2001 From: wouterlucas Date: Fri, 30 Aug 2024 20:05:07 +0200 Subject: [PATCH 4/8] chore: Extend viewport-events test --- examples/tests/viewport-events.ts | 148 ++++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) diff --git a/examples/tests/viewport-events.ts b/examples/tests/viewport-events.ts index 19da04a7..deb14550 100644 --- a/examples/tests/viewport-events.ts +++ b/examples/tests/viewport-events.ts @@ -29,6 +29,23 @@ export default async function ({ renderer, testRoot }: ExampleSettings) { parent: testRoot, }); + const yellowStatus = renderer.createTextNode({ + text: 'Yellow Status: ', + fontSize: 30, + x: 800, + y: 10, + parent: testRoot, + }); + + const clippingStatus = renderer.createTextNode({ + text: 'Clipping: ON', + fontSize: 30, + x: 800, + y: 50, + parent: testRoot, + color: 0x00ff00ff, + }); + const boundaryRect = renderer.createNode({ x: 1920 / 2 - (1920 * 0.75) / 2, y: 1080 / 2 - (1080 * 0.75) / 2, @@ -51,6 +68,50 @@ export default async function ({ renderer, testRoot }: ExampleSettings) { parent: boundaryRect, }); + const yellow1Rect = renderer.createNode({ + x: 20, + y: 20, + alpha: 1, + width: 20, + height: 20, + color: 0xffff00ff, + pivot: 0, + parent: redRect, + }); + + const yellow2Rect = renderer.createNode({ + x: 50, + y: 50, + alpha: 1, + width: 20, + height: 20, + color: 0xffff00ff, + pivot: 0, + parent: redRect, + }); + + const yellow3Rect = renderer.createNode({ + x: 80, + y: 80, + alpha: 1, + width: 20, + height: 20, + color: 0xffff00ff, + pivot: 0, + parent: redRect, + }); + + const yellow4Rect = renderer.createNode({ + x: 110, + y: 110, + alpha: 1, + width: 20, + height: 20, + color: 0xffff00ff, + pivot: 0, + parent: redRect, + }); + redRect.on('outOfBounds', () => { console.log('red rect out of bounds'); redStatus.text = 'Red Status: rect out of bounds'; @@ -69,6 +130,84 @@ export default async function ({ renderer, testRoot }: ExampleSettings) { redStatus.color = 0xffff00ff; }); + // yellowstate + // 0 : out of bounds + // 1 : in bounds + // 2 : in viewport + const yellowRectState = [0, 0, 0, 0]; + const updateYellowState = (state: number, yellowIdx: number) => { + let stateString = ''; + yellowRectState[yellowIdx] = state; + + Array(4) + .fill(0) + .forEach((_, i) => { + stateString += `${yellowRectState[i]} `; + }); + + yellowStatus.text = `Yellow Status: ${stateString}`; + }; + + yellow1Rect.on('inBounds', () => { + console.log('yellow 1 rect inside render bounds'); + updateYellowState(1, 0); + }); + + yellow1Rect.on('inViewport', () => { + console.log('yellow 1 rect in view port'); + updateYellowState(2, 0); + }); + + yellow1Rect.on('outOfBounds', () => { + console.log('yellow 1 rect out of bounds'); + updateYellowState(0, 0); + }); + + yellow2Rect.on('inBounds', () => { + console.log('yellow 2 rect inside render bounds'); + updateYellowState(1, 1); + }); + + yellow2Rect.on('inViewport', () => { + console.log('yellow 2 rect in view port'); + updateYellowState(2, 1); + }); + + yellow2Rect.on('outOfBounds', () => { + console.log('yellow 2 rect out of bounds'); + updateYellowState(0, 1); + }); + + yellow3Rect.on('inBounds', () => { + console.log('yellow 3 rect inside render bounds'); + updateYellowState(1, 2); + }); + + yellow3Rect.on('inViewport', () => { + console.log('yellow 3 rect in view port'); + updateYellowState(2, 2); + }); + + yellow3Rect.on('outOfBounds', () => { + console.log('yellow 3 rect out of bounds'); + updateYellowState(0, 2); + }); + + yellow4Rect.on('inBounds', () => { + console.log('yellow 4 rect inside render bounds'); + updateYellowState(1, 3); + }); + + yellow4Rect.on('inViewport', () => { + console.log('yellow 4 rect in view port'); + updateYellowState(2, 3); + }); + + yellow4Rect.on('outOfBounds', () => { + console.log('yellow 4 rect out of bounds'); + updateYellowState(0, 3); + }); + const blueRect = renderer.createNode({ x: 1920 / 2 - 200, y: 100, @@ -181,5 +320,14 @@ export default async function ({ renderer, testRoot }: ExampleSettings) { redRect.x = 520; blueRect.x = 1920 / 2 - 200; } + + if (e.key === 't') { + boundaryRect.clipping = !boundaryRect.clipping; + + clippingStatus.text = boundaryRect.clipping + ? 'Clipping: ON' + : 'Clipping: OFF'; + clippingStatus.color = boundaryRect.clipping ? 0x00ff00ff : 0xff0000ff; + } }; } From 65175c630dee11a5ae17129b5debf975ff4d8a78 Mon Sep 17 00:00:00 2001 From: wouterlucas Date: Fri, 30 Aug 2024 20:21:49 +0200 Subject: [PATCH 5/8] refactor: Update setUpdateType to use local as start --- src/core/CoreNode.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/CoreNode.ts b/src/core/CoreNode.ts index 231fc34b..22c698ec 100644 --- a/src/core/CoreNode.ts +++ b/src/core/CoreNode.ts @@ -745,7 +745,7 @@ export class CoreNode extends EventEmitter { this.updateScaleRotateTransform(); this.setUpdateType( - UpdateType.Global | UpdateType.RenderBounds | UpdateType.RenderState, + UpdateType.Local | UpdateType.RenderBounds | UpdateType.RenderState, ); } From 931a8d71134c292801fe3e238c5dac37f229d764 Mon Sep 17 00:00:00 2001 From: erikhaandrikman Date: Tue, 3 Sep 2024 15:54:36 +0200 Subject: [PATCH 6/8] Remove duplicate code --- src/core/CoreNode.ts | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/src/core/CoreNode.ts b/src/core/CoreNode.ts index 22c698ec..1c25770f 100644 --- a/src/core/CoreNode.ts +++ b/src/core/CoreNode.ts @@ -959,26 +959,6 @@ export class CoreNode extends EventEmitter { const parent = this.props.parent; let childUpdateType = UpdateType.None; - 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 - ) { - this.children.forEach((child) => { - child.setUpdateType(UpdateType.All); - }); - } - if (this.updateType & UpdateType.Global) { assertTruthy(this.localTransform); From eb20b059deb6ba7beac0735028853c2e688bb9a1 Mon Sep 17 00:00:00 2001 From: erikhaandrikman Date: Tue, 3 Sep 2024 16:17:22 +0200 Subject: [PATCH 7/8] Use stage bound when clipping is disabled --- src/core/CoreNode.ts | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/src/core/CoreNode.ts b/src/core/CoreNode.ts index 1c25770f..8dc7682e 100644 --- a/src/core/CoreNode.ts +++ b/src/core/CoreNode.ts @@ -1208,25 +1208,27 @@ export class CoreNode extends EventEmitter { assertTruthy(this.stage); // no clipping, use parent's bounds - if (this.clipping === false && this.parent !== null) { - this.strictBound = - this.parent.strictBound ?? - createBound(0, 0, this.stage.root.width, this.stage.root.height); - this.preloadBound = - this.parent.preloadBound ?? this.createPreloadBounds(this.strictBound); - return; - } + if (this.clipping === false) { + if (this.parent !== null) { + this.strictBound = + this.parent.strictBound ?? + createBound(0, 0, this.stage.root.width, this.stage.root.height); + + this.preloadBound = + this.parent.preloadBound ?? + this.createPreloadBounds(this.strictBound); + return; + } else { + this.strictBound = createBound( + 0, + 0, + this.stage.root.width, + this.stage.root.height, + ); - // no parent, use stage's root bounds - if (this.parent === null) { - this.strictBound = createBound( - 0, - 0, - this.stage.root.width, - this.stage.root.height, - ); - this.preloadBound = this.createPreloadBounds(this.strictBound); - return; + this.preloadBound = this.createPreloadBounds(this.strictBound); + return; + } } // clipping is enabled create our own bounds From 3a86fe873ae712523196d27f11c60cf244be71a5 Mon Sep 17 00:00:00 2001 From: erikhaandrikman Date: Tue, 3 Sep 2024 20:26:50 +0200 Subject: [PATCH 8/8] Reordered CoreNode renderTexture test --- src/core/CoreNode.ts | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/core/CoreNode.ts b/src/core/CoreNode.ts index 8dc7682e..cc1de820 100644 --- a/src/core/CoreNode.ts +++ b/src/core/CoreNode.ts @@ -959,6 +959,26 @@ export class CoreNode extends EventEmitter { const parent = this.props.parent; let childUpdateType = UpdateType.None; + 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 + ) { + this.children.forEach((child) => { + child.setUpdateType(UpdateType.All); + }); + } + if (this.updateType & UpdateType.Global) { assertTruthy(this.localTransform); @@ -1007,26 +1027,6 @@ export class CoreNode extends EventEmitter { return; } - 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 - ) { - this.children.forEach((child) => { - child.setUpdateType(UpdateType.All); - }); - } - if (this.updateType & UpdateType.Clipping) { this.calculateClippingRect(parentClippingRect); this.setUpdateType(UpdateType.Children);