diff --git a/exports/index.ts b/exports/index.ts index b2854fa9..b0845452 100644 --- a/exports/index.ts +++ b/exports/index.ts @@ -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'; diff --git a/src/common/CommonTypes.ts b/src/common/CommonTypes.ts index 33671c49..bf077830 100644 --- a/src/common/CommonTypes.ts +++ b/src/common/CommonTypes.ts @@ -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 @@ -71,7 +72,7 @@ export type NodeTextFailedPayload = { */ export type NodeTextureFailedPayload = { type: 'texture'; - error: Error; + error: TextureError; }; /** diff --git a/src/core/CoreTextureManager.ts b/src/core/CoreTextureManager.ts index d8a06906..7bdc23e9 100644 --- a/src/core/CoreTextureManager.ts +++ b/src/core/CoreTextureManager.ts @@ -32,6 +32,7 @@ import { validateCreateImageBitmap, type CreateImageBitmapSupport, } from './lib/validateImageBitmap.js'; +import { TextureError, TextureErrorCode } from './TextureError.js'; /** * Augmentable map of texture class types @@ -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 @@ -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; } @@ -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; } diff --git a/src/core/TextureError.ts b/src/core/TextureError.ts new file mode 100644 index 00000000..34ae8127 --- /dev/null +++ b/src/core/TextureError.ts @@ -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.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') + ); +} diff --git a/src/core/TextureMemoryManager.ts b/src/core/TextureMemoryManager.ts index 8d407608..12b99c27 100644 --- a/src/core/TextureMemoryManager.ts +++ b/src/core/TextureMemoryManager.ts @@ -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; @@ -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; } } diff --git a/src/core/textures/Texture.ts b/src/core/textures/Texture.ts index bdfc26f2..af4ec87c 100644 --- a/src/core/textures/Texture.ts +++ b/src/core/textures/Texture.ts @@ -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 @@ -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'; @@ -189,7 +190,7 @@ export abstract class Texture extends EventEmitter { return this._dimensions; } - get error(): Error | null { + get error(): TextureError | null { return this._error; } diff --git a/src/main-api/Inspector.ts b/src/main-api/Inspector.ts index a6d6b6ca..e3cdd9d4 100644 --- a/src/main-api/Inspector.ts +++ b/src/main-api/Inspector.ts @@ -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'); }