Skip to content

Commit

Permalink
Perf: Skip nodes that have no renderable props.
Browse files Browse the repository at this point in the history
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:
```
<ParentNode x=100 y=200 width=500 height=500 zIndex=2>
  <ChildNode x=10 y=10 src="..image">
  <ChildNode ...>
</ParentNode>
```

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.
  • Loading branch information
wouterlucas committed Dec 14, 2023
1 parent 7f5913c commit 03bf123
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 59 deletions.
130 changes: 72 additions & 58 deletions src/core/CoreNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -214,11 +215,13 @@ export class CoreNode extends EventEmitter implements ICoreNode {
}
this.props.texture = null;
this.props.textureOptions = null;
this.checkIsRenderable();
}

private onTextureLoaded: TextureLoadedEventHandler = (target, dimensions) => {
// Texture was loaded. In case the RAF loop has already stopped, we request
// a render to ensure the texture is rendered.
this.checkIsRenderable();
this.stage.requestRender();
this.emit('loaded', {
type: 'texture',
Expand All @@ -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();
}

/**
Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
}
Expand Down Expand Up @@ -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.
*
Expand Down
13 changes: 13 additions & 0 deletions src/core/CoreTextNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'] {
Expand Down Expand Up @@ -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(
Expand Down
5 changes: 4 additions & 1 deletion src/core/Stage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];

Expand Down

0 comments on commit 03bf123

Please sign in to comment.