Skip to content

Commit

Permalink
Introduce Texture Throttling (#472)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
wouterlucas authored Dec 19, 2024
2 parents cfe035e + 7a0fafb commit c58eda3
Show file tree
Hide file tree
Showing 33 changed files with 1,110 additions and 364 deletions.
35 changes: 30 additions & 5 deletions examples/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ const defaultPhysicalPixelRatio = 1;
const resolution = Number(urlParams.get('resolution')) || 720;
const enableInspector = urlParams.get('inspector') === 'true';
const forceWebGL2 = urlParams.get('webgl2') === 'true';
const textureProcessingLimit =
Number(urlParams.get('textureProcessingLimit')) || 0;

const physicalPixelRatio =
Number(urlParams.get('ppr')) || defaultPhysicalPixelRatio;
Expand All @@ -114,6 +116,7 @@ const defaultPhysicalPixelRatio = 1;
perfMultiplier,
enableInspector,
forceWebGL2,
textureProcessingLimit,
);
return;
}
Expand All @@ -136,6 +139,7 @@ async function runTest(
perfMultiplier: number,
enableInspector: boolean,
forceWebGL2: boolean,
textureProcessingLimit: number,
) {
const testModule = testModules[getTestPath(test)];
if (!testModule) {
Expand All @@ -157,6 +161,7 @@ async function runTest(
physicalPixelRatio,
enableInspector,
forceWebGL2,
textureProcessingLimit,
customSettings,
);

Expand All @@ -170,9 +175,13 @@ async function runTest(
parent: renderer.root,
fontSize: 50,
});
overlayText.once(
overlayText.on(
'loaded',
(target: any, { dimensions }: NodeLoadedPayload) => {
(target: any, { type, dimensions }: NodeLoadedPayload) => {
if (type !== 'text') {
return;
}

overlayText.x = renderer.settings.appWidth - dimensions.width - 20;
overlayText.y = renderer.settings.appHeight - dimensions.height - 20;
},
Expand Down Expand Up @@ -227,6 +236,7 @@ async function initRenderer(
physicalPixelRatio: number,
enableInspector: boolean,
forceWebGL2?: boolean,
textureProcessingLimit?: number,
customSettings?: Partial<RendererMainSettings>,
) {
let inspector: typeof Inspector | undefined;
Expand All @@ -246,6 +256,7 @@ async function initRenderer(
renderEngine:
renderMode === 'webgl' ? WebGlCoreRenderer : CanvasCoreRenderer,
fontEngines: [SdfTextRenderer, CanvasTextRenderer],
textureProcessingLimit: textureProcessingLimit,
...customSettings,
},
'app',
Expand Down Expand Up @@ -425,7 +436,7 @@ async function runAutomation(

// Allow some time for all images to load and the RaF to unpause
// and render if needed.
await delay(200);
await new Promise((resolve) => setTimeout(resolve, 200));
if (snapshot) {
console.log(`Calling snapshot(${testName})`);
await snapshot(testName, adjustedOptions);
Expand Down Expand Up @@ -454,6 +465,20 @@ async function runAutomation(
}
}

function delay(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
function waitForRendererIdle(renderer: RendererMain) {
return new Promise<void>((resolve) => {
let timeout: NodeJS.Timeout | undefined;
const startTimeout = () => {
timeout = setTimeout(() => {
resolve();
}, 200);
};

renderer.once('idle', () => {
if (timeout) {
clearTimeout(timeout);
}
startTimeout();
});
});
}
9 changes: 0 additions & 9 deletions examples/tests/alpha.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,6 @@ export async function automation(settings: ExampleSettings) {
}

export default async function test({ renderer, testRoot }: ExampleSettings) {
/*
* redRect will persist and change color every frame
* greenRect will persist and be detached and reattached to the root every second
* blueRect will be created and destroyed every 500 ms
*/

const parent = renderer.createNode({
x: 200,
y: 240,
Expand All @@ -55,8 +49,5 @@ export default async function test({ renderer, testRoot }: ExampleSettings) {
alpha: 1,
});

/*
* End: Sprite Map Demo
*/
console.log('ready!');
}
37 changes: 37 additions & 0 deletions examples/tests/stress-images.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { ExampleSettings } from '../common/ExampleSettings.js';

export default async function ({ renderer, testRoot }: ExampleSettings) {
const screenWidth = 1920;
const screenHeight = 1080;
const totalImages = 1000;

// Calculate the grid dimensions for square images
const gridSize = Math.ceil(Math.sqrt(totalImages)); // Approximate grid size
const imageSize = Math.floor(
Math.min(screenWidth / gridSize, screenHeight / gridSize),
); // Square size

// Create a root node for the grid
const gridNode = renderer.createNode({
x: 0,
y: 0,
width: screenWidth,
height: screenHeight,
parent: testRoot,
});

// Create and position images in the grid
new Array(totalImages).fill(0).forEach((_, i) => {
const x = (i % gridSize) * imageSize;
const y = Math.floor(i / gridSize) * imageSize;

renderer.createNode({
parent: gridNode,
x,
y,
width: imageSize,
height: imageSize,
src: `https://picsum.photos/id/${i}/${imageSize}/${imageSize}`, // Random images
});
});
}
100 changes: 100 additions & 0 deletions examples/tests/stress-mix.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import type { INode, ITextNode } from '../../dist/exports/index.js';
import type { ExampleSettings } from '../common/ExampleSettings.js';

export const Colors = {
Black: 0x000000ff,
Red: 0xff0000ff,
Green: 0x00ff00ff,
Blue: 0x0000ffff,
Magenta: 0xff00ffff,
Gray: 0x7f7f7fff,
White: 0xffffffff,
};

const textureType = ['Image', 'Color', 'Text', 'Gradient'];

const gradients = [
'colorTl',
'colorTr',
'colorBl',
'colorBr',
'colorTop',
'colorBottom',
'colorLeft',
'colorRight',
'color',
];

export default async function ({ renderer, testRoot }: ExampleSettings) {
const screenWidth = 1920;
const screenHeight = 1080;
const totalImages = 1000;

// Calculate the grid dimensions for square images
const gridSize = Math.ceil(Math.sqrt(totalImages)); // Approximate grid size
const imageSize = Math.floor(
Math.min(screenWidth / gridSize, screenHeight / gridSize),
); // Square size

// Create a root node for the grid
const gridNode = renderer.createNode({
x: 0,
y: 0,
width: screenWidth,
height: screenHeight,
parent: testRoot,
});

// Create and position images in the grid
new Array(totalImages).fill(0).forEach((_, i) => {
const x = (i % gridSize) * imageSize;
const y = Math.floor(i / gridSize) * imageSize;

// pick a random texture type
const texture = textureType[Math.floor(Math.random() * textureType.length)];

// pick a random color from Colors
const clr =
Object.values(Colors)[
Math.floor(Math.random() * Object.keys(Colors).length)
];

const node = {
parent: gridNode,
x,
y,
width: imageSize,
height: imageSize,
} as Partial<INode> | Partial<ITextNode>;

if (texture === 'Image') {
node.src = `https://picsum.photos/id/${i}/${imageSize}/${imageSize}`;
} else if (texture === 'Text') {
(node as Partial<ITextNode>).text = `Text ${i}`;
(node as Partial<ITextNode>).fontSize = 18;
node.color = clr;
} else if (texture === 'Gradient') {
const gradient = gradients[Math.floor(Math.random() * gradients.length)];
// @ts-ignore
node[gradient] = clr;

const secondGradient =
gradients[Math.floor(Math.random() * gradients.length)];
const secondColor =
Object.values(Colors)[
Math.floor(Math.random() * Object.keys(Colors).length)
];

// @ts-ignore
node[secondGradient] = secondColor;
} else {
node.color = clr;
}

if (texture === 'Text') {
renderer.createTextNode(node as ITextNode);
} else {
renderer.createNode(node);
}
});
}
53 changes: 53 additions & 0 deletions examples/tests/stress-textures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { ExampleSettings } from '../common/ExampleSettings.js';

export const Colors = {
Black: 0x000000ff,
Red: 0xff0000ff,
Green: 0x00ff00ff,
Blue: 0x0000ffff,
Magenta: 0xff00ffff,
Gray: 0x7f7f7fff,
White: 0xffffffff,
};

export default async function ({ renderer, testRoot }: ExampleSettings) {
const screenWidth = 1920;
const screenHeight = 1080;
const totalImages = 1000;

// Calculate the grid dimensions for square images
const gridSize = Math.ceil(Math.sqrt(totalImages)); // Approximate grid size
const imageSize = Math.floor(
Math.min(screenWidth / gridSize, screenHeight / gridSize),
); // Square size

// Create a root node for the grid
const gridNode = renderer.createNode({
x: 0,
y: 0,
width: screenWidth,
height: screenHeight,
parent: testRoot,
});

// Create and position images in the grid
new Array(totalImages).fill(0).forEach((_, i) => {
const x = (i % gridSize) * imageSize;
const y = Math.floor(i / gridSize) * imageSize;

// pick a random color from Colors
const clr =
Object.values(Colors)[
Math.floor(Math.random() * Object.keys(Colors).length)
];

renderer.createNode({
parent: gridNode,
x,
y,
width: imageSize,
height: imageSize,
color: clr,
});
});
}
Loading

0 comments on commit c58eda3

Please sign in to comment.