From f661e9c95be8ab9fb441677f7dd70446680f9b33 Mon Sep 17 00:00:00 2001 From: Wouter lucas van Boesschoten Date: Thu, 14 Dec 2023 20:30:19 +0100 Subject: [PATCH] Perf: Skip nodes that have no renderable props. If the node has no renderable properties it will be skipped by the render step. Saving an otherwise expensive step in the render pipeline. This will allow application / application frameworks to add nodes that do have X / Y / zIndex positioning for its children, but have no "renderable properties" that require to be drawn on the screen. For example: ``` ``` It allows you to freely manipulate the ParentsNode world position/width/etc. without it getting actually rendered. Prior to this change even ParentNode would get a GPU Render Pass, just have an empty instruction so the GPU gets the upload but then figures out it doesn't need to draw anything. By forcing ParentNode to be color=0 from the App Framework like Solid or Blits, you can have the render still do the positioning but skip the rendering portion. For added clarity, that render step is 80% of the resources spent to render that node. Thus increasing overall FPS. This helps a lot by avoiding render steps on nested nodes that do not require to be rendered, but are part of the tree for positioning / zIndex / alpha / container-ish logic. --- src/core/CoreNode.ts | 130 ++++++++++++++++++++++----------------- src/core/CoreTextNode.ts | 13 ++++ src/core/Stage.ts | 5 +- 3 files changed, 89 insertions(+), 59 deletions(-) diff --git a/src/core/CoreNode.ts b/src/core/CoreNode.ts index 1cff038b..b815b9a2 100644 --- a/src/core/CoreNode.ts +++ b/src/core/CoreNode.ts @@ -155,6 +155,7 @@ export class CoreNode extends EventEmitter implements ICoreNode { public scaleRotateTransform?: Matrix3d; public localTransform?: Matrix3d; public clippingRect: Rect | null = null; + public isRenderable = false; private parentClippingRect: Rect | null = null; public worldAlpha = 1; public premultipliedColorTl = 0; @@ -191,6 +192,7 @@ export class CoreNode extends EventEmitter implements ICoreNode { this.props.texture = texture; this.props.textureOptions = options; + this.checkIsRenderable(); // If texture is already loaded / failed, trigger loaded event manually // so that users get a consistent event experience. @@ -214,6 +216,7 @@ export class CoreNode extends EventEmitter implements ICoreNode { } this.props.texture = null; this.props.textureOptions = null; + this.checkIsRenderable(); } private onTextureLoaded: TextureLoadedEventHandler = (target, dimensions) => { @@ -243,6 +246,7 @@ export class CoreNode extends EventEmitter implements ICoreNode { const { shader, props: p } = shManager.loadShader(shaderType, props); this.props.shader = shader; this.props.shaderProps = p; + this.checkIsRenderable(); } /** @@ -310,27 +314,23 @@ export class CoreNode extends EventEmitter implements ICoreNode { const parent = this.props.parent; let childUpdateType = UpdateType.None; if (this.updateType & UpdateType.Global) { + assertTruthy(this.localTransform); + this.globalTransform = Matrix3d.copy( + parent?.globalTransform || this.localTransform, + this.globalTransform, + ); + if (parent) { - assertTruthy(this.localTransform && parent.globalTransform); - this.globalTransform = Matrix3d.copy( - parent.globalTransform, - this.globalTransform, - ).multiply(this.localTransform); - this.setUpdateType(UpdateType.Clipping | UpdateType.Children); - childUpdateType |= UpdateType.Global; - } else { - assertTruthy(this.localTransform); - this.globalTransform = Matrix3d.copy( - this.localTransform, - this.globalTransform, - ); - this.setUpdateType(UpdateType.Clipping | UpdateType.Children); - childUpdateType |= UpdateType.Global; + this.globalTransform.multiply(this.localTransform); } + + this.setUpdateType(UpdateType.Clipping | UpdateType.Children); + childUpdateType |= UpdateType.Global; } if (this.updateType & UpdateType.Clipping) { this.calculateClippingRect(parentClippingRect); + this.checkIsRenderable(); this.setUpdateType(UpdateType.Children); childUpdateType |= UpdateType.Clipping; } @@ -346,49 +346,28 @@ export class CoreNode extends EventEmitter implements ICoreNode { } if (this.updateType & UpdateType.PremultipliedColors) { - if (parent) { - this.premultipliedColorTl = mergeColorAlphaPremultiplied( - this.props.colorTl, - this.worldAlpha, - true, - ); - this.premultipliedColorTr = mergeColorAlphaPremultiplied( - this.props.colorTr, - this.worldAlpha, - true, - ); - this.premultipliedColorBl = mergeColorAlphaPremultiplied( - this.props.colorBl, - this.worldAlpha, - true, - ); - this.premultipliedColorBr = mergeColorAlphaPremultiplied( - this.props.colorBr, - this.worldAlpha, - true, - ); - } else { - this.premultipliedColorTl = mergeColorAlphaPremultiplied( - this.props.colorTl, - this.worldAlpha, - true, - ); - this.premultipliedColorTr = mergeColorAlphaPremultiplied( - this.props.colorTr, - this.worldAlpha, - true, - ); - this.premultipliedColorBl = mergeColorAlphaPremultiplied( - this.props.colorBl, - this.worldAlpha, - true, - ); - this.premultipliedColorBr = mergeColorAlphaPremultiplied( - this.props.colorBr, - this.worldAlpha, - true, - ); - } + this.premultipliedColorTl = mergeColorAlphaPremultiplied( + this.props.colorTl, + this.worldAlpha, + true, + ); + this.premultipliedColorTr = mergeColorAlphaPremultiplied( + this.props.colorTr, + this.worldAlpha, + true, + ); + this.premultipliedColorBl = mergeColorAlphaPremultiplied( + this.props.colorBl, + this.worldAlpha, + true, + ); + this.premultipliedColorBr = mergeColorAlphaPremultiplied( + this.props.colorBr, + this.worldAlpha, + true, + ); + + this.checkIsRenderable(); this.setUpdateType(UpdateType.Children); childUpdateType |= UpdateType.PremultipliedColors; } @@ -423,6 +402,41 @@ export class CoreNode extends EventEmitter implements ICoreNode { this.updateType = 0; } + // This function checks if the current node is renderable based on certain properties. + // It returns true if any of the specified properties are truthy or if any color property is not 0, otherwise it returns false. + checkIsRenderable(): boolean { + if (this.props.texture) { + return (this.isRenderable = true); + } + + if (this.props.shader) { + return (this.isRenderable = true); + } + + if (this.props.clipping) { + return (this.isRenderable = true); + } + + const colors = [ + 'color', + 'colorTop', + 'colorBottom', + 'colorLeft', + 'colorRight', + 'colorTl', + 'colorTr', + 'colorBl', + 'colorBr', + ]; + if ( + colors.some((color) => this.props[color as keyof CoreNodeProps] !== 0) + ) { + return (this.isRenderable = true); + } + + return (this.isRenderable = false); + } + /** * This function calculates the clipping rectangle for a node. * diff --git a/src/core/CoreTextNode.ts b/src/core/CoreTextNode.ts index 519f5545..63780169 100644 --- a/src/core/CoreTextNode.ts +++ b/src/core/CoreTextNode.ts @@ -151,6 +151,7 @@ export class CoreTextNode extends CoreNode implements ICoreTextNode { set text(value: string) { this.textRenderer.set.text(this.trState, value); + this.checkIsRenderable(); } get textRendererOverride(): CoreTextNodeProps['textRendererOverride'] { @@ -272,6 +273,18 @@ export class CoreTextNode extends CoreNode implements ICoreTextNode { this.textRenderer.set.y(this.trState, this.globalTransform.ty); } + override checkIsRenderable(): boolean { + if (super.checkIsRenderable()) { + return true; + } + + if (this.trState.props.text !== '') { + return (this.isRenderable = true); + } + + return (this.isRenderable = false); + } + override renderQuads(renderer: CoreRenderer) { assertTruthy(this.globalTransform); this.textRenderer.renderQuads( diff --git a/src/core/Stage.ts b/src/core/Stage.ts index faabe89b..d01b9899 100644 --- a/src/core/Stage.ts +++ b/src/core/Stage.ts @@ -220,7 +220,10 @@ export class Stage extends EventEmitter { addQuads(node: CoreNode) { assertTruthy(this.renderer && node.globalTransform); - node.renderQuads(this.renderer); + if (node.isRenderable) { + node.renderQuads(this.renderer); + } + for (let i = 0; i < node.children.length; i++) { const child = node.children[i];