@vib3code/sdk 2.0.1

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 (258) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/DOCS/BLUEPRINT_EXECUTION_PLAN_2026-01-07.md +34 -0
  3. package/DOCS/CI_TESTING.md +38 -0
  4. package/DOCS/CLI_ONBOARDING.md +75 -0
  5. package/DOCS/CONTROL_REFERENCE.md +64 -0
  6. package/DOCS/DEV_TRACK_ANALYSIS.md +77 -0
  7. package/DOCS/DEV_TRACK_PLAN_2026-01-07.md +42 -0
  8. package/DOCS/DEV_TRACK_SESSION_2026-01-31.md +220 -0
  9. package/DOCS/ENV_SETUP.md +189 -0
  10. package/DOCS/EXPORT_FORMATS.md +417 -0
  11. package/DOCS/GPU_DISPOSAL_GUIDE.md +21 -0
  12. package/DOCS/LICENSING_TIERS.md +275 -0
  13. package/DOCS/MASTER_PLAN_2026-01-31.md +570 -0
  14. package/DOCS/OBS_SETUP_GUIDE.md +98 -0
  15. package/DOCS/PROJECT_SETUP.md +66 -0
  16. package/DOCS/RENDERER_LIFECYCLE.md +40 -0
  17. package/DOCS/REPO_MANIFEST.md +121 -0
  18. package/DOCS/SESSION_014_PLAN.md +195 -0
  19. package/DOCS/SESSION_LOG_2026-01-07.md +56 -0
  20. package/DOCS/STRATEGIC_BLUEPRINT_2026-01-07.md +72 -0
  21. package/DOCS/SYSTEM_AUDIT_2026-01-30.md +738 -0
  22. package/DOCS/SYSTEM_INVENTORY.md +520 -0
  23. package/DOCS/TELEMETRY_EXPORTS.md +34 -0
  24. package/DOCS/WEBGPU_STATUS.md +38 -0
  25. package/DOCS/XR_BENCHMARKS.md +608 -0
  26. package/LICENSE +21 -0
  27. package/README.md +426 -0
  28. package/docs/.nojekyll +0 -0
  29. package/docs/01-dissolution_of_euclidean_hegemony.html +346 -0
  30. package/docs/02-hyperspatial_ego_death.html +346 -0
  31. package/docs/03-post_cartesian_sublime.html +346 -0
  32. package/docs/04-crystalline_void_meditation.html +346 -0
  33. package/docs/05-quantum_decoherence_ballet.html +346 -0
  34. package/docs/06-dissolution_of_euclidean_hegemony.html +346 -0
  35. package/docs/07-hyperspatial_ego_death.html +346 -0
  36. package/docs/08-post_cartesian_sublime.html +346 -0
  37. package/docs/09-crystalline_void_meditation.html +346 -0
  38. package/docs/10-quantum_decoherence_ballet.html +346 -0
  39. package/docs/11-dissolution_of_euclidean_hegemony.html +346 -0
  40. package/docs/12-hyperspatial_ego_death.html +346 -0
  41. package/docs/13-post_cartesian_sublime.html +346 -0
  42. package/docs/index.html +794 -0
  43. package/docs/test-hub.html +441 -0
  44. package/docs/url-state.js +102 -0
  45. package/docs/vib3-exports/01-quantum-quantum-tetrahedron-lattice.html +489 -0
  46. package/docs/vib3-exports/02-quantum-quantum-hypersphere-matrix.html +489 -0
  47. package/docs/vib3-exports/03-quantum-quantum-hypertetra-fractal.html +489 -0
  48. package/docs/vib3-exports/04-faceted-faceted-crystal-structure.html +407 -0
  49. package/docs/vib3-exports/05-faceted-faceted-klein-bottle.html +407 -0
  50. package/docs/vib3-exports/06-faceted-faceted-hypertetra-torus.html +407 -0
  51. package/docs/vib3-exports/07-holographic-holographic-wave-field.html +457 -0
  52. package/docs/vib3-exports/08-holographic-holographic-hypersphere-sphere.html +457 -0
  53. package/docs/vib3-exports/09-holographic-holographic-hypertetra-crystal.html +457 -0
  54. package/docs/vib3-exports/index.html +238 -0
  55. package/docs/webgpu-live.html +702 -0
  56. package/package.json +367 -0
  57. package/src/advanced/AIPresetGenerator.js +777 -0
  58. package/src/advanced/MIDIController.js +703 -0
  59. package/src/advanced/OffscreenWorker.js +1051 -0
  60. package/src/advanced/WebGPUCompute.js +1051 -0
  61. package/src/advanced/WebXRRenderer.js +680 -0
  62. package/src/agent/cli/AgentCLI.js +615 -0
  63. package/src/agent/cli/index.js +14 -0
  64. package/src/agent/index.js +73 -0
  65. package/src/agent/mcp/MCPServer.js +950 -0
  66. package/src/agent/mcp/index.js +9 -0
  67. package/src/agent/mcp/tools.js +548 -0
  68. package/src/agent/telemetry/EventStream.js +669 -0
  69. package/src/agent/telemetry/Instrumentation.js +618 -0
  70. package/src/agent/telemetry/TelemetryExporters.js +427 -0
  71. package/src/agent/telemetry/TelemetryService.js +464 -0
  72. package/src/agent/telemetry/index.js +52 -0
  73. package/src/benchmarks/BenchmarkRunner.js +381 -0
  74. package/src/benchmarks/MetricsCollector.js +299 -0
  75. package/src/benchmarks/index.js +9 -0
  76. package/src/benchmarks/scenes.js +259 -0
  77. package/src/cli/index.js +675 -0
  78. package/src/config/ApiConfig.js +88 -0
  79. package/src/core/CanvasManager.js +217 -0
  80. package/src/core/ErrorReporter.js +117 -0
  81. package/src/core/ParameterMapper.js +333 -0
  82. package/src/core/Parameters.js +396 -0
  83. package/src/core/RendererContracts.js +200 -0
  84. package/src/core/UnifiedResourceManager.js +370 -0
  85. package/src/core/VIB3Engine.js +636 -0
  86. package/src/core/renderers/FacetedRendererAdapter.js +32 -0
  87. package/src/core/renderers/HolographicRendererAdapter.js +29 -0
  88. package/src/core/renderers/QuantumRendererAdapter.js +29 -0
  89. package/src/core/renderers/RendererLifecycleManager.js +63 -0
  90. package/src/creative/ColorPresetsSystem.js +980 -0
  91. package/src/creative/ParameterTimeline.js +1061 -0
  92. package/src/creative/PostProcessingPipeline.js +1113 -0
  93. package/src/creative/TransitionAnimator.js +683 -0
  94. package/src/export/CSSExporter.js +226 -0
  95. package/src/export/CardGeneratorBase.js +279 -0
  96. package/src/export/ExportManager.js +580 -0
  97. package/src/export/FacetedCardGenerator.js +279 -0
  98. package/src/export/HolographicCardGenerator.js +543 -0
  99. package/src/export/LottieExporter.js +552 -0
  100. package/src/export/QuantumCardGenerator.js +315 -0
  101. package/src/export/SVGExporter.js +519 -0
  102. package/src/export/ShaderExporter.js +903 -0
  103. package/src/export/TradingCardGenerator.js +3055 -0
  104. package/src/export/TradingCardManager.js +181 -0
  105. package/src/export/VIB3PackageExporter.js +559 -0
  106. package/src/export/index.js +14 -0
  107. package/src/export/systems/TradingCardSystemFaceted.js +494 -0
  108. package/src/export/systems/TradingCardSystemHolographic.js +452 -0
  109. package/src/export/systems/TradingCardSystemQuantum.js +411 -0
  110. package/src/faceted/FacetedSystem.js +963 -0
  111. package/src/features/CollectionManager.js +433 -0
  112. package/src/gallery/CollectionManager.js +240 -0
  113. package/src/gallery/GallerySystem.js +485 -0
  114. package/src/geometry/GeometryFactory.js +314 -0
  115. package/src/geometry/GeometryLibrary.js +72 -0
  116. package/src/geometry/buffers/BufferBuilder.js +338 -0
  117. package/src/geometry/buffers/index.js +18 -0
  118. package/src/geometry/generators/Crystal.js +420 -0
  119. package/src/geometry/generators/Fractal.js +298 -0
  120. package/src/geometry/generators/KleinBottle.js +197 -0
  121. package/src/geometry/generators/Sphere.js +192 -0
  122. package/src/geometry/generators/Tesseract.js +160 -0
  123. package/src/geometry/generators/Tetrahedron.js +225 -0
  124. package/src/geometry/generators/Torus.js +304 -0
  125. package/src/geometry/generators/Wave.js +341 -0
  126. package/src/geometry/index.js +142 -0
  127. package/src/geometry/warp/HypersphereCore.js +211 -0
  128. package/src/geometry/warp/HypertetraCore.js +386 -0
  129. package/src/geometry/warp/index.js +57 -0
  130. package/src/holograms/HolographicVisualizer.js +1073 -0
  131. package/src/holograms/RealHolographicSystem.js +966 -0
  132. package/src/holograms/variantRegistry.js +69 -0
  133. package/src/integrations/FigmaPlugin.js +854 -0
  134. package/src/integrations/OBSMode.js +754 -0
  135. package/src/integrations/ThreeJsPackage.js +660 -0
  136. package/src/integrations/TouchDesignerExport.js +552 -0
  137. package/src/integrations/frameworks/Vib3React.js +591 -0
  138. package/src/integrations/frameworks/Vib3Svelte.js +654 -0
  139. package/src/integrations/frameworks/Vib3Vue.js +628 -0
  140. package/src/llm/LLMParameterInterface.js +240 -0
  141. package/src/llm/LLMParameterUI.js +577 -0
  142. package/src/math/Mat4x4.js +708 -0
  143. package/src/math/Projection.js +341 -0
  144. package/src/math/Rotor4D.js +637 -0
  145. package/src/math/Vec4.js +476 -0
  146. package/src/math/constants.js +164 -0
  147. package/src/math/index.js +68 -0
  148. package/src/math/projections.js +54 -0
  149. package/src/math/rotations.js +196 -0
  150. package/src/quantum/QuantumEngine.js +906 -0
  151. package/src/quantum/QuantumVisualizer.js +1103 -0
  152. package/src/reactivity/ReactivityConfig.js +499 -0
  153. package/src/reactivity/ReactivityManager.js +586 -0
  154. package/src/reactivity/SpatialInputSystem.js +1783 -0
  155. package/src/reactivity/index.js +93 -0
  156. package/src/render/CommandBuffer.js +465 -0
  157. package/src/render/MultiCanvasBridge.js +340 -0
  158. package/src/render/RenderCommand.js +514 -0
  159. package/src/render/RenderResourceRegistry.js +523 -0
  160. package/src/render/RenderState.js +552 -0
  161. package/src/render/RenderTarget.js +512 -0
  162. package/src/render/ShaderLoader.js +253 -0
  163. package/src/render/ShaderProgram.js +599 -0
  164. package/src/render/UnifiedRenderBridge.js +496 -0
  165. package/src/render/backends/WebGLBackend.js +1108 -0
  166. package/src/render/backends/WebGPUBackend.js +1409 -0
  167. package/src/render/commands/CommandBufferExecutor.js +607 -0
  168. package/src/render/commands/RenderCommandBuffer.js +661 -0
  169. package/src/render/commands/index.js +17 -0
  170. package/src/render/index.js +367 -0
  171. package/src/scene/Disposable.js +498 -0
  172. package/src/scene/MemoryPool.js +618 -0
  173. package/src/scene/Node4D.js +697 -0
  174. package/src/scene/ResourceManager.js +599 -0
  175. package/src/scene/Scene4D.js +540 -0
  176. package/src/scene/index.js +98 -0
  177. package/src/schemas/error.schema.json +84 -0
  178. package/src/schemas/extension.schema.json +88 -0
  179. package/src/schemas/index.js +214 -0
  180. package/src/schemas/parameters.schema.json +142 -0
  181. package/src/schemas/tool-pack.schema.json +44 -0
  182. package/src/schemas/tool-response.schema.json +127 -0
  183. package/src/shaders/common/fullscreen.vert.glsl +5 -0
  184. package/src/shaders/common/fullscreen.vert.wgsl +17 -0
  185. package/src/shaders/common/geometry24.glsl +65 -0
  186. package/src/shaders/common/geometry24.wgsl +54 -0
  187. package/src/shaders/common/rotation4d.glsl +85 -0
  188. package/src/shaders/common/rotation4d.wgsl +86 -0
  189. package/src/shaders/common/uniforms.glsl +44 -0
  190. package/src/shaders/common/uniforms.wgsl +48 -0
  191. package/src/shaders/faceted/faceted.frag.glsl +129 -0
  192. package/src/shaders/faceted/faceted.frag.wgsl +164 -0
  193. package/src/shaders/holographic/holographic.frag.glsl +406 -0
  194. package/src/shaders/holographic/holographic.frag.wgsl +185 -0
  195. package/src/shaders/quantum/quantum.frag.glsl +513 -0
  196. package/src/shaders/quantum/quantum.frag.wgsl +361 -0
  197. package/src/testing/ParallelTestFramework.js +519 -0
  198. package/src/testing/__snapshots__/exportFormats.test.js.snap +24 -0
  199. package/src/testing/exportFormats.test.js +8 -0
  200. package/src/testing/projections.test.js +14 -0
  201. package/src/testing/rotations.test.js +37 -0
  202. package/src/ui/InteractivityMenu.js +516 -0
  203. package/src/ui/StatusManager.js +96 -0
  204. package/src/ui/adaptive/renderers/webgpu/BufferLayout.ts +252 -0
  205. package/src/ui/adaptive/renderers/webgpu/PolytopeInstanceBuffer.ts +144 -0
  206. package/src/ui/adaptive/renderers/webgpu/TripleBufferedUniform.ts +170 -0
  207. package/src/ui/adaptive/renderers/webgpu/WebGPURenderer.ts +735 -0
  208. package/src/ui/adaptive/renderers/webgpu/index.ts +112 -0
  209. package/src/variations/VariationManager.js +431 -0
  210. package/src/viewer/AudioReactivity.js +505 -0
  211. package/src/viewer/CardBending.js +481 -0
  212. package/src/viewer/GalleryUI.js +832 -0
  213. package/src/viewer/ReactivityManager.js +590 -0
  214. package/src/viewer/TradingCardExporter.js +600 -0
  215. package/src/viewer/ViewerPortal.js +374 -0
  216. package/src/viewer/index.js +12 -0
  217. package/src/wasm/WasmLoader.js +296 -0
  218. package/src/wasm/index.js +132 -0
  219. package/tools/agentic/mcpTools.js +88 -0
  220. package/tools/cli/agent-cli.js +92 -0
  221. package/tools/export/formats.js +24 -0
  222. package/tools/math/rotation-baseline.mjs +64 -0
  223. package/tools/shader-sync-verify.js +937 -0
  224. package/tools/telemetry/manifestPipeline.js +141 -0
  225. package/tools/telemetry/telemetryEvents.js +35 -0
  226. package/types/adaptive-sdk.d.ts +185 -0
  227. package/types/advanced/AIPresetGenerator.d.ts +81 -0
  228. package/types/advanced/MIDIController.d.ts +100 -0
  229. package/types/advanced/OffscreenWorker.d.ts +82 -0
  230. package/types/advanced/WebGPUCompute.d.ts +52 -0
  231. package/types/advanced/WebXRRenderer.d.ts +77 -0
  232. package/types/advanced/index.d.ts +46 -0
  233. package/types/core/ErrorReporter.d.ts +50 -0
  234. package/types/core/VIB3Engine.d.ts +204 -0
  235. package/types/creative/ColorPresetsSystem.d.ts +91 -0
  236. package/types/creative/ParameterTimeline.d.ts +74 -0
  237. package/types/creative/PostProcessingPipeline.d.ts +109 -0
  238. package/types/creative/TransitionAnimator.d.ts +71 -0
  239. package/types/creative/index.d.ts +35 -0
  240. package/types/integrations/FigmaPlugin.d.ts +46 -0
  241. package/types/integrations/OBSMode.d.ts +74 -0
  242. package/types/integrations/ThreeJsPackage.d.ts +62 -0
  243. package/types/integrations/TouchDesignerExport.d.ts +36 -0
  244. package/types/integrations/Vib3React.d.ts +74 -0
  245. package/types/integrations/Vib3Svelte.d.ts +63 -0
  246. package/types/integrations/Vib3Vue.d.ts +55 -0
  247. package/types/integrations/index.d.ts +52 -0
  248. package/types/reactivity/SpatialInputSystem.d.ts +173 -0
  249. package/types/reactivity/index.d.ts +394 -0
  250. package/types/render/CommandBuffer.d.ts +169 -0
  251. package/types/render/RenderCommand.d.ts +312 -0
  252. package/types/render/RenderState.d.ts +279 -0
  253. package/types/render/RenderTarget.d.ts +254 -0
  254. package/types/render/ShaderProgram.d.ts +277 -0
  255. package/types/render/UnifiedRenderBridge.d.ts +143 -0
  256. package/types/render/WebGLBackend.d.ts +168 -0
  257. package/types/render/WebGPUBackend.d.ts +186 -0
  258. package/types/render/index.d.ts +141 -0
@@ -0,0 +1,1051 @@
1
+ /**
2
+ * VIB3+ WebGPU Compute Pipeline
3
+ * GPU-accelerated particle systems and audio FFT processing.
4
+ *
5
+ * Provides two compute pipelines:
6
+ * 1. Particle Physics -- 65536 particles influenced by 4D geometry fields,
7
+ * audio reactivity, and user parameters.
8
+ * 2. Audio FFT -- Transforms time-domain audio samples into frequency-domain
9
+ * bands (bass, mid, high, plus configurable sub-bands).
10
+ *
11
+ * Both pipelines are fully self-contained WGSL compute shaders dispatched
12
+ * through a single WebGPU device.
13
+ *
14
+ * @module advanced/WebGPUCompute
15
+ */
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // WGSL Shader Sources
19
+ // ---------------------------------------------------------------------------
20
+
21
+ /**
22
+ * WGSL compute shader for particle physics simulation.
23
+ *
24
+ * Each particle has: position (vec4), velocity (vec4), life (f32), phase (f32).
25
+ * Stride per particle = 10 floats = 40 bytes.
26
+ *
27
+ * The shader applies 4D rotation fields, gravity toward geometry attractors,
28
+ * audio-driven turbulence, and damping per-frame.
29
+ */
30
+ const PARTICLE_COMPUTE_WGSL = /* wgsl */ `
31
+
32
+ struct Params {
33
+ time: f32,
34
+ deltaTime: f32,
35
+ particleCount: u32,
36
+ _pad0: f32,
37
+
38
+ // 6D rotation angles
39
+ rotXY: f32, rotXZ: f32, rotYZ: f32, _pad1: f32,
40
+ rotXW: f32, rotYW: f32, rotZW: f32, _pad2: f32,
41
+
42
+ // VIB3 parameters
43
+ geometry: f32,
44
+ gridDensity: f32,
45
+ morphFactor: f32,
46
+ chaos: f32,
47
+
48
+ speed: f32,
49
+ hue: f32,
50
+ intensity: f32,
51
+ dimension: f32,
52
+
53
+ // Audio
54
+ bass: f32,
55
+ mid: f32,
56
+ high: f32,
57
+ _pad3: f32,
58
+ };
59
+
60
+ @group(0) @binding(0) var<storage, read_write> particles: array<f32>;
61
+ @group(0) @binding(1) var<uniform> params: Params;
62
+
63
+ // ---- Rotation helpers (4D) ----
64
+
65
+ fn rotateXY(p: vec4<f32>, a: f32) -> vec4<f32> {
66
+ let c = cos(a); let s = sin(a);
67
+ return vec4<f32>(p.x * c - p.y * s, p.x * s + p.y * c, p.z, p.w);
68
+ }
69
+
70
+ fn rotateXZ(p: vec4<f32>, a: f32) -> vec4<f32> {
71
+ let c = cos(a); let s = sin(a);
72
+ return vec4<f32>(p.x * c - p.z * s, p.y, p.x * s + p.z * c, p.w);
73
+ }
74
+
75
+ fn rotateYZ(p: vec4<f32>, a: f32) -> vec4<f32> {
76
+ let c = cos(a); let s = sin(a);
77
+ return vec4<f32>(p.x, p.y * c - p.z * s, p.y * s + p.z * c, p.w);
78
+ }
79
+
80
+ fn rotateXW(p: vec4<f32>, a: f32) -> vec4<f32> {
81
+ let c = cos(a); let s = sin(a);
82
+ return vec4<f32>(p.x * c - p.w * s, p.y, p.z, p.x * s + p.w * c);
83
+ }
84
+
85
+ fn rotateYW(p: vec4<f32>, a: f32) -> vec4<f32> {
86
+ let c = cos(a); let s = sin(a);
87
+ return vec4<f32>(p.x, p.y * c - p.w * s, p.z, p.y * s + p.w * c);
88
+ }
89
+
90
+ fn rotateZW(p: vec4<f32>, a: f32) -> vec4<f32> {
91
+ let c = cos(a); let s = sin(a);
92
+ return vec4<f32>(p.x, p.y, p.z * c - p.w * s, p.z * s + p.w * c);
93
+ }
94
+
95
+ fn rotate4D(p: vec4<f32>) -> vec4<f32> {
96
+ var q = p;
97
+ q = rotateXY(q, params.rotXY);
98
+ q = rotateXZ(q, params.rotXZ);
99
+ q = rotateYZ(q, params.rotYZ);
100
+ q = rotateXW(q, params.rotXW);
101
+ q = rotateYW(q, params.rotYW);
102
+ q = rotateZW(q, params.rotZW);
103
+ return q;
104
+ }
105
+
106
+ // ---- Pseudo-random (hash-based) ----
107
+
108
+ fn hash(n: f32) -> f32 {
109
+ return fract(sin(n) * 43758.5453123);
110
+ }
111
+
112
+ fn hash3(p: vec3<f32>) -> vec3<f32> {
113
+ let q = vec3<f32>(
114
+ dot(p, vec3<f32>(127.1, 311.7, 74.7)),
115
+ dot(p, vec3<f32>(269.5, 183.3, 246.1)),
116
+ dot(p, vec3<f32>(113.5, 271.9, 124.6))
117
+ );
118
+ return fract(sin(q) * 43758.5453123);
119
+ }
120
+
121
+ // ---- Geometry attractor field ----
122
+
123
+ fn geometryAttractor(pos: vec4<f32>, geom: f32, t: f32) -> vec4<f32> {
124
+ let gi = u32(geom) % 8u;
125
+ var target = vec4<f32>(0.0);
126
+
127
+ switch gi {
128
+ case 0u: { // Tetrahedron vertices
129
+ let phase = t * 0.5;
130
+ target = vec4<f32>(
131
+ sin(phase + pos.x * 3.14159),
132
+ cos(phase + pos.y * 3.14159),
133
+ sin(phase * 0.7 + pos.z * 3.14159),
134
+ cos(phase * 0.3)
135
+ ) * 0.5;
136
+ }
137
+ case 1u: { // Hypercube lattice
138
+ target = vec4<f32>(
139
+ round(pos.x * 2.0) * 0.5,
140
+ round(pos.y * 2.0) * 0.5,
141
+ round(pos.z * 2.0) * 0.5,
142
+ round(pos.w * 2.0) * 0.5
143
+ );
144
+ }
145
+ case 2u: { // Sphere surface
146
+ let len = max(length(pos), 0.001);
147
+ target = pos / len * 0.6;
148
+ }
149
+ case 3u: { // Torus
150
+ let R = 0.5; let r_minor = 0.2;
151
+ let angle1 = atan2(pos.y, pos.x);
152
+ let ringCenter = vec2<f32>(cos(angle1), sin(angle1)) * R;
153
+ let toCenter = vec2<f32>(pos.x - ringCenter.x, pos.y - ringCenter.y);
154
+ let angle2 = atan2(pos.z, length(toCenter));
155
+ target = vec4<f32>(
156
+ (R + r_minor * cos(angle2)) * cos(angle1),
157
+ (R + r_minor * cos(angle2)) * sin(angle1),
158
+ r_minor * sin(angle2),
159
+ pos.w * 0.5
160
+ );
161
+ }
162
+ case 4u: { // Klein bottle (figure-8 immersion)
163
+ let u_angle = atan2(pos.y, pos.x);
164
+ let v_angle = atan2(pos.w, pos.z);
165
+ target = vec4<f32>(
166
+ (2.0 + cos(v_angle)) * cos(u_angle),
167
+ (2.0 + cos(v_angle)) * sin(u_angle),
168
+ sin(v_angle) * cos(u_angle * 0.5),
169
+ sin(v_angle) * sin(u_angle * 0.5)
170
+ ) * 0.25;
171
+ }
172
+ case 5u: { // Fractal (Menger-like attractor)
173
+ var fp = pos;
174
+ for (var i = 0u; i < 3u; i++) {
175
+ fp = abs(fp) * 2.0 - vec4<f32>(1.0);
176
+ }
177
+ target = fp * 0.15;
178
+ }
179
+ case 6u: { // Wave
180
+ target = vec4<f32>(
181
+ pos.x,
182
+ sin(pos.x * 6.28 + t) * 0.3,
183
+ pos.z,
184
+ cos(pos.z * 6.28 + t * 0.7) * 0.3
185
+ );
186
+ }
187
+ case 7u: { // Crystal (octahedral)
188
+ let absSum = abs(pos.x) + abs(pos.y) + abs(pos.z) + abs(pos.w);
189
+ let scale = select(0.6 / absSum, 1.0, absSum < 0.001);
190
+ target = pos * scale;
191
+ }
192
+ default: {
193
+ target = vec4<f32>(0.0);
194
+ }
195
+ }
196
+
197
+ return target;
198
+ }
199
+
200
+ // ---- Main compute ----
201
+
202
+ @compute @workgroup_size(256)
203
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
204
+ let idx = gid.x;
205
+ if (idx >= params.particleCount) { return; }
206
+
207
+ let stride = 10u; // floats per particle
208
+ let base = idx * stride;
209
+
210
+ // Read particle state
211
+ var pos = vec4<f32>(particles[base + 0u], particles[base + 1u],
212
+ particles[base + 2u], particles[base + 3u]);
213
+ var vel = vec4<f32>(particles[base + 4u], particles[base + 5u],
214
+ particles[base + 6u], particles[base + 7u]);
215
+ var life = particles[base + 8u];
216
+ var phase = particles[base + 9u];
217
+
218
+ let dt = params.deltaTime * params.speed;
219
+ let t = params.time;
220
+ let fi = f32(idx);
221
+
222
+ // ---- Respawn dead particles ----
223
+ if (life <= 0.0) {
224
+ let seed = hash3(vec3<f32>(fi, t, fi * 0.37));
225
+ pos = vec4<f32>(
226
+ (seed.x - 0.5) * 2.0,
227
+ (seed.y - 0.5) * 2.0,
228
+ (seed.z - 0.5) * 2.0,
229
+ (hash(fi + t) - 0.5) * 2.0
230
+ );
231
+ vel = vec4<f32>(0.0);
232
+ life = 2.0 + hash(fi * 7.13 + t) * 4.0;
233
+ phase = hash(fi * 3.17 + t) * 6.28318;
234
+ }
235
+
236
+ // ---- 4D rotation field ----
237
+ let rotatedTarget = rotate4D(pos);
238
+ let rotForce = (rotatedTarget - pos) * 0.5;
239
+
240
+ // ---- Geometry attractor ----
241
+ let attractor = geometryAttractor(pos, params.geometry, t);
242
+ let attractForce = (attractor - pos) * (0.3 + params.morphFactor * 0.5);
243
+
244
+ // ---- Audio turbulence ----
245
+ let audioForce = vec4<f32>(
246
+ sin(phase + t * 3.0) * params.bass * 0.8,
247
+ cos(phase + t * 2.5) * params.mid * 0.6,
248
+ sin(phase * 1.3 + t * 1.8) * params.high * 0.7,
249
+ cos(phase * 0.7 + t) * (params.bass + params.mid) * 0.3
250
+ );
251
+
252
+ // ---- Chaos / noise ----
253
+ let chaosNoise = vec4<f32>(
254
+ hash(fi + t * 100.0) - 0.5,
255
+ hash(fi * 2.0 + t * 100.0) - 0.5,
256
+ hash(fi * 3.0 + t * 100.0) - 0.5,
257
+ hash(fi * 4.0 + t * 100.0) - 0.5
258
+ ) * params.chaos * 2.0;
259
+
260
+ // ---- Grid density influence (repulsion from neighbors approximation) ----
261
+ let densityRepel = -pos * (params.gridDensity * 0.001);
262
+
263
+ // ---- Integrate ----
264
+ let totalForce = rotForce + attractForce + audioForce + chaosNoise + densityRepel;
265
+ vel = vel + totalForce * dt;
266
+
267
+ // Damping
268
+ let damping = 0.96 - params.chaos * 0.1;
269
+ vel = vel * damping;
270
+
271
+ // Clamp velocity
272
+ let maxSpeed = 2.0 + params.speed;
273
+ let speed = length(vel);
274
+ if (speed > maxSpeed) {
275
+ vel = vel * (maxSpeed / speed);
276
+ }
277
+
278
+ pos = pos + vel * dt;
279
+
280
+ // Boundary wrap
281
+ pos = (fract((pos + vec4<f32>(2.0)) / 4.0) - vec4<f32>(0.5)) * 4.0;
282
+
283
+ // Life decay
284
+ life = life - dt;
285
+ phase = phase + dt * 1.5;
286
+
287
+ // ---- Write back ----
288
+ particles[base + 0u] = pos.x;
289
+ particles[base + 1u] = pos.y;
290
+ particles[base + 2u] = pos.z;
291
+ particles[base + 3u] = pos.w;
292
+ particles[base + 4u] = vel.x;
293
+ particles[base + 5u] = vel.y;
294
+ particles[base + 6u] = vel.z;
295
+ particles[base + 7u] = vel.w;
296
+ particles[base + 8u] = life;
297
+ particles[base + 9u] = phase;
298
+ }
299
+ `;
300
+
301
+ /**
302
+ * WGSL compute shader for audio FFT processing.
303
+ *
304
+ * Performs a radix-2 Cooley-Tukey FFT on 1024 samples,
305
+ * then bins the magnitudes into frequency bands.
306
+ */
307
+ const FFT_COMPUTE_WGSL = /* wgsl */ `
308
+
309
+ // -- Bindings --
310
+ // binding 0: input time-domain audio (1024 f32 samples, read)
311
+ // binding 1: working buffer for FFT (2048 f32: 1024 real + 1024 imag, read_write)
312
+ // binding 2: output frequency bands (32 f32, read_write)
313
+ // binding 3: FFT params uniform
314
+
315
+ struct FFTParams {
316
+ sampleCount: u32,
317
+ bandCount: u32,
318
+ sampleRate: f32,
319
+ _pad: f32,
320
+ };
321
+
322
+ @group(0) @binding(0) var<storage, read> audioInput: array<f32>;
323
+ @group(0) @binding(1) var<storage, read_write> fftWork: array<f32>;
324
+ @group(0) @binding(2) var<storage, read_write> bands: array<f32>;
325
+ @group(0) @binding(3) var<uniform> fftParams: FFTParams;
326
+
327
+ // ---- Bit-reversal permutation ----
328
+ fn bitReverse(x: u32, bits: u32) -> u32 {
329
+ var v = x;
330
+ var r = 0u;
331
+ for (var i = 0u; i < bits; i++) {
332
+ r = (r << 1u) | (v & 1u);
333
+ v = v >> 1u;
334
+ }
335
+ return r;
336
+ }
337
+
338
+ // ---- Pass 1: Bit-reverse copy + Hann window ----
339
+ @compute @workgroup_size(256)
340
+ fn fftBitReverse(@builtin(global_invocation_id) gid: vec3<u32>) {
341
+ let idx = gid.x;
342
+ let N = fftParams.sampleCount;
343
+ if (idx >= N) { return; }
344
+
345
+ let bits = u32(log2(f32(N)));
346
+ let target = bitReverse(idx, bits);
347
+
348
+ // Apply Hann window
349
+ let n = f32(idx);
350
+ let Nf = f32(N);
351
+ let window = 0.5 * (1.0 - cos(6.28318530718 * n / (Nf - 1.0)));
352
+ let sample = audioInput[idx] * window;
353
+
354
+ // Bit-reversed copy: real in [0..N), imag in [N..2N)
355
+ fftWork[target] = sample;
356
+ fftWork[target + N] = 0.0;
357
+ }
358
+
359
+ // ---- Pass 2..log2(N): Butterfly operations ----
360
+ // We dispatch this multiple times from JS, once per FFT stage,
361
+ // passing the stage via push constant emulation in the work buffer layout.
362
+
363
+ // For simplicity and correctness, we encode a full iterative FFT in a
364
+ // single dispatch with a serial loop per invocation over its assigned
365
+ // butterflies across all stages. Each invocation handles one butterfly
366
+ // within the current stage dispatched from JS.
367
+
368
+ // In practice we use a staged approach: JS dispatches log2(N) times.
369
+ // Each dispatch operates on one stage. The stage index is encoded at
370
+ // fftWork[2*N] (a reserved slot written by JS before each dispatch).
371
+
372
+ @compute @workgroup_size(256)
373
+ fn fftButterfly(@builtin(global_invocation_id) gid: vec3<u32>) {
374
+ let N = fftParams.sampleCount;
375
+ let idx = gid.x;
376
+
377
+ // Stage encoded at the reserved slot
378
+ let stage = u32(fftWork[2u * N]);
379
+ let halfLen = 1u << stage; // half the sub-DFT length
380
+ let fullLen = halfLen << 1u; // full sub-DFT length
381
+ let numButterflies = N / 2u;
382
+
383
+ if (idx >= numButterflies) { return; }
384
+
385
+ // Determine which butterfly within which sub-DFT
386
+ let subDFT = idx / halfLen;
387
+ let j = idx % halfLen;
388
+ let base = subDFT * fullLen;
389
+
390
+ let evenIdx = base + j;
391
+ let oddIdx = base + j + halfLen;
392
+
393
+ // Twiddle factor: W_N^(j * N/fullLen)
394
+ let angle = -6.28318530718 * f32(j) / f32(fullLen);
395
+ let tw_re = cos(angle);
396
+ let tw_im = sin(angle);
397
+
398
+ // Read even and odd
399
+ let e_re = fftWork[evenIdx];
400
+ let e_im = fftWork[evenIdx + N];
401
+ let o_re = fftWork[oddIdx];
402
+ let o_im = fftWork[oddIdx + N];
403
+
404
+ // Twiddle multiply
405
+ let t_re = o_re * tw_re - o_im * tw_im;
406
+ let t_im = o_re * tw_im + o_im * tw_re;
407
+
408
+ // Butterfly
409
+ fftWork[evenIdx] = e_re + t_re;
410
+ fftWork[evenIdx + N] = e_im + t_im;
411
+ fftWork[oddIdx] = e_re - t_re;
412
+ fftWork[oddIdx + N] = e_im - t_im;
413
+ }
414
+
415
+ // ---- Pass final: Compute magnitude bands ----
416
+ @compute @workgroup_size(32)
417
+ fn fftBands(@builtin(global_invocation_id) gid: vec3<u32>) {
418
+ let bandIdx = gid.x;
419
+ let bandCount = fftParams.bandCount;
420
+ if (bandIdx >= bandCount) { return; }
421
+
422
+ let N = fftParams.sampleCount;
423
+ let halfN = N / 2u; // Nyquist
424
+
425
+ // Map bands logarithmically across spectrum
426
+ let lo = f32(bandIdx) / f32(bandCount);
427
+ let hi = f32(bandIdx + 1u) / f32(bandCount);
428
+
429
+ // Logarithmic frequency mapping
430
+ let freqLo = u32(pow(f32(halfN), lo));
431
+ let freqHi = max(u32(pow(f32(halfN), hi)), freqLo + 1u);
432
+
433
+ var mag = 0.0;
434
+ var count = 0.0;
435
+ for (var k = freqLo; k < freqHi && k < halfN; k++) {
436
+ let re = fftWork[k];
437
+ let im = fftWork[k + N];
438
+ mag += sqrt(re * re + im * im);
439
+ count += 1.0;
440
+ }
441
+
442
+ if (count > 0.0) {
443
+ mag = mag / count;
444
+ }
445
+
446
+ // Normalize (approximate, assumes input in [-1, 1])
447
+ mag = mag / f32(N) * 2.0;
448
+
449
+ bands[bandIdx] = mag;
450
+ }
451
+ `;
452
+
453
+ // ---------------------------------------------------------------------------
454
+ // Exports
455
+ // ---------------------------------------------------------------------------
456
+
457
+ export class WebGPUComputePipeline {
458
+ constructor() {
459
+ /** @type {GPUDevice|null} */
460
+ this.device = null;
461
+
462
+ /** @type {GPUAdapter|null} */
463
+ this.adapter = null;
464
+
465
+ // -- Particle resources --
466
+ /** @type {GPUComputePipeline|null} */
467
+ this.particlePipeline = null;
468
+
469
+ /** @type {GPUBuffer|null} */
470
+ this.particleBuffer = null;
471
+
472
+ /** @type {GPUBuffer|null} */
473
+ this.particleParamsBuffer = null;
474
+
475
+ /** @type {GPUBuffer|null} */
476
+ this.particleReadBuffer = null;
477
+
478
+ /** @type {GPUBindGroup|null} */
479
+ this.particleBindGroup = null;
480
+
481
+ /** @type {number} */
482
+ this.particleCount = 65536;
483
+
484
+ /** @type {number} Floats per particle (pos4 + vel4 + life + phase) */
485
+ this.particleStride = 10;
486
+
487
+ // -- FFT resources --
488
+ /** @type {GPUComputePipeline|null} FFT bit-reverse pipeline */
489
+ this.fftBitReversePipeline = null;
490
+
491
+ /** @type {GPUComputePipeline|null} FFT butterfly pipeline */
492
+ this.fftButterflyPipeline = null;
493
+
494
+ /** @type {GPUComputePipeline|null} FFT band computation pipeline */
495
+ this.fftBandsPipeline = null;
496
+
497
+ /** @type {GPUBuffer|null} */
498
+ this.fftInputBuffer = null;
499
+
500
+ /** @type {GPUBuffer|null} */
501
+ this.fftWorkBuffer = null;
502
+
503
+ /** @type {GPUBuffer|null} */
504
+ this.fftBandsBuffer = null;
505
+
506
+ /** @type {GPUBuffer|null} */
507
+ this.fftParamsBuffer = null;
508
+
509
+ /** @type {GPUBuffer|null} */
510
+ this.fftReadBuffer = null;
511
+
512
+ /** @type {GPUBindGroup|null} */
513
+ this.fftBindGroup = null;
514
+
515
+ /** @type {number} */
516
+ this.fftSize = 1024;
517
+
518
+ /** @type {number} */
519
+ this.bandCount = 32;
520
+
521
+ /** @type {boolean} */
522
+ this._initialized = false;
523
+ }
524
+
525
+ // -----------------------------------------------------------------------
526
+ // Initialization
527
+ // -----------------------------------------------------------------------
528
+
529
+ /**
530
+ * Initialize WebGPU adapter, device, and both compute pipelines.
531
+ * @returns {Promise<void>}
532
+ * @throws {Error} If WebGPU is unavailable
533
+ */
534
+ async initialize() {
535
+ if (this._initialized) return;
536
+
537
+ if (!navigator.gpu) {
538
+ throw new Error('WebGPU is not supported in this browser.');
539
+ }
540
+
541
+ this.adapter = await navigator.gpu.requestAdapter({
542
+ powerPreference: 'high-performance'
543
+ });
544
+
545
+ if (!this.adapter) {
546
+ throw new Error('Failed to obtain WebGPU adapter.');
547
+ }
548
+
549
+ this.device = await this.adapter.requestDevice({
550
+ requiredLimits: {
551
+ maxStorageBufferBindingSize: this.adapter.limits.maxStorageBufferBindingSize,
552
+ maxComputeWorkgroupsPerDimension: this.adapter.limits.maxComputeWorkgroupsPerDimension
553
+ }
554
+ });
555
+
556
+ this.device.lost.then((info) => {
557
+ console.error('WebGPU device lost:', info.message);
558
+ this._initialized = false;
559
+ });
560
+
561
+ this._createParticlePipeline();
562
+ this._createFFTPipeline();
563
+
564
+ this._initialized = true;
565
+ }
566
+
567
+ // -----------------------------------------------------------------------
568
+ // Particle System
569
+ // -----------------------------------------------------------------------
570
+
571
+ /**
572
+ * Return the WGSL source for the particle compute shader.
573
+ * @returns {string}
574
+ */
575
+ createParticleComputeShader() {
576
+ return PARTICLE_COMPUTE_WGSL;
577
+ }
578
+
579
+ /**
580
+ * Build particle compute pipeline and allocate GPU buffers.
581
+ * @private
582
+ */
583
+ _createParticlePipeline() {
584
+ const device = this.device;
585
+ const totalFloats = this.particleCount * this.particleStride;
586
+ const bufferSize = totalFloats * 4; // bytes
587
+
588
+ // Particle storage buffer
589
+ this.particleBuffer = device.createBuffer({
590
+ size: bufferSize,
591
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
592
+ mappedAtCreation: true
593
+ });
594
+
595
+ // Initialize particles with random positions
596
+ {
597
+ const data = new Float32Array(this.particleBuffer.getMappedRange());
598
+ for (let i = 0; i < this.particleCount; i++) {
599
+ const base = i * this.particleStride;
600
+ // Position
601
+ data[base + 0] = (Math.random() - 0.5) * 2.0;
602
+ data[base + 1] = (Math.random() - 0.5) * 2.0;
603
+ data[base + 2] = (Math.random() - 0.5) * 2.0;
604
+ data[base + 3] = (Math.random() - 0.5) * 2.0;
605
+ // Velocity
606
+ data[base + 4] = 0;
607
+ data[base + 5] = 0;
608
+ data[base + 6] = 0;
609
+ data[base + 7] = 0;
610
+ // Life
611
+ data[base + 8] = 2.0 + Math.random() * 4.0;
612
+ // Phase
613
+ data[base + 9] = Math.random() * Math.PI * 2.0;
614
+ }
615
+ this.particleBuffer.unmap();
616
+ }
617
+
618
+ // Params uniform buffer (must be 16-byte aligned, total struct = 96 bytes)
619
+ const paramsSize = 128; // Rounded up for alignment
620
+ this.particleParamsBuffer = device.createBuffer({
621
+ size: paramsSize,
622
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
623
+ });
624
+
625
+ // Readback staging buffer
626
+ this.particleReadBuffer = device.createBuffer({
627
+ size: bufferSize,
628
+ usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
629
+ });
630
+
631
+ // Shader module
632
+ const shaderModule = device.createShaderModule({
633
+ label: 'VIB3 Particle Compute',
634
+ code: PARTICLE_COMPUTE_WGSL
635
+ });
636
+
637
+ // Bind group layout
638
+ const bindGroupLayout = device.createBindGroupLayout({
639
+ entries: [
640
+ {
641
+ binding: 0,
642
+ visibility: GPUShaderStage.COMPUTE,
643
+ buffer: { type: 'storage' }
644
+ },
645
+ {
646
+ binding: 1,
647
+ visibility: GPUShaderStage.COMPUTE,
648
+ buffer: { type: 'uniform' }
649
+ }
650
+ ]
651
+ });
652
+
653
+ // Pipeline
654
+ this.particlePipeline = device.createComputePipeline({
655
+ label: 'VIB3 Particle Pipeline',
656
+ layout: device.createPipelineLayout({
657
+ bindGroupLayouts: [bindGroupLayout]
658
+ }),
659
+ compute: {
660
+ module: shaderModule,
661
+ entryPoint: 'main'
662
+ }
663
+ });
664
+
665
+ // Bind group
666
+ this.particleBindGroup = device.createBindGroup({
667
+ layout: bindGroupLayout,
668
+ entries: [
669
+ { binding: 0, resource: { buffer: this.particleBuffer } },
670
+ { binding: 1, resource: { buffer: this.particleParamsBuffer } }
671
+ ]
672
+ });
673
+ }
674
+
675
+ /**
676
+ * Dispatch the particle compute shader to update all particle positions.
677
+ *
678
+ * @param {Object} params - VIB3 parameter state
679
+ * @param {number} params.time - Current time in seconds
680
+ * @param {number} params.deltaTime - Frame delta in seconds
681
+ * @param {number} [params.rotXY=0] - XY rotation (radians)
682
+ * @param {number} [params.rotXZ=0] - XZ rotation
683
+ * @param {number} [params.rotYZ=0] - YZ rotation
684
+ * @param {number} [params.rotXW=0] - XW rotation
685
+ * @param {number} [params.rotYW=0] - YW rotation
686
+ * @param {number} [params.rotZW=0] - ZW rotation
687
+ * @param {number} [params.geometry=0] - Geometry index (0-23)
688
+ * @param {number} [params.gridDensity=20] - Grid density
689
+ * @param {number} [params.morphFactor=0] - Morph factor
690
+ * @param {number} [params.chaos=0] - Chaos amount
691
+ * @param {number} [params.speed=1] - Animation speed
692
+ * @param {number} [params.hue=180] - Color hue
693
+ * @param {number} [params.intensity=0.5] - Intensity
694
+ * @param {number} [params.dimension=4.0] - Projection dimension
695
+ * @param {number} [params.bass=0] - Audio bass level
696
+ * @param {number} [params.mid=0] - Audio mid level
697
+ * @param {number} [params.high=0] - Audio high level
698
+ */
699
+ updateParticles(params) {
700
+ if (!this._initialized || !this.device) return;
701
+
702
+ // Pack params into Float32Array matching the Params struct layout
703
+ const data = new Float32Array(32); // 128 bytes / 4
704
+ data[0] = params.time || 0;
705
+ data[1] = params.deltaTime || 0.016;
706
+ // u32 particleCount at byte offset 8 -- use DataView
707
+ const dv = new DataView(data.buffer);
708
+ dv.setUint32(8, this.particleCount, true);
709
+ data[3] = 0; // _pad0
710
+
711
+ data[4] = params.rotXY || 0;
712
+ data[5] = params.rotXZ || 0;
713
+ data[6] = params.rotYZ || 0;
714
+ data[7] = 0; // _pad1
715
+
716
+ data[8] = params.rotXW || 0;
717
+ data[9] = params.rotYW || 0;
718
+ data[10] = params.rotZW || 0;
719
+ data[11] = 0; // _pad2
720
+
721
+ data[12] = params.geometry || 0;
722
+ data[13] = params.gridDensity || 20;
723
+ data[14] = params.morphFactor || 0;
724
+ data[15] = params.chaos || 0;
725
+
726
+ data[16] = params.speed || 1;
727
+ data[17] = params.hue || 180;
728
+ data[18] = params.intensity || 0.5;
729
+ data[19] = params.dimension || 4.0;
730
+
731
+ data[20] = params.bass || 0;
732
+ data[21] = params.mid || 0;
733
+ data[22] = params.high || 0;
734
+ data[23] = 0; // _pad3
735
+
736
+ this.device.queue.writeBuffer(this.particleParamsBuffer, 0, data);
737
+
738
+ // Dispatch
739
+ const encoder = this.device.createCommandEncoder();
740
+ const pass = encoder.beginComputePass();
741
+ pass.setPipeline(this.particlePipeline);
742
+ pass.setBindGroup(0, this.particleBindGroup);
743
+
744
+ const workgroupSize = 256;
745
+ const workgroupCount = Math.ceil(this.particleCount / workgroupSize);
746
+ pass.dispatchWorkgroups(workgroupCount);
747
+ pass.end();
748
+
749
+ this.device.queue.submit([encoder.finish()]);
750
+ }
751
+
752
+ /**
753
+ * Read back particle data from the GPU.
754
+ * Returns a Float32Array of all particle data (position, velocity, life, phase).
755
+ *
756
+ * @returns {Promise<Float32Array>} Particle data array
757
+ */
758
+ async getParticleData() {
759
+ if (!this._initialized || !this.device) {
760
+ return new Float32Array(0);
761
+ }
762
+
763
+ const totalBytes = this.particleCount * this.particleStride * 4;
764
+
765
+ const encoder = this.device.createCommandEncoder();
766
+ encoder.copyBufferToBuffer(
767
+ this.particleBuffer, 0,
768
+ this.particleReadBuffer, 0,
769
+ totalBytes
770
+ );
771
+ this.device.queue.submit([encoder.finish()]);
772
+
773
+ await this.particleReadBuffer.mapAsync(GPUMapMode.READ);
774
+ const data = new Float32Array(this.particleReadBuffer.getMappedRange().slice(0));
775
+ this.particleReadBuffer.unmap();
776
+
777
+ return data;
778
+ }
779
+
780
+ /**
781
+ * Get the GPU particle buffer directly for use in a render pipeline
782
+ * (zero-copy path).
783
+ * @returns {GPUBuffer|null}
784
+ */
785
+ getParticleBuffer() {
786
+ return this.particleBuffer;
787
+ }
788
+
789
+ // -----------------------------------------------------------------------
790
+ // Audio FFT
791
+ // -----------------------------------------------------------------------
792
+
793
+ /**
794
+ * Return the WGSL source for the FFT compute shader.
795
+ * @returns {string}
796
+ */
797
+ createFFTComputeShader() {
798
+ return FFT_COMPUTE_WGSL;
799
+ }
800
+
801
+ /**
802
+ * Build FFT compute pipelines and allocate GPU buffers.
803
+ * @private
804
+ */
805
+ _createFFTPipeline() {
806
+ const device = this.device;
807
+ const N = this.fftSize;
808
+
809
+ // Input buffer (time-domain samples)
810
+ this.fftInputBuffer = device.createBuffer({
811
+ size: N * 4,
812
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
813
+ });
814
+
815
+ // Work buffer: N real + N imag + 1 stage index = (2N + 1) floats
816
+ const workSize = (2 * N + 4) * 4; // +4 for alignment
817
+ this.fftWorkBuffer = device.createBuffer({
818
+ size: workSize,
819
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
820
+ });
821
+
822
+ // Output bands buffer
823
+ this.fftBandsBuffer = device.createBuffer({
824
+ size: this.bandCount * 4,
825
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
826
+ });
827
+
828
+ // Read buffer for bands
829
+ this.fftReadBuffer = device.createBuffer({
830
+ size: this.bandCount * 4,
831
+ usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
832
+ });
833
+
834
+ // Params uniform
835
+ this.fftParamsBuffer = device.createBuffer({
836
+ size: 16, // FFTParams struct
837
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
838
+ });
839
+
840
+ // Write FFT params
841
+ const fftParamsData = new ArrayBuffer(16);
842
+ const fftParamsDV = new DataView(fftParamsData);
843
+ fftParamsDV.setUint32(0, N, true); // sampleCount
844
+ fftParamsDV.setUint32(4, this.bandCount, true); // bandCount
845
+ fftParamsDV.setFloat32(8, 44100.0, true); // sampleRate
846
+ fftParamsDV.setFloat32(12, 0.0, true); // _pad
847
+ device.queue.writeBuffer(this.fftParamsBuffer, 0, fftParamsData);
848
+
849
+ // Shader module
850
+ const shaderModule = device.createShaderModule({
851
+ label: 'VIB3 FFT Compute',
852
+ code: FFT_COMPUTE_WGSL
853
+ });
854
+
855
+ // Bind group layout (shared for all 3 entry points)
856
+ const bindGroupLayout = device.createBindGroupLayout({
857
+ entries: [
858
+ { binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } },
859
+ { binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } },
860
+ { binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } },
861
+ { binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'uniform' } }
862
+ ]
863
+ });
864
+
865
+ const pipelineLayout = device.createPipelineLayout({
866
+ bindGroupLayouts: [bindGroupLayout]
867
+ });
868
+
869
+ // Three pipelines for three entry points
870
+ this.fftBitReversePipeline = device.createComputePipeline({
871
+ label: 'VIB3 FFT BitReverse',
872
+ layout: pipelineLayout,
873
+ compute: { module: shaderModule, entryPoint: 'fftBitReverse' }
874
+ });
875
+
876
+ this.fftButterflyPipeline = device.createComputePipeline({
877
+ label: 'VIB3 FFT Butterfly',
878
+ layout: pipelineLayout,
879
+ compute: { module: shaderModule, entryPoint: 'fftButterfly' }
880
+ });
881
+
882
+ this.fftBandsPipeline = device.createComputePipeline({
883
+ label: 'VIB3 FFT Bands',
884
+ layout: pipelineLayout,
885
+ compute: { module: shaderModule, entryPoint: 'fftBands' }
886
+ });
887
+
888
+ // Bind group
889
+ this.fftBindGroup = device.createBindGroup({
890
+ layout: bindGroupLayout,
891
+ entries: [
892
+ { binding: 0, resource: { buffer: this.fftInputBuffer } },
893
+ { binding: 1, resource: { buffer: this.fftWorkBuffer } },
894
+ { binding: 2, resource: { buffer: this.fftBandsBuffer } },
895
+ { binding: 3, resource: { buffer: this.fftParamsBuffer } }
896
+ ]
897
+ });
898
+ }
899
+
900
+ /**
901
+ * Process time-domain audio data through the GPU FFT pipeline.
902
+ *
903
+ * @param {Float32Array} audioData - Time-domain audio samples (length should equal fftSize)
904
+ * @returns {Promise<{bands: Float32Array, bass: number, mid: number, high: number}>}
905
+ */
906
+ async processAudioFFT(audioData) {
907
+ if (!this._initialized || !this.device) {
908
+ return { bands: new Float32Array(this.bandCount), bass: 0, mid: 0, high: 0 };
909
+ }
910
+
911
+ const N = this.fftSize;
912
+
913
+ // Pad or truncate input to fftSize
914
+ let input = audioData;
915
+ if (audioData.length !== N) {
916
+ input = new Float32Array(N);
917
+ input.set(audioData.subarray(0, Math.min(audioData.length, N)));
918
+ }
919
+
920
+ // Upload audio data
921
+ this.device.queue.writeBuffer(this.fftInputBuffer, 0, input);
922
+
923
+ const encoder = this.device.createCommandEncoder();
924
+
925
+ // Pass 1: Bit-reverse copy with windowing
926
+ {
927
+ const pass = encoder.beginComputePass();
928
+ pass.setPipeline(this.fftBitReversePipeline);
929
+ pass.setBindGroup(0, this.fftBindGroup);
930
+ pass.dispatchWorkgroups(Math.ceil(N / 256));
931
+ pass.end();
932
+ }
933
+
934
+ // Pass 2..log2(N): Butterfly stages
935
+ const numStages = Math.log2(N);
936
+ for (let stage = 0; stage < numStages; stage++) {
937
+ // Write stage index into the reserved slot in fftWorkBuffer
938
+ const stageData = new Float32Array([stage]);
939
+ this.device.queue.writeBuffer(this.fftWorkBuffer, 2 * N * 4, stageData);
940
+
941
+ // Submit the pending commands so the stage index write is visible
942
+ this.device.queue.submit([encoder.finish()]);
943
+
944
+ const stageEncoder = this.device.createCommandEncoder();
945
+ const pass = stageEncoder.beginComputePass();
946
+ pass.setPipeline(this.fftButterflyPipeline);
947
+ pass.setBindGroup(0, this.fftBindGroup);
948
+ pass.dispatchWorkgroups(Math.ceil(N / 2 / 256));
949
+ pass.end();
950
+
951
+ // Use stageEncoder for next iteration or final steps
952
+ if (stage < numStages - 1) {
953
+ this.device.queue.submit([stageEncoder.finish()]);
954
+ } else {
955
+ // Last stage -- continue to band computation
956
+ const bandsPass = stageEncoder.beginComputePass();
957
+ bandsPass.setPipeline(this.fftBandsPipeline);
958
+ bandsPass.setBindGroup(0, this.fftBindGroup);
959
+ bandsPass.dispatchWorkgroups(Math.ceil(this.bandCount / 32));
960
+ bandsPass.end();
961
+
962
+ // Copy bands to readback buffer
963
+ stageEncoder.copyBufferToBuffer(
964
+ this.fftBandsBuffer, 0,
965
+ this.fftReadBuffer, 0,
966
+ this.bandCount * 4
967
+ );
968
+
969
+ this.device.queue.submit([stageEncoder.finish()]);
970
+ }
971
+ }
972
+
973
+ // Read back bands
974
+ await this.fftReadBuffer.mapAsync(GPUMapMode.READ);
975
+ const bandsData = new Float32Array(this.fftReadBuffer.getMappedRange().slice(0));
976
+ this.fftReadBuffer.unmap();
977
+
978
+ // Derive bass / mid / high from bands
979
+ // bass: bands 0-7, mid: bands 8-19, high: bands 20-31
980
+ const bassEnd = Math.floor(this.bandCount * 0.25);
981
+ const midEnd = Math.floor(this.bandCount * 0.625);
982
+
983
+ let bass = 0, mid = 0, high = 0;
984
+ for (let i = 0; i < this.bandCount; i++) {
985
+ if (i < bassEnd) {
986
+ bass += bandsData[i];
987
+ } else if (i < midEnd) {
988
+ mid += bandsData[i];
989
+ } else {
990
+ high += bandsData[i];
991
+ }
992
+ }
993
+
994
+ bass /= bassEnd || 1;
995
+ mid /= (midEnd - bassEnd) || 1;
996
+ high /= (this.bandCount - midEnd) || 1;
997
+
998
+ return { bands: bandsData, bass, mid, high };
999
+ }
1000
+
1001
+ // -----------------------------------------------------------------------
1002
+ // Cleanup
1003
+ // -----------------------------------------------------------------------
1004
+
1005
+ /**
1006
+ * Release all GPU resources.
1007
+ */
1008
+ dispose() {
1009
+ const buffers = [
1010
+ this.particleBuffer,
1011
+ this.particleParamsBuffer,
1012
+ this.particleReadBuffer,
1013
+ this.fftInputBuffer,
1014
+ this.fftWorkBuffer,
1015
+ this.fftBandsBuffer,
1016
+ this.fftParamsBuffer,
1017
+ this.fftReadBuffer
1018
+ ];
1019
+
1020
+ for (const buf of buffers) {
1021
+ if (buf) {
1022
+ try {
1023
+ buf.destroy();
1024
+ } catch (_e) {
1025
+ // Buffer may already be destroyed
1026
+ }
1027
+ }
1028
+ }
1029
+
1030
+ this.particleBuffer = null;
1031
+ this.particleParamsBuffer = null;
1032
+ this.particleReadBuffer = null;
1033
+ this.fftInputBuffer = null;
1034
+ this.fftWorkBuffer = null;
1035
+ this.fftBandsBuffer = null;
1036
+ this.fftParamsBuffer = null;
1037
+ this.fftReadBuffer = null;
1038
+
1039
+ this.particlePipeline = null;
1040
+ this.fftBitReversePipeline = null;
1041
+ this.fftButterflyPipeline = null;
1042
+ this.fftBandsPipeline = null;
1043
+
1044
+ this.particleBindGroup = null;
1045
+ this.fftBindGroup = null;
1046
+
1047
+ this.device = null;
1048
+ this.adapter = null;
1049
+ this._initialized = false;
1050
+ }
1051
+ }