@lightningjs/renderer 2.17.0 → 2.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/src/core/CoreNode.js +7 -5
  2. package/dist/src/core/CoreNode.js.map +1 -1
  3. package/dist/src/core/CoreTextureManager.d.ts +14 -8
  4. package/dist/src/core/CoreTextureManager.js +33 -59
  5. package/dist/src/core/CoreTextureManager.js.map +1 -1
  6. package/dist/src/core/Stage.d.ts +3 -3
  7. package/dist/src/core/Stage.js +9 -14
  8. package/dist/src/core/Stage.js.map +1 -1
  9. package/dist/src/core/TextureMemoryManager.d.ts +21 -17
  10. package/dist/src/core/TextureMemoryManager.js +99 -106
  11. package/dist/src/core/TextureMemoryManager.js.map +1 -1
  12. package/dist/src/core/lib/WebGlContextWrapper.d.ts +10 -0
  13. package/dist/src/core/lib/WebGlContextWrapper.js +32 -0
  14. package/dist/src/core/lib/WebGlContextWrapper.js.map +1 -1
  15. package/dist/src/core/lib/textureCompression.js +13 -6
  16. package/dist/src/core/lib/textureCompression.js.map +1 -1
  17. package/dist/src/core/platform.js +4 -1
  18. package/dist/src/core/platform.js.map +1 -1
  19. package/dist/src/core/renderers/CoreContextTexture.d.ts +1 -0
  20. package/dist/src/core/renderers/CoreContextTexture.js.map +1 -1
  21. package/dist/src/core/renderers/canvas/CanvasCoreTexture.d.ts +1 -0
  22. package/dist/src/core/renderers/canvas/CanvasCoreTexture.js +4 -3
  23. package/dist/src/core/renderers/canvas/CanvasCoreTexture.js.map +1 -1
  24. package/dist/src/core/renderers/webgl/WebGlCoreCtxTexture.d.ts +9 -0
  25. package/dist/src/core/renderers/webgl/WebGlCoreCtxTexture.js +59 -29
  26. package/dist/src/core/renderers/webgl/WebGlCoreCtxTexture.js.map +1 -1
  27. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/SdfTextRenderer.js +4 -4
  28. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/SdfTextRenderer.js.map +1 -1
  29. package/dist/src/core/textures/ColorTexture.d.ts +2 -2
  30. package/dist/src/core/textures/ColorTexture.js +1 -2
  31. package/dist/src/core/textures/ColorTexture.js.map +1 -1
  32. package/dist/src/core/textures/ImageTexture.d.ts +7 -1
  33. package/dist/src/core/textures/ImageTexture.js +55 -39
  34. package/dist/src/core/textures/ImageTexture.js.map +1 -1
  35. package/dist/src/core/textures/NoiseTexture.js +1 -1
  36. package/dist/src/core/textures/NoiseTexture.js.map +1 -1
  37. package/dist/src/core/textures/RenderTexture.js +1 -1
  38. package/dist/src/core/textures/RenderTexture.js.map +1 -1
  39. package/dist/src/core/textures/SubTexture.d.ts +1 -2
  40. package/dist/src/core/textures/SubTexture.js +11 -29
  41. package/dist/src/core/textures/SubTexture.js.map +1 -1
  42. package/dist/src/core/textures/Texture.d.ts +51 -7
  43. package/dist/src/core/textures/Texture.js +127 -15
  44. package/dist/src/core/textures/Texture.js.map +1 -1
  45. package/dist/src/main-api/Inspector.d.ts +3 -0
  46. package/dist/src/main-api/Inspector.js +156 -0
  47. package/dist/src/main-api/Inspector.js.map +1 -1
  48. package/dist/src/main-api/Renderer.d.ts +1 -3
  49. package/dist/src/main-api/Renderer.js +2 -4
  50. package/dist/src/main-api/Renderer.js.map +1 -1
  51. package/dist/tsconfig.dist.tsbuildinfo +1 -1
  52. package/package.json +1 -1
  53. package/src/core/CoreNode.ts +8 -4
  54. package/src/core/CoreTextureManager.ts +59 -65
  55. package/src/core/Stage.ts +10 -16
  56. package/src/core/TextureMemoryManager.ts +118 -131
  57. package/src/core/lib/WebGlContextWrapper.ts +38 -0
  58. package/src/core/lib/textureCompression.ts +18 -7
  59. package/src/core/platform.ts +5 -1
  60. package/src/core/renderers/CoreContextTexture.ts +1 -0
  61. package/src/core/renderers/canvas/CanvasCoreTexture.ts +5 -3
  62. package/src/core/renderers/webgl/WebGlCoreCtxTexture.ts +78 -40
  63. package/src/core/text-rendering/renderers/SdfTextRenderer/SdfTextRenderer.ts +10 -4
  64. package/src/core/textures/ColorTexture.ts +4 -7
  65. package/src/core/textures/ImageTexture.ts +66 -51
  66. package/src/core/textures/NoiseTexture.ts +1 -1
  67. package/src/core/textures/RenderTexture.ts +1 -1
  68. package/src/core/textures/SubTexture.ts +14 -31
  69. package/src/core/textures/Texture.ts +150 -21
  70. package/src/main-api/Inspector.ts +203 -0
  71. package/src/main-api/Renderer.ts +2 -4
@@ -17,9 +17,8 @@
17
17
  * limitations under the License.
18
18
  */
19
19
  import { isProductionEnvironment } from '../utils.js';
20
- import { getTimeStamp } from './platform.js';
21
20
  import type { Stage } from './Stage.js';
22
- import { Texture, TextureType, type TextureState } from './textures/Texture.js';
21
+ import { Texture, TextureType } from './textures/Texture.js';
23
22
  import { bytesToMb } from './utils.js';
24
23
 
25
24
  export interface TextureMemoryManagerSettings {
@@ -116,14 +115,14 @@ export interface MemoryInfo {
116
115
  */
117
116
  export class TextureMemoryManager {
118
117
  private memUsed = 0;
119
- private loadedTextures: Map<Texture, number> = new Map();
120
- private orphanedTextures: Texture[] = [];
118
+ private loadedTextures: (Texture | null)[] = [];
121
119
  private criticalThreshold: number;
122
120
  private targetThreshold: number;
123
121
  private cleanupInterval: number;
124
122
  private debugLogging: boolean;
125
123
  private lastCleanupTime = 0;
126
124
  private baselineMemoryAllocation: number;
125
+ private needsDefrag = false;
127
126
 
128
127
  public criticalCleanupRequested = false;
129
128
  public doNotExceedCriticalThreshold: boolean;
@@ -181,35 +180,6 @@ export class TextureMemoryManager {
181
180
  }
182
181
  }
183
182
 
184
- /**
185
- * Add a texture to the orphaned textures list
186
- *
187
- * @param texture - The texture to add to the orphaned textures list
188
- */
189
- addToOrphanedTextures(texture: Texture) {
190
- // if the texture is already in the orphaned textures list add it at the end
191
- if (this.orphanedTextures.includes(texture)) {
192
- this.removeFromOrphanedTextures(texture);
193
- }
194
-
195
- // If the texture can be cleaned up, add it to the orphaned textures list
196
- if (texture.preventCleanup === false) {
197
- this.orphanedTextures.push(texture);
198
- }
199
- }
200
-
201
- /**
202
- * Remove a texture from the orphaned textures list
203
- *
204
- * @param texture - The texture to remove from the orphaned textures list
205
- */
206
- removeFromOrphanedTextures(texture: Texture) {
207
- const index = this.orphanedTextures.indexOf(texture);
208
- if (index !== -1) {
209
- this.orphanedTextures.splice(index, 1);
210
- }
211
- }
212
-
213
183
  /**
214
184
  * Set the memory usage of a texture
215
185
  *
@@ -217,17 +187,25 @@ export class TextureMemoryManager {
217
187
  * @param byteSize - The size of the texture in bytes
218
188
  */
219
189
  setTextureMemUse(texture: Texture, byteSize: number) {
220
- if (this.loadedTextures.has(texture)) {
221
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
222
- this.memUsed -= this.loadedTextures.get(texture)!;
223
- }
190
+ // Update global memory counter by subtracting old value
191
+ this.memUsed -= texture.memUsed;
224
192
 
225
193
  if (byteSize === 0) {
226
- this.loadedTextures.delete(texture);
194
+ // PERFORMANCE: Mark for deletion instead of splice (zero overhead)
195
+ const index = this.loadedTextures.indexOf(texture);
196
+ if (index !== -1) {
197
+ this.loadedTextures[index] = null;
198
+ this.needsDefrag = true;
199
+ }
200
+ texture.memUsed = 0;
227
201
  return;
228
202
  } else {
203
+ // Update texture memory and add to tracking if not already present
204
+ texture.memUsed = byteSize;
229
205
  this.memUsed += byteSize;
230
- this.loadedTextures.set(texture, byteSize);
206
+ if (this.loadedTextures.indexOf(texture) === -1) {
207
+ this.loadedTextures.push(texture);
208
+ }
231
209
  }
232
210
 
233
211
  if (this.memUsed > this.criticalThreshold) {
@@ -247,42 +225,21 @@ export class TextureMemoryManager {
247
225
  return this.memUsed > this.criticalThreshold;
248
226
  }
249
227
 
250
- cleanupQuick(critical: boolean) {
251
- // Free non-renderable textures until we reach the target threshold
252
- const memTarget = this.targetThreshold;
253
- const timestamp = getTimeStamp();
254
-
255
- while (
256
- this.memUsed >= memTarget &&
257
- this.orphanedTextures.length > 0 &&
258
- (critical || getTimeStamp() - timestamp < 10)
259
- ) {
260
- const texture = this.orphanedTextures.shift();
261
-
262
- if (texture === undefined) {
263
- continue;
264
- }
265
-
266
- if (texture.renderable === true) {
267
- // If the texture is renderable, we can't free it up
268
- continue;
269
- }
270
-
271
- // Skip textures that are in transitional states - we only want to clean up
272
- // textures that are in a stable state (loaded, failed, or freed)
273
- if (
274
- texture.state === 'initial' ||
275
- Texture.TRANSITIONAL_TEXTURE_STATES.includes(texture.state)
276
- ) {
277
- continue;
278
- }
279
-
280
- this.destroyTexture(texture);
281
- }
228
+ /**
229
+ * Check if defragmentation is needed
230
+ *
231
+ * @remarks
232
+ * Returns true if the loadedTextures array has null entries that need
233
+ * to be compacted. Called by platform during idle periods.
234
+ *
235
+ * @returns true if defragmentation should be performed
236
+ */
237
+ checkDefrag() {
238
+ return this.needsDefrag;
282
239
  }
283
240
 
284
241
  /**
285
- * Destroy a texture and remove it from the memory manager
242
+ * Destroy a texture and null out its array position
286
243
  *
287
244
  * @param texture - The texture to destroy
288
245
  */
@@ -293,46 +250,25 @@ export class TextureMemoryManager {
293
250
  );
294
251
  }
295
252
 
253
+ // PERFORMANCE: Null out array position instead of splice (zero overhead)
254
+ const index = this.loadedTextures.indexOf(texture);
255
+ if (index !== -1) {
256
+ this.loadedTextures[index] = null;
257
+ this.needsDefrag = true;
258
+ }
259
+
260
+ // Destroy texture and update memory counters
296
261
  const txManager = this.stage.txManager;
297
- txManager.removeTextureFromQueue(texture);
298
262
  txManager.removeTextureFromCache(texture);
299
263
 
300
264
  texture.destroy();
301
265
 
302
- this.removeFromOrphanedTextures(texture);
303
- this.loadedTextures.delete(texture);
304
- }
305
- cleanupDeep(critical: boolean) {
306
- // Free non-renderable textures until we reach the target threshold
307
- const memTarget = critical ? this.criticalThreshold : this.targetThreshold;
308
-
309
- // Filter for textures that are candidates for cleanup
310
- // note: This is an expensive operation, so we only do it in deep cleanup
311
- const cleanupCandidates = [...this.loadedTextures.keys()].filter(
312
- (texture) => {
313
- return (
314
- (texture.type === TextureType.image ||
315
- texture.type === TextureType.noise ||
316
- texture.type === TextureType.renderToTexture) &&
317
- texture.renderable === false &&
318
- texture.preventCleanup === false &&
319
- texture.state !== 'initial' &&
320
- !Texture.TRANSITIONAL_TEXTURE_STATES.includes(texture.state)
321
- );
322
- },
323
- );
324
-
325
- while (this.memUsed >= memTarget && cleanupCandidates.length > 0) {
326
- const texture = cleanupCandidates.shift();
327
- if (texture === undefined) {
328
- continue;
329
- }
330
-
331
- this.destroyTexture(texture);
332
- }
266
+ // Update memory counters
267
+ this.memUsed -= texture.memUsed;
268
+ texture.memUsed = 0;
333
269
  }
334
270
 
335
- cleanup(aggressive: boolean = false) {
271
+ cleanup() {
336
272
  const critical = this.criticalCleanupRequested;
337
273
  this.lastCleanupTime = this.frameTime;
338
274
 
@@ -345,25 +281,41 @@ export class TextureMemoryManager {
345
281
 
346
282
  if (this.debugLogging === true) {
347
283
  console.log(
348
- `[TextureMemoryManager] Cleaning up textures. Critical: ${critical}. Aggressive: ${aggressive}`,
284
+ `[TextureMemoryManager] Cleaning up textures. Critical: ${critical}.`,
349
285
  );
350
286
  }
351
287
 
352
- // Note: We skip textures in transitional states during cleanup:
353
- // - 'initial': These textures haven't started loading yet
354
- // - 'fetching': These textures are in the process of being fetched
355
- // - 'fetched': These textures have been fetched but not yet uploaded to GPU
356
- // - 'loading': These textures are being uploaded to the GPU
357
- //
358
- // For 'failed' and 'freed' states, we only remove them from the tracking
359
- // arrays without trying to free GPU resources that don't exist.
360
-
361
- // try a quick cleanup first
362
- this.cleanupQuick(critical);
363
-
364
- // if we're still above the target threshold, do a deep cleanup
365
- if (aggressive === true && this.memUsed >= this.criticalThreshold) {
366
- this.cleanupDeep(critical);
288
+ // Free non-renderable textures until we reach the target threshold
289
+ const memTarget = critical ? this.criticalThreshold : this.targetThreshold;
290
+
291
+ // PERFORMANCE: Zero-overhead cleanup with null marking
292
+ // Skip null entries, mark cleaned textures as null for later defrag
293
+ let currentMemUsed = this.memUsed;
294
+
295
+ for (let i = 0; i < this.loadedTextures.length; i++) {
296
+ // Early exit: target memory reached
297
+ if (currentMemUsed < memTarget) {
298
+ break;
299
+ }
300
+
301
+ const texture = this.loadedTextures[i];
302
+ if (!texture) continue; // Skip null entries from previous deletions
303
+
304
+ // Fast type check for cleanable textures
305
+ const isCleanableType =
306
+ texture.type === TextureType.image ||
307
+ texture.type === TextureType.noise ||
308
+ texture.type === TextureType.renderToTexture;
309
+
310
+ // Immediate cleanup if eligible
311
+ if (isCleanableType && texture.canBeCleanedUp() === true) {
312
+ // Get memory before destroying
313
+ const textureMemory = texture.memUsed;
314
+
315
+ // Destroy texture (which will null out the array position)
316
+ this.destroyTexture(texture);
317
+ currentMemUsed -= textureMemory;
318
+ }
367
319
  }
368
320
 
369
321
  if (this.memUsed >= this.criticalThreshold) {
@@ -382,6 +334,37 @@ export class TextureMemoryManager {
382
334
  }
383
335
  }
384
336
 
337
+ /**
338
+ * Defragment the loadedTextures array by removing null entries
339
+ *
340
+ * @remarks
341
+ * This should be called during idle periods to compact the array
342
+ * after null-marking deletions. Zero overhead during critical cleanup.
343
+ */
344
+ defragment() {
345
+ if (!this.needsDefrag) {
346
+ return;
347
+ }
348
+
349
+ // PERFORMANCE: Single-pass compaction
350
+ let writeIndex = 0;
351
+ for (
352
+ let readIndex = 0;
353
+ readIndex < this.loadedTextures.length;
354
+ readIndex++
355
+ ) {
356
+ const texture = this.loadedTextures[readIndex];
357
+ if (texture !== null && texture !== undefined) {
358
+ this.loadedTextures[writeIndex] = texture;
359
+ writeIndex++;
360
+ }
361
+ }
362
+
363
+ // Trim array to new size
364
+ this.loadedTextures.length = writeIndex;
365
+ this.needsDefrag = false;
366
+ }
367
+
385
368
  /**
386
369
  * Get the current texture memory usage information
387
370
  *
@@ -391,15 +374,19 @@ export class TextureMemoryManager {
391
374
  */
392
375
  getMemoryInfo(): MemoryInfo {
393
376
  let renderableTexturesLoaded = 0;
394
- const renderableMemUsed = [...this.loadedTextures.keys()].reduce(
395
- (acc, texture) => {
396
- renderableTexturesLoaded += texture.renderable ? 1 : 0;
397
- // Get the memory used by the texture, defaulting to 0 if not found
398
- const textureMemory = this.loadedTextures.get(texture) ?? 0;
399
- return acc + (texture.renderable ? textureMemory : 0);
400
- },
401
- this.baselineMemoryAllocation,
402
- );
377
+ let renderableMemUsed = this.baselineMemoryAllocation;
378
+
379
+ for (const texture of this.loadedTextures) {
380
+ if (texture && texture.renderable) {
381
+ renderableTexturesLoaded += 1;
382
+ renderableMemUsed += texture.memUsed;
383
+ }
384
+ }
385
+
386
+ // Count non-null entries for accurate loaded texture count
387
+ const actualLoadedTextures = this.loadedTextures.filter(
388
+ (t) => t !== null,
389
+ ).length;
403
390
 
404
391
  return {
405
392
  criticalThreshold: this.criticalThreshold,
@@ -407,7 +394,7 @@ export class TextureMemoryManager {
407
394
  renderableMemUsed,
408
395
  memUsed: this.memUsed,
409
396
  renderableTexturesLoaded,
410
- loadedTextures: this.loadedTextures.size,
397
+ loadedTextures: actualLoadedTextures,
411
398
  baselineMemoryAllocation: this.baselineMemoryAllocation,
412
399
  };
413
400
  }
@@ -1277,6 +1277,44 @@ export class WebGlContextWrapper {
1277
1277
  const { gl } = this;
1278
1278
  gl.deleteShader(shader);
1279
1279
  }
1280
+
1281
+ /**
1282
+ * Check for WebGL errors and return error information
1283
+ * @param operation Description of the operation for error reporting
1284
+ * @returns Object with error information or null if no error
1285
+ */
1286
+ checkError(
1287
+ operation: string,
1288
+ ): { error: number; errorName: string; message: string } | null {
1289
+ const error = this.getError();
1290
+ if (error !== 0) {
1291
+ // 0 is GL_NO_ERROR
1292
+ let errorName = 'UNKNOWN_ERROR';
1293
+ switch (error) {
1294
+ case this.INVALID_ENUM:
1295
+ errorName = 'INVALID_ENUM';
1296
+ break;
1297
+ case 0x0501: // GL_INVALID_VALUE
1298
+ errorName = 'INVALID_VALUE';
1299
+ break;
1300
+ case this.INVALID_OPERATION:
1301
+ errorName = 'INVALID_OPERATION';
1302
+ break;
1303
+ case 0x0505: // GL_OUT_OF_MEMORY
1304
+ errorName = 'OUT_OF_MEMORY';
1305
+ break;
1306
+ case 0x9242: // GL_CONTEXT_LOST_WEBGL
1307
+ errorName = 'CONTEXT_LOST_WEBGL';
1308
+ break;
1309
+ }
1310
+
1311
+ const message = `WebGL ${errorName} (0x${error.toString(
1312
+ 16,
1313
+ )}) during ${operation}`;
1314
+ return { error, errorName, message };
1315
+ }
1316
+ return null;
1317
+ }
1280
1318
  }
1281
1319
 
1282
1320
  // prettier-ignore
@@ -39,14 +39,25 @@ export function isCompressedTextureContainer(url: string): boolean {
39
39
  export const loadCompressedTexture = async (
40
40
  url: string,
41
41
  ): Promise<TextureData> => {
42
- const response = await fetch(url);
43
- const arrayBuffer = await response.arrayBuffer();
42
+ try {
43
+ const response = await fetch(url);
44
44
 
45
- if (url.indexOf('.ktx') !== -1) {
46
- return loadKTXData(arrayBuffer);
47
- }
45
+ if (!response.ok) {
46
+ throw new Error(
47
+ `Failed to fetch compressed texture: ${response.status} ${response.statusText}`,
48
+ );
49
+ }
50
+
51
+ const arrayBuffer = await response.arrayBuffer();
52
+
53
+ if (url.indexOf('.ktx') !== -1) {
54
+ return loadKTXData(arrayBuffer);
55
+ }
48
56
 
49
- return loadPVRData(arrayBuffer);
57
+ return loadPVRData(arrayBuffer);
58
+ } catch (error) {
59
+ throw new Error(`Failed to load compressed texture from ${url}: ${error}`);
60
+ }
50
61
  };
51
62
 
52
63
  /**
@@ -111,7 +122,7 @@ const loadPVRData = async (buffer: ArrayBuffer): Promise<TextureData> => {
111
122
  const header = new Int32Array(arrayBuffer, 0, pvrHeaderLength);
112
123
 
113
124
  // @ts-expect-error Object possibly undefined
114
- // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
125
+
115
126
  const dataOffset = header[pvrMetadata] + 52;
116
127
  const pvrtcData = new Uint8Array(arrayBuffer, dataOffset);
117
128
  const mipmaps = [];
@@ -63,7 +63,11 @@ export const startLoop = (stage: Stage) => {
63
63
  }
64
64
 
65
65
  if (stage.txMemManager.checkCleanup() === true) {
66
- stage.txMemManager.cleanup(false);
66
+ stage.txMemManager.cleanup();
67
+ }
68
+
69
+ if (stage.txMemManager.checkDefrag() === true) {
70
+ stage.txMemManager.defragment();
67
71
  }
68
72
 
69
73
  stage.flushFrameEvents();
@@ -35,6 +35,7 @@ export abstract class CoreContextTexture {
35
35
  }
36
36
 
37
37
  abstract load(): Promise<void>;
38
+ abstract release(): void;
38
39
  abstract free(): void;
39
40
 
40
41
  get renderable(): boolean {
@@ -41,18 +41,20 @@ export class CanvasCoreTexture extends CoreContextTexture {
41
41
  try {
42
42
  const size = await this.onLoadRequest();
43
43
  this.textureSource.setState('loaded', size);
44
- this.textureSource.freeTextureData();
45
44
  this.updateMemSize();
46
45
  } catch (err) {
47
46
  this.textureSource.setState('failed', err as Error);
48
- this.textureSource.freeTextureData();
49
47
  throw err;
50
48
  }
51
49
  }
52
50
 
53
- free(): void {
51
+ release(): void {
54
52
  this.image = undefined;
55
53
  this.tintCache = undefined;
54
+ }
55
+
56
+ free(): void {
57
+ this.release();
56
58
  this.textureSource.setState('freed');
57
59
  this.setTextureMemUse(0);
58
60
  this.textureSource.freeTextureData();
@@ -22,7 +22,6 @@ import { assertTruthy } from '../../../utils.js';
22
22
  import type { TextureMemoryManager } from '../../TextureMemoryManager.js';
23
23
  import type { WebGlContextWrapper } from '../../lib/WebGlContextWrapper.js';
24
24
  import type { Texture } from '../../textures/Texture.js';
25
- import { isPowerOfTwo } from '../../utils.js';
26
25
  import { CoreContextTexture } from '../CoreContextTexture.js';
27
26
  import { isHTMLImageElement } from './internal/RendererUtils.js';
28
27
 
@@ -52,6 +51,25 @@ export class WebGlCoreCtxTexture extends CoreContextTexture {
52
51
  super(memManager, textureSource);
53
52
  }
54
53
 
54
+ /**
55
+ * GL error check with direct state marking
56
+ * Uses cached error result to minimize function calls
57
+ */
58
+ private checkGLError(): boolean {
59
+ // Skip if already failed to prevent double-processing
60
+ if (this.state === 'failed') {
61
+ return true;
62
+ }
63
+
64
+ const error = this.glw.getError();
65
+ if (error !== 0) {
66
+ this.state = 'failed';
67
+ this.textureSource.setState('failed', new Error(`WebGL Error: ${error}`));
68
+ return true;
69
+ }
70
+ return false;
71
+ }
72
+
55
73
  get ctxTexture(): WebGLTexture | null {
56
74
  if (this.state === 'freed') {
57
75
  this.load();
@@ -92,10 +110,11 @@ export class WebGlCoreCtxTexture extends CoreContextTexture {
92
110
 
93
111
  if (this._nativeCtxTexture === null) {
94
112
  this.state = 'failed';
95
- const error = new Error('Could not create WebGL Texture');
96
- this.textureSource.setState('failed', error);
97
- console.error('Could not create WebGL Texture');
98
- throw error;
113
+ this.textureSource.setState(
114
+ 'failed',
115
+ new Error('WebGL Texture creation failed'),
116
+ );
117
+ return;
99
118
  }
100
119
 
101
120
  try {
@@ -113,16 +132,7 @@ export class WebGlCoreCtxTexture extends CoreContextTexture {
113
132
  // Update the texture source's width and height so that it can be used
114
133
  // for rendering.
115
134
  this.textureSource.setState('loaded', { width, height });
116
-
117
- // cleanup source texture data next tick
118
- // This is done using queueMicrotask to ensure it runs after the current
119
- // event loop tick, allowing the texture to be fully loaded and bound
120
- // to the GL context before freeing the source data.
121
- // This is important to avoid issues with the texture data being
122
- // freed while the texture is still being loaded or used.
123
- queueMicrotask(() => {
124
- this.textureSource.freeTextureData();
125
- });
135
+ this.textureSource.freeTextureData();
126
136
  } catch (err: unknown) {
127
137
  // If the texture has been freed while loading, return early.
128
138
  // Type assertion needed because state could change during async operations
@@ -130,12 +140,9 @@ export class WebGlCoreCtxTexture extends CoreContextTexture {
130
140
  return;
131
141
  }
132
142
 
143
+ // Ensure texture is marked as failed
133
144
  this.state = 'failed';
134
- const error = err instanceof Error ? err : new Error(String(err));
135
- this.textureSource.setState('failed', error);
136
- this.textureSource.freeTextureData();
137
- console.error(err);
138
- throw error; // Re-throw to propagate the error
145
+ this.textureSource.setState('failed');
139
146
  }
140
147
  }
141
148
 
@@ -145,10 +152,19 @@ export class WebGlCoreCtxTexture extends CoreContextTexture {
145
152
  async onLoadRequest(): Promise<Dimensions> {
146
153
  const { glw } = this;
147
154
  const textureData = this.textureSource.textureData;
155
+
156
+ // Early return if texture is already failed
157
+ if (this.state === 'failed') {
158
+ return { width: 0, height: 0 };
159
+ }
160
+
148
161
  if (textureData === null || this._nativeCtxTexture === null) {
149
- throw new Error(
150
- 'Texture data or native texture is null ' + this.textureSource.type,
162
+ this.state = 'failed';
163
+ this.textureSource.setState(
164
+ 'failed',
165
+ new Error('No texture data available'),
151
166
  );
167
+ return { width: 0, height: 0 };
152
168
  }
153
169
 
154
170
  // Set to a 1x1 transparent texture
@@ -160,6 +176,11 @@ export class WebGlCoreCtxTexture extends CoreContextTexture {
160
176
 
161
177
  glw.activeTexture(0);
162
178
 
179
+ // High-performance error check - single call, direct state marking
180
+ if (this.checkGLError() === true) {
181
+ return { width: 0, height: 0 };
182
+ }
183
+
163
184
  const tdata = textureData.data;
164
185
  const format = glw.RGBA;
165
186
  const formatBytes = 4;
@@ -183,25 +204,13 @@ export class WebGlCoreCtxTexture extends CoreContextTexture {
183
204
 
184
205
  glw.texImage2D(0, format, format, glw.UNSIGNED_BYTE, tdata);
185
206
 
186
- this.setTextureMemUse(height * width * formatBytes * memoryPadding);
187
- } else if (tdata === null) {
188
- width = 0;
189
- height = 0;
190
- // Reset to a 1x1 transparent texture
191
- glw.bindTexture(this._nativeCtxTexture);
207
+ // Check for errors after image upload operations
208
+ if (this.checkGLError() === true) {
209
+ return { width: 0, height: 0 };
210
+ }
192
211
 
193
- glw.texImage2D(
194
- 0,
195
- format,
196
- 1,
197
- 1,
198
- 0,
199
- format,
200
- glw.UNSIGNED_BYTE,
201
- TRANSPARENT_TEXTURE_DATA,
202
- );
203
- this.setTextureMemUse(TRANSPARENT_TEXTURE_DATA.byteLength);
204
- } else if ('mipmaps' in tdata && tdata.mipmaps) {
212
+ this.setTextureMemUse(height * width * formatBytes * memoryPadding);
213
+ } else if (tdata && 'mipmaps' in tdata && tdata.mipmaps) {
205
214
  const { mipmaps, width = 0, height = 0, type, glInternalFormat } = tdata;
206
215
  const view =
207
216
  type === 'ktx'
@@ -216,6 +225,11 @@ export class WebGlCoreCtxTexture extends CoreContextTexture {
216
225
  glw.texParameteri(glw.TEXTURE_MAG_FILTER, glw.LINEAR);
217
226
  glw.texParameteri(glw.TEXTURE_MIN_FILTER, glw.LINEAR);
218
227
 
228
+ // Check for errors after compressed texture operations
229
+ if (this.checkGLError() === true) {
230
+ return { width: 0, height: 0 };
231
+ }
232
+
219
233
  this.setTextureMemUse(view.byteLength);
220
234
  } else if (tdata && tdata instanceof Uint8Array) {
221
235
  // Color Texture
@@ -239,12 +253,24 @@ export class WebGlCoreCtxTexture extends CoreContextTexture {
239
253
  tdata,
240
254
  );
241
255
 
256
+ // Check for errors after color texture operations
257
+ if (this.checkGLError() === true) {
258
+ return { width: 0, height: 0 };
259
+ }
260
+
242
261
  this.setTextureMemUse(width * height * formatBytes);
243
262
  } else {
244
263
  console.error(
245
264
  `WebGlCoreCtxTexture.onLoadRequest: Unexpected textureData returned`,
246
265
  textureData,
247
266
  );
267
+
268
+ this.state = 'failed';
269
+ this.textureSource.setState(
270
+ 'failed',
271
+ new Error('Unexpected texture data'),
272
+ );
273
+ return { width: 0, height: 0 };
248
274
  }
249
275
 
250
276
  return {
@@ -265,6 +291,13 @@ export class WebGlCoreCtxTexture extends CoreContextTexture {
265
291
 
266
292
  this.state = 'freed';
267
293
  this.textureSource.setState('freed');
294
+ this.release();
295
+ }
296
+
297
+ /**
298
+ * Release the WebGLTexture from the GPU without changing state
299
+ */
300
+ release(): void {
268
301
  this._w = 0;
269
302
  this._h = 0;
270
303
 
@@ -308,6 +341,11 @@ export class WebGlCoreCtxTexture extends CoreContextTexture {
308
341
  glw.texParameteri(glw.TEXTURE_WRAP_S, glw.CLAMP_TO_EDGE);
309
342
  glw.texParameteri(glw.TEXTURE_WRAP_T, glw.CLAMP_TO_EDGE);
310
343
 
344
+ const error = glw.getError();
345
+ if (error !== 0) {
346
+ return null;
347
+ }
348
+
311
349
  return nativeTexture;
312
350
  }
313
351
  }