Skip to content

Commit

Permalink
fix: SubTexture caching & loading, duplicate textures, image error ha…
Browse files Browse the repository at this point in the history
…ndling
  • Loading branch information
wouterlucas committed Dec 15, 2024
1 parent 93e5473 commit c676746
Show file tree
Hide file tree
Showing 6 changed files with 87 additions and 29 deletions.
6 changes: 3 additions & 3 deletions examples/tests/textures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/core/CoreNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
53 changes: 44 additions & 9 deletions src/core/CoreTextureManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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++;
Expand All @@ -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
*
Expand All @@ -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;
}
}
16 changes: 12 additions & 4 deletions src/core/textures/ImageTexture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ export interface ImageTextureProps {
* {@link ImageTextureProps.premultiplyAlpha} prop to `false`.
*/
export class ImageTexture extends Texture {
props: Required<ImageTextureProps>;
public props: Required<ImageTextureProps>;

public override type: TextureType = TextureType.image;

Expand All @@ -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';
}

Expand Down Expand Up @@ -230,7 +230,15 @@ export class ImageTexture extends Texture {
}

override async getTextureSource(): Promise<TextureData> {
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'));
Expand Down Expand Up @@ -261,7 +269,7 @@ export class ImageTexture extends Texture {
};
}

determineImageType() {
determineImageTypeAndLoadImage() {
const { src, premultiplyAlpha, type } = this.props;
if (src === null) {
return {
Expand Down
22 changes: 17 additions & 5 deletions src/core/textures/SubTexture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -121,10 +137,6 @@ export class SubTexture extends Texture {

override async getTextureSource(): Promise<TextureData> {
// Check if parent texture is loaded
if (this.parentTexture.state !== 'loaded') {
await this.txManager.loadTexture(this.parentTexture);
}

return {
data: this.props,
};
Expand Down
17 changes: 10 additions & 7 deletions src/core/textures/Texture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}
Expand Down

0 comments on commit c676746

Please sign in to comment.