@react-three/tsl 10.0.0-canary.87166dc → 10.0.0-canary.88b1ca7

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/index.cjs CHANGED
@@ -73,6 +73,13 @@ function withStagedOverlay(store, kind, committed) {
73
73
  }
74
74
  return view;
75
75
  }
76
+ function readWithStaged(store, kind, committed, scope, isLeaf) {
77
+ if (scope === void 0) return withStagedOverlay(store, kind, committed);
78
+ const scopeData = committed[scope];
79
+ const current = scopeData && typeof scopeData === "object" && !isLeaf(scopeData) ? scopeData : {};
80
+ const staged = scope === ROOT_SCOPE ? void 0 : peekStaged(store, kind, scope);
81
+ return staged ? { ...current, ...staged } : current;
82
+ }
76
83
  function flushStagedScope(store, kind, scopeKey, isLeaf) {
77
84
  const registry = getKindRegistry(store, kind);
78
85
  const staged = registry.staged.get(scopeKey);
@@ -271,6 +278,103 @@ function configureTSL(next) {
271
278
  for (const store of primaries) applyConfig(store, config);
272
279
  }
273
280
 
281
+ const owners = /* @__PURE__ */ new WeakMap();
282
+ const warned$1 = /* @__PURE__ */ new WeakSet();
283
+ const sweeps = /* @__PURE__ */ new WeakMap();
284
+ function createOwner(store, rebuild) {
285
+ return { store, pipeline: null, scenePass: null, passes: {}, mounted: false, releaseTimer: null, rebuild };
286
+ }
287
+ function disposeOwnerResources(owner) {
288
+ owner.scenePass?.node.dispose();
289
+ owner.pipeline?.dispose();
290
+ owner.scenePass = null;
291
+ owner.pipeline = null;
292
+ owner.passes = {};
293
+ }
294
+ function activeOwner(store) {
295
+ return owners.get(store)?.at(-1);
296
+ }
297
+ function renderThroughPipeline(store) {
298
+ return () => {
299
+ const pipeline = store.getState().renderPipeline;
300
+ if (pipeline?.render) pipeline.render();
301
+ };
302
+ }
303
+ function claimOwnership(owner) {
304
+ if (owner.releaseTimer !== null) clearTimeout(owner.releaseTimer);
305
+ owner.releaseTimer = null;
306
+ owner.mounted = true;
307
+ let list = owners.get(owner.store);
308
+ if (!list) owners.set(owner.store, list = []);
309
+ if (list.includes(owner)) return;
310
+ if (list.some((other) => other.mounted) && !warned$1.has(owner.store)) {
311
+ warned$1.add(owner.store);
312
+ if (typeof process === "undefined" || process.env.NODE_ENV !== "production") {
313
+ console.warn(
314
+ "[useRenderPipeline] More than one mounted component configures this canvas's render pipeline. The most recently mounted one renders; the others take over again when it unmounts."
315
+ );
316
+ }
317
+ }
318
+ list.push(owner);
319
+ scheduleSweep(owner.store);
320
+ }
321
+ function scheduleSweep(store) {
322
+ if (sweeps.has(store)) return;
323
+ sweeps.set(
324
+ store,
325
+ setTimeout(() => {
326
+ sweeps.delete(store);
327
+ const active = activeOwner(store);
328
+ for (const owner of [...owners.get(store) ?? []]) {
329
+ if (owner !== active && !owner.mounted && owner.releaseTimer === null) releaseOwner(owner);
330
+ }
331
+ })
332
+ );
333
+ }
334
+ function publishOwner(owner) {
335
+ if (activeOwner(owner.store) !== owner || !owner.pipeline) return;
336
+ owner.store.setState({ renderPipeline: owner.pipeline, passes: owner.passes });
337
+ extension.setRenderOverride(owner.store, renderThroughPipeline(owner.store));
338
+ }
339
+ function scheduleRelease(owner) {
340
+ owner.mounted = false;
341
+ if (owner.releaseTimer !== null) return;
342
+ owner.releaseTimer = setTimeout(() => releaseOwner(owner));
343
+ }
344
+ function releaseOwner(owner) {
345
+ if (owner.releaseTimer !== null) clearTimeout(owner.releaseTimer);
346
+ owner.releaseTimer = null;
347
+ const { store } = owner;
348
+ const list = owners.get(store);
349
+ const index = list?.indexOf(owner) ?? -1;
350
+ if (list && index !== -1) {
351
+ const wasActive = index === list.length - 1;
352
+ list.splice(index, 1);
353
+ if (wasActive) {
354
+ const next = activeOwner(store);
355
+ if (next) {
356
+ publishOwner(next);
357
+ } else {
358
+ store.setState({ renderPipeline: null, passes: {} });
359
+ extension.setRenderOverride(store, null);
360
+ }
361
+ }
362
+ }
363
+ disposeOwnerResources(owner);
364
+ }
365
+ function releaseAllOwners(store) {
366
+ clearTimeout(sweeps.get(store));
367
+ sweeps.delete(store);
368
+ const list = owners.get(store);
369
+ if (!list) return;
370
+ owners.delete(store);
371
+ for (const owner of list) {
372
+ if (owner.releaseTimer !== null) clearTimeout(owner.releaseTimer);
373
+ owner.releaseTimer = null;
374
+ disposeOwnerResources(owner);
375
+ }
376
+ }
377
+
274
378
  const SHARED_KEYS = ["uniforms", "nodes", "buffers", "gpuStorage", "_hmrVersion"];
275
379
  const following = /* @__PURE__ */ new WeakMap();
276
380
  function createResourceState() {
@@ -299,6 +403,7 @@ function followPrimary(store, primary) {
299
403
  following.set(store, primary.subscribe(sync));
300
404
  }
301
405
  function dispose(store) {
406
+ releaseAllOwners(store);
302
407
  untrackPrimary(store);
303
408
  following.get(store)?.();
304
409
  following.delete(store);
@@ -704,8 +809,9 @@ function useUniforms(creatorOrScope, scope) {
704
809
  input: memoizedInput,
705
810
  create: () => {
706
811
  if (isReader) return {};
707
- if (typeof processedInput.candidates !== "object" || processedInput.candidates === null)
812
+ if (typeof processedInput.candidates !== "object" || processedInput.candidates === null) {
708
813
  throw new Error("Invalid uniform input");
814
+ }
709
815
  return processedInput.candidates;
710
816
  },
711
817
  prepare: (name, candidate) => createUniform(name, candidate, scope),
@@ -723,13 +829,7 @@ function useUniforms(creatorOrScope, scope) {
723
829
  if (!equals) existing.value = newVal;
724
830
  }
725
831
  });
726
- let uniforms = created;
727
- if (memoizedInput === void 0) {
728
- uniforms = storeUniforms;
729
- } else if (typeof memoizedInput === "string") {
730
- const scopeData = storeUniforms[memoizedInput];
731
- uniforms = scopeData && !isUniformNode(scopeData) ? scopeData : {};
732
- }
832
+ const uniforms = isReader ? readWithStaged(store, "uniforms", storeUniforms, memoizedInput, isUniformNode) : created;
733
833
  return { ...uniforms, removeUniforms, clearUniforms, rebuildUniforms };
734
834
  }
735
835
  function rebuildAllUniforms(store, scope) {
@@ -846,13 +946,7 @@ function useNodes(creatorOrScope, scope) {
846
946
  return node;
847
947
  }
848
948
  });
849
- let nodes = created;
850
- if (creatorOrScope === void 0) {
851
- nodes = storeNodes;
852
- } else if (typeof creatorOrScope === "string") {
853
- const scopeData = storeNodes[creatorOrScope];
854
- nodes = scopeData && !isTSLNode(scopeData) ? scopeData : {};
855
- }
949
+ const nodes = isReader ? readWithStaged(store, "nodes", storeNodes, creatorOrScope, isTSLNode) : created;
856
950
  return { ...nodes, removeNodes, clearNodes, rebuildNodes };
857
951
  }
858
952
  function rebuildAllNodes(store, scope) {
@@ -1007,13 +1101,7 @@ function useBuffers(creatorOrScope, scope) {
1007
1101
  return buffer;
1008
1102
  }
1009
1103
  });
1010
- let buffers = created;
1011
- if (creatorOrScope === void 0) {
1012
- buffers = storeBuffers;
1013
- } else if (typeof creatorOrScope === "string") {
1014
- const scopeData = storeBuffers[creatorOrScope];
1015
- buffers = scopeData && !isBufferLike(scopeData) ? scopeData : {};
1016
- }
1104
+ const buffers = isReader ? readWithStaged(store, "buffers", storeBuffers, creatorOrScope, isBufferLike) : created;
1017
1105
  return { ...buffers, removeBuffers, clearBuffers, rebuildBuffers, disposeBuffers };
1018
1106
  }
1019
1107
  function rebuildAllBuffers(store, scope) {
@@ -1077,13 +1165,7 @@ function useGPUStorage(creatorOrScope, scope) {
1077
1165
  return storage;
1078
1166
  }
1079
1167
  });
1080
- let gpuStorage = created;
1081
- if (creatorOrScope === void 0) {
1082
- gpuStorage = storeStorage;
1083
- } else if (typeof creatorOrScope === "string") {
1084
- const scopeData = storeStorage[creatorOrScope];
1085
- gpuStorage = scopeData && !isStorageLike(scopeData) ? scopeData : {};
1086
- }
1168
+ const gpuStorage = isReader ? readWithStaged(store, "gpuStorage", storeStorage, creatorOrScope, isStorageLike) : created;
1087
1169
  return {
1088
1170
  ...gpuStorage,
1089
1171
  removeStorage,
@@ -1097,120 +1179,164 @@ function rebuildAllStorage(store, scope) {
1097
1179
  }
1098
1180
 
1099
1181
  const NO_PASSES = Object.freeze({});
1100
- function renderThroughPipeline(store) {
1101
- return () => {
1102
- const pipeline = store.getState().renderPipeline;
1103
- if (pipeline?.render) pipeline.render();
1104
- };
1105
- }
1106
- function useRenderPipeline(mainCB, setupCB) {
1182
+ function useRenderPipeline(mainCB, setupCB, options) {
1107
1183
  const store = extension.useStore();
1184
+ const primary = usePrimaryStore();
1185
+ const view = React.useMemo(() => createResourceView(primary, store), [primary, store]);
1108
1186
  const { scene, camera, renderer, isLegacy } = extension.useThree();
1187
+ const hmrVersion = usePrimaryThree((s) => s._hmrVersion);
1188
+ const isOwner = mainCB !== void 0 || setupCB !== void 0;
1109
1189
  const callbacksRanRef = React.useRef(false);
1110
- const scenePassCacheRef = React.useRef(null);
1111
1190
  const forceScenePassRebuildRef = React.useRef(false);
1112
1191
  const mainCBRef = React.useRef(mainCB);
1113
1192
  const setupCBRef = React.useRef(setupCB);
1193
+ const persistRef = React.useRef(options?.persist ?? false);
1114
1194
  mainCBRef.current = mainCB;
1115
1195
  setupCBRef.current = setupCB;
1196
+ persistRef.current = options?.persist ?? false;
1116
1197
  const [rebuildVersion, setRebuildVersion] = React.useState(0);
1117
- React.useEffect(() => {
1198
+ const rerun = React.useCallback((freshScenePass) => {
1118
1199
  callbacksRanRef.current = false;
1119
- forceScenePassRebuildRef.current = true;
1200
+ if (freshScenePass) forceScenePassRebuildRef.current = true;
1201
+ setRebuildVersion((v) => v + 1);
1120
1202
  }, []);
1203
+ const ownerRef = React.useRef(null);
1204
+ const target = React.useCallback(() => (isOwner ? ownerRef.current : activeOwner(store)) ?? null, [isOwner, store]);
1121
1205
  const clearPasses = React.useCallback(() => {
1122
- store.setState({ passes: {} });
1123
- }, [store]);
1206
+ const owner = target();
1207
+ if (!owner) return;
1208
+ owner.passes = {};
1209
+ if (activeOwner(store) === owner) store.setState({ passes: {} });
1210
+ }, [store, target]);
1124
1211
  const reset = React.useCallback(() => {
1125
- store.setState({
1126
- renderPipeline: null,
1127
- passes: {}
1128
- });
1129
- extension.setRenderOverride(store, null);
1130
- scenePassCacheRef.current?.scenePass.dispose();
1131
- callbacksRanRef.current = false;
1132
- scenePassCacheRef.current = null;
1133
- forceScenePassRebuildRef.current = false;
1134
- }, [store]);
1212
+ const owner = target();
1213
+ if (!owner) return;
1214
+ releaseOwner(owner);
1215
+ if (owner === ownerRef.current) committedReads.current = null;
1216
+ }, [target]);
1135
1217
  const rebuild = React.useCallback(() => {
1136
- callbacksRanRef.current = false;
1137
- forceScenePassRebuildRef.current = true;
1138
- setRebuildVersion((v) => v + 1);
1139
- }, []);
1218
+ if (isOwner) rerun(true);
1219
+ else activeOwner(store)?.rebuild();
1220
+ }, [isOwner, rerun, store]);
1221
+ const hmrVersionRef = React.useRef(hmrVersion);
1222
+ React.useEffect(() => {
1223
+ if (hmrVersionRef.current === hmrVersion) return;
1224
+ hmrVersionRef.current = hmrVersion;
1225
+ if (isOwner) rerun(false);
1226
+ }, [hmrVersion, isOwner, rerun]);
1227
+ const committedReads = React.useRef(null);
1228
+ const readsVersion = React.useRef(0);
1229
+ const getReadsVersion = () => {
1230
+ const tracker = committedReads.current;
1231
+ if (tracker && !tracker.stale && isTrackerStale(tracker, view)) readsVersion.current++;
1232
+ return readsVersion.current;
1233
+ };
1234
+ const subscribe = React.useCallback(
1235
+ (onChange) => {
1236
+ const unsubscribe = primary.subscribe(onChange);
1237
+ if (store === primary) return unsubscribe;
1238
+ const unsubscribeLocal = store.subscribe(onChange);
1239
+ return () => {
1240
+ unsubscribe();
1241
+ unsubscribeLocal();
1242
+ };
1243
+ },
1244
+ [primary, store]
1245
+ );
1246
+ const resourceVersion = React.useSyncExternalStore(subscribe, getReadsVersion, getReadsVersion);
1247
+ const lastResourceVersion = React.useRef(resourceVersion);
1140
1248
  React.useLayoutEffect(() => {
1141
1249
  if (isLegacy) {
1142
- throw new Error("useRenderPipeline is only available with WebGPU renderer. Set renderer prop on Canvas.");
1250
+ throw new Error(
1251
+ "useRenderPipeline needs WebGPU: import Canvas from @react-three/fiber, not @react-three/fiber/legacy."
1252
+ );
1143
1253
  }
1254
+ if (!isOwner) return;
1144
1255
  if (!renderer || !scene || !camera) return;
1145
1256
  if (!("isWebGPURenderer" in renderer)) {
1146
1257
  throw new Error("useRenderPipeline: renderer is not a WebGPURenderer but isLegacy is false.");
1147
1258
  }
1148
- const state = store.getState();
1149
- const set = store.setState;
1259
+ let owner = ownerRef.current;
1260
+ if (owner && owner.store !== store) {
1261
+ releaseOwner(owner);
1262
+ owner = null;
1263
+ }
1264
+ if (!owner) owner = ownerRef.current = createOwner(store, () => rerun(true));
1265
+ claimOwnership(owner);
1266
+ const currentOwner = owner;
1150
1267
  try {
1151
- let pipeline = state.renderPipeline ?? null;
1152
- let currentPasses = { ...state.passes };
1153
1268
  let justCreatedPipeline = false;
1154
- if (!pipeline) {
1155
- pipeline = new THREE__namespace.RenderPipeline(renderer);
1269
+ if (!owner.pipeline || owner.pipeline.renderer !== renderer) {
1270
+ disposeOwnerResources(owner);
1271
+ owner.pipeline = new THREE__namespace.RenderPipeline(renderer);
1156
1272
  justCreatedPipeline = true;
1157
1273
  }
1158
- const cacheValid = !forceScenePassRebuildRef.current && scenePassCacheRef.current && scenePassCacheRef.current.sceneUuid === scene.uuid && scenePassCacheRef.current.cameraUuid === camera.uuid;
1274
+ const pipeline = owner.pipeline;
1275
+ const cached = owner.scenePass;
1276
+ const cacheValid = !forceScenePassRebuildRef.current && cached !== null && cached.sceneUuid === scene.uuid && cached.cameraUuid === camera.uuid;
1159
1277
  let outgoingScenePass = null;
1160
1278
  let scenePass;
1161
- if (cacheValid) {
1162
- scenePass = scenePassCacheRef.current.scenePass;
1279
+ if (cacheValid && cached) {
1280
+ scenePass = cached.node;
1163
1281
  } else {
1164
- outgoingScenePass = scenePassCacheRef.current?.scenePass ?? null;
1282
+ outgoingScenePass = cached?.node ?? null;
1165
1283
  scenePass = tsl.pass(scene, camera);
1166
- scenePassCacheRef.current = { sceneUuid: scene.uuid, cameraUuid: camera.uuid, scenePass };
1284
+ owner.scenePass = { sceneUuid: scene.uuid, cameraUuid: camera.uuid, node: scenePass };
1167
1285
  forceScenePassRebuildRef.current = false;
1168
1286
  }
1169
- currentPasses.scenePass = scenePass;
1170
1287
  let outputNodeChanged = false;
1171
1288
  if (!pipeline.outputNode || justCreatedPipeline || pipeline.outputNode === outgoingScenePass) {
1172
1289
  pipeline.outputNode = scenePass;
1173
1290
  outputNodeChanged = true;
1174
1291
  }
1175
- set({ renderPipeline: pipeline, passes: currentPasses });
1176
- extension.setRenderOverride(store, renderThroughPipeline(store));
1177
- const shouldRunCallbacks = justCreatedPipeline || !callbacksRanRef.current || !cacheValid;
1292
+ const readsChanged = lastResourceVersion.current !== resourceVersion;
1293
+ lastResourceVersion.current = resourceVersion;
1294
+ const shouldRunCallbacks = justCreatedPipeline || !callbacksRanRef.current || !cacheValid || readsChanged;
1178
1295
  if (shouldRunCallbacks) {
1179
- if (setupCBRef.current) {
1180
- const freshState = Object.assign({}, store.getState(), {
1181
- renderPipeline: pipeline,
1182
- passes: { ...currentPasses, scenePass },
1183
- renderer,
1184
- isLegacy: false
1185
- });
1186
- const setupResult = setupCBRef.current(freshState);
1187
- if (setupResult && typeof setupResult === "object") {
1188
- currentPasses = { ...currentPasses, ...setupResult };
1189
- set({ passes: currentPasses });
1296
+ let currentPasses = { scenePass };
1297
+ owner.passes = currentPasses;
1298
+ const tracker = createReadTracker("useRenderPipeline");
1299
+ const callbackState = () => Object.assign(
1300
+ createLazyCreatorState(store.getState(), primary, {
1301
+ reads: { observe: tracker.observe, nested: true },
1302
+ view
1303
+ }),
1304
+ { renderPipeline: pipeline, passes: { ...currentPasses, scenePass }, renderer, isLegacy: false }
1305
+ );
1306
+ try {
1307
+ if (setupCBRef.current) {
1308
+ const setupResult = setupCBRef.current(callbackState());
1309
+ if (setupResult && typeof setupResult === "object") {
1310
+ currentPasses = { ...currentPasses, ...setupResult };
1311
+ owner.passes = currentPasses;
1312
+ }
1190
1313
  }
1191
- }
1192
- if (mainCBRef.current) {
1193
- const freshState = Object.assign({}, store.getState(), {
1194
- renderPipeline: pipeline,
1195
- passes: { ...currentPasses, scenePass },
1196
- renderer,
1197
- isLegacy: false
1198
- });
1199
- const mainResult = mainCBRef.current(freshState);
1200
- if (mainResult && typeof mainResult === "object") {
1201
- currentPasses = { ...currentPasses, ...mainResult };
1202
- set({ passes: currentPasses });
1314
+ if (mainCBRef.current) {
1315
+ const mainResult = mainCBRef.current(callbackState());
1316
+ if (mainResult && typeof mainResult === "object") {
1317
+ currentPasses = { ...currentPasses, ...mainResult };
1318
+ owner.passes = currentPasses;
1319
+ }
1203
1320
  }
1321
+ } finally {
1322
+ tracker.closed = true;
1323
+ committedReads.current = tracker;
1204
1324
  }
1205
1325
  outputNodeChanged = true;
1206
1326
  callbacksRanRef.current = true;
1207
1327
  }
1208
1328
  if (outputNodeChanged) pipeline.needsUpdate = true;
1329
+ publishOwner(owner);
1209
1330
  if (outgoingScenePass && outgoingScenePass !== scenePass) outgoingScenePass.dispose();
1210
1331
  } catch (error) {
1332
+ publishOwner(owner);
1211
1333
  console.warn("[useRenderPipeline] Setup failed; the render pipeline was not configured:", error);
1212
1334
  }
1213
- }, [store, renderer, scene, camera, isLegacy, rebuildVersion]);
1335
+ return () => {
1336
+ if (persistRef.current) currentOwner.mounted = false;
1337
+ else scheduleRelease(currentOwner);
1338
+ };
1339
+ }, [store, primary, view, renderer, scene, camera, isLegacy, isOwner, rebuildVersion, resourceVersion, rerun]);
1214
1340
  const passes = extension.useThree((s) => s.passes) ?? NO_PASSES;
1215
1341
  const renderPipeline = extension.useThree((s) => s.renderPipeline) ?? null;
1216
1342
  const actions = { passes, clearPasses, reset, rebuild };