@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.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { registerRootExtension, getTextureView, useStore, useThree, setRenderOverride } from '@react-three/fiber/extension';
1
+ import { setRenderOverride, registerRootExtension, getTextureView, useStore, useThree } from '@react-three/fiber/extension';
2
2
  import { uniform, pass } from 'three/tsl';
3
3
  import * as THREE from 'three/webgpu';
4
4
  import { Color } from 'three/webgpu';
@@ -58,6 +58,13 @@ function withStagedOverlay(store, kind, committed) {
58
58
  }
59
59
  return view;
60
60
  }
61
+ function readWithStaged(store, kind, committed, scope, isLeaf) {
62
+ if (scope === void 0) return withStagedOverlay(store, kind, committed);
63
+ const scopeData = committed[scope];
64
+ const current = scopeData && typeof scopeData === "object" && !isLeaf(scopeData) ? scopeData : {};
65
+ const staged = scope === ROOT_SCOPE ? void 0 : peekStaged(store, kind, scope);
66
+ return staged ? { ...current, ...staged } : current;
67
+ }
61
68
  function flushStagedScope(store, kind, scopeKey, isLeaf) {
62
69
  const registry = getKindRegistry(store, kind);
63
70
  const staged = registry.staged.get(scopeKey);
@@ -256,6 +263,103 @@ function configureTSL(next) {
256
263
  for (const store of primaries) applyConfig(store, config);
257
264
  }
258
265
 
266
+ const owners = /* @__PURE__ */ new WeakMap();
267
+ const warned$1 = /* @__PURE__ */ new WeakSet();
268
+ const sweeps = /* @__PURE__ */ new WeakMap();
269
+ function createOwner(store, rebuild) {
270
+ return { store, pipeline: null, scenePass: null, passes: {}, mounted: false, releaseTimer: null, rebuild };
271
+ }
272
+ function disposeOwnerResources(owner) {
273
+ owner.scenePass?.node.dispose();
274
+ owner.pipeline?.dispose();
275
+ owner.scenePass = null;
276
+ owner.pipeline = null;
277
+ owner.passes = {};
278
+ }
279
+ function activeOwner(store) {
280
+ return owners.get(store)?.at(-1);
281
+ }
282
+ function renderThroughPipeline(store) {
283
+ return () => {
284
+ const pipeline = store.getState().renderPipeline;
285
+ if (pipeline?.render) pipeline.render();
286
+ };
287
+ }
288
+ function claimOwnership(owner) {
289
+ if (owner.releaseTimer !== null) clearTimeout(owner.releaseTimer);
290
+ owner.releaseTimer = null;
291
+ owner.mounted = true;
292
+ let list = owners.get(owner.store);
293
+ if (!list) owners.set(owner.store, list = []);
294
+ if (list.includes(owner)) return;
295
+ if (list.some((other) => other.mounted) && !warned$1.has(owner.store)) {
296
+ warned$1.add(owner.store);
297
+ if (typeof process === "undefined" || process.env.NODE_ENV !== "production") {
298
+ console.warn(
299
+ "[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."
300
+ );
301
+ }
302
+ }
303
+ list.push(owner);
304
+ scheduleSweep(owner.store);
305
+ }
306
+ function scheduleSweep(store) {
307
+ if (sweeps.has(store)) return;
308
+ sweeps.set(
309
+ store,
310
+ setTimeout(() => {
311
+ sweeps.delete(store);
312
+ const active = activeOwner(store);
313
+ for (const owner of [...owners.get(store) ?? []]) {
314
+ if (owner !== active && !owner.mounted && owner.releaseTimer === null) releaseOwner(owner);
315
+ }
316
+ })
317
+ );
318
+ }
319
+ function publishOwner(owner) {
320
+ if (activeOwner(owner.store) !== owner || !owner.pipeline) return;
321
+ owner.store.setState({ renderPipeline: owner.pipeline, passes: owner.passes });
322
+ setRenderOverride(owner.store, renderThroughPipeline(owner.store));
323
+ }
324
+ function scheduleRelease(owner) {
325
+ owner.mounted = false;
326
+ if (owner.releaseTimer !== null) return;
327
+ owner.releaseTimer = setTimeout(() => releaseOwner(owner));
328
+ }
329
+ function releaseOwner(owner) {
330
+ if (owner.releaseTimer !== null) clearTimeout(owner.releaseTimer);
331
+ owner.releaseTimer = null;
332
+ const { store } = owner;
333
+ const list = owners.get(store);
334
+ const index = list?.indexOf(owner) ?? -1;
335
+ if (list && index !== -1) {
336
+ const wasActive = index === list.length - 1;
337
+ list.splice(index, 1);
338
+ if (wasActive) {
339
+ const next = activeOwner(store);
340
+ if (next) {
341
+ publishOwner(next);
342
+ } else {
343
+ store.setState({ renderPipeline: null, passes: {} });
344
+ setRenderOverride(store, null);
345
+ }
346
+ }
347
+ }
348
+ disposeOwnerResources(owner);
349
+ }
350
+ function releaseAllOwners(store) {
351
+ clearTimeout(sweeps.get(store));
352
+ sweeps.delete(store);
353
+ const list = owners.get(store);
354
+ if (!list) return;
355
+ owners.delete(store);
356
+ for (const owner of list) {
357
+ if (owner.releaseTimer !== null) clearTimeout(owner.releaseTimer);
358
+ owner.releaseTimer = null;
359
+ disposeOwnerResources(owner);
360
+ }
361
+ }
362
+
259
363
  const SHARED_KEYS = ["uniforms", "nodes", "buffers", "gpuStorage", "_hmrVersion"];
260
364
  const following = /* @__PURE__ */ new WeakMap();
261
365
  function createResourceState() {
@@ -284,6 +388,7 @@ function followPrimary(store, primary) {
284
388
  following.set(store, primary.subscribe(sync));
285
389
  }
286
390
  function dispose(store) {
391
+ releaseAllOwners(store);
287
392
  untrackPrimary(store);
288
393
  following.get(store)?.();
289
394
  following.delete(store);
@@ -689,8 +794,9 @@ function useUniforms(creatorOrScope, scope) {
689
794
  input: memoizedInput,
690
795
  create: () => {
691
796
  if (isReader) return {};
692
- if (typeof processedInput.candidates !== "object" || processedInput.candidates === null)
797
+ if (typeof processedInput.candidates !== "object" || processedInput.candidates === null) {
693
798
  throw new Error("Invalid uniform input");
799
+ }
694
800
  return processedInput.candidates;
695
801
  },
696
802
  prepare: (name, candidate) => createUniform(name, candidate, scope),
@@ -708,13 +814,7 @@ function useUniforms(creatorOrScope, scope) {
708
814
  if (!equals) existing.value = newVal;
709
815
  }
710
816
  });
711
- let uniforms = created;
712
- if (memoizedInput === void 0) {
713
- uniforms = storeUniforms;
714
- } else if (typeof memoizedInput === "string") {
715
- const scopeData = storeUniforms[memoizedInput];
716
- uniforms = scopeData && !isUniformNode(scopeData) ? scopeData : {};
717
- }
817
+ const uniforms = isReader ? readWithStaged(store, "uniforms", storeUniforms, memoizedInput, isUniformNode) : created;
718
818
  return { ...uniforms, removeUniforms, clearUniforms, rebuildUniforms };
719
819
  }
720
820
  function rebuildAllUniforms(store, scope) {
@@ -831,13 +931,7 @@ function useNodes(creatorOrScope, scope) {
831
931
  return node;
832
932
  }
833
933
  });
834
- let nodes = created;
835
- if (creatorOrScope === void 0) {
836
- nodes = storeNodes;
837
- } else if (typeof creatorOrScope === "string") {
838
- const scopeData = storeNodes[creatorOrScope];
839
- nodes = scopeData && !isTSLNode(scopeData) ? scopeData : {};
840
- }
934
+ const nodes = isReader ? readWithStaged(store, "nodes", storeNodes, creatorOrScope, isTSLNode) : created;
841
935
  return { ...nodes, removeNodes, clearNodes, rebuildNodes };
842
936
  }
843
937
  function rebuildAllNodes(store, scope) {
@@ -992,13 +1086,7 @@ function useBuffers(creatorOrScope, scope) {
992
1086
  return buffer;
993
1087
  }
994
1088
  });
995
- let buffers = created;
996
- if (creatorOrScope === void 0) {
997
- buffers = storeBuffers;
998
- } else if (typeof creatorOrScope === "string") {
999
- const scopeData = storeBuffers[creatorOrScope];
1000
- buffers = scopeData && !isBufferLike(scopeData) ? scopeData : {};
1001
- }
1089
+ const buffers = isReader ? readWithStaged(store, "buffers", storeBuffers, creatorOrScope, isBufferLike) : created;
1002
1090
  return { ...buffers, removeBuffers, clearBuffers, rebuildBuffers, disposeBuffers };
1003
1091
  }
1004
1092
  function rebuildAllBuffers(store, scope) {
@@ -1062,13 +1150,7 @@ function useGPUStorage(creatorOrScope, scope) {
1062
1150
  return storage;
1063
1151
  }
1064
1152
  });
1065
- let gpuStorage = created;
1066
- if (creatorOrScope === void 0) {
1067
- gpuStorage = storeStorage;
1068
- } else if (typeof creatorOrScope === "string") {
1069
- const scopeData = storeStorage[creatorOrScope];
1070
- gpuStorage = scopeData && !isStorageLike(scopeData) ? scopeData : {};
1071
- }
1153
+ const gpuStorage = isReader ? readWithStaged(store, "gpuStorage", storeStorage, creatorOrScope, isStorageLike) : created;
1072
1154
  return {
1073
1155
  ...gpuStorage,
1074
1156
  removeStorage,
@@ -1082,120 +1164,164 @@ function rebuildAllStorage(store, scope) {
1082
1164
  }
1083
1165
 
1084
1166
  const NO_PASSES = Object.freeze({});
1085
- function renderThroughPipeline(store) {
1086
- return () => {
1087
- const pipeline = store.getState().renderPipeline;
1088
- if (pipeline?.render) pipeline.render();
1089
- };
1090
- }
1091
- function useRenderPipeline(mainCB, setupCB) {
1167
+ function useRenderPipeline(mainCB, setupCB, options) {
1092
1168
  const store = useStore();
1169
+ const primary = usePrimaryStore();
1170
+ const view = useMemo(() => createResourceView(primary, store), [primary, store]);
1093
1171
  const { scene, camera, renderer, isLegacy } = useThree();
1172
+ const hmrVersion = usePrimaryThree((s) => s._hmrVersion);
1173
+ const isOwner = mainCB !== void 0 || setupCB !== void 0;
1094
1174
  const callbacksRanRef = useRef(false);
1095
- const scenePassCacheRef = useRef(null);
1096
1175
  const forceScenePassRebuildRef = useRef(false);
1097
1176
  const mainCBRef = useRef(mainCB);
1098
1177
  const setupCBRef = useRef(setupCB);
1178
+ const persistRef = useRef(options?.persist ?? false);
1099
1179
  mainCBRef.current = mainCB;
1100
1180
  setupCBRef.current = setupCB;
1181
+ persistRef.current = options?.persist ?? false;
1101
1182
  const [rebuildVersion, setRebuildVersion] = useState(0);
1102
- useEffect(() => {
1183
+ const rerun = useCallback((freshScenePass) => {
1103
1184
  callbacksRanRef.current = false;
1104
- forceScenePassRebuildRef.current = true;
1185
+ if (freshScenePass) forceScenePassRebuildRef.current = true;
1186
+ setRebuildVersion((v) => v + 1);
1105
1187
  }, []);
1188
+ const ownerRef = useRef(null);
1189
+ const target = useCallback(() => (isOwner ? ownerRef.current : activeOwner(store)) ?? null, [isOwner, store]);
1106
1190
  const clearPasses = useCallback(() => {
1107
- store.setState({ passes: {} });
1108
- }, [store]);
1191
+ const owner = target();
1192
+ if (!owner) return;
1193
+ owner.passes = {};
1194
+ if (activeOwner(store) === owner) store.setState({ passes: {} });
1195
+ }, [store, target]);
1109
1196
  const reset = useCallback(() => {
1110
- store.setState({
1111
- renderPipeline: null,
1112
- passes: {}
1113
- });
1114
- setRenderOverride(store, null);
1115
- scenePassCacheRef.current?.scenePass.dispose();
1116
- callbacksRanRef.current = false;
1117
- scenePassCacheRef.current = null;
1118
- forceScenePassRebuildRef.current = false;
1119
- }, [store]);
1197
+ const owner = target();
1198
+ if (!owner) return;
1199
+ releaseOwner(owner);
1200
+ if (owner === ownerRef.current) committedReads.current = null;
1201
+ }, [target]);
1120
1202
  const rebuild = useCallback(() => {
1121
- callbacksRanRef.current = false;
1122
- forceScenePassRebuildRef.current = true;
1123
- setRebuildVersion((v) => v + 1);
1124
- }, []);
1203
+ if (isOwner) rerun(true);
1204
+ else activeOwner(store)?.rebuild();
1205
+ }, [isOwner, rerun, store]);
1206
+ const hmrVersionRef = useRef(hmrVersion);
1207
+ useEffect(() => {
1208
+ if (hmrVersionRef.current === hmrVersion) return;
1209
+ hmrVersionRef.current = hmrVersion;
1210
+ if (isOwner) rerun(false);
1211
+ }, [hmrVersion, isOwner, rerun]);
1212
+ const committedReads = useRef(null);
1213
+ const readsVersion = useRef(0);
1214
+ const getReadsVersion = () => {
1215
+ const tracker = committedReads.current;
1216
+ if (tracker && !tracker.stale && isTrackerStale(tracker, view)) readsVersion.current++;
1217
+ return readsVersion.current;
1218
+ };
1219
+ const subscribe = useCallback(
1220
+ (onChange) => {
1221
+ const unsubscribe = primary.subscribe(onChange);
1222
+ if (store === primary) return unsubscribe;
1223
+ const unsubscribeLocal = store.subscribe(onChange);
1224
+ return () => {
1225
+ unsubscribe();
1226
+ unsubscribeLocal();
1227
+ };
1228
+ },
1229
+ [primary, store]
1230
+ );
1231
+ const resourceVersion = useSyncExternalStore(subscribe, getReadsVersion, getReadsVersion);
1232
+ const lastResourceVersion = useRef(resourceVersion);
1125
1233
  useLayoutEffect(() => {
1126
1234
  if (isLegacy) {
1127
- throw new Error("useRenderPipeline is only available with WebGPU renderer. Set renderer prop on Canvas.");
1235
+ throw new Error(
1236
+ "useRenderPipeline needs WebGPU: import Canvas from @react-three/fiber, not @react-three/fiber/legacy."
1237
+ );
1128
1238
  }
1239
+ if (!isOwner) return;
1129
1240
  if (!renderer || !scene || !camera) return;
1130
1241
  if (!("isWebGPURenderer" in renderer)) {
1131
1242
  throw new Error("useRenderPipeline: renderer is not a WebGPURenderer but isLegacy is false.");
1132
1243
  }
1133
- const state = store.getState();
1134
- const set = store.setState;
1244
+ let owner = ownerRef.current;
1245
+ if (owner && owner.store !== store) {
1246
+ releaseOwner(owner);
1247
+ owner = null;
1248
+ }
1249
+ if (!owner) owner = ownerRef.current = createOwner(store, () => rerun(true));
1250
+ claimOwnership(owner);
1251
+ const currentOwner = owner;
1135
1252
  try {
1136
- let pipeline = state.renderPipeline ?? null;
1137
- let currentPasses = { ...state.passes };
1138
1253
  let justCreatedPipeline = false;
1139
- if (!pipeline) {
1140
- pipeline = new THREE.RenderPipeline(renderer);
1254
+ if (!owner.pipeline || owner.pipeline.renderer !== renderer) {
1255
+ disposeOwnerResources(owner);
1256
+ owner.pipeline = new THREE.RenderPipeline(renderer);
1141
1257
  justCreatedPipeline = true;
1142
1258
  }
1143
- const cacheValid = !forceScenePassRebuildRef.current && scenePassCacheRef.current && scenePassCacheRef.current.sceneUuid === scene.uuid && scenePassCacheRef.current.cameraUuid === camera.uuid;
1259
+ const pipeline = owner.pipeline;
1260
+ const cached = owner.scenePass;
1261
+ const cacheValid = !forceScenePassRebuildRef.current && cached !== null && cached.sceneUuid === scene.uuid && cached.cameraUuid === camera.uuid;
1144
1262
  let outgoingScenePass = null;
1145
1263
  let scenePass;
1146
- if (cacheValid) {
1147
- scenePass = scenePassCacheRef.current.scenePass;
1264
+ if (cacheValid && cached) {
1265
+ scenePass = cached.node;
1148
1266
  } else {
1149
- outgoingScenePass = scenePassCacheRef.current?.scenePass ?? null;
1267
+ outgoingScenePass = cached?.node ?? null;
1150
1268
  scenePass = pass(scene, camera);
1151
- scenePassCacheRef.current = { sceneUuid: scene.uuid, cameraUuid: camera.uuid, scenePass };
1269
+ owner.scenePass = { sceneUuid: scene.uuid, cameraUuid: camera.uuid, node: scenePass };
1152
1270
  forceScenePassRebuildRef.current = false;
1153
1271
  }
1154
- currentPasses.scenePass = scenePass;
1155
1272
  let outputNodeChanged = false;
1156
1273
  if (!pipeline.outputNode || justCreatedPipeline || pipeline.outputNode === outgoingScenePass) {
1157
1274
  pipeline.outputNode = scenePass;
1158
1275
  outputNodeChanged = true;
1159
1276
  }
1160
- set({ renderPipeline: pipeline, passes: currentPasses });
1161
- setRenderOverride(store, renderThroughPipeline(store));
1162
- const shouldRunCallbacks = justCreatedPipeline || !callbacksRanRef.current || !cacheValid;
1277
+ const readsChanged = lastResourceVersion.current !== resourceVersion;
1278
+ lastResourceVersion.current = resourceVersion;
1279
+ const shouldRunCallbacks = justCreatedPipeline || !callbacksRanRef.current || !cacheValid || readsChanged;
1163
1280
  if (shouldRunCallbacks) {
1164
- if (setupCBRef.current) {
1165
- const freshState = Object.assign({}, store.getState(), {
1166
- renderPipeline: pipeline,
1167
- passes: { ...currentPasses, scenePass },
1168
- renderer,
1169
- isLegacy: false
1170
- });
1171
- const setupResult = setupCBRef.current(freshState);
1172
- if (setupResult && typeof setupResult === "object") {
1173
- currentPasses = { ...currentPasses, ...setupResult };
1174
- set({ passes: currentPasses });
1281
+ let currentPasses = { scenePass };
1282
+ owner.passes = currentPasses;
1283
+ const tracker = createReadTracker("useRenderPipeline");
1284
+ const callbackState = () => Object.assign(
1285
+ createLazyCreatorState(store.getState(), primary, {
1286
+ reads: { observe: tracker.observe, nested: true },
1287
+ view
1288
+ }),
1289
+ { renderPipeline: pipeline, passes: { ...currentPasses, scenePass }, renderer, isLegacy: false }
1290
+ );
1291
+ try {
1292
+ if (setupCBRef.current) {
1293
+ const setupResult = setupCBRef.current(callbackState());
1294
+ if (setupResult && typeof setupResult === "object") {
1295
+ currentPasses = { ...currentPasses, ...setupResult };
1296
+ owner.passes = currentPasses;
1297
+ }
1175
1298
  }
1176
- }
1177
- if (mainCBRef.current) {
1178
- const freshState = Object.assign({}, store.getState(), {
1179
- renderPipeline: pipeline,
1180
- passes: { ...currentPasses, scenePass },
1181
- renderer,
1182
- isLegacy: false
1183
- });
1184
- const mainResult = mainCBRef.current(freshState);
1185
- if (mainResult && typeof mainResult === "object") {
1186
- currentPasses = { ...currentPasses, ...mainResult };
1187
- set({ passes: currentPasses });
1299
+ if (mainCBRef.current) {
1300
+ const mainResult = mainCBRef.current(callbackState());
1301
+ if (mainResult && typeof mainResult === "object") {
1302
+ currentPasses = { ...currentPasses, ...mainResult };
1303
+ owner.passes = currentPasses;
1304
+ }
1188
1305
  }
1306
+ } finally {
1307
+ tracker.closed = true;
1308
+ committedReads.current = tracker;
1189
1309
  }
1190
1310
  outputNodeChanged = true;
1191
1311
  callbacksRanRef.current = true;
1192
1312
  }
1193
1313
  if (outputNodeChanged) pipeline.needsUpdate = true;
1314
+ publishOwner(owner);
1194
1315
  if (outgoingScenePass && outgoingScenePass !== scenePass) outgoingScenePass.dispose();
1195
1316
  } catch (error) {
1317
+ publishOwner(owner);
1196
1318
  console.warn("[useRenderPipeline] Setup failed; the render pipeline was not configured:", error);
1197
1319
  }
1198
- }, [store, renderer, scene, camera, isLegacy, rebuildVersion]);
1320
+ return () => {
1321
+ if (persistRef.current) currentOwner.mounted = false;
1322
+ else scheduleRelease(currentOwner);
1323
+ };
1324
+ }, [store, primary, view, renderer, scene, camera, isLegacy, isOwner, rebuildVersion, resourceVersion, rerun]);
1199
1325
  const passes = useThree((s) => s.passes) ?? NO_PASSES;
1200
1326
  const renderPipeline = useThree((s) => s.renderPipeline) ?? null;
1201
1327
  const actions = { passes, clearPasses, reset, rebuild };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@react-three/tsl",
3
- "version": "10.0.0-canary.87166dc",
3
+ "version": "10.0.0-canary.88b1ca7",
4
4
  "description": "TSL resource hooks for react-three-fiber: uniforms, nodes, buffers, GPU storage and render pipelines on WebGPU",
5
5
  "keywords": [
6
6
  "react",
@@ -56,7 +56,7 @@
56
56
  "@types/three": "^0.185.0",
57
57
  "@webgpu/types": "^0.1.64",
58
58
  "three": ">=0.185.0",
59
- "@react-three/fiber": "10.0.0-canary.87166dc"
59
+ "@react-three/fiber": "10.0.0-canary.88b1ca7"
60
60
  },
61
61
  "scripts": {
62
62
  "build": "unbuild",
package/readme.md CHANGED
@@ -3,7 +3,7 @@
3
3
  TSL resource hooks for [react-three-fiber](https://github.com/pmndrs/react-three-fiber) on WebGPU: shared uniforms, nodes, buffers, GPU storage and render pipelines.
4
4
 
5
5
  ```bash
6
- npm install @react-three/tsl
6
+ npm install @react-three/tsl@alpha
7
7
  ```
8
8
 
9
9
  ```tsx
@@ -32,7 +32,7 @@ function Driver() {
32
32
  }
33
33
 
34
34
  export const App = () => (
35
- <Canvas renderer>
35
+ <Canvas>
36
36
  <Wobble />
37
37
  <Driver />
38
38
  </Canvas>
@@ -48,6 +48,6 @@ export const App = () => (
48
48
 
49
49
  Resources belong to the renderer: a `<Canvas primary>`, the canvases sharing its renderer (every other WebGPU canvas, or `share="id"`) and every portal inside them share one set. They live on the primary canvas's `RootState` (`state.uniforms`, `state.nodes`, `state.buffers`, `state.gpuStorage`); secondaries hold the same objects and portals inherit them from their parent (a portal's scene and camera stay its own), so `state.uniforms` reads the same everywhere.
50
50
 
51
- Works under any Canvas that runs the WebGPU renderer: `@react-three/fiber` with the `renderer` prop, or `@react-three/fiber/webgpu`. It reaches fiber only through the three-free `@react-three/fiber/extension` entry, so it adds no renderer and no second copy of fiber to your bundle.
51
+ Works under any Canvas that runs the WebGPU renderer: `@react-three/fiber` or `@react-three/fiber/webgpu` (not `/legacy`). It reaches fiber only through the three-free `@react-three/fiber/extension` entry, so it adds no renderer and no second copy of fiber to your bundle.
52
52
 
53
53
  Full documentation: [WebGPU & TSL](https://github.com/pmndrs/react-three-fiber/tree/v10/docs/webgpu).