diff --git a/src/core/CoreTextureManager.ts b/src/core/CoreTextureManager.ts index af03fc3f..922b52af 100644 --- a/src/core/CoreTextureManager.ts +++ b/src/core/CoreTextureManager.ts @@ -407,8 +407,7 @@ export class CoreTextureManager extends EventEmitter { return; } - texture.setSourceState('loading'); - texture.setCoreCtxState('loading'); + texture.setState('loading'); // if we're not initialized, just queue the texture into the priority queue if (this.initialized === false) { @@ -462,6 +461,10 @@ export class CoreTextureManager extends EventEmitter { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const texture = this.priorityQueue.shift()!; texture.getTextureData().then(() => { + if (texture.state === 'failed') { + return; + } + this.uploadTexture(texture); }); } @@ -484,6 +487,10 @@ export class CoreTextureManager extends EventEmitter { const texture = this.downloadTextureSourceQueue.shift()!; queueMicrotask(() => { texture.getTextureData().then(() => { + if (texture.state === 'failed') { + return; + } + this.enqueueUploadTexture(texture); }); }); diff --git a/src/core/renderers/canvas/CanvasCoreTexture.ts b/src/core/renderers/canvas/CanvasCoreTexture.ts index 19c41125..538cfe7b 100644 --- a/src/core/renderers/canvas/CanvasCoreTexture.ts +++ b/src/core/renderers/canvas/CanvasCoreTexture.ts @@ -36,22 +36,22 @@ export class CanvasCoreTexture extends CoreContextTexture { | undefined; load(): void { - this.textureSource.setCoreCtxState('loading'); + this.textureSource.setState('loading'); this.onLoadRequest() .then((size) => { - this.textureSource.setCoreCtxState('loaded', size); + this.textureSource.setState('loaded', size); this.updateMemSize(); }) .catch((err) => { - this.textureSource.setCoreCtxState('failed', err as Error); + this.textureSource.setState('failed', err as Error); }); } free(): void { this.image = undefined; this.tintCache = undefined; - this.textureSource.setCoreCtxState('freed'); + this.textureSource.setState('freed'); this.setTextureMemUse(0); } diff --git a/src/core/renderers/webgl/WebGlCoreCtxTexture.ts b/src/core/renderers/webgl/WebGlCoreCtxTexture.ts index 49bae013..83b23032 100644 --- a/src/core/renderers/webgl/WebGlCoreCtxTexture.ts +++ b/src/core/renderers/webgl/WebGlCoreCtxTexture.ts @@ -85,12 +85,12 @@ export class WebGlCoreCtxTexture extends CoreContextTexture { } this.state = 'loading'; - this.textureSource.setCoreCtxState('loading'); + this.textureSource.setState('loading'); this._nativeCtxTexture = this.createNativeCtxTexture(); if (this._nativeCtxTexture === null) { this.state = 'failed'; - this.textureSource.setCoreCtxState( + this.textureSource.setState( 'failed', new Error('Could not create WebGL Texture'), ); @@ -110,7 +110,7 @@ export class WebGlCoreCtxTexture extends CoreContextTexture { this._h = height; // Update the texture source's width and height so that it can be used // for rendering. - this.textureSource.setCoreCtxState('loaded', { width, height }); + this.textureSource.setState('loaded', { width, height }); }) .catch((err) => { // If the texture has been freed while loading, return early. @@ -118,7 +118,7 @@ export class WebGlCoreCtxTexture extends CoreContextTexture { return; } this.state = 'failed'; - this.textureSource.setCoreCtxState('failed', err); + this.textureSource.setState('failed', err); console.error(err); }); } @@ -248,7 +248,7 @@ export class WebGlCoreCtxTexture extends CoreContextTexture { return; } this.state = 'freed'; - this.textureSource.setCoreCtxState('freed'); + this.textureSource.setState('freed'); this._w = 0; this._h = 0; if (!this._nativeCtxTexture) { diff --git a/src/core/textures/ColorTexture.ts b/src/core/textures/ColorTexture.ts index 21f926ee..563f880f 100644 --- a/src/core/textures/ColorTexture.ts +++ b/src/core/textures/ColorTexture.ts @@ -77,7 +77,7 @@ export class ColorTexture extends Texture { pixelData[3] = (this.color >>> 24) & 0xff; // Alpha } - this.setSourceState('loaded', { width: 1, height: 1 }); + this.setState('fetched', { width: 1, height: 1 }); return { data: pixelData, diff --git a/src/core/textures/ImageTexture.ts b/src/core/textures/ImageTexture.ts index 7f62adc5..0e08dde4 100644 --- a/src/core/textures/ImageTexture.ts +++ b/src/core/textures/ImageTexture.ts @@ -232,14 +232,14 @@ export class ImageTexture extends Texture { try { resp = await this.determineImageTypeAndLoadImage(); } catch (e) { - this.setSourceState('failed', e as Error); + this.setState('failed', e as Error); return { data: null, }; } if (resp.data === null) { - this.setSourceState('failed', Error('ImageTexture: No image data')); + this.setState('failed', Error('ImageTexture: No image data')); return { data: null, }; @@ -257,7 +257,7 @@ export class ImageTexture extends Texture { } // we're loaded! - this.setSourceState('loaded', { + this.setState('fetched', { width, height, }); diff --git a/src/core/textures/NoiseTexture.ts b/src/core/textures/NoiseTexture.ts index 943bf6cc..a8fd9020 100644 --- a/src/core/textures/NoiseTexture.ts +++ b/src/core/textures/NoiseTexture.ts @@ -75,7 +75,7 @@ export class NoiseTexture extends Texture { pixelData8[i + 3] = 255; } - this.setSourceState('loaded'); + this.setState('fetched'); return { data: new ImageData(pixelData8, width, height), diff --git a/src/core/textures/RenderTexture.ts b/src/core/textures/RenderTexture.ts index b3704b0b..4fd96e50 100644 --- a/src/core/textures/RenderTexture.ts +++ b/src/core/textures/RenderTexture.ts @@ -64,7 +64,7 @@ export class RenderTexture extends Texture { } override async getTextureSource(): Promise { - this.setSourceState('loaded'); + this.setState('fetched'); return { data: null, diff --git a/src/core/textures/SubTexture.ts b/src/core/textures/SubTexture.ts index 6ee552d4..e44213b1 100644 --- a/src/core/textures/SubTexture.ts +++ b/src/core/textures/SubTexture.ts @@ -17,6 +17,7 @@ * limitations under the License. */ +import type { Dimensions } from '../../common/CommonTypes.js'; import { assertTruthy } from '../../utils.js'; import type { CoreTextureManager } from '../CoreTextureManager.js'; import { ImageTexture } from './ImageTexture.js'; @@ -26,6 +27,7 @@ import { type TextureData, type TextureFailedEventHandler, type TextureLoadedEventHandler, + type TextureState, } from './Texture.js'; /** @@ -95,12 +97,10 @@ export class SubTexture extends Texture { // Resolve parent texture from cache or fallback to provided texture this.parentTexture = txManager.resolveParentTexture(this.props.texture); - if (this.parentTexture.state === 'freed') { - this.txManager.loadTexture(this.parentTexture); + if (this.renderableOwners.size > 0) { + this.parentTexture.setRenderableOwner(this, true); } - this.parentTexture.setRenderableOwner(this, true); - // If parent texture is already loaded / failed, trigger loaded event manually // so that users get a consistent event experience. // We do this in a microtask to allow listeners to be attached in the same @@ -109,12 +109,21 @@ export class SubTexture extends Texture { const parentTx = this.parentTexture; if (parentTx.state === 'loaded') { this.onParentTxLoaded(parentTx, parentTx.dimensions!); + } else if (parentTx.state === 'fetching') { + this.onParentTxFetching(); + } else if (parentTx.state === 'fetched') { + this.onParentTxFetched(); + } else if (parentTx.state === 'loading') { + this.onParentTxLoading(); } else if (parentTx.state === 'failed') { this.onParentTxFailed(parentTx, parentTx.error!); } else if (parentTx.state === 'freed') { this.onParentTxFreed(); } + parentTx.on('fetched', this.onParentTxFetched); + parentTx.on('loading', this.onParentTxLoading); + parentTx.on('fetching', this.onParentTxFetching); parentTx.on('loaded', this.onParentTxLoaded); parentTx.on('failed', this.onParentTxFailed); parentTx.on('freed', this.onParentTxFreed); @@ -124,28 +133,47 @@ export class SubTexture extends Texture { private onParentTxLoaded: TextureLoadedEventHandler = () => { // We ignore the parent's passed dimensions, and simply use the SubTexture's // configured dimensions (because that's all that matters here) - this.setSourceState('loaded', { + this.forwardParentTxState('loaded', { width: this.props.width, height: this.props.height, }); - // If the parent already has a ctxTexture, we can set the core ctx state - if (this.parentTexture.ctxTexture !== undefined) { - this.setCoreCtxState('loaded', { - width: this.props.width, - height: this.props.height, - }); - } + // free our source, if any + this.freeTextureData(); }; private onParentTxFailed: TextureFailedEventHandler = (target, error) => { - this.setSourceState('failed', error); + this.forwardParentTxState('failed', error); + this.free(); + }; + + private onParentTxFetched = () => { + this.forwardParentTxState('fetched', { + width: this.props.width, + height: this.props.height, + }); + }; + + private onParentTxFetching = () => { + this.forwardParentTxState('fetching'); + }; + + private onParentTxLoading = () => { + this.forwardParentTxState('loading'); }; private onParentTxFreed = () => { - this.setSourceState('freed'); + this.forwardParentTxState('freed'); + this.free(); }; + private forwardParentTxState( + state: TextureState, + errorOrDimensions?: Error | Dimensions, + ) { + this.setState(state, errorOrDimensions); + } + override onChangeIsRenderable(isRenderable: boolean): void { // Propagate the renderable owner change to the parent texture this.parentTexture.setRenderableOwner(this, isRenderable); @@ -153,9 +181,27 @@ export class SubTexture extends Texture { override async getTextureSource(): Promise { // Check if parent texture is loaded - return { - data: this.props, - }; + return new Promise((resolve, reject) => { + if (this.parentTexture.state === 'loaded') { + resolve({ + data: this.props, + }); + } + + this.parentTexture.once('loaded', (target, data) => { + resolve({ + data: this.props, + }); + }); + + this.parentTexture.once('failed', (target, error) => { + reject(error); + }); + + this.parentTexture.once('freed', () => { + reject(new Error('Parent texture was freed')); + }); + }); } // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/src/core/textures/Texture.ts b/src/core/textures/Texture.ts index d104dacf..01bfde22 100644 --- a/src/core/textures/Texture.ts +++ b/src/core/textures/Texture.ts @@ -102,11 +102,13 @@ export interface TextureData { } export type TextureState = - | 'initial' - | 'freed' - | 'loading' - | 'loaded' - | 'failed'; + | 'initial' // Before anything is loaded + | 'fetching' // Fetching or generating texture source + | 'fetched' // Texture source is ready + | 'loading' // Uploading to GPU + | 'loaded' // Fully loaded and usable + | 'failed' // Failed to load + | 'freed'; // Released and must be reloaded export enum TextureType { 'generic' = 0, @@ -117,27 +119,6 @@ export enum TextureType { 'subTexture' = 5, } -export interface TextureStateEventMap { - freed: TextureFreedEventHandler; - loading: TextureLoadingEventHandler; - loaded: TextureLoadedEventHandler; - failed: TextureFailedEventHandler; -} - -export type UpdateType = 'source' | 'coreCtx'; - -/** - * Like the built-in Parameters<> type but skips the first parameter (which is - * `target` currently) - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type ParametersSkipTarget any> = T extends ( - target: any, - ...args: infer P -) => any - ? P - : never; - /** * Represents a source of texture data for a CoreContextTexture. * @@ -161,10 +142,6 @@ export abstract class Texture extends EventEmitter { // aggregate state public state: TextureState = 'initial'; - // texture source state - private sourceState: TextureState = 'initial'; - // texture (gpu) state - private coreCtxState: TextureState = 'initial'; readonly renderableOwners = new Set(); @@ -223,13 +200,12 @@ export abstract class Texture extends EventEmitter { } load(): void { - if (this.sourceState === 'loaded' && this.coreCtxState === 'freed') { - // we need to load the texture data to the gpu - this.txManager.enqueueUploadTexture(this); - return; - } - - if (this.state === 'loading' || this.state === 'loaded') { + if ( + this.state === 'fetching' || + this.state === 'loading' || + this.state === 'loaded' || + this.state === 'failed' + ) { return; } @@ -269,6 +245,7 @@ export abstract class Texture extends EventEmitter { */ free(): void { this.ctxTexture?.free(); + this.textureData = null; } /** @@ -280,90 +257,28 @@ export abstract class Texture extends EventEmitter { */ freeTextureData(): void { this.textureData = null; - this.setSourceState('freed'); } - private setState( + public setState( state: TextureState, - type: UpdateType, errorOrDimensions?: Error | Dimensions, ): void { - const stateObj = type === 'source' ? 'sourceState' : 'coreCtxState'; - - if (this[stateObj] === state) { + if (this.state === state) { return; } - this[stateObj] = state; - + let payload: Error | Dimensions | null = null; if (state === 'loaded') { (this.dimensions as Dimensions) = errorOrDimensions as Dimensions; + payload = this.dimensions; } else if (state === 'failed') { (this.error as Error) = errorOrDimensions as Error; - } - - this.updateState(); - } - - /** - * Set the source state of the texture - * - * @remarks - * The source of the texture can either be generated by the texture itself or - * loaded from an external source. - * - * @param state State of the texture - * @param errorOrDimensions Error or dimensions of the texture - */ - public setSourceState( - state: TextureState, - errorOrDimensions?: Error | Dimensions, - ): void { - this.setState(state, 'source', errorOrDimensions); - } - - /** - * Set the core context state of the texture - * - * @remarks - * The core context state of the texture is the state of the texture on the GPU. - * - * @param state State of the texture - * @param errorOrDimensions Error or dimensions of the texture - */ - public setCoreCtxState( - state: TextureState, - errorOrDimensions?: Error | Dimensions, - ): void { - this.setState(state, 'coreCtx', errorOrDimensions); - } - - private updateState(): void { - const ctxState = this.coreCtxState; - const sourceState = this.sourceState; - - let newState: TextureState = 'freed'; - let payload: Error | Dimensions | null = null; - - if (sourceState === 'failed' || ctxState === 'failed') { - newState = 'failed'; - payload = this.error; // Error set by the source - } else if (sourceState === 'loading' || ctxState === 'loading') { - newState = 'loading'; - } else if (sourceState === 'loaded' && ctxState === 'loaded') { - newState = 'loaded'; - payload = this.dimensions; // Dimensions set by the source - } else { - newState = 'freed'; - } - - if (this.state === newState) { - return; + payload = this.error; } // emit the new state - this.state = newState; - this.emit(newState, payload); + this.state = state; + this.emit(state, payload); } /**