@plasius/gpu-shared 0.1.13 → 0.1.14

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 (37) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +55 -3
  3. package/dist/{chunk-NCPJWLX3.js → chunk-2GM64LB6.js} +1 -9
  4. package/dist/{chunk-NCPJWLX3.js.map → chunk-2GM64LB6.js.map} +1 -1
  5. package/dist/{chunk-DABW627O.js → chunk-3ARPGHCQ.js} +8 -2
  6. package/dist/chunk-3ARPGHCQ.js.map +1 -0
  7. package/dist/chunk-4ZJ24VRS.js +402 -0
  8. package/dist/chunk-4ZJ24VRS.js.map +1 -0
  9. package/dist/{chunk-DQX4DXBR.js → chunk-W5GA3VA6.js} +79 -6
  10. package/dist/chunk-W5GA3VA6.js.map +1 -0
  11. package/dist/gltf-loader-YDPLZS5Q.js +8 -0
  12. package/dist/index.cjs +1230 -6198
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.js +31 -5
  15. package/dist/index.js.map +1 -1
  16. package/dist/product-studio-runtime-HDAUDWYO.js +11 -0
  17. package/dist/showcase-inline-assets-WT4PSNKI.js +7 -0
  18. package/dist/showcase-inline-assets-WT4PSNKI.js.map +1 -0
  19. package/dist/showcase-runtime-SNCUFSSC.js +3785 -0
  20. package/dist/showcase-runtime-SNCUFSSC.js.map +1 -0
  21. package/package.json +6 -8
  22. package/src/feature-flags.js +1 -0
  23. package/src/gltf-loader.js +10 -2
  24. package/src/index.d.ts +72 -1
  25. package/src/index.js +33 -0
  26. package/src/product-studio-runtime.js +465 -0
  27. package/src/showcase-runtime.js +875 -72
  28. package/dist/chunk-2FIFSBB4.js +0 -74
  29. package/dist/chunk-2FIFSBB4.js.map +0 -1
  30. package/dist/chunk-DABW627O.js.map +0 -1
  31. package/dist/chunk-DQX4DXBR.js.map +0 -1
  32. package/dist/gltf-loader-WAM23F37.js +0 -9
  33. package/dist/showcase-inline-assets-B7U7VX5H.js +0 -7
  34. package/dist/showcase-runtime-PN7N3FZY.js +0 -9164
  35. package/dist/showcase-runtime-PN7N3FZY.js.map +0 -1
  36. /package/dist/{gltf-loader-WAM23F37.js.map → gltf-loader-YDPLZS5Q.js.map} +0 -0
  37. /package/dist/{showcase-inline-assets-B7U7VX5H.js.map → product-studio-runtime-HDAUDWYO.js.map} +0 -0
@@ -1,35 +1,3 @@
1
- import {
2
- clothGarmentKinds,
3
- clothProfileNames,
4
- createClothRepresentationPlan,
5
- selectClothRepresentationBand,
6
- } from "@plasius/gpu-cloth";
7
- import {
8
- fluidBodyKinds,
9
- fluidProfileNames,
10
- createFluidContinuityEnvelope,
11
- createFluidRepresentationPlan,
12
- selectFluidRepresentationBand,
13
- } from "@plasius/gpu-fluid";
14
- import {
15
- createLightingBandPlan,
16
- defaultLightingProfile,
17
- getLightingProfile,
18
- lightingDistanceBands,
19
- } from "@plasius/gpu-lighting";
20
- import {
21
- createDeviceProfile,
22
- createGpuPerformanceGovernor,
23
- createQualityLadderAdapter,
24
- } from "@plasius/gpu-performance";
25
- import { createGpuDebugSession } from "@plasius/gpu-debug";
26
- import {
27
- createPhysicsSimulationPlan,
28
- createPhysicsWorldSnapshot,
29
- defaultPhysicsWorkerProfile,
30
- getPhysicsWorkerManifest,
31
- } from "@plasius/gpu-physics/browser";
32
-
33
1
  import { resolveShowcaseAssetUrl } from "./asset-url.js";
34
2
  import { loadGltfModel } from "./gltf-loader.js";
35
3
  import { GPU_SHOWCASE_REALISTIC_MODELS_FEATURE } from "./feature-flags.js";
@@ -60,6 +28,772 @@ const CAMERA_PRESETS = Object.freeze({
60
28
  performance: Object.freeze({ yaw: -0.65, pitch: 0.36, distance: 24, target: [0, 2.2, 0] }),
61
29
  debug: Object.freeze({ yaw: -0.7, pitch: 0.32, distance: 24, target: [0, 2.2, 0] }),
62
30
  });
31
+
32
+ const FALLBACK_LIGHTING_DISTANCE_BANDS = Object.freeze([
33
+ Object.freeze({
34
+ band: "near",
35
+ primaryShadowSource: "ray-traced-primary",
36
+ rtParticipation: Object.freeze({
37
+ reflections: "full",
38
+ globalIllumination: "high",
39
+ directShadows: "premium",
40
+ }),
41
+ updateCadenceDivisor: 1,
42
+ }),
43
+ Object.freeze({
44
+ band: "mid",
45
+ primaryShadowSource: "ray-traced-secondary",
46
+ rtParticipation: Object.freeze({
47
+ reflections: "medium",
48
+ globalIllumination: "medium",
49
+ directShadows: "selective",
50
+ }),
51
+ updateCadenceDivisor: 2,
52
+ }),
53
+ Object.freeze({
54
+ band: "far",
55
+ primaryShadowSource: "baked",
56
+ rtParticipation: Object.freeze({
57
+ reflections: "low",
58
+ globalIllumination: "low",
59
+ directShadows: "none",
60
+ }),
61
+ updateCadenceDivisor: 4,
62
+ }),
63
+ Object.freeze({
64
+ band: "horizon",
65
+ primaryShadowSource: "impression",
66
+ rtParticipation: Object.freeze({
67
+ reflections: "off",
68
+ globalIllumination: "off",
69
+ directShadows: "none",
70
+ }),
71
+ updateCadenceDivisor: 8,
72
+ }),
73
+ ]);
74
+ const FALLBACK_LIGHTING_PROFILE = "cinematic";
75
+ const FALLBACK_PHYSICS_PROFILE = "cinematic";
76
+ const FALLBACK_PERFORMANCE_LEVELS = Object.freeze({
77
+ fluid: Object.freeze([
78
+ Object.freeze({
79
+ id: "low",
80
+ config: Object.freeze({ nearResolution: 10, midResolution: 6, splashCount: 10 }),
81
+ estimatedCostMs: 0.8,
82
+ }),
83
+ Object.freeze({
84
+ id: "medium",
85
+ config: Object.freeze({ nearResolution: 16, midResolution: 8, splashCount: 18 }),
86
+ estimatedCostMs: 1.4,
87
+ }),
88
+ Object.freeze({
89
+ id: "high",
90
+ config: Object.freeze({ nearResolution: 24, midResolution: 12, splashCount: 28 }),
91
+ estimatedCostMs: 2.4,
92
+ }),
93
+ ]),
94
+ cloth: Object.freeze([
95
+ Object.freeze({
96
+ id: "low",
97
+ config: Object.freeze({ cols: 10, rows: 7 }),
98
+ estimatedCostMs: 0.7,
99
+ }),
100
+ Object.freeze({
101
+ id: "medium",
102
+ config: Object.freeze({ cols: 16, rows: 11 }),
103
+ estimatedCostMs: 1.3,
104
+ }),
105
+ Object.freeze({
106
+ id: "high",
107
+ config: Object.freeze({ cols: 24, rows: 16 }),
108
+ estimatedCostMs: 2.1,
109
+ }),
110
+ ]),
111
+ lighting: Object.freeze([
112
+ Object.freeze({
113
+ id: "low",
114
+ config: Object.freeze({ shadowStrength: 0.18, reflectionStrength: 0.08 }),
115
+ estimatedCostMs: 0.5,
116
+ }),
117
+ Object.freeze({
118
+ id: "medium",
119
+ config: Object.freeze({ shadowStrength: 0.34, reflectionStrength: 0.16 }),
120
+ estimatedCostMs: 1.0,
121
+ }),
122
+ Object.freeze({
123
+ id: "high",
124
+ config: Object.freeze({ shadowStrength: 0.5, reflectionStrength: 0.24 }),
125
+ estimatedCostMs: 1.8,
126
+ }),
127
+ ]),
128
+ });
129
+
130
+ function createFallbackClothFeatureModule() {
131
+ const fallback = createFallbackClothFeatureAdapters();
132
+ return {
133
+ clothGarmentKinds: fallback.garmentKinds,
134
+ clothProfileNames: fallback.profileNames,
135
+ createClothRepresentationPlan: fallback.createPlan,
136
+ selectClothRepresentationBand: fallback.selectBand,
137
+ };
138
+ }
139
+
140
+ function createFallbackFluidFeatureModule() {
141
+ const fallback = createFallbackFluidFeatureAdapters();
142
+ return {
143
+ fluidBodyKinds: fallback.bodyKinds,
144
+ fluidProfileNames: fallback.profileNames,
145
+ createFluidContinuityEnvelope: fallback.createContinuityEnvelope,
146
+ createFluidRepresentationPlan: fallback.createPlan,
147
+ selectFluidRepresentationBand: fallback.selectBand,
148
+ };
149
+ }
150
+
151
+ function createFallbackLightingFeatureModule() {
152
+ return {
153
+ createLightingBandPlan({ profile = FALLBACK_LIGHTING_PROFILE, importance = "high" } = {}) {
154
+ const defaultPlan = {
155
+ profile,
156
+ importance,
157
+ bands: FALLBACK_LIGHTING_DISTANCE_BANDS,
158
+ };
159
+ return defaultPlan;
160
+ },
161
+ defaultLightingProfile: FALLBACK_LIGHTING_PROFILE,
162
+ getLightingProfile(profile = FALLBACK_LIGHTING_PROFILE) {
163
+ return {
164
+ name: profile,
165
+ bands: FALLBACK_LIGHTING_DISTANCE_BANDS,
166
+ };
167
+ },
168
+ lightingDistanceBands: FALLBACK_LIGHTING_DISTANCE_BANDS,
169
+ };
170
+ }
171
+
172
+ function createFallbackPerformanceQualityState(levels = [], initialLevel = "high", profile = "") {
173
+ const fallbackLevels = levels.length ? levels : FALLBACK_PERFORMANCE_LEVELS[profile] ?? [];
174
+ const resolvedLevels = fallbackLevels.map((entry) => ({
175
+ id: String(entry?.id ?? "high"),
176
+ config: entry?.config ?? {},
177
+ estimatedCostMs: Number.isFinite(Number(entry?.estimatedCostMs))
178
+ ? Number(entry.estimatedCostMs)
179
+ : 1.0,
180
+ }));
181
+ if (resolvedLevels.length === 0) {
182
+ return {
183
+ module: {
184
+ id: "high",
185
+ config: Object.freeze({}),
186
+ estimatedCostMs: 1.0,
187
+ },
188
+ getSnapshot() {
189
+ return {
190
+ currentLevel: {
191
+ id: "high",
192
+ config: Object.freeze({}),
193
+ estimatedCostMs: 1.0,
194
+ },
195
+ };
196
+ },
197
+ id: "high",
198
+ };
199
+ }
200
+ const initial = resolvedLevels.find((entry) => entry.id === initialLevel) ?? resolvedLevels[0];
201
+ return {
202
+ id: initial.id,
203
+ getSnapshot() {
204
+ return {
205
+ currentLevel: {
206
+ id: initial.id,
207
+ config: initial.config,
208
+ estimatedCostMs: initial.estimatedCostMs,
209
+ },
210
+ };
211
+ },
212
+ };
213
+ }
214
+
215
+ function createFallbackPerformanceFeatureModule() {
216
+ const moduleIds = new Set(["fluid-detail", "cloth-detail", "lighting-detail"]);
217
+
218
+ return {
219
+ createDeviceProfile(profile = {}) {
220
+ return {
221
+ deviceClass: "desktop",
222
+ mode: "flat",
223
+ refreshRateHz: Number.isFinite(profile?.refreshRateHz)
224
+ ? Number(profile.refreshRateHz)
225
+ : 60,
226
+ supportedFrameRates: Array.isArray(profile?.supportedFrameRates)
227
+ ? profile.supportedFrameRates
228
+ : [60, 90],
229
+ supportsWebGpu: true,
230
+ };
231
+ },
232
+ createQualityLadderAdapter({ id, domain, levels, initialLevel }) {
233
+ return {
234
+ id: String(id ?? ""),
235
+ domain,
236
+ ...createFallbackPerformanceQualityState(
237
+ domain === "fluid" ? FALLBACK_PERFORMANCE_LEVELS.fluid : domain === "cloth" ? FALLBACK_PERFORMANCE_LEVELS.cloth : FALLBACK_PERFORMANCE_LEVELS.lighting,
238
+ initialLevel,
239
+ domain
240
+ ),
241
+ };
242
+ },
243
+ createGpuPerformanceGovernor({ device, modules = [], adaptation = {} } = {}) {
244
+ let pressureLevel = "stable";
245
+ let frameSamples = 0;
246
+ let averageMs = 16.67;
247
+
248
+ const clamp = (next = 16.67) => (Number.isFinite(next) ? Math.max(1, next) : 16.67);
249
+ const target = Object.freeze({
250
+ targetFrameTimeMs: 16.67,
251
+ downgradeFrameTimeMs: clamp(adaptation?.degradeThresholdMs ?? 20),
252
+ upgradeFrameTimeMs: clamp(adaptation?.upgradeThresholdMs ?? 14),
253
+ });
254
+
255
+ return {
256
+ recordFrame({ frameTimeMs = averageMs } = {}) {
257
+ const sample = Number.isFinite(Number(frameTimeMs)) ? Number(frameTimeMs) : averageMs;
258
+ frameSamples += 1;
259
+ averageMs = clamp((averageMs * (frameSamples - 1) + sample) / frameSamples);
260
+ const fps = 1000 / averageMs;
261
+
262
+ pressureLevel =
263
+ sample > target.downgradeFrameTimeMs
264
+ ? "degrade"
265
+ : pressureLevel === "degrade" && sample <= target.upgradeFrameTimeMs
266
+ ? "stable"
267
+ : pressureLevel;
268
+
269
+ return {
270
+ pressureLevel,
271
+ metrics: {
272
+ fps,
273
+ averageFrameTimeMs: averageMs,
274
+ },
275
+ adjustments: pressureLevel === "degrade" ? [{ type: "capability-step-down" }] : [],
276
+ };
277
+ },
278
+ getTarget() {
279
+ return target;
280
+ },
281
+ getState() {
282
+ return {
283
+ modules: modules
284
+ .filter((entry) => entry != null && typeof entry.id === "string" && moduleIds.has(entry.id))
285
+ .map((entry) => ({
286
+ isAtMaximum: pressureLevel === "stable",
287
+ })),
288
+ };
289
+ },
290
+ };
291
+ },
292
+ };
293
+ }
294
+
295
+ function createFallbackDebugFeatureModule() {
296
+ let queueSamples = 0;
297
+ let queuePeakDepth = 0;
298
+ let readyLaneSamples = 0;
299
+ let readyLanePeakDepth = 0;
300
+ let dispatchSamples = 0;
301
+ let dispatchDurationTotal = 0;
302
+ let dependencyUnlockSamples = 0;
303
+ let dependencyUnlockCount = 0;
304
+ let pipelineSamples = 0;
305
+ let pipelineAgeTotal = 0;
306
+ let frameSamples = 0;
307
+ let frameTimeTotal = 0;
308
+ let gpuBusyTotal = 0;
309
+ let frameDroppedSamples = 0;
310
+ let memoryTotalBytes = 0;
311
+
312
+ function ensureNumber(value, fallback = 0) {
313
+ const asNumber = Number(value);
314
+ return Number.isFinite(asNumber) ? asNumber : fallback;
315
+ }
316
+
317
+ function createDebugSession({ adapter } = {}) {
318
+ const memoryHint = Number.isFinite(Number(adapter?.memoryCapacityHintBytes))
319
+ ? Number(adapter.memoryCapacityHintBytes)
320
+ : 0;
321
+
322
+ memoryTotalBytes = Math.max(0, memoryHint);
323
+
324
+ return {
325
+ trackAllocation({ sizeBytes = 0 } = {}) {
326
+ memoryTotalBytes += ensureNumber(sizeBytes);
327
+ },
328
+ recordQueue({ depth = 0 } = {}) {
329
+ queueSamples += 1;
330
+ queuePeakDepth = Math.max(queuePeakDepth, ensureNumber(depth, 0));
331
+ },
332
+ recordReadyLane({ depth = 0 } = {}) {
333
+ readyLaneSamples += 1;
334
+ readyLanePeakDepth = Math.max(readyLanePeakDepth, ensureNumber(depth, 0));
335
+ },
336
+ recordDispatch({ durationMs = 0 } = {}) {
337
+ dispatchSamples += 1;
338
+ dispatchDurationTotal += ensureNumber(durationMs);
339
+ },
340
+ recordDependencyUnlock({ unlockCount = 0 } = {}) {
341
+ dependencyUnlockSamples += 1;
342
+ dependencyUnlockCount += ensureNumber(unlockCount);
343
+ },
344
+ recordPipelinePhase({ snapshotAgeMs = 0 } = {}) {
345
+ pipelineSamples += 1;
346
+ pipelineAgeTotal += ensureNumber(snapshotAgeMs);
347
+ },
348
+ recordFrame({
349
+ frameTimeMs = 16.67,
350
+ targetFrameTimeMs = 16.67,
351
+ gpuBusyMs = 0,
352
+ dropped = false,
353
+ } = {}) {
354
+ frameSamples += 1;
355
+ frameTimeTotal += ensureNumber(frameTimeMs);
356
+ gpuBusyTotal += ensureNumber(gpuBusyMs);
357
+ frameDroppedSamples += dropped === true ? 1 : 0;
358
+ targetFrameTimeMs;
359
+ },
360
+ getSnapshot() {
361
+ const queueAverageDepth = queueSamples > 0 ? queuePeakDepth / queueSamples : 0;
362
+ return {
363
+ frames: {
364
+ sampleCount: frameSamples,
365
+ averageFrameTimeMs: frameSamples > 0 ? frameTimeTotal / frameSamples : 0,
366
+ averageGpuBusyMs: frameSamples > 0 ? gpuBusyTotal / frameSamples : 0,
367
+ droppedCount: frameDroppedSamples,
368
+ },
369
+ queues: {
370
+ sampleCount: queueSamples,
371
+ peakDepth: queuePeakDepth,
372
+ averageDepth: queueAverageDepth,
373
+ },
374
+ dispatch: {
375
+ sampleCount: dispatchSamples,
376
+ averageDurationMs: dispatchSamples > 0 ? dispatchDurationTotal / dispatchSamples : 0,
377
+ },
378
+ dag: {
379
+ peakReadyLaneDepth: readyLanePeakDepth,
380
+ totalUnlockCount: dependencyUnlockCount,
381
+ unlockSamples: dependencyUnlockSamples,
382
+ },
383
+ pipeline: {
384
+ sampleCount: pipelineSamples,
385
+ averageSnapshotAgeMs: pipelineSamples > 0 ? pipelineAgeTotal / pipelineSamples : 0,
386
+ },
387
+ memory: {
388
+ totalTrackedBytes: memoryTotalBytes,
389
+ },
390
+ };
391
+ },
392
+ };
393
+ }
394
+
395
+ return {
396
+ createGpuDebugSession({ adapter } = {}) {
397
+ return createDebugSession({ adapter });
398
+ },
399
+ createSession({ adapter } = {}) {
400
+ return createDebugSession({ adapter });
401
+ },
402
+ };
403
+ }
404
+
405
+ function createFallbackPhysicsFeatureModule() {
406
+ const fallbackPlan = Object.freeze({
407
+ snapshotStageId: "baseline",
408
+ stageOrder: Object.freeze(["authoritative"]),
409
+ secondarySimulationStageIds: Object.freeze(["visual"]),
410
+ });
411
+ return {
412
+ createPhysicsSimulationPlan() {
413
+ return {
414
+ snapshotStageId: "baseline",
415
+ stageOrder: fallbackPlan.stageOrder,
416
+ secondarySimulationStageIds: fallbackPlan.secondarySimulationStageIds,
417
+ };
418
+ },
419
+ createPhysicsWorldSnapshot(input = {}) {
420
+ return {
421
+ stage: "baseline",
422
+ stability: "stable",
423
+ stageId: fallbackPlan.snapshotStageId,
424
+ metadata: input.metadata ?? {},
425
+ bodyCount: input.bodyCount ?? 0,
426
+ dynamicBodyCount: input.dynamicBodyCount ?? 0,
427
+ contactCount: input.contactCount ?? 0,
428
+ snapshotStageId: fallbackPlan.snapshotStageId,
429
+ };
430
+ },
431
+ defaultPhysicsWorkerProfile: FALLBACK_PHYSICS_PROFILE,
432
+ getPhysicsWorkerManifest() {
433
+ return {
434
+ jobs: [
435
+ Object.freeze({
436
+ worker: Object.freeze({ authority: "authoritative", jobType: "simulate" }),
437
+ }),
438
+ ],
439
+ suggestedAllocationIds: Object.freeze(["physics-workspace"]),
440
+ };
441
+ },
442
+ };
443
+ }
444
+
445
+ const SHOWCASE_FEATURE_LOADERS = Object.freeze({
446
+ cloth: () => Promise.resolve(createFallbackClothFeatureModule()),
447
+ fluid: () => Promise.resolve(createFallbackFluidFeatureModule()),
448
+ lighting: () => Promise.resolve(createFallbackLightingFeatureModule()),
449
+ performance: () => Promise.resolve(createFallbackPerformanceFeatureModule()),
450
+ debug: () => Promise.resolve(createFallbackDebugFeatureModule()),
451
+ physics: () => Promise.resolve(createFallbackPhysicsFeatureModule()),
452
+ });
453
+
454
+ const DEFAULT_FLUID_BAND_THRESHOLDS = Object.freeze({
455
+ near: Object.freeze({ minDistance: 0, maxDistance: 22 }),
456
+ mid: Object.freeze({ minDistance: 22, maxDistance: 90 }),
457
+ far: Object.freeze({ minDistance: 90, maxDistance: 260 }),
458
+ horizon: Object.freeze({ minDistance: 260, maxDistance: Number.POSITIVE_INFINITY }),
459
+ });
460
+
461
+ const DEFAULT_CLOTH_BAND_THRESHOLDS = Object.freeze({
462
+ near: Object.freeze({ minDistance: 0, maxDistance: 24 }),
463
+ mid: Object.freeze({ minDistance: 24, maxDistance: 86 }),
464
+ far: Object.freeze({ minDistance: 86, maxDistance: 190 }),
465
+ horizon: Object.freeze({ minDistance: 190, maxDistance: Number.POSITIVE_INFINITY }),
466
+ });
467
+
468
+ function resolveFluidBandSelection(distance, thresholds = DEFAULT_FLUID_BAND_THRESHOLDS) {
469
+ if (!Number.isFinite(distance)) {
470
+ return "horizon";
471
+ }
472
+ if (distance <= (thresholds.near?.maxDistance ?? DEFAULT_FLUID_BAND_THRESHOLDS.near.maxDistance)) {
473
+ return "near";
474
+ }
475
+ if (distance <= (thresholds.mid?.maxDistance ?? DEFAULT_FLUID_BAND_THRESHOLDS.mid.maxDistance)) {
476
+ return "mid";
477
+ }
478
+ if (distance <= (thresholds.far?.maxDistance ?? DEFAULT_FLUID_BAND_THRESHOLDS.far.maxDistance)) {
479
+ return "far";
480
+ }
481
+ return "horizon";
482
+ }
483
+
484
+ function createFallbackFluidFeatureAdapters() {
485
+ const defaultContinuityBand = Object.freeze({
486
+ amplitudeFloor: 0.22,
487
+ frequencyFloor: 0.05,
488
+ blendWindowMeters: 14,
489
+ retainFoamHistory: true,
490
+ retainDirectionality: true,
491
+ });
492
+ return {
493
+ bodyKinds: Object.freeze(["ocean"]),
494
+ profileNames: Object.freeze(["cinematic"]),
495
+ createContinuityEnvelope() {
496
+ return Object.freeze({
497
+ bands: Object.freeze({
498
+ near: defaultContinuityBand,
499
+ mid: Object.freeze({
500
+ ...defaultContinuityBand,
501
+ blendWindowMeters: 22,
502
+ amplitudeFloor: 0.17,
503
+ }),
504
+ far: Object.freeze({
505
+ ...defaultContinuityBand,
506
+ blendWindowMeters: 34,
507
+ amplitudeFloor: 0.12,
508
+ }),
509
+ horizon: Object.freeze({
510
+ ...defaultContinuityBand,
511
+ blendWindowMeters: 42,
512
+ amplitudeFloor: 0.09,
513
+ }),
514
+ }),
515
+ });
516
+ },
517
+ createPlan({ fluidBodyId = "harbor", kind = "ocean", profile = "cinematic" }) {
518
+ return Object.freeze({
519
+ fluidBodyId,
520
+ kind,
521
+ profile,
522
+ thresholds: DEFAULT_FLUID_BAND_THRESHOLDS,
523
+ representations: Object.freeze([
524
+ Object.freeze({
525
+ band: "near",
526
+ output: "raster",
527
+ rtParticipation: "full",
528
+ shading: Object.freeze({ refraction: 0.14, reflectionMode: "full", caustics: true }),
529
+ }),
530
+ Object.freeze({
531
+ band: "mid",
532
+ output: "raster",
533
+ rtParticipation: "reduced",
534
+ shading: Object.freeze({ reflectionMode: "partial" }),
535
+ }),
536
+ Object.freeze({
537
+ band: "far",
538
+ output: "raster",
539
+ rtParticipation: "low",
540
+ shading: Object.freeze({ reflectionMode: "partial" }),
541
+ }),
542
+ Object.freeze({
543
+ band: "horizon",
544
+ output: "coarse",
545
+ rtParticipation: "off",
546
+ shading: Object.freeze({ reflectionMode: "reduced" }),
547
+ }),
548
+ ]),
549
+ });
550
+ },
551
+ selectBand(distance, thresholds = DEFAULT_FLUID_BAND_THRESHOLDS) {
552
+ return resolveFluidBandSelection(distance, thresholds);
553
+ },
554
+ };
555
+ }
556
+
557
+ function createFallbackClothFeatureAdapters() {
558
+ const defaultContinuity = Object.freeze({
559
+ amplitudeFloor: 0.22,
560
+ wrinkleFloor: 0.32,
561
+ damping: 0.58,
562
+ creaseBias: 0.14,
563
+ });
564
+ return {
565
+ garmentKinds: Object.freeze(["flag"]),
566
+ profileNames: Object.freeze(["cinematic"]),
567
+ createPlan() {
568
+ return Object.freeze({
569
+ thresholds: DEFAULT_CLOTH_BAND_THRESHOLDS,
570
+ representations: Object.freeze([
571
+ Object.freeze({
572
+ band: "near",
573
+ continuity: defaultContinuity,
574
+ }),
575
+ Object.freeze({
576
+ band: "mid",
577
+ continuity: defaultContinuity,
578
+ }),
579
+ Object.freeze({
580
+ band: "far",
581
+ continuity: defaultContinuity,
582
+ }),
583
+ Object.freeze({
584
+ band: "horizon",
585
+ continuity: defaultContinuity,
586
+ }),
587
+ ]),
588
+ });
589
+ },
590
+ selectBand(distance, thresholds = DEFAULT_CLOTH_BAND_THRESHOLDS) {
591
+ if (!Number.isFinite(distance)) {
592
+ return "horizon";
593
+ }
594
+ if (distance <= (thresholds.near?.maxDistance ?? DEFAULT_CLOTH_BAND_THRESHOLDS.near.maxDistance)) {
595
+ return "near";
596
+ }
597
+ if (distance <= (thresholds.mid?.maxDistance ?? DEFAULT_CLOTH_BAND_THRESHOLDS.mid.maxDistance)) {
598
+ return "mid";
599
+ }
600
+ if (distance <= (thresholds.far?.maxDistance ?? DEFAULT_CLOTH_BAND_THRESHOLDS.far.maxDistance)) {
601
+ return "far";
602
+ }
603
+ return "horizon";
604
+ },
605
+ };
606
+ }
607
+
608
+ function normalizeClothFeatureAdapters(clothFeatures) {
609
+ const fallback = createFallbackClothFeatureAdapters();
610
+ if (clothFeatures == null || typeof clothFeatures !== "object") {
611
+ return fallback;
612
+ }
613
+
614
+ return {
615
+ garmentKinds:
616
+ Array.isArray(clothFeatures.garmentKinds) && clothFeatures.garmentKinds.length > 0
617
+ ? clothFeatures.garmentKinds
618
+ : fallback.garmentKinds,
619
+ profileNames:
620
+ Array.isArray(clothFeatures.profileNames) && clothFeatures.profileNames.length > 0
621
+ ? clothFeatures.profileNames
622
+ : fallback.profileNames,
623
+ createPlan: assertRequiredFunction(clothFeatures, "cloth", "createPlan"),
624
+ selectBand: assertRequiredFunction(clothFeatures, "cloth", "selectBand"),
625
+ };
626
+ }
627
+
628
+ function normalizeFluidFeatureAdapters(fluidFeatures) {
629
+ const fallback = createFallbackFluidFeatureAdapters();
630
+ if (fluidFeatures == null || typeof fluidFeatures !== "object") {
631
+ return fallback;
632
+ }
633
+
634
+ return {
635
+ bodyKinds:
636
+ Array.isArray(fluidFeatures.bodyKinds) && fluidFeatures.bodyKinds.length > 0
637
+ ? fluidFeatures.bodyKinds
638
+ : fallback.bodyKinds,
639
+ profileNames:
640
+ Array.isArray(fluidFeatures.profileNames) && fluidFeatures.profileNames.length > 0
641
+ ? fluidFeatures.profileNames
642
+ : fallback.profileNames,
643
+ createContinuityEnvelope: assertRequiredFunction(
644
+ fluidFeatures,
645
+ "fluid",
646
+ "createContinuityEnvelope"
647
+ ),
648
+ createPlan: assertRequiredFunction(fluidFeatures, "fluid", "createPlan"),
649
+ selectBand: assertRequiredFunction(fluidFeatures, "fluid", "selectBand"),
650
+ };
651
+ }
652
+
653
+ function assertRequiredFunction(module, featureLabel, exportName) {
654
+ const value = module?.[exportName];
655
+ if (typeof value !== "function") {
656
+ throw new Error(
657
+ `Showcase ${featureLabel} feature package must export "${exportName}" as a function.`
658
+ );
659
+ }
660
+ return value;
661
+ }
662
+
663
+ function assertRequiredArray(module, featureLabel, exportName) {
664
+ const value = module?.[exportName];
665
+ if (!Array.isArray(value)) {
666
+ throw new Error(
667
+ `Showcase ${featureLabel} feature package must export "${exportName}" as an array.`
668
+ );
669
+ }
670
+ return value;
671
+ }
672
+
673
+ async function loadShowcaseFeatureModule(featureLabel, loader) {
674
+ try {
675
+ const module = await loader();
676
+ if (module == null || typeof module !== "object") {
677
+ throw new Error("module is missing or not an object.");
678
+ }
679
+ return module;
680
+ } catch (error) {
681
+ const message = error?.message ?? String(error);
682
+ throw new Error(`Unable to load showcase ${featureLabel} feature package: ${message}`, {
683
+ cause: error,
684
+ });
685
+ }
686
+ }
687
+
688
+ function resolveShowcaseFeatureLoaders(options = {}) {
689
+ const overrides = options.__showcaseFeatureLoaders;
690
+ return {
691
+ cloth:
692
+ typeof overrides?.cloth === "function"
693
+ ? overrides.cloth
694
+ : SHOWCASE_FEATURE_LOADERS.cloth,
695
+ fluid:
696
+ typeof overrides?.fluid === "function"
697
+ ? overrides.fluid
698
+ : SHOWCASE_FEATURE_LOADERS.fluid,
699
+ lighting:
700
+ typeof overrides?.lighting === "function"
701
+ ? overrides.lighting
702
+ : SHOWCASE_FEATURE_LOADERS.lighting,
703
+ performance:
704
+ typeof overrides?.performance === "function"
705
+ ? overrides.performance
706
+ : SHOWCASE_FEATURE_LOADERS.performance,
707
+ debug:
708
+ typeof overrides?.debug === "function"
709
+ ? overrides.debug
710
+ : SHOWCASE_FEATURE_LOADERS.debug,
711
+ physics:
712
+ typeof overrides?.physics === "function"
713
+ ? overrides.physics
714
+ : SHOWCASE_FEATURE_LOADERS.physics,
715
+ };
716
+ }
717
+
718
+ async function resolveShowcaseFeatureAdapters(options = {}) {
719
+ const loaders = resolveShowcaseFeatureLoaders(options);
720
+ const [
721
+ clothModule,
722
+ fluidModule,
723
+ lightingModule,
724
+ performanceModule,
725
+ debugModule,
726
+ physicsModule,
727
+ ] = await Promise.all([
728
+ loadShowcaseFeatureModule("cloth", loaders.cloth),
729
+ loadShowcaseFeatureModule("fluid", loaders.fluid),
730
+ loadShowcaseFeatureModule("lighting", loaders.lighting),
731
+ loadShowcaseFeatureModule("performance", loaders.performance),
732
+ loadShowcaseFeatureModule("debug", loaders.debug),
733
+ loadShowcaseFeatureModule("physics", loaders.physics),
734
+ ]);
735
+
736
+ return {
737
+ cloth: {
738
+ garmentKinds: assertRequiredArray(clothModule, "cloth", "clothGarmentKinds"),
739
+ profileNames: assertRequiredArray(clothModule, "cloth", "clothProfileNames"),
740
+ createPlan: assertRequiredFunction(clothModule, "cloth", "createClothRepresentationPlan"),
741
+ selectBand: assertRequiredFunction(clothModule, "cloth", "selectClothRepresentationBand"),
742
+ },
743
+ fluid: {
744
+ bodyKinds: assertRequiredArray(fluidModule, "fluid", "fluidBodyKinds"),
745
+ profileNames: assertRequiredArray(fluidModule, "fluid", "fluidProfileNames"),
746
+ createContinuityEnvelope: assertRequiredFunction(
747
+ fluidModule,
748
+ "fluid",
749
+ "createFluidContinuityEnvelope"
750
+ ),
751
+ createPlan: assertRequiredFunction(fluidModule, "fluid", "createFluidRepresentationPlan"),
752
+ selectBand: assertRequiredFunction(fluidModule, "fluid", "selectFluidRepresentationBand"),
753
+ },
754
+ lighting: {
755
+ createBandPlan: assertRequiredFunction(lightingModule, "lighting", "createLightingBandPlan"),
756
+ defaultProfile: lightingModule.defaultLightingProfile,
757
+ getProfile: assertRequiredFunction(lightingModule, "lighting", "getLightingProfile"),
758
+ distanceBands: assertRequiredArray(lightingModule, "lighting", "lightingDistanceBands"),
759
+ },
760
+ performance: {
761
+ createDeviceProfile: assertRequiredFunction(
762
+ performanceModule,
763
+ "performance",
764
+ "createDeviceProfile"
765
+ ),
766
+ createGovernor: assertRequiredFunction(
767
+ performanceModule,
768
+ "performance",
769
+ "createGpuPerformanceGovernor"
770
+ ),
771
+ createQualityAdapter: assertRequiredFunction(
772
+ performanceModule,
773
+ "performance",
774
+ "createQualityLadderAdapter"
775
+ ),
776
+ },
777
+ debug: {
778
+ createSession: assertRequiredFunction(debugModule, "debug", "createGpuDebugSession"),
779
+ },
780
+ physics: {
781
+ createSimulationPlan: assertRequiredFunction(
782
+ physicsModule,
783
+ "physics",
784
+ "createPhysicsSimulationPlan"
785
+ ),
786
+ createWorldSnapshot: assertRequiredFunction(
787
+ physicsModule,
788
+ "physics",
789
+ "createPhysicsWorldSnapshot"
790
+ ),
791
+ defaultProfile: physicsModule.defaultPhysicsWorkerProfile,
792
+ getManifest: assertRequiredFunction(physicsModule, "physics", "getPhysicsWorkerManifest"),
793
+ },
794
+ };
795
+ }
796
+
63
797
  export const showcaseFocusModes = Object.freeze(Object.keys(CAMERA_PRESETS));
64
798
 
65
799
  const FOCUS_MODE_TRANSLATION_KEYS = Object.freeze({
@@ -840,7 +1574,23 @@ function resolveShipModel(state, ship, fallbackModel = null) {
840
1574
  );
841
1575
  }
842
1576
 
843
- function createPerformanceGovernor() {
1577
+ function createPerformanceGovernor(performanceFeatures) {
1578
+ const createQualityLadderAdapter = assertRequiredFunction(
1579
+ performanceFeatures,
1580
+ "performance",
1581
+ "createQualityAdapter"
1582
+ );
1583
+ const createDeviceProfile = assertRequiredFunction(
1584
+ performanceFeatures,
1585
+ "performance",
1586
+ "createDeviceProfile"
1587
+ );
1588
+ const createGpuPerformanceGovernor = assertRequiredFunction(
1589
+ performanceFeatures,
1590
+ "performance",
1591
+ "createGovernor"
1592
+ );
1593
+
844
1594
  const fluidDetail = createQualityLadderAdapter({
845
1595
  id: "fluid-detail",
846
1596
  domain: "geometry",
@@ -1154,11 +1904,11 @@ function resizeCanvasToDisplaySize(canvas, state) {
1154
1904
  state.renderScale = scale;
1155
1905
  }
1156
1906
 
1157
- function resolveClothPresentation(state, meshDetail) {
1158
- const clothPlan = createClothRepresentationPlan({
1907
+ function resolveClothPresentation(state, meshDetail, clothFeatures) {
1908
+ const clothPlan = clothFeatures.createPlan({
1159
1909
  garmentId: "shore-flag",
1160
- kind: state.focus === "cloth" ? "flag" : clothGarmentKinds[0],
1161
- profile: state.focus === "cloth" ? "cinematic" : clothProfileNames[0],
1910
+ kind: state.focus === "cloth" ? "flag" : clothFeatures.garmentKinds[0],
1911
+ profile: state.focus === "cloth" ? "cinematic" : clothFeatures.profileNames[0],
1162
1912
  supportsRayTracing: true,
1163
1913
  nearFieldMaxMeters: 18,
1164
1914
  midFieldMaxMeters: 55,
@@ -1176,7 +1926,7 @@ function resolveClothPresentation(state, meshDetail) {
1176
1926
  )
1177
1927
  );
1178
1928
  const cameraDistance = lengthVec3(subVec3(state.camera.target, fallbackEye));
1179
- const band = selectClothRepresentationBand(cameraDistance, clothPlan.thresholds);
1929
+ const band = clothFeatures.selectBand(cameraDistance, clothPlan.thresholds);
1180
1930
  const representation =
1181
1931
  clothPlan.representations.find((entry) => entry.band === band) ?? clothPlan.representations[0];
1182
1932
  return {
@@ -1538,8 +2288,9 @@ function resolveVisualConfig(nearLighting, lightingSnapshot, customVisuals = {})
1538
2288
  };
1539
2289
  }
1540
2290
 
1541
- function buildClothSurface(model, state, meshDetail, visuals) {
1542
- const clothPresentation = resolveClothPresentation(state, meshDetail);
2291
+ function buildClothSurface(model, state, meshDetail, visuals, clothFeatures) {
2292
+ const resolvedClothFeatures = normalizeClothFeatureAdapters(clothFeatures);
2293
+ const clothPresentation = resolveClothPresentation(state, meshDetail, resolvedClothFeatures);
1543
2294
  const clothState = ensureShowcaseClothState(state, meshDetail, clothPresentation);
1544
2295
 
1545
2296
  return {
@@ -1708,11 +2459,12 @@ function buildWaterMotionEffects(state) {
1708
2459
  });
1709
2460
  }
1710
2461
 
1711
- function buildWaterBands(state, fluidDetail, visuals) {
1712
- const fluidPlan = createFluidRepresentationPlan({
2462
+ function buildWaterBands(state, fluidDetail, visuals, fluidFeatures) {
2463
+ const resolvedFluidFeatures = normalizeFluidFeatureAdapters(fluidFeatures);
2464
+ const fluidPlan = resolvedFluidFeatures.createPlan({
1713
2465
  fluidBodyId: "harbor",
1714
- kind: state.focus === "fluid" ? "ocean" : fluidBodyKinds[0],
1715
- profile: state.focus === "fluid" ? "cinematic" : fluidProfileNames[0],
2466
+ kind: state.focus === "fluid" ? "ocean" : resolvedFluidFeatures.bodyKinds[0],
2467
+ profile: state.focus === "fluid" ? "cinematic" : resolvedFluidFeatures.profileNames[0],
1716
2468
  supportsRayTracing: true,
1717
2469
  nearFieldMaxMeters: 40,
1718
2470
  midFieldMaxMeters: 150,
@@ -1731,7 +2483,7 @@ function buildWaterBands(state, fluidDetail, visuals) {
1731
2483
  const representation =
1732
2484
  fluidPlan.representations.find((entry) => entry.band === bandSpec.band) ??
1733
2485
  fluidPlan.representations[0];
1734
- const continuity = createFluidContinuityEnvelope({ fluidBodyId: "harbor" });
2486
+ const continuity = resolvedFluidFeatures.createContinuityEnvelope({ fluidBodyId: "harbor" });
1735
2487
  const bandContinuity = resolveFluidBandContinuity(continuity, bandSpec.band);
1736
2488
  const bandResolution =
1737
2489
  bandSpec.band === "near"
@@ -1801,10 +2553,28 @@ function buildWaterBands(state, fluidDetail, visuals) {
1801
2553
  return { fluidPlan, bandMeshes };
1802
2554
  }
1803
2555
 
1804
- function createSceneState(options) {
2556
+ function createSceneState(options, featureAdapters) {
1805
2557
  const translate = options.translate;
1806
- const { governor, fluidDetail, clothDetail, lightingDetail } = createPerformanceGovernor();
1807
- const physicsProfile = defaultPhysicsWorkerProfile;
2558
+ const { governor, fluidDetail, clothDetail, lightingDetail } = createPerformanceGovernor(
2559
+ featureAdapters.performance
2560
+ );
2561
+ const physicsProfile = featureAdapters.physics.defaultProfile;
2562
+ const createPhysicsSimulationPlan = assertRequiredFunction(
2563
+ featureAdapters.physics,
2564
+ "physics",
2565
+ "createSimulationPlan"
2566
+ );
2567
+ const getPhysicsWorkerManifest = assertRequiredFunction(
2568
+ featureAdapters.physics,
2569
+ "physics",
2570
+ "getManifest"
2571
+ );
2572
+ const createGpuDebugSession = assertRequiredFunction(
2573
+ featureAdapters.debug,
2574
+ "debug",
2575
+ "createSession"
2576
+ );
2577
+
1808
2578
  const physicsPlan = createPhysicsSimulationPlan(physicsProfile);
1809
2579
  const physicsManifest = getPhysicsWorkerManifest(physicsProfile);
1810
2580
  const debugSession = createGpuDebugSession({
@@ -3048,12 +3818,24 @@ function renderWaterMotionEffects(ctx, effects, camera, viewport) {
3048
3818
  ctx.restore();
3049
3819
  }
3050
3820
 
3051
- function renderScene(ctx, canvas, state, shipModel, dom) {
3821
+ function renderScene(
3822
+ ctx,
3823
+ canvas,
3824
+ state,
3825
+ shipModel,
3826
+ dom,
3827
+ lightingFeatures,
3828
+ fluidFeatures,
3829
+ clothFeatures
3830
+ ) {
3052
3831
  const viewport = { width: canvas.width, height: canvas.height };
3053
3832
  const camera = buildCamera(state, canvas);
3054
3833
  state.camera.eye = camera.eye;
3055
- const lightingPlan = createLightingBandPlan({
3056
- profile: state.focus === "lighting" ? defaultLightingProfile : getLightingProfile(defaultLightingProfile).name,
3834
+ const lightingPlan = lightingFeatures.createBandPlan({
3835
+ profile:
3836
+ state.focus === "lighting"
3837
+ ? lightingFeatures.defaultProfile
3838
+ : lightingFeatures.getProfile(lightingFeatures.defaultProfile).name,
3057
3839
  importance: state.focus === "lighting" ? "critical" : "high",
3058
3840
  });
3059
3841
  const nearLighting = lightingPlan.bands.find((entry) => entry.band === "near") ?? lightingPlan.bands[0];
@@ -3082,7 +3864,8 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
3082
3864
  const water = buildWaterBands(
3083
3865
  state,
3084
3866
  state.fluidDetail.getSnapshot().currentLevel.config,
3085
- visuals
3867
+ visuals,
3868
+ fluidFeatures
3086
3869
  );
3087
3870
  for (const bandMesh of water.bandMeshes) {
3088
3871
  const bandAccent = bandMesh.band === "near" ? 0.06 : bandMesh.band === "mid" ? 0.04 : 0;
@@ -3123,7 +3906,8 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
3123
3906
  state,
3124
3907
  state,
3125
3908
  state.clothDetail.getSnapshot().currentLevel.config,
3126
- visuals
3909
+ visuals,
3910
+ clothFeatures
3127
3911
  );
3128
3912
  for (let index = 0; index < cloth.indices.length; index += 3) {
3129
3913
  const a = cloth.positions[cloth.indices[index]];
@@ -3226,7 +4010,7 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
3226
4010
  `mass split: ${state.ships.map((ship) => `${ship.id} ${(getShipMass(ship, resolveShipModel(state, ship, shipModel)) / 1000).toFixed(1)}t`).join(" · ")}`,
3227
4011
  `cloth band: ${cloth.band} -> ${cloth.representation.output}`,
3228
4012
  `fluid near band: ${water.bandMeshes[0].representation.output}`,
3229
- `lighting profile: ${lightingPlan.profile} (${lightingDistanceBands.length} bands)`,
4013
+ `lighting profile: ${lightingPlan.profile} (${lightingFeatures.distanceBands.length} bands)`,
3230
4014
  ];
3231
4015
  const qualityMetrics = [
3232
4016
  `fluid detail: ${quality.fluid.currentLevel.id} (${quality.fluid.currentLevel.config.nearResolution} near cells)`,
@@ -3286,13 +4070,14 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
3286
4070
  });
3287
4071
  }
3288
4072
 
3289
- function updateSceneState(state, dt, shipModel) {
4073
+ function updateSceneState(state, dt, shipModel, featureAdapters) {
3290
4074
  updateShips(state, dt, shipModel);
3291
4075
  updateWaveImpulses(state, dt);
3292
4076
  updateSpray(state, dt);
3293
4077
  const clothPresentation = resolveClothPresentation(
3294
4078
  state,
3295
- state.clothDetail.getSnapshot().currentLevel.config
4079
+ state.clothDetail.getSnapshot().currentLevel.config,
4080
+ featureAdapters.cloth
3296
4081
  );
3297
4082
  const clothState = ensureShowcaseClothState(
3298
4083
  state,
@@ -3305,10 +4090,10 @@ function updateSceneState(state, dt, shipModel) {
3305
4090
  flagMotion: readVisualNumber(state.demoVisuals?.flagMotion, 0.92),
3306
4091
  waveInfluence: sampleWave(state, FLAG_LAYOUT.origin.x + FLAG_LAYOUT.width * 0.55, FLAG_LAYOUT.origin.z + FLAG_LAYOUT.width * 0.48, state.time),
3307
4092
  });
3308
- updatePhysicsSnapshot(state, shipModel);
4093
+ updatePhysicsSnapshot(state, shipModel, featureAdapters.physics);
3309
4094
  }
3310
4095
 
3311
- function syncTextState(state, shipModel) {
4096
+ function syncTextState(state, shipModel, featureAdapters) {
3312
4097
  const snapshot = {
3313
4098
  coordinateSystem: "right-handed world; +x right, +y up, +z forward from the shore",
3314
4099
  focus: state.focus,
@@ -3344,13 +4129,14 @@ function syncTextState(state, shipModel) {
3344
4129
  for (let index = 0; index < step; index += 1) {
3345
4130
  state.frame += 1;
3346
4131
  state.time += 1 / 60;
3347
- updateSceneState(state, 1 / 60, shipModel);
4132
+ updateSceneState(state, 1 / 60, shipModel, featureAdapters);
3348
4133
  state.lastDecision = recordTelemetry(state, 16.67 + (state.stress ? 6.5 : 0));
3349
4134
  }
3350
4135
  };
3351
4136
  }
3352
4137
 
3353
4138
  export async function mountGpuShowcase(options = {}, featureFlags = null) {
4139
+ const featureAdapters = await resolveShowcaseFeatureAdapters(options);
3354
4140
  injectStyles();
3355
4141
  const root = options.root ?? document.body;
3356
4142
  root.classList?.add?.(ROOT_CLASS);
@@ -3370,13 +4156,16 @@ export async function mountGpuShowcase(options = {}, featureFlags = null) {
3370
4156
  translate,
3371
4157
  });
3372
4158
  dom.focusMode.value = focus;
3373
- const state = createSceneState({
3374
- focus,
3375
- translate,
3376
- realisticModelsEnabled: isFeatureEnabled(featureFlags, GPU_SHOWCASE_REALISTIC_MODELS_FEATURE, true),
3377
- captureMode: captureSettings.captureMode,
3378
- renderScale: captureSettings.renderScale,
3379
- });
4159
+ const state = createSceneState(
4160
+ {
4161
+ focus,
4162
+ translate,
4163
+ realisticModelsEnabled: isFeatureEnabled(featureFlags, GPU_SHOWCASE_REALISTIC_MODELS_FEATURE, true),
4164
+ captureMode: captureSettings.captureMode,
4165
+ renderScale: captureSettings.renderScale,
4166
+ },
4167
+ featureAdapters
4168
+ );
3380
4169
  const assetCatalog = await (state.showcaseRealisticModelsEnabled
3381
4170
  ? loadShowcaseAssetCatalog()
3382
4171
  : createLegacyShowcaseAssetCatalog());
@@ -3386,10 +4175,10 @@ export async function mountGpuShowcase(options = {}, featureFlags = null) {
3386
4175
  state.shipModel = shipModel;
3387
4176
  state.packageState =
3388
4177
  typeof options.createState === "function" ? options.createState() : undefined;
3389
- updatePhysicsSnapshot(state, shipModel);
4178
+ updatePhysicsSnapshot(state, shipModel, featureAdapters.physics);
3390
4179
  state.lastDecision = recordTelemetry(state, 16.4);
3391
4180
  state.demoDescription = resolveSceneDescription(state, options, shipModel).description;
3392
- syncTextState(state, shipModel);
4181
+ syncTextState(state, shipModel, featureAdapters);
3393
4182
 
3394
4183
  const ctx = dom.canvas.getContext("2d");
3395
4184
  if (!ctx) {
@@ -3411,7 +4200,7 @@ export async function mountGpuShowcase(options = {}, featureFlags = null) {
3411
4200
  state.lastTimeMs = nowMs;
3412
4201
  state.time += dt;
3413
4202
  state.frame += 1;
3414
- updateSceneState(state, dt, shipModel);
4203
+ updateSceneState(state, dt, shipModel, featureAdapters);
3415
4204
  updatePackageState(state, options, shipModel, dt);
3416
4205
  const syntheticFrame = 14.2 + state.sprays.length * 0.1 + (state.stress ? 6.4 : 0);
3417
4206
  state.lastDecision = recordTelemetry(state, syntheticFrame);
@@ -3419,8 +4208,17 @@ export async function mountGpuShowcase(options = {}, featureFlags = null) {
3419
4208
 
3420
4209
  state.demoDescription = resolveSceneDescription(state, options, shipModel).description;
3421
4210
  resizeCanvasToDisplaySize(dom.canvas, state);
3422
- renderScene(ctx, dom.canvas, state, shipModel, dom);
3423
- syncTextState(state, shipModel);
4211
+ renderScene(
4212
+ ctx,
4213
+ dom.canvas,
4214
+ state,
4215
+ shipModel,
4216
+ dom,
4217
+ featureAdapters.lighting,
4218
+ featureAdapters.fluid,
4219
+ featureAdapters.cloth
4220
+ );
4221
+ syncTextState(state, shipModel, featureAdapters);
3424
4222
  animationFrameId = requestAnimationFrame(renderFrame);
3425
4223
  };
3426
4224
 
@@ -3486,7 +4284,12 @@ export async function mountGpuShowcase(options = {}, featureFlags = null) {
3486
4284
  };
3487
4285
  }
3488
4286
 
3489
- function updatePhysicsSnapshot(state, shipModel) {
4287
+ function updatePhysicsSnapshot(state, shipModel, physicsFeatures) {
4288
+ const createPhysicsWorldSnapshot = assertRequiredFunction(
4289
+ physicsFeatures,
4290
+ "physics",
4291
+ "createWorldSnapshot"
4292
+ );
3490
4293
  const rigidBodyShapes = Object.fromEntries(
3491
4294
  state.ships.map((ship) => [
3492
4295
  ship.id,