@solidtv/renderer 1.1.1 → 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.
@@ -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,
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();
@@ -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;