diff --git a/examples/tests/textures.ts b/examples/tests/textures.ts index 60009545..32bfc1e2 100644 --- a/examples/tests/textures.ts +++ b/examples/tests/textures.ts @@ -78,7 +78,7 @@ export default async function test({ renderer, testRoot }: ExampleSettings) { await execLoadingTest(elevator, 200, 268); - // Test: Check that we capture a texture load failure + // // Test: Check that we capture a texture load failure const failure = renderer.createNode({ x: curX, y: curY, @@ -88,7 +88,7 @@ export default async function test({ renderer, testRoot }: ExampleSettings) { await execFailureTest(failure); - // Test: Check that we capture a texture load failure + // // Test: Check that we capture a texture load failure const failure2 = renderer.createNode({ x: curX, y: curY, @@ -98,7 +98,7 @@ export default async function test({ renderer, testRoot }: ExampleSettings) { await execFailureTest(failure2); - // Test: NoiseTexture + // // Test: NoiseTexture curX = renderer.settings.appWidth / 2; curY = BEGIN_Y; diff --git a/src/core/CoreNode.ts b/src/core/CoreNode.ts index 2e2c9845..41cdb9d9 100644 --- a/src/core/CoreNode.ts +++ b/src/core/CoreNode.ts @@ -1412,7 +1412,7 @@ export class CoreNode extends EventEmitter { // this only needs to happen once or until the texture is no longer loaded if ( this.texture !== null && - this.texture.state !== 'loaded' && + this.texture.state === 'freed' && this.renderState > CoreNodeRenderState.OutOfBounds ) { this.stage.txManager.loadTexture(this.texture); diff --git a/src/core/CoreTextureManager.ts b/src/core/CoreTextureManager.ts index f0165b54..d50fa9d8 100644 --- a/src/core/CoreTextureManager.ts +++ b/src/core/CoreTextureManager.ts @@ -366,6 +366,13 @@ export class CoreTextureManager extends EventEmitter { * @param immediate - Whether to prioritize the texture for immediate loading */ loadTexture(texture: Texture, priority?: boolean): void { + if (texture.state === 'loaded' || texture.state === 'loading') { + return; + } + + texture.setSourceState('loading'); + texture.setCoreCtxState('loading'); + // prioritize the texture for immediate loading if (priority === true) { texture @@ -415,14 +422,9 @@ export class CoreTextureManager extends EventEmitter { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const texture = this.downloadTextureSourceQueue.shift()!; queueMicrotask(() => { - texture - .getTextureData() - .then(() => { - this.enqueueUploadTexture(texture); - }) - .catch((err) => { - console.error(err); - }); + texture.getTextureData().then(() => { + this.enqueueUploadTexture(texture); + }); }); itemsProcessed++; @@ -436,12 +438,27 @@ export class CoreTextureManager extends EventEmitter { ); } - private initTextureToCache(texture: Texture, cacheKey: string) { + /** + * Initialize a texture to the cache + * + * @param texture Texture to cache + * @param cacheKey Cache key for the texture + */ + initTextureToCache(texture: Texture, cacheKey: string) { const { keyCache, inverseKeyCache } = this; keyCache.set(cacheKey, texture); inverseKeyCache.set(texture, cacheKey); } + /** + * Get a texture from the cache + * + * @param cacheKey + */ + getTextureFromCache(cacheKey: string): Texture | undefined { + return this.keyCache.get(cacheKey); + } + /** * Remove a texture from the cache * @@ -457,4 +474,22 @@ export class CoreTextureManager extends EventEmitter { keyCache.delete(cacheKey); } } + + /** + * Resolve a parent texture from the cache or fallback to the provided texture. + * + * @param texture - The provided texture to resolve. + * @returns The cached or provided texture. + */ + resolveParentTexture(texture: ImageTexture): Texture { + if (!texture?.props) { + return texture; + } + + const cacheKey = ImageTexture.makeCacheKey(texture.props); + const cachedTexture = cacheKey + ? this.getTextureFromCache(cacheKey) + : undefined; + return cachedTexture ?? texture; + } } diff --git a/src/core/textures/ImageTexture.ts b/src/core/textures/ImageTexture.ts index 07382b62..84d563c7 100644 --- a/src/core/textures/ImageTexture.ts +++ b/src/core/textures/ImageTexture.ts @@ -119,7 +119,7 @@ export interface ImageTextureProps { * {@link ImageTextureProps.premultiplyAlpha} prop to `false`. */ export class ImageTexture extends Texture { - props: Required; + public props: Required; public override type: TextureType = TextureType.image; @@ -134,7 +134,7 @@ export class ImageTexture extends Texture { async loadImageFallback(src: string, hasAlpha: boolean) { const img = new Image(); - if (!(src.startsWith('data:'))) { + if (!src.startsWith('data:')) { img.crossOrigin = 'Anonymous'; } @@ -230,7 +230,15 @@ export class ImageTexture extends Texture { } override async getTextureSource(): Promise { - const resp = await this.determineImageType(); + let resp; + try { + resp = await this.determineImageTypeAndLoadImage(); + } catch (e) { + this.setSourceState('failed', e as Error); + return { + data: null, + }; + } if (resp.data === null) { this.setSourceState('failed', Error('ImageTexture: No image data')); @@ -261,7 +269,7 @@ export class ImageTexture extends Texture { }; } - determineImageType() { + determineImageTypeAndLoadImage() { const { src, premultiplyAlpha, type } = this.props; if (src === null) { return { diff --git a/src/core/textures/SubTexture.ts b/src/core/textures/SubTexture.ts index b3612095..6eb5aaf8 100644 --- a/src/core/textures/SubTexture.ts +++ b/src/core/textures/SubTexture.ts @@ -17,7 +17,9 @@ * limitations under the License. */ +import { assertTruthy } from '../../utils.js'; import type { CoreTextureManager } from '../CoreTextureManager.js'; +import { ImageTexture } from './ImageTexture.js'; import { Texture, TextureType, @@ -83,7 +85,21 @@ export class SubTexture extends Texture { constructor(txManager: CoreTextureManager, props: SubTextureProps) { super(txManager); this.props = SubTexture.resolveDefaults(props || {}); - this.parentTexture = this.props.texture; + + assertTruthy(this.props.texture, 'SubTexture requires a parent texture'); + assertTruthy( + this.props.texture instanceof ImageTexture, + 'SubTexture requires an ImageTexture parent', + ); + + // 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); + } + + this.parentTexture.setRenderableOwner(this, true); // If parent texture is already loaded / failed, trigger loaded event manually // so that users get a consistent event experience. @@ -121,10 +137,6 @@ export class SubTexture extends Texture { override async getTextureSource(): Promise { // Check if parent texture is loaded - if (this.parentTexture.state !== 'loaded') { - await this.txManager.loadTexture(this.parentTexture); - } - return { data: this.props, }; diff --git a/src/core/textures/Texture.ts b/src/core/textures/Texture.ts index 0e332209..281cfabb 100644 --- a/src/core/textures/Texture.ts +++ b/src/core/textures/Texture.ts @@ -316,18 +316,21 @@ export abstract class Texture extends EventEmitter { let newState: TextureState = 'freed'; let payload: Error | Dimensions | null = null; + if (sourceState === 'failed' || ctxState === 'failed') { newState = 'failed'; - - // If the texture failed to load, the error is set by the source - payload = this.error; + payload = this.error; // Error set by the source } else if (sourceState === 'loading' || ctxState === 'loading') { newState = 'loading'; - } else if (this.sourceState === 'loaded' && ctxState === 'loaded') { + } else if (sourceState === 'loaded' && ctxState === 'loaded') { newState = 'loaded'; - - // If the texture is loaded, the dimensions are set by the source - payload = this.dimensions; + payload = this.dimensions; // Dimensions set by the source + } else if ( + (sourceState === 'loaded' && ctxState === 'freed') || + (ctxState === 'loaded' && sourceState === 'freed') + ) { + // If one is loaded and the other is freed, then we are in a loading state + newState = 'loading'; } else { newState = 'freed'; }