@solidtv/renderer 1.1.1 → 1.1.3

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 (52) hide show
  1. package/README.md +1 -4
  2. package/dist/src/core/CoreNode.d.ts +27 -0
  3. package/dist/src/core/CoreNode.js +164 -43
  4. package/dist/src/core/CoreNode.js.map +1 -1
  5. package/dist/src/core/CoreTextureManager.d.ts +0 -4
  6. package/dist/src/core/CoreTextureManager.js +70 -17
  7. package/dist/src/core/CoreTextureManager.js.map +1 -1
  8. package/dist/src/core/Stage.js +5 -4
  9. package/dist/src/core/Stage.js.map +1 -1
  10. package/dist/src/core/lib/Matrix3d.d.ts +9 -0
  11. package/dist/src/core/lib/Matrix3d.js +29 -3
  12. package/dist/src/core/lib/Matrix3d.js.map +1 -1
  13. package/dist/src/core/lib/WebGlContextWrapper.js +0 -4
  14. package/dist/src/core/lib/WebGlContextWrapper.js.map +1 -1
  15. package/dist/src/core/lib/collectionUtils.js +1 -1
  16. package/dist/src/core/lib/collectionUtils.js.map +1 -1
  17. package/dist/src/core/renderers/canvas/CanvasRenderer.d.ts +3 -3
  18. package/dist/src/core/renderers/canvas/CanvasRenderer.js +3 -3
  19. package/dist/src/core/renderers/canvas/CanvasRenderer.js.map +1 -1
  20. package/dist/src/core/text-rendering/CanvasFontHandler.d.ts +1 -1
  21. package/dist/src/core/text-rendering/CanvasFontHandler.js +3 -3
  22. package/dist/src/core/text-rendering/CanvasFontHandler.js.map +1 -1
  23. package/dist/src/core/text-rendering/CanvasTextRenderer.d.ts +2 -3
  24. package/dist/src/core/text-rendering/CanvasTextRenderer.js +2 -1
  25. package/dist/src/core/text-rendering/CanvasTextRenderer.js.map +1 -1
  26. package/dist/src/core/text-rendering/SdfFontHandler.js +3 -3
  27. package/dist/src/core/text-rendering/SdfFontHandler.js.map +1 -1
  28. package/dist/src/core/text-rendering/Utils.js.map +1 -1
  29. package/dist/src/core/textures/ImageTexture.js +1 -1
  30. package/dist/src/core/textures/ImageTexture.js.map +1 -1
  31. package/dist/src/core/textures/SubTexture.js.map +1 -1
  32. package/dist/src/core/textures/Texture.d.ts +6 -0
  33. package/dist/src/core/textures/Texture.js +6 -0
  34. package/dist/src/core/textures/Texture.js.map +1 -1
  35. package/dist/tsconfig.dist.tsbuildinfo +1 -1
  36. package/package.json +7 -7
  37. package/src/core/CoreNode.test.ts +438 -0
  38. package/src/core/CoreNode.ts +176 -61
  39. package/src/core/CoreTextureManager.ts +77 -20
  40. package/src/core/Stage.ts +10 -7
  41. package/src/core/lib/Matrix3d.test.ts +72 -0
  42. package/src/core/lib/Matrix3d.ts +30 -3
  43. package/src/core/lib/WebGlContextWrapper.ts +0 -2
  44. package/src/core/lib/collectionUtils.ts +1 -1
  45. package/src/core/renderers/canvas/CanvasRenderer.ts +3 -3
  46. package/src/core/text-rendering/CanvasFontHandler.ts +4 -4
  47. package/src/core/text-rendering/CanvasTextRenderer.ts +7 -2
  48. package/src/core/text-rendering/SdfFontHandler.ts +4 -4
  49. package/src/core/text-rendering/Utils.ts +0 -2
  50. package/src/core/textures/ImageTexture.ts +1 -4
  51. package/src/core/textures/SubTexture.ts +0 -2
  52. package/src/core/textures/Texture.ts +7 -0
@@ -761,10 +761,15 @@ export class CoreNode extends EventEmitter {
761
761
  protected _id: number = getNewId();
762
762
  readonly props: CoreNodeProps;
763
763
  public readonly isCoreNode = true as const;
764
+ /**
765
+ * Lazily allocated on first `animateProp` call. Animations are rare across
766
+ * the scene graph, so deferring the object literal avoids per-node GC
767
+ * pressure during construction.
768
+ */
764
769
  private _animations: Record<
765
770
  string,
766
771
  { controller: AnimationConfig; settings: Partial<AnimationSettings> }
767
- > = {};
772
+ > | null = null;
768
773
 
769
774
  // WebGL Render Op State
770
775
  public renderOpBufferIdx: number = 0;
@@ -811,6 +816,28 @@ export class CoreNode extends EventEmitter {
811
816
  public isRenderable = false;
812
817
  public renderState: CoreNodeRenderState = CoreNodeRenderState.Init;
813
818
  public isSimple = true;
819
+ /**
820
+ * `true` when `localTransform` is in identity-shape (ta=1, tb=0, tc=0, td=1)
821
+ * — i.e. a pure translation. Lets the simple-path `updateLocalTransform`
822
+ * skip redundant ta/tb/tc/td writes between frames. Defaults to `true`
823
+ * because the matrix is eagerly allocated as an identity in the constructor.
824
+ */
825
+ public _localIsTranslate = true;
826
+ /**
827
+ * Cached result of the texture `contain` resizeMode check used by
828
+ * `updateLocalTransform` and `updateIsSimple`. Updated whenever the
829
+ * texture or textureOptions change (via `updateIsSimple`), so the hot
830
+ * paths can avoid the optional-chain + string compare on every frame.
831
+ */
832
+ public _hasContainResize = false;
833
+ /**
834
+ * `true` when `globalTransform` is in identity-shape (ta=1, tb=0, tc=0, td=1).
835
+ * Propagates from parent: a node's global is translate-only iff the parent's
836
+ * global is translate-only AND the node itself is `isSimple`. Default `true`
837
+ * because freshly-constructed nodes have no transform applied yet, and the
838
+ * Stage root is configured with an identity-shape global.
839
+ */
840
+ public _globalIsTranslate = true;
814
841
 
815
842
  public worldAlpha = 1;
816
843
  public premultipliedColorTl = 0;
@@ -835,6 +862,13 @@ export class CoreNode extends EventEmitter {
835
862
  constructor(readonly stage: Stage, props: CoreNodeProps) {
836
863
  super();
837
864
 
865
+ // Eagerly allocate the local/global transform matrices as identity.
866
+ // This keeps the field's hidden class monomorphic (Matrix3d, not
867
+ // Matrix3d|undefined) from construction onward and lets the simple
868
+ // / translate-only fast paths take effect on the very first update.
869
+ this.localTransform = Matrix3d.identity();
870
+ this.globalTransform = Matrix3d.identity();
871
+
838
872
  //inital update type
839
873
  let initialUpdateType =
840
874
  UpdateType.Local | UpdateType.RenderBounds | UpdateType.RenderState;
@@ -1089,7 +1123,18 @@ export class CoreNode extends EventEmitter {
1089
1123
  const { x, y } = p;
1090
1124
 
1091
1125
  if (this.isSimple) {
1126
+ // Fast path: when localTransform is already in identity-shape
1127
+ // (ta=1, tb=0, tc=0, td=1), only tx/ty change between frames, so we
1128
+ // skip the 4 redundant field writes Matrix3d.translate would do.
1129
+ // _localIsTranslate becomes stale when a node was non-simple (had
1130
+ // rotation/scale/mount) on a previous frame — in that case do a
1131
+ // full reset to identity-translate.
1132
+ if (this._localIsTranslate === true) {
1133
+ this.localTransform!.setTranslate(x, y);
1134
+ return;
1135
+ }
1092
1136
  this.localTransform = Matrix3d.translate(x, y, this.localTransform);
1137
+ this._localIsTranslate = true;
1093
1138
  return;
1094
1139
  }
1095
1140
 
@@ -1097,10 +1142,15 @@ export class CoreNode extends EventEmitter {
1097
1142
  const mountTranslateX = p.mountX * w;
1098
1143
  const mountTranslateY = p.mountY * h;
1099
1144
 
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,
1145
+ const rotation = p.rotation;
1146
+ const scaleX = p.scaleX;
1147
+ const scaleY = p.scaleY;
1148
+
1149
+ if (rotation !== 0) {
1150
+ // Full rotation (+ optional scale + pivot)
1151
+ const scaleRotate = Matrix3d.rotate(rotation, Matrix3d.temp).scale(
1152
+ scaleX,
1153
+ scaleY,
1104
1154
  );
1105
1155
  const pivotTranslateX = p.pivotX * w;
1106
1156
  const pivotTranslateY = p.pivotY * h;
@@ -1112,7 +1162,21 @@ export class CoreNode extends EventEmitter {
1112
1162
  )
1113
1163
  .multiply(scaleRotate)
1114
1164
  .translate(-pivotTranslateX, -pivotTranslateY);
1165
+ } else if (scaleX !== 1 || scaleY !== 1) {
1166
+ // Scale (+ optional pivot) without rotation — skip the rotate matrix
1167
+ // and the 8-mul multiply; `.scale()` is a 4-mul in-place op.
1168
+ const pivotTranslateX = p.pivotX * w;
1169
+ const pivotTranslateY = p.pivotY * h;
1170
+
1171
+ this.localTransform = Matrix3d.translate(
1172
+ x - mountTranslateX + pivotTranslateX,
1173
+ y - mountTranslateY + pivotTranslateY,
1174
+ this.localTransform,
1175
+ )
1176
+ .scale(scaleX, scaleY)
1177
+ .translate(-pivotTranslateX, -pivotTranslateY);
1115
1178
  } else {
1179
+ // Mount (or texture-contain) only — pure translation.
1116
1180
  this.localTransform = Matrix3d.translate(
1117
1181
  x - mountTranslateX,
1118
1182
  y - mountTranslateY,
@@ -1120,12 +1184,13 @@ export class CoreNode extends EventEmitter {
1120
1184
  );
1121
1185
  }
1122
1186
 
1123
- // Handle 'contain' resize mode
1187
+ // Handle 'contain' resize mode (cached check; dimensions still need
1188
+ // a runtime null-check because they're populated asynchronously).
1124
1189
  const texture = p.texture;
1125
1190
  if (
1126
- texture &&
1127
- texture.dimensions &&
1128
- p.textureOptions.resizeMode?.type === 'contain'
1191
+ this._hasContainResize === true &&
1192
+ texture !== null &&
1193
+ texture.dimensions !== null
1129
1194
  ) {
1130
1195
  let resizeModeScaleX = 1;
1131
1196
  let resizeModeScaleY = 1;
@@ -1157,17 +1222,23 @@ export class CoreNode extends EventEmitter {
1157
1222
  .translate(extraX, extraY)
1158
1223
  .scale(resizeModeScaleX, resizeModeScaleY);
1159
1224
  }
1225
+
1226
+ this._localIsTranslate = false;
1160
1227
  }
1161
1228
 
1162
1229
  updateIsSimple() {
1163
1230
  const p = this.props;
1231
+ // Cache the texture-contain check so updateLocalTransform doesn't have to
1232
+ // run the optional-chain + string compare on every Local update.
1233
+ this._hasContainResize =
1234
+ p.texture !== null && p.textureOptions?.resizeMode?.type === 'contain';
1164
1235
  this.isSimple =
1165
1236
  p.rotation === 0 &&
1166
1237
  p.scaleX === 1 &&
1167
1238
  p.scaleY === 1 &&
1168
1239
  p.mountX === 0 &&
1169
1240
  p.mountY === 0 &&
1170
- !(p.texture && p.textureOptions.resizeMode?.type === 'contain');
1241
+ this._hasContainResize === false;
1171
1242
  }
1172
1243
 
1173
1244
  /**
@@ -1209,6 +1280,10 @@ export class CoreNode extends EventEmitter {
1209
1280
  }
1210
1281
 
1211
1282
  if (updateType & UpdateType.Global) {
1283
+ const lt = this.localTransform!;
1284
+ const gt = this.globalTransform!;
1285
+ let fastPathApplied = false;
1286
+
1212
1287
  if (
1213
1288
  USE_RTT &&
1214
1289
  this.parentHasRenderTexture === true &&
@@ -1216,7 +1291,7 @@ export class CoreNode extends EventEmitter {
1216
1291
  ) {
1217
1292
  // we are at the start of the RTT chain, so we need to reset the globalTransform
1218
1293
  // for correct RTT rendering
1219
- this.globalTransform = Matrix3d.identity(this.globalTransform);
1294
+ Matrix3d.identity(gt);
1220
1295
 
1221
1296
  // Maintain a full scene global transform for bounds detection
1222
1297
  const parentTransform =
@@ -1225,7 +1300,10 @@ export class CoreNode extends EventEmitter {
1225
1300
  this.sceneGlobalTransform = Matrix3d.copy(
1226
1301
  parentTransform,
1227
1302
  this.sceneGlobalTransform,
1228
- ).translateOrMultiply(this.localTransform!);
1303
+ ).translateOrMultiply(lt);
1304
+
1305
+ // identity * local => translate-only iff this node is simple
1306
+ this._globalIsTranslate = this.isSimple;
1229
1307
  } else if (
1230
1308
  USE_RTT &&
1231
1309
  this.parentHasRenderTexture === true &&
@@ -1234,32 +1312,48 @@ export class CoreNode extends EventEmitter {
1234
1312
  // we're part of an RTT chain but our parent is not the main RTT node
1235
1313
  // so we need to propogate the sceneGlobalTransform of the parent
1236
1314
  // to maintain a full scene global transform for bounds detection
1237
- const parentSceneTransform =
1238
- parent.sceneGlobalTransform || this.localTransform!;
1315
+ const parentSceneTransform = parent.sceneGlobalTransform || lt;
1239
1316
 
1240
1317
  this.sceneGlobalTransform = Matrix3d.copy(
1241
1318
  parentSceneTransform,
1242
1319
  this.sceneGlobalTransform,
1243
- ).translateOrMultiply(this.localTransform!);
1320
+ ).translateOrMultiply(lt);
1244
1321
 
1245
- this.globalTransform = Matrix3d.copy(
1246
- parent.globalTransform || this.localTransform!,
1247
- this.globalTransform,
1248
- );
1322
+ Matrix3d.copy(parent.globalTransform!, gt);
1323
+
1324
+ // Conservative: RTT chains rarely hit the translate fast path
1325
+ this._globalIsTranslate = false;
1249
1326
  } else {
1250
- this.globalTransform = Matrix3d.copy(
1251
- parent.globalTransform || this.localTransform!,
1252
- this.globalTransform,
1253
- );
1327
+ // Common non-RTT path
1328
+ const parentGT = parent.globalTransform!;
1329
+ if (this.isSimple === true && parent._globalIsTranslate === true) {
1330
+ // Translate-only fast path: parent global and local are both pure
1331
+ // translations, so the resulting global is also a pure translation
1332
+ // and collapses to 2 adds on tx/ty.
1333
+ if (this._globalIsTranslate === false) {
1334
+ // Transitioning back into translate-only — reset ta/tb/tc/td
1335
+ // that may have been left non-identity by a prior frame.
1336
+ gt.ta = 1;
1337
+ gt.tb = 0;
1338
+ gt.tc = 0;
1339
+ gt.td = 1;
1340
+ }
1341
+ gt.setTranslate(parentGT.tx + lt.tx, parentGT.ty + lt.ty);
1342
+ this._globalIsTranslate = true;
1343
+ fastPathApplied = true;
1344
+ } else {
1345
+ Matrix3d.copy(parentGT, gt);
1346
+ this._globalIsTranslate =
1347
+ this.isSimple === true && parent._globalIsTranslate === true;
1348
+ }
1254
1349
  }
1255
1350
 
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!);
1351
+ if (fastPathApplied === false) {
1352
+ if (this.isSimple) {
1353
+ gt.translate(lt.tx, lt.ty);
1354
+ } else {
1355
+ gt.translateOrMultiply(lt);
1356
+ }
1263
1357
  }
1264
1358
  this.calculateRenderCoords();
1265
1359
  this.updateBoundingRect();
@@ -1380,18 +1474,30 @@ export class CoreNode extends EventEmitter {
1380
1474
  childClippingRect = NO_CLIPPING_RECT;
1381
1475
  }
1382
1476
 
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) {
1477
+ const children = this.children;
1478
+ const length = children.length;
1479
+ if (childUpdateType !== 0) {
1480
+ // Specialized loop: OR-in the inherited update bits for every child,
1481
+ // then update if non-zero. Avoids the per-iter `childUpdateType !== 0`
1482
+ // compare.
1483
+ for (let i = 0; i < length; i++) {
1484
+ const child = children[i] as CoreNode;
1387
1485
  child.updateType |= childUpdateType;
1486
+ if (child.updateType === 0) {
1487
+ continue;
1488
+ }
1489
+ child.update(delta, childClippingRect);
1388
1490
  }
1389
-
1390
- if (child.updateType === 0) {
1391
- continue;
1491
+ } else {
1492
+ // Specialized loop: nothing to inherit, so only walk children that
1493
+ // already have pending work of their own.
1494
+ for (let i = 0; i < length; i++) {
1495
+ const child = children[i] as CoreNode;
1496
+ if (child.updateType === 0) {
1497
+ continue;
1498
+ }
1499
+ child.update(delta, childClippingRect);
1392
1500
  }
1393
-
1394
- child.update(delta, childClippingRect);
1395
1501
  }
1396
1502
  }
1397
1503
 
@@ -1503,7 +1609,7 @@ export class CoreNode extends EventEmitter {
1503
1609
  const renderCoords = (this.sceneRenderCoords ||
1504
1610
  this.renderCoords) as RenderCoords;
1505
1611
 
1506
- if (transform.tb === 0 || transform.tc === 0) {
1612
+ if (transform.tb === 0 && transform.tc === 0) {
1507
1613
  this.renderBound = createBound(
1508
1614
  renderCoords.x1,
1509
1615
  renderCoords.y1,
@@ -2835,27 +2941,32 @@ export class CoreNode extends EventEmitter {
2835
2941
  value: number,
2836
2942
  settings: Partial<AnimationSettings>,
2837
2943
  ): IAnimationController {
2838
- const existing = this._animations[name];
2839
-
2840
- if (existing && existing.settings === settings) {
2841
- const controller = existing.controller;
2842
- const values = controller.props ? controller.props[name] : null;
2843
-
2844
- if (values) {
2845
- values.start = (this as any)[name] ?? 0;
2846
- values.target = value;
2847
- controller.progress = 0;
2848
-
2849
- if (settings.adaptiveDuration === true) {
2850
- const now = performance.now();
2851
- const elapsed = now - controller.lastRunTime;
2852
- controller.lastRunTime = now;
2853
- const duration = settings.duration ?? controller.duration;
2854
- controller.duration = elapsed < duration ? elapsed : duration;
2855
- }
2944
+ let animations = this._animations;
2945
+ if (animations !== null) {
2946
+ const existing = animations[name];
2947
+
2948
+ if (existing && existing.settings === settings) {
2949
+ const controller = existing.controller;
2950
+ const values = controller.props ? controller.props[name] : null;
2951
+
2952
+ if (values) {
2953
+ values.start = (this as any)[name] ?? 0;
2954
+ values.target = value;
2955
+ controller.progress = 0;
2956
+
2957
+ if (settings.adaptiveDuration === true) {
2958
+ const now = performance.now();
2959
+ const elapsed = now - controller.lastRunTime;
2960
+ controller.lastRunTime = now;
2961
+ const duration = settings.duration ?? controller.duration;
2962
+ controller.duration = elapsed < duration ? elapsed : duration;
2963
+ }
2856
2964
 
2857
- return controller.start();
2965
+ return controller.start();
2966
+ }
2858
2967
  }
2968
+ } else {
2969
+ animations = this._animations = {};
2859
2970
  }
2860
2971
 
2861
2972
  const animationProps: Partial<CoreNodeAnimateProps> = { [name]: value };
@@ -2865,12 +2976,16 @@ export class CoreNode extends EventEmitter {
2865
2976
  animationProps,
2866
2977
  settings,
2867
2978
  );
2868
- this._animations[name] = { controller, settings };
2979
+ animations[name] = { controller, settings };
2869
2980
  return controller.start();
2870
2981
  }
2871
2982
 
2872
2983
  animateToTarget(prop: string): number | undefined {
2873
- const animation = this._animations[prop];
2984
+ const animations = this._animations;
2985
+ if (animations === null) {
2986
+ return undefined;
2987
+ }
2988
+ const animation = animations[prop];
2874
2989
  if (!animation) {
2875
2990
  return undefined;
2876
2991
  }
@@ -159,16 +159,78 @@ export interface TextureOptions {
159
159
  resizeMode?: ResizeModeOptions;
160
160
  }
161
161
 
162
- export class CoreTextureManager extends EventEmitter {
162
+ /**
163
+ * Insertion-ordered, dedup'd FIFO queue used by the texture upload
164
+ * pipeline.
165
+ *
166
+ * Backed by an array (for ordered traversal without per-call iterator
167
+ * allocation) plus a Set (for O(1) membership and cancel-by-reference).
168
+ * Removed entries are tombstoned in the array — they're skipped on the
169
+ * next `shift()`, and the array is compacted once the consumed prefix
170
+ * grows large enough to be worth reclaiming.
171
+ */
172
+ class TextureUploadQueue {
173
+ private list: Texture[] = [];
174
+ private membership: Set<Texture> = new Set();
175
+ private head = 0;
176
+
177
+ get size(): number {
178
+ return this.membership.size;
179
+ }
180
+
181
+ has(texture: Texture): boolean {
182
+ return this.membership.has(texture);
183
+ }
184
+
185
+ add(texture: Texture): void {
186
+ if (this.membership.has(texture)) {
187
+ return;
188
+ }
189
+ this.membership.add(texture);
190
+ this.list.push(texture);
191
+ }
192
+
193
+ delete(texture: Texture): boolean {
194
+ return this.membership.delete(texture);
195
+ }
196
+
163
197
  /**
164
- * Map of textures by cache key
198
+ * Remove and return the oldest live entry, or `undefined` if empty.
165
199
  */
166
- keyCache: Map<string, Texture> = new Map();
200
+ shift(): Texture | undefined {
201
+ const list = this.list;
202
+ const membership = this.membership;
203
+ while (this.head < list.length) {
204
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
205
+ const texture = list[this.head++]!;
206
+ if (membership.has(texture)) {
207
+ membership.delete(texture);
208
+ this.compactIfNeeded();
209
+ return texture;
210
+ }
211
+ }
212
+ // Fully drained — reset for the next batch.
213
+ this.head = 0;
214
+ list.length = 0;
215
+ return undefined;
216
+ }
217
+
218
+ private compactIfNeeded(): void {
219
+ // Drop the consumed prefix once it's substantial in absolute terms
220
+ // and at least half the array. Avoids growing the array unboundedly
221
+ // when shift() runs faster than the queue refills.
222
+ if (this.head >= 64 && this.head >= this.list.length >> 1) {
223
+ this.list = this.list.slice(this.head);
224
+ this.head = 0;
225
+ }
226
+ }
227
+ }
167
228
 
229
+ export class CoreTextureManager extends EventEmitter {
168
230
  /**
169
- * Map of cache keys by texture
231
+ * Map of textures by cache key
170
232
  */
171
- inverseKeyCache: WeakMap<Texture, string> = new WeakMap();
233
+ keyCache: Map<string, Texture> = new Map();
172
234
 
173
235
  /**
174
236
  * Map of texture constructors by their type name
@@ -176,7 +238,7 @@ export class CoreTextureManager extends EventEmitter {
176
238
  txConstructors: Partial<TextureMap> = {};
177
239
 
178
240
  public maxRetryCount: number;
179
- private uploadTextureQueue: Set<Texture> = new Set();
241
+ private uploadTextureQueue: TextureUploadQueue = new TextureUploadQueue();
180
242
  private initialized = false;
181
243
  private stage: Stage;
182
244
  private numImageWorkers: number;
@@ -494,13 +556,9 @@ export class CoreTextureManager extends EventEmitter {
494
556
  texture.state === 'failed' || texture.state === 'freed';
495
557
 
496
558
  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);
559
+ while (pending.length < prefetchLimit) {
560
+ const texture = this.uploadTextureQueue.shift();
561
+ if (texture === undefined) break;
504
562
 
505
563
  if (isDead(texture)) {
506
564
  continue;
@@ -570,9 +628,8 @@ export class CoreTextureManager extends EventEmitter {
570
628
  * @param cacheKey Cache key for the texture
571
629
  */
572
630
  initTextureToCache(texture: Texture, cacheKey: string) {
573
- const { keyCache, inverseKeyCache } = this;
574
- keyCache.set(cacheKey, texture);
575
- inverseKeyCache.set(texture, cacheKey);
631
+ this.keyCache.set(cacheKey, texture);
632
+ texture.cacheKey = cacheKey;
576
633
  }
577
634
 
578
635
  /**
@@ -593,10 +650,10 @@ export class CoreTextureManager extends EventEmitter {
593
650
  * @param texture
594
651
  */
595
652
  removeTextureFromCache(texture: Texture) {
596
- const { inverseKeyCache, keyCache } = this;
597
- const cacheKey = inverseKeyCache.get(texture);
598
- if (cacheKey) {
599
- keyCache.delete(cacheKey);
653
+ const cacheKey = texture.cacheKey;
654
+ if (cacheKey !== null) {
655
+ this.keyCache.delete(cacheKey);
656
+ texture.cacheKey = null;
600
657
  }
601
658
  }
602
659
 
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();
@@ -1066,7 +1067,7 @@ export class Stage {
1066
1067
  * @param options - Font loading options specific to the renderer type
1067
1068
  * @returns Promise that resolves when the font is loaded
1068
1069
  */
1069
- async loadFont(
1070
+ loadFont(
1070
1071
  rendererType: TextRenderers,
1071
1072
  options: FontLoadOptions,
1072
1073
  ): Promise<void> {
@@ -1074,10 +1075,12 @@ export class Stage {
1074
1075
  const fontHandler = this.fontHandlers[rendererTypeKey];
1075
1076
 
1076
1077
  if (!fontHandler) {
1077
- throw new Error(
1078
- `Font handler for renderer type '${rendererTypeKey}' not found. Available types: ${Object.keys(
1079
- this.fontHandlers,
1080
- ).join(', ')}`,
1078
+ return Promise.reject(
1079
+ new Error(
1080
+ `Font handler for renderer type '${rendererTypeKey}' not found. Available types: ${Object.keys(
1081
+ this.fontHandlers,
1082
+ ).join(', ')}`,
1083
+ ),
1081
1084
  );
1082
1085
  }
1083
1086
 
@@ -0,0 +1,72 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { Matrix3d } from './Matrix3d.js';
3
+
4
+ describe('Matrix3d.setTranslate', () => {
5
+ it('writes tx/ty without touching ta/tb/tc/td', () => {
6
+ const m = Matrix3d.identity();
7
+ m.setTranslate(10, 20);
8
+ expect(m.tx).toBe(10);
9
+ expect(m.ty).toBe(20);
10
+ expect(m.ta).toBe(1);
11
+ expect(m.tb).toBe(0);
12
+ expect(m.tc).toBe(0);
13
+ expect(m.td).toBe(1);
14
+ });
15
+
16
+ it('updates the float array on subsequent getFloatArr() calls', () => {
17
+ const m = Matrix3d.identity();
18
+ m.setTranslate(5, 7);
19
+ // First read populates the array
20
+ const arr = m.getFloatArr();
21
+ expect(arr[6]).toBe(5);
22
+ expect(arr[7]).toBe(7);
23
+
24
+ // Mutate, then expect getFloatArr() to pick up the change.
25
+ m.setTranslate(9, 11);
26
+ const arr2 = m.getFloatArr();
27
+ expect(arr2[6]).toBe(9);
28
+ expect(arr2[7]).toBe(11);
29
+ // Same array reference reused (no GC pressure).
30
+ expect(arr2).toBe(arr);
31
+ });
32
+
33
+ it('overwrites prior tx/ty values', () => {
34
+ const m = Matrix3d.translate(100, 200);
35
+ m.setTranslate(0, 0);
36
+ expect(m.tx).toBe(0);
37
+ expect(m.ty).toBe(0);
38
+ });
39
+ });
40
+
41
+ describe('Matrix3d.rotate(0) fast path', () => {
42
+ it('produces an identity matrix for angle=0', () => {
43
+ const m = Matrix3d.rotate(0);
44
+ expect(m.ta).toBe(1);
45
+ expect(m.tb).toBe(0);
46
+ expect(m.tc).toBe(0);
47
+ expect(m.td).toBe(1);
48
+ expect(m.tx).toBe(0);
49
+ expect(m.ty).toBe(0);
50
+ });
51
+
52
+ it('resets a pre-populated out matrix to identity on angle=0', () => {
53
+ const m = Matrix3d.rotate(Math.PI / 4);
54
+ // Now reuse `m` with angle=0; should overwrite to identity.
55
+ Matrix3d.rotate(0, m);
56
+ expect(m.ta).toBe(1);
57
+ expect(m.tb).toBe(0);
58
+ expect(m.tc).toBe(0);
59
+ expect(m.td).toBe(1);
60
+ expect(m.tx).toBe(0);
61
+ expect(m.ty).toBe(0);
62
+ });
63
+
64
+ it('still produces a real rotation when angle != 0', () => {
65
+ const m = Matrix3d.rotate(Math.PI / 2);
66
+ // cos(pi/2) ~ 0, sin(pi/2) = 1
67
+ expect(Math.abs(m.ta)).toBeLessThan(1e-10);
68
+ expect(m.tb).toBeCloseTo(-1, 10);
69
+ expect(m.tc).toBeCloseTo(1, 10);
70
+ expect(Math.abs(m.td)).toBeLessThan(1e-10);
71
+ });
72
+ });
@@ -124,11 +124,24 @@ export class Matrix3d {
124
124
  }
125
125
 
126
126
  public static rotate(angle: number, out?: Matrix3d): Matrix3d {
127
- const cos = Math.cos(angle);
128
- const sin = Math.sin(angle);
129
- if (!out) {
127
+ if (out === undefined) {
130
128
  out = new Matrix3d();
131
129
  }
130
+ if (angle === 0) {
131
+ // Skip Math.cos(0) / Math.sin(0) — neither V8 nor JSC constant-folds
132
+ // these calls. Identity rotation is the common case when callers
133
+ // combine rotate().scale() and only the scale is non-default.
134
+ out.ta = 1;
135
+ out.tb = 0;
136
+ out.tx = 0;
137
+ out.tc = 0;
138
+ out.td = 1;
139
+ out.ty = 0;
140
+ out.mutation = true;
141
+ return out;
142
+ }
143
+ const cos = Math.cos(angle);
144
+ const sin = Math.sin(angle);
132
145
  out.ta = cos;
133
146
  out.tb = -sin;
134
147
  out.tx = 0;
@@ -160,6 +173,20 @@ export class Matrix3d {
160
173
  return this;
161
174
  }
162
175
 
176
+ /**
177
+ * Writes `tx`/`ty` directly and marks the matrix mutated.
178
+ *
179
+ * Caller is responsible for ensuring `ta`/`tb`/`tc`/`td` are already the
180
+ * desired values — typically the identity rotation/scale (1, 0, 0, 1).
181
+ * Used by hot paths that know the matrix is in identity-shape so the
182
+ * 4 redundant field writes performed by `Matrix3d.translate` can be skipped.
183
+ */
184
+ public setTranslate(x: number, y: number): void {
185
+ this.tx = x;
186
+ this.ty = y;
187
+ this.mutation = true;
188
+ }
189
+
163
190
  public scale(sx: number, sy: number): Matrix3d {
164
191
  this.ta = this.ta * sx;
165
192
  this.tb = this.tb * sy;
@@ -1,8 +1,6 @@
1
1
  /* eslint-disable @typescript-eslint/no-unsafe-return */
2
2
  /* eslint-disable @typescript-eslint/no-explicit-any */
3
3
  /* eslint-disable @typescript-eslint/no-unsafe-argument */
4
-
5
- import { assertTruthy, isProductionEnvironment } from '../../utils.js';
6
4
  import type {
7
5
  Vec2,
8
6
  Vec3,