From a0a90bebd774772047581ed3c6cd4f3b72f7ca27 Mon Sep 17 00:00:00 2001 From: wouterlucas Date: Mon, 30 Jun 2025 19:14:26 +0200 Subject: [PATCH] fix: enhance texture loading --- src/core/CoreTextNode.ts | 4 +- src/core/CoreTextureManager.ts | 37 ++++++-- src/core/Stage.ts | 9 +- src/core/renderers/CoreContextTexture.ts | 2 +- .../renderers/canvas/CanvasCoreTexture.ts | 22 ++--- .../webgl/WebGlCoreCtxRenderTexture.ts | 5 + .../renderers/webgl/WebGlCoreCtxTexture.ts | 92 +++++++++++-------- src/main-api/Renderer.ts | 2 +- 8 files changed, 106 insertions(+), 67 deletions(-) diff --git a/src/core/CoreTextNode.ts b/src/core/CoreTextNode.ts index 3c7f0a15..aa8f3b7b 100644 --- a/src/core/CoreTextNode.ts +++ b/src/core/CoreTextNode.ts @@ -84,8 +84,8 @@ export class CoreTextNode extends CoreNode implements CoreTextNodeProps { this._textRendererOverride = props.textRendererOverride; this.textRenderer = textRenderer; const textRendererState = this.createState({ - x: this.absX, - y: this.absY, + x: 0, + y: 0, width: props.width, height: props.height, textAlign: props.textAlign, diff --git a/src/core/CoreTextureManager.ts b/src/core/CoreTextureManager.ts index 206dd446..4bd3ef8f 100644 --- a/src/core/CoreTextureManager.ts +++ b/src/core/CoreTextureManager.ts @@ -389,12 +389,18 @@ export class CoreTextureManager extends EventEmitter { // For non-image textures, upload immediately if (texture.type !== TextureType.image) { - this.uploadTexture(texture); + this.uploadTexture(texture).catch((err) => { + console.error('Failed to upload non-image texture:', err); + texture.setState('failed'); + }); } else { // For image textures, queue for throttled upload // If it's a priority texture, upload it immediately if (priority === true) { - this.uploadTexture(texture); + this.uploadTexture(texture).catch((err) => { + console.error('Failed to upload priority texture:', err); + texture.setState('failed'); + }); } else { this.enqueueUploadTexture(texture); } @@ -410,8 +416,9 @@ export class CoreTextureManager extends EventEmitter { * Upload a texture to the GPU * * @param texture Texture to upload + * @returns Promise that resolves when the texture is fully loaded */ - uploadTexture(texture: Texture): void { + async uploadTexture(texture: Texture): Promise { if ( this.stage.txMemManager.doNotExceedCriticalThreshold === true && this.stage.txMemManager.criticalCleanupRequested === true @@ -427,7 +434,7 @@ export class CoreTextureManager extends EventEmitter { return; } - coreContext.load(); + await coreContext.load(); } /** @@ -442,7 +449,7 @@ export class CoreTextureManager extends EventEmitter { * * @param maxProcessingTime - The maximum processing time in milliseconds */ - processSome(maxProcessingTime: number): void { + async processSome(maxProcessingTime: number): Promise { if (this.initialized === false) { return; } @@ -456,18 +463,28 @@ export class CoreTextureManager extends EventEmitter { ) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const texture = this.priorityQueue.pop()!; - texture.getTextureData().then(() => { - this.uploadTexture(texture); - }); + try { + await texture.getTextureData(); + await this.uploadTexture(texture); + } catch (error) { + console.error('Failed to process priority texture:', error); + // Continue with next texture instead of stopping entire queue + } } - // Process uploads + // Process uploads - await each upload to prevent GPU overload while ( this.uploadTextureQueue.length > 0 && getTimeStamp() - startTime < maxProcessingTime ) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.uploadTexture(this.uploadTextureQueue.shift()!); + const texture = this.uploadTextureQueue.shift()!; + try { + await this.uploadTexture(texture); + } catch (error) { + console.error('Failed to upload texture:', error); + // Continue with next texture instead of stopping entire queue + } } } diff --git a/src/core/Stage.ts b/src/core/Stage.ts index 4c710be9..59e9f4e1 100644 --- a/src/core/Stage.ts +++ b/src/core/Stage.ts @@ -372,8 +372,13 @@ export class Stage { this.root.update(this.deltaTime, this.root.clippingRect); } - // Process some textures - this.txManager.processSome(this.options.textureProcessingTimeLimit); + // Process some textures asynchronously but don't block the frame + // Use a background task to prevent frame drops + this.txManager + .processSome(this.options.textureProcessingTimeLimit) + .catch((err) => { + console.error('Error processing textures:', err); + }); // Reset render operations and clear the canvas renderer.reset(); diff --git a/src/core/renderers/CoreContextTexture.ts b/src/core/renderers/CoreContextTexture.ts index d300844d..f00f9f91 100644 --- a/src/core/renderers/CoreContextTexture.ts +++ b/src/core/renderers/CoreContextTexture.ts @@ -34,7 +34,7 @@ export abstract class CoreContextTexture { this.memManager.setTextureMemUse(this.textureSource, byteSize); } - abstract load(): void; + abstract load(): Promise; abstract free(): void; get renderable(): boolean { diff --git a/src/core/renderers/canvas/CanvasCoreTexture.ts b/src/core/renderers/canvas/CanvasCoreTexture.ts index bf7a962e..9e73b31a 100644 --- a/src/core/renderers/canvas/CanvasCoreTexture.ts +++ b/src/core/renderers/canvas/CanvasCoreTexture.ts @@ -35,19 +35,19 @@ export class CanvasCoreTexture extends CoreContextTexture { } | undefined; - load(): void { + async load(): Promise { this.textureSource.setState('loading'); - this.onLoadRequest() - .then((size) => { - this.textureSource.setState('loaded', size); - this.textureSource.freeTextureData(); - this.updateMemSize(); - }) - .catch((err) => { - this.textureSource.setState('failed', err as Error); - this.textureSource.freeTextureData(); - }); + try { + const size = await this.onLoadRequest(); + this.textureSource.setState('loaded', size); + this.textureSource.freeTextureData(); + this.updateMemSize(); + } catch (err) { + this.textureSource.setState('failed', err as Error); + this.textureSource.freeTextureData(); + throw err; + } } free(): void { diff --git a/src/core/renderers/webgl/WebGlCoreCtxRenderTexture.ts b/src/core/renderers/webgl/WebGlCoreCtxRenderTexture.ts index 589c2bdd..072a7f17 100644 --- a/src/core/renderers/webgl/WebGlCoreCtxRenderTexture.ts +++ b/src/core/renderers/webgl/WebGlCoreCtxRenderTexture.ts @@ -41,6 +41,11 @@ export class WebGlCoreCtxRenderTexture extends WebGlCoreCtxTexture { const { glw } = this; const nativeTexture = (this._nativeCtxTexture = this.createNativeCtxTexture()); + + if (!nativeTexture) { + throw new Error('Failed to create native texture for RenderTexture'); + } + const { width, height } = this.textureSource; // Create Framebuffer object diff --git a/src/core/renderers/webgl/WebGlCoreCtxTexture.ts b/src/core/renderers/webgl/WebGlCoreCtxTexture.ts index 5e5a046e..abb3b4e8 100644 --- a/src/core/renderers/webgl/WebGlCoreCtxTexture.ts +++ b/src/core/renderers/webgl/WebGlCoreCtxTexture.ts @@ -78,54 +78,65 @@ export class WebGlCoreCtxTexture extends CoreContextTexture { * to force the texture to be pre-loaded prior to accessing the ctxTexture * property. */ - load() { - // If the texture is already loading or loaded, don't load it again. + async load(): Promise { + // If the texture is already loading or loaded, return resolved promise if (this.state === 'loading' || this.state === 'loaded') { - return; + return Promise.resolve(); } this.state = 'loading'; this.textureSource.setState('loading'); + + // Await the native texture creation to ensure GPU buffer is fully allocated this._nativeCtxTexture = this.createNativeCtxTexture(); if (this._nativeCtxTexture === null) { this.state = 'failed'; - this.textureSource.setState( - 'failed', - new Error('Could not create WebGL Texture'), - ); + const error = new Error('Could not create WebGL Texture'); + this.textureSource.setState('failed', error); console.error('Could not create WebGL Texture'); - return; + throw error; } - this.onLoadRequest() - .then(({ width, height }) => { - // If the texture has been freed while loading, return early. - if (this.state === 'freed') { - return; - } - - this.state = 'loaded'; - this._w = width; - this._h = height; - // Update the texture source's width and height so that it can be used - // for rendering. - this.textureSource.setState('loaded', { width, height }); - - // cleanup source texture data - this.textureSource.freeTextureData(); - }) - .catch((err) => { - // If the texture has been freed while loading, return early. - if (this.state === 'freed') { - return; - } - - this.state = 'failed'; - this.textureSource.setState('failed', err); + try { + const { width, height } = await this.onLoadRequest(); + + // If the texture has been freed while loading, return early. + // Type assertion needed because state could change during async operations + if ((this.state as string) === 'freed') { + return; + } + + this.state = 'loaded'; + this._w = width; + this._h = height; + // Update the texture source's width and height so that it can be used + // for rendering. + this.textureSource.setState('loaded', { width, height }); + + // cleanup source texture data next tick + // This is done using queueMicrotask to ensure it runs after the current + // event loop tick, allowing the texture to be fully loaded and bound + // to the GL context before freeing the source data. + // This is important to avoid issues with the texture data being + // freed while the texture is still being loaded or used. + queueMicrotask(() => { this.textureSource.freeTextureData(); - console.error(err); }); + } catch (err: unknown) { + // If the texture has been freed while loading, return early. + // Type assertion needed because state could change during async operations + if ((this.state as string) === 'freed') { + return; + } + + this.state = 'failed'; + const error = err instanceof Error ? err : new Error(String(err)); + this.textureSource.setState('failed', error); + this.textureSource.freeTextureData(); + console.error(err); + throw error; // Re-throw to propagate the error + } } /** @@ -268,17 +279,17 @@ export class WebGlCoreCtxTexture extends CoreContextTexture { } /** - * Create native context texture + * Create native context texture asynchronously * * @remarks - * When this method returns the returned texture will be bound to the GL context state. + * When this method resolves, the returned texture will be bound to the GL context state + * and fully ready for use. This ensures proper GPU resource allocation timing. * - * @param width - * @param height - * @returns + * @returns Promise that resolves to the native WebGL texture or null on failure */ - protected createNativeCtxTexture() { + protected createNativeCtxTexture(): WebGLTexture | null { const { glw } = this; + const nativeTexture = glw.createTexture(); if (!nativeTexture) { return null; @@ -296,6 +307,7 @@ export class WebGlCoreCtxTexture extends CoreContextTexture { // texture wrapping method glw.texParameteri(glw.TEXTURE_WRAP_S, glw.CLAMP_TO_EDGE); glw.texParameteri(glw.TEXTURE_WRAP_T, glw.CLAMP_TO_EDGE); + return nativeTexture; } } diff --git a/src/main-api/Renderer.ts b/src/main-api/Renderer.ts index 97ad65c8..bdd57b59 100644 --- a/src/main-api/Renderer.ts +++ b/src/main-api/Renderer.ts @@ -412,7 +412,7 @@ export class RendererMain extends EventEmitter { quadBufferSize: settings.quadBufferSize ?? 4 * 1024 * 1024, fontEngines: settings.fontEngines, strictBounds: settings.strictBounds ?? true, - textureProcessingTimeLimit: settings.textureProcessingTimeLimit || 10, + textureProcessingTimeLimit: settings.textureProcessingTimeLimit || 42, canvas: settings.canvas || document.createElement('canvas'), createImageBitmapSupport: settings.createImageBitmapSupport || 'full', };