Skip to content

Commit c58eda3

Browse files
authored
Introduce Texture Throttling (#472)
Batch the amount of textures being downloaded (source) and uploaded (core ctx creation) to the GPU to reduce load on constrained devices. The default is set to `0` where everything will be processed in a single frame, this can be reduced to any value (e.g. `50` or `25`) to limit the amount of textures being processed per frame. Todo: - [x] Fix loaded events w/ dimensions - [x] Run through all the tests (+fixes) - [x] Check Canvas2D regression - [x] Align RGB / RGBA handling with hasAlpha on Texture Creates to save some bits
2 parents cfe035e + 7a0fafb commit c58eda3

33 files changed

+1110
-364
lines changed

examples/index.ts

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ const defaultPhysicalPixelRatio = 1;
9090
const resolution = Number(urlParams.get('resolution')) || 720;
9191
const enableInspector = urlParams.get('inspector') === 'true';
9292
const forceWebGL2 = urlParams.get('webgl2') === 'true';
93+
const textureProcessingLimit =
94+
Number(urlParams.get('textureProcessingLimit')) || 0;
9395

9496
const physicalPixelRatio =
9597
Number(urlParams.get('ppr')) || defaultPhysicalPixelRatio;
@@ -114,6 +116,7 @@ const defaultPhysicalPixelRatio = 1;
114116
perfMultiplier,
115117
enableInspector,
116118
forceWebGL2,
119+
textureProcessingLimit,
117120
);
118121
return;
119122
}
@@ -136,6 +139,7 @@ async function runTest(
136139
perfMultiplier: number,
137140
enableInspector: boolean,
138141
forceWebGL2: boolean,
142+
textureProcessingLimit: number,
139143
) {
140144
const testModule = testModules[getTestPath(test)];
141145
if (!testModule) {
@@ -157,6 +161,7 @@ async function runTest(
157161
physicalPixelRatio,
158162
enableInspector,
159163
forceWebGL2,
164+
textureProcessingLimit,
160165
customSettings,
161166
);
162167

@@ -170,9 +175,13 @@ async function runTest(
170175
parent: renderer.root,
171176
fontSize: 50,
172177
});
173-
overlayText.once(
178+
overlayText.on(
174179
'loaded',
175-
(target: any, { dimensions }: NodeLoadedPayload) => {
180+
(target: any, { type, dimensions }: NodeLoadedPayload) => {
181+
if (type !== 'text') {
182+
return;
183+
}
184+
176185
overlayText.x = renderer.settings.appWidth - dimensions.width - 20;
177186
overlayText.y = renderer.settings.appHeight - dimensions.height - 20;
178187
},
@@ -227,6 +236,7 @@ async function initRenderer(
227236
physicalPixelRatio: number,
228237
enableInspector: boolean,
229238
forceWebGL2?: boolean,
239+
textureProcessingLimit?: number,
230240
customSettings?: Partial<RendererMainSettings>,
231241
) {
232242
let inspector: typeof Inspector | undefined;
@@ -246,6 +256,7 @@ async function initRenderer(
246256
renderEngine:
247257
renderMode === 'webgl' ? WebGlCoreRenderer : CanvasCoreRenderer,
248258
fontEngines: [SdfTextRenderer, CanvasTextRenderer],
259+
textureProcessingLimit: textureProcessingLimit,
249260
...customSettings,
250261
},
251262
'app',
@@ -425,7 +436,7 @@ async function runAutomation(
425436

426437
// Allow some time for all images to load and the RaF to unpause
427438
// and render if needed.
428-
await delay(200);
439+
await new Promise((resolve) => setTimeout(resolve, 200));
429440
if (snapshot) {
430441
console.log(`Calling snapshot(${testName})`);
431442
await snapshot(testName, adjustedOptions);
@@ -454,6 +465,20 @@ async function runAutomation(
454465
}
455466
}
456467

457-
function delay(ms: number) {
458-
return new Promise((resolve) => setTimeout(resolve, ms));
468+
function waitForRendererIdle(renderer: RendererMain) {
469+
return new Promise<void>((resolve) => {
470+
let timeout: NodeJS.Timeout | undefined;
471+
const startTimeout = () => {
472+
timeout = setTimeout(() => {
473+
resolve();
474+
}, 200);
475+
};
476+
477+
renderer.once('idle', () => {
478+
if (timeout) {
479+
clearTimeout(timeout);
480+
}
481+
startTimeout();
482+
});
483+
});
459484
}

examples/tests/alpha.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,6 @@ export async function automation(settings: ExampleSettings) {
2626
}
2727

2828
export default async function test({ renderer, testRoot }: ExampleSettings) {
29-
/*
30-
* redRect will persist and change color every frame
31-
* greenRect will persist and be detached and reattached to the root every second
32-
* blueRect will be created and destroyed every 500 ms
33-
*/
34-
3529
const parent = renderer.createNode({
3630
x: 200,
3731
y: 240,
@@ -55,8 +49,5 @@ export default async function test({ renderer, testRoot }: ExampleSettings) {
5549
alpha: 1,
5650
});
5751

58-
/*
59-
* End: Sprite Map Demo
60-
*/
6152
console.log('ready!');
6253
}

examples/tests/stress-images.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import type { ExampleSettings } from '../common/ExampleSettings.js';
2+
3+
export default async function ({ renderer, testRoot }: ExampleSettings) {
4+
const screenWidth = 1920;
5+
const screenHeight = 1080;
6+
const totalImages = 1000;
7+
8+
// Calculate the grid dimensions for square images
9+
const gridSize = Math.ceil(Math.sqrt(totalImages)); // Approximate grid size
10+
const imageSize = Math.floor(
11+
Math.min(screenWidth / gridSize, screenHeight / gridSize),
12+
); // Square size
13+
14+
// Create a root node for the grid
15+
const gridNode = renderer.createNode({
16+
x: 0,
17+
y: 0,
18+
width: screenWidth,
19+
height: screenHeight,
20+
parent: testRoot,
21+
});
22+
23+
// Create and position images in the grid
24+
new Array(totalImages).fill(0).forEach((_, i) => {
25+
const x = (i % gridSize) * imageSize;
26+
const y = Math.floor(i / gridSize) * imageSize;
27+
28+
renderer.createNode({
29+
parent: gridNode,
30+
x,
31+
y,
32+
width: imageSize,
33+
height: imageSize,
34+
src: `https://picsum.photos/id/${i}/${imageSize}/${imageSize}`, // Random images
35+
});
36+
});
37+
}

examples/tests/stress-mix.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import type { INode, ITextNode } from '../../dist/exports/index.js';
2+
import type { ExampleSettings } from '../common/ExampleSettings.js';
3+
4+
export const Colors = {
5+
Black: 0x000000ff,
6+
Red: 0xff0000ff,
7+
Green: 0x00ff00ff,
8+
Blue: 0x0000ffff,
9+
Magenta: 0xff00ffff,
10+
Gray: 0x7f7f7fff,
11+
White: 0xffffffff,
12+
};
13+
14+
const textureType = ['Image', 'Color', 'Text', 'Gradient'];
15+
16+
const gradients = [
17+
'colorTl',
18+
'colorTr',
19+
'colorBl',
20+
'colorBr',
21+
'colorTop',
22+
'colorBottom',
23+
'colorLeft',
24+
'colorRight',
25+
'color',
26+
];
27+
28+
export default async function ({ renderer, testRoot }: ExampleSettings) {
29+
const screenWidth = 1920;
30+
const screenHeight = 1080;
31+
const totalImages = 1000;
32+
33+
// Calculate the grid dimensions for square images
34+
const gridSize = Math.ceil(Math.sqrt(totalImages)); // Approximate grid size
35+
const imageSize = Math.floor(
36+
Math.min(screenWidth / gridSize, screenHeight / gridSize),
37+
); // Square size
38+
39+
// Create a root node for the grid
40+
const gridNode = renderer.createNode({
41+
x: 0,
42+
y: 0,
43+
width: screenWidth,
44+
height: screenHeight,
45+
parent: testRoot,
46+
});
47+
48+
// Create and position images in the grid
49+
new Array(totalImages).fill(0).forEach((_, i) => {
50+
const x = (i % gridSize) * imageSize;
51+
const y = Math.floor(i / gridSize) * imageSize;
52+
53+
// pick a random texture type
54+
const texture = textureType[Math.floor(Math.random() * textureType.length)];
55+
56+
// pick a random color from Colors
57+
const clr =
58+
Object.values(Colors)[
59+
Math.floor(Math.random() * Object.keys(Colors).length)
60+
];
61+
62+
const node = {
63+
parent: gridNode,
64+
x,
65+
y,
66+
width: imageSize,
67+
height: imageSize,
68+
} as Partial<INode> | Partial<ITextNode>;
69+
70+
if (texture === 'Image') {
71+
node.src = `https://picsum.photos/id/${i}/${imageSize}/${imageSize}`;
72+
} else if (texture === 'Text') {
73+
(node as Partial<ITextNode>).text = `Text ${i}`;
74+
(node as Partial<ITextNode>).fontSize = 18;
75+
node.color = clr;
76+
} else if (texture === 'Gradient') {
77+
const gradient = gradients[Math.floor(Math.random() * gradients.length)];
78+
// @ts-ignore
79+
node[gradient] = clr;
80+
81+
const secondGradient =
82+
gradients[Math.floor(Math.random() * gradients.length)];
83+
const secondColor =
84+
Object.values(Colors)[
85+
Math.floor(Math.random() * Object.keys(Colors).length)
86+
];
87+
88+
// @ts-ignore
89+
node[secondGradient] = secondColor;
90+
} else {
91+
node.color = clr;
92+
}
93+
94+
if (texture === 'Text') {
95+
renderer.createTextNode(node as ITextNode);
96+
} else {
97+
renderer.createNode(node);
98+
}
99+
});
100+
}

examples/tests/stress-textures.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import type { ExampleSettings } from '../common/ExampleSettings.js';
2+
3+
export const Colors = {
4+
Black: 0x000000ff,
5+
Red: 0xff0000ff,
6+
Green: 0x00ff00ff,
7+
Blue: 0x0000ffff,
8+
Magenta: 0xff00ffff,
9+
Gray: 0x7f7f7fff,
10+
White: 0xffffffff,
11+
};
12+
13+
export default async function ({ renderer, testRoot }: ExampleSettings) {
14+
const screenWidth = 1920;
15+
const screenHeight = 1080;
16+
const totalImages = 1000;
17+
18+
// Calculate the grid dimensions for square images
19+
const gridSize = Math.ceil(Math.sqrt(totalImages)); // Approximate grid size
20+
const imageSize = Math.floor(
21+
Math.min(screenWidth / gridSize, screenHeight / gridSize),
22+
); // Square size
23+
24+
// Create a root node for the grid
25+
const gridNode = renderer.createNode({
26+
x: 0,
27+
y: 0,
28+
width: screenWidth,
29+
height: screenHeight,
30+
parent: testRoot,
31+
});
32+
33+
// Create and position images in the grid
34+
new Array(totalImages).fill(0).forEach((_, i) => {
35+
const x = (i % gridSize) * imageSize;
36+
const y = Math.floor(i / gridSize) * imageSize;
37+
38+
// pick a random color from Colors
39+
const clr =
40+
Object.values(Colors)[
41+
Math.floor(Math.random() * Object.keys(Colors).length)
42+
];
43+
44+
renderer.createNode({
45+
parent: gridNode,
46+
x,
47+
y,
48+
width: imageSize,
49+
height: imageSize,
50+
color: clr,
51+
});
52+
});
53+
}

0 commit comments

Comments
 (0)