@solidtv/renderer 1.1.0 → 1.1.2

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 (33) hide show
  1. package/dist/src/core/CoreNode.d.ts +22 -0
  2. package/dist/src/core/CoreNode.js +131 -25
  3. package/dist/src/core/CoreNode.js.map +1 -1
  4. package/dist/src/core/CoreTextureManager.js +82 -24
  5. package/dist/src/core/CoreTextureManager.js.map +1 -1
  6. package/dist/src/core/Stage.js +3 -2
  7. package/dist/src/core/Stage.js.map +1 -1
  8. package/dist/src/core/TextureError.d.ts +2 -1
  9. package/dist/src/core/TextureError.js +2 -0
  10. package/dist/src/core/TextureError.js.map +1 -1
  11. package/dist/src/core/lib/ImageWorker.d.ts +1 -1
  12. package/dist/src/core/lib/ImageWorker.js +46 -26
  13. package/dist/src/core/lib/ImageWorker.js.map +1 -1
  14. package/dist/src/core/lib/Matrix3d.d.ts +9 -0
  15. package/dist/src/core/lib/Matrix3d.js +29 -3
  16. package/dist/src/core/lib/Matrix3d.js.map +1 -1
  17. package/dist/src/core/textures/ColorTexture.js +3 -1
  18. package/dist/src/core/textures/ColorTexture.js.map +1 -1
  19. package/dist/src/core/textures/ImageTexture.js +24 -16
  20. package/dist/src/core/textures/ImageTexture.js.map +1 -1
  21. package/dist/tsconfig.dist.tsbuildinfo +1 -1
  22. package/dist/tsconfig.tsbuildinfo +1 -1
  23. package/package.json +1 -1
  24. package/src/core/CoreNode.test.ts +438 -0
  25. package/src/core/CoreNode.ts +140 -39
  26. package/src/core/CoreTextureManager.ts +101 -22
  27. package/src/core/Stage.ts +3 -2
  28. package/src/core/TextureError.ts +2 -0
  29. package/src/core/lib/ImageWorker.ts +50 -27
  30. package/src/core/lib/Matrix3d.test.ts +72 -0
  31. package/src/core/lib/Matrix3d.ts +30 -3
  32. package/src/core/textures/ColorTexture.ts +3 -1
  33. package/src/core/textures/ImageTexture.ts +29 -20
@@ -811,6 +811,28 @@ export class CoreNode extends EventEmitter {
811
811
  public isRenderable = false;
812
812
  public renderState: CoreNodeRenderState = CoreNodeRenderState.Init;
813
813
  public isSimple = true;
814
+ /**
815
+ * `true` when `localTransform` is in identity-shape (ta=1, tb=0, tc=0, td=1)
816
+ * — i.e. a pure translation. Lets the simple-path `updateLocalTransform`
817
+ * skip redundant ta/tb/tc/td writes between frames. Defaults to `true`
818
+ * because the matrix is eagerly allocated as an identity in the constructor.
819
+ */
820
+ public _localIsTranslate = true;
821
+ /**
822
+ * Cached result of the texture `contain` resizeMode check used by
823
+ * `updateLocalTransform` and `updateIsSimple`. Updated whenever the
824
+ * texture or textureOptions change (via `updateIsSimple`), so the hot
825
+ * paths can avoid the optional-chain + string compare on every frame.
826
+ */
827
+ public _hasContainResize = false;
828
+ /**
829
+ * `true` when `globalTransform` is in identity-shape (ta=1, tb=0, tc=0, td=1).
830
+ * Propagates from parent: a node's global is translate-only iff the parent's
831
+ * global is translate-only AND the node itself is `isSimple`. Default `true`
832
+ * because freshly-constructed nodes have no transform applied yet, and the
833
+ * Stage root is configured with an identity-shape global.
834
+ */
835
+ public _globalIsTranslate = true;
814
836
 
815
837
  public worldAlpha = 1;
816
838
  public premultipliedColorTl = 0;
@@ -835,6 +857,13 @@ export class CoreNode extends EventEmitter {
835
857
  constructor(readonly stage: Stage, props: CoreNodeProps) {
836
858
  super();
837
859
 
860
+ // Eagerly allocate the local/global transform matrices as identity.
861
+ // This keeps the field's hidden class monomorphic (Matrix3d, not
862
+ // Matrix3d|undefined) from construction onward and lets the simple
863
+ // / translate-only fast paths take effect on the very first update.
864
+ this.localTransform = Matrix3d.identity();
865
+ this.globalTransform = Matrix3d.identity();
866
+
838
867
  //inital update type
839
868
  let initialUpdateType =
840
869
  UpdateType.Local | UpdateType.RenderBounds | UpdateType.RenderState;
@@ -1089,7 +1118,18 @@ export class CoreNode extends EventEmitter {
1089
1118
  const { x, y } = p;
1090
1119
 
1091
1120
  if (this.isSimple) {
1121
+ // Fast path: when localTransform is already in identity-shape
1122
+ // (ta=1, tb=0, tc=0, td=1), only tx/ty change between frames, so we
1123
+ // skip the 4 redundant field writes Matrix3d.translate would do.
1124
+ // _localIsTranslate becomes stale when a node was non-simple (had
1125
+ // rotation/scale/mount) on a previous frame — in that case do a
1126
+ // full reset to identity-translate.
1127
+ if (this._localIsTranslate === true) {
1128
+ this.localTransform!.setTranslate(x, y);
1129
+ return;
1130
+ }
1092
1131
  this.localTransform = Matrix3d.translate(x, y, this.localTransform);
1132
+ this._localIsTranslate = true;
1093
1133
  return;
1094
1134
  }
1095
1135
 
@@ -1097,10 +1137,15 @@ export class CoreNode extends EventEmitter {
1097
1137
  const mountTranslateX = p.mountX * w;
1098
1138
  const mountTranslateY = p.mountY * h;
1099
1139
 
1100
- if (p.rotation !== 0 || p.scaleX !== 1 || p.scaleY !== 1) {
1101
- const scaleRotate = Matrix3d.rotate(p.rotation, Matrix3d.temp).scale(
1102
- p.scaleX,
1103
- p.scaleY,
1140
+ const rotation = p.rotation;
1141
+ const scaleX = p.scaleX;
1142
+ const scaleY = p.scaleY;
1143
+
1144
+ if (rotation !== 0) {
1145
+ // Full rotation (+ optional scale + pivot)
1146
+ const scaleRotate = Matrix3d.rotate(rotation, Matrix3d.temp).scale(
1147
+ scaleX,
1148
+ scaleY,
1104
1149
  );
1105
1150
  const pivotTranslateX = p.pivotX * w;
1106
1151
  const pivotTranslateY = p.pivotY * h;
@@ -1112,7 +1157,21 @@ export class CoreNode extends EventEmitter {
1112
1157
  )
1113
1158
  .multiply(scaleRotate)
1114
1159
  .translate(-pivotTranslateX, -pivotTranslateY);
1160
+ } else if (scaleX !== 1 || scaleY !== 1) {
1161
+ // Scale (+ optional pivot) without rotation — skip the rotate matrix
1162
+ // and the 8-mul multiply; `.scale()` is a 4-mul in-place op.
1163
+ const pivotTranslateX = p.pivotX * w;
1164
+ const pivotTranslateY = p.pivotY * h;
1165
+
1166
+ this.localTransform = Matrix3d.translate(
1167
+ x - mountTranslateX + pivotTranslateX,
1168
+ y - mountTranslateY + pivotTranslateY,
1169
+ this.localTransform,
1170
+ )
1171
+ .scale(scaleX, scaleY)
1172
+ .translate(-pivotTranslateX, -pivotTranslateY);
1115
1173
  } else {
1174
+ // Mount (or texture-contain) only — pure translation.
1116
1175
  this.localTransform = Matrix3d.translate(
1117
1176
  x - mountTranslateX,
1118
1177
  y - mountTranslateY,
@@ -1120,12 +1179,13 @@ export class CoreNode extends EventEmitter {
1120
1179
  );
1121
1180
  }
1122
1181
 
1123
- // Handle 'contain' resize mode
1182
+ // Handle 'contain' resize mode (cached check; dimensions still need
1183
+ // a runtime null-check because they're populated asynchronously).
1124
1184
  const texture = p.texture;
1125
1185
  if (
1126
- texture &&
1127
- texture.dimensions &&
1128
- p.textureOptions.resizeMode?.type === 'contain'
1186
+ this._hasContainResize === true &&
1187
+ texture !== null &&
1188
+ texture.dimensions !== null
1129
1189
  ) {
1130
1190
  let resizeModeScaleX = 1;
1131
1191
  let resizeModeScaleY = 1;
@@ -1157,17 +1217,23 @@ export class CoreNode extends EventEmitter {
1157
1217
  .translate(extraX, extraY)
1158
1218
  .scale(resizeModeScaleX, resizeModeScaleY);
1159
1219
  }
1220
+
1221
+ this._localIsTranslate = false;
1160
1222
  }
1161
1223
 
1162
1224
  updateIsSimple() {
1163
1225
  const p = this.props;
1226
+ // Cache the texture-contain check so updateLocalTransform doesn't have to
1227
+ // run the optional-chain + string compare on every Local update.
1228
+ this._hasContainResize =
1229
+ p.texture !== null && p.textureOptions?.resizeMode?.type === 'contain';
1164
1230
  this.isSimple =
1165
1231
  p.rotation === 0 &&
1166
1232
  p.scaleX === 1 &&
1167
1233
  p.scaleY === 1 &&
1168
1234
  p.mountX === 0 &&
1169
1235
  p.mountY === 0 &&
1170
- !(p.texture && p.textureOptions.resizeMode?.type === 'contain');
1236
+ this._hasContainResize === false;
1171
1237
  }
1172
1238
 
1173
1239
  /**
@@ -1209,6 +1275,10 @@ export class CoreNode extends EventEmitter {
1209
1275
  }
1210
1276
 
1211
1277
  if (updateType & UpdateType.Global) {
1278
+ const lt = this.localTransform!;
1279
+ const gt = this.globalTransform!;
1280
+ let fastPathApplied = false;
1281
+
1212
1282
  if (
1213
1283
  USE_RTT &&
1214
1284
  this.parentHasRenderTexture === true &&
@@ -1216,7 +1286,7 @@ export class CoreNode extends EventEmitter {
1216
1286
  ) {
1217
1287
  // we are at the start of the RTT chain, so we need to reset the globalTransform
1218
1288
  // for correct RTT rendering
1219
- this.globalTransform = Matrix3d.identity(this.globalTransform);
1289
+ Matrix3d.identity(gt);
1220
1290
 
1221
1291
  // Maintain a full scene global transform for bounds detection
1222
1292
  const parentTransform =
@@ -1225,7 +1295,10 @@ export class CoreNode extends EventEmitter {
1225
1295
  this.sceneGlobalTransform = Matrix3d.copy(
1226
1296
  parentTransform,
1227
1297
  this.sceneGlobalTransform,
1228
- ).translateOrMultiply(this.localTransform!);
1298
+ ).translateOrMultiply(lt);
1299
+
1300
+ // identity * local => translate-only iff this node is simple
1301
+ this._globalIsTranslate = this.isSimple;
1229
1302
  } else if (
1230
1303
  USE_RTT &&
1231
1304
  this.parentHasRenderTexture === true &&
@@ -1234,32 +1307,48 @@ export class CoreNode extends EventEmitter {
1234
1307
  // we're part of an RTT chain but our parent is not the main RTT node
1235
1308
  // so we need to propogate the sceneGlobalTransform of the parent
1236
1309
  // to maintain a full scene global transform for bounds detection
1237
- const parentSceneTransform =
1238
- parent.sceneGlobalTransform || this.localTransform!;
1310
+ const parentSceneTransform = parent.sceneGlobalTransform || lt;
1239
1311
 
1240
1312
  this.sceneGlobalTransform = Matrix3d.copy(
1241
1313
  parentSceneTransform,
1242
1314
  this.sceneGlobalTransform,
1243
- ).translateOrMultiply(this.localTransform!);
1315
+ ).translateOrMultiply(lt);
1244
1316
 
1245
- this.globalTransform = Matrix3d.copy(
1246
- parent.globalTransform || this.localTransform!,
1247
- this.globalTransform,
1248
- );
1317
+ Matrix3d.copy(parent.globalTransform!, gt);
1318
+
1319
+ // Conservative: RTT chains rarely hit the translate fast path
1320
+ this._globalIsTranslate = false;
1249
1321
  } else {
1250
- this.globalTransform = Matrix3d.copy(
1251
- parent.globalTransform || this.localTransform!,
1252
- this.globalTransform,
1253
- );
1322
+ // Common non-RTT path
1323
+ const parentGT = parent.globalTransform!;
1324
+ if (this.isSimple === true && parent._globalIsTranslate === true) {
1325
+ // Translate-only fast path: parent global and local are both pure
1326
+ // translations, so the resulting global is also a pure translation
1327
+ // and collapses to 2 adds on tx/ty.
1328
+ if (this._globalIsTranslate === false) {
1329
+ // Transitioning back into translate-only — reset ta/tb/tc/td
1330
+ // that may have been left non-identity by a prior frame.
1331
+ gt.ta = 1;
1332
+ gt.tb = 0;
1333
+ gt.tc = 0;
1334
+ gt.td = 1;
1335
+ }
1336
+ gt.setTranslate(parentGT.tx + lt.tx, parentGT.ty + lt.ty);
1337
+ this._globalIsTranslate = true;
1338
+ fastPathApplied = true;
1339
+ } else {
1340
+ Matrix3d.copy(parentGT, gt);
1341
+ this._globalIsTranslate =
1342
+ this.isSimple === true && parent._globalIsTranslate === true;
1343
+ }
1254
1344
  }
1255
1345
 
1256
- if (this.isSimple) {
1257
- this.globalTransform.translate(
1258
- this.localTransform!.tx,
1259
- this.localTransform!.ty,
1260
- );
1261
- } else {
1262
- this.globalTransform.translateOrMultiply(this.localTransform!);
1346
+ if (fastPathApplied === false) {
1347
+ if (this.isSimple) {
1348
+ gt.translate(lt.tx, lt.ty);
1349
+ } else {
1350
+ gt.translateOrMultiply(lt);
1351
+ }
1263
1352
  }
1264
1353
  this.calculateRenderCoords();
1265
1354
  this.updateBoundingRect();
@@ -1380,18 +1469,30 @@ export class CoreNode extends EventEmitter {
1380
1469
  childClippingRect = NO_CLIPPING_RECT;
1381
1470
  }
1382
1471
 
1383
- for (let i = 0, length = this.children.length; i < length; i++) {
1384
- const child = this.children[i] as CoreNode;
1385
-
1386
- if (childUpdateType !== 0) {
1472
+ const children = this.children;
1473
+ const length = children.length;
1474
+ if (childUpdateType !== 0) {
1475
+ // Specialized loop: OR-in the inherited update bits for every child,
1476
+ // then update if non-zero. Avoids the per-iter `childUpdateType !== 0`
1477
+ // compare.
1478
+ for (let i = 0; i < length; i++) {
1479
+ const child = children[i] as CoreNode;
1387
1480
  child.updateType |= childUpdateType;
1481
+ if (child.updateType === 0) {
1482
+ continue;
1483
+ }
1484
+ child.update(delta, childClippingRect);
1388
1485
  }
1389
-
1390
- if (child.updateType === 0) {
1391
- continue;
1486
+ } else {
1487
+ // Specialized loop: nothing to inherit, so only walk children that
1488
+ // already have pending work of their own.
1489
+ for (let i = 0; i < length; i++) {
1490
+ const child = children[i] as CoreNode;
1491
+ if (child.updateType === 0) {
1492
+ continue;
1493
+ }
1494
+ child.update(delta, childClippingRect);
1392
1495
  }
1393
-
1394
- child.update(delta, childClippingRect);
1395
1496
  }
1396
1497
  }
1397
1498
 
@@ -1503,7 +1604,7 @@ export class CoreNode extends EventEmitter {
1503
1604
  const renderCoords = (this.sceneRenderCoords ||
1504
1605
  this.renderCoords) as RenderCoords;
1505
1606
 
1506
- if (transform.tb === 0 || transform.tc === 0) {
1607
+ if (transform.tb === 0 && transform.tc === 0) {
1507
1608
  this.renderBound = createBound(
1508
1609
  renderCoords.x1,
1509
1610
  renderCoords.y1,
@@ -281,12 +281,21 @@ export class CoreTextureManager extends EventEmitter {
281
281
  );
282
282
  } else {
283
283
  console.warn(
284
- '[Lightning] Imageworker is 0 or not supported on this browser. Image loading will be slower.',
284
+ '[Lightning] Image worker count is 0 or workers are not supported on this browser. Image loading will be slower.',
285
285
  );
286
286
  }
287
287
 
288
288
  this.initialized = true;
289
289
  this.emit('initialized');
290
+
291
+ // Anything that arrived before initialization completed is now safe to
292
+ // process. Without this, queued textures would sit until the next frame
293
+ // tick happens to call processSome().
294
+ if (this.uploadTextureQueue.size > 0) {
295
+ this.processSome(Infinity).catch((err) => {
296
+ console.error('Failed to drain pre-init texture queue:', err);
297
+ });
298
+ }
290
299
  }
291
300
 
292
301
  /**
@@ -295,6 +304,9 @@ export class CoreTextureManager extends EventEmitter {
295
304
  * @param texture - The texture to upload
296
305
  */
297
306
  enqueueUploadTexture(texture: Texture): void {
307
+ if (texture.state === 'failed' || texture.state === 'freed') {
308
+ return;
309
+ }
298
310
  this.uploadTextureQueue.add(texture);
299
311
  }
300
312
 
@@ -308,7 +320,6 @@ export class CoreTextureManager extends EventEmitter {
308
320
  textureType: Type,
309
321
  props: ExtractProps<TextureMap[Type]>,
310
322
  ): InstanceType<TextureMap[Type]> {
311
- let texture: Texture | undefined;
312
323
  const TextureClass = this.txConstructors[textureType];
313
324
  if (!TextureClass) {
314
325
  throw new TextureError(
@@ -316,20 +327,26 @@ export class CoreTextureManager extends EventEmitter {
316
327
  `Texture type "${textureType}" is not registered`,
317
328
  );
318
329
  }
319
- const resolvedProps = TextureClass.resolveDefaults(props as any);
320
- const cacheKey = TextureClass.makeCacheKey(resolvedProps as any);
321
- if (cacheKey && this.keyCache.has(cacheKey)) {
322
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
323
- texture = this.keyCache.get(cacheKey)!;
324
- } else {
325
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any
326
- texture = new TextureClass(this, resolvedProps as any);
327
330
 
328
- if (cacheKey) {
329
- this.initTextureToCache(texture, cacheKey);
331
+ // Cache key is computed from raw props (each Texture's makeCacheKey
332
+ // inlines its own defaults) so we can skip the resolveDefaults
333
+ // allocation on a cache hit.
334
+ const cacheKey = TextureClass.makeCacheKey(props as any);
335
+ if (cacheKey) {
336
+ const cached = this.keyCache.get(cacheKey);
337
+ if (cached) {
338
+ return cached as InstanceType<TextureMap[Type]>;
330
339
  }
331
340
  }
332
341
 
342
+ const resolvedProps = TextureClass.resolveDefaults(props as any);
343
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any
344
+ const texture = new TextureClass(this, resolvedProps as any);
345
+
346
+ if (cacheKey) {
347
+ this.initTextureToCache(texture, cacheKey);
348
+ }
349
+
333
350
  return texture as InstanceType<TextureMap[Type]>;
334
351
  }
335
352
 
@@ -381,7 +398,10 @@ export class CoreTextureManager extends EventEmitter {
381
398
  console.error(`Failed to upload texture:`, err);
382
399
  texture.setState(
383
400
  'failed',
384
- new TextureError(TextureErrorCode.TEXTURE_DATA_NULL),
401
+ new TextureError(
402
+ TextureErrorCode.TEXTURE_UPLOAD_FAILED,
403
+ err instanceof Error ? err.message : undefined,
404
+ ),
385
405
  );
386
406
  });
387
407
  return;
@@ -432,7 +452,7 @@ export class CoreTextureManager extends EventEmitter {
432
452
  }
433
453
 
434
454
  const coreContext = texture.loadCtxTexture();
435
- if (coreContext !== null && coreContext.state === 'loaded') {
455
+ if (coreContext.state === 'loaded') {
436
456
  texture.setState('loaded');
437
457
  return;
438
458
  }
@@ -460,24 +480,83 @@ export class CoreTextureManager extends EventEmitter {
460
480
  const platform = this.platform;
461
481
  const startTime = platform.getTimeStamp();
462
482
 
463
- // Process uploads - await each upload to prevent GPU overload
483
+ // Decode / fetch ("getTextureData") is IO-bound and parallelisable across
484
+ // image workers, while GPU upload is effectively serial. Keep a small
485
+ // sliding window of in-flight data fetches so the next decode runs while
486
+ // we're uploading the current one.
487
+ const prefetchLimit = Math.max(1, this.numImageWorkers);
488
+ const pending: Array<{ texture: Texture; data: Promise<unknown> }> = [];
489
+
490
+ // Helper avoids TS narrowing `texture.state` permanently after the first
491
+ // discriminated check — the property is mutable and can transition across
492
+ // awaits, so we need to re-read it freshly each time.
493
+ const isDead = (texture: Texture): boolean =>
494
+ texture.state === 'failed' || texture.state === 'freed';
495
+
496
+ const fillPrefetch = () => {
497
+ while (
498
+ pending.length < prefetchLimit &&
499
+ this.uploadTextureQueue.size > 0
500
+ ) {
501
+ const [texture] = this.uploadTextureQueue;
502
+ if (!texture) break;
503
+ this.uploadTextureQueue.delete(texture);
504
+
505
+ if (isDead(texture)) {
506
+ continue;
507
+ }
508
+
509
+ // Swallow the rejection here so an early failure doesn't surface as
510
+ // an unhandled promise rejection while it sits in the prefetch
511
+ // window; we re-check state after awaiting.
512
+ const data =
513
+ texture.textureData === null
514
+ ? texture.getTextureData().catch((err) => {
515
+ console.error('Failed to fetch texture data:', err);
516
+ return null;
517
+ })
518
+ : Promise.resolve(texture.textureData);
519
+
520
+ pending.push({ texture, data });
521
+ }
522
+ };
523
+
524
+ fillPrefetch();
525
+
464
526
  while (
465
- this.uploadTextureQueue.size > 0 &&
527
+ pending.length > 0 &&
466
528
  platform.getTimeStamp() - startTime < maxProcessingTime
467
529
  ) {
468
- const [texture] = this.uploadTextureQueue;
469
- if (!texture) break;
470
- this.uploadTextureQueue.delete(texture);
530
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
531
+ const next = pending.shift()!;
532
+ // Top up the prefetch window before awaiting — the next decode starts
533
+ // now and overlaps with this upload.
534
+ fillPrefetch();
535
+
536
+ if (isDead(next.texture)) {
537
+ continue;
538
+ }
539
+
471
540
  try {
472
- if (texture.textureData === null) {
473
- await texture.getTextureData();
541
+ await next.data;
542
+ if (isDead(next.texture)) {
543
+ continue;
474
544
  }
475
- await this.uploadTexture(texture);
545
+ await this.uploadTexture(next.texture);
476
546
  } catch (error) {
477
547
  console.error('Failed to upload texture:', error);
478
548
  // Continue with next texture instead of stopping entire queue
479
549
  }
480
550
  }
551
+
552
+ // Time ran out before we got to these. Put them back so we don't lose
553
+ // them — their getTextureData() is already in flight and will populate
554
+ // `textureData` for the next tick.
555
+ for (const { texture } of pending) {
556
+ if (!isDead(texture)) {
557
+ this.uploadTextureQueue.add(texture);
558
+ }
559
+ }
481
560
  }
482
561
 
483
562
  public hasUpdates(): boolean {
package/src/core/Stage.ts CHANGED
@@ -359,9 +359,10 @@ export class Stage {
359
359
 
360
360
  this.root = rootNode;
361
361
 
362
- // Initialize root node properties
362
+ // Initialize root node properties. Copy in place into the eagerly-allocated
363
+ // matrices so the hidden class for these fields stays stable.
363
364
  rootNode.updateLocalTransform();
364
- rootNode.globalTransform = Matrix3d.copy(rootNode.localTransform!);
365
+ Matrix3d.copy(rootNode.localTransform!, rootNode.globalTransform);
365
366
  rootNode.sceneGlobalTransform = Matrix3d.copy(rootNode.localTransform!);
366
367
  rootNode.calculateRenderCoords();
367
368
  rootNode.updateBoundingRect();
@@ -2,6 +2,7 @@ export enum TextureErrorCode {
2
2
  MEMORY_THRESHOLD_EXCEEDED = 'MEMORY_THRESHOLD_EXCEEDED',
3
3
  TEXTURE_DATA_NULL = 'TEXTURE_DATA_NULL',
4
4
  TEXTURE_TYPE_NOT_REGISTERED = 'TEXTURE_TYPE_NOT_REGISTERED',
5
+ TEXTURE_UPLOAD_FAILED = 'TEXTURE_UPLOAD_FAILED',
5
6
  }
6
7
 
7
8
  const defaultMessages: Record<TextureErrorCode, string> = {
@@ -9,6 +10,7 @@ const defaultMessages: Record<TextureErrorCode, string> = {
9
10
  [TextureErrorCode.TEXTURE_DATA_NULL]: 'Texture data is null',
10
11
  [TextureErrorCode.TEXTURE_TYPE_NOT_REGISTERED]:
11
12
  'Texture type is not registered',
13
+ [TextureErrorCode.TEXTURE_UPLOAD_FAILED]: 'Texture upload failed',
12
14
  };
13
15
 
14
16
  export class TextureError extends Error {
@@ -67,7 +67,7 @@ function createImageWorker() {
67
67
 
68
68
  var blob = xhr.response;
69
69
  var withAlphaChannel =
70
- premultiplyAlpha !== undefined
70
+ premultiplyAlpha !== undefined && premultiplyAlpha !== null
71
71
  ? premultiplyAlpha
72
72
  : hasAlphaChannel(blob.type);
73
73
 
@@ -83,7 +83,7 @@ function createImageWorker() {
83
83
  imageOrientation: 'none',
84
84
  })
85
85
  .then(function (data) {
86
- resolve({ data, premultiplyAlpha: premultiplyAlpha });
86
+ resolve({ data: data, premultiplyAlpha: withAlphaChannel });
87
87
  })
88
88
  .catch(function (error) {
89
89
  reject(error);
@@ -91,13 +91,13 @@ function createImageWorker() {
91
91
  return;
92
92
  } else if (
93
93
  supportsOptionsCreateImageBitmap === false &&
94
- supportsOptionsCreateImageBitmap === false
94
+ supportsFullCreateImageBitmap === false
95
95
  ) {
96
96
  // Fallback for browsers that do not support createImageBitmap with options
97
97
  // this is supported for Chrome v50 to v52/54 that doesn't support options
98
98
  createImageBitmap(blob)
99
99
  .then(function (data) {
100
- resolve({ data, premultiplyAlpha: premultiplyAlpha });
100
+ resolve({ data: data, premultiplyAlpha: withAlphaChannel });
101
101
  })
102
102
  .catch(function (error) {
103
103
  reject(error);
@@ -109,7 +109,7 @@ function createImageWorker() {
109
109
  imageOrientation: 'none',
110
110
  })
111
111
  .then(function (data) {
112
- resolve({ data, premultiplyAlpha: premultiplyAlpha });
112
+ resolve({ data: data, premultiplyAlpha: withAlphaChannel });
113
113
  })
114
114
  .catch(function (error) {
115
115
  reject(error);
@@ -156,7 +156,6 @@ function createImageWorker() {
156
156
  /* eslint-enable */
157
157
 
158
158
  export class ImageWorkerManager {
159
- imageWorkersEnabled = true;
160
159
  messageManager: Record<number, MessageCallback> = {};
161
160
  workers: Worker[] = [];
162
161
  workerLoad: number[] = [];
@@ -172,6 +171,8 @@ export class ImageWorkerManager {
172
171
  );
173
172
  this.workers.forEach((worker, index) => {
174
173
  worker.onmessage = (event) => this.handleMessage(event, index);
174
+ worker.onerror = (event) => this.handleWorkerError(event, index);
175
+ worker.onmessageerror = (event) => this.handleWorkerError(event, index);
175
176
  });
176
177
  }
177
178
 
@@ -194,6 +195,25 @@ export class ImageWorkerManager {
194
195
  }
195
196
  }
196
197
 
198
+ private handleWorkerError(event: Event | ErrorEvent, workerIndex: number) {
199
+ const message =
200
+ event instanceof ErrorEvent && event.message
201
+ ? event.message
202
+ : 'Image worker encountered an unrecoverable error';
203
+
204
+ // Reject all pending requests; we cannot map a worker-level crash to a
205
+ // specific message id, so fail everything outstanding to avoid hangs.
206
+ for (const id in this.messageManager) {
207
+ const msg = this.messageManager[id];
208
+ if (msg) {
209
+ const [, reject] = msg;
210
+ delete this.messageManager[id];
211
+ reject(new Error(message));
212
+ }
213
+ }
214
+ this.workerLoad[workerIndex] = 0;
215
+ }
216
+
197
217
  private createWorkers(
198
218
  numWorkers = 1,
199
219
  createImageBitmapSupport: CreateImageBitmapSupport,
@@ -224,19 +244,22 @@ export class ImageWorkerManager {
224
244
  const blob: Blob = new Blob([workerCode], {
225
245
  type: 'application/javascript',
226
246
  });
227
- const blobURL: string = (self.URL ? URL : webkitURL).createObjectURL(blob);
247
+ const urlFactory = self.URL ? URL : webkitURL;
248
+ const blobURL: string = urlFactory.createObjectURL(blob);
228
249
  const workers: Worker[] = [];
229
250
  for (let i = 0; i < numWorkers; i++) {
230
251
  workers.push(new Worker(blobURL));
231
252
  this.workerLoad.push(0);
232
253
  }
254
+ // Workers retain the script; the URL itself is no longer needed.
255
+ urlFactory.revokeObjectURL(blobURL);
233
256
  return workers;
234
257
  }
235
258
 
236
259
  private getNextWorkerIndex(): number {
237
260
  if (this.workers.length === 0) return -1;
238
261
 
239
- let minLoad = 99;
262
+ let minLoad = Infinity;
240
263
  let workerIndex = 0;
241
264
 
242
265
  for (let i = 0; i < this.workers.length; i++) {
@@ -264,26 +287,26 @@ export class ImageWorkerManager {
264
287
  ): Promise<TextureData> {
265
288
  return new Promise((resolve, reject) => {
266
289
  try {
267
- if (this.workers) {
268
- const id = this.nextId++;
269
- this.messageManager[id] = [resolve, reject];
270
- const nextWorkerIndex = this.getNextWorkerIndex();
271
-
272
- if (nextWorkerIndex !== -1) {
273
- const worker = this.workers[nextWorkerIndex];
274
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
275
- this.workerLoad[nextWorkerIndex]!++;
276
- worker!.postMessage({
277
- id,
278
- src: src,
279
- premultiplyAlpha,
280
- sx,
281
- sy,
282
- sw,
283
- sh,
284
- });
285
- }
290
+ const nextWorkerIndex = this.getNextWorkerIndex();
291
+ if (nextWorkerIndex === -1) {
292
+ reject(new Error('No image workers available'));
293
+ return;
286
294
  }
295
+
296
+ const id = this.nextId++;
297
+ this.messageManager[id] = [resolve, reject];
298
+ const worker = this.workers[nextWorkerIndex];
299
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
300
+ this.workerLoad[nextWorkerIndex]!++;
301
+ worker!.postMessage({
302
+ id,
303
+ src: src,
304
+ premultiplyAlpha,
305
+ sx,
306
+ sy,
307
+ sw,
308
+ sh,
309
+ });
287
310
  } catch (error) {
288
311
  reject(error);
289
312
  }