@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.
- package/dist/src/core/CoreNode.d.ts +22 -0
- package/dist/src/core/CoreNode.js +131 -25
- package/dist/src/core/CoreNode.js.map +1 -1
- package/dist/src/core/CoreTextureManager.js +82 -24
- package/dist/src/core/CoreTextureManager.js.map +1 -1
- package/dist/src/core/Stage.js +3 -2
- package/dist/src/core/Stage.js.map +1 -1
- package/dist/src/core/TextureError.d.ts +2 -1
- package/dist/src/core/TextureError.js +2 -0
- package/dist/src/core/TextureError.js.map +1 -1
- package/dist/src/core/lib/ImageWorker.d.ts +1 -1
- package/dist/src/core/lib/ImageWorker.js +46 -26
- package/dist/src/core/lib/ImageWorker.js.map +1 -1
- package/dist/src/core/lib/Matrix3d.d.ts +9 -0
- package/dist/src/core/lib/Matrix3d.js +29 -3
- package/dist/src/core/lib/Matrix3d.js.map +1 -1
- package/dist/src/core/textures/ColorTexture.js +3 -1
- package/dist/src/core/textures/ColorTexture.js.map +1 -1
- package/dist/src/core/textures/ImageTexture.js +24 -16
- package/dist/src/core/textures/ImageTexture.js.map +1 -1
- package/dist/tsconfig.dist.tsbuildinfo +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/core/CoreNode.test.ts +438 -0
- package/src/core/CoreNode.ts +140 -39
- package/src/core/CoreTextureManager.ts +101 -22
- package/src/core/Stage.ts +3 -2
- package/src/core/TextureError.ts +2 -0
- package/src/core/lib/ImageWorker.ts +50 -27
- package/src/core/lib/Matrix3d.test.ts +72 -0
- package/src/core/lib/Matrix3d.ts +30 -3
- package/src/core/textures/ColorTexture.ts +3 -1
- package/src/core/textures/ImageTexture.ts +29 -20
package/src/core/CoreNode.ts
CHANGED
|
@@ -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
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
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
|
-
|
|
1127
|
-
texture
|
|
1128
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
1315
|
+
).translateOrMultiply(lt);
|
|
1244
1316
|
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
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
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
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 (
|
|
1257
|
-
this.
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
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
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
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
|
-
|
|
1391
|
-
|
|
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
|
|
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]
|
|
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
|
-
|
|
329
|
-
|
|
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(
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
527
|
+
pending.length > 0 &&
|
|
466
528
|
platform.getTimeStamp() - startTime < maxProcessingTime
|
|
467
529
|
) {
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
-
|
|
473
|
-
|
|
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
|
-
|
|
365
|
+
Matrix3d.copy(rootNode.localTransform!, rootNode.globalTransform);
|
|
365
366
|
rootNode.sceneGlobalTransform = Matrix3d.copy(rootNode.localTransform!);
|
|
366
367
|
rootNode.calculateRenderCoords();
|
|
367
368
|
rootNode.updateBoundingRect();
|
package/src/core/TextureError.ts
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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 =
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
}
|