@motion-core/motion-gpu 0.5.0 → 0.7.0

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 (71) hide show
  1. package/README.md +35 -2
  2. package/dist/core/compute-bindgroup-cache.d.ts +13 -0
  3. package/dist/core/compute-bindgroup-cache.d.ts.map +1 -0
  4. package/dist/core/compute-bindgroup-cache.js +45 -0
  5. package/dist/core/compute-bindgroup-cache.js.map +1 -0
  6. package/dist/core/compute-shader.d.ts +48 -0
  7. package/dist/core/compute-shader.d.ts.map +1 -1
  8. package/dist/core/compute-shader.js +34 -1
  9. package/dist/core/compute-shader.js.map +1 -1
  10. package/dist/core/error-diagnostics.d.ts +8 -1
  11. package/dist/core/error-diagnostics.d.ts.map +1 -1
  12. package/dist/core/error-diagnostics.js +7 -3
  13. package/dist/core/error-diagnostics.js.map +1 -1
  14. package/dist/core/error-report.d.ts.map +1 -1
  15. package/dist/core/error-report.js +19 -1
  16. package/dist/core/error-report.js.map +1 -1
  17. package/dist/core/material.d.ts.map +1 -1
  18. package/dist/core/material.js +2 -1
  19. package/dist/core/material.js.map +1 -1
  20. package/dist/core/pointer.d.ts +96 -0
  21. package/dist/core/pointer.d.ts.map +1 -0
  22. package/dist/core/pointer.js +71 -0
  23. package/dist/core/pointer.js.map +1 -0
  24. package/dist/core/renderer.d.ts.map +1 -1
  25. package/dist/core/renderer.js +150 -85
  26. package/dist/core/renderer.js.map +1 -1
  27. package/dist/core/runtime-loop.d.ts.map +1 -1
  28. package/dist/core/runtime-loop.js +26 -14
  29. package/dist/core/runtime-loop.js.map +1 -1
  30. package/dist/core/shader.d.ts +7 -2
  31. package/dist/core/shader.d.ts.map +1 -1
  32. package/dist/core/shader.js +1 -0
  33. package/dist/core/shader.js.map +1 -1
  34. package/dist/core/textures.d.ts +4 -0
  35. package/dist/core/textures.d.ts.map +1 -1
  36. package/dist/core/textures.js +2 -1
  37. package/dist/core/textures.js.map +1 -1
  38. package/dist/core/types.d.ts +1 -1
  39. package/dist/core/types.d.ts.map +1 -1
  40. package/dist/react/advanced.js +2 -1
  41. package/dist/react/index.d.ts +2 -0
  42. package/dist/react/index.d.ts.map +1 -1
  43. package/dist/react/index.js +2 -1
  44. package/dist/react/use-pointer.d.ts +94 -0
  45. package/dist/react/use-pointer.d.ts.map +1 -0
  46. package/dist/react/use-pointer.js +285 -0
  47. package/dist/react/use-pointer.js.map +1 -0
  48. package/dist/svelte/advanced.js +2 -1
  49. package/dist/svelte/index.d.ts +2 -0
  50. package/dist/svelte/index.d.ts.map +1 -1
  51. package/dist/svelte/index.js +2 -1
  52. package/dist/svelte/use-pointer.d.ts +94 -0
  53. package/dist/svelte/use-pointer.d.ts.map +1 -0
  54. package/dist/svelte/use-pointer.js +292 -0
  55. package/dist/svelte/use-pointer.js.map +1 -0
  56. package/package.json +1 -1
  57. package/src/lib/core/compute-bindgroup-cache.ts +73 -0
  58. package/src/lib/core/compute-shader.ts +86 -0
  59. package/src/lib/core/error-diagnostics.ts +29 -4
  60. package/src/lib/core/error-report.ts +26 -1
  61. package/src/lib/core/material.ts +2 -1
  62. package/src/lib/core/pointer.ts +177 -0
  63. package/src/lib/core/renderer.ts +198 -92
  64. package/src/lib/core/runtime-loop.ts +37 -16
  65. package/src/lib/core/shader.ts +13 -2
  66. package/src/lib/core/textures.ts +6 -1
  67. package/src/lib/core/types.ts +1 -1
  68. package/src/lib/react/index.ts +10 -0
  69. package/src/lib/react/use-pointer.ts +515 -0
  70. package/src/lib/svelte/index.ts +10 -0
  71. package/src/lib/svelte/use-pointer.ts +507 -0
@@ -0,0 +1,177 @@
1
+ import type { RenderMode } from './types.js';
2
+
3
+ /**
4
+ * Pointer kind normalized from DOM `PointerEvent.pointerType`.
5
+ */
6
+ export type PointerKind = 'mouse' | 'pen' | 'touch';
7
+
8
+ /**
9
+ * 2D tuple used by pointer coordinate payloads.
10
+ */
11
+ export type PointerVec2 = [number, number];
12
+
13
+ /**
14
+ * Normalized pointer coordinates exposed to runtime hooks.
15
+ */
16
+ export interface PointerPoint {
17
+ /**
18
+ * CSS pixel coordinates relative to canvas top-left corner.
19
+ */
20
+ px: PointerVec2;
21
+ /**
22
+ * UV coordinates in shader-friendly orientation (`y` grows upward).
23
+ */
24
+ uv: PointerVec2;
25
+ /**
26
+ * Normalized device coordinates (`-1..1`, `y` grows upward).
27
+ */
28
+ ndc: PointerVec2;
29
+ }
30
+
31
+ /**
32
+ * Mutable pointer state snapshot exposed by `usePointer`.
33
+ */
34
+ export interface PointerState extends PointerPoint {
35
+ inside: boolean;
36
+ pressed: boolean;
37
+ dragging: boolean;
38
+ pointerType: PointerKind | null;
39
+ pointerId: number | null;
40
+ button: number | null;
41
+ buttons: number;
42
+ time: number;
43
+ downPx: PointerVec2 | null;
44
+ downUv: PointerVec2 | null;
45
+ deltaPx: PointerVec2;
46
+ deltaUv: PointerVec2;
47
+ velocityPx: PointerVec2;
48
+ velocityUv: PointerVec2;
49
+ }
50
+
51
+ /**
52
+ * Modifier key snapshot attached to pointer click events.
53
+ */
54
+ export interface PointerModifiers {
55
+ alt: boolean;
56
+ ctrl: boolean;
57
+ shift: boolean;
58
+ meta: boolean;
59
+ }
60
+
61
+ /**
62
+ * Click/tap payload produced by `usePointer`.
63
+ */
64
+ export interface PointerClick extends PointerPoint {
65
+ id: number;
66
+ time: number;
67
+ pointerType: PointerKind;
68
+ pointerId: number;
69
+ button: number;
70
+ modifiers: PointerModifiers;
71
+ }
72
+
73
+ /**
74
+ * Frame wake-up strategy for pointer-driven interactions.
75
+ */
76
+ export type PointerFrameRequestMode = 'advance' | 'auto' | 'invalidate' | 'none';
77
+
78
+ /**
79
+ * Returns a monotonic timestamp in seconds.
80
+ */
81
+ export function getPointerNowSeconds(): number {
82
+ if (typeof performance !== 'undefined' && typeof performance.now === 'function') {
83
+ return performance.now() / 1000;
84
+ }
85
+
86
+ return Date.now() / 1000;
87
+ }
88
+
89
+ /**
90
+ * Creates the initial pointer state snapshot.
91
+ */
92
+ export function createInitialPointerState(): PointerState {
93
+ return {
94
+ px: [0, 0],
95
+ uv: [0, 0],
96
+ ndc: [-1, -1],
97
+ inside: false,
98
+ pressed: false,
99
+ dragging: false,
100
+ pointerType: null,
101
+ pointerId: null,
102
+ button: null,
103
+ buttons: 0,
104
+ time: getPointerNowSeconds(),
105
+ downPx: null,
106
+ downUv: null,
107
+ deltaPx: [0, 0],
108
+ deltaUv: [0, 0],
109
+ velocityPx: [0, 0],
110
+ velocityUv: [0, 0]
111
+ };
112
+ }
113
+
114
+ /**
115
+ * Normalized coordinate payload for a pointer position against a canvas rect.
116
+ */
117
+ export interface PointerCoordinates extends PointerPoint {
118
+ inside: boolean;
119
+ }
120
+
121
+ /**
122
+ * Converts client coordinates to canvas-relative pointer coordinates.
123
+ */
124
+ export function getPointerCoordinates(
125
+ clientX: number,
126
+ clientY: number,
127
+ rect: Pick<DOMRectReadOnly, 'height' | 'left' | 'top' | 'width'>
128
+ ): PointerCoordinates {
129
+ const width = Math.max(rect.width, 1);
130
+ const height = Math.max(rect.height, 1);
131
+ const nx = (clientX - rect.left) / width;
132
+ const ny = (clientY - rect.top) / height;
133
+ const pxX = clientX - rect.left;
134
+ const pxY = clientY - rect.top;
135
+ const uvX = nx;
136
+ const uvY = 1 - ny;
137
+
138
+ return {
139
+ px: [pxX, pxY],
140
+ uv: [uvX, uvY],
141
+ ndc: [nx * 2 - 1, uvY * 2 - 1],
142
+ inside: nx >= 0 && nx <= 1 && ny >= 0 && ny <= 1
143
+ };
144
+ }
145
+
146
+ /**
147
+ * Resolves frame wake-up strategy for pointer-driven updates.
148
+ */
149
+ export function resolvePointerFrameRequestMode(
150
+ mode: PointerFrameRequestMode,
151
+ renderMode: RenderMode
152
+ ): Exclude<PointerFrameRequestMode, 'auto'> {
153
+ if (mode !== 'auto') {
154
+ return mode;
155
+ }
156
+
157
+ if (renderMode === 'manual') {
158
+ return 'advance';
159
+ }
160
+
161
+ if (renderMode === 'on-demand') {
162
+ return 'invalidate';
163
+ }
164
+
165
+ return 'none';
166
+ }
167
+
168
+ /**
169
+ * Normalizes unknown pointer kind values to the public `PointerKind`.
170
+ */
171
+ export function normalizePointerKind(pointerType: string): PointerKind {
172
+ if (pointerType === 'mouse' || pointerType === 'pen' || pointerType === 'touch') {
173
+ return pointerType;
174
+ }
175
+
176
+ return 'mouse';
177
+ }
@@ -18,11 +18,12 @@ import {
18
18
  } from './textures.js';
19
19
  import { packUniformsInto } from './uniforms.js';
20
20
  import {
21
- buildComputeShaderSource,
22
- buildPingPongComputeShaderSource,
21
+ buildComputeShaderSourceWithMap,
22
+ buildPingPongComputeShaderSourceWithMap,
23
23
  extractWorkgroupSize,
24
24
  storageTextureSampleScalarType
25
25
  } from './compute-shader.js';
26
+ import { createComputeStorageBindGroupCache } from './compute-bindgroup-cache.js';
26
27
  import { normalizeStorageBufferDefinition } from './storage-buffers.js';
27
28
  import type {
28
29
  AnyPass,
@@ -62,6 +63,7 @@ interface RuntimeTextureBinding {
62
63
  key: string;
63
64
  samplerBinding: number;
64
65
  textureBinding: number;
66
+ fragmentVisible: boolean;
65
67
  sampler: GPUSampler;
66
68
  fallbackTexture: GPUTexture;
67
69
  fallbackView: GPUTextureView;
@@ -109,6 +111,8 @@ interface PingPongTexturePair {
109
111
  textureB: GPUTexture;
110
112
  viewB: GPUTextureView;
111
113
  bindGroupLayout: GPUBindGroupLayout;
114
+ readAWriteBBindGroup: GPUBindGroup | null;
115
+ readBWriteABindGroup: GPUBindGroup | null;
112
116
  }
113
117
 
114
118
  /**
@@ -187,6 +191,7 @@ async function assertCompilation(
187
191
  options?: {
188
192
  lineMap?: ShaderLineMap;
189
193
  fragmentSource?: string;
194
+ computeSource?: string;
190
195
  includeSources?: Record<string, string>;
191
196
  defineBlockSource?: string;
192
197
  materialSource?: {
@@ -197,6 +202,8 @@ async function assertCompilation(
197
202
  functionName?: string;
198
203
  } | null;
199
204
  runtimeContext?: ShaderCompilationRuntimeContext;
205
+ errorPrefix?: string;
206
+ shaderStage?: 'fragment' | 'compute';
200
207
  }
201
208
  ): Promise<void> {
202
209
  const info = await module.getCompilationInfo();
@@ -227,11 +234,14 @@ async function assertCompilation(
227
234
  return `[${contextLabel.join(' | ')}] ${diagnostic.message}`;
228
235
  })
229
236
  .join('\n');
230
- const error = new Error(`WGSL compilation failed:\n${summary}`);
237
+ const prefix = options?.errorPrefix ?? 'WGSL compilation failed';
238
+ const error = new Error(`${prefix}:\n${summary}`);
231
239
  throw attachShaderCompilationDiagnostics(error, {
232
240
  kind: 'shader-compilation',
241
+ ...(options?.shaderStage !== undefined ? { shaderStage: options.shaderStage } : {}),
233
242
  diagnostics,
234
243
  fragmentSource: options?.fragmentSource ?? '',
244
+ ...(options?.computeSource !== undefined ? { computeSource: options.computeSource } : {}),
235
245
  includeSources: options?.includeSources ?? {},
236
246
  ...(options?.defineBlockSource !== undefined
237
247
  ? { defineBlockSource: options.defineBlockSource }
@@ -245,6 +255,64 @@ function toSortedUniqueStrings(values: string[]): string[] {
245
255
  return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
246
256
  }
247
257
 
258
+ function extractGeneratedLineFromComputeError(message: string): number | null {
259
+ const lineMatch = message.match(/\bline\s+(\d+)\b/i);
260
+ if (lineMatch) {
261
+ const parsed = Number.parseInt(lineMatch[1] ?? '', 10);
262
+ if (Number.isFinite(parsed) && parsed > 0) {
263
+ return parsed;
264
+ }
265
+ }
266
+
267
+ const colonMatch = message.match(/:(\d+):\d+/);
268
+ if (colonMatch) {
269
+ const parsed = Number.parseInt(colonMatch[1] ?? '', 10);
270
+ if (Number.isFinite(parsed) && parsed > 0) {
271
+ return parsed;
272
+ }
273
+ }
274
+
275
+ return null;
276
+ }
277
+
278
+ function toComputeCompilationError(input: {
279
+ error: unknown;
280
+ lineMap: ShaderLineMap;
281
+ computeSource: string;
282
+ runtimeContext: ShaderCompilationRuntimeContext;
283
+ }): Error {
284
+ const baseError =
285
+ input.error instanceof Error ? input.error : new Error(String(input.error ?? 'Unknown error'));
286
+ const generatedLine = extractGeneratedLineFromComputeError(baseError.message) ?? 0;
287
+ const sourceLocation = generatedLine > 0 ? (input.lineMap[generatedLine] ?? null) : null;
288
+ const diagnostics = [
289
+ {
290
+ generatedLine,
291
+ message: baseError.message,
292
+ sourceLocation
293
+ }
294
+ ];
295
+ const sourceLabel = formatShaderSourceLocation(sourceLocation);
296
+ const generatedLineLabel = generatedLine > 0 ? `generated WGSL line ${generatedLine}` : null;
297
+ const contextLabel = [sourceLabel, generatedLineLabel].filter((value) => Boolean(value));
298
+ const summary =
299
+ contextLabel.length > 0
300
+ ? `[${contextLabel.join(' | ')}] ${baseError.message}`
301
+ : baseError.message;
302
+ const wrapped = new Error(`Compute shader compilation failed:\n${summary}`);
303
+
304
+ return attachShaderCompilationDiagnostics(wrapped, {
305
+ kind: 'shader-compilation',
306
+ shaderStage: 'compute',
307
+ diagnostics,
308
+ fragmentSource: '',
309
+ computeSource: input.computeSource,
310
+ includeSources: {},
311
+ materialSource: null,
312
+ runtimeContext: input.runtimeContext
313
+ });
314
+ }
315
+
248
316
  function buildPassGraphSnapshot(
249
317
  passes: AnyPass[] | undefined
250
318
  ): NonNullable<ShaderCompilationRuntimeContext['passGraph']> {
@@ -667,10 +735,13 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
667
735
  try {
668
736
  const runtimeContext = buildShaderCompilationRuntimeContext(options);
669
737
  const convertLinearToSrgb = shouldConvertLinearToSrgb(options.outputColorSpace, format);
738
+ const fragmentTextureKeys = options.textureKeys.filter(
739
+ (key) => options.textureDefinitions[key]?.fragmentVisible !== false
740
+ );
670
741
  const builtShader = buildShaderSourceWithMap(
671
742
  options.fragmentWgsl,
672
743
  options.uniformLayout,
673
- options.textureKeys,
744
+ fragmentTextureKeys,
674
745
  {
675
746
  convertLinearToSrgb,
676
747
  fragmentLineMap: options.fragmentLineMap,
@@ -702,13 +773,18 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
702
773
  const storageBufferDefinitions = options.storageBufferDefinitions ?? {};
703
774
  const storageTextureKeys = options.storageTextureKeys ?? [];
704
775
  const storageTextureKeySet = new Set(storageTextureKeys);
705
- const textureBindings = options.textureKeys.map((key, index): RuntimeTextureBinding => {
776
+ const fragmentTextureIndexByKey = new Map(
777
+ fragmentTextureKeys.map((key, index) => [key, index] as const)
778
+ );
779
+ const textureBindings = options.textureKeys.map((key): RuntimeTextureBinding => {
706
780
  const config = normalizedTextureDefinitions[key];
707
781
  if (!config) {
708
782
  throw new Error(`Missing texture definition for "${key}"`);
709
783
  }
710
784
 
711
- const { samplerBinding, textureBinding } = getTextureBindings(index);
785
+ const fragmentTextureIndex = fragmentTextureIndexByKey.get(key);
786
+ const fragmentVisible = fragmentTextureIndex !== undefined;
787
+ const { samplerBinding, textureBinding } = getTextureBindings(fragmentTextureIndex ?? 0);
712
788
  const sampler = device.createSampler({
713
789
  magFilter: config.filter,
714
790
  minFilter: config.filter,
@@ -731,6 +807,7 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
731
807
  key,
732
808
  samplerBinding,
733
809
  textureBinding,
810
+ fragmentVisible,
734
811
  sampler,
735
812
  fallbackTexture,
736
813
  fallbackView,
@@ -779,9 +856,49 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
779
856
 
780
857
  return runtimeBinding;
781
858
  });
859
+ const textureBindingByKey = new Map(textureBindings.map((binding) => [binding.key, binding]));
860
+ const fragmentTextureBindings = textureBindings.filter((binding) => binding.fragmentVisible);
861
+
862
+ const computeStorageBufferLayoutEntries: GPUBindGroupLayoutEntry[] = storageBufferKeys.map(
863
+ (key, index) => {
864
+ const def = storageBufferDefinitions[key];
865
+ const access = def?.access ?? 'read-write';
866
+ const bufferType: GPUBufferBindingType =
867
+ access === 'read' ? 'read-only-storage' : 'storage';
868
+ return {
869
+ binding: index,
870
+ visibility: GPUShaderStage.COMPUTE,
871
+ buffer: { type: bufferType }
872
+ };
873
+ }
874
+ );
875
+ const computeStorageBufferTopologyKey = storageBufferKeys
876
+ .map((key) => `${key}:${storageBufferDefinitions[key]?.access ?? 'read-write'}`)
877
+ .join('|');
878
+
879
+ const computeStorageTextureLayoutEntries: GPUBindGroupLayoutEntry[] = storageTextureKeys.map(
880
+ (key, index) => {
881
+ const config = normalizedTextureDefinitions[key];
882
+ return {
883
+ binding: index,
884
+ visibility: GPUShaderStage.COMPUTE,
885
+ storageTexture: {
886
+ access: 'write-only' as GPUStorageTextureAccess,
887
+ format: (config?.format ?? 'rgba8unorm') as GPUTextureFormat,
888
+ viewDimension: '2d'
889
+ }
890
+ };
891
+ }
892
+ );
893
+ const computeStorageTextureTopologyKey = storageTextureKeys
894
+ .map((key) => `${key}:${normalizedTextureDefinitions[key]?.format ?? 'rgba8unorm'}`)
895
+ .join('|');
896
+
897
+ const computeStorageBufferBindGroupCache = createComputeStorageBindGroupCache(device);
898
+ const computeStorageTextureBindGroupCache = createComputeStorageBindGroupCache(device);
782
899
 
783
900
  const bindGroupLayout = device.createBindGroupLayout({
784
- entries: createBindGroupLayoutEntries(textureBindings)
901
+ entries: createBindGroupLayoutEntries(fragmentTextureBindings)
785
902
  });
786
903
  const fragmentStorageBindGroupLayout =
787
904
  storageBufferKeys.length > 0
@@ -977,7 +1094,9 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
977
1094
  viewA: textureA.createView(),
978
1095
  textureB,
979
1096
  viewB: textureB.createView(),
980
- bindGroupLayout
1097
+ bindGroupLayout,
1098
+ readAWriteBBindGroup: null,
1099
+ readBWriteABindGroup: null
981
1100
  };
982
1101
  pingPongTexturePairs.set(target, pair);
983
1102
  return pair;
@@ -1028,8 +1147,8 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
1028
1147
  const isPingPongPipeline = Boolean(
1029
1148
  buildOptions.pingPongTarget && buildOptions.pingPongFormat
1030
1149
  );
1031
- const shaderCode = isPingPongPipeline
1032
- ? buildPingPongComputeShaderSource({
1150
+ const builtComputeShader = isPingPongPipeline
1151
+ ? buildPingPongComputeShaderSourceWithMap({
1033
1152
  compute: buildOptions.computeSource,
1034
1153
  uniformLayout: options.uniformLayout,
1035
1154
  storageBufferKeys,
@@ -1037,7 +1156,7 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
1037
1156
  target: buildOptions.pingPongTarget!,
1038
1157
  targetFormat: buildOptions.pingPongFormat!
1039
1158
  })
1040
- : buildComputeShaderSource({
1159
+ : buildComputeShaderSourceWithMap({
1041
1160
  compute: buildOptions.computeSource,
1042
1161
  uniformLayout: options.uniformLayout,
1043
1162
  storageBufferKeys,
@@ -1046,7 +1165,7 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
1046
1165
  storageTextureDefinitions: storageTextureDefs
1047
1166
  });
1048
1167
 
1049
- const computeShaderModule = device.createShaderModule({ code: shaderCode });
1168
+ const computeShaderModule = device.createShaderModule({ code: builtComputeShader.code });
1050
1169
  const workgroupSize = extractWorkgroupSize(buildOptions.computeSource);
1051
1170
 
1052
1171
  // Compute bind group layout: group(0)=uniforms, group(1)=storage buffers, group(2)=storage textures
@@ -1065,20 +1184,9 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
1065
1184
  ]
1066
1185
  });
1067
1186
 
1068
- const storageBGLEntries: GPUBindGroupLayoutEntry[] = storageBufferKeys.map((key, index) => {
1069
- const def = storageBufferDefinitions[key];
1070
- const access = def?.access ?? 'read-write';
1071
- const bufferType: GPUBufferBindingType =
1072
- access === 'read' ? 'read-only-storage' : 'storage';
1073
- return {
1074
- binding: index,
1075
- visibility: GPUShaderStage.COMPUTE,
1076
- buffer: { type: bufferType }
1077
- };
1078
- });
1079
1187
  const storageBGL =
1080
- storageBGLEntries.length > 0
1081
- ? device.createBindGroupLayout({ entries: storageBGLEntries })
1188
+ computeStorageBufferLayoutEntries.length > 0
1189
+ ? device.createBindGroupLayout({ entries: computeStorageBufferLayoutEntries })
1082
1190
  : null;
1083
1191
 
1084
1192
  const storageTextureBGLEntries: GPUBindGroupLayoutEntry[] = isPingPongPipeline
@@ -1104,18 +1212,7 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
1104
1212
  }
1105
1213
  }
1106
1214
  ]
1107
- : storageTextureKeys.map((key, index) => {
1108
- const texDef = options.textureDefinitions[key];
1109
- return {
1110
- binding: index,
1111
- visibility: GPUShaderStage.COMPUTE,
1112
- storageTexture: {
1113
- access: 'write-only' as GPUStorageTextureAccess,
1114
- format: (texDef?.format ?? 'rgba8unorm') as GPUTextureFormat,
1115
- viewDimension: '2d'
1116
- }
1117
- };
1118
- });
1215
+ : computeStorageTextureLayoutEntries;
1119
1216
  const storageTextureBGL =
1120
1217
  storageTextureBGLEntries.length > 0
1121
1218
  ? device.createBindGroupLayout({ entries: storageTextureBGLEntries })
@@ -1133,13 +1230,23 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
1133
1230
  }
1134
1231
 
1135
1232
  const computePipelineLayout = device.createPipelineLayout({ bindGroupLayouts });
1136
- const pipeline = device.createComputePipeline({
1137
- layout: computePipelineLayout,
1138
- compute: {
1139
- module: computeShaderModule,
1140
- entryPoint: 'compute'
1141
- }
1142
- });
1233
+ let pipeline: GPUComputePipeline;
1234
+ try {
1235
+ pipeline = device.createComputePipeline({
1236
+ layout: computePipelineLayout,
1237
+ compute: {
1238
+ module: computeShaderModule,
1239
+ entryPoint: 'compute'
1240
+ }
1241
+ });
1242
+ } catch (error) {
1243
+ throw toComputeCompilationError({
1244
+ error,
1245
+ lineMap: builtComputeShader.lineMap,
1246
+ computeSource: buildOptions.computeSource,
1247
+ runtimeContext
1248
+ });
1249
+ }
1143
1250
 
1144
1251
  // Build uniform bind group for compute (group 0)
1145
1252
  const computeUniformBindGroup = device.createBindGroup({
@@ -1162,63 +1269,49 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
1162
1269
 
1163
1270
  // Helper to get the storage bind group for dispatch
1164
1271
  const getComputeStorageBindGroup = (): GPUBindGroup | null => {
1165
- if (storageBufferKeys.length === 0) {
1272
+ if (computeStorageBufferLayoutEntries.length === 0) {
1166
1273
  return null;
1167
1274
  }
1168
- // Rebuild bind group with current storage buffers
1169
- const storageBGLEntries: GPUBindGroupLayoutEntry[] = storageBufferKeys.map((key, index) => {
1170
- const def = storageBufferDefinitions[key];
1171
- const access = def?.access ?? 'read-write';
1172
- const bufferType: GPUBufferBindingType =
1173
- access === 'read' ? 'read-only-storage' : 'storage';
1174
- return {
1175
- binding: index,
1176
- visibility: GPUShaderStage.COMPUTE,
1177
- buffer: { type: bufferType }
1178
- };
1179
- });
1180
- const storageBGL = device.createBindGroupLayout({ entries: storageBGLEntries });
1181
- const storageEntries: GPUBindGroupEntry[] = storageBufferKeys.map((key, index) => {
1275
+ const resources: GPUBuffer[] = storageBufferKeys.map((key) => {
1182
1276
  const buffer = storageBufferMap.get(key);
1183
1277
  if (!buffer) {
1184
1278
  throw new Error(`Storage buffer "${key}" not allocated.`);
1185
1279
  }
1280
+ return buffer;
1281
+ });
1282
+ const storageEntries: GPUBindGroupEntry[] = resources.map((buffer, index) => {
1186
1283
  return { binding: index, resource: { buffer } };
1187
1284
  });
1188
- return device.createBindGroup({
1189
- layout: storageBGL,
1190
- entries: storageEntries
1285
+ return computeStorageBufferBindGroupCache.getOrCreate({
1286
+ topologyKey: computeStorageBufferTopologyKey,
1287
+ layoutEntries: computeStorageBufferLayoutEntries,
1288
+ bindGroupEntries: storageEntries,
1289
+ resourceRefs: resources
1191
1290
  });
1192
1291
  };
1193
1292
 
1194
1293
  // Helper to get the storage texture bind group for compute dispatch (group 2)
1195
1294
  const getComputeStorageTextureBindGroup = (): GPUBindGroup | null => {
1196
- if (storageTextureKeys.length === 0) {
1295
+ if (computeStorageTextureLayoutEntries.length === 0) {
1197
1296
  return null;
1198
1297
  }
1199
- const entries: GPUBindGroupLayoutEntry[] = storageTextureKeys.map((key, index) => {
1200
- const texDef = options.textureDefinitions[key];
1201
- return {
1202
- binding: index,
1203
- visibility: GPUShaderStage.COMPUTE,
1204
- storageTexture: {
1205
- access: 'write-only' as GPUStorageTextureAccess,
1206
- format: (texDef?.format ?? 'rgba8unorm') as GPUTextureFormat,
1207
- viewDimension: '2d' as GPUTextureViewDimension
1208
- }
1209
- };
1210
- });
1211
- const bgl = device.createBindGroupLayout({ entries });
1212
-
1213
- const bgEntries: GPUBindGroupEntry[] = storageTextureKeys.map((key, index) => {
1214
- const binding = textureBindings.find((b) => b.key === key);
1298
+ const resources: GPUTextureView[] = storageTextureKeys.map((key) => {
1299
+ const binding = textureBindingByKey.get(key);
1215
1300
  if (!binding || !binding.texture) {
1216
1301
  throw new Error(`Storage texture "${key}" not allocated.`);
1217
1302
  }
1218
- return { binding: index, resource: binding.view };
1303
+ return binding.view;
1304
+ });
1305
+ const bgEntries: GPUBindGroupEntry[] = resources.map((view, index) => {
1306
+ return { binding: index, resource: view };
1219
1307
  });
1220
1308
 
1221
- return device.createBindGroup({ layout: bgl, entries: bgEntries });
1309
+ return computeStorageTextureBindGroupCache.getOrCreate({
1310
+ topologyKey: computeStorageTextureTopologyKey,
1311
+ layoutEntries: computeStorageTextureLayoutEntries,
1312
+ bindGroupEntries: bgEntries,
1313
+ resourceRefs: resources
1314
+ });
1222
1315
  };
1223
1316
 
1224
1317
  // Helper to get ping-pong storage texture bind group for compute dispatch (group 2)
@@ -1227,15 +1320,28 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
1227
1320
  readFromA: boolean
1228
1321
  ): GPUBindGroup => {
1229
1322
  const pair = ensurePingPongTexturePair(target);
1230
- const readView = readFromA ? pair.viewA : pair.viewB;
1231
- const writeView = readFromA ? pair.viewB : pair.viewA;
1232
- return device.createBindGroup({
1233
- layout: pair.bindGroupLayout,
1234
- entries: [
1235
- { binding: 0, resource: readView },
1236
- { binding: 1, resource: writeView }
1237
- ]
1238
- });
1323
+ if (readFromA) {
1324
+ if (!pair.readAWriteBBindGroup) {
1325
+ pair.readAWriteBBindGroup = device.createBindGroup({
1326
+ layout: pair.bindGroupLayout,
1327
+ entries: [
1328
+ { binding: 0, resource: pair.viewA },
1329
+ { binding: 1, resource: pair.viewB }
1330
+ ]
1331
+ });
1332
+ }
1333
+ return pair.readAWriteBBindGroup;
1334
+ }
1335
+ if (!pair.readBWriteABindGroup) {
1336
+ pair.readBWriteABindGroup = device.createBindGroup({
1337
+ layout: pair.bindGroupLayout,
1338
+ entries: [
1339
+ { binding: 0, resource: pair.viewB },
1340
+ { binding: 1, resource: pair.viewA }
1341
+ ]
1342
+ });
1343
+ }
1344
+ return pair.readBWriteABindGroup;
1239
1345
  };
1240
1346
 
1241
1347
  const frameBuffer = device.createBuffer({
@@ -1267,7 +1373,7 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
1267
1373
  { binding: UNIFORM_BINDING, resource: { buffer: uniformBuffer } }
1268
1374
  ];
1269
1375
 
1270
- for (const binding of textureBindings) {
1376
+ for (const binding of fragmentTextureBindings) {
1271
1377
  entries.push({
1272
1378
  binding: binding.samplerBinding,
1273
1379
  resource: binding.sampler
@@ -1839,7 +1945,7 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
1839
1945
  if (storageTextureKeySet.has(binding.key)) continue;
1840
1946
  const nextTexture =
1841
1947
  textures[binding.key] ?? normalizedTextureDefinitions[binding.key]?.source ?? null;
1842
- if (updateTextureBinding(binding, nextTexture, renderMode)) {
1948
+ if (updateTextureBinding(binding, nextTexture, renderMode) && binding.fragmentVisible) {
1843
1949
  bindGroupDirty = true;
1844
1950
  }
1845
1951
  }