Skip to content
5 changes: 5 additions & 0 deletions exports/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ export {
type TextureMap,
} from '../src/core/CoreTextureManager.js';
export type { MemoryInfo } from '../src/core/TextureMemoryManager.js';
export {
TextureError,
TextureErrorCode,
isTextureError,
} from '../src/core/TextureError.js';
export type { ShaderMap, EffectMap } from '../src/core/CoreShaderManager.js';
export type { TextRendererMap } from '../src/core/text-rendering/renderers/TextRenderer.js';
export type { TrFontFaceMap } from '../src/core/text-rendering/font-face-types/TrFontFace.js';
Expand Down
3 changes: 2 additions & 1 deletion src/common/CommonTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
*/

import type { CoreNodeRenderState } from '../core/CoreNode.js';
import type { TextureError } from '../core/TextureError.js';

/**
* Types shared between Main Space and Core Space
Expand Down Expand Up @@ -71,7 +72,7 @@ export type NodeTextFailedPayload = {
*/
export type NodeTextureFailedPayload = {
type: 'texture';
error: Error;
error: TextureError;
};

/**
Expand Down
17 changes: 14 additions & 3 deletions src/core/CoreTextureManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
validateCreateImageBitmap,
type CreateImageBitmapSupport,
} from './lib/validateImageBitmap.js';
import { TextureError, TextureErrorCode } from './TextureError.js';

/**
* Augmentable map of texture class types
Expand Down Expand Up @@ -328,9 +329,13 @@ export class CoreTextureManager extends EventEmitter {
let texture: Texture | undefined;
const TextureClass = this.txConstructors[textureType];
if (!TextureClass) {
throw new Error(`Texture type "${textureType}" is not registered`);
throw new TextureError(
TextureErrorCode.TEXTURE_TYPE_NOT_REGISTERED,
`Texture type "${textureType}" is not registered`,
);
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
const cacheKey = TextureClass.makeCacheKey(props as any);
if (cacheKey && this.keyCache.has(cacheKey)) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
Expand Down Expand Up @@ -415,7 +420,10 @@ export class CoreTextureManager extends EventEmitter {
this.stage.txMemManager.criticalCleanupRequested === true
) {
// we're at a critical memory threshold, don't upload textures
texture.setState('failed', new Error('Memory threshold exceeded'));
texture.setState(
'failed',
new TextureError(TextureErrorCode.MEMORY_THRESHOLD_EXCEEDED),
);
return;
}

Expand All @@ -432,7 +440,10 @@ export class CoreTextureManager extends EventEmitter {
if (texture.textureData === null) {
texture.setState(
'failed',
new Error('Texture data is null, cannot upload texture'),
new TextureError(
TextureErrorCode.TEXTURE_DATA_NULL,
'Texture data is null, cannot upload texture',
),
);
return;
}
Expand Down
46 changes: 46 additions & 0 deletions src/core/TextureError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
export enum TextureErrorCode {
MEMORY_THRESHOLD_EXCEEDED = 'MEMORY_THRESHOLD_EXCEEDED',
TEXTURE_DATA_NULL = 'TEXTURE_DATA_NULL',
TEXTURE_TYPE_NOT_REGISTERED = 'TEXTURE_TYPE_NOT_REGISTERED',
}

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',
};

export class TextureError extends Error {
code?: TextureErrorCode;

constructor(message: string);
constructor(code: TextureErrorCode, message?: string);
constructor(codeOrMessage: TextureErrorCode | string, maybeMessage?: string) {
const isCode = Object.values(TextureErrorCode).includes(
codeOrMessage as TextureErrorCode,
);

const code = isCode ? (codeOrMessage as TextureErrorCode) : undefined;
let message: string;
if (isCode && code) {
message = maybeMessage ?? defaultMessages[code];
} else {
message = String(codeOrMessage);
}

super(message);
this.name = new.target.name;
if (code) this.code = code;
}
}

export function isTextureError(err: unknown): err is TextureError {
return (
err instanceof TextureError ||
(typeof err === 'object' &&
err !== null &&
(err as { name?: unknown }).name === 'TextureError' &&
typeof (err as { code?: unknown }).code === 'string')
);
}
11 changes: 9 additions & 2 deletions src/core/TextureMemoryManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ export class TextureMemoryManager {
private debugLogging: boolean;
private lastCleanupTime = 0;
private baselineMemoryAllocation: number;
private hasWarnedAboveCritical = false;

public criticalCleanupRequested = false;
public doNotExceedCriticalThreshold: boolean;
Expand Down Expand Up @@ -313,14 +314,20 @@ export class TextureMemoryManager {
memUsed: this.memUsed,
criticalThreshold: this.criticalThreshold,
});

if (this.debugLogging === true || isProductionEnvironment() === false) {
// Only emit the warning once per over-threshold period
if (
!this.hasWarnedAboveCritical &&
(this.debugLogging === true || isProductionEnvironment() === false)
) {
console.warn(
`[TextureMemoryManager] Memory usage above critical threshold after cleanup: ${this.memUsed}`,
);

this.hasWarnedAboveCritical = true;
}
} else {
this.criticalCleanupRequested = false;
this.hasWarnedAboveCritical = false;
}
}

Expand Down
5 changes: 3 additions & 2 deletions src/core/textures/Texture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import type { SubTextureProps } from './SubTexture.js';
import type { Dimensions } from '../../common/CommonTypes.js';
import { EventEmitter } from '../../common/EventEmitter.js';
import type { CoreContextTexture } from '../renderers/CoreContextTexture.js';
import type { TextureError } from '../TextureError.js';

/**
* Event handler for when a Texture is freed
Expand Down Expand Up @@ -135,7 +136,7 @@ export abstract class Texture extends EventEmitter {
* `null`.
*/
private _dimensions: Dimensions | null = null;
private _error: Error | null = null;
private _error: TextureError | null = null;

// aggregate state
public state: TextureState = 'initial';
Expand Down Expand Up @@ -189,7 +190,7 @@ export abstract class Texture extends EventEmitter {
return this._dimensions;
}

get error(): Error | null {
get error(): TextureError | null {
return this._error;
}

Expand Down
5 changes: 4 additions & 1 deletion src/main-api/Inspector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -533,7 +533,10 @@ export class Inspector {

// Update error information if present
if (texture.error) {
div.setAttribute('data-texture-error', texture.error.message);
div.setAttribute(
'data-texture-error',
texture.error.code || texture.error.message,
);
} else {
div.removeAttribute('data-texture-error');
}
Expand Down