@holoscript/engine 6.0.3 → 6.0.4

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 (192) hide show
  1. package/dist/AutoMesher-CK47F6AV.js +17 -0
  2. package/dist/GPUBuffers-2LHBCD7X.js +9 -0
  3. package/dist/WebGPUContext-TNEUYU2Y.js +11 -0
  4. package/dist/animation/index.cjs +38 -38
  5. package/dist/animation/index.d.cts +1 -1
  6. package/dist/animation/index.d.ts +1 -1
  7. package/dist/animation/index.js +1 -1
  8. package/dist/audio/index.cjs +16 -6
  9. package/dist/audio/index.d.cts +1 -1
  10. package/dist/audio/index.d.ts +1 -1
  11. package/dist/audio/index.js +1 -1
  12. package/dist/camera/index.cjs +23 -23
  13. package/dist/camera/index.d.cts +1 -1
  14. package/dist/camera/index.d.ts +1 -1
  15. package/dist/camera/index.js +1 -1
  16. package/dist/character/index.cjs +6 -4
  17. package/dist/character/index.js +1 -1
  18. package/dist/choreography/index.cjs +1194 -0
  19. package/dist/choreography/index.d.cts +687 -0
  20. package/dist/choreography/index.d.ts +687 -0
  21. package/dist/choreography/index.js +1156 -0
  22. package/dist/chunk-2CSNRI2N.js +217 -0
  23. package/dist/chunk-33T2WINR.js +266 -0
  24. package/dist/chunk-35R73OFM.js +1257 -0
  25. package/dist/chunk-4MMDSUNP.js +1256 -0
  26. package/dist/chunk-5V6HOU72.js +319 -0
  27. package/dist/chunk-6QOP6PYF.js +1038 -0
  28. package/dist/chunk-7KMJVHIL.js +8944 -0
  29. package/dist/chunk-7VPUC62U.js +1106 -0
  30. package/dist/chunk-A2Y6RCAT.js +1878 -0
  31. package/dist/chunk-AHM42MK6.js +8944 -0
  32. package/dist/chunk-BL7IDTHE.js +218 -0
  33. package/dist/chunk-CITOMSWL.js +10462 -0
  34. package/dist/chunk-CXDPKW2K.js +8944 -0
  35. package/dist/chunk-CXZPLD4S.js +223 -0
  36. package/dist/chunk-CZYJE7IH.js +5169 -0
  37. package/dist/chunk-D2OP7YC7.js +6325 -0
  38. package/dist/chunk-EDRVQHUU.js +1544 -0
  39. package/dist/chunk-EJSLOOW2.js +3589 -0
  40. package/dist/chunk-F53SFGW5.js +1878 -0
  41. package/dist/chunk-HCFPELPY.js +919 -0
  42. package/dist/chunk-HNEE36PY.js +93 -0
  43. package/dist/chunk-HYXNV36F.js +1256 -0
  44. package/dist/chunk-IB7KHVFY.js +821 -0
  45. package/dist/chunk-IBBO7YYG.js +690 -0
  46. package/dist/chunk-ILIBGINU.js +5470 -0
  47. package/dist/chunk-IS4MHLKN.js +5479 -0
  48. package/dist/chunk-JT2PFKWD.js +5479 -0
  49. package/dist/chunk-K4CUB4NY.js +1038 -0
  50. package/dist/chunk-KATDQXRJ.js +10462 -0
  51. package/dist/chunk-KBQE6ZFJ.js +8944 -0
  52. package/dist/chunk-KBVD5K7E.js +560 -0
  53. package/dist/chunk-KCDPVQRY.js +4088 -0
  54. package/dist/chunk-KN4QJPKN.js +8944 -0
  55. package/dist/chunk-KWJ3ROSI.js +8944 -0
  56. package/dist/chunk-L45VF6DD.js +919 -0
  57. package/dist/chunk-LY4T37YK.js +307 -0
  58. package/dist/chunk-MDN5WZXA.js +1544 -0
  59. package/dist/chunk-MGCDP6VU.js +928 -0
  60. package/dist/chunk-NCX7X6G2.js +8681 -0
  61. package/dist/chunk-OF54BPVD.js +913 -0
  62. package/dist/chunk-OWSN2Q3Q.js +690 -0
  63. package/dist/chunk-PRRB5TTA.js +406 -0
  64. package/dist/chunk-PXWVQF76.js +4086 -0
  65. package/dist/chunk-PYCOIDT2.js +812 -0
  66. package/dist/chunk-PZCSADOV.js +928 -0
  67. package/dist/chunk-Q2XBVS2K.js +1038 -0
  68. package/dist/chunk-QDZRXWN5.js +1776 -0
  69. package/dist/chunk-RNWOZ6WQ.js +913 -0
  70. package/dist/chunk-ROLFT4CJ.js +1693 -0
  71. package/dist/chunk-SLTJRZ2N.js +266 -0
  72. package/dist/chunk-SRUS5XSU.js +4088 -0
  73. package/dist/chunk-TKCA3WZ5.js +5409 -0
  74. package/dist/chunk-TNRMXYI2.js +1650 -0
  75. package/dist/chunk-TQB3GJGM.js +9763 -0
  76. package/dist/chunk-TUFGXG6K.js +510 -0
  77. package/dist/chunk-U6KMTGQJ.js +632 -0
  78. package/dist/chunk-VMGJQST6.js +8681 -0
  79. package/dist/chunk-X4F4TCG4.js +5470 -0
  80. package/dist/chunk-ZIFROE75.js +1544 -0
  81. package/dist/chunk-ZIJQYHSQ.js +1204 -0
  82. package/dist/combat/index.cjs +4 -4
  83. package/dist/combat/index.d.cts +1 -1
  84. package/dist/combat/index.d.ts +1 -1
  85. package/dist/combat/index.js +1 -1
  86. package/dist/ecs/index.cjs +1 -1
  87. package/dist/ecs/index.js +1 -1
  88. package/dist/environment/index.cjs +14 -14
  89. package/dist/environment/index.d.cts +1 -1
  90. package/dist/environment/index.d.ts +1 -1
  91. package/dist/environment/index.js +1 -1
  92. package/dist/gpu/index.cjs +4810 -0
  93. package/dist/gpu/index.js +3714 -0
  94. package/dist/hologram/index.cjs +27 -1
  95. package/dist/hologram/index.js +1 -1
  96. package/dist/index-B2PIsAmR.d.cts +2180 -0
  97. package/dist/index-B2PIsAmR.d.ts +2180 -0
  98. package/dist/index-BHySEPX7.d.cts +2921 -0
  99. package/dist/index-BJV21zuy.d.cts +341 -0
  100. package/dist/index-BJV21zuy.d.ts +341 -0
  101. package/dist/index-BQutTphC.d.cts +790 -0
  102. package/dist/index-ByIq2XrS.d.cts +3910 -0
  103. package/dist/index-BysHjDSO.d.cts +224 -0
  104. package/dist/index-BysHjDSO.d.ts +224 -0
  105. package/dist/index-CKwAJGck.d.ts +455 -0
  106. package/dist/index-CUl3QstQ.d.cts +3006 -0
  107. package/dist/index-CUl3QstQ.d.ts +3006 -0
  108. package/dist/index-CmYtNiI-.d.cts +953 -0
  109. package/dist/index-CmYtNiI-.d.ts +953 -0
  110. package/dist/index-CnRzWxi_.d.cts +522 -0
  111. package/dist/index-CnRzWxi_.d.ts +522 -0
  112. package/dist/index-CwRWbSC7.d.ts +2921 -0
  113. package/dist/index-CxKIBstO.d.ts +790 -0
  114. package/dist/index-DJ6-R8vh.d.cts +455 -0
  115. package/dist/index-DQKisbcI.d.cts +4968 -0
  116. package/dist/index-DQKisbcI.d.ts +4968 -0
  117. package/dist/index-DRT2zJez.d.ts +3910 -0
  118. package/dist/index-DfNLiAka.d.cts +192 -0
  119. package/dist/index-DfNLiAka.d.ts +192 -0
  120. package/dist/index-nMvkoRm8.d.cts +405 -0
  121. package/dist/index-nMvkoRm8.d.ts +405 -0
  122. package/dist/index-s9yOFU37.d.cts +604 -0
  123. package/dist/index-s9yOFU37.d.ts +604 -0
  124. package/dist/index.cjs +22966 -6960
  125. package/dist/index.d.cts +864 -20
  126. package/dist/index.d.ts +864 -20
  127. package/dist/index.js +3062 -48
  128. package/dist/input/index.cjs +1 -1
  129. package/dist/input/index.js +1 -1
  130. package/dist/orbital/index.cjs +3 -3
  131. package/dist/orbital/index.d.cts +1 -1
  132. package/dist/orbital/index.d.ts +1 -1
  133. package/dist/orbital/index.js +1 -1
  134. package/dist/particles/index.cjs +16 -16
  135. package/dist/particles/index.d.cts +1 -1
  136. package/dist/particles/index.d.ts +1 -1
  137. package/dist/particles/index.js +1 -1
  138. package/dist/physics/index.cjs +2377 -21
  139. package/dist/physics/index.d.cts +1 -1
  140. package/dist/physics/index.d.ts +1 -1
  141. package/dist/physics/index.js +35 -1
  142. package/dist/postfx/index.cjs +3491 -0
  143. package/dist/postfx/index.js +93 -0
  144. package/dist/procedural/index.cjs +1 -1
  145. package/dist/procedural/index.js +1 -1
  146. package/dist/puppeteer-5VF6KDVO.js +52197 -0
  147. package/dist/puppeteer-IZVZ3SG4.js +52197 -0
  148. package/dist/rendering/index.cjs +33 -32
  149. package/dist/rendering/index.d.cts +1 -1
  150. package/dist/rendering/index.d.ts +1 -1
  151. package/dist/rendering/index.js +8 -6
  152. package/dist/runtime/index.cjs +23 -13
  153. package/dist/runtime/index.d.cts +1 -1
  154. package/dist/runtime/index.d.ts +1 -1
  155. package/dist/runtime/index.js +8 -6
  156. package/dist/runtime/protocols/index.cjs +349 -0
  157. package/dist/runtime/protocols/index.js +15 -0
  158. package/dist/scene/index.cjs +8 -8
  159. package/dist/scene/index.d.cts +1 -1
  160. package/dist/scene/index.d.ts +1 -1
  161. package/dist/scene/index.js +1 -1
  162. package/dist/shader/index.cjs +3087 -0
  163. package/dist/shader/index.js +3044 -0
  164. package/dist/simulation/index.cjs +10680 -0
  165. package/dist/simulation/index.d.cts +3 -0
  166. package/dist/simulation/index.d.ts +3 -0
  167. package/dist/simulation/index.js +307 -0
  168. package/dist/spatial/index.cjs +2443 -0
  169. package/dist/spatial/index.d.cts +1545 -0
  170. package/dist/spatial/index.d.ts +1545 -0
  171. package/dist/spatial/index.js +2400 -0
  172. package/dist/terrain/index.cjs +1 -1
  173. package/dist/terrain/index.d.cts +1 -1
  174. package/dist/terrain/index.d.ts +1 -1
  175. package/dist/terrain/index.js +1 -1
  176. package/dist/transformers.node-4NKAPD5U.js +45620 -0
  177. package/dist/vm/index.cjs +7 -8
  178. package/dist/vm/index.d.cts +1 -1
  179. package/dist/vm/index.d.ts +1 -1
  180. package/dist/vm/index.js +1 -1
  181. package/dist/vm-bridge/index.cjs +2 -2
  182. package/dist/vm-bridge/index.d.cts +2 -2
  183. package/dist/vm-bridge/index.d.ts +2 -2
  184. package/dist/vm-bridge/index.js +1 -1
  185. package/dist/vr/index.cjs +6 -6
  186. package/dist/vr/index.js +1 -1
  187. package/dist/world/index.cjs +3 -3
  188. package/dist/world/index.d.cts +1 -1
  189. package/dist/world/index.d.ts +1 -1
  190. package/dist/world/index.js +1 -1
  191. package/package.json +53 -21
  192. package/LICENSE +0 -21
@@ -0,0 +1,3714 @@
1
+ import {
2
+ GPUBufferManager,
3
+ createInitialParticleData
4
+ } from "../chunk-CXZPLD4S.js";
5
+ import {
6
+ GaussianSplatExtractor
7
+ } from "../chunk-HNEE36PY.js";
8
+ import {
9
+ SparseLinearSolver
10
+ } from "../chunk-TUFGXG6K.js";
11
+ import {
12
+ WebGPUContext,
13
+ createPhysicsSimulation,
14
+ getGlobalWebGPUContext
15
+ } from "../chunk-2CSNRI2N.js";
16
+ import "../chunk-AKLW2MUS.js";
17
+
18
+ // src/gpu/ComputePipeline.ts
19
+ var ComputePipeline = class {
20
+ context;
21
+ bufferManager;
22
+ device;
23
+ options;
24
+ pipeline = null;
25
+ bindGroupLayout = null;
26
+ bindGroup = null;
27
+ workgroupsX = 0;
28
+ constructor(context, bufferManager, options) {
29
+ this.context = context;
30
+ this.bufferManager = bufferManager;
31
+ this.device = context.getDevice();
32
+ this.options = {
33
+ shaderCode: options.shaderCode,
34
+ entryPoint: options.entryPoint ?? "main",
35
+ workgroupSize: options.workgroupSize ?? this.context.getOptimalWorkgroupSize()
36
+ };
37
+ }
38
+ /**
39
+ * Initialize compute pipeline and bind groups
40
+ */
41
+ async initialize() {
42
+ const shaderModule = this.device.createShaderModule({
43
+ label: "particle-physics-shader",
44
+ code: this.options.shaderCode
45
+ });
46
+ const compilationInfo = await shaderModule.getCompilationInfo();
47
+ for (const message of compilationInfo.messages) {
48
+ if (message.type === "error") {
49
+ console.error("Shader error:", message.message, `at line ${message.lineNum}`);
50
+ } else if (message.type === "warning") {
51
+ console.warn("Shader warning:", message.message, `at line ${message.lineNum}`);
52
+ }
53
+ }
54
+ this.bindGroupLayout = this.device.createBindGroupLayout({
55
+ label: "particle-physics-bind-group-layout",
56
+ entries: [
57
+ // @binding(0): Uniforms (read-only)
58
+ {
59
+ binding: 0,
60
+ visibility: GPUShaderStage.COMPUTE,
61
+ buffer: { type: "uniform" }
62
+ },
63
+ // @binding(1): positions_in (read-only)
64
+ {
65
+ binding: 1,
66
+ visibility: GPUShaderStage.COMPUTE,
67
+ buffer: { type: "read-only-storage" }
68
+ },
69
+ // @binding(2): velocities_in (read-only)
70
+ {
71
+ binding: 2,
72
+ visibility: GPUShaderStage.COMPUTE,
73
+ buffer: { type: "read-only-storage" }
74
+ },
75
+ // @binding(3): states_in (read-only)
76
+ {
77
+ binding: 3,
78
+ visibility: GPUShaderStage.COMPUTE,
79
+ buffer: { type: "read-only-storage" }
80
+ },
81
+ // @binding(4): positions_out (read-write)
82
+ {
83
+ binding: 4,
84
+ visibility: GPUShaderStage.COMPUTE,
85
+ buffer: { type: "storage" }
86
+ },
87
+ // @binding(5): velocities_out (read-write)
88
+ {
89
+ binding: 5,
90
+ visibility: GPUShaderStage.COMPUTE,
91
+ buffer: { type: "storage" }
92
+ },
93
+ // @binding(6): states_out (read-write)
94
+ {
95
+ binding: 6,
96
+ visibility: GPUShaderStage.COMPUTE,
97
+ buffer: { type: "storage" }
98
+ }
99
+ ]
100
+ });
101
+ const pipelineLayout = this.device.createPipelineLayout({
102
+ label: "particle-physics-pipeline-layout",
103
+ bindGroupLayouts: [this.bindGroupLayout]
104
+ });
105
+ this.pipeline = this.device.createComputePipeline({
106
+ label: "particle-physics-pipeline",
107
+ layout: pipelineLayout,
108
+ compute: {
109
+ module: shaderModule,
110
+ entryPoint: this.options.entryPoint
111
+ }
112
+ });
113
+ this.createBindGroup();
114
+ const particleCount = this.bufferManager.getParticleCount();
115
+ this.workgroupsX = Math.ceil(particleCount / this.options.workgroupSize);
116
+ }
117
+ /**
118
+ * Create bind group linking buffers to shader bindings
119
+ */
120
+ createBindGroup() {
121
+ if (!this.bindGroupLayout) {
122
+ throw new Error("Bind group layout not created");
123
+ }
124
+ const buffers = this.bufferManager.getBuffers();
125
+ this.bindGroup = this.device.createBindGroup({
126
+ label: "particle-physics-bind-group",
127
+ layout: this.bindGroupLayout,
128
+ entries: [
129
+ { binding: 0, resource: { buffer: buffers.uniforms } },
130
+ { binding: 1, resource: { buffer: buffers.positionsRead } },
131
+ { binding: 2, resource: { buffer: buffers.velocitiesRead } },
132
+ { binding: 3, resource: { buffer: buffers.statesRead } },
133
+ { binding: 4, resource: { buffer: buffers.positionsWrite } },
134
+ { binding: 5, resource: { buffer: buffers.velocitiesWrite } },
135
+ { binding: 6, resource: { buffer: buffers.statesWrite } }
136
+ ]
137
+ });
138
+ }
139
+ /**
140
+ * Update uniform buffer with simulation parameters
141
+ */
142
+ updateUniforms(uniforms) {
143
+ this.bufferManager.uploadUniformData(uniforms);
144
+ }
145
+ /**
146
+ * Dispatch compute shader to update all particles
147
+ *
148
+ * @param commandEncoder Optional command encoder (creates new one if not provided)
149
+ * @returns Command encoder (for chaining or submission)
150
+ */
151
+ dispatch(commandEncoder) {
152
+ if (!this.pipeline || !this.bindGroup) {
153
+ throw new Error("Pipeline not initialized. Call initialize() first.");
154
+ }
155
+ const encoder = commandEncoder ?? this.device.createCommandEncoder({
156
+ label: "particle-physics-compute-encoder"
157
+ });
158
+ const computePass = encoder.beginComputePass({
159
+ label: "particle-physics-compute-pass"
160
+ });
161
+ computePass.setPipeline(this.pipeline);
162
+ computePass.setBindGroup(0, this.bindGroup);
163
+ computePass.dispatchWorkgroups(this.workgroupsX, 1, 1);
164
+ computePass.end();
165
+ return encoder;
166
+ }
167
+ /**
168
+ * Execute a single simulation step
169
+ *
170
+ * Convenience method that dispatches compute shader and submits to queue.
171
+ *
172
+ * @param uniforms Simulation parameters for this step
173
+ */
174
+ async step(uniforms) {
175
+ this.updateUniforms(uniforms);
176
+ const encoder = this.dispatch();
177
+ this.device.queue.submit([encoder.finish()]);
178
+ this.bufferManager.swap();
179
+ this.createBindGroup();
180
+ }
181
+ /**
182
+ * Execute multiple simulation steps (batch processing)
183
+ *
184
+ * @param steps Number of steps to execute
185
+ * @param uniforms Simulation parameters (same for all steps)
186
+ * @param onProgress Optional progress callback
187
+ */
188
+ async run(steps, uniforms, onProgress) {
189
+ const _startTime = performance.now();
190
+ for (let i = 0; i < steps; i++) {
191
+ await this.step(uniforms);
192
+ if (onProgress && i % 10 === 0) {
193
+ onProgress(i + 1, steps);
194
+ }
195
+ }
196
+ }
197
+ /**
198
+ * Get pipeline statistics
199
+ */
200
+ getStats() {
201
+ return {
202
+ particleCount: this.bufferManager.getParticleCount(),
203
+ workgroupSize: this.options.workgroupSize,
204
+ workgroups: this.workgroupsX,
205
+ threadsTotal: this.workgroupsX * this.options.workgroupSize
206
+ };
207
+ }
208
+ /**
209
+ * Cleanup resources
210
+ */
211
+ destroy() {
212
+ this.pipeline = null;
213
+ this.bindGroupLayout = null;
214
+ this.bindGroup = null;
215
+ }
216
+ };
217
+ async function createGPUPhysicsSimulation(options) {
218
+ const { WebGPUContext: WebGPUContext2 } = await import("../WebGPUContext-TNEUYU2Y.js");
219
+ const { GPUBufferManager: GPUBufferManager2 } = await import("../GPUBuffers-2LHBCD7X.js");
220
+ const context = new WebGPUContext2(options.contextOptions);
221
+ await context.initialize();
222
+ if (!context.isSupported()) {
223
+ throw new Error("WebGPU not supported on this device");
224
+ }
225
+ const bufferManager = new GPUBufferManager2(context, options.particleCount);
226
+ await bufferManager.initialize();
227
+ const pipeline = new ComputePipeline(context, bufferManager, {
228
+ shaderCode: options.shaderCode,
229
+ workgroupSize: options.workgroupSize
230
+ });
231
+ await pipeline.initialize();
232
+ return { context, bufferManager, pipeline };
233
+ }
234
+
235
+ // wgsl-raw:C:\Users\josep\Documents\GitHub\HoloScript\packages\engine\src\gpu\shaders\radix-sort.wgsl
236
+ var radix_sort_default = `/**
237
+ * Wait-Free Hierarchical Radix Sort - WebGPU Compute Shader
238
+ *
239
+ * Implements a 4-pass 8-bit LSD radix sort using Blelloch exclusive prefix sum.
240
+ * Designed for sorting Gaussian splat indices by depth (back-to-front).
241
+ *
242
+ * Architecture:
243
+ * - 4 passes, each sorting 8 bits of a 32-bit key (LSD order: bits 0-7, 8-15, 16-23, 24-31)
244
+ * - Each pass: histogram -> Blelloch scan -> scatter
245
+ * - Wait-free: no global atomics, only workgroup-level shared memory with barriers
246
+ * - Hierarchical block scan for cross-workgroup prefix sums
247
+ *
248
+ * Key design decisions for cross-browser compatibility:
249
+ * - No subgroup operations (not supported in Safari/Firefox)
250
+ * - No global atomics (wait-free guarantee)
251
+ * - workgroup_size(256) fits all GPU vendors (NVIDIA, AMD, Apple, Qualcomm)
252
+ * - All shared memory fits within 16KB (WebGPU minimum guarantee)
253
+ *
254
+ * References:
255
+ * - Blelloch (1990): "Prefix Sums and Their Applications"
256
+ * - Merrill & Grimshaw (2010): "Revisiting Sorting on GPUs"
257
+ * - HoloScript W.035, G.030.01 (3.COMPRESS research)
258
+ *
259
+ * @version 1.0.0
260
+ */
261
+
262
+ // =============================================================================
263
+ // Constants
264
+ // =============================================================================
265
+
266
+ const WORKGROUP_SIZE: u32 = 256u;
267
+ const RADIX_BITS: u32 = 8u;
268
+ const RADIX_SIZE: u32 = 256u; // 2^8 = 256 buckets per pass
269
+ const ELEMENTS_PER_THREAD: u32 = 4u;
270
+ const BLOCK_SIZE: u32 = 1024u; // WORKGROUP_SIZE * ELEMENTS_PER_THREAD
271
+
272
+ // =============================================================================
273
+ // Uniforms
274
+ // =============================================================================
275
+
276
+ struct SortUniforms {
277
+ totalCount: u32, // Total number of splats to sort
278
+ bitOffset: u32, // Current bit offset (0, 8, 16, 24)
279
+ blockCount: u32, // Number of workgroup blocks
280
+ _pad: u32,
281
+ };
282
+
283
+ @group(0) @binding(0) var<uniform> uniforms: SortUniforms;
284
+
285
+ // =============================================================================
286
+ // Storage Buffers
287
+ // =============================================================================
288
+
289
+ // Keys (depth values, 32-bit uint - quantized camera-space Z)
290
+ @group(0) @binding(1) var<storage, read> keysIn: array<u32>;
291
+ @group(0) @binding(2) var<storage, read_write> keysOut: array<u32>;
292
+
293
+ // Values (splat indices, 32-bit uint)
294
+ @group(0) @binding(3) var<storage, read> valuesIn: array<u32>;
295
+ @group(0) @binding(4) var<storage, read_write> valuesOut: array<u32>;
296
+
297
+ // Per-block histograms: blockCount * RADIX_SIZE
298
+ @group(0) @binding(5) var<storage, read_write> blockHistograms: array<u32>;
299
+
300
+ // Global prefix sums for each radix digit: RADIX_SIZE
301
+ @group(0) @binding(6) var<storage, read_write> globalPrefixes: array<u32>;
302
+
303
+ // =============================================================================
304
+ // Shared Memory
305
+ // =============================================================================
306
+
307
+ // Shared histogram for Blelloch scan (256 entries)
308
+ var<workgroup> sharedHist: array<u32, 256>;
309
+
310
+ // Shared scratch for local key/value staging
311
+ var<workgroup> sharedKeys: array<u32, 1024>;
312
+ var<workgroup> sharedVals: array<u32, 1024>;
313
+
314
+ // Shared local histogram for 256-way counting
315
+ var<workgroup> sharedLocalHist: array<atomic<u32>, 256>;
316
+
317
+ // =============================================================================
318
+ // Helper: Extract radix digit from key
319
+ // =============================================================================
320
+
321
+ fn extractDigit(key: u32, bitOffset: u32) -> u32 {
322
+ return (key >> bitOffset) & 0xFFu;
323
+ }
324
+
325
+ // =============================================================================
326
+ // Pass 1: Build Per-Block Histograms
327
+ // =============================================================================
328
+
329
+ /**
330
+ * Each workgroup processes BLOCK_SIZE elements, counting occurrences of each
331
+ * 8-bit radix digit. Results written to blockHistograms[blockIdx * 256 + digit].
332
+ *
333
+ * This is wait-free: each workgroup writes only to its own histogram region.
334
+ */
335
+ @compute @workgroup_size(256)
336
+ fn buildHistogram(
337
+ @builtin(global_invocation_id) globalId: vec3<u32>,
338
+ @builtin(local_invocation_id) localId: vec3<u32>,
339
+ @builtin(workgroup_id) groupId: vec3<u32>,
340
+ ) {
341
+ let tid = localId.x;
342
+ let blockIdx = groupId.x;
343
+ let blockStart = blockIdx * BLOCK_SIZE;
344
+
345
+ // Clear shared local histogram
346
+ atomicStore(&sharedLocalHist[tid], 0u);
347
+ workgroupBarrier();
348
+
349
+ // Each thread counts ELEMENTS_PER_THREAD elements
350
+ for (var i = 0u; i < ELEMENTS_PER_THREAD; i++) {
351
+ let idx = blockStart + tid * ELEMENTS_PER_THREAD + i;
352
+ if (idx < uniforms.totalCount) {
353
+ let key = keysIn[idx];
354
+ let digit = extractDigit(key, uniforms.bitOffset);
355
+ atomicAdd(&sharedLocalHist[digit], 1u);
356
+ }
357
+ }
358
+
359
+ workgroupBarrier();
360
+
361
+ // Write shared histogram to global memory
362
+ // Each thread writes one bucket value (256 threads, 256 buckets)
363
+ let histValue = atomicLoad(&sharedLocalHist[tid]);
364
+ blockHistograms[blockIdx * RADIX_SIZE + tid] = histValue;
365
+ }
366
+
367
+ // =============================================================================
368
+ // Pass 2: Blelloch Exclusive Prefix Sum (Hierarchical)
369
+ // =============================================================================
370
+
371
+ /**
372
+ * Computes exclusive prefix sums across all block histograms for each radix digit.
373
+ *
374
+ * For each digit d (0..255):
375
+ * globalPrefixes[d] = sum of all blockHistograms[block * 256 + d] for block < blockCount
376
+ *
377
+ * This pass processes one radix digit per workgroup.
378
+ * Each workgroup computes prefix sums across blocks for its assigned digit.
379
+ *
380
+ * Wait-free: uses Blelloch scan on shared memory with workgroup barriers only.
381
+ */
382
+ @compute @workgroup_size(256)
383
+ fn blellochScan(
384
+ @builtin(local_invocation_id) localId: vec3<u32>,
385
+ @builtin(workgroup_id) groupId: vec3<u32>,
386
+ ) {
387
+ let tid = localId.x;
388
+ let digit = groupId.x; // One workgroup per radix digit
389
+
390
+ // Load block histogram values for this digit into shared memory
391
+ // If blockCount <= 256, each thread loads one value
392
+ // For larger counts, we'd need multi-pass (rare for typical splat counts)
393
+ if (tid < uniforms.blockCount) {
394
+ sharedHist[tid] = blockHistograms[tid * RADIX_SIZE + digit];
395
+ } else {
396
+ sharedHist[tid] = 0u;
397
+ }
398
+
399
+ workgroupBarrier();
400
+
401
+ // ---- Blelloch Up-Sweep (Reduce) Phase ----
402
+ // Build partial sums bottom-up in a binary tree
403
+ var offset = 1u;
404
+ for (var d = WORKGROUP_SIZE >> 1u; d > 0u; d >>= 1u) {
405
+ if (tid < d) {
406
+ let ai = offset * (2u * tid + 1u) - 1u;
407
+ let bi = offset * (2u * tid + 2u) - 1u;
408
+ if (ai < WORKGROUP_SIZE && bi < WORKGROUP_SIZE) {
409
+ sharedHist[bi] += sharedHist[ai];
410
+ }
411
+ }
412
+ offset <<= 1u;
413
+ workgroupBarrier();
414
+ }
415
+
416
+ // Store total sum and clear last element for exclusive scan
417
+ if (tid == 0u) {
418
+ // Total across all blocks for this digit
419
+ globalPrefixes[digit] = sharedHist[WORKGROUP_SIZE - 1u];
420
+ sharedHist[WORKGROUP_SIZE - 1u] = 0u;
421
+ }
422
+
423
+ workgroupBarrier();
424
+
425
+ // ---- Blelloch Down-Sweep Phase ----
426
+ // Propagate partial sums back down to produce exclusive prefix sums
427
+ for (var d = 1u; d < WORKGROUP_SIZE; d <<= 1u) {
428
+ offset >>= 1u;
429
+ if (tid < d) {
430
+ let ai = offset * (2u * tid + 1u) - 1u;
431
+ let bi = offset * (2u * tid + 2u) - 1u;
432
+ if (ai < WORKGROUP_SIZE && bi < WORKGROUP_SIZE) {
433
+ let temp = sharedHist[ai];
434
+ sharedHist[ai] = sharedHist[bi];
435
+ sharedHist[bi] += temp;
436
+ }
437
+ }
438
+ workgroupBarrier();
439
+ }
440
+
441
+ // Write back the exclusive prefix sum for each block
442
+ if (tid < uniforms.blockCount) {
443
+ blockHistograms[tid * RADIX_SIZE + digit] = sharedHist[tid];
444
+ }
445
+ }
446
+
447
+ // =============================================================================
448
+ // Pass 2b: Global Prefix Sum over Digit Totals
449
+ // =============================================================================
450
+
451
+ /**
452
+ * After blellochScan, globalPrefixes[d] contains the total count for digit d.
453
+ * This pass computes an exclusive prefix sum over those totals to get the
454
+ * global scatter offset for each digit.
455
+ *
456
+ * Single workgroup: 256 threads for 256 digits.
457
+ */
458
+ @compute @workgroup_size(256)
459
+ fn globalPrefixScan(
460
+ @builtin(local_invocation_id) localId: vec3<u32>,
461
+ ) {
462
+ let tid = localId.x;
463
+
464
+ // Load digit totals into shared memory
465
+ sharedHist[tid] = globalPrefixes[tid];
466
+
467
+ workgroupBarrier();
468
+
469
+ // ---- Blelloch Up-Sweep ----
470
+ var offset = 1u;
471
+ for (var d = WORKGROUP_SIZE >> 1u; d > 0u; d >>= 1u) {
472
+ if (tid < d) {
473
+ let ai = offset * (2u * tid + 1u) - 1u;
474
+ let bi = offset * (2u * tid + 2u) - 1u;
475
+ if (ai < WORKGROUP_SIZE && bi < WORKGROUP_SIZE) {
476
+ sharedHist[bi] += sharedHist[ai];
477
+ }
478
+ }
479
+ offset <<= 1u;
480
+ workgroupBarrier();
481
+ }
482
+
483
+ // Clear last for exclusive scan
484
+ if (tid == 0u) {
485
+ sharedHist[WORKGROUP_SIZE - 1u] = 0u;
486
+ }
487
+ workgroupBarrier();
488
+
489
+ // ---- Blelloch Down-Sweep ----
490
+ for (var d = 1u; d < WORKGROUP_SIZE; d <<= 1u) {
491
+ offset >>= 1u;
492
+ if (tid < d) {
493
+ let ai = offset * (2u * tid + 1u) - 1u;
494
+ let bi = offset * (2u * tid + 2u) - 1u;
495
+ if (ai < WORKGROUP_SIZE && bi < WORKGROUP_SIZE) {
496
+ let temp = sharedHist[ai];
497
+ sharedHist[ai] = sharedHist[bi];
498
+ sharedHist[bi] += temp;
499
+ }
500
+ }
501
+ workgroupBarrier();
502
+ }
503
+
504
+ // Write exclusive prefix sums back
505
+ globalPrefixes[tid] = sharedHist[tid];
506
+ }
507
+
508
+ // =============================================================================
509
+ // Pass 3: Scatter (Reorder)
510
+ // =============================================================================
511
+
512
+ /**
513
+ * Each workgroup scatters its BLOCK_SIZE elements to globally sorted positions.
514
+ *
515
+ * For element at local position i with digit d:
516
+ * globalDst = globalPrefixes[d] // global offset for digit d
517
+ * + blockHistograms[block*256+d] // offset from prior blocks
518
+ * + localRank // rank within this block for digit d
519
+ *
520
+ * Wait-free: each element writes to a unique output position.
521
+ */
522
+ @compute @workgroup_size(256)
523
+ fn scatter(
524
+ @builtin(local_invocation_id) localId: vec3<u32>,
525
+ @builtin(workgroup_id) groupId: vec3<u32>,
526
+ ) {
527
+ let tid = localId.x;
528
+ let blockIdx = groupId.x;
529
+ let blockStart = blockIdx * BLOCK_SIZE;
530
+
531
+ // Clear shared local histogram for rank computation
532
+ atomicStore(&sharedLocalHist[tid], 0u);
533
+ workgroupBarrier();
534
+
535
+ // Load elements into shared memory and compute local histograms
536
+ var myKeys: array<u32, 4>;
537
+ var myVals: array<u32, 4>;
538
+ var myDigits: array<u32, 4>;
539
+
540
+ for (var i = 0u; i < ELEMENTS_PER_THREAD; i++) {
541
+ let idx = blockStart + tid * ELEMENTS_PER_THREAD + i;
542
+ if (idx < uniforms.totalCount) {
543
+ myKeys[i] = keysIn[idx];
544
+ myVals[i] = valuesIn[idx];
545
+ myDigits[i] = extractDigit(myKeys[i], uniforms.bitOffset);
546
+ } else {
547
+ myKeys[i] = 0xFFFFFFFFu; // Sentinel: sorts to end
548
+ myVals[i] = 0u;
549
+ myDigits[i] = 255u;
550
+ }
551
+ }
552
+
553
+ // Two-phase local ranking:
554
+ // Phase 1: Count elements per digit in this block
555
+ for (var i = 0u; i < ELEMENTS_PER_THREAD; i++) {
556
+ let idx = blockStart + tid * ELEMENTS_PER_THREAD + i;
557
+ if (idx < uniforms.totalCount) {
558
+ atomicAdd(&sharedLocalHist[myDigits[i]], 1u);
559
+ }
560
+ }
561
+
562
+ workgroupBarrier();
563
+
564
+ // Phase 2: Each thread needs its rank within its digit bucket.
565
+ // We use a serialized approach per-digit that's safe across all browsers.
566
+ // Load histogram into non-atomic shared for prefix computation.
567
+ let digitCount = atomicLoad(&sharedLocalHist[tid]);
568
+ sharedHist[tid] = digitCount;
569
+
570
+ workgroupBarrier();
571
+
572
+ // Compute exclusive prefix sum of digit counts (local to this block)
573
+ // This gives the starting offset within the block for each digit
574
+ var blockDigitOffset = 0u;
575
+ for (var d = 0u; d < tid; d++) {
576
+ blockDigitOffset += sharedHist[d];
577
+ }
578
+
579
+ // Store the block-local prefix for digit tid
580
+ sharedKeys[tid] = blockDigitOffset;
581
+
582
+ workgroupBarrier();
583
+
584
+ // Reset shared histogram for per-element ranking
585
+ atomicStore(&sharedLocalHist[tid], 0u);
586
+
587
+ workgroupBarrier();
588
+
589
+ // Each thread scatters its elements
590
+ for (var i = 0u; i < ELEMENTS_PER_THREAD; i++) {
591
+ let idx = blockStart + tid * ELEMENTS_PER_THREAD + i;
592
+ if (idx < uniforms.totalCount) {
593
+ let digit = myDigits[i];
594
+
595
+ // Get rank within this digit in this block (atomically increment)
596
+ let localRank = atomicAdd(&sharedLocalHist[digit], 1u);
597
+
598
+ // Compute global destination:
599
+ // globalPrefixes[digit] + blockHistograms[blockIdx * 256 + digit] + localRank
600
+ let globalOffset = globalPrefixes[digit];
601
+ let blockOffset = blockHistograms[blockIdx * RADIX_SIZE + digit];
602
+ let dst = globalOffset + blockOffset + localRank;
603
+
604
+ if (dst < uniforms.totalCount) {
605
+ keysOut[dst] = myKeys[i];
606
+ valuesOut[dst] = myVals[i];
607
+ }
608
+ }
609
+ }
610
+ }
611
+ `;
612
+
613
+ // wgsl-raw:C:\Users\josep\Documents\GitHub\HoloScript\packages\engine\src\gpu\shaders\splat-compress.wgsl
614
+ var splat_compress_default = "/**\n * Gaussian Splat Data Compression & Depth Key Generation\n *\n * Compresses Gaussian splat data for efficient GPU sorting and rendering:\n * - RGBA8 color packing: 4 bytes instead of 16 bytes (vec4<f32>)\n * - Compressed ellipse axes: 2D covariance stored as 3x f16 (6 bytes)\n * - Depth key generation: quantized camera-space Z for radix sort\n *\n * Memory layout (compressed, 32 bytes per splat):\n * [0:12] position (vec3<f32>) 12 bytes\n * [12:16] packedColor (u32, RGBA8) 4 bytes\n * [16:22] packedCov2D (3x u16) 6 bytes (f16 cov entries)\n * [22:24] opacity (f16) 2 bytes\n * [24:28] depth (f32) 4 bytes (camera-space Z)\n * [28:32] padding 4 bytes\n *\n * vs. uncompressed (64 bytes per splat):\n * position: vec3<f32> 12 bytes\n * scale: vec3<f32> 12 bytes\n * rotation: vec4<f32> 16 bytes\n * color: vec4<f32> 16 bytes\n * padding 8 bytes\n *\n * Compression ratio: 32/64 = 50% memory reduction\n *\n * Cross-browser notes:\n * - Uses u32 bit packing instead of f16 (f16 requires shader-f16 feature)\n * - All operations use u32 and f32, universally supported\n *\n * @version 1.0.0\n */\n\n// =============================================================================\n// Structures\n// =============================================================================\n\nstruct SplatRaw {\n pos: vec3<f32>,\n scale: vec3<f32>,\n rot: vec4<f32>, // quaternion\n color: vec4<f32>, // RGBA float\n};\n\nstruct SplatCompressed {\n pos: vec3<f32>, // 12 bytes\n packedColor: u32, // 4 bytes (RGBA8)\n packedCov2D_01: u32, // 4 bytes (cov[0] and cov[1] as f16 pair)\n packedCov2D_2_opacity: u32, // 4 bytes (cov[2] as f16, opacity as f16)\n depth: f32, // 4 bytes (camera-space Z)\n _pad: u32, // 4 bytes alignment\n};\n\nstruct CompressUniforms {\n viewMatrix: mat4x4<f32>, // 64 bytes\n projMatrix: mat4x4<f32>, // 64 bytes\n screenWidth: f32, // 4 bytes\n screenHeight: f32, // 4 bytes\n focalX: f32, // 4 bytes\n focalY: f32, // 4 bytes\n splatCount: u32, // 4 bytes\n _pad1: u32,\n _pad2: u32,\n _pad3: u32,\n};\n\n// =============================================================================\n// Bindings\n// =============================================================================\n\n@group(0) @binding(0) var<uniform> uniforms: CompressUniforms;\n@group(0) @binding(1) var<storage, read> splatsIn: array<SplatRaw>;\n@group(0) @binding(2) var<storage, read_write> splatsOut: array<SplatCompressed>;\n@group(0) @binding(3) var<storage, read_write> sortKeys: array<u32>;\n@group(0) @binding(4) var<storage, read_write> sortValues: array<u32>;\n\n// =============================================================================\n// f16 Packing Helpers (no shader-f16 feature required)\n// =============================================================================\n\n/**\n * Pack a f32 value into f16 (IEEE 754 half-precision) stored in lower 16 bits of u32.\n * Handles normals, denormals, inf, and nan correctly.\n */\nfn f32ToF16(value: f32) -> u32 {\n let bits = bitcast<u32>(value);\n let sign = (bits >> 16u) & 0x8000u;\n let exponent = (bits >> 23u) & 0xFFu;\n let mantissa = bits & 0x7FFFFFu;\n\n // Handle special cases\n if (exponent == 0u) {\n // Zero or denormal -> zero in f16\n return sign;\n }\n if (exponent == 255u) {\n // Inf or NaN\n if (mantissa != 0u) {\n return sign | 0x7E00u; // NaN\n }\n return sign | 0x7C00u; // Inf\n }\n\n // Bias conversion: f32 bias=127, f16 bias=15\n let newExponent = i32(exponent) - 127 + 15;\n\n if (newExponent <= 0) {\n // Underflow to zero\n return sign;\n }\n if (newExponent >= 31) {\n // Overflow to infinity\n return sign | 0x7C00u;\n }\n\n return sign | (u32(newExponent) << 10u) | (mantissa >> 13u);\n}\n\n/**\n * Unpack f16 (stored in lower 16 bits of u32) back to f32.\n */\nfn f16ToF32(h: u32) -> f32 {\n let sign = (h & 0x8000u) << 16u;\n let exponent = (h >> 10u) & 0x1Fu;\n let mantissa = h & 0x3FFu;\n\n if (exponent == 0u) {\n if (mantissa == 0u) {\n return bitcast<f32>(sign); // Signed zero\n }\n // Denormalized: convert to normalized f32\n var m = mantissa;\n var e = 0u;\n while ((m & 0x400u) == 0u) {\n m <<= 1u;\n e++;\n }\n let newExp = (127u - 15u - e) << 23u;\n let newMant = (m & 0x3FFu) << 13u;\n return bitcast<f32>(sign | newExp | newMant);\n }\n if (exponent == 31u) {\n if (mantissa == 0u) {\n return bitcast<f32>(sign | 0x7F800000u); // Inf\n }\n return bitcast<f32>(sign | 0x7FC00000u); // NaN\n }\n\n let newExp = (exponent + 127u - 15u) << 23u;\n let newMant = mantissa << 13u;\n return bitcast<f32>(sign | newExp | newMant);\n}\n\n/**\n * Pack two f16 values into a single u32.\n */\nfn packF16x2(a: f32, b: f32) -> u32 {\n return f32ToF16(a) | (f32ToF16(b) << 16u);\n}\n\n// =============================================================================\n// RGBA8 Color Packing\n// =============================================================================\n\n/**\n * Pack vec4<f32> color (0..1 range) into RGBA8 u32.\n * Layout: R[7:0] G[15:8] B[23:16] A[31:24]\n */\nfn packRGBA8(color: vec4<f32>) -> u32 {\n let r = u32(clamp(color.r * 255.0, 0.0, 255.0));\n let g = u32(clamp(color.g * 255.0, 0.0, 255.0));\n let b = u32(clamp(color.b * 255.0, 0.0, 255.0));\n let a = u32(clamp(color.a * 255.0, 0.0, 255.0));\n return r | (g << 8u) | (b << 16u) | (a << 24u);\n}\n\n/**\n * Unpack RGBA8 u32 back to vec4<f32>.\n */\nfn unpackRGBA8(packed: u32) -> vec4<f32> {\n return vec4<f32>(\n f32(packed & 0xFFu) / 255.0,\n f32((packed >> 8u) & 0xFFu) / 255.0,\n f32((packed >> 16u) & 0xFFu) / 255.0,\n f32((packed >> 24u) & 0xFFu) / 255.0,\n );\n}\n\n// =============================================================================\n// 3D to 2D Covariance Projection (Compressed Ellipse Axes)\n// =============================================================================\n\n/**\n * Compute 2D covariance matrix from 3D Gaussian parameters.\n *\n * The 3D covariance matrix Sigma = R * S * S^T * R^T where:\n * R = rotation matrix from quaternion\n * S = diagonal scale matrix\n *\n * Projected to 2D using the Jacobian of the perspective projection:\n * Sigma_2D = J * V * Sigma * V^T * J^T\n *\n * where V is the upper-left 3x3 of the view matrix and J is the Jacobian.\n *\n * Returns: vec3(cov[0][0], cov[0][1], cov[1][1]) - the symmetric 2x2 matrix.\n */\nfn computeCov2D(\n pos: vec3<f32>,\n scale: vec3<f32>,\n rot: vec4<f32>,\n viewMatrix: mat4x4<f32>,\n focalX: f32,\n focalY: f32,\n) -> vec3<f32> {\n // Transform position to camera space\n let camPos = viewMatrix * vec4<f32>(pos, 1.0);\n let tz = camPos.z;\n\n // Avoid division by zero\n let tzSafe = select(tz, 0.001, abs(tz) < 0.001);\n\n // Jacobian of perspective projection\n let tanFovX = 1.0 / focalX;\n let tanFovY = 1.0 / focalY;\n let limX = 1.3 * tanFovX;\n let limY = 1.3 * tanFovY;\n\n let tx = clamp(camPos.x / tzSafe, -limX, limX) * tzSafe;\n let ty = clamp(camPos.y / tzSafe, -limY, limY) * tzSafe;\n\n // Jacobian matrix (2x3)\n let J00 = focalX / tzSafe;\n let J02 = -focalX * tx / (tzSafe * tzSafe);\n let J11 = focalY / tzSafe;\n let J12 = -focalY * ty / (tzSafe * tzSafe);\n\n // Build rotation matrix from quaternion\n let r = rot.x;\n let x = rot.y;\n let y = rot.z;\n let z = rot.w;\n\n let R = mat3x3<f32>(\n vec3<f32>(1.0 - 2.0*(y*y + z*z), 2.0*(x*y - r*z), 2.0*(x*z + r*y)),\n vec3<f32>(2.0*(x*y + r*z), 1.0 - 2.0*(x*x + z*z), 2.0*(y*z - r*x)),\n vec3<f32>(2.0*(x*z - r*y), 2.0*(y*z + r*x), 1.0 - 2.0*(x*x + y*y)),\n );\n\n // Scale matrix (diagonal) applied as column scaling\n let M = mat3x3<f32>(\n R[0] * scale.x,\n R[1] * scale.y,\n R[2] * scale.z,\n );\n\n // 3D covariance: Sigma = M * M^T\n let Sigma = mat3x3<f32>(\n vec3<f32>(dot(M[0], M[0]), dot(M[0], M[1]), dot(M[0], M[2])),\n vec3<f32>(dot(M[1], M[0]), dot(M[1], M[1]), dot(M[1], M[2])),\n vec3<f32>(dot(M[2], M[0]), dot(M[2], M[1]), dot(M[2], M[2])),\n );\n\n // View rotation (upper-left 3x3)\n let V = mat3x3<f32>(\n viewMatrix[0].xyz,\n viewMatrix[1].xyz,\n viewMatrix[2].xyz,\n );\n\n // Transform covariance to camera space: T = V * Sigma * V^T\n let T = V * Sigma * transpose(V);\n\n // Apply Jacobian to get 2D covariance\n // cov2D = J * T * J^T (where J is 2x3, T is 3x3)\n let cov00 = J00 * J00 * T[0][0] + 2.0 * J00 * J02 * T[0][2] + J02 * J02 * T[2][2];\n let cov01 = J00 * J11 * T[0][1] + J00 * J12 * T[0][2] + J02 * J11 * T[1][2] + J02 * J12 * T[2][2];\n let cov11 = J11 * J11 * T[1][1] + 2.0 * J11 * J12 * T[1][2] + J12 * J12 * T[2][2];\n\n // Add low-pass filter to avoid aliasing (minimum 0.3px Gaussian)\n let covFiltered = vec3<f32>(cov00 + 0.3, cov01, cov11 + 0.3);\n\n return covFiltered;\n}\n\n// =============================================================================\n// Main Compression + Depth Key Compute Shader\n// =============================================================================\n\n/**\n * Compress raw splats and generate sort keys in a single pass.\n *\n * For each splat:\n * 1. Project to camera space, compute depth\n * 2. Compute 2D covariance (compressed ellipse axes)\n * 3. Pack color as RGBA8\n * 4. Pack covariance as f16x3\n * 5. Generate quantized depth key for radix sort\n * 6. Initialize sort value (splat index)\n */\n@compute @workgroup_size(256)\nfn compressAndKey(\n @builtin(global_invocation_id) globalId: vec3<u32>,\n) {\n let idx = globalId.x;\n if (idx >= uniforms.splatCount) {\n return;\n }\n\n let raw = splatsIn[idx];\n\n // Transform to camera space for depth\n let camPos = uniforms.viewMatrix * vec4<f32>(raw.pos, 1.0);\n let depth = camPos.z;\n\n // Frustum culling: skip splats behind camera\n // (They'll be sorted to the end with max depth key)\n var depthKey: u32;\n if (depth < 0.01) {\n depthKey = 0xFFFFFFFFu; // Behind camera -> max depth -> sorted last\n } else {\n // Quantize depth to 32-bit uint for radix sort\n // Use bit-cast of float: IEEE 754 floats sort correctly as uint when positive\n // (which camera-space depth always is for visible splats)\n depthKey = bitcast<u32>(depth);\n }\n\n // Compute 2D covariance (compressed ellipse axes)\n let cov2D = computeCov2D(\n raw.pos,\n raw.scale,\n raw.rot,\n uniforms.viewMatrix,\n uniforms.focalX,\n uniforms.focalY,\n );\n\n // Pack compressed splat\n var compressed: SplatCompressed;\n compressed.pos = raw.pos;\n compressed.packedColor = packRGBA8(raw.color);\n compressed.packedCov2D_01 = packF16x2(cov2D.x, cov2D.y);\n compressed.packedCov2D_2_opacity = packF16x2(cov2D.z, raw.color.a);\n compressed.depth = depth;\n compressed._pad = 0u;\n\n // Write compressed data\n splatsOut[idx] = compressed;\n\n // Write sort key-value pair\n sortKeys[idx] = depthKey;\n sortValues[idx] = idx;\n}\n";
615
+
616
+ // wgsl-raw:C:\Users\josep\Documents\GitHub\HoloScript\packages\engine\src\gpu\shaders\splat-render-sorted.wgsl
617
+ var splat_render_sorted_default = "/**\n * Sorted Gaussian Splat Renderer\n *\n * Renders compressed, depth-sorted Gaussian splats using the output\n * of the radix sort pipeline. Reads RGBA8-packed colors and f16-packed\n * 2D covariance (compressed ellipse axes) for efficient memory bandwidth.\n *\n * Rendering approach:\n * - Instance-based quad rendering (4 vertices per splat)\n * - Back-to-front order (via radix-sorted indices)\n * - Alpha blending with premultiplied alpha\n * - 2D Gaussian falloff using projected covariance (elliptical)\n *\n * Cross-browser compatible:\n * - No f16 shader feature required\n * - No subgroup operations\n * - Standard vertex/fragment pipeline\n *\n * @version 1.0.0\n */\n\n// =============================================================================\n// Structures\n// =============================================================================\n\nstruct SplatCompressed {\n pos: vec3<f32>,\n packedColor: u32,\n packedCov2D_01: u32,\n packedCov2D_2_opacity: u32,\n depth: f32,\n _pad: u32,\n};\n\nstruct RenderUniforms {\n viewProjection: mat4x4<f32>,\n viewMatrix: mat4x4<f32>,\n cameraPosition: vec3<f32>,\n screenWidth: f32,\n screenHeight: f32,\n focalX: f32,\n focalY: f32,\n _pad: f32,\n};\n\nstruct VertexOutput {\n @builtin(position) position: vec4<f32>,\n @location(0) color: vec4<f32>,\n @location(1) conicAndOpacity: vec4<f32>, // conic.xyz + opacity\n @location(2) centerScreen: vec2<f32>,\n};\n\n// =============================================================================\n// Bindings\n// =============================================================================\n\n@group(0) @binding(0) var<uniform> uniforms: RenderUniforms;\n@group(0) @binding(1) var<storage, read> splats: array<SplatCompressed>;\n@group(0) @binding(2) var<storage, read> sortedIndices: array<u32>;\n\n// =============================================================================\n// f16 Unpacking Helpers\n// =============================================================================\n\nfn f16ToF32(h: u32) -> f32 {\n let sign = (h & 0x8000u) << 16u;\n let exponent = (h >> 10u) & 0x1Fu;\n let mantissa = h & 0x3FFu;\n\n if (exponent == 0u) {\n if (mantissa == 0u) {\n return bitcast<f32>(sign);\n }\n var m = mantissa;\n var e = 0u;\n while ((m & 0x400u) == 0u) {\n m <<= 1u;\n e++;\n }\n let newExp = (127u - 15u - e) << 23u;\n let newMant = (m & 0x3FFu) << 13u;\n return bitcast<f32>(sign | newExp | newMant);\n }\n if (exponent == 31u) {\n if (mantissa == 0u) {\n return bitcast<f32>(sign | 0x7F800000u);\n }\n return bitcast<f32>(sign | 0x7FC00000u);\n }\n\n let newExp = (exponent + 127u - 15u) << 23u;\n let newMant = mantissa << 13u;\n return bitcast<f32>(sign | newExp | newMant);\n}\n\nfn unpackF16Low(packed: u32) -> f32 {\n return f16ToF32(packed & 0xFFFFu);\n}\n\nfn unpackF16High(packed: u32) -> f32 {\n return f16ToF32((packed >> 16u) & 0xFFFFu);\n}\n\nfn unpackRGBA8(packed: u32) -> vec4<f32> {\n return vec4<f32>(\n f32(packed & 0xFFu) / 255.0,\n f32((packed >> 8u) & 0xFFu) / 255.0,\n f32((packed >> 16u) & 0xFFu) / 255.0,\n f32((packed >> 24u) & 0xFFu) / 255.0,\n );\n}\n\n// =============================================================================\n// Vertex Shader\n// =============================================================================\n\n/**\n * Renders a billboard quad for each sorted Gaussian splat.\n *\n * The quad is sized based on the 2D covariance ellipse to tightly\n * bound the Gaussian at the 3-sigma level.\n *\n * vertex_index 0..3 maps to quad corners:\n * 0: (-1, -1) 1: (1, -1) 2: (-1, 1) 3: (1, 1)\n */\n@vertex\nfn vs_main(\n @builtin(vertex_index) vertexIndex: u32,\n @builtin(instance_index) instanceIndex: u32,\n) -> VertexOutput {\n // Look up sorted splat index\n let splatIndex = sortedIndices[instanceIndex];\n let splat = splats[splatIndex];\n\n // Unpack compressed data\n let color = unpackRGBA8(splat.packedColor);\n let cov00 = unpackF16Low(splat.packedCov2D_01);\n let cov01 = unpackF16High(splat.packedCov2D_01);\n let cov11 = unpackF16Low(splat.packedCov2D_2_opacity);\n let opacity = unpackF16High(splat.packedCov2D_2_opacity);\n\n // Project center to screen space\n let clipPos = uniforms.viewProjection * vec4<f32>(splat.pos, 1.0);\n let ndcPos = clipPos.xyz / clipPos.w;\n\n // Screen-space center\n let centerScreen = vec2<f32>(\n (ndcPos.x * 0.5 + 0.5) * uniforms.screenWidth,\n (ndcPos.y * -0.5 + 0.5) * uniforms.screenHeight,\n );\n\n // Compute inverse covariance (conic) for Gaussian evaluation in fragment shader\n // For 2x2 symmetric matrix [[a, b], [b, c]]:\n // det = a*c - b*b\n // inv = [[c, -b], [-b, a]] / det\n let det = cov00 * cov11 - cov01 * cov01;\n let detSafe = max(det, 1e-6);\n let conic = vec3<f32>(cov11 / detSafe, -cov01 / detSafe, cov00 / detSafe);\n\n // Compute eigenvalues for quad sizing (ellipse bounding box)\n let mid = 0.5 * (cov00 + cov11);\n let discriminant = max(mid * mid - det, 0.0);\n let lambda1 = mid + sqrt(discriminant);\n let lambda2 = mid - sqrt(discriminant);\n\n // 3-sigma bounding radius in pixels\n let maxRadius = ceil(3.0 * sqrt(max(lambda1, 0.0)));\n\n // Quad vertex positions (billboard)\n let quadUV = vec2<f32>(\n f32(vertexIndex & 1u) * 2.0 - 1.0,\n f32((vertexIndex >> 1u) & 1u) * 2.0 - 1.0,\n );\n\n let pixelOffset = quadUV * maxRadius;\n let screenPos = centerScreen + pixelOffset;\n\n // Convert back to NDC\n let finalNdc = vec2<f32>(\n (screenPos.x / uniforms.screenWidth) * 2.0 - 1.0,\n -((screenPos.y / uniforms.screenHeight) * 2.0 - 1.0),\n );\n\n var out: VertexOutput;\n out.position = vec4<f32>(finalNdc, ndcPos.z, 1.0);\n out.color = color;\n out.conicAndOpacity = vec4<f32>(conic, opacity);\n out.centerScreen = centerScreen;\n\n return out;\n}\n\n// =============================================================================\n// Fragment Shader\n// =============================================================================\n\n/**\n * Evaluates the 2D Gaussian using the inverse covariance (conic).\n *\n * For a pixel at position p relative to the Gaussian center c:\n * power = -0.5 * (d^T * Sigma^{-1} * d)\n * alpha = opacity * exp(power)\n *\n * where d = p - c, and Sigma^{-1} is the conic (inverse covariance).\n */\n@fragment\nfn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {\n // Fragment position in screen space (pixels)\n let fragScreen = in.position.xy;\n\n // Distance from Gaussian center in pixels\n let d = fragScreen - in.centerScreen;\n\n // Evaluate Gaussian: power = -0.5 * (conic.x * dx^2 + 2*conic.y * dx*dy + conic.z * dy^2)\n let power = -0.5 * (in.conicAndOpacity.x * d.x * d.x\n + 2.0 * in.conicAndOpacity.y * d.x * d.y\n + in.conicAndOpacity.z * d.y * d.y);\n\n // Clamp power to avoid numerical issues\n if (power > 0.0) {\n discard;\n }\n\n let alpha = min(0.99, in.conicAndOpacity.w * exp(power));\n\n // Discard nearly transparent fragments\n if (alpha < 1.0 / 255.0) {\n discard;\n }\n\n // Premultiplied alpha output for back-to-front compositing\n return vec4<f32>(in.color.rgb * alpha, alpha);\n}\n";
618
+
619
+ // src/gpu/GaussianSplatSorter.ts
620
+ var RADIX_BITS = 8;
621
+ var RADIX_SIZE = 256;
622
+ var NUM_PASSES = 4;
623
+ var BYTES_PER_COMPRESSED_SPLAT = 32;
624
+ var BYTES_PER_RAW_SPLAT = 64;
625
+ var GaussianSplatSorter = class {
626
+ context;
627
+ device;
628
+ options;
629
+ // Shader modules
630
+ sortShaderModule = null;
631
+ compressShaderModule = null;
632
+ renderShaderModule = null;
633
+ // Compute pipelines
634
+ compressPipeline = null;
635
+ histogramPipeline = null;
636
+ blellochScanPipeline = null;
637
+ globalPrefixPipeline = null;
638
+ scatterPipeline = null;
639
+ // Render pipeline
640
+ renderPipeline = null;
641
+ // Buffers
642
+ rawSplatBuffer = null;
643
+ compressedSplatBuffer = null;
644
+ sortKeysA = null;
645
+ sortKeysB = null;
646
+ sortValuesA = null;
647
+ sortValuesB = null;
648
+ blockHistogramsBuffer = null;
649
+ globalPrefixesBuffer = null;
650
+ compressUniformBuffer = null;
651
+ sortUniformBuffer = null;
652
+ renderUniformBuffer = null;
653
+ // Bind groups (rebuilt each frame due to ping-pong)
654
+ compressBindGroup = null;
655
+ // State
656
+ splatCount = 0;
657
+ blockCount = 0;
658
+ initialized = false;
659
+ constructor(context, options) {
660
+ this.context = context;
661
+ this.device = context.getDevice();
662
+ this.options = {
663
+ maxSplats: options.maxSplats,
664
+ workgroupSize: options.workgroupSize ?? 256,
665
+ elementsPerThread: options.elementsPerThread ?? 4,
666
+ enableTimestamps: options.enableTimestamps ?? false,
667
+ canvasWidth: options.canvasWidth,
668
+ canvasHeight: options.canvasHeight
669
+ };
670
+ }
671
+ // ===========================================================================
672
+ // Initialization
673
+ // ===========================================================================
674
+ /**
675
+ * Initialize all GPU resources: shaders, pipelines, buffers.
676
+ *
677
+ * Must be called before any sort or render operations.
678
+ */
679
+ async initialize() {
680
+ if (this.initialized) {
681
+ console.warn("GaussianSplatSorter already initialized");
682
+ return;
683
+ }
684
+ await this.createShaderModules();
685
+ this.createComputePipelines();
686
+ this.createRenderPipeline();
687
+ this.createBuffers();
688
+ this.initialized = true;
689
+ }
690
+ /**
691
+ * Create and validate shader modules with cross-browser error reporting.
692
+ */
693
+ async createShaderModules() {
694
+ const createModule = async (code, label) => {
695
+ const module = this.device.createShaderModule({ label, code });
696
+ try {
697
+ const info = await module.getCompilationInfo();
698
+ for (const msg of info.messages) {
699
+ if (msg.type === "error") {
700
+ throw new Error(
701
+ `Shader compilation error in ${label}: ${msg.message} (line ${msg.lineNum})`
702
+ );
703
+ }
704
+ if (msg.type === "warning") {
705
+ console.warn(`Shader warning in ${label}: ${msg.message} (line ${msg.lineNum})`);
706
+ }
707
+ }
708
+ } catch (e) {
709
+ if (e instanceof Error && e.message?.includes("Shader compilation error")) {
710
+ throw e;
711
+ }
712
+ console.warn(
713
+ `Could not validate shader ${label}:`,
714
+ e instanceof Error ? e.message : String(e)
715
+ );
716
+ }
717
+ return module;
718
+ };
719
+ this.sortShaderModule = await createModule(radix_sort_default, "radix-sort");
720
+ this.compressShaderModule = await createModule(splat_compress_default, "splat-compress");
721
+ this.renderShaderModule = await createModule(splat_render_sorted_default, "splat-render-sorted");
722
+ }
723
+ /**
724
+ * Create all compute pipelines for the sort.
725
+ */
726
+ createComputePipelines() {
727
+ if (!this.sortShaderModule || !this.compressShaderModule) {
728
+ throw new Error("Shader modules not created");
729
+ }
730
+ this.compressPipeline = this.device.createComputePipeline({
731
+ label: "splat-compress-pipeline",
732
+ layout: "auto",
733
+ compute: {
734
+ module: this.compressShaderModule,
735
+ entryPoint: "compressAndKey"
736
+ }
737
+ });
738
+ this.histogramPipeline = this.device.createComputePipeline({
739
+ label: "radix-histogram-pipeline",
740
+ layout: "auto",
741
+ compute: {
742
+ module: this.sortShaderModule,
743
+ entryPoint: "buildHistogram"
744
+ }
745
+ });
746
+ this.blellochScanPipeline = this.device.createComputePipeline({
747
+ label: "blelloch-scan-pipeline",
748
+ layout: "auto",
749
+ compute: {
750
+ module: this.sortShaderModule,
751
+ entryPoint: "blellochScan"
752
+ }
753
+ });
754
+ this.globalPrefixPipeline = this.device.createComputePipeline({
755
+ label: "global-prefix-pipeline",
756
+ layout: "auto",
757
+ compute: {
758
+ module: this.sortShaderModule,
759
+ entryPoint: "globalPrefixScan"
760
+ }
761
+ });
762
+ this.scatterPipeline = this.device.createComputePipeline({
763
+ label: "radix-scatter-pipeline",
764
+ layout: "auto",
765
+ compute: {
766
+ module: this.sortShaderModule,
767
+ entryPoint: "scatter"
768
+ }
769
+ });
770
+ }
771
+ /**
772
+ * Create render pipeline for sorted splat rendering.
773
+ */
774
+ createRenderPipeline() {
775
+ if (!this.renderShaderModule) {
776
+ throw new Error("Render shader module not created");
777
+ }
778
+ this.renderPipeline = this.device.createRenderPipeline({
779
+ label: "sorted-splat-render-pipeline",
780
+ layout: "auto",
781
+ vertex: {
782
+ module: this.renderShaderModule,
783
+ entryPoint: "vs_main",
784
+ buffers: []
785
+ // All data comes from storage buffers
786
+ },
787
+ fragment: {
788
+ module: this.renderShaderModule,
789
+ entryPoint: "fs_main",
790
+ targets: [
791
+ {
792
+ format: navigator.gpu.getPreferredCanvasFormat(),
793
+ blend: {
794
+ // Premultiplied alpha blending (back-to-front)
795
+ color: {
796
+ srcFactor: "one",
797
+ dstFactor: "one-minus-src-alpha",
798
+ operation: "add"
799
+ },
800
+ alpha: {
801
+ srcFactor: "one",
802
+ dstFactor: "one-minus-src-alpha",
803
+ operation: "add"
804
+ }
805
+ }
806
+ }
807
+ ]
808
+ },
809
+ primitive: {
810
+ topology: "triangle-strip",
811
+ stripIndexFormat: void 0
812
+ },
813
+ depthStencil: {
814
+ format: "depth24plus",
815
+ // Disable depth write for sorted splats (they're already in order)
816
+ depthWriteEnabled: false,
817
+ depthCompare: "always"
818
+ }
819
+ });
820
+ }
821
+ /**
822
+ * Allocate all GPU buffers.
823
+ */
824
+ createBuffers() {
825
+ const maxSplats = this.options.maxSplats;
826
+ const blockSize = this.options.workgroupSize * this.options.elementsPerThread;
827
+ const maxBlocks = Math.ceil(maxSplats / blockSize);
828
+ this.rawSplatBuffer = this.device.createBuffer({
829
+ label: "raw-splats",
830
+ size: maxSplats * BYTES_PER_RAW_SPLAT,
831
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
832
+ });
833
+ this.compressedSplatBuffer = this.device.createBuffer({
834
+ label: "compressed-splats",
835
+ size: maxSplats * BYTES_PER_COMPRESSED_SPLAT,
836
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
837
+ });
838
+ const sortBufferSize = maxSplats * 4;
839
+ this.sortKeysA = this.createSortBuffer("sort-keys-a", sortBufferSize);
840
+ this.sortKeysB = this.createSortBuffer("sort-keys-b", sortBufferSize);
841
+ this.sortValuesA = this.createSortBuffer("sort-values-a", sortBufferSize);
842
+ this.sortValuesB = this.createSortBuffer("sort-values-b", sortBufferSize);
843
+ this.blockHistogramsBuffer = this.device.createBuffer({
844
+ label: "block-histograms",
845
+ size: maxBlocks * RADIX_SIZE * 4,
846
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
847
+ });
848
+ this.globalPrefixesBuffer = this.device.createBuffer({
849
+ label: "global-prefixes",
850
+ size: RADIX_SIZE * 4,
851
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
852
+ });
853
+ this.compressUniformBuffer = this.device.createBuffer({
854
+ label: "compress-uniforms",
855
+ size: 160,
856
+ // 2 * mat4x4 (128) + 4 floats (16) + 4 u32 (16) = 160
857
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
858
+ });
859
+ this.sortUniformBuffer = this.device.createBuffer({
860
+ label: "sort-uniforms",
861
+ size: 16,
862
+ // totalCount (4) + bitOffset (4) + blockCount (4) + pad (4)
863
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
864
+ });
865
+ this.renderUniformBuffer = this.device.createBuffer({
866
+ label: "render-uniforms",
867
+ size: 160,
868
+ // viewProj (64) + view (64) + camPos (12) + 5 floats (20)
869
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
870
+ });
871
+ }
872
+ createSortBuffer(label, size) {
873
+ return this.device.createBuffer({
874
+ label,
875
+ size,
876
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC
877
+ });
878
+ }
879
+ // ===========================================================================
880
+ // Data Upload
881
+ // ===========================================================================
882
+ /**
883
+ * Upload raw splat data to the GPU.
884
+ *
885
+ * Expected layout per splat (64 bytes):
886
+ * position: vec3<f32> (12 bytes)
887
+ * scale: vec3<f32> (12 bytes)
888
+ * rotation: vec4<f32> (16 bytes) - quaternion (w, x, y, z)
889
+ * color: vec4<f32> (16 bytes) - RGBA [0..1]
890
+ * padding: (8 bytes)
891
+ *
892
+ * @param data Raw splat data as Float32Array
893
+ * @param count Number of splats (not bytes)
894
+ */
895
+ uploadSplatData(data, count) {
896
+ if (!this.rawSplatBuffer) {
897
+ throw new Error("Buffers not initialized");
898
+ }
899
+ if (count > this.options.maxSplats) {
900
+ throw new Error(`Splat count ${count} exceeds max ${this.options.maxSplats}`);
901
+ }
902
+ this.splatCount = count;
903
+ this.blockCount = Math.ceil(
904
+ count / (this.options.workgroupSize * this.options.elementsPerThread)
905
+ );
906
+ this.device.queue.writeBuffer(
907
+ this.rawSplatBuffer,
908
+ 0,
909
+ data.buffer,
910
+ data.byteOffset,
911
+ count * BYTES_PER_RAW_SPLAT
912
+ );
913
+ }
914
+ // ===========================================================================
915
+ // Sort Execution
916
+ // ===========================================================================
917
+ /**
918
+ * Execute the full sort pipeline: compress -> 4-pass radix sort.
919
+ *
920
+ * Should be called each frame before rendering when the camera moves.
921
+ * Uses a single command encoder for all passes to minimize CPU overhead.
922
+ *
923
+ * @param camera Current camera state for depth computation
924
+ * @param commandEncoder Optional encoder to chain with other passes
925
+ * @returns Command encoder with all sort passes recorded
926
+ */
927
+ sort(camera, commandEncoder) {
928
+ if (!this.initialized) {
929
+ throw new Error("Not initialized. Call initialize() first.");
930
+ }
931
+ const encoder = commandEncoder ?? this.device.createCommandEncoder({
932
+ label: "gaussian-splat-sort-encoder"
933
+ });
934
+ this.recordCompressPass(encoder, camera);
935
+ for (let pass = 0; pass < NUM_PASSES; pass++) {
936
+ const bitOffset = pass * RADIX_BITS;
937
+ const readFromA = pass % 2 === 0;
938
+ this.recordSortPass(encoder, bitOffset, readFromA);
939
+ }
940
+ return encoder;
941
+ }
942
+ /**
943
+ * Record compression compute pass.
944
+ */
945
+ recordCompressPass(encoder, camera) {
946
+ if (!this.compressPipeline || !this.compressUniformBuffer) {
947
+ throw new Error("Compress pipeline not created");
948
+ }
949
+ const uniforms = new Float32Array(40);
950
+ uniforms.set(camera.viewMatrix, 0);
951
+ uniforms.set(camera.projMatrix, 16);
952
+ const uintView = new Uint32Array(uniforms.buffer);
953
+ uniforms[32] = this.options.canvasWidth;
954
+ uniforms[33] = this.options.canvasHeight;
955
+ uniforms[34] = camera.focalX;
956
+ uniforms[35] = camera.focalY;
957
+ uintView[36] = this.splatCount;
958
+ uintView[37] = 0;
959
+ uintView[38] = 0;
960
+ uintView[39] = 0;
961
+ this.device.queue.writeBuffer(this.compressUniformBuffer, 0, uniforms);
962
+ this.compressBindGroup = this.device.createBindGroup({
963
+ label: "compress-bind-group",
964
+ layout: this.compressPipeline.getBindGroupLayout(0),
965
+ entries: [
966
+ { binding: 0, resource: { buffer: this.compressUniformBuffer } },
967
+ { binding: 1, resource: { buffer: this.rawSplatBuffer } },
968
+ { binding: 2, resource: { buffer: this.compressedSplatBuffer } },
969
+ { binding: 3, resource: { buffer: this.sortKeysA } },
970
+ { binding: 4, resource: { buffer: this.sortValuesA } }
971
+ ]
972
+ });
973
+ const computePass = encoder.beginComputePass({ label: "compress-pass" });
974
+ computePass.setPipeline(this.compressPipeline);
975
+ computePass.setBindGroup(0, this.compressBindGroup);
976
+ computePass.dispatchWorkgroups(Math.ceil(this.splatCount / this.options.workgroupSize));
977
+ computePass.end();
978
+ }
979
+ /**
980
+ * Record one radix sort pass (histogram + scan + scatter).
981
+ */
982
+ recordSortPass(encoder, bitOffset, readFromA) {
983
+ const keysIn = readFromA ? this.sortKeysA : this.sortKeysB;
984
+ const keysOut = readFromA ? this.sortKeysB : this.sortKeysA;
985
+ const valuesIn = readFromA ? this.sortValuesA : this.sortValuesB;
986
+ const valuesOut = readFromA ? this.sortValuesB : this.sortValuesA;
987
+ const sortUniforms = new Uint32Array([
988
+ this.splatCount,
989
+ bitOffset,
990
+ this.blockCount,
991
+ 0
992
+ // pad
993
+ ]);
994
+ this.device.queue.writeBuffer(this.sortUniformBuffer, 0, sortUniforms);
995
+ const histBindGroup = this.device.createBindGroup({
996
+ label: `histogram-bind-group-pass-${bitOffset}`,
997
+ layout: this.histogramPipeline.getBindGroupLayout(0),
998
+ entries: [
999
+ { binding: 0, resource: { buffer: this.sortUniformBuffer } },
1000
+ { binding: 1, resource: { buffer: keysIn } },
1001
+ { binding: 2, resource: { buffer: keysOut } },
1002
+ { binding: 3, resource: { buffer: valuesIn } },
1003
+ { binding: 4, resource: { buffer: valuesOut } },
1004
+ { binding: 5, resource: { buffer: this.blockHistogramsBuffer } },
1005
+ { binding: 6, resource: { buffer: this.globalPrefixesBuffer } }
1006
+ ]
1007
+ });
1008
+ const histPass = encoder.beginComputePass({ label: `histogram-${bitOffset}` });
1009
+ histPass.setPipeline(this.histogramPipeline);
1010
+ histPass.setBindGroup(0, histBindGroup);
1011
+ histPass.dispatchWorkgroups(this.blockCount);
1012
+ histPass.end();
1013
+ const scanBindGroup = this.device.createBindGroup({
1014
+ label: `blelloch-scan-bind-group-pass-${bitOffset}`,
1015
+ layout: this.blellochScanPipeline.getBindGroupLayout(0),
1016
+ entries: [
1017
+ { binding: 0, resource: { buffer: this.sortUniformBuffer } },
1018
+ { binding: 1, resource: { buffer: keysIn } },
1019
+ { binding: 2, resource: { buffer: keysOut } },
1020
+ { binding: 3, resource: { buffer: valuesIn } },
1021
+ { binding: 4, resource: { buffer: valuesOut } },
1022
+ { binding: 5, resource: { buffer: this.blockHistogramsBuffer } },
1023
+ { binding: 6, resource: { buffer: this.globalPrefixesBuffer } }
1024
+ ]
1025
+ });
1026
+ const scanPass = encoder.beginComputePass({ label: `blelloch-scan-${bitOffset}` });
1027
+ scanPass.setPipeline(this.blellochScanPipeline);
1028
+ scanPass.setBindGroup(0, scanBindGroup);
1029
+ scanPass.dispatchWorkgroups(RADIX_SIZE);
1030
+ scanPass.end();
1031
+ const globalPrefixBindGroup = this.device.createBindGroup({
1032
+ label: `global-prefix-bind-group-pass-${bitOffset}`,
1033
+ layout: this.globalPrefixPipeline.getBindGroupLayout(0),
1034
+ entries: [
1035
+ { binding: 0, resource: { buffer: this.sortUniformBuffer } },
1036
+ { binding: 1, resource: { buffer: keysIn } },
1037
+ { binding: 2, resource: { buffer: keysOut } },
1038
+ { binding: 3, resource: { buffer: valuesIn } },
1039
+ { binding: 4, resource: { buffer: valuesOut } },
1040
+ { binding: 5, resource: { buffer: this.blockHistogramsBuffer } },
1041
+ { binding: 6, resource: { buffer: this.globalPrefixesBuffer } }
1042
+ ]
1043
+ });
1044
+ const globalPrefixPass = encoder.beginComputePass({ label: `global-prefix-${bitOffset}` });
1045
+ globalPrefixPass.setPipeline(this.globalPrefixPipeline);
1046
+ globalPrefixPass.setBindGroup(0, globalPrefixBindGroup);
1047
+ globalPrefixPass.dispatchWorkgroups(1);
1048
+ globalPrefixPass.end();
1049
+ const scatterBindGroup = this.device.createBindGroup({
1050
+ label: `scatter-bind-group-pass-${bitOffset}`,
1051
+ layout: this.scatterPipeline.getBindGroupLayout(0),
1052
+ entries: [
1053
+ { binding: 0, resource: { buffer: this.sortUniformBuffer } },
1054
+ { binding: 1, resource: { buffer: keysIn } },
1055
+ { binding: 2, resource: { buffer: keysOut } },
1056
+ { binding: 3, resource: { buffer: valuesIn } },
1057
+ { binding: 4, resource: { buffer: valuesOut } },
1058
+ { binding: 5, resource: { buffer: this.blockHistogramsBuffer } },
1059
+ { binding: 6, resource: { buffer: this.globalPrefixesBuffer } }
1060
+ ]
1061
+ });
1062
+ const scatterPass = encoder.beginComputePass({ label: `scatter-${bitOffset}` });
1063
+ scatterPass.setPipeline(this.scatterPipeline);
1064
+ scatterPass.setBindGroup(0, scatterBindGroup);
1065
+ scatterPass.dispatchWorkgroups(this.blockCount);
1066
+ scatterPass.end();
1067
+ }
1068
+ // ===========================================================================
1069
+ // Rendering
1070
+ // ===========================================================================
1071
+ /**
1072
+ * Record render pass for sorted Gaussian splats.
1073
+ *
1074
+ * @param encoder Command encoder to record into
1075
+ * @param camera Camera state for rendering
1076
+ * @param colorView Color attachment view
1077
+ * @param depthView Depth attachment view
1078
+ * @param clearColor Optional clear color (default: transparent black)
1079
+ */
1080
+ recordRenderPass(encoder, camera, colorView, depthView, clearColor) {
1081
+ if (!this.renderPipeline || !this.renderUniformBuffer) {
1082
+ throw new Error("Render pipeline not created");
1083
+ }
1084
+ const renderUniforms = new Float32Array(40);
1085
+ renderUniforms.set(camera.viewProjectionMatrix, 0);
1086
+ renderUniforms.set(camera.viewMatrix, 16);
1087
+ renderUniforms[32] = camera.cameraPosition[0];
1088
+ renderUniforms[33] = camera.cameraPosition[1];
1089
+ renderUniforms[34] = camera.cameraPosition[2];
1090
+ renderUniforms[35] = this.options.canvasWidth;
1091
+ renderUniforms[36] = this.options.canvasHeight;
1092
+ renderUniforms[37] = camera.focalX;
1093
+ renderUniforms[38] = camera.focalY;
1094
+ renderUniforms[39] = 0;
1095
+ this.device.queue.writeBuffer(this.renderUniformBuffer, 0, renderUniforms);
1096
+ const sortedIndicesBuffer = this.sortValuesA;
1097
+ const renderBindGroup = this.device.createBindGroup({
1098
+ label: "sorted-splat-render-bind-group",
1099
+ layout: this.renderPipeline.getBindGroupLayout(0),
1100
+ entries: [
1101
+ { binding: 0, resource: { buffer: this.renderUniformBuffer } },
1102
+ { binding: 1, resource: { buffer: this.compressedSplatBuffer } },
1103
+ { binding: 2, resource: { buffer: sortedIndicesBuffer } }
1104
+ ]
1105
+ });
1106
+ const renderPass = encoder.beginRenderPass({
1107
+ label: "sorted-splat-render-pass",
1108
+ colorAttachments: [
1109
+ {
1110
+ view: colorView,
1111
+ clearValue: clearColor ?? { r: 0, g: 0, b: 0, a: 0 },
1112
+ loadOp: clearColor ? "clear" : "load",
1113
+ storeOp: "store"
1114
+ }
1115
+ ],
1116
+ depthStencilAttachment: {
1117
+ view: depthView,
1118
+ depthClearValue: 1,
1119
+ depthLoadOp: "clear",
1120
+ depthStoreOp: "store"
1121
+ }
1122
+ });
1123
+ renderPass.setPipeline(this.renderPipeline);
1124
+ renderPass.setBindGroup(0, renderBindGroup);
1125
+ renderPass.draw(4, this.splatCount);
1126
+ renderPass.end();
1127
+ }
1128
+ /**
1129
+ * Execute full frame: sort + render in a single command submission.
1130
+ *
1131
+ * This is the main per-frame method for most use cases.
1132
+ *
1133
+ * @param camera Current camera state
1134
+ * @param colorView Color attachment view
1135
+ * @param depthView Depth attachment view
1136
+ * @param clearColor Optional clear color
1137
+ */
1138
+ frame(camera, colorView, depthView, clearColor) {
1139
+ const encoder = this.device.createCommandEncoder({
1140
+ label: "gaussian-splat-frame-encoder"
1141
+ });
1142
+ this.sort(camera, encoder);
1143
+ this.recordRenderPass(encoder, camera, colorView, depthView, clearColor);
1144
+ this.device.queue.submit([encoder.finish()]);
1145
+ }
1146
+ // ===========================================================================
1147
+ // Statistics & Debugging
1148
+ // ===========================================================================
1149
+ /**
1150
+ * Get current sort statistics.
1151
+ */
1152
+ getStats() {
1153
+ return {
1154
+ splatCount: this.splatCount,
1155
+ blockCount: this.blockCount,
1156
+ memoryUsageBytes: this.getMemoryUsage()
1157
+ };
1158
+ }
1159
+ /**
1160
+ * Calculate total GPU memory usage in bytes.
1161
+ */
1162
+ getMemoryUsage() {
1163
+ const maxSplats = this.options.maxSplats;
1164
+ const maxBlocks = Math.ceil(
1165
+ maxSplats / (this.options.workgroupSize * this.options.elementsPerThread)
1166
+ );
1167
+ return maxSplats * BYTES_PER_RAW_SPLAT + // raw splats
1168
+ maxSplats * BYTES_PER_COMPRESSED_SPLAT + // compressed splats
1169
+ maxSplats * 4 * 4 + // 2x keys + 2x values (u32 each)
1170
+ maxBlocks * RADIX_SIZE * 4 + // block histograms
1171
+ RADIX_SIZE * 4 + // global prefixes
1172
+ 160 + 16 + 160;
1173
+ }
1174
+ /**
1175
+ * Get the sorted index buffer (for external rendering integration).
1176
+ *
1177
+ * After sort(), the sorted indices are in sortValuesA (for even pass count).
1178
+ */
1179
+ getSortedIndicesBuffer() {
1180
+ if (!this.sortValuesA) {
1181
+ throw new Error("Buffers not initialized");
1182
+ }
1183
+ return this.sortValuesA;
1184
+ }
1185
+ /**
1186
+ * Get the compressed splat buffer (for external rendering integration).
1187
+ */
1188
+ getCompressedSplatBuffer() {
1189
+ if (!this.compressedSplatBuffer) {
1190
+ throw new Error("Buffers not initialized");
1191
+ }
1192
+ return this.compressedSplatBuffer;
1193
+ }
1194
+ /**
1195
+ * Update canvas dimensions (e.g., on resize).
1196
+ */
1197
+ updateDimensions(width, height) {
1198
+ this.options.canvasWidth = width;
1199
+ this.options.canvasHeight = height;
1200
+ }
1201
+ // ===========================================================================
1202
+ // Cleanup
1203
+ // ===========================================================================
1204
+ /**
1205
+ * Destroy all GPU resources.
1206
+ */
1207
+ destroy() {
1208
+ const buffers = [
1209
+ this.rawSplatBuffer,
1210
+ this.compressedSplatBuffer,
1211
+ this.sortKeysA,
1212
+ this.sortKeysB,
1213
+ this.sortValuesA,
1214
+ this.sortValuesB,
1215
+ this.blockHistogramsBuffer,
1216
+ this.globalPrefixesBuffer,
1217
+ this.compressUniformBuffer,
1218
+ this.sortUniformBuffer,
1219
+ this.renderUniformBuffer
1220
+ ];
1221
+ for (const buffer of buffers) {
1222
+ buffer?.destroy();
1223
+ }
1224
+ this.rawSplatBuffer = null;
1225
+ this.compressedSplatBuffer = null;
1226
+ this.sortKeysA = null;
1227
+ this.sortKeysB = null;
1228
+ this.sortValuesA = null;
1229
+ this.sortValuesB = null;
1230
+ this.blockHistogramsBuffer = null;
1231
+ this.globalPrefixesBuffer = null;
1232
+ this.compressUniformBuffer = null;
1233
+ this.sortUniformBuffer = null;
1234
+ this.renderUniformBuffer = null;
1235
+ this.sortShaderModule = null;
1236
+ this.compressShaderModule = null;
1237
+ this.renderShaderModule = null;
1238
+ this.compressPipeline = null;
1239
+ this.histogramPipeline = null;
1240
+ this.blellochScanPipeline = null;
1241
+ this.globalPrefixPipeline = null;
1242
+ this.scatterPipeline = null;
1243
+ this.renderPipeline = null;
1244
+ this.initialized = false;
1245
+ }
1246
+ };
1247
+ async function createGaussianSplatSorter(options) {
1248
+ const { WebGPUContext: WebGPUContext2 } = await import("../WebGPUContext-TNEUYU2Y.js");
1249
+ const context = new WebGPUContext2(options.contextOptions);
1250
+ await context.initialize();
1251
+ if (!context.isSupported()) {
1252
+ throw new Error("WebGPU not supported - GaussianSplatSorter requires WebGPU");
1253
+ }
1254
+ const sorter = new GaussianSplatSorter(context, options);
1255
+ await sorter.initialize();
1256
+ return sorter;
1257
+ }
1258
+
1259
+ // src/gpu/InstancedRenderer.ts
1260
+ var InstancedRenderer = class {
1261
+ context;
1262
+ device;
1263
+ canvas;
1264
+ gpuContext = null;
1265
+ options;
1266
+ // Rendering resources
1267
+ pipeline = null;
1268
+ vertexBuffer = null;
1269
+ indexBuffer = null;
1270
+ instanceBuffer = null;
1271
+ uniformBuffer = null;
1272
+ // Geometry data
1273
+ indexCount = 0;
1274
+ vertexCount = 0;
1275
+ // State
1276
+ lastFrameTime = 0;
1277
+ frameCount = 0;
1278
+ constructor(context, canvas, options) {
1279
+ this.context = context;
1280
+ this.device = context.getDevice();
1281
+ this.canvas = canvas;
1282
+ this.options = {
1283
+ maxParticles: options.maxParticles,
1284
+ sphereSegments: options.sphereSegments ?? 16,
1285
+ enableLOD: options.enableLOD ?? true,
1286
+ lodDistances: options.lodDistances ?? [20, 50, 100],
1287
+ enableFrustumCulling: options.enableFrustumCulling ?? true
1288
+ };
1289
+ }
1290
+ /**
1291
+ * Initialize renderer
1292
+ */
1293
+ async initialize() {
1294
+ this.gpuContext = this.canvas.getContext("webgpu");
1295
+ if (!this.gpuContext) {
1296
+ throw new Error("Failed to get WebGPU canvas context");
1297
+ }
1298
+ const canvasFormat = navigator.gpu.getPreferredCanvasFormat();
1299
+ this.gpuContext.configure({
1300
+ device: this.device,
1301
+ format: canvasFormat,
1302
+ alphaMode: "opaque"
1303
+ });
1304
+ this.createSphereGeometry();
1305
+ this.createBuffers();
1306
+ this.createRenderPipeline(canvasFormat);
1307
+ }
1308
+ /**
1309
+ * Create sphere geometry
1310
+ */
1311
+ createSphereGeometry() {
1312
+ const segments = this.options.sphereSegments;
1313
+ const vertices = [];
1314
+ const indices = [];
1315
+ for (let lat = 0; lat <= segments; lat++) {
1316
+ const theta = lat * Math.PI / segments;
1317
+ const sinTheta = Math.sin(theta);
1318
+ const cosTheta = Math.cos(theta);
1319
+ for (let lon = 0; lon <= segments; lon++) {
1320
+ const phi = lon * 2 * Math.PI / segments;
1321
+ const sinPhi = Math.sin(phi);
1322
+ const cosPhi = Math.cos(phi);
1323
+ const x = cosPhi * sinTheta;
1324
+ const y = cosTheta;
1325
+ const z = sinPhi * sinTheta;
1326
+ vertices.push(x, y, z);
1327
+ vertices.push(x, y, z);
1328
+ }
1329
+ }
1330
+ for (let lat = 0; lat < segments; lat++) {
1331
+ for (let lon = 0; lon < segments; lon++) {
1332
+ const first = lat * (segments + 1) + lon;
1333
+ const second = first + segments + 1;
1334
+ indices.push(first, second, first + 1);
1335
+ indices.push(second, second + 1, first + 1);
1336
+ }
1337
+ }
1338
+ this.vertexCount = vertices.length / 6;
1339
+ this.indexCount = indices.length;
1340
+ this.createVertexBuffer(new Float32Array(vertices));
1341
+ this.createIndexBuffer(new Uint16Array(indices));
1342
+ }
1343
+ /**
1344
+ * Create vertex buffer
1345
+ */
1346
+ createVertexBuffer(vertices) {
1347
+ this.vertexBuffer = this.device.createBuffer({
1348
+ label: "sphere-vertices",
1349
+ size: vertices.byteLength,
1350
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST
1351
+ });
1352
+ this.device.queue.writeBuffer(
1353
+ this.vertexBuffer,
1354
+ 0,
1355
+ vertices
1356
+ );
1357
+ }
1358
+ /**
1359
+ * Create index buffer
1360
+ */
1361
+ createIndexBuffer(indices) {
1362
+ this.indexBuffer = this.device.createBuffer({
1363
+ label: "sphere-indices",
1364
+ size: indices.byteLength,
1365
+ usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST
1366
+ });
1367
+ this.device.queue.writeBuffer(
1368
+ this.indexBuffer,
1369
+ 0,
1370
+ indices
1371
+ );
1372
+ }
1373
+ /**
1374
+ * Create instance and uniform buffers
1375
+ */
1376
+ createBuffers() {
1377
+ const instanceSize = 8 * Float32Array.BYTES_PER_ELEMENT;
1378
+ const instanceBufferSize = this.options.maxParticles * instanceSize;
1379
+ this.instanceBuffer = this.device.createBuffer({
1380
+ label: "instance-buffer",
1381
+ size: instanceBufferSize,
1382
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST
1383
+ });
1384
+ const uniformBufferSize = 32 * Float32Array.BYTES_PER_ELEMENT;
1385
+ this.uniformBuffer = this.device.createBuffer({
1386
+ label: "camera-uniforms",
1387
+ size: uniformBufferSize,
1388
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
1389
+ });
1390
+ }
1391
+ /**
1392
+ * Create render pipeline
1393
+ */
1394
+ createRenderPipeline(format) {
1395
+ const shaderCode = `
1396
+ struct Uniforms {
1397
+ view: mat4x4<f32>,
1398
+ projection: mat4x4<f32>,
1399
+ };
1400
+
1401
+ @group(0) @binding(0) var<uniform> uniforms: Uniforms;
1402
+
1403
+ struct VertexInput {
1404
+ @location(0) position: vec3<f32>,
1405
+ @location(1) normal: vec3<f32>,
1406
+ };
1407
+
1408
+ struct InstanceInput {
1409
+ @location(2) instancePosition: vec4<f32>, // xyz = pos, w = radius
1410
+ @location(3) instanceColor: vec4<f32>, // rgba
1411
+ };
1412
+
1413
+ struct VertexOutput {
1414
+ @builtin(position) position: vec4<f32>,
1415
+ @location(0) normal: vec3<f32>,
1416
+ @location(1) worldPos: vec3<f32>,
1417
+ @location(2) color: vec4<f32>,
1418
+ };
1419
+
1420
+ @vertex
1421
+ fn vertexMain(
1422
+ vertex: VertexInput,
1423
+ instance: InstanceInput
1424
+ ) -> VertexOutput {
1425
+ var output: VertexOutput;
1426
+
1427
+ // Scale vertex by instance radius
1428
+ let scaledPos = vertex.position * instance.instancePosition.w;
1429
+
1430
+ // Translate to instance position
1431
+ let worldPos = scaledPos + instance.instancePosition.xyz;
1432
+
1433
+ // Transform to clip space
1434
+ output.position = uniforms.projection * uniforms.view * vec4<f32>(worldPos, 1.0);
1435
+ output.normal = vertex.normal;
1436
+ output.worldPos = worldPos;
1437
+ output.color = instance.instanceColor;
1438
+
1439
+ return output;
1440
+ }
1441
+
1442
+ @fragment
1443
+ fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {
1444
+ // Simple Phong lighting
1445
+ let lightDir = normalize(vec3<f32>(1.0, 1.0, 1.0));
1446
+ let normal = normalize(input.normal);
1447
+
1448
+ let ambient = 0.3;
1449
+ let diffuse = max(dot(normal, lightDir), 0.0) * 0.7;
1450
+ let lighting = ambient + diffuse;
1451
+
1452
+ return vec4<f32>(input.color.rgb * lighting, input.color.a);
1453
+ }
1454
+ `;
1455
+ const shaderModule = this.device.createShaderModule({
1456
+ label: "instanced-shader",
1457
+ code: shaderCode
1458
+ });
1459
+ const pipelineLayout = this.device.createPipelineLayout({
1460
+ bindGroupLayouts: [
1461
+ this.device.createBindGroupLayout({
1462
+ entries: [
1463
+ {
1464
+ binding: 0,
1465
+ visibility: GPUShaderStage.VERTEX,
1466
+ buffer: { type: "uniform" }
1467
+ }
1468
+ ]
1469
+ })
1470
+ ]
1471
+ });
1472
+ this.pipeline = this.device.createRenderPipeline({
1473
+ label: "instanced-pipeline",
1474
+ layout: pipelineLayout,
1475
+ vertex: {
1476
+ module: shaderModule,
1477
+ entryPoint: "vertexMain",
1478
+ buffers: [
1479
+ // Vertex buffer (per-vertex)
1480
+ {
1481
+ arrayStride: 6 * Float32Array.BYTES_PER_ELEMENT,
1482
+ // position + normal
1483
+ attributes: [
1484
+ { shaderLocation: 0, offset: 0, format: "float32x3" },
1485
+ // position
1486
+ { shaderLocation: 1, offset: 3 * 4, format: "float32x3" }
1487
+ // normal
1488
+ ]
1489
+ },
1490
+ // Instance buffer (per-instance)
1491
+ {
1492
+ arrayStride: 8 * Float32Array.BYTES_PER_ELEMENT,
1493
+ // position+radius + color
1494
+ stepMode: "instance",
1495
+ attributes: [
1496
+ { shaderLocation: 2, offset: 0, format: "float32x4" },
1497
+ // instancePosition
1498
+ { shaderLocation: 3, offset: 4 * 4, format: "float32x4" }
1499
+ // instanceColor
1500
+ ]
1501
+ }
1502
+ ]
1503
+ },
1504
+ fragment: {
1505
+ module: shaderModule,
1506
+ entryPoint: "fragmentMain",
1507
+ targets: [{ format }]
1508
+ },
1509
+ primitive: {
1510
+ topology: "triangle-list",
1511
+ cullMode: "back"
1512
+ },
1513
+ depthStencil: {
1514
+ format: "depth24plus",
1515
+ depthWriteEnabled: true,
1516
+ depthCompare: "less"
1517
+ }
1518
+ });
1519
+ }
1520
+ /**
1521
+ * Update instance buffer with particle data
1522
+ */
1523
+ updateInstances(positions, count) {
1524
+ if (!this.instanceBuffer) return;
1525
+ const instanceData = new Float32Array(count * 8);
1526
+ for (let i = 0; i < count; i++) {
1527
+ const posIdx = i * 4;
1528
+ const instIdx = i * 8;
1529
+ instanceData[instIdx + 0] = positions[posIdx + 0];
1530
+ instanceData[instIdx + 1] = positions[posIdx + 1];
1531
+ instanceData[instIdx + 2] = positions[posIdx + 2];
1532
+ instanceData[instIdx + 3] = positions[posIdx + 3];
1533
+ const y = positions[posIdx + 1];
1534
+ instanceData[instIdx + 4] = 0.4 + y * 0.02;
1535
+ instanceData[instIdx + 5] = 0.5 + y * 0.01;
1536
+ instanceData[instIdx + 6] = 0.8;
1537
+ instanceData[instIdx + 7] = 1;
1538
+ }
1539
+ this.device.queue.writeBuffer(this.instanceBuffer, 0, instanceData);
1540
+ }
1541
+ /**
1542
+ * Update camera uniforms
1543
+ */
1544
+ updateCamera(camera) {
1545
+ if (!this.uniformBuffer) return;
1546
+ const view = this.buildViewMatrix(camera.position, camera.target);
1547
+ const projection = this.buildProjectionMatrix(
1548
+ camera.fov,
1549
+ camera.aspect,
1550
+ camera.near,
1551
+ camera.far
1552
+ );
1553
+ const uniforms = new Float32Array(32);
1554
+ uniforms.set(view, 0);
1555
+ uniforms.set(projection, 16);
1556
+ this.device.queue.writeBuffer(this.uniformBuffer, 0, uniforms);
1557
+ }
1558
+ /**
1559
+ * Build view matrix (lookAt)
1560
+ */
1561
+ buildViewMatrix(eye, target) {
1562
+ const zAxis = this.normalize([eye[0] - target[0], eye[1] - target[1], eye[2] - target[2]]);
1563
+ const xAxis = this.normalize(this.cross([0, 1, 0], zAxis));
1564
+ const yAxis = this.cross(zAxis, xAxis);
1565
+ return new Float32Array([
1566
+ xAxis[0],
1567
+ yAxis[0],
1568
+ zAxis[0],
1569
+ 0,
1570
+ xAxis[1],
1571
+ yAxis[1],
1572
+ zAxis[1],
1573
+ 0,
1574
+ xAxis[2],
1575
+ yAxis[2],
1576
+ zAxis[2],
1577
+ 0,
1578
+ -this.dot(xAxis, eye),
1579
+ -this.dot(yAxis, eye),
1580
+ -this.dot(zAxis, eye),
1581
+ 1
1582
+ ]);
1583
+ }
1584
+ /**
1585
+ * Build projection matrix (perspective)
1586
+ */
1587
+ buildProjectionMatrix(fov, aspect, near, far) {
1588
+ const f = 1 / Math.tan(fov / 2);
1589
+ const rangeInv = 1 / (near - far);
1590
+ return new Float32Array([
1591
+ f / aspect,
1592
+ 0,
1593
+ 0,
1594
+ 0,
1595
+ 0,
1596
+ f,
1597
+ 0,
1598
+ 0,
1599
+ 0,
1600
+ 0,
1601
+ (near + far) * rangeInv,
1602
+ -1,
1603
+ 0,
1604
+ 0,
1605
+ near * far * rangeInv * 2,
1606
+ 0
1607
+ ]);
1608
+ }
1609
+ /**
1610
+ * Render particles
1611
+ */
1612
+ render(positions, particleCount, camera) {
1613
+ if (!this.gpuContext || !this.pipeline || !this.vertexBuffer || !this.indexBuffer || !this.instanceBuffer || !this.uniformBuffer) {
1614
+ throw new Error("Renderer not initialized");
1615
+ }
1616
+ this.updateInstances(positions, particleCount);
1617
+ this.updateCamera(camera);
1618
+ const depthTexture = this.device.createTexture({
1619
+ size: [this.canvas.width, this.canvas.height],
1620
+ format: "depth24plus",
1621
+ usage: GPUTextureUsage.RENDER_ATTACHMENT
1622
+ });
1623
+ const bindGroup = this.device.createBindGroup({
1624
+ layout: this.pipeline.getBindGroupLayout(0),
1625
+ entries: [{ binding: 0, resource: { buffer: this.uniformBuffer } }]
1626
+ });
1627
+ const commandEncoder = this.device.createCommandEncoder({ label: "render-encoder" });
1628
+ const renderPass = commandEncoder.beginRenderPass({
1629
+ colorAttachments: [
1630
+ {
1631
+ view: this.gpuContext.getCurrentTexture().createView(),
1632
+ loadOp: "clear",
1633
+ clearValue: { r: 0.1, g: 0.1, b: 0.15, a: 1 },
1634
+ storeOp: "store"
1635
+ }
1636
+ ],
1637
+ depthStencilAttachment: {
1638
+ view: depthTexture.createView(),
1639
+ depthLoadOp: "clear",
1640
+ depthClearValue: 1,
1641
+ depthStoreOp: "store"
1642
+ }
1643
+ });
1644
+ renderPass.setPipeline(this.pipeline);
1645
+ renderPass.setBindGroup(0, bindGroup);
1646
+ renderPass.setVertexBuffer(0, this.vertexBuffer);
1647
+ renderPass.setVertexBuffer(1, this.instanceBuffer);
1648
+ renderPass.setIndexBuffer(this.indexBuffer, "uint16");
1649
+ renderPass.drawIndexed(this.indexCount, particleCount, 0, 0, 0);
1650
+ renderPass.end();
1651
+ this.device.queue.submit([commandEncoder.finish()]);
1652
+ this.frameCount++;
1653
+ const now = performance.now();
1654
+ if (now - this.lastFrameTime >= 1e3) {
1655
+ this.frameCount = 0;
1656
+ this.lastFrameTime = now;
1657
+ }
1658
+ }
1659
+ /**
1660
+ * Vector math helpers
1661
+ */
1662
+ normalize(v) {
1663
+ const len = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
1664
+ return [v[0] / len, v[1] / len, v[2] / len];
1665
+ }
1666
+ cross(a, b) {
1667
+ return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
1668
+ }
1669
+ dot(a, b) {
1670
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
1671
+ }
1672
+ /**
1673
+ * Cleanup resources
1674
+ */
1675
+ destroy() {
1676
+ this.vertexBuffer?.destroy();
1677
+ this.indexBuffer?.destroy();
1678
+ this.instanceBuffer?.destroy();
1679
+ this.uniformBuffer?.destroy();
1680
+ this.pipeline = null;
1681
+ }
1682
+ };
1683
+
1684
+ // src/gpu/SpatialGrid.ts
1685
+ var SpatialGrid = class {
1686
+ context;
1687
+ device;
1688
+ particleCount;
1689
+ options;
1690
+ // Grid dimensions
1691
+ totalCells;
1692
+ // Pipelines
1693
+ clearPipeline = null;
1694
+ buildPipeline = null;
1695
+ collisionPipeline = null;
1696
+ // Buffers
1697
+ uniformBuffer = null;
1698
+ gridCellStartBuffer = null;
1699
+ gridCellEndBuffer = null;
1700
+ gridParticleIndicesBuffer = null;
1701
+ collisionForcesBuffer = null;
1702
+ // Bind groups
1703
+ clearBindGroup = null;
1704
+ buildBindGroup = null;
1705
+ collisionBindGroup = null;
1706
+ constructor(context, particleCount, options) {
1707
+ this.context = context;
1708
+ this.device = context.getDevice();
1709
+ this.particleCount = particleCount;
1710
+ this.options = {
1711
+ cellSize: options.cellSize,
1712
+ gridDimensions: options.gridDimensions,
1713
+ maxParticlesPerCell: options.maxParticlesPerCell ?? 64,
1714
+ shaderCode: options.shaderCode
1715
+ };
1716
+ this.totalCells = this.options.gridDimensions.x * this.options.gridDimensions.y * this.options.gridDimensions.z;
1717
+ }
1718
+ /**
1719
+ * Initialize spatial grid buffers and pipelines
1720
+ */
1721
+ async initialize() {
1722
+ const shaderModule = this.device.createShaderModule({
1723
+ label: "spatial-grid-shader",
1724
+ code: this.options.shaderCode
1725
+ });
1726
+ const compilationInfo = await shaderModule.getCompilationInfo();
1727
+ for (const message of compilationInfo.messages) {
1728
+ if (message.type === "error") {
1729
+ console.error("Spatial grid shader error:", message.message);
1730
+ }
1731
+ }
1732
+ this.createBuffers();
1733
+ this.createPipelines(shaderModule);
1734
+ }
1735
+ /**
1736
+ * Create GPU buffers for spatial grid
1737
+ */
1738
+ createBuffers() {
1739
+ this.uniformBuffer = this.device.createBuffer({
1740
+ label: "spatial-grid-uniforms",
1741
+ size: 32,
1742
+ // 8 × f32/u32 = 32 bytes
1743
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
1744
+ });
1745
+ const cellCounterSize = this.totalCells * Uint32Array.BYTES_PER_ELEMENT;
1746
+ this.gridCellStartBuffer = this.device.createBuffer({
1747
+ label: "grid-cell-start",
1748
+ size: cellCounterSize,
1749
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
1750
+ });
1751
+ this.gridCellEndBuffer = this.device.createBuffer({
1752
+ label: "grid-cell-end",
1753
+ size: cellCounterSize,
1754
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
1755
+ });
1756
+ const indicesSize = this.totalCells * this.options.maxParticlesPerCell * Uint32Array.BYTES_PER_ELEMENT;
1757
+ this.gridParticleIndicesBuffer = this.device.createBuffer({
1758
+ label: "grid-particle-indices",
1759
+ size: indicesSize,
1760
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
1761
+ });
1762
+ const forcesSize = this.particleCount * 4 * Float32Array.BYTES_PER_ELEMENT;
1763
+ this.collisionForcesBuffer = this.device.createBuffer({
1764
+ label: "collision-forces",
1765
+ size: forcesSize,
1766
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
1767
+ });
1768
+ this.uploadUniforms();
1769
+ }
1770
+ /**
1771
+ * Upload uniform data to GPU
1772
+ */
1773
+ uploadUniforms() {
1774
+ if (!this.uniformBuffer) return;
1775
+ const data = new ArrayBuffer(32);
1776
+ const floatView = new Float32Array(data);
1777
+ const uintView = new Uint32Array(data);
1778
+ floatView[0] = this.options.cellSize;
1779
+ uintView[1] = this.options.gridDimensions.x;
1780
+ uintView[2] = this.options.gridDimensions.y;
1781
+ uintView[3] = this.options.gridDimensions.z;
1782
+ uintView[4] = this.particleCount;
1783
+ uintView[5] = this.options.maxParticlesPerCell;
1784
+ uintView[6] = 0;
1785
+ uintView[7] = 0;
1786
+ this.device.queue.writeBuffer(this.uniformBuffer, 0, data);
1787
+ }
1788
+ /**
1789
+ * Create compute pipelines
1790
+ */
1791
+ createPipelines(shaderModule) {
1792
+ const clearLayout = this.createClearBindGroupLayout();
1793
+ const buildLayout = this.createBuildBindGroupLayout();
1794
+ const collisionLayout = this.createCollisionBindGroupLayout();
1795
+ this.clearPipeline = this.device.createComputePipeline({
1796
+ label: "grid-clear-pipeline",
1797
+ layout: this.device.createPipelineLayout({
1798
+ bindGroupLayouts: [clearLayout]
1799
+ }),
1800
+ compute: {
1801
+ module: shaderModule,
1802
+ entryPoint: "gridClear"
1803
+ }
1804
+ });
1805
+ this.buildPipeline = this.device.createComputePipeline({
1806
+ label: "grid-build-pipeline",
1807
+ layout: this.device.createPipelineLayout({
1808
+ bindGroupLayouts: [buildLayout]
1809
+ }),
1810
+ compute: {
1811
+ module: shaderModule,
1812
+ entryPoint: "gridBuild"
1813
+ }
1814
+ });
1815
+ this.collisionPipeline = this.device.createComputePipeline({
1816
+ label: "grid-collision-pipeline",
1817
+ layout: this.device.createPipelineLayout({
1818
+ bindGroupLayouts: [collisionLayout]
1819
+ }),
1820
+ compute: {
1821
+ module: shaderModule,
1822
+ entryPoint: "gridCollision"
1823
+ }
1824
+ });
1825
+ }
1826
+ /**
1827
+ * Create bind group layout for clear pass
1828
+ */
1829
+ createClearBindGroupLayout() {
1830
+ return this.device.createBindGroupLayout({
1831
+ label: "grid-clear-layout",
1832
+ entries: [
1833
+ { binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: "uniform" } },
1834
+ { binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } },
1835
+ { binding: 4, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } }
1836
+ ]
1837
+ });
1838
+ }
1839
+ /**
1840
+ * Create bind group layout for build pass
1841
+ */
1842
+ createBuildBindGroupLayout() {
1843
+ return this.device.createBindGroupLayout({
1844
+ label: "grid-build-layout",
1845
+ entries: [
1846
+ { binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: "uniform" } },
1847
+ { binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } },
1848
+ { binding: 4, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } },
1849
+ { binding: 5, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } }
1850
+ ]
1851
+ });
1852
+ }
1853
+ /**
1854
+ * Create bind group layout for collision pass
1855
+ */
1856
+ createCollisionBindGroupLayout() {
1857
+ return this.device.createBindGroupLayout({
1858
+ label: "grid-collision-layout",
1859
+ entries: [
1860
+ { binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: "uniform" } },
1861
+ { binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } },
1862
+ { binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } },
1863
+ { binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } },
1864
+ { binding: 4, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } },
1865
+ { binding: 5, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } },
1866
+ { binding: 6, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } }
1867
+ ]
1868
+ });
1869
+ }
1870
+ /**
1871
+ * Clear grid counters (run before buildGrid each frame)
1872
+ */
1873
+ clearGrid(commandEncoder) {
1874
+ if (!this.clearPipeline || !this.uniformBuffer || !this.gridCellStartBuffer || !this.gridCellEndBuffer) {
1875
+ throw new Error("Spatial grid not initialized");
1876
+ }
1877
+ if (!this.clearBindGroup) {
1878
+ this.clearBindGroup = this.device.createBindGroup({
1879
+ layout: this.clearPipeline.getBindGroupLayout(0),
1880
+ entries: [
1881
+ { binding: 0, resource: { buffer: this.uniformBuffer } },
1882
+ { binding: 3, resource: { buffer: this.gridCellStartBuffer } },
1883
+ { binding: 4, resource: { buffer: this.gridCellEndBuffer } }
1884
+ ]
1885
+ });
1886
+ }
1887
+ const encoder = commandEncoder ?? this.device.createCommandEncoder({ label: "grid-clear-encoder" });
1888
+ const pass = encoder.beginComputePass({ label: "grid-clear-pass" });
1889
+ pass.setPipeline(this.clearPipeline);
1890
+ pass.setBindGroup(0, this.clearBindGroup);
1891
+ const workgroups = Math.ceil(this.totalCells / 256);
1892
+ pass.dispatchWorkgroups(workgroups, 1, 1);
1893
+ pass.end();
1894
+ return encoder;
1895
+ }
1896
+ /**
1897
+ * Build spatial grid from particle positions
1898
+ */
1899
+ buildGrid(positionBuffer, commandEncoder) {
1900
+ if (!this.buildPipeline || !this.uniformBuffer || !this.gridCellEndBuffer || !this.gridParticleIndicesBuffer) {
1901
+ throw new Error("Spatial grid not initialized");
1902
+ }
1903
+ if (!this.buildBindGroup) {
1904
+ this.buildBindGroup = this.device.createBindGroup({
1905
+ layout: this.buildPipeline.getBindGroupLayout(0),
1906
+ entries: [
1907
+ { binding: 0, resource: { buffer: this.uniformBuffer } },
1908
+ { binding: 1, resource: { buffer: positionBuffer } },
1909
+ { binding: 4, resource: { buffer: this.gridCellEndBuffer } },
1910
+ { binding: 5, resource: { buffer: this.gridParticleIndicesBuffer } }
1911
+ ]
1912
+ });
1913
+ }
1914
+ const encoder = commandEncoder ?? this.device.createCommandEncoder({ label: "grid-build-encoder" });
1915
+ const pass = encoder.beginComputePass({ label: "grid-build-pass" });
1916
+ pass.setPipeline(this.buildPipeline);
1917
+ pass.setBindGroup(0, this.buildBindGroup);
1918
+ const workgroups = Math.ceil(this.particleCount / 256);
1919
+ pass.dispatchWorkgroups(workgroups, 1, 1);
1920
+ pass.end();
1921
+ return encoder;
1922
+ }
1923
+ /**
1924
+ * Detect collisions using spatial grid
1925
+ */
1926
+ detectCollisions(positionBuffer, velocityBuffer, commandEncoder) {
1927
+ if (!this.collisionPipeline || !this.uniformBuffer || !this.collisionForcesBuffer) {
1928
+ throw new Error("Spatial grid not initialized");
1929
+ }
1930
+ if (!this.collisionBindGroup) {
1931
+ this.collisionBindGroup = this.device.createBindGroup({
1932
+ layout: this.collisionPipeline.getBindGroupLayout(0),
1933
+ entries: [
1934
+ { binding: 0, resource: { buffer: this.uniformBuffer } },
1935
+ { binding: 1, resource: { buffer: positionBuffer } },
1936
+ { binding: 2, resource: { buffer: velocityBuffer } },
1937
+ { binding: 3, resource: { buffer: this.gridCellStartBuffer } },
1938
+ { binding: 4, resource: { buffer: this.gridCellEndBuffer } },
1939
+ { binding: 5, resource: { buffer: this.gridParticleIndicesBuffer } },
1940
+ { binding: 6, resource: { buffer: this.collisionForcesBuffer } }
1941
+ ]
1942
+ });
1943
+ }
1944
+ const encoder = commandEncoder ?? this.device.createCommandEncoder({ label: "grid-collision-encoder" });
1945
+ const pass = encoder.beginComputePass({ label: "grid-collision-pass" });
1946
+ pass.setPipeline(this.collisionPipeline);
1947
+ pass.setBindGroup(0, this.collisionBindGroup);
1948
+ const workgroups = Math.ceil(this.particleCount / 256);
1949
+ pass.dispatchWorkgroups(workgroups, 1, 1);
1950
+ pass.end();
1951
+ return encoder;
1952
+ }
1953
+ /**
1954
+ * Execute full collision detection pipeline
1955
+ *
1956
+ * Convenience method that runs all three passes: clear → build → detect
1957
+ */
1958
+ async execute(positionBuffer, velocityBuffer) {
1959
+ const encoder = this.device.createCommandEncoder({ label: "spatial-grid-full-encoder" });
1960
+ this.clearGrid(encoder);
1961
+ this.buildGrid(positionBuffer, encoder);
1962
+ this.detectCollisions(positionBuffer, velocityBuffer, encoder);
1963
+ this.device.queue.submit([encoder.finish()]);
1964
+ return await this.downloadCollisionForces();
1965
+ }
1966
+ /**
1967
+ * Download collision forces from GPU
1968
+ */
1969
+ async downloadCollisionForces() {
1970
+ if (!this.collisionForcesBuffer) {
1971
+ throw new Error("Collision forces buffer not initialized");
1972
+ }
1973
+ const stagingBuffer = this.device.createBuffer({
1974
+ size: this.collisionForcesBuffer.size,
1975
+ usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
1976
+ });
1977
+ const encoder = this.device.createCommandEncoder({ label: "collision-forces-readback" });
1978
+ encoder.copyBufferToBuffer(
1979
+ this.collisionForcesBuffer,
1980
+ 0,
1981
+ stagingBuffer,
1982
+ 0,
1983
+ this.collisionForcesBuffer.size
1984
+ );
1985
+ this.device.queue.submit([encoder.finish()]);
1986
+ await stagingBuffer.mapAsync(GPUMapMode.READ);
1987
+ const data = new Float32Array(stagingBuffer.getMappedRange()).slice();
1988
+ stagingBuffer.unmap();
1989
+ stagingBuffer.destroy();
1990
+ return data;
1991
+ }
1992
+ /**
1993
+ * Get collision forces buffer (for use in other compute passes)
1994
+ */
1995
+ getCollisionForcesBuffer() {
1996
+ if (!this.collisionForcesBuffer) {
1997
+ throw new Error("Collision forces buffer not initialized");
1998
+ }
1999
+ return this.collisionForcesBuffer;
2000
+ }
2001
+ /**
2002
+ * Calculate memory usage
2003
+ */
2004
+ calculateMemoryUsage() {
2005
+ const cellCounters = this.totalCells * 4 * 2;
2006
+ const indices = this.totalCells * this.options.maxParticlesPerCell * 4;
2007
+ const forces = this.particleCount * 4 * 4;
2008
+ const total = (cellCounters + indices + forces) / 1024 / 1024;
2009
+ return `${total.toFixed(2)} MB`;
2010
+ }
2011
+ /**
2012
+ * Get grid statistics
2013
+ */
2014
+ getStats() {
2015
+ return {
2016
+ cellSize: this.options.cellSize,
2017
+ gridDimensions: this.options.gridDimensions,
2018
+ totalCells: this.totalCells,
2019
+ maxParticlesPerCell: this.options.maxParticlesPerCell,
2020
+ memoryUsage: this.calculateMemoryUsage()
2021
+ };
2022
+ }
2023
+ /**
2024
+ * Cleanup resources
2025
+ */
2026
+ destroy() {
2027
+ this.uniformBuffer?.destroy();
2028
+ this.gridCellStartBuffer?.destroy();
2029
+ this.gridCellEndBuffer?.destroy();
2030
+ this.gridParticleIndicesBuffer?.destroy();
2031
+ this.collisionForcesBuffer?.destroy();
2032
+ this.clearPipeline = null;
2033
+ this.buildPipeline = null;
2034
+ this.collisionPipeline = null;
2035
+ this.clearBindGroup = null;
2036
+ this.buildBindGroup = null;
2037
+ this.collisionBindGroup = null;
2038
+ }
2039
+ };
2040
+
2041
+ // src/gpu/codecs/IGaussianCodec.ts
2042
+ var GaussianCodecError = class extends Error {
2043
+ constructor(message, codecId, operation, cause) {
2044
+ super(`[${codecId}] ${operation}: ${message}`);
2045
+ this.codecId = codecId;
2046
+ this.operation = operation;
2047
+ this.cause = cause;
2048
+ this.name = "GaussianCodecError";
2049
+ }
2050
+ codecId;
2051
+ operation;
2052
+ cause;
2053
+ };
2054
+ var CodecNotSupportedError = class extends GaussianCodecError {
2055
+ constructor(codecId, operation) {
2056
+ super(
2057
+ `Operation '${operation}' is not supported by this codec. Check capabilities before calling.`,
2058
+ codecId,
2059
+ operation
2060
+ );
2061
+ this.name = "CodecNotSupportedError";
2062
+ }
2063
+ };
2064
+ var CodecDecodeError = class extends GaussianCodecError {
2065
+ constructor(codecId, message, cause) {
2066
+ super(message, codecId, "decode", cause);
2067
+ this.name = "CodecDecodeError";
2068
+ }
2069
+ };
2070
+ var CodecEncodeError = class extends GaussianCodecError {
2071
+ constructor(codecId, message, cause) {
2072
+ super(message, codecId, "encode", cause);
2073
+ this.name = "CodecEncodeError";
2074
+ }
2075
+ };
2076
+ var CodecMemoryError = class extends GaussianCodecError {
2077
+ constructor(codecId, requiredMB, budgetMB) {
2078
+ super(
2079
+ `Memory budget exceeded: operation requires ~${requiredMB.toFixed(1)} MB but budget is ${budgetMB} MB`,
2080
+ codecId,
2081
+ "decode"
2082
+ );
2083
+ this.requiredMB = requiredMB;
2084
+ this.budgetMB = budgetMB;
2085
+ this.name = "CodecMemoryError";
2086
+ }
2087
+ requiredMB;
2088
+ budgetMB;
2089
+ };
2090
+ var CodecDecompressError = class extends GaussianCodecError {
2091
+ constructor(codecId, message, cause) {
2092
+ super(message, codecId, "decompress", cause);
2093
+ this.name = "CodecDecompressError";
2094
+ }
2095
+ };
2096
+ var AbstractGaussianCodec = class {
2097
+ initialized = false;
2098
+ // Default implementations for optional operations:
2099
+ async encode(_data, _options) {
2100
+ throw new CodecNotSupportedError(this.getCapabilities().id, "encode");
2101
+ }
2102
+ async *stream(_source, _options) {
2103
+ throw new CodecNotSupportedError(this.getCapabilities().id, "stream");
2104
+ }
2105
+ async decompress(_compressed) {
2106
+ throw new CodecNotSupportedError(this.getCapabilities().id, "decompress");
2107
+ }
2108
+ async initialize() {
2109
+ this.initialized = true;
2110
+ }
2111
+ dispose() {
2112
+ this.initialized = false;
2113
+ }
2114
+ /**
2115
+ * Estimate memory footprint in MB for a given Gaussian count.
2116
+ * Used by decode() to check memory budgets.
2117
+ */
2118
+ estimateMemoryMB(gaussianCount) {
2119
+ const bytesPerGaussian = 15 * 4;
2120
+ return gaussianCount * bytesPerGaussian / (1024 * 1024);
2121
+ }
2122
+ /**
2123
+ * Check if a decode operation would exceed the memory budget.
2124
+ * @throws CodecMemoryError if budget would be exceeded
2125
+ */
2126
+ checkMemoryBudget(gaussianCount, maxMemoryMB) {
2127
+ const requiredMB = this.estimateMemoryMB(gaussianCount);
2128
+ if (requiredMB > maxMemoryMB) {
2129
+ throw new CodecMemoryError(this.getCapabilities().id, requiredMB, maxMemoryMB);
2130
+ }
2131
+ }
2132
+ };
2133
+
2134
+ // src/gpu/codecs/SpzCodec.ts
2135
+ var SPZ_MAGIC = 1347635022;
2136
+ var SPZ_HEADER_SIZE = 16;
2137
+ var SPZ_MAX_POINTS = 1e7;
2138
+ var DEFAULT_MAX_MEMORY_MB = 512;
2139
+ var SPZ_COLOR_SCALE = 0.15;
2140
+ var SH_C0 = 0.2820948;
2141
+ var SQRT1_2 = Math.SQRT1_2;
2142
+ var GZIP_MAGIC_0 = 31;
2143
+ var GZIP_MAGIC_1 = 139;
2144
+ var SpzCodec = class extends AbstractGaussianCodec {
2145
+ codecId;
2146
+ constructor() {
2147
+ super();
2148
+ this.codecId = "khr.spz.v2";
2149
+ }
2150
+ // ─── Capabilities ─────────────────────────────────────────────────────────
2151
+ getCapabilities() {
2152
+ return {
2153
+ id: this.codecId,
2154
+ name: "Niantic SPZ Gaussian Splat Codec",
2155
+ version: "1.0.0",
2156
+ fileExtensions: ["spz"],
2157
+ mimeTypes: ["application/x-spz", "application/gzip"],
2158
+ canEncode: true,
2159
+ canDecode: true,
2160
+ canStream: true,
2161
+ canDecodeTemporal: false,
2162
+ maxSHDegree: 3,
2163
+ maxGaussianCount: SPZ_MAX_POINTS,
2164
+ requiresWasm: false,
2165
+ requiresWebGPU: false,
2166
+ standard: "khronos",
2167
+ maturity: "production"
2168
+ };
2169
+ }
2170
+ // ─── Probe ────────────────────────────────────────────────────────────────
2171
+ canDecode(buffer) {
2172
+ if (buffer.byteLength < 2) return false;
2173
+ const bytes = new Uint8Array(buffer, 0, 2);
2174
+ return bytes[0] === GZIP_MAGIC_0 && bytes[1] === GZIP_MAGIC_1;
2175
+ }
2176
+ // ─── Decompress ───────────────────────────────────────────────────────────
2177
+ async decompress(compressed) {
2178
+ try {
2179
+ return await decompressGzip(compressed);
2180
+ } catch (err) {
2181
+ throw new CodecDecompressError(
2182
+ this.codecId,
2183
+ "Gzip decompression failed. Ensure the data is a valid SPZ file.",
2184
+ err instanceof Error ? err : void 0
2185
+ );
2186
+ }
2187
+ }
2188
+ // ─── Extract Metadata ─────────────────────────────────────────────────────
2189
+ async extractMetadata(buffer) {
2190
+ const raw = await this.decompress(buffer);
2191
+ const header = parseSpzHeader(new DataView(raw));
2192
+ validateSpzHeader(header, this.codecId);
2193
+ const isV3 = header.version >= 3;
2194
+ const rotBytes = isV3 ? 4 : 3;
2195
+ const shDim = shDimForDegree(header.shDegree);
2196
+ const uncompressedSize = SPZ_HEADER_SIZE + header.numPoints * 9 + // positions
2197
+ header.numPoints + // alphas
2198
+ header.numPoints * 3 + // colors
2199
+ header.numPoints * 3 + // scales
2200
+ header.numPoints * rotBytes + // rotations
2201
+ header.numPoints * shDim * 3;
2202
+ return {
2203
+ version: header.version,
2204
+ gaussianCount: header.numPoints,
2205
+ shDegree: header.shDegree,
2206
+ compressedSizeBytes: buffer.byteLength,
2207
+ uncompressedSizeBytes: uncompressedSize,
2208
+ compressionRatio: buffer.byteLength / uncompressedSize,
2209
+ antialiased: (header.flags & 1) !== 0
2210
+ };
2211
+ }
2212
+ // ─── Decode ───────────────────────────────────────────────────────────────
2213
+ async decode(buffer, options) {
2214
+ const startTime = performance.now();
2215
+ const warnings = [];
2216
+ const maxGaussians = options?.maxGaussians ?? SPZ_MAX_POINTS;
2217
+ const maxMemoryMB = options?.maxMemoryMB ?? DEFAULT_MAX_MEMORY_MB;
2218
+ const decodeSH = options?.decodeSH ?? true;
2219
+ const alphaThreshold = options?.alphaThreshold ?? 0;
2220
+ const raw = await this.decompress(buffer);
2221
+ const data = new Uint8Array(raw);
2222
+ const view = new DataView(raw);
2223
+ const header = parseSpzHeader(view);
2224
+ validateSpzHeader(header, this.codecId);
2225
+ const N = Math.min(header.numPoints, maxGaussians);
2226
+ if (N < header.numPoints) {
2227
+ warnings.push(
2228
+ `Clamped Gaussian count from ${header.numPoints.toLocaleString()} to ${N.toLocaleString()} (maxGaussians limit)`
2229
+ );
2230
+ }
2231
+ this.checkMemoryBudget(N, maxMemoryMB);
2232
+ const isV3 = header.version >= 3;
2233
+ const rotBytes = isV3 ? 4 : 3;
2234
+ const shDim = decodeSH ? shDimForDegree(header.shDegree) : 0;
2235
+ const posScale = 1 / (1 << header.fractionalBits);
2236
+ const posStart = SPZ_HEADER_SIZE;
2237
+ const alphaStart = posStart + header.numPoints * 9;
2238
+ const colorStart = alphaStart + header.numPoints;
2239
+ const scaleStart = colorStart + header.numPoints * 3;
2240
+ const rotStart = scaleStart + header.numPoints * 3;
2241
+ const shStart = rotStart + header.numPoints * rotBytes;
2242
+ const expectedSize = shStart + header.numPoints * shDimForDegree(header.shDegree) * 3;
2243
+ if (data.length < expectedSize) {
2244
+ throw new CodecDecodeError(
2245
+ this.codecId,
2246
+ `SPZ buffer too short: ${data.length} bytes, expected at least ${expectedSize} bytes for ${header.numPoints} points with SH degree ${header.shDegree}`
2247
+ );
2248
+ }
2249
+ const positions = new Float32Array(N * 3);
2250
+ const scales = new Float32Array(N * 3);
2251
+ const rotations = new Float32Array(N * 4);
2252
+ const colors = new Float32Array(N * 4);
2253
+ const opacities = new Float32Array(N);
2254
+ let shCoefficients;
2255
+ if (shDim > 0) {
2256
+ shCoefficients = new Float32Array(N * shDim * 3);
2257
+ }
2258
+ for (let i = 0; i < N; i++) {
2259
+ const pOff = posStart + i * 9;
2260
+ for (let c = 0; c < 3; c++) {
2261
+ const byteOff = pOff + c * 3;
2262
+ let fixed32 = data[byteOff] | data[byteOff + 1] << 8 | data[byteOff + 2] << 16;
2263
+ if (fixed32 & 8388608) fixed32 |= 4278190080;
2264
+ fixed32 = fixed32 | 0;
2265
+ positions[i * 3 + c] = fixed32 * posScale;
2266
+ }
2267
+ }
2268
+ for (let i = 0; i < N; i++) {
2269
+ const rawAlpha = data[alphaStart + i] / 255;
2270
+ opacities[i] = rawAlpha;
2271
+ colors[i * 4 + 3] = rawAlpha;
2272
+ }
2273
+ for (let i = 0; i < N; i++) {
2274
+ const cOff = colorStart + i * 3;
2275
+ for (let c = 0; c < 3; c++) {
2276
+ const normalized = data[cOff + c] / 255;
2277
+ const shCoeff = (normalized - 0.5) / SPZ_COLOR_SCALE;
2278
+ const rgb = 0.5 + SH_C0 * shCoeff;
2279
+ colors[i * 4 + c] = Math.max(0, Math.min(1, rgb));
2280
+ }
2281
+ }
2282
+ for (let i = 0; i < N; i++) {
2283
+ const sOff = scaleStart + i * 3;
2284
+ for (let c = 0; c < 3; c++) {
2285
+ const logScale = data[sOff + c] / 16 - 10;
2286
+ scales[i * 3 + c] = Math.exp(logScale);
2287
+ }
2288
+ }
2289
+ for (let i = 0; i < N; i++) {
2290
+ const rOff = rotStart + i * rotBytes;
2291
+ let quat;
2292
+ if (isV3) {
2293
+ quat = decodeQuaternionV3(data, rOff);
2294
+ } else {
2295
+ quat = decodeQuaternionV2(data, rOff);
2296
+ }
2297
+ rotations[i * 4] = quat[0];
2298
+ rotations[i * 4 + 1] = quat[1];
2299
+ rotations[i * 4 + 2] = quat[2];
2300
+ rotations[i * 4 + 3] = quat[3];
2301
+ }
2302
+ if (shCoefficients && shDim > 0) {
2303
+ for (let i = 0; i < N; i++) {
2304
+ for (let s = 0; s < shDim; s++) {
2305
+ const off = shStart + (i * shDim + s) * 3;
2306
+ for (let c = 0; c < 3; c++) {
2307
+ const raw2 = data[off + c];
2308
+ const signed = raw2 > 127 ? raw2 - 256 : raw2;
2309
+ shCoefficients[(i * shDim + s) * 3 + c] = signed / 128 * SPZ_COLOR_SCALE;
2310
+ }
2311
+ }
2312
+ }
2313
+ }
2314
+ let finalCount = N;
2315
+ if (alphaThreshold > 0) {
2316
+ let writeIdx = 0;
2317
+ for (let i = 0; i < N; i++) {
2318
+ if (opacities[i] >= alphaThreshold) {
2319
+ if (writeIdx !== i) {
2320
+ positions[writeIdx * 3] = positions[i * 3];
2321
+ positions[writeIdx * 3 + 1] = positions[i * 3 + 1];
2322
+ positions[writeIdx * 3 + 2] = positions[i * 3 + 2];
2323
+ scales[writeIdx * 3] = scales[i * 3];
2324
+ scales[writeIdx * 3 + 1] = scales[i * 3 + 1];
2325
+ scales[writeIdx * 3 + 2] = scales[i * 3 + 2];
2326
+ rotations[writeIdx * 4] = rotations[i * 4];
2327
+ rotations[writeIdx * 4 + 1] = rotations[i * 4 + 1];
2328
+ rotations[writeIdx * 4 + 2] = rotations[i * 4 + 2];
2329
+ rotations[writeIdx * 4 + 3] = rotations[i * 4 + 3];
2330
+ colors[writeIdx * 4] = colors[i * 4];
2331
+ colors[writeIdx * 4 + 1] = colors[i * 4 + 1];
2332
+ colors[writeIdx * 4 + 2] = colors[i * 4 + 2];
2333
+ colors[writeIdx * 4 + 3] = colors[i * 4 + 3];
2334
+ opacities[writeIdx] = opacities[i];
2335
+ }
2336
+ writeIdx++;
2337
+ }
2338
+ }
2339
+ finalCount = writeIdx;
2340
+ if (finalCount < N) {
2341
+ warnings.push(
2342
+ `Filtered ${N - finalCount} Gaussians below alpha threshold ${alphaThreshold}`
2343
+ );
2344
+ }
2345
+ }
2346
+ const result = {
2347
+ positions: finalCount < N ? positions.slice(0, finalCount * 3) : positions,
2348
+ scales: finalCount < N ? scales.slice(0, finalCount * 3) : scales,
2349
+ rotations: finalCount < N ? rotations.slice(0, finalCount * 4) : rotations,
2350
+ colors: finalCount < N ? colors.slice(0, finalCount * 4) : colors,
2351
+ opacities: finalCount < N ? opacities.slice(0, finalCount) : opacities,
2352
+ shCoefficients: shCoefficients ? finalCount < N ? shCoefficients.slice(0, finalCount * shDim * 3) : shCoefficients : void 0,
2353
+ shDegree: decodeSH ? header.shDegree : 0,
2354
+ count: finalCount
2355
+ };
2356
+ const durationMs = performance.now() - startTime;
2357
+ return {
2358
+ data: result,
2359
+ durationMs,
2360
+ warnings
2361
+ };
2362
+ }
2363
+ // ─── Encode ───────────────────────────────────────────────────────────────
2364
+ async encode(data, options) {
2365
+ const startTime = performance.now();
2366
+ const warnings = [];
2367
+ const shDegree = options?.shDegree ?? data.shDegree;
2368
+ const fractionalBits = options?.fractionalBits ?? 12;
2369
+ const antialiased = options?.antialiased ?? false;
2370
+ const encodingVersion = options?.encodingVersion ?? 3;
2371
+ const N = data.count;
2372
+ if (N > SPZ_MAX_POINTS) {
2373
+ throw new CodecEncodeError(
2374
+ this.codecId,
2375
+ `Cannot encode ${N.toLocaleString()} Gaussians: exceeds maximum of ${SPZ_MAX_POINTS.toLocaleString()}`
2376
+ );
2377
+ }
2378
+ const isV3 = encodingVersion >= 3;
2379
+ const shDim = shDimForDegree(shDegree);
2380
+ const rotBytes = isV3 ? 4 : 3;
2381
+ const payloadSize = SPZ_HEADER_SIZE + N * 9 + // positions
2382
+ N + // alphas
2383
+ N * 3 + // colors
2384
+ N * 3 + // scales
2385
+ N * rotBytes + // rotations
2386
+ N * shDim * 3;
2387
+ const buffer = new ArrayBuffer(payloadSize);
2388
+ const out = new Uint8Array(buffer);
2389
+ const outView = new DataView(buffer);
2390
+ outView.setUint32(0, SPZ_MAGIC, true);
2391
+ outView.setUint32(4, encodingVersion, true);
2392
+ outView.setUint32(8, N, true);
2393
+ out[12] = shDegree;
2394
+ out[13] = fractionalBits;
2395
+ out[14] = antialiased ? 1 : 0;
2396
+ out[15] = 0;
2397
+ const posScale = 1 << fractionalBits;
2398
+ const posStart = SPZ_HEADER_SIZE;
2399
+ for (let i = 0; i < N; i++) {
2400
+ const pOff = posStart + i * 9;
2401
+ for (let c = 0; c < 3; c++) {
2402
+ const fixed = Math.round(data.positions[i * 3 + c] * posScale);
2403
+ const clamped = Math.max(-8388608, Math.min(8388607, fixed));
2404
+ const unsigned = clamped & 16777215;
2405
+ const byteOff = pOff + c * 3;
2406
+ out[byteOff] = unsigned & 255;
2407
+ out[byteOff + 1] = unsigned >> 8 & 255;
2408
+ out[byteOff + 2] = unsigned >> 16 & 255;
2409
+ }
2410
+ }
2411
+ const alphaStart = posStart + N * 9;
2412
+ for (let i = 0; i < N; i++) {
2413
+ out[alphaStart + i] = Math.round(Math.max(0, Math.min(1, data.opacities[i])) * 255);
2414
+ }
2415
+ const colorStart = alphaStart + N;
2416
+ for (let i = 0; i < N; i++) {
2417
+ const cOff = colorStart + i * 3;
2418
+ for (let c = 0; c < 3; c++) {
2419
+ const rgb = data.colors[i * 4 + c];
2420
+ const shCoeff = (rgb - 0.5) / SH_C0;
2421
+ const normalized = shCoeff * SPZ_COLOR_SCALE + 0.5;
2422
+ out[cOff + c] = Math.round(Math.max(0, Math.min(1, normalized)) * 255);
2423
+ }
2424
+ }
2425
+ const scaleStart = colorStart + N * 3;
2426
+ for (let i = 0; i < N; i++) {
2427
+ const sOff = scaleStart + i * 3;
2428
+ for (let c = 0; c < 3; c++) {
2429
+ const scale = data.scales[i * 3 + c];
2430
+ const logScale = Math.log(Math.max(1e-10, scale));
2431
+ const encoded2 = Math.round((logScale + 10) * 16);
2432
+ out[sOff + c] = Math.max(0, Math.min(255, encoded2));
2433
+ }
2434
+ }
2435
+ const rotStart = scaleStart + N * 3;
2436
+ if (isV3) {
2437
+ for (let i = 0; i < N; i++) {
2438
+ const rOff = rotStart + i * 4;
2439
+ const packed = encodeQuaternionV3(
2440
+ data.rotations[i * 4],
2441
+ data.rotations[i * 4 + 1],
2442
+ data.rotations[i * 4 + 2],
2443
+ data.rotations[i * 4 + 3]
2444
+ );
2445
+ out[rOff] = packed & 255;
2446
+ out[rOff + 1] = packed >>> 8 & 255;
2447
+ out[rOff + 2] = packed >>> 16 & 255;
2448
+ out[rOff + 3] = packed >>> 24 & 255;
2449
+ }
2450
+ } else {
2451
+ for (let i = 0; i < N; i++) {
2452
+ const rOff = rotStart + i * 3;
2453
+ const x = data.rotations[i * 4];
2454
+ const y = data.rotations[i * 4 + 1];
2455
+ const z = data.rotations[i * 4 + 2];
2456
+ out[rOff] = Math.round(Math.max(0, Math.min(255, (x + 1) * 127.5)));
2457
+ out[rOff + 1] = Math.round(Math.max(0, Math.min(255, (y + 1) * 127.5)));
2458
+ out[rOff + 2] = Math.round(Math.max(0, Math.min(255, (z + 1) * 127.5)));
2459
+ }
2460
+ }
2461
+ if (shDim > 0 && data.shCoefficients) {
2462
+ const shStartOffset = rotStart + N * rotBytes;
2463
+ for (let i = 0; i < N; i++) {
2464
+ for (let s = 0; s < shDim; s++) {
2465
+ const off = shStartOffset + (i * shDim + s) * 3;
2466
+ for (let c = 0; c < 3; c++) {
2467
+ const coeff = data.shCoefficients[(i * shDim + s) * 3 + c];
2468
+ const scaled = coeff / SPZ_COLOR_SCALE * 128;
2469
+ const clamped = Math.round(Math.max(-128, Math.min(127, scaled)));
2470
+ out[off + c] = clamped < 0 ? clamped + 256 : clamped;
2471
+ }
2472
+ }
2473
+ }
2474
+ }
2475
+ const compressed = await compressGzip(buffer);
2476
+ const metadata = {
2477
+ version: encodingVersion,
2478
+ gaussianCount: N,
2479
+ shDegree,
2480
+ compressedSizeBytes: compressed.byteLength,
2481
+ uncompressedSizeBytes: payloadSize,
2482
+ compressionRatio: compressed.byteLength / payloadSize,
2483
+ antialiased
2484
+ };
2485
+ const encoded = {
2486
+ data: compressed,
2487
+ codecId: this.codecId,
2488
+ metadata
2489
+ };
2490
+ return {
2491
+ data: encoded,
2492
+ durationMs: performance.now() - startTime,
2493
+ warnings
2494
+ };
2495
+ }
2496
+ // ─── Stream Decode ────────────────────────────────────────────────────────
2497
+ async *stream(source, options) {
2498
+ const signal = options?.signal;
2499
+ let buffer;
2500
+ if (typeof source === "string") {
2501
+ const response = await fetch(source, { signal });
2502
+ if (!response.ok) {
2503
+ throw new CodecDecodeError(this.codecId, `HTTP ${response.status}: ${response.statusText}`);
2504
+ }
2505
+ const contentLength = parseInt(response.headers.get("content-length") ?? "0", 10);
2506
+ if (!response.body) {
2507
+ buffer = await response.arrayBuffer();
2508
+ options?.onProgress?.({
2509
+ bytesLoaded: buffer.byteLength,
2510
+ bytesTotal: buffer.byteLength,
2511
+ gaussiansDecoded: 0,
2512
+ gaussiansTotal: -1,
2513
+ phase: "downloading"
2514
+ });
2515
+ } else {
2516
+ const reader = response.body.getReader();
2517
+ const chunks = [];
2518
+ let loaded = 0;
2519
+ while (true) {
2520
+ if (signal?.aborted) {
2521
+ reader.cancel();
2522
+ return;
2523
+ }
2524
+ const { done, value } = await reader.read();
2525
+ if (done) break;
2526
+ chunks.push(value);
2527
+ loaded += value.byteLength;
2528
+ options?.onProgress?.({
2529
+ bytesLoaded: loaded,
2530
+ bytesTotal: contentLength || -1,
2531
+ gaussiansDecoded: 0,
2532
+ gaussiansTotal: -1,
2533
+ phase: "downloading"
2534
+ });
2535
+ }
2536
+ const result = new Uint8Array(loaded);
2537
+ let offset = 0;
2538
+ for (const chunk of chunks) {
2539
+ result.set(chunk, offset);
2540
+ offset += chunk.byteLength;
2541
+ }
2542
+ buffer = result.buffer;
2543
+ }
2544
+ } else {
2545
+ const reader = source.getReader();
2546
+ const chunks = [];
2547
+ let loaded = 0;
2548
+ while (true) {
2549
+ const { done, value } = await reader.read();
2550
+ if (done) break;
2551
+ chunks.push(value);
2552
+ loaded += value.byteLength;
2553
+ }
2554
+ const result = new Uint8Array(loaded);
2555
+ let offset = 0;
2556
+ for (const chunk of chunks) {
2557
+ result.set(chunk, offset);
2558
+ offset += chunk.byteLength;
2559
+ }
2560
+ buffer = result.buffer;
2561
+ }
2562
+ options?.onProgress?.({
2563
+ bytesLoaded: buffer.byteLength,
2564
+ bytesTotal: buffer.byteLength,
2565
+ gaussiansDecoded: 0,
2566
+ gaussiansTotal: -1,
2567
+ phase: "decompressing"
2568
+ });
2569
+ const decoded = await this.decode(buffer, options);
2570
+ options?.onProgress?.({
2571
+ bytesLoaded: buffer.byteLength,
2572
+ bytesTotal: buffer.byteLength,
2573
+ gaussiansDecoded: decoded.data.count,
2574
+ gaussiansTotal: decoded.data.count,
2575
+ phase: "complete"
2576
+ });
2577
+ yield decoded;
2578
+ }
2579
+ };
2580
+ async function decompressGzip(compressed) {
2581
+ if (typeof DecompressionStream !== "undefined") {
2582
+ const ds = new DecompressionStream("gzip");
2583
+ const writer = ds.writable.getWriter();
2584
+ const reader = ds.readable.getReader();
2585
+ writer.write(new Uint8Array(compressed));
2586
+ writer.close();
2587
+ const chunks = [];
2588
+ let totalLength = 0;
2589
+ while (true) {
2590
+ const { done, value } = await reader.read();
2591
+ if (done) break;
2592
+ chunks.push(value);
2593
+ totalLength += value.byteLength;
2594
+ }
2595
+ const result = new Uint8Array(totalLength);
2596
+ let offset = 0;
2597
+ for (const chunk of chunks) {
2598
+ result.set(chunk, offset);
2599
+ offset += chunk.byteLength;
2600
+ }
2601
+ return result.buffer;
2602
+ }
2603
+ const _global = globalThis;
2604
+ if (typeof globalThis !== "undefined" && _global.pako) {
2605
+ const pako = _global.pako;
2606
+ const decompressed = pako.inflate(new Uint8Array(compressed));
2607
+ return decompressed.buffer;
2608
+ }
2609
+ throw new Error(
2610
+ "SPZ decompression requires DecompressionStream API (modern browsers) or pako library."
2611
+ );
2612
+ }
2613
+ async function compressGzip(data) {
2614
+ if (typeof CompressionStream !== "undefined") {
2615
+ const cs = new CompressionStream("gzip");
2616
+ const writer = cs.writable.getWriter();
2617
+ const reader = cs.readable.getReader();
2618
+ writer.write(new Uint8Array(data));
2619
+ writer.close();
2620
+ const chunks = [];
2621
+ let totalLength = 0;
2622
+ while (true) {
2623
+ const { done, value } = await reader.read();
2624
+ if (done) break;
2625
+ chunks.push(value);
2626
+ totalLength += value.byteLength;
2627
+ }
2628
+ const result = new Uint8Array(totalLength);
2629
+ let offset = 0;
2630
+ for (const chunk of chunks) {
2631
+ result.set(chunk, offset);
2632
+ offset += chunk.byteLength;
2633
+ }
2634
+ return result.buffer;
2635
+ }
2636
+ throw new Error("SPZ encoding requires CompressionStream API (modern browsers).");
2637
+ }
2638
+ function parseSpzHeader(view) {
2639
+ return {
2640
+ magic: view.getUint32(0, true),
2641
+ version: view.getUint32(4, true),
2642
+ numPoints: view.getUint32(8, true),
2643
+ shDegree: view.getUint8(12),
2644
+ fractionalBits: view.getUint8(13),
2645
+ flags: view.getUint8(14),
2646
+ reserved: view.getUint8(15)
2647
+ };
2648
+ }
2649
+ function validateSpzHeader(header, codecId) {
2650
+ if (header.magic !== SPZ_MAGIC) {
2651
+ throw new CodecDecodeError(
2652
+ codecId,
2653
+ `Invalid SPZ magic: 0x${header.magic.toString(16).toUpperCase()}, expected 0x${SPZ_MAGIC.toString(16).toUpperCase()} ("NGSP")`
2654
+ );
2655
+ }
2656
+ if (header.version < 1 || header.version > 3) {
2657
+ throw new CodecDecodeError(
2658
+ codecId,
2659
+ `Unsupported SPZ version: ${header.version} (supported: 1-3)`
2660
+ );
2661
+ }
2662
+ if (header.numPoints > SPZ_MAX_POINTS) {
2663
+ throw new CodecDecodeError(
2664
+ codecId,
2665
+ `SPZ file contains ${header.numPoints.toLocaleString()} points, exceeding maximum of ${SPZ_MAX_POINTS.toLocaleString()}`
2666
+ );
2667
+ }
2668
+ if (header.shDegree > 3) {
2669
+ throw new CodecDecodeError(codecId, `Invalid SPZ SH degree: ${header.shDegree} (max 3)`);
2670
+ }
2671
+ }
2672
+ function shDimForDegree(degree) {
2673
+ switch (degree) {
2674
+ case 0:
2675
+ return 0;
2676
+ case 1:
2677
+ return 3;
2678
+ case 2:
2679
+ return 8;
2680
+ case 3:
2681
+ return 15;
2682
+ default:
2683
+ return 0;
2684
+ }
2685
+ }
2686
+ function decodeQuaternionV2(data, offset) {
2687
+ const x = data[offset] / 127.5 - 1;
2688
+ const y = data[offset + 1] / 127.5 - 1;
2689
+ const z = data[offset + 2] / 127.5 - 1;
2690
+ const w = Math.sqrt(Math.max(0, 1 - x * x - y * y - z * z));
2691
+ return [x, y, z, w];
2692
+ }
2693
+ function decodeQuaternionV3(data, offset) {
2694
+ const comp = data[offset] | data[offset + 1] << 8 | data[offset + 2] << 16 | data[offset + 3] << 24;
2695
+ const iLargest = comp >>> 30 & 3;
2696
+ const MASK_9 = (1 << 9) - 1;
2697
+ const quat = [0, 0, 0, 0];
2698
+ let sumSquares = 0;
2699
+ let bitPos = 0;
2700
+ for (let i = 0; i < 4; i++) {
2701
+ if (i === iLargest) continue;
2702
+ const mag = comp >>> bitPos & MASK_9;
2703
+ const negBit = comp >>> bitPos + 9 & 1;
2704
+ bitPos += 10;
2705
+ let value = SQRT1_2 * mag / MASK_9;
2706
+ if (negBit === 1) value = -value;
2707
+ quat[i] = value;
2708
+ sumSquares += value * value;
2709
+ }
2710
+ quat[iLargest] = Math.sqrt(Math.max(0, 1 - sumSquares));
2711
+ return quat;
2712
+ }
2713
+ function encodeQuaternionV3(x, y, z, w) {
2714
+ const len = Math.sqrt(x * x + y * y + z * z + w * w);
2715
+ if (len > 0) {
2716
+ const invLen = 1 / len;
2717
+ x *= invLen;
2718
+ y *= invLen;
2719
+ z *= invLen;
2720
+ w *= invLen;
2721
+ } else {
2722
+ x = 0;
2723
+ y = 0;
2724
+ z = 0;
2725
+ w = 1;
2726
+ }
2727
+ const abs = [Math.abs(x), Math.abs(y), Math.abs(z), Math.abs(w)];
2728
+ let iLargest = 0;
2729
+ if (abs[1] > abs[iLargest]) iLargest = 1;
2730
+ if (abs[2] > abs[iLargest]) iLargest = 2;
2731
+ if (abs[3] > abs[iLargest]) iLargest = 3;
2732
+ const quat = [x, y, z, w];
2733
+ if (quat[iLargest] < 0) {
2734
+ quat[0] = -quat[0];
2735
+ quat[1] = -quat[1];
2736
+ quat[2] = -quat[2];
2737
+ quat[3] = -quat[3];
2738
+ }
2739
+ const MASK_9 = (1 << 9) - 1;
2740
+ let packed = 0;
2741
+ let bitPos = 0;
2742
+ for (let i = 0; i < 4; i++) {
2743
+ if (i === iLargest) continue;
2744
+ const value = quat[i];
2745
+ const negBit = value < 0 ? 1 : 0;
2746
+ const mag = Math.min(MASK_9, Math.round(Math.abs(value) / SQRT1_2 * MASK_9));
2747
+ packed |= mag << bitPos;
2748
+ packed |= negBit << bitPos + 9;
2749
+ bitPos += 10;
2750
+ }
2751
+ packed |= iLargest << 30;
2752
+ return packed >>> 0;
2753
+ }
2754
+
2755
+ // src/gpu/codecs/GltfGaussianSplatCodec.ts
2756
+ var GLB_MAGIC = 1179937895;
2757
+ var GLB_HEADER_SIZE = 12;
2758
+ var GLB_CHUNK_HEADER_SIZE = 8;
2759
+ var GLB_CHUNK_TYPE_JSON = 1313821514;
2760
+ var GLB_CHUNK_TYPE_BIN = 5130562;
2761
+ var EXT_GAUSSIAN_SPLATTING = "KHR_gaussian_splatting";
2762
+ var EXT_GAUSSIAN_SPLATTING_COMPRESSION_SPZ = "KHR_gaussian_splatting_compression_spz";
2763
+ var MAX_GAUSSIAN_COUNT = 1e7;
2764
+ var DEFAULT_MAX_MEMORY_MB2 = 512;
2765
+ var SH_C02 = 0.2820947917738781;
2766
+ var SH_DC_BIAS = 0.5;
2767
+ var GLTF_TYPE_SIZES = {
2768
+ SCALAR: 1,
2769
+ VEC2: 2,
2770
+ VEC3: 3,
2771
+ VEC4: 4,
2772
+ MAT2: 4,
2773
+ MAT3: 9,
2774
+ MAT4: 16
2775
+ };
2776
+ var GLTF_COMPONENT_SIZES = {
2777
+ [5120 /* BYTE */]: 1,
2778
+ [5121 /* UNSIGNED_BYTE */]: 1,
2779
+ [5122 /* SHORT */]: 2,
2780
+ [5123 /* UNSIGNED_SHORT */]: 2,
2781
+ [5125 /* UNSIGNED_INT */]: 4,
2782
+ [5126 /* FLOAT */]: 4
2783
+ };
2784
+ function linearToSrgb(linear) {
2785
+ return linear <= 31308e-7 ? linear * 12.92 : 1.055 * Math.pow(linear, 1 / 2.4) - 0.055;
2786
+ }
2787
+ var GltfGaussianSplatCodec = class extends AbstractGaussianCodec {
2788
+ codecId;
2789
+ spzCodec;
2790
+ constructor() {
2791
+ super();
2792
+ this.codecId = "khr.gltf.gaussian";
2793
+ this.spzCodec = new SpzCodec();
2794
+ }
2795
+ // ─── Capabilities ─────────────────────────────────────────────────────────
2796
+ getCapabilities() {
2797
+ return {
2798
+ id: this.codecId,
2799
+ name: "glTF Gaussian Splat Codec (KHR_gaussian_splatting)",
2800
+ version: "1.0.0",
2801
+ fileExtensions: ["glb", "gltf"],
2802
+ mimeTypes: ["model/gltf-binary", "model/gltf+json"],
2803
+ canEncode: false,
2804
+ canDecode: true,
2805
+ canStream: false,
2806
+ canDecodeTemporal: false,
2807
+ maxSHDegree: 3,
2808
+ maxGaussianCount: MAX_GAUSSIAN_COUNT,
2809
+ requiresWasm: false,
2810
+ requiresWebGPU: false,
2811
+ standard: "khronos",
2812
+ maturity: "beta"
2813
+ };
2814
+ }
2815
+ // ─── Probe ────────────────────────────────────────────────────────────────
2816
+ /**
2817
+ * Check if a buffer contains a GLB file by inspecting the magic bytes,
2818
+ * or if it starts with a JSON object (potential .gltf).
2819
+ */
2820
+ canDecode(buffer) {
2821
+ if (buffer.byteLength < 4) return false;
2822
+ const view = new DataView(buffer);
2823
+ if (view.getUint32(0, true) === GLB_MAGIC) {
2824
+ return true;
2825
+ }
2826
+ const firstByte = new Uint8Array(buffer, 0, 1)[0];
2827
+ if (firstByte === 123) {
2828
+ try {
2829
+ const decoder = new TextDecoder("utf-8");
2830
+ const preview = decoder.decode(buffer.slice(0, Math.min(buffer.byteLength, 4096)));
2831
+ return preview.includes(EXT_GAUSSIAN_SPLATTING);
2832
+ } catch {
2833
+ return false;
2834
+ }
2835
+ }
2836
+ return false;
2837
+ }
2838
+ // ─── Extract Metadata ─────────────────────────────────────────────────────
2839
+ async extractMetadata(buffer) {
2840
+ const parsed = this.parseGltfContainer(buffer);
2841
+ const gltf = parsed.json;
2842
+ const { primitive, gaussianExt, spzExt } = this.findGaussianPrimitive(gltf);
2843
+ let gaussianCount = 0;
2844
+ let shDegree = 0;
2845
+ if (spzExt) {
2846
+ const spzData = this.extractBufferViewData(parsed, spzExt.bufferView);
2847
+ const spzMeta = await this.spzCodec.extractMetadata(spzData);
2848
+ gaussianCount = spzMeta.gaussianCount;
2849
+ shDegree = spzMeta.shDegree;
2850
+ } else {
2851
+ const posAccessorIndex = primitive.attributes["POSITION"];
2852
+ if (posAccessorIndex !== void 0 && gltf.accessors) {
2853
+ gaussianCount = gltf.accessors[posAccessorIndex].count;
2854
+ }
2855
+ shDegree = this.detectShDegree(primitive);
2856
+ }
2857
+ return {
2858
+ version: 1,
2859
+ gaussianCount,
2860
+ shDegree,
2861
+ compressedSizeBytes: buffer.byteLength,
2862
+ uncompressedSizeBytes: gaussianCount * 60,
2863
+ // estimate
2864
+ compressionRatio: spzExt ? buffer.byteLength / (gaussianCount * 60) : 1,
2865
+ antialiased: false,
2866
+ extensions: {
2867
+ colorSpace: gaussianExt.colorSpace ?? "srgb_rec709_display",
2868
+ hasSpzCompression: !!spzExt
2869
+ }
2870
+ };
2871
+ }
2872
+ // ─── Decode ───────────────────────────────────────────────────────────────
2873
+ async decode(buffer, options) {
2874
+ const startTime = performance.now();
2875
+ const warnings = [];
2876
+ const maxGaussians = options?.maxGaussians ?? MAX_GAUSSIAN_COUNT;
2877
+ const maxMemoryMB = options?.maxMemoryMB ?? DEFAULT_MAX_MEMORY_MB2;
2878
+ const decodeSH = options?.decodeSH ?? true;
2879
+ const parsed = this.parseGltfContainer(buffer);
2880
+ const gltf = parsed.json;
2881
+ const { primitive, gaussianExt, spzExt } = this.findGaussianPrimitive(gltf);
2882
+ let result;
2883
+ if (spzExt) {
2884
+ warnings.push("Decoding via SPZ compression extension delegation");
2885
+ const spzData = this.extractBufferViewData(parsed, spzExt.bufferView);
2886
+ result = await this.spzCodec.decode(spzData, options);
2887
+ const colorSpace = gaussianExt.colorSpace ?? "srgb_rec709_display";
2888
+ if (colorSpace === "lin_rec709_display") {
2889
+ this.convertColorsLinearToSrgb(result.data.colors, result.data.count);
2890
+ warnings.push("Converted linear RGB to sRGB for display");
2891
+ }
2892
+ } else {
2893
+ result = this.decodeBaseline(
2894
+ parsed,
2895
+ gltf,
2896
+ primitive,
2897
+ gaussianExt,
2898
+ maxGaussians,
2899
+ maxMemoryMB,
2900
+ decodeSH,
2901
+ warnings
2902
+ );
2903
+ }
2904
+ const durationMs = performance.now() - startTime;
2905
+ return {
2906
+ data: result.data,
2907
+ durationMs,
2908
+ warnings: [...warnings, ...result.warnings]
2909
+ };
2910
+ }
2911
+ // ─── Lifecycle ──────────────────────────────────────────────────────────────
2912
+ async initialize() {
2913
+ await super.initialize();
2914
+ await this.spzCodec.initialize();
2915
+ }
2916
+ dispose() {
2917
+ this.spzCodec.dispose();
2918
+ super.dispose();
2919
+ }
2920
+ // ─── Private: glTF Container Parsing ────────────────────────────────────
2921
+ /**
2922
+ * Parse a glTF container, handling both GLB and JSON-only formats.
2923
+ *
2924
+ * @returns Parsed glTF JSON and optional binary buffer
2925
+ */
2926
+ parseGltfContainer(buffer) {
2927
+ const view = new DataView(buffer);
2928
+ if (buffer.byteLength >= GLB_HEADER_SIZE && view.getUint32(0, true) === GLB_MAGIC) {
2929
+ return this.parseGlb(buffer, view);
2930
+ }
2931
+ return this.parseGltfJson(buffer);
2932
+ }
2933
+ /**
2934
+ * Parse a GLB binary container.
2935
+ *
2936
+ * GLB layout:
2937
+ * [Header: 12 bytes] magic(4) + version(4) + length(4)
2938
+ * [JSON chunk: 8 + N bytes] chunkLength(4) + chunkType(4) + jsonData(N)
2939
+ * [BIN chunk: 8 + M bytes] chunkLength(4) + chunkType(4) + binData(M)
2940
+ */
2941
+ parseGlb(buffer, view) {
2942
+ const version = view.getUint32(4, true);
2943
+ const totalLength = view.getUint32(8, true);
2944
+ if (version < 2) {
2945
+ throw new CodecDecodeError(
2946
+ this.codecId,
2947
+ `Unsupported GLB version: ${version} (expected >= 2)`
2948
+ );
2949
+ }
2950
+ if (totalLength > buffer.byteLength) {
2951
+ throw new CodecDecodeError(
2952
+ this.codecId,
2953
+ `GLB header declares ${totalLength} bytes but buffer is only ${buffer.byteLength} bytes`
2954
+ );
2955
+ }
2956
+ let jsonChunk;
2957
+ let binChunk;
2958
+ let offset = GLB_HEADER_SIZE;
2959
+ while (offset < totalLength) {
2960
+ if (offset + GLB_CHUNK_HEADER_SIZE > totalLength) break;
2961
+ const chunkLength = view.getUint32(offset, true);
2962
+ const chunkType = view.getUint32(offset + 4, true);
2963
+ const chunkDataStart = offset + GLB_CHUNK_HEADER_SIZE;
2964
+ const chunkDataEnd = chunkDataStart + chunkLength;
2965
+ if (chunkDataEnd > totalLength) {
2966
+ throw new CodecDecodeError(
2967
+ this.codecId,
2968
+ `GLB chunk at offset ${offset} extends beyond file (${chunkDataEnd} > ${totalLength})`
2969
+ );
2970
+ }
2971
+ if (chunkType === GLB_CHUNK_TYPE_JSON) {
2972
+ const jsonBytes = new Uint8Array(buffer, chunkDataStart, chunkLength);
2973
+ const decoder = new TextDecoder("utf-8");
2974
+ const jsonText = decoder.decode(jsonBytes);
2975
+ try {
2976
+ jsonChunk = JSON.parse(jsonText);
2977
+ } catch (e) {
2978
+ throw new CodecDecodeError(
2979
+ this.codecId,
2980
+ "Failed to parse GLB JSON chunk",
2981
+ e instanceof Error ? e : void 0
2982
+ );
2983
+ }
2984
+ } else if (chunkType === GLB_CHUNK_TYPE_BIN) {
2985
+ binChunk = buffer.slice(chunkDataStart, chunkDataEnd);
2986
+ }
2987
+ offset = chunkDataEnd;
2988
+ while (offset % 4 !== 0 && offset < totalLength) offset++;
2989
+ }
2990
+ if (!jsonChunk) {
2991
+ throw new CodecDecodeError(this.codecId, "GLB file does not contain a JSON chunk");
2992
+ }
2993
+ return {
2994
+ json: jsonChunk,
2995
+ binaryChunks: binChunk ? [binChunk] : []
2996
+ };
2997
+ }
2998
+ /**
2999
+ * Parse a JSON-only glTF file.
3000
+ */
3001
+ parseGltfJson(buffer) {
3002
+ const decoder = new TextDecoder("utf-8");
3003
+ const jsonText = decoder.decode(buffer);
3004
+ let json;
3005
+ try {
3006
+ json = JSON.parse(jsonText);
3007
+ } catch (e) {
3008
+ throw new CodecDecodeError(
3009
+ this.codecId,
3010
+ "Failed to parse glTF JSON",
3011
+ e instanceof Error ? e : void 0
3012
+ );
3013
+ }
3014
+ return {
3015
+ json,
3016
+ binaryChunks: []
3017
+ };
3018
+ }
3019
+ // ─── Private: Gaussian Primitive Discovery ──────────────────────────────
3020
+ /**
3021
+ * Find the first mesh primitive that carries KHR_gaussian_splatting.
3022
+ *
3023
+ * @throws CodecDecodeError if no Gaussian splatting primitive is found
3024
+ */
3025
+ findGaussianPrimitive(gltf) {
3026
+ if (!gltf.meshes || gltf.meshes.length === 0) {
3027
+ throw new CodecDecodeError(this.codecId, "glTF file contains no meshes");
3028
+ }
3029
+ for (const mesh of gltf.meshes) {
3030
+ for (const primitive of mesh.primitives) {
3031
+ const gsExt = primitive.extensions?.[EXT_GAUSSIAN_SPLATTING];
3032
+ if (gsExt) {
3033
+ const spzExt = primitive.extensions?.[EXT_GAUSSIAN_SPLATTING_COMPRESSION_SPZ];
3034
+ return { primitive, gaussianExt: gsExt, spzExt };
3035
+ }
3036
+ }
3037
+ }
3038
+ throw new CodecDecodeError(
3039
+ this.codecId,
3040
+ `No mesh primitive with ${EXT_GAUSSIAN_SPLATTING} extension found in glTF file`
3041
+ );
3042
+ }
3043
+ // ─── Private: BufferView Data Extraction ────────────────────────────────
3044
+ /**
3045
+ * Extract raw bytes from a glTF bufferView.
3046
+ */
3047
+ extractBufferViewData(parsed, bufferViewIndex) {
3048
+ const gltf = parsed.json;
3049
+ if (!gltf.bufferViews || bufferViewIndex >= gltf.bufferViews.length) {
3050
+ throw new CodecDecodeError(
3051
+ this.codecId,
3052
+ `BufferView index ${bufferViewIndex} is out of range (${gltf.bufferViews?.length ?? 0} bufferViews)`
3053
+ );
3054
+ }
3055
+ const bv = gltf.bufferViews[bufferViewIndex];
3056
+ const bufferIndex = bv.buffer;
3057
+ const byteOffset = bv.byteOffset ?? 0;
3058
+ const byteLength = bv.byteLength;
3059
+ const bufferData = this.resolveBuffer(parsed, bufferIndex);
3060
+ if (byteOffset + byteLength > bufferData.byteLength) {
3061
+ throw new CodecDecodeError(
3062
+ this.codecId,
3063
+ `BufferView [${bufferViewIndex}] range (${byteOffset}..${byteOffset + byteLength}) exceeds buffer [${bufferIndex}] size (${bufferData.byteLength})`
3064
+ );
3065
+ }
3066
+ return bufferData.slice(byteOffset, byteOffset + byteLength);
3067
+ }
3068
+ /**
3069
+ * Resolve a glTF buffer index to its ArrayBuffer data.
3070
+ *
3071
+ * For GLB: buffer 0 is the BIN chunk.
3072
+ * For external URIs: not supported (would require async fetch).
3073
+ */
3074
+ resolveBuffer(parsed, bufferIndex) {
3075
+ if (bufferIndex === 0 && parsed.binaryChunks.length > 0) {
3076
+ return parsed.binaryChunks[0];
3077
+ }
3078
+ const gltf = parsed.json;
3079
+ if (gltf.buffers && bufferIndex < gltf.buffers.length) {
3080
+ const buffer = gltf.buffers[bufferIndex];
3081
+ if (buffer.uri && buffer.uri.startsWith("data:")) {
3082
+ return this.decodeDataUri(buffer.uri);
3083
+ }
3084
+ }
3085
+ throw new CodecDecodeError(
3086
+ this.codecId,
3087
+ `Cannot resolve buffer [${bufferIndex}]: external URI buffers are not supported in synchronous decode. Use GLB format or data URIs for embedded data.`
3088
+ );
3089
+ }
3090
+ /**
3091
+ * Decode a data URI to an ArrayBuffer.
3092
+ */
3093
+ decodeDataUri(uri) {
3094
+ const commaIndex = uri.indexOf(",");
3095
+ if (commaIndex === -1) {
3096
+ throw new CodecDecodeError(this.codecId, "Invalid data URI format");
3097
+ }
3098
+ const header = uri.substring(0, commaIndex);
3099
+ const data = uri.substring(commaIndex + 1);
3100
+ if (header.includes(";base64")) {
3101
+ const binary = atob(data);
3102
+ const bytes = new Uint8Array(binary.length);
3103
+ for (let i = 0; i < binary.length; i++) {
3104
+ bytes[i] = binary.charCodeAt(i);
3105
+ }
3106
+ return bytes.buffer;
3107
+ }
3108
+ const decoded = decodeURIComponent(data);
3109
+ const encoder = new TextEncoder();
3110
+ return encoder.encode(decoded).buffer;
3111
+ }
3112
+ // ─── Private: Accessor Data Reading ─────────────────────────────────────
3113
+ /**
3114
+ * Read accessor data as a Float32Array.
3115
+ *
3116
+ * Handles component type conversion and normalization for:
3117
+ * - FLOAT: Direct read
3118
+ * - BYTE/SHORT (normalized): Scale to [-1, 1]
3119
+ * - UNSIGNED_BYTE/UNSIGNED_SHORT (normalized): Scale to [0, 1]
3120
+ * - BYTE/SHORT/UNSIGNED_BYTE/UNSIGNED_SHORT (non-normalized): Direct int-to-float
3121
+ */
3122
+ readAccessorFloat32(parsed, accessorIndex) {
3123
+ const gltf = parsed.json;
3124
+ if (!gltf.accessors || accessorIndex >= gltf.accessors.length) {
3125
+ throw new CodecDecodeError(this.codecId, `Accessor index ${accessorIndex} is out of range`);
3126
+ }
3127
+ const accessor = gltf.accessors[accessorIndex];
3128
+ const componentCount = GLTF_TYPE_SIZES[accessor.type] ?? 1;
3129
+ const totalElements = accessor.count * componentCount;
3130
+ const result = new Float32Array(totalElements);
3131
+ if (accessor.bufferView === void 0) {
3132
+ return result;
3133
+ }
3134
+ const bv = gltf.bufferViews[accessor.bufferView];
3135
+ const bufferData = this.resolveBuffer(parsed, bv.buffer);
3136
+ const byteOffset = (bv.byteOffset ?? 0) + (accessor.byteOffset ?? 0);
3137
+ const componentSize = GLTF_COMPONENT_SIZES[accessor.componentType] ?? 4;
3138
+ const stride = bv.byteStride ?? componentCount * componentSize;
3139
+ const dataView = new DataView(bufferData);
3140
+ for (let i = 0; i < accessor.count; i++) {
3141
+ const elementOffset = byteOffset + i * stride;
3142
+ for (let c = 0; c < componentCount; c++) {
3143
+ const compOffset = elementOffset + c * componentSize;
3144
+ let value;
3145
+ switch (accessor.componentType) {
3146
+ case 5126 /* FLOAT */:
3147
+ value = dataView.getFloat32(compOffset, true);
3148
+ break;
3149
+ case 5120 /* BYTE */:
3150
+ value = dataView.getInt8(compOffset);
3151
+ if (accessor.normalized) value = Math.max(value / 127, -1);
3152
+ break;
3153
+ case 5121 /* UNSIGNED_BYTE */:
3154
+ value = dataView.getUint8(compOffset);
3155
+ if (accessor.normalized) value = value / 255;
3156
+ break;
3157
+ case 5122 /* SHORT */:
3158
+ value = dataView.getInt16(compOffset, true);
3159
+ if (accessor.normalized) value = Math.max(value / 32767, -1);
3160
+ break;
3161
+ case 5123 /* UNSIGNED_SHORT */:
3162
+ value = dataView.getUint16(compOffset, true);
3163
+ if (accessor.normalized) value = value / 65535;
3164
+ break;
3165
+ case 5125 /* UNSIGNED_INT */:
3166
+ value = dataView.getUint32(compOffset, true);
3167
+ break;
3168
+ default:
3169
+ value = 0;
3170
+ }
3171
+ result[i * componentCount + c] = value;
3172
+ }
3173
+ }
3174
+ return result;
3175
+ }
3176
+ // ─── Private: Baseline Decode ─────────────────────────────────────────────
3177
+ /**
3178
+ * Decode uncompressed KHR_gaussian_splatting attributes from glTF accessors.
3179
+ */
3180
+ decodeBaseline(parsed, gltf, primitive, gaussianExt, maxGaussians, maxMemoryMB, decodeSH, warnings) {
3181
+ const attrs = primitive.attributes;
3182
+ const posIndex = attrs["POSITION"];
3183
+ if (posIndex === void 0) {
3184
+ throw new CodecDecodeError(
3185
+ this.codecId,
3186
+ "Missing required POSITION attribute in Gaussian splatting primitive"
3187
+ );
3188
+ }
3189
+ const posAccessor = gltf.accessors[posIndex];
3190
+ const totalCount = posAccessor.count;
3191
+ const N = Math.min(totalCount, maxGaussians);
3192
+ if (N < totalCount) {
3193
+ warnings.push(
3194
+ `Clamped Gaussian count from ${totalCount.toLocaleString()} to ${N.toLocaleString()} (maxGaussians limit)`
3195
+ );
3196
+ }
3197
+ this.checkMemoryBudget(N, maxMemoryMB);
3198
+ const rawPositions = this.readAccessorFloat32(parsed, posIndex);
3199
+ const positions = N < totalCount ? rawPositions.slice(0, N * 3) : rawPositions;
3200
+ const rotIndex = attrs["KHR_gaussian_splatting:ROTATION"] ?? attrs["_ROTATION"];
3201
+ let rotations;
3202
+ if (rotIndex !== void 0) {
3203
+ const rawRotations = this.readAccessorFloat32(parsed, rotIndex);
3204
+ rotations = N < totalCount ? rawRotations.slice(0, N * 4) : rawRotations;
3205
+ } else {
3206
+ warnings.push("Missing ROTATION attribute; using identity quaternions");
3207
+ rotations = new Float32Array(N * 4);
3208
+ for (let i = 0; i < N; i++) {
3209
+ rotations[i * 4 + 3] = 1;
3210
+ }
3211
+ }
3212
+ const scaleIndex = attrs["KHR_gaussian_splatting:SCALE"] ?? attrs["_SCALE"];
3213
+ let scales;
3214
+ if (scaleIndex !== void 0) {
3215
+ const rawScales = this.readAccessorFloat32(parsed, scaleIndex);
3216
+ scales = new Float32Array(N * 3);
3217
+ const count = Math.min(rawScales.length, N * 3);
3218
+ for (let i = 0; i < count; i++) {
3219
+ scales[i] = Math.exp(rawScales[i]);
3220
+ }
3221
+ } else {
3222
+ warnings.push("Missing SCALE attribute; using uniform scale 0.01");
3223
+ scales = new Float32Array(N * 3);
3224
+ scales.fill(0.01);
3225
+ }
3226
+ const opacityIndex = attrs["KHR_gaussian_splatting:OPACITY"] ?? attrs["_OPACITY"];
3227
+ let opacities;
3228
+ if (opacityIndex !== void 0) {
3229
+ const rawOpacities = this.readAccessorFloat32(parsed, opacityIndex);
3230
+ opacities = N < totalCount ? rawOpacities.slice(0, N) : rawOpacities;
3231
+ } else {
3232
+ warnings.push("Missing OPACITY attribute; using full opacity (1.0)");
3233
+ opacities = new Float32Array(N);
3234
+ opacities.fill(1);
3235
+ }
3236
+ const colors = new Float32Array(N * 4);
3237
+ const colorIndex = attrs["COLOR_0"];
3238
+ const shDc0Index = attrs["KHR_gaussian_splatting:SH_DEGREE_0_COEF_0"] ?? attrs["_SH_DEGREE_0_COEF_0"];
3239
+ if (colorIndex !== void 0) {
3240
+ const rawColors = this.readAccessorFloat32(parsed, colorIndex);
3241
+ const colorAccessor = gltf.accessors[colorIndex];
3242
+ const colorComponents = GLTF_TYPE_SIZES[colorAccessor.type] ?? 4;
3243
+ for (let i = 0; i < N; i++) {
3244
+ if (colorComponents >= 3) {
3245
+ colors[i * 4] = rawColors[i * colorComponents];
3246
+ colors[i * 4 + 1] = rawColors[i * colorComponents + 1];
3247
+ colors[i * 4 + 2] = rawColors[i * colorComponents + 2];
3248
+ }
3249
+ colors[i * 4 + 3] = colorComponents >= 4 ? rawColors[i * colorComponents + 3] : opacities[i];
3250
+ }
3251
+ } else if (shDc0Index !== void 0) {
3252
+ const rawSh0 = this.readAccessorFloat32(parsed, shDc0Index);
3253
+ for (let i = 0; i < N; i++) {
3254
+ for (let c = 0; c < 3; c++) {
3255
+ const shCoeff = rawSh0[i * 3 + c];
3256
+ colors[i * 4 + c] = Math.max(0, Math.min(1, SH_C02 * shCoeff + SH_DC_BIAS));
3257
+ }
3258
+ colors[i * 4 + 3] = opacities[i];
3259
+ }
3260
+ } else {
3261
+ warnings.push("No color or SH degree 0 data found; using mid-gray");
3262
+ for (let i = 0; i < N; i++) {
3263
+ colors[i * 4] = 0.5;
3264
+ colors[i * 4 + 1] = 0.5;
3265
+ colors[i * 4 + 2] = 0.5;
3266
+ colors[i * 4 + 3] = opacities[i];
3267
+ }
3268
+ }
3269
+ const colorSpace = gaussianExt.colorSpace ?? "srgb_rec709_display";
3270
+ if (colorSpace === "lin_rec709_display") {
3271
+ this.convertColorsLinearToSrgb(colors, N);
3272
+ warnings.push("Converted linear RGB to sRGB for display");
3273
+ }
3274
+ let shCoefficients;
3275
+ let shDegree = 0;
3276
+ if (decodeSH) {
3277
+ shDegree = this.detectShDegree(primitive);
3278
+ if (shDegree > 0) {
3279
+ const shDim = shDimForDegree2(shDegree);
3280
+ shCoefficients = new Float32Array(N * shDim * 3);
3281
+ let shOffset = 0;
3282
+ for (let d = 1; d <= shDegree; d++) {
3283
+ const coeffsPerDegree = 2 * d + 1;
3284
+ for (let coef = 0; coef < coeffsPerDegree; coef++) {
3285
+ const attrName = attrs[`KHR_gaussian_splatting:SH_DEGREE_${d}_COEF_${coef}`] ?? attrs[`_SH_DEGREE_${d}_COEF_${coef}`];
3286
+ if (attrName !== void 0) {
3287
+ const rawSh = this.readAccessorFloat32(parsed, attrName);
3288
+ for (let i = 0; i < N; i++) {
3289
+ for (let c = 0; c < 3; c++) {
3290
+ shCoefficients[(i * shDim + shOffset) * 3 + c] = rawSh[i * 3 + c];
3291
+ }
3292
+ }
3293
+ }
3294
+ shOffset++;
3295
+ }
3296
+ }
3297
+ }
3298
+ }
3299
+ const data = {
3300
+ positions,
3301
+ scales,
3302
+ rotations,
3303
+ colors,
3304
+ opacities,
3305
+ shCoefficients,
3306
+ shDegree,
3307
+ count: N
3308
+ };
3309
+ return {
3310
+ data,
3311
+ durationMs: 0,
3312
+ // Will be overridden by caller
3313
+ warnings
3314
+ };
3315
+ }
3316
+ // ─── Private: SH Degree Detection ─────────────────────────────────────────
3317
+ /**
3318
+ * Detect the highest SH degree present in a primitive's attributes.
3319
+ */
3320
+ detectShDegree(primitive) {
3321
+ const attrs = primitive.attributes;
3322
+ let degree = 0;
3323
+ if (attrs["KHR_gaussian_splatting:SH_DEGREE_3_COEF_0"] !== void 0 || attrs["_SH_DEGREE_3_COEF_0"] !== void 0) {
3324
+ degree = 3;
3325
+ } else if (attrs["KHR_gaussian_splatting:SH_DEGREE_2_COEF_0"] !== void 0 || attrs["_SH_DEGREE_2_COEF_0"] !== void 0) {
3326
+ degree = 2;
3327
+ } else if (attrs["KHR_gaussian_splatting:SH_DEGREE_1_COEF_0"] !== void 0 || attrs["_SH_DEGREE_1_COEF_0"] !== void 0) {
3328
+ degree = 1;
3329
+ }
3330
+ return degree;
3331
+ }
3332
+ // ─── Private: Color Space Conversion ──────────────────────────────────────
3333
+ /**
3334
+ * Convert color array from linear RGB to sRGB in-place.
3335
+ */
3336
+ convertColorsLinearToSrgb(colors, count) {
3337
+ for (let i = 0; i < count; i++) {
3338
+ colors[i * 4] = linearToSrgb(Math.max(0, Math.min(1, colors[i * 4])));
3339
+ colors[i * 4 + 1] = linearToSrgb(Math.max(0, Math.min(1, colors[i * 4 + 1])));
3340
+ colors[i * 4 + 2] = linearToSrgb(Math.max(0, Math.min(1, colors[i * 4 + 2])));
3341
+ }
3342
+ }
3343
+ };
3344
+ function shDimForDegree2(degree) {
3345
+ switch (degree) {
3346
+ case 0:
3347
+ return 0;
3348
+ case 1:
3349
+ return 3;
3350
+ case 2:
3351
+ return 8;
3352
+ case 3:
3353
+ return 15;
3354
+ default:
3355
+ return 0;
3356
+ }
3357
+ }
3358
+
3359
+ // src/gpu/codecs/MpegGscCodec.ts
3360
+ var MPEG_GSC_MAGIC = 1296520003;
3361
+ var MpegGscCodec = class extends AbstractGaussianCodec {
3362
+ codecId = "mpeg.gsc.v1";
3363
+ // ─── Capabilities ─────────────────────────────────────────────────────────
3364
+ getCapabilities() {
3365
+ return {
3366
+ id: this.codecId,
3367
+ name: "MPEG Gaussian Splat Coding (Stub)",
3368
+ version: "0.1.0-stub",
3369
+ fileExtensions: [],
3370
+ // TBD when standard is finalized
3371
+ mimeTypes: [],
3372
+ // TBD when standard is finalized
3373
+ canEncode: false,
3374
+ canDecode: false,
3375
+ canStream: false,
3376
+ canDecodeTemporal: false,
3377
+ maxSHDegree: 3,
3378
+ maxGaussianCount: -1,
3379
+ // Unlimited (TBD)
3380
+ requiresWasm: true,
3381
+ // Likely will require WASM for HEVC decode
3382
+ requiresWebGPU: false,
3383
+ standard: "mpeg",
3384
+ maturity: "stub"
3385
+ };
3386
+ }
3387
+ // ─── Probe ────────────────────────────────────────────────────────────────
3388
+ canDecode(buffer) {
3389
+ if (buffer.byteLength < 4) return false;
3390
+ const view = new DataView(buffer);
3391
+ return view.getUint32(0, true) === MPEG_GSC_MAGIC;
3392
+ }
3393
+ // ─── Extract Metadata (Stub) ──────────────────────────────────────────────
3394
+ async extractMetadata(_buffer) {
3395
+ throw new CodecNotSupportedError(
3396
+ this.codecId,
3397
+ "extractMetadata (MPEG GSC standard is not yet finalized)"
3398
+ );
3399
+ }
3400
+ // ─── Decode (Stub) ────────────────────────────────────────────────────────
3401
+ async decode(_buffer, _options) {
3402
+ throw new CodecNotSupportedError(
3403
+ this.codecId,
3404
+ "decode (MPEG GSC standard is not yet finalized - currently in MPEG Exploration phase as of 2026-03-01)"
3405
+ );
3406
+ }
3407
+ // ─── Decompress (Stub) ────────────────────────────────────────────────────
3408
+ async decompress(_compressed) {
3409
+ throw new CodecNotSupportedError(
3410
+ this.codecId,
3411
+ "decompress (MPEG GSC standard is not yet finalized)"
3412
+ );
3413
+ }
3414
+ // ─── Informational Methods ────────────────────────────────────────────────
3415
+ /**
3416
+ * Get the current standardization status of MPEG GSC.
3417
+ *
3418
+ * This method is specific to the MPEG stub and provides context about
3419
+ * when the full implementation can be expected.
3420
+ */
3421
+ getStandardizationStatus() {
3422
+ return {
3423
+ phase: "exploration",
3424
+ workingGroups: ["WG4", "WG5", "WG7"],
3425
+ lastMeetingDate: "2026-01-23",
3426
+ lastMeetingName: "41st JVET / 153rd MPEG",
3427
+ compressionApproaches: [
3428
+ "GPCC v1 (Geometry-based Point Cloud Compression)",
3429
+ "HEVC (High Efficiency Video Coding) for attribute maps",
3430
+ "Custom Gaussian-specific entropy coding"
3431
+ ],
3432
+ expectedTimeline: "TBD - no formal standardization kicked off",
3433
+ referenceUrl: "https://mpeg.expert/gsc/index.html"
3434
+ };
3435
+ }
3436
+ /**
3437
+ * Check if a newer version of the MPEG GSC codec is available.
3438
+ *
3439
+ * In the future, this could check a remote registry for codec updates.
3440
+ * For now, it always returns false since the standard is not finalized.
3441
+ */
3442
+ async checkForUpdates() {
3443
+ return false;
3444
+ }
3445
+ };
3446
+
3447
+ // src/gpu/codecs/GaussianCodecRegistry.ts
3448
+ var GaussianCodecRegistry = class {
3449
+ codecs = /* @__PURE__ */ new Map();
3450
+ // ─── Registration ─────────────────────────────────────────────────────────
3451
+ /**
3452
+ * Register a codec with the registry.
3453
+ *
3454
+ * @param codec - Codec instance to register
3455
+ * @param priority - Priority for codec selection (default: 0, higher = preferred)
3456
+ * @returns The registry instance (for chaining)
3457
+ */
3458
+ register(codec, priority = 0) {
3459
+ const capabilities = codec.getCapabilities();
3460
+ this.codecs.set(capabilities.id, {
3461
+ codec,
3462
+ capabilities,
3463
+ priority,
3464
+ initialized: false
3465
+ });
3466
+ return this;
3467
+ }
3468
+ /**
3469
+ * Unregister a codec by ID.
3470
+ *
3471
+ * @param codecId - ID of the codec to unregister
3472
+ * @returns true if the codec was found and removed
3473
+ */
3474
+ unregister(codecId) {
3475
+ const entry = this.codecs.get(codecId);
3476
+ if (entry) {
3477
+ entry.codec.dispose();
3478
+ this.codecs.delete(codecId);
3479
+ return true;
3480
+ }
3481
+ return false;
3482
+ }
3483
+ // ─── Codec Access ─────────────────────────────────────────────────────────
3484
+ /**
3485
+ * Get a specific codec by ID.
3486
+ *
3487
+ * @param codecId - Codec identifier
3488
+ * @returns The codec instance, or undefined if not registered
3489
+ */
3490
+ getCodec(codecId) {
3491
+ return this.codecs.get(codecId)?.codec;
3492
+ }
3493
+ /**
3494
+ * Get a specific codec by ID, throwing if not found.
3495
+ *
3496
+ * @param codecId - Codec identifier
3497
+ * @returns The codec instance
3498
+ * @throws Error if the codec is not registered
3499
+ */
3500
+ requireCodec(codecId) {
3501
+ const codec = this.getCodec(codecId);
3502
+ if (!codec) {
3503
+ throw new Error(
3504
+ `Codec '${codecId}' is not registered. Available codecs: ${this.getRegisteredIds().join(", ")}`
3505
+ );
3506
+ }
3507
+ return codec;
3508
+ }
3509
+ /**
3510
+ * Get all registered codec IDs.
3511
+ */
3512
+ getRegisteredIds() {
3513
+ return Array.from(this.codecs.keys());
3514
+ }
3515
+ /**
3516
+ * Get capabilities of all registered codecs.
3517
+ */
3518
+ getAllCapabilities() {
3519
+ return Array.from(this.codecs.values()).map((entry) => entry.capabilities);
3520
+ }
3521
+ /**
3522
+ * Check if a specific codec is registered.
3523
+ */
3524
+ hasCodec(codecId) {
3525
+ return this.codecs.has(codecId);
3526
+ }
3527
+ // ─── Auto-Detection ───────────────────────────────────────────────────────
3528
+ /**
3529
+ * Auto-detect the best codec for a given file.
3530
+ *
3531
+ * Detection priority:
3532
+ * 1. Explicit codecId in options (bypass detection)
3533
+ * 2. Magic byte detection from headerBytes
3534
+ * 3. File extension detection from URL
3535
+ * 4. Priority-based fallback among matching codecs
3536
+ *
3537
+ * @param options - Detection options (URL, header bytes, etc.)
3538
+ * @returns Best-matching codec, or undefined if no codec can handle the file
3539
+ */
3540
+ detectCodec(options) {
3541
+ if (options.codecId) {
3542
+ return this.getCodec(options.codecId);
3543
+ }
3544
+ const candidates = [];
3545
+ if (options.headerBytes) {
3546
+ for (const entry of this.codecs.values()) {
3547
+ if (this.matchesMaturity(entry, options.maturityFilter)) {
3548
+ if (entry.codec.canDecode(options.headerBytes)) {
3549
+ candidates.push(entry);
3550
+ }
3551
+ }
3552
+ }
3553
+ }
3554
+ if (candidates.length === 0 && options.url) {
3555
+ const ext = this.extractExtension(options.url);
3556
+ if (ext) {
3557
+ for (const entry of this.codecs.values()) {
3558
+ if (this.matchesMaturity(entry, options.maturityFilter)) {
3559
+ if (entry.capabilities.fileExtensions.includes(ext)) {
3560
+ candidates.push(entry);
3561
+ }
3562
+ }
3563
+ }
3564
+ }
3565
+ }
3566
+ if (candidates.length === 0) return void 0;
3567
+ candidates.sort((a, b) => b.priority - a.priority);
3568
+ return candidates[0].codec;
3569
+ }
3570
+ /**
3571
+ * Auto-detect and decode a buffer.
3572
+ *
3573
+ * Convenience method that combines detection and decode in one call.
3574
+ *
3575
+ * @param buffer - Raw binary data
3576
+ * @param options - Decode options + detection options
3577
+ * @returns Decoded Gaussian data
3578
+ * @throws Error if no codec can handle the data
3579
+ */
3580
+ async decode(buffer, options) {
3581
+ const codec = this.detectCodec({
3582
+ codecId: options?.codecId,
3583
+ headerBytes: buffer.slice(0, Math.min(buffer.byteLength, 64)),
3584
+ url: options?.url,
3585
+ maturityFilter: options?.maturityFilter
3586
+ });
3587
+ if (!codec) {
3588
+ throw new Error(
3589
+ `No codec found that can decode this data. Registered codecs: ${this.getRegisteredIds().join(", ")}. Ensure the correct codec is registered or specify codecId explicitly.`
3590
+ );
3591
+ }
3592
+ await this.ensureInitialized(codec);
3593
+ return codec.decode(buffer, options);
3594
+ }
3595
+ /**
3596
+ * Auto-detect codec from URL and decode via streaming.
3597
+ *
3598
+ * @param url - URL to fetch and decode
3599
+ * @param options - Decode and detection options
3600
+ * @returns Decoded Gaussian data
3601
+ */
3602
+ async decodeFromUrl(url, options) {
3603
+ const codec = this.detectCodec({
3604
+ codecId: options?.codecId,
3605
+ url,
3606
+ maturityFilter: options?.maturityFilter ?? ["production", "beta"]
3607
+ });
3608
+ if (!codec) {
3609
+ throw new Error(
3610
+ `No codec found for URL '${url}'. Registered codecs: ${this.getRegisteredIds().join(", ")}`
3611
+ );
3612
+ }
3613
+ await this.ensureInitialized(codec);
3614
+ const response = await fetch(url);
3615
+ if (!response.ok) {
3616
+ throw new Error(`HTTP ${response.status}: ${response.statusText} for ${url}`);
3617
+ }
3618
+ const buffer = await response.arrayBuffer();
3619
+ return codec.decode(buffer, options);
3620
+ }
3621
+ // ─── Lifecycle ────────────────────────────────────────────────────────────
3622
+ /**
3623
+ * Initialize all registered codecs.
3624
+ *
3625
+ * Useful for pre-warming at application startup.
3626
+ */
3627
+ async initializeAll() {
3628
+ const entries = Array.from(this.codecs.values());
3629
+ await Promise.all(
3630
+ entries.map(async (entry) => {
3631
+ if (!entry.initialized) {
3632
+ await entry.codec.initialize();
3633
+ entry.initialized = true;
3634
+ }
3635
+ })
3636
+ );
3637
+ }
3638
+ /**
3639
+ * Dispose all registered codecs and clear the registry.
3640
+ */
3641
+ disposeAll() {
3642
+ for (const entry of this.codecs.values()) {
3643
+ entry.codec.dispose();
3644
+ }
3645
+ this.codecs.clear();
3646
+ }
3647
+ // ─── Private Helpers ──────────────────────────────────────────────────────
3648
+ async ensureInitialized(codec) {
3649
+ const id = codec.getCapabilities().id;
3650
+ const entry = this.codecs.get(id);
3651
+ if (entry && !entry.initialized) {
3652
+ await codec.initialize();
3653
+ entry.initialized = true;
3654
+ }
3655
+ }
3656
+ extractExtension(url) {
3657
+ const clean = url.split(/[?#]/)[0];
3658
+ const ext = clean.split(".").pop()?.toLowerCase();
3659
+ return ext;
3660
+ }
3661
+ matchesMaturity(entry, filter) {
3662
+ if (!filter) return true;
3663
+ return filter.includes(entry.capabilities.maturity);
3664
+ }
3665
+ };
3666
+ function createDefaultCodecRegistry() {
3667
+ const registry = new GaussianCodecRegistry();
3668
+ registry.register(new SpzCodec(), 100);
3669
+ registry.register(new GltfGaussianSplatCodec(), 50);
3670
+ registry.register(new MpegGscCodec(), 0);
3671
+ return registry;
3672
+ }
3673
+ var globalRegistry = null;
3674
+ function getGlobalCodecRegistry() {
3675
+ if (!globalRegistry) {
3676
+ globalRegistry = createDefaultCodecRegistry();
3677
+ }
3678
+ return globalRegistry;
3679
+ }
3680
+ function resetGlobalCodecRegistry() {
3681
+ if (globalRegistry) {
3682
+ globalRegistry.disposeAll();
3683
+ globalRegistry = null;
3684
+ }
3685
+ }
3686
+ export {
3687
+ AbstractGaussianCodec,
3688
+ CodecDecodeError,
3689
+ CodecDecompressError,
3690
+ CodecEncodeError,
3691
+ CodecMemoryError,
3692
+ CodecNotSupportedError,
3693
+ ComputePipeline,
3694
+ GPUBufferManager,
3695
+ GaussianCodecError,
3696
+ GaussianCodecRegistry,
3697
+ GaussianSplatExtractor,
3698
+ GaussianSplatSorter,
3699
+ GltfGaussianSplatCodec,
3700
+ InstancedRenderer,
3701
+ MpegGscCodec,
3702
+ SparseLinearSolver,
3703
+ SpatialGrid,
3704
+ SpzCodec,
3705
+ WebGPUContext,
3706
+ createDefaultCodecRegistry,
3707
+ createGPUPhysicsSimulation,
3708
+ createGaussianSplatSorter,
3709
+ createInitialParticleData,
3710
+ createPhysicsSimulation,
3711
+ getGlobalCodecRegistry,
3712
+ getGlobalWebGPUContext,
3713
+ resetGlobalCodecRegistry
3714
+ };