Skip to content

Commit

Permalink
Performance: Modify bounds detection to inheritance when clipping is …
Browse files Browse the repository at this point in the history
…disabled (#365)

This PR changes the CoreNode update behavior, including how bounds
detection works. Instead of creating local strictBoundaries and preLoad
boundaries, the boundaries are inherited from the parent unless clipping
is enabled.

# Why?

Bounds detection added a significant CPU impact to the the rendering
loop of the L3 Renderer, causing low end devices to struggle to keep up
as the CPU would be overloaded and not in time enough to provide new
render instructions to the GPU.

Bounds detection is needed to ensure we only draw what is required on
screen / view port and do not render nodes that are outside of the view
ports bounds.

# What changed?

Previously every node would on every `update()` calculate what its
`strictBound` and `preloadBounds` where based off of its world position.
This is quite expensive to do and most of time time not needed unless
clipping is enabled on a particular node.

This changes:
- `strictBounds` and `preloadBounds` are inherited from the parent, if
the parent has no bounds the viewport stage bounds are used.
- If a node enabled clipping, only that parent will calculate it's own
`strictBound` and `preloadBound` for it's own children
- This is calculated once for all children
- Don't process anything that's not needed when out of bounds

Unrelated to bounds but performance changes that I ran into:
- Only run through clipping if clipping is enabled, previously clipping
was executed every global Transform (== expensive)
- Minor conditional checks and small performance gains

# Test results

Prior to the PR I ran two tests, a `CoreNode.update()` throughput test
(using #364) and a stress benchmark with bounds for FPS measurements.

Tested on a Ryzen 7 6800H / 3070 win11 machine using Chrome Version
128.0.6613.113 (Official Build) (64-bit) on **20x slowdown**

## Baseline

**FPS**
---------------------------------
index.ts:292 Average FPS: 30.33
index.ts:293 Median FPS: 31
index.ts:294 P01 FPS: 20
index.ts:295 P05 FPS: 25
index.ts:296 P25 FPS: 29
index.ts:297 Std Dev FPS: 2.8001964216818784
index.ts:298 Num samples: 100
index.ts:299 ---------------------------------

**Throughput**

```
┌───┬───────────┬─────────┬───────────────────┬────────┬─────────┐
│   │ Task Name │ ops/sec │ Average Time (ns) │ Margin │ Samples │
├───┼───────────┼─────────┼───────────────────┼────────┼─────────┤
│ 0 │ update    │ 126     │ 7899612.499999996 │ ±2.44% │ 64      │
└───┴───────────┴─────────┴───────────────────┴────────┴─────────┘
```

## These changes

**FPS**
---------------------------------
index.ts:292 Average FPS: 40.73
index.ts:293 Median FPS: 41
index.ts:294 P01 FPS: 33
index.ts:295 P05 FPS: 35
index.ts:296 P25 FPS: 39
index.ts:297 Std Dev FPS: 3.3700296734598636
index.ts:298 Num samples: 100
index.ts:299 ---------------------------------

**Throughput**
```
┌───┬───────────┬───────────┬───────────────────┬────────┬─────────┐
│   │ Task Name │ ops/sec   │ Average Time (ns) │ Margin │ Samples │
├───┼───────────┼───────────┼───────────────────┼────────┼─────────┤
│ 0 │ update    │ 4,295,328 │ 232.8110762198926 │ ±1.28% │ 2147665 │
└───┴───────────┴───────────┴───────────────────┴────────┴─────────┘
```

About ~10 FPS on 20x slowdown and going from 126 to 4k ops in
throughput.
  • Loading branch information
wouterlucas authored Sep 3, 2024
2 parents f147247 + 3a86fe8 commit b85cb48
Show file tree
Hide file tree
Showing 3 changed files with 261 additions and 43 deletions.
148 changes: 148 additions & 0 deletions examples/tests/viewport-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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';
Expand All @@ -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,
Expand Down Expand Up @@ -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;
}
};
}
Loading

0 comments on commit b85cb48

Please sign in to comment.