Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions src/core/CoreTextureManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,12 +281,21 @@ export class CoreTextureManager extends EventEmitter {
);
} else {
console.warn(
'[Lightning] Imageworker is 0 or not supported on this browser. Image loading will be slower.',
'[Lightning] Image worker count is 0 or workers are not supported on this browser. Image loading will be slower.',
);
}

this.initialized = true;
this.emit('initialized');

// Anything that arrived before initialization completed is now safe to
// process. Without this, queued textures would sit until the next frame
// tick happens to call processSome().
if (this.uploadTextureQueue.size > 0) {
this.processSome(Infinity).catch((err) => {
console.error('Failed to drain pre-init texture queue:', err);
});
}
}

/**
Expand All @@ -295,6 +304,9 @@ export class CoreTextureManager extends EventEmitter {
* @param texture - The texture to upload
*/
enqueueUploadTexture(texture: Texture): void {
if (texture.state === 'failed' || texture.state === 'freed') {
return;
}
this.uploadTextureQueue.add(texture);
}

Expand Down Expand Up @@ -381,7 +393,10 @@ export class CoreTextureManager extends EventEmitter {
console.error(`Failed to upload texture:`, err);
texture.setState(
'failed',
new TextureError(TextureErrorCode.TEXTURE_DATA_NULL),
new TextureError(
TextureErrorCode.TEXTURE_UPLOAD_FAILED,
err instanceof Error ? err.message : undefined,
),
);
});
return;
Expand Down Expand Up @@ -432,7 +447,7 @@ export class CoreTextureManager extends EventEmitter {
}

const coreContext = texture.loadCtxTexture();
if (coreContext !== null && coreContext.state === 'loaded') {
if (coreContext.state === 'loaded') {
texture.setState('loaded');
return;
}
Expand Down Expand Up @@ -468,6 +483,12 @@ export class CoreTextureManager extends EventEmitter {
const [texture] = this.uploadTextureQueue;
if (!texture) break;
this.uploadTextureQueue.delete(texture);

// Skip textures that were freed or failed between enqueue and now.
if (texture.state === 'failed' || texture.state === 'freed') {
continue;
}

try {
if (texture.textureData === null) {
await texture.getTextureData();
Expand Down
2 changes: 2 additions & 0 deletions src/core/TextureError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ export enum TextureErrorCode {
MEMORY_THRESHOLD_EXCEEDED = 'MEMORY_THRESHOLD_EXCEEDED',
TEXTURE_DATA_NULL = 'TEXTURE_DATA_NULL',
TEXTURE_TYPE_NOT_REGISTERED = 'TEXTURE_TYPE_NOT_REGISTERED',
TEXTURE_UPLOAD_FAILED = 'TEXTURE_UPLOAD_FAILED',
}

const defaultMessages: Record<TextureErrorCode, string> = {
[TextureErrorCode.MEMORY_THRESHOLD_EXCEEDED]: 'Memory threshold exceeded',
[TextureErrorCode.TEXTURE_DATA_NULL]: 'Texture data is null',
[TextureErrorCode.TEXTURE_TYPE_NOT_REGISTERED]:
'Texture type is not registered',
[TextureErrorCode.TEXTURE_UPLOAD_FAILED]: 'Texture upload failed',
};

export class TextureError extends Error {
Expand Down
77 changes: 50 additions & 27 deletions src/core/lib/ImageWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ function createImageWorker() {

var blob = xhr.response;
var withAlphaChannel =
premultiplyAlpha !== undefined
premultiplyAlpha !== undefined && premultiplyAlpha !== null
? premultiplyAlpha
: hasAlphaChannel(blob.type);

Expand All @@ -83,21 +83,21 @@ function createImageWorker() {
imageOrientation: 'none',
})
.then(function (data) {
resolve({ data, premultiplyAlpha: premultiplyAlpha });
resolve({ data: data, premultiplyAlpha: withAlphaChannel });
})
.catch(function (error) {
reject(error);
});
return;
} else if (
supportsOptionsCreateImageBitmap === false &&
supportsOptionsCreateImageBitmap === false
supportsFullCreateImageBitmap === false
) {
// Fallback for browsers that do not support createImageBitmap with options
// this is supported for Chrome v50 to v52/54 that doesn't support options
createImageBitmap(blob)
.then(function (data) {
resolve({ data, premultiplyAlpha: premultiplyAlpha });
resolve({ data: data, premultiplyAlpha: withAlphaChannel });
})
.catch(function (error) {
reject(error);
Expand All @@ -109,7 +109,7 @@ function createImageWorker() {
imageOrientation: 'none',
})
.then(function (data) {
resolve({ data, premultiplyAlpha: premultiplyAlpha });
resolve({ data: data, premultiplyAlpha: withAlphaChannel });
})
.catch(function (error) {
reject(error);
Expand Down Expand Up @@ -156,7 +156,6 @@ function createImageWorker() {
/* eslint-enable */

export class ImageWorkerManager {
imageWorkersEnabled = true;
messageManager: Record<number, MessageCallback> = {};
workers: Worker[] = [];
workerLoad: number[] = [];
Expand All @@ -172,6 +171,8 @@ export class ImageWorkerManager {
);
this.workers.forEach((worker, index) => {
worker.onmessage = (event) => this.handleMessage(event, index);
worker.onerror = (event) => this.handleWorkerError(event, index);
worker.onmessageerror = (event) => this.handleWorkerError(event, index);
});
}

Expand All @@ -194,6 +195,25 @@ export class ImageWorkerManager {
}
}

private handleWorkerError(event: Event | ErrorEvent, workerIndex: number) {
const message =
event instanceof ErrorEvent && event.message
? event.message
: 'Image worker encountered an unrecoverable error';

// Reject all pending requests; we cannot map a worker-level crash to a
// specific message id, so fail everything outstanding to avoid hangs.
for (const id in this.messageManager) {
const msg = this.messageManager[id];
if (msg) {
const [, reject] = msg;
delete this.messageManager[id];
reject(new Error(message));
}
}
this.workerLoad[workerIndex] = 0;
}

private createWorkers(
numWorkers = 1,
createImageBitmapSupport: CreateImageBitmapSupport,
Expand Down Expand Up @@ -224,19 +244,22 @@ export class ImageWorkerManager {
const blob: Blob = new Blob([workerCode], {
type: 'application/javascript',
});
const blobURL: string = (self.URL ? URL : webkitURL).createObjectURL(blob);
const urlFactory = self.URL ? URL : webkitURL;
const blobURL: string = urlFactory.createObjectURL(blob);
const workers: Worker[] = [];
for (let i = 0; i < numWorkers; i++) {
workers.push(new Worker(blobURL));
this.workerLoad.push(0);
}
// Workers retain the script; the URL itself is no longer needed.
urlFactory.revokeObjectURL(blobURL);
return workers;
}

private getNextWorkerIndex(): number {
if (this.workers.length === 0) return -1;

let minLoad = 99;
let minLoad = Infinity;
let workerIndex = 0;

for (let i = 0; i < this.workers.length; i++) {
Expand Down Expand Up @@ -264,26 +287,26 @@ export class ImageWorkerManager {
): Promise<TextureData> {
return new Promise((resolve, reject) => {
try {
if (this.workers) {
const id = this.nextId++;
this.messageManager[id] = [resolve, reject];
const nextWorkerIndex = this.getNextWorkerIndex();

if (nextWorkerIndex !== -1) {
const worker = this.workers[nextWorkerIndex];
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.workerLoad[nextWorkerIndex]!++;
worker!.postMessage({
id,
src: src,
premultiplyAlpha,
sx,
sy,
sw,
sh,
});
}
const nextWorkerIndex = this.getNextWorkerIndex();
if (nextWorkerIndex === -1) {
reject(new Error('No image workers available'));
return;
}

const id = this.nextId++;
this.messageManager[id] = [resolve, reject];
const worker = this.workers[nextWorkerIndex];
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.workerLoad[nextWorkerIndex]!++;
worker!.postMessage({
id,
src: src,
premultiplyAlpha,
sx,
sy,
sw,
sh,
});
} catch (error) {
reject(error);
}
Expand Down
37 changes: 21 additions & 16 deletions src/core/textures/ImageTexture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,11 +147,22 @@ export class ImageTexture extends Texture {
data: HTMLImageElement | null;
premultiplyAlpha: boolean;
}>((resolve, reject) => {
let objectUrl: string | null = null;

const cleanup = () => {
if (objectUrl !== null) {
URL.revokeObjectURL(objectUrl);
objectUrl = null;
}
};

img.onload = () => {
cleanup();
resolve({ data: img, premultiplyAlpha: hasAlpha });
};

img.onerror = (err) => {
cleanup();
const errorMessage =
err instanceof Error
? err.message
Expand All @@ -162,7 +173,8 @@ export class ImageTexture extends Texture {
};

if (src instanceof Blob) {
img.src = URL.createObjectURL(src);
objectUrl = URL.createObjectURL(src);
img.src = objectUrl;
} else {
img.src = src;
}
Expand Down Expand Up @@ -218,10 +230,11 @@ export class ImageTexture extends Texture {

async loadImage(src: string) {
const { premultiplyAlpha, sx, sy, sw, sh } = this.props;
const isBase64 = isBase64Image(src);

if (this.txManager.hasCreateImageBitmap === true) {
if (
isBase64Image(src) === false &&
isBase64 === false &&
this.txManager.hasWorker === true &&
this.txManager.imageWorkerManager !== null
) {
Expand All @@ -235,15 +248,9 @@ export class ImageTexture extends Texture {
);
}

let blob;

if (isBase64Image(src) === true) {
blob = dataURIToBlob(src);
} else {
blob = await fetchJson(src, 'blob').then(
(response) => response as Blob,
);
}
const blob = isBase64
? dataURIToBlob(src)
: ((await fetchJson(src, 'blob')) as Blob);

return this.createImageBitmap(blob, premultiplyAlpha, sx, sy, sw, sh);
}
Expand Down Expand Up @@ -362,11 +369,9 @@ export class ImageTexture extends Texture {
}`;

if (props.sh !== null && props.sw !== null) {
cacheKey += ',';
cacheKey += props.sx ?? '';
cacheKey += props.sy ?? '';
cacheKey += props.sw || '';
cacheKey += props.sh || '';
cacheKey += `,${props.sx ?? ''},${props.sy ?? ''},${props.sw || ''},${
props.sh || ''
}`;
}

return cacheKey;
Expand Down
Loading