Skip to content

Commit

Permalink
feat: Add priority uploads and add default texture
Browse files Browse the repository at this point in the history
  • Loading branch information
wouterlucas committed Dec 5, 2024
1 parent bc48d6d commit 34a5e6d
Show file tree
Hide file tree
Showing 9 changed files with 144 additions and 52 deletions.
20 changes: 17 additions & 3 deletions examples/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,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 waitForRendererIdle(renderer);
if (snapshot) {
console.log(`Calling snapshot(${testName})`);
await snapshot(testName, adjustedOptions);
Expand Down Expand Up @@ -454,6 +454,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();
});
});
}
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
});
});
}
18 changes: 17 additions & 1 deletion examples/tests/stress-textures.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
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;
Expand All @@ -25,13 +35,19 @@ export default async function ({ renderer, testRoot }: ExampleSettings) {
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,
src: `https://picsum.photos/id/${i}/${imageSize}/${imageSize}`, // Random images
color: clr,
});
});
}
4 changes: 1 addition & 3 deletions src/core/CoreNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -771,9 +771,7 @@ export class CoreNode extends EventEmitter {

// load default texture if no texture is set
if (!this.props.src && !this.props.texture && !this.props.rtt) {
this.texture = this.stage.txManager.loadTexture('ColorTexture', {
color: 0xffffffff,
});
this.texture = this.stage.defaultTexture;
}
}

Expand Down
35 changes: 30 additions & 5 deletions src/core/CoreTextureManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ export class CoreTextureManager extends EventEmitter {

private downloadTextureSourceQueue: Array<ImageTexture> = [];
private uploadTextureQueue: Array<Texture> = [];
private uploadPriorityQueue: Array<Texture> = [];

private maxItemsPerFrame = 25; // Configurable limit for items to process per frame
private initialized = false;
Expand Down Expand Up @@ -363,10 +364,24 @@ export class CoreTextureManager extends EventEmitter {

/**
* Enqueue a texture for uploading to the GPU.
*
* @param texture - The texture to upload
* @param priority - Whether to prioritize this texture for upload
*/
enqueueUploadTexture(texture: Texture): void {
if (!this.uploadTextureQueue.includes(texture)) {
enqueueUploadTexture(texture: Texture, priority: boolean): void {
if (
priority === false &&
this.uploadTextureQueue.includes(texture) === false
) {
this.uploadTextureQueue.push(texture);
return;
}

if (
priority === true &&
this.uploadPriorityQueue.includes(texture) === false
) {
this.uploadPriorityQueue.push(texture);
}
}

Expand All @@ -376,6 +391,7 @@ export class CoreTextureManager extends EventEmitter {
loadTexture<Type extends keyof TextureMap>(
textureType: Type,
props: ExtractProps<TextureMap[Type]>,
priority?: boolean,
): InstanceType<TextureMap[Type]> {
const TextureClass = this.txConstructors[textureType];
if (!TextureClass) {
Expand All @@ -389,7 +405,7 @@ export class CoreTextureManager extends EventEmitter {
if (texture instanceof ImageTexture) {
this.enqueueDownloadTextureSource(texture);
} else {
this.enqueueUploadTexture(texture);
this.enqueueUploadTexture(texture, priority || false);
}

return texture as InstanceType<TextureMap[Type]>;
Expand All @@ -405,6 +421,16 @@ export class CoreTextureManager extends EventEmitter {

let itemsProcessed = 0;

// Process priority uploads
while (this.uploadPriorityQueue.length > 0 && itemsProcessed < maxItems) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const texture = this.uploadPriorityQueue.shift()!;
queueMicrotask(() => {
texture.loadCtxTexture();
});
itemsProcessed++;
}

// Process uploads
while (this.uploadTextureQueue.length > 0 && itemsProcessed < maxItems) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
Expand All @@ -428,11 +454,10 @@ export class CoreTextureManager extends EventEmitter {
texture
.getTextureData()
.then(() => {
this.enqueueUploadTexture(texture);
this.enqueueUploadTexture(texture, false);
})
.catch((err) => {
texture.setState('failed', err);
console.error(`Failed to download texture: ${texture.type}`, err);
});
});

Expand Down
23 changes: 23 additions & 0 deletions src/core/Stage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ import { santizeCustomDataMap } from '../main-api/utils.js';
import type { SdfTextRenderer } from './text-rendering/renderers/SdfTextRenderer/SdfTextRenderer.js';
import type { CanvasTextRenderer } from './text-rendering/renderers/CanvasTextRenderer.js';
import { createBound, createPreloadBounds, type Bound } from './lib/utils.js';
import type { Texture } from './textures/Texture.js';
import { ColorTexture } from './textures/ColorTexture.js';

export interface StageOptions {
appWidth: number;
Expand Down Expand Up @@ -103,6 +105,7 @@ export class Stage {
public readonly strictBound: Bound;
public readonly preloadBound: Bound;
public readonly strictBounds: boolean;
public readonly defaultTexture: Texture;

/**
* Renderer Event Bus for the Stage to emit events onto
Expand Down Expand Up @@ -155,6 +158,26 @@ export class Stage {
this.requestRender();
});

this.defaultTexture = this.txManager.loadTexture(
'ColorTexture',
{
color: 0xffffffff,
},
true,
);
assertTruthy(this.defaultTexture instanceof ColorTexture);

// Mark the default texture as ALWAYS renderable
// This prevents it from ever being cleaned up.
// Fixes https://github.com/lightning-js/renderer/issues/262
this.defaultTexture.setRenderableOwner(this, true);

// When the default texture is loaded, request a render in case the
// RAF is paused. Fixes: https://github.com/lightning-js/renderer/issues/123
this.defaultTexture.once('loaded', () => {
this.requestRender();
});

this.txMemManager = new TextureMemoryManager(this, textureMemory);
this.shManager = new CoreShaderManager();
this.animationManager = new AnimationManager();
Expand Down
31 changes: 5 additions & 26 deletions src/core/renderers/webgl/WebGlCoreRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,6 @@ export class WebGlCoreRenderer extends CoreRenderer {
/**
* White pixel texture used by default when no texture is specified.
*/
defaultTexture: Texture;

quadBufferUsage = 0;
/**
Expand All @@ -114,19 +113,6 @@ export class WebGlCoreRenderer extends CoreRenderer {

const { canvas, clearColor, bufferMemory } = options;

this.defaultTexture = new ColorTexture(this.txManager);

// Mark the default texture as ALWAYS renderable
// This prevents it from ever being cleaned up.
// Fixes https://github.com/lightning-js/renderer/issues/262
this.defaultTexture.setRenderableOwner(this, true);

// When the default texture is loaded, request a render in case the
// RAF is paused. Fixes: https://github.com/lightning-js/renderer/issues/123
this.defaultTexture.once('loaded', () => {
this.stage.requestRender();
});

const gl = createWebGLContext(
canvas,
options.forceWebGL2,
Expand Down Expand Up @@ -237,7 +223,10 @@ export class WebGlCoreRenderer extends CoreRenderer {
*/
addQuad(params: QuadOptions) {
const { fQuadBuffer, uiQuadBuffer } = this;
let texture = params.texture || this.defaultTexture;
let texture = params.texture;

assertTruthy(texture !== null, 'Texture is required');
assertTruthy(texture.ctxTexture !== undefined, 'Invalid texture type');

/**
* If the shader props contain any automatic properties, update it with the
Expand All @@ -255,12 +244,6 @@ export class WebGlCoreRenderer extends CoreRenderer {
}
}

// assertTruthy(texture.ctxTexture !== undefined, 'Invalid texture type');
if (!texture.ctxTexture) {
console.warn('Invalid texture type', texture);
return;
}

let { curBufferIdx: bufferIdx, curRenderOp } = this;
const targetDims = { width: -1, height: -1 };
targetDims.width = params.width;
Expand Down Expand Up @@ -356,11 +339,7 @@ export class WebGlCoreRenderer extends CoreRenderer {
}

const ctxTexture = texture.ctxTexture as WebGlCoreCtxTexture;
if (!ctxTexture) {
console.warn('Invalid texture type', texture);
return;
}

assertTruthy(ctxTexture instanceof WebGlCoreCtxTexture);
const textureIdx = this.addTexture(ctxTexture, bufferIdx);

assertTruthy(this.curRenderOp !== null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,27 +77,25 @@ export class SdfTrFontFace<
);

// Load image
this.texture = stage.txManager.loadTexture('ImageTexture', {
src: atlasUrl,
// IMPORTANT: The SDF shader requires the alpha channel to NOT be
// premultiplied on the atlas texture. If it is premultiplied, the
// rendering of SDF glyphs (especially single-channel SDF fonts) will
// be very jagged.
premultiplyAlpha: false,
});
this.texture = stage.txManager.loadTexture(
'ImageTexture',
{
src: atlasUrl,
// IMPORTANT: The SDF shader requires the alpha channel to NOT be
// premultiplied on the atlas texture. If it is premultiplied, the
// rendering of SDF glyphs (especially single-channel SDF fonts) will
// be very jagged.
premultiplyAlpha: false,
},
true,
);

this.texture.on('loaded', () => {
this.checkLoaded();
// Make sure we mark the stage for a re-render (in case the font's texture was freed and reloaded)
stage.requestRender();
});

// // Pre-load it
// this should be done automatically with throttling
// stage.txManager.once('initialized', () => {
// this.texture.ctxTexture.load();
// });

// Set this.data to the fetched data from dataUrl
fetch(atlasDataUrl)
.then(async (response) => {
Expand Down
2 changes: 2 additions & 0 deletions src/core/textures/ColorTexture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ export class ColorTexture extends Texture {
pixelData[3] = (this.color >>> 24) & 0xff; // Alpha
}

this.setState('loaded', { width: 1, height: 1 });

return {
data: pixelData,
premultiplyAlpha: true,
Expand Down

0 comments on commit 34a5e6d

Please sign in to comment.