@forgeax/engine-vfx-render 0.1.2

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 (91) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +163 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/__tests__/billboard-advanced.integration.test.d.ts +2 -0
  5. package/dist/__tests__/billboard-advanced.integration.test.d.ts.map +1 -0
  6. package/dist/__tests__/data-interface-providers.unit.test.d.ts +2 -0
  7. package/dist/__tests__/data-interface-providers.unit.test.d.ts.map +1 -0
  8. package/dist/__tests__/data-interface-public-api.test-d.d.ts +2 -0
  9. package/dist/__tests__/data-interface-public-api.test-d.d.ts.map +1 -0
  10. package/dist/__tests__/event-resources.unit.test.d.ts +2 -0
  11. package/dist/__tests__/event-resources.unit.test.d.ts.map +1 -0
  12. package/dist/__tests__/fog-producer.integration.test.d.ts +2 -0
  13. package/dist/__tests__/fog-producer.integration.test.d.ts.map +1 -0
  14. package/dist/__tests__/gpu-host.integration.test.d.ts +2 -0
  15. package/dist/__tests__/gpu-host.integration.test.d.ts.map +1 -0
  16. package/dist/__tests__/inspect-snapshot.integration.test.d.ts +2 -0
  17. package/dist/__tests__/inspect-snapshot.integration.test.d.ts.map +1 -0
  18. package/dist/__tests__/particle-resources.unit.test.d.ts +2 -0
  19. package/dist/__tests__/particle-resources.unit.test.d.ts.map +1 -0
  20. package/dist/__tests__/render-vocabulary-owner.test-d.d.ts +2 -0
  21. package/dist/__tests__/render-vocabulary-owner.test-d.d.ts.map +1 -0
  22. package/dist/__tests__/stage-plan.integration.test.d.ts +2 -0
  23. package/dist/__tests__/stage-plan.integration.test.d.ts.map +1 -0
  24. package/dist/__tests__/stage-recovery.integration.test.d.ts +2 -0
  25. package/dist/__tests__/stage-recovery.integration.test.d.ts.map +1 -0
  26. package/dist/__tests__/stage-resource-access-owner.test-d.d.ts +2 -0
  27. package/dist/__tests__/stage-resource-access-owner.test-d.d.ts.map +1 -0
  28. package/dist/__tests__/stage-resource-layout.unit.test.d.ts +2 -0
  29. package/dist/__tests__/stage-resource-layout.unit.test.d.ts.map +1 -0
  30. package/dist/__tests__/topology-capacity.unit.test.d.ts +2 -0
  31. package/dist/__tests__/topology-capacity.unit.test.d.ts.map +1 -0
  32. package/dist/__tests__/topology-resources.unit.test.d.ts +2 -0
  33. package/dist/__tests__/topology-resources.unit.test.d.ts.map +1 -0
  34. package/dist/feature/camera.d.ts +11 -0
  35. package/dist/feature/camera.d.ts.map +1 -0
  36. package/dist/feature/event-resources.d.ts +9 -0
  37. package/dist/feature/event-resources.d.ts.map +1 -0
  38. package/dist/feature/gpu-particle-feature.d.ts +96 -0
  39. package/dist/feature/gpu-particle-feature.d.ts.map +1 -0
  40. package/dist/feature/particle-resources.d.ts +66 -0
  41. package/dist/feature/particle-resources.d.ts.map +1 -0
  42. package/dist/feature/stage-plan.d.ts +54 -0
  43. package/dist/feature/stage-plan.d.ts.map +1 -0
  44. package/dist/feature/topologies/beam.d.ts +16 -0
  45. package/dist/feature/topologies/beam.d.ts.map +1 -0
  46. package/dist/feature/topologies/ribbon.d.ts +15 -0
  47. package/dist/feature/topologies/ribbon.d.ts.map +1 -0
  48. package/dist/feature/topologies/trail.d.ts +19 -0
  49. package/dist/feature/topologies/trail.d.ts.map +1 -0
  50. package/dist/host/data-interface-providers.d.ts +19 -0
  51. package/dist/host/data-interface-providers.d.ts.map +1 -0
  52. package/dist/host/vfx-runtime-host.d.ts +87 -0
  53. package/dist/host/vfx-runtime-host.d.ts.map +1 -0
  54. package/dist/index.d.ts +12 -0
  55. package/dist/index.d.ts.map +1 -0
  56. package/dist/index.mjs +1572 -0
  57. package/dist/index.mjs.map +1 -0
  58. package/package.json +63 -0
  59. package/src/__tests__/billboard-advanced.integration.test.ts +69 -0
  60. package/src/__tests__/data-interface-providers.unit.test.ts +55 -0
  61. package/src/__tests__/data-interface-public-api.test-d.ts +58 -0
  62. package/src/__tests__/event-resources.unit.test.ts +71 -0
  63. package/src/__tests__/fog-producer.integration.test.ts +59 -0
  64. package/src/__tests__/gpu-host.integration.test.ts +441 -0
  65. package/src/__tests__/inspect-snapshot.integration.test.ts +26 -0
  66. package/src/__tests__/particle-resources.unit.test.ts +75 -0
  67. package/src/__tests__/render-vocabulary-owner.test-d.ts +81 -0
  68. package/src/__tests__/stage-plan.integration.test.ts +16 -0
  69. package/src/__tests__/stage-recovery.integration.test.ts +24 -0
  70. package/src/__tests__/stage-resource-access-owner.test-d.ts +26 -0
  71. package/src/__tests__/stage-resource-layout.unit.test.ts +40 -0
  72. package/src/__tests__/topology-capacity.unit.test.ts +59 -0
  73. package/src/__tests__/topology-resources.unit.test.ts +29 -0
  74. package/src/feature/camera.ts +12 -0
  75. package/src/feature/event-resources.ts +62 -0
  76. package/src/feature/gpu-particle-feature.ts +1225 -0
  77. package/src/feature/particle-resources.ts +203 -0
  78. package/src/feature/stage-plan.ts +282 -0
  79. package/src/feature/topologies/beam.ts +41 -0
  80. package/src/feature/topologies/ribbon.ts +31 -0
  81. package/src/feature/topologies/trail.ts +43 -0
  82. package/src/host/data-interface-providers.ts +119 -0
  83. package/src/host/vfx-runtime-host.ts +402 -0
  84. package/src/index.ts +57 -0
  85. package/src/shaders/beam.wgsl +69 -0
  86. package/src/shaders/billboard.wgsl +132 -0
  87. package/src/shaders/event.wgsl +11 -0
  88. package/src/shaders/mesh.wgsl +98 -0
  89. package/src/shaders/ribbon.wgsl +69 -0
  90. package/src/shaders/stage.wgsl +5 -0
  91. package/src/shaders/trail.wgsl +77 -0
package/dist/index.mjs ADDED
@@ -0,0 +1,1572 @@
1
+ import { frustum } from '@forgeax/engine-math';
2
+ import { RenderFeatureStageFailedError, RENDER_FEATURE_VERTEX_LAYOUTS, RenderFeaturePreparationFailedError } from '@forgeax/engine-render';
3
+ import { Transform } from '@forgeax/engine-scene';
4
+ import { err, ok } from '@forgeax/engine-types';
5
+ import { VFX_GPU_RUNTIME_RESOURCE_KEY, vfxGpuEffectContribution, vfxGpuRuntimePlugin, resolveVfxDataInterfaces, ParticleEffectPlayer } from '@forgeax/engine-vfx';
6
+ import { getAssetRegistryResolver } from '@forgeax/engine-assets-runtime/internal';
7
+ import { createWorldContext } from '@forgeax/engine-ecs';
8
+
9
+ // src/feature/event-resources.ts
10
+ var VFX_EVENT_INPUT_BYTES = 32;
11
+ var VFX_EVENT_BYTES = 32;
12
+ var VFX_EVENT_COUNTER_BYTES = 16;
13
+ function channelFanOut(emitter, channel) {
14
+ return Math.max(
15
+ 1,
16
+ ...(emitter.events ?? []).filter((event) => event.channel === channel).map((event) => event.fanOut)
17
+ );
18
+ }
19
+ function eventInputCapacity(emitter) {
20
+ return Math.max(
21
+ 1,
22
+ (emitter.channels ?? []).reduce((total, channel) => total + channel.capacity, 0)
23
+ );
24
+ }
25
+ function eventCapacity(emitter) {
26
+ const capacity = (emitter.channels ?? []).reduce(
27
+ (total, channel) => total + channel.capacity * channelFanOut(emitter, channel.id),
28
+ 0
29
+ );
30
+ return Math.max(1, Math.min(emitter.capacity, capacity));
31
+ }
32
+ function channelKey(channel) {
33
+ let hash = 2166136261;
34
+ for (const code of channel) {
35
+ hash ^= code.codePointAt(0) ?? 0;
36
+ hash = Math.imul(hash, 16777619);
37
+ }
38
+ return hash >>> 0;
39
+ }
40
+ function encodeEventInputs(intent) {
41
+ const capacity = eventInputCapacity(intent.emitter);
42
+ const data = new ArrayBuffer(capacity * VFX_EVENT_INPUT_BYTES);
43
+ const bytes = new Uint8Array(data);
44
+ bytes.fill(255);
45
+ const view = new DataView(data);
46
+ for (const [index, input] of intent.channelInputs.entries()) {
47
+ if (index >= capacity) break;
48
+ const offset = index * VFX_EVENT_INPUT_BYTES;
49
+ view.setFloat32(offset, input.payload.position[0], true);
50
+ view.setFloat32(offset + 4, input.payload.position[1], true);
51
+ view.setFloat32(offset + 8, input.payload.position[2], true);
52
+ view.setFloat32(offset + 16, input.payload.strength, true);
53
+ view.setUint32(offset + 20, input.sequence, true);
54
+ view.setUint32(offset + 24, channelKey(input.channel), true);
55
+ view.setUint32(offset + 28, channelFanOut(intent.emitter, input.channel), true);
56
+ }
57
+ return bytes;
58
+ }
59
+ function eventCounterData() {
60
+ return new Uint8Array(VFX_EVENT_COUNTER_BYTES);
61
+ }
62
+ var PARTICLE_SHADER_IDENTIFIERS = Object.freeze({
63
+ billboard: "forgeax::vfx-render.particles.billboard",
64
+ mesh: "forgeax::vfx-render.particles.mesh",
65
+ ribbon: "forgeax::vfx-render.particles.ribbon",
66
+ trail: "forgeax::vfx-render.particles.trail",
67
+ beam: "forgeax::vfx-render.particles.beam"
68
+ });
69
+ function createTopologyResourcePlan(renderer) {
70
+ if (renderer === null || typeof renderer !== "object" || Array.isArray(renderer))
71
+ return err({
72
+ code: "vfx-topology-resource-invalid",
73
+ expected: "a topology renderer object",
74
+ hint: "declare a ribbon, trail, or beam renderer",
75
+ detail: { path: "renderer" }
76
+ });
77
+ const value = renderer;
78
+ if (value.kind !== "ribbon" && value.kind !== "trail" && value.kind !== "beam")
79
+ return err({
80
+ code: "vfx-topology-resource-invalid",
81
+ expected: "ribbon, trail, or beam",
82
+ hint: "do not alias topology output to billboard or mesh",
83
+ detail: { path: "renderer.kind" }
84
+ });
85
+ if (typeof value.capacity !== "number" || !Number.isInteger(value.capacity) || value.capacity <= 0 || value.capacity > 65536)
86
+ return err({
87
+ code: "vfx-topology-resource-invalid",
88
+ expected: "capacity in the range 1..65536",
89
+ hint: "bound topology resources before allocating them",
90
+ detail: { path: "renderer.capacity" }
91
+ });
92
+ const capacity = value.capacity;
93
+ if (value.kind === "ribbon" && value.stripKey !== "alive-index")
94
+ return err({
95
+ code: "vfx-topology-resource-invalid",
96
+ expected: "stripKey 'alive-index'",
97
+ hint: "use the managed alive-list order until a custom WGSL topology stage owns grouping",
98
+ detail: { path: "renderer.stripKey" }
99
+ });
100
+ if (value.kind === "trail" && (typeof value.historyLength !== "number" || !Number.isInteger(value.historyLength) || value.historyLength <= 0 || value.historyLength > 256))
101
+ return err({
102
+ code: "vfx-topology-resource-invalid",
103
+ expected: "historyLength in the range 1..256",
104
+ hint: "bound trail history storage",
105
+ detail: { path: "renderer.historyLength" }
106
+ });
107
+ if (value.kind === "beam" && value.endpointField !== "velocity")
108
+ return err({
109
+ code: "vfx-topology-resource-invalid",
110
+ expected: "endpointField 'velocity'",
111
+ hint: "use the managed velocity endpoint until a custom WGSL topology stage owns endpoints",
112
+ detail: { path: "renderer.endpointField" }
113
+ });
114
+ const vertexStride = 12 * 4;
115
+ const segments = value.kind === "trail" ? capacity * Math.max(1, value.historyLength - 1) : capacity;
116
+ return ok({
117
+ topology: value.kind,
118
+ capacity,
119
+ vertexBytes: Math.max(vertexStride, segments * vertexStride),
120
+ indexBytes: 0,
121
+ indirectBytes: 20,
122
+ resourceKey: `vfx-topology-${value.kind}`,
123
+ ...value.kind === "ribbon" ? { stripKey: "alive-index" } : {},
124
+ ...value.kind === "trail" ? { historyLength: value.historyLength } : {},
125
+ ...value.kind === "beam" ? { endpointField: "velocity" } : {}
126
+ });
127
+ }
128
+ function topologyCapacitySnapshot(plan, input) {
129
+ const produced = Math.max(0, Math.min(plan.capacity, input.produced));
130
+ const requested = Math.max(0, input.requested);
131
+ return {
132
+ topology: plan.topology,
133
+ capacity: plan.capacity,
134
+ produced,
135
+ dropped: Math.max(0, requested - produced),
136
+ overflow: Math.max(0, requested - plan.capacity),
137
+ degenerate: Math.max(0, input.degenerate ?? 0)
138
+ };
139
+ }
140
+ function particleMaterialPass(kind, material) {
141
+ const pass = material?.passes?.find((candidate) => candidate.name === `particle-${kind}`);
142
+ return {
143
+ shader: pass?.program.module ?? PARTICLE_SHADER_IDENTIFIERS[kind],
144
+ ...pass?.renderState === void 0 ? {} : { renderState: pass.renderState }
145
+ };
146
+ }
147
+ function particleMaterialUsesSceneDepth(contract) {
148
+ return contract === "group-0-resource" || contract === "view-with-resource";
149
+ }
150
+ function particleMaterialUsesBindings(material) {
151
+ return (material?.parameters?.length ?? 0) > 0;
152
+ }
153
+ function floatAttribute(value) {
154
+ if (value instanceof Float32Array) return value;
155
+ if (value instanceof Uint16Array) return Float32Array.from(value);
156
+ return value === void 0 ? new Float32Array() : new Float32Array(value);
157
+ }
158
+ function canonicalMeshVertices(mesh) {
159
+ if (mesh.vertices.length > 0 && mesh.vertices.length % 12 === 0) return mesh.vertices;
160
+ const positions = floatAttribute(mesh.attributes.position);
161
+ const vertexCount = Math.floor(positions.length / 3);
162
+ const normals = floatAttribute(mesh.attributes.normal);
163
+ const uvs = floatAttribute(mesh.attributes.uv);
164
+ const tangents = floatAttribute(mesh.attributes.tangent);
165
+ const result = new Float32Array(vertexCount * 12);
166
+ for (let index = 0; index < vertexCount; index += 1) {
167
+ const target2 = index * 12;
168
+ result.set(positions.subarray(index * 3, index * 3 + 3), target2);
169
+ result.set(
170
+ normals.length >= index * 3 + 3 ? normals.subarray(index * 3, index * 3 + 3) : [0, 0, 1],
171
+ target2 + 3
172
+ );
173
+ result.set(
174
+ uvs.length >= index * 2 + 2 ? uvs.subarray(index * 2, index * 2 + 2) : [0, 0],
175
+ target2 + 6
176
+ );
177
+ result.set(
178
+ tangents.length >= index * 4 + 4 ? tangents.subarray(index * 4, index * 4 + 4) : [1, 0, 0, 1],
179
+ target2 + 8
180
+ );
181
+ }
182
+ return result;
183
+ }
184
+
185
+ // src/feature/stage-plan.ts
186
+ var MANAGED_STAGES = /* @__PURE__ */ new Set(["spawn", "update", "scan", "compact"]);
187
+ var RESOURCE_NAMES = /* @__PURE__ */ new Set([
188
+ "particles",
189
+ "runtime",
190
+ "aliveIndices",
191
+ "counters",
192
+ "indirect",
193
+ "scratch",
194
+ "billboardInstances",
195
+ "channelInputs",
196
+ "events",
197
+ "eventCounters"
198
+ ]);
199
+ var STAGE_ID = /^[a-z][a-z0-9-]{0,31}$/;
200
+ var ENTRY_POINT = /^forgeax_vfx_stage_[a-z][a-z0-9-]{0,31}_main$/;
201
+ var MAX_ITERATIONS = 64;
202
+ function failure(code, stageId, hint) {
203
+ return { ok: false, error: { code, stageId, hint } };
204
+ }
205
+ function stableStage(stage) {
206
+ return {
207
+ id: stage.id,
208
+ entry: stage.entry,
209
+ entryPoint: stage.entryPoint,
210
+ domain: stage.domain,
211
+ resources: Object.freeze(
212
+ stage.resources.map((resource) => ({ name: resource.name, access: resource.access }))
213
+ ),
214
+ dependsOn: Object.freeze([...stage.dependsOn]),
215
+ iterationBudget: stage.iterationBudget
216
+ };
217
+ }
218
+ function validatedStagePlan(reflection, generation) {
219
+ const source = reflection ?? [];
220
+ const ids = /* @__PURE__ */ new Set();
221
+ const stages = [];
222
+ for (const stage of source) {
223
+ if (!STAGE_ID.test(stage.id) || ids.has(stage.id)) {
224
+ return failure(
225
+ "stage-id-invalid",
226
+ stage.id,
227
+ "recook the effect with unique bounded stage ids"
228
+ );
229
+ }
230
+ ids.add(stage.id);
231
+ if (stage.domain !== "particle" || MANAGED_STAGES.has(stage.domain)) {
232
+ return failure("stage-domain-invalid", stage.id, "use the managed particle dispatch domain");
233
+ }
234
+ if (!ENTRY_POINT.test(stage.entryPoint)) {
235
+ return failure(
236
+ "stage-entry-point-invalid",
237
+ stage.id,
238
+ "use the compiler-generated stage entry point"
239
+ );
240
+ }
241
+ if (!Number.isInteger(stage.iterationBudget) || stage.iterationBudget < 1 || stage.iterationBudget > MAX_ITERATIONS) {
242
+ return failure(
243
+ "stage-budget-invalid",
244
+ stage.id,
245
+ "use an integer iteration budget from 1 through 64"
246
+ );
247
+ }
248
+ const resources = /* @__PURE__ */ new Set();
249
+ for (const resource of stage.resources) {
250
+ if (!RESOURCE_NAMES.has(resource.name) || !["read", "write", "read-write"].includes(resource.access)) {
251
+ return failure(
252
+ "stage-resource-invalid",
253
+ stage.id,
254
+ "declare only managed VFX resources with a supported access mode"
255
+ );
256
+ }
257
+ if (resources.has(resource.name)) {
258
+ return failure(
259
+ "stage-resource-invalid",
260
+ stage.id,
261
+ "declare each managed VFX resource once"
262
+ );
263
+ }
264
+ resources.add(resource.name);
265
+ }
266
+ stages.push(stableStage(stage));
267
+ }
268
+ const byId = new Map(stages.map((stage) => [stage.id, stage]));
269
+ const visiting = /* @__PURE__ */ new Set();
270
+ const visited = /* @__PURE__ */ new Set();
271
+ const ordered = [];
272
+ const visit = (stage) => {
273
+ if (visited.has(stage.id)) return void 0;
274
+ if (visiting.has(stage.id))
275
+ return {
276
+ code: "stage-cycle",
277
+ stageId: stage.id,
278
+ hint: "remove the stage dependency cycle and recook the effect"
279
+ };
280
+ visiting.add(stage.id);
281
+ for (const dependency of stage.dependsOn) {
282
+ if (MANAGED_STAGES.has(dependency)) continue;
283
+ const dependencyStage = byId.get(dependency);
284
+ if (dependencyStage === void 0) {
285
+ return {
286
+ code: "stage-dependency-invalid",
287
+ stageId: stage.id,
288
+ hint: "declare every stage dependency or use a managed stage"
289
+ };
290
+ }
291
+ const error = visit(dependencyStage);
292
+ if (error !== void 0) return error;
293
+ }
294
+ visiting.delete(stage.id);
295
+ visited.add(stage.id);
296
+ ordered.push(stage);
297
+ return void 0;
298
+ };
299
+ for (const stage of stages) {
300
+ const error = visit(stage);
301
+ if (error !== void 0) return { ok: false, error };
302
+ }
303
+ const normalized = Object.freeze(ordered);
304
+ return {
305
+ ok: true,
306
+ value: Object.freeze({
307
+ stages: normalized,
308
+ fingerprint: JSON.stringify(normalized),
309
+ generation
310
+ })
311
+ };
312
+ }
313
+ function stageDispatches(plan, workgroups, bindings) {
314
+ return plan.stages.map((stage) => ({
315
+ entryPoint: stage.entryPoint,
316
+ workgroups: [workgroups],
317
+ bindings
318
+ }));
319
+ }
320
+ function observeStagePlan(candidate, generation, lastKnownGoodStage) {
321
+ if (candidate.ok) {
322
+ return {
323
+ validatedStagePlan: candidate.value,
324
+ stageReadiness: candidate.value.stages.map((stage) => ({
325
+ id: stage.id,
326
+ state: "ready",
327
+ generation,
328
+ lastKnownGoodGeneration: generation,
329
+ retryable: false
330
+ })),
331
+ stageOutput: candidate.value.stages.length === 0 ? "empty" : "active",
332
+ lastKnownGoodStage: candidate.value.stages.length === 0 ? lastKnownGoodStage === void 0 ? void 0 : {
333
+ fingerprint: lastKnownGoodStage.fingerprint,
334
+ generation: lastKnownGoodStage.generation
335
+ } : { fingerprint: candidate.value.fingerprint, generation }
336
+ };
337
+ }
338
+ const retained = lastKnownGoodStage ?? { stages: [], fingerprint: "", generation };
339
+ return {
340
+ validatedStagePlan: retained,
341
+ stageReadiness: [
342
+ {
343
+ id: candidate.error.stageId,
344
+ state: "candidate-rejected",
345
+ generation,
346
+ ...lastKnownGoodStage === void 0 ? {} : { lastKnownGoodGeneration: lastKnownGoodStage.generation },
347
+ retryable: true,
348
+ error: candidate.error.code
349
+ }
350
+ ],
351
+ stageOutput: lastKnownGoodStage === void 0 ? "empty" : "last-known-good",
352
+ lastKnownGoodStage: lastKnownGoodStage === void 0 ? void 0 : {
353
+ fingerprint: lastKnownGoodStage.fingerprint,
354
+ generation: lastKnownGoodStage.generation
355
+ }
356
+ };
357
+ }
358
+ function stageRecoveryReadiness(plan, generation, recovery) {
359
+ return plan.stages.map((stage) => ({
360
+ id: stage.id,
361
+ state: recovery === "stale" ? "stale" : "rebuilding",
362
+ generation,
363
+ lastKnownGoodGeneration: plan.generation,
364
+ retryable: true,
365
+ error: recovery
366
+ }));
367
+ }
368
+
369
+ // src/feature/gpu-particle-feature.ts
370
+ var IDENTITY = "forgeax.vfx-render.gpu-particles";
371
+ var WORKGROUP_SIZE = 256;
372
+ var PARTICLE_BYTES = 80;
373
+ var BILLBOARD_INSTANCE_BYTES = 31 * 4;
374
+ var MESH_INSTANCE_BYTES = 28 * 4;
375
+ var COUNTERS_BYTES = 24;
376
+ var RUNTIME_BYTES = 72 * 4;
377
+ var MAX_TICK_RINGS = 8;
378
+ var IDENTITY_MATRIX = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
379
+ function createVfxRenderInspectSnapshot(input) {
380
+ return {
381
+ topology: input.topology,
382
+ counters: { capacity: input.capacity, produced: input.produced, dropped: input.dropped },
383
+ stageReadiness: input.stageReadiness,
384
+ providerReadiness: input.providerReadiness,
385
+ gpuTiming: input.gpuTiming
386
+ };
387
+ }
388
+ function resolveBillboardAdvancedState(renderer, sample) {
389
+ if (renderer.softParticle !== void 0 && !sample.depthAvailable) {
390
+ return err({
391
+ code: "vfx-renderer-depth-missing",
392
+ expected: "a scene-depth provider for soft particles",
393
+ hint: "attach the scene-depth data interface or disable soft particles",
394
+ detail: { path: "renderer.softParticle" }
395
+ });
396
+ }
397
+ const sheet = renderer.textureSheet;
398
+ const frameCount = sheet?.frameCount ?? (sheet === void 0 ? 1 : sheet.columns * sheet.rows);
399
+ const frameIndex = sheet === void 0 || sheet.frameRate === 0 ? 0 : Math.min(
400
+ frameCount - 1,
401
+ Math.max(0, Math.floor(Math.max(0, sample.age) * sheet.frameRate))
402
+ );
403
+ const softParticleFade = renderer.softParticle === void 0 ? 1 : Math.max(
404
+ 0,
405
+ Math.min(
406
+ 1,
407
+ (sample.sceneDepth - sample.particleDepth) / renderer.softParticle.fadeDistance
408
+ )
409
+ );
410
+ return ok({
411
+ frameIndex,
412
+ pivot: renderer.pivot ?? [0, 0],
413
+ softParticleFade,
414
+ sortingKey: renderer.sorting === "back-to-front" ? sample.particleDepth : 0
415
+ });
416
+ }
417
+ function topologyRecoveryHint(topology, reason) {
418
+ if (reason === "capacity")
419
+ return `${topology} capacity overflow was bounded; increase its explicit capacity`;
420
+ if (reason === "broken")
421
+ return `${topology} continuity broke; inspect its explicit source key and keep the last valid segment`;
422
+ if (reason === "degenerate")
423
+ return `${topology} produced no drawable segment; preserve zero output and inspect source endpoints`;
424
+ return `${topology} device resources recovered from the last known good generation`;
425
+ }
426
+ function finite(value, fallback) {
427
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
428
+ }
429
+ function vector(value, fallback, size) {
430
+ return Array.isArray(value) ? Array.from({ length: size }, (_, index) => finite(value[index], fallback[index] ?? 0)) : fallback;
431
+ }
432
+ function runtimeData(intent, camera, material, localToWorld, renderer, rendererIndex = 0) {
433
+ const storage = new ArrayBuffer(RUNTIME_BYTES);
434
+ const floats = new Float32Array(storage);
435
+ const words = new Uint32Array(storage);
436
+ floats[0] = intent.fixedDelta;
437
+ words[1] = intent.phaseTick;
438
+ words[2] = intent.seed;
439
+ words[3] = intent.playCycle;
440
+ words[4] = intent.emitter.capacity;
441
+ words[5] = intent.spawnCount;
442
+ words[6] = intent.firstParticleId;
443
+ words[7] = intent.emitter.renderers.length;
444
+ floats.set(camera.viewProjection, 8);
445
+ floats.set(camera.right, 24);
446
+ floats.set(camera.up, 28);
447
+ const values = material?.values ?? {};
448
+ floats.set(vector(values.baseColor, [1, 1, 1, 1], 4), 32);
449
+ const emissive = vector(values.emissive, [0, 0, 0], 3);
450
+ floats.set(emissive, 36);
451
+ floats[39] = finite(values.emissiveIntensity, 0);
452
+ floats[40] = finite(values.metallic, 0);
453
+ floats[41] = finite(values.roughness, 0.5);
454
+ floats[42] = finite(values.clearcoat, 0);
455
+ floats[43] = finite(values.clearcoatRoughness, 0.5);
456
+ floats.set(localToWorld, 44);
457
+ words[60] = rendererIndex;
458
+ words[61] = renderer?.kind === "trail" ? renderer.historyLength : 0;
459
+ words[62] = renderer?.kind === "ribbon" || renderer?.kind === "trail" || renderer?.kind === "beam" ? renderer.capacity : intent.emitter.capacity;
460
+ words[63] = renderer?.kind === "billboard" && renderer.sorting === "back-to-front" ? 2 : renderer?.kind === "billboard" && renderer.sorting === "emitter" ? 1 : 0;
461
+ floats[64] = renderer?.kind === "billboard" ? renderer.pivot?.[0] ?? 0 : renderer?.kind === "ribbon" || renderer?.kind === "trail" || renderer?.kind === "beam" ? renderer.width ?? 0.1 : 0.1;
462
+ floats[65] = renderer?.kind === "billboard" ? renderer.pivot?.[1] ?? 0 : 0;
463
+ floats[66] = renderer?.kind === "billboard" ? renderer.softParticle?.fadeDistance ?? 0 : 0;
464
+ const sheet = renderer?.kind === "billboard" ? renderer.textureSheet : void 0;
465
+ floats[68] = sheet?.columns ?? 1;
466
+ floats[69] = sheet?.rows ?? 1;
467
+ floats[70] = sheet?.frameRate ?? 0;
468
+ floats[71] = sheet?.frameCount ?? (sheet === void 0 ? 1 : sheet.columns * sheet.rows);
469
+ return new Uint8Array(storage);
470
+ }
471
+ function emitterTransform(world, intent) {
472
+ if (intent.emitter.space === "world") return IDENTITY_MATRIX;
473
+ const transform = world.get(intent.player, Transform);
474
+ return transform.ok ? transform.value.world : IDENTITY_MATRIX;
475
+ }
476
+ function emitterVisible(intent, camera, localToWorld) {
477
+ const bounds = intent.emitter.bounds;
478
+ const center = bounds.kind === "sphere" ? bounds.center : [
479
+ (bounds.min[0] + bounds.max[0]) * 0.5,
480
+ (bounds.min[1] + bounds.max[1]) * 0.5,
481
+ (bounds.min[2] + bounds.max[2]) * 0.5
482
+ ];
483
+ const radius = bounds.kind === "sphere" ? bounds.radius : Math.hypot(
484
+ (bounds.max[0] - bounds.min[0]) * 0.5,
485
+ (bounds.max[1] - bounds.min[1]) * 0.5,
486
+ (bounds.max[2] - bounds.min[2]) * 0.5
487
+ );
488
+ const matrix = (index) => localToWorld[index] ?? 0;
489
+ const worldPosition = new Float32Array([
490
+ matrix(0) * center[0] + matrix(4) * center[1] + matrix(8) * center[2] + matrix(12),
491
+ matrix(1) * center[0] + matrix(5) * center[1] + matrix(9) * center[2] + matrix(13),
492
+ matrix(2) * center[0] + matrix(6) * center[1] + matrix(10) * center[2] + matrix(14)
493
+ ]);
494
+ const scale = Math.max(
495
+ Math.hypot(matrix(0), matrix(1), matrix(2)),
496
+ Math.hypot(matrix(4), matrix(5), matrix(6)),
497
+ Math.hypot(matrix(8), matrix(9), matrix(10))
498
+ );
499
+ const planes = frustum.fromViewProjection(frustum.create(), camera.viewProjection);
500
+ return frustum.intersectsSphere(planes, worldPosition, radius * scale);
501
+ }
502
+ function resetData(size) {
503
+ return new Uint8Array(size);
504
+ }
505
+ function target(targets, kind) {
506
+ return targets.find((entry) => entry.kind === kind);
507
+ }
508
+ function missingMeshPreparation(guid) {
509
+ return new RenderFeaturePreparationFailedError(
510
+ IDENTITY,
511
+ -1,
512
+ "asset-load",
513
+ "vertex-data",
514
+ `mesh:${guid}`,
515
+ "asset-not-ready",
516
+ "next-frame"
517
+ );
518
+ }
519
+ function materialResourceKey(shader) {
520
+ return shader.replaceAll(/[^a-zA-Z0-9_.:-]/g, "_");
521
+ }
522
+ function requiresSceneDepth(intent) {
523
+ return (intent.emitter.reflection.dataInterfaces ?? []).some(
524
+ (requirement) => requirement.kind === "scene-depth"
525
+ );
526
+ }
527
+ function gpuParticleRenderFeature(options) {
528
+ const worldIds = /* @__PURE__ */ new WeakMap();
529
+ let nextWorldId = 0;
530
+ const states = /* @__PURE__ */ new Map();
531
+ let lastObservation = Object.freeze({
532
+ frameNumber: -1,
533
+ dispatches: 0,
534
+ indirectDraws: 0,
535
+ subjectOutputs: 0
536
+ });
537
+ const keyOf = (world, intent) => {
538
+ let worldId = worldIds.get(world);
539
+ if (worldId === void 0) {
540
+ worldId = nextWorldId++;
541
+ worldIds.set(world, worldId);
542
+ }
543
+ return `${worldId}:${intent.player}:${intent.emitter.id}`;
544
+ };
545
+ const stateFor = (world, intent) => {
546
+ const key = keyOf(world, intent);
547
+ let state = states.get(key);
548
+ if (state !== void 0 && state.fingerprint !== intent.programFingerprint) {
549
+ states.delete(key);
550
+ state = void 0;
551
+ }
552
+ if (state !== void 0) return state;
553
+ const fingerprint = intent.programFingerprint.slice(0, 12).replaceAll(":", "_");
554
+ state = {
555
+ world,
556
+ player: intent.player,
557
+ emitterId: intent.emitter.id,
558
+ fingerprint: intent.programFingerprint,
559
+ capacity: intent.emitter.capacity,
560
+ names: `gpu.${key.replaceAll(":", ".")}.${fingerprint}`,
561
+ rings: [],
562
+ projections: [],
563
+ draws: [],
564
+ depthSampledDraws: [],
565
+ colorTarget: void 0,
566
+ depthTarget: void 0,
567
+ indirectInitialized: false,
568
+ culled: false,
569
+ stageReadiness: [],
570
+ stageOutput: "empty"
571
+ };
572
+ states.set(key, state);
573
+ return state;
574
+ };
575
+ return {
576
+ identity: IDENTITY,
577
+ requiredCapabilities: ["compute", "indirectDrawing"],
578
+ requiredMaterialShaders: Object.values(PARTICLE_SHADER_IDENTIFIERS),
579
+ extract: (context) => {
580
+ const extracted = [];
581
+ for (const world of context.worlds) {
582
+ if (!world.hasResource(VFX_GPU_RUNTIME_RESOURCE_KEY)) continue;
583
+ const camera = options.camera.read(world);
584
+ if (camera === void 0) continue;
585
+ const runtime = world.getResource(VFX_GPU_RUNTIME_RESOURCE_KEY);
586
+ const intents = runtime.snapshot().filter((intent) => {
587
+ if (options.playerConsumption?.isEnabled(world, intent.player) === false) return false;
588
+ const requirements = intent.emitter.reflection.dataInterfaces ?? [];
589
+ if (requirements.length === 0) return true;
590
+ return options.dataInterfaces?.resolve(requirements, intent.instanceGeneration).ok === true;
591
+ });
592
+ extracted.push({ world, runtime, camera, intents });
593
+ }
594
+ return ok({ worlds: extracted, frameNumber: context.frameNumber });
595
+ },
596
+ prepare: (frame, context) => {
597
+ const gpu = context.gpu;
598
+ if (gpu === void 0) {
599
+ return err(new RenderFeatureStageFailedError(IDENTITY, -1, "prepare", "renderer-recover"));
600
+ }
601
+ let pendingProgramError;
602
+ const preparedIntents = [];
603
+ for (const entry of frame.worlds) {
604
+ for (const intent of entry.intents) {
605
+ const key = keyOf(entry.world, intent);
606
+ let state = states.get(key);
607
+ const candidate = validatedStagePlan(
608
+ intent.emitter.reflection.stages,
609
+ intent.instanceGeneration
610
+ );
611
+ if (!candidate.ok) {
612
+ if (state === void 0) {
613
+ return err(new RenderFeatureStageFailedError(IDENTITY, -1, "prepare", "next-frame"));
614
+ }
615
+ const observation2 = observeStagePlan(
616
+ candidate,
617
+ intent.instanceGeneration,
618
+ state.lastKnownGoodStage
619
+ );
620
+ state.stagePlan = observation2.validatedStagePlan;
621
+ state.stageReadiness = observation2.stageReadiness;
622
+ state.stageOutput = observation2.stageOutput;
623
+ continue;
624
+ }
625
+ if (state !== void 0 && state.fingerprint !== intent.programFingerprint) {
626
+ states.delete(key);
627
+ state = void 0;
628
+ }
629
+ state ??= stateFor(entry.world, intent);
630
+ const observation = observeStagePlan(
631
+ candidate,
632
+ intent.instanceGeneration,
633
+ state.lastKnownGoodStage
634
+ );
635
+ state.stagePlan = observation.validatedStagePlan;
636
+ state.lastKnownGoodStage = candidate.value;
637
+ state.stageReadiness = observation.stageReadiness;
638
+ state.stageOutput = observation.stageOutput;
639
+ const program = gpu.prepareProgram(`${state.names}.program`, {
640
+ wgsl: intent.emitter.wgsl,
641
+ entryPoints: intent.emitter.reflection.entryPoints,
642
+ bindings: intent.emitter.reflection.bindings
643
+ });
644
+ if (!program.ok) {
645
+ if (program.error.code !== "render-feature-preparation-failed" || program.error.detail.recovery !== "next-frame") {
646
+ return program;
647
+ }
648
+ pendingProgramError ??= program.error;
649
+ }
650
+ preparedIntents.push({ entry, intent, state });
651
+ }
652
+ }
653
+ if (pendingProgramError !== void 0) return err(pendingProgramError);
654
+ let pendingGraphicsError;
655
+ for (const { entry, intent, state } of preparedIntents) {
656
+ if (intent.reset) state.indirectInitialized = false;
657
+ state.lastIntent = intent;
658
+ const base = state.names;
659
+ const program = gpu.prepareProgram(`${base}.program`, {
660
+ wgsl: intent.emitter.wgsl,
661
+ entryPoints: intent.emitter.reflection.entryPoints,
662
+ bindings: intent.emitter.reflection.bindings
663
+ });
664
+ if (!program.ok) return program;
665
+ const prepare = (name, size, usage, data) => gpu.prepareBuffer(`${base}.${name}`, {
666
+ size,
667
+ usage,
668
+ ...data === void 0 ? {} : { data }
669
+ });
670
+ const particles = prepare(
671
+ "particles",
672
+ state.capacity * PARTICLE_BYTES,
673
+ ["storage"],
674
+ intent.reset ? resetData(state.capacity * PARTICLE_BYTES) : void 0
675
+ );
676
+ if (!particles.ok) return particles;
677
+ const aliveIndices = prepare("alive-indices", state.capacity * 4, ["storage"]);
678
+ if (!aliveIndices.ok) return aliveIndices;
679
+ const counters = prepare(
680
+ "counters",
681
+ COUNTERS_BYTES,
682
+ ["storage"],
683
+ intent.reset ? resetData(COUNTERS_BYTES) : void 0
684
+ );
685
+ if (!counters.ok) return counters;
686
+ const indirect = prepare("indirect", Math.max(1, intent.emitter.renderers.length) * 20, [
687
+ "storage",
688
+ "indirect"
689
+ ]);
690
+ if (!indirect.ok) return indirect;
691
+ const scratchBytes = (state.capacity * 2 + Math.ceil(state.capacity / WORKGROUP_SIZE)) * 4;
692
+ const scratch = prepare(
693
+ "scratch",
694
+ scratchBytes,
695
+ ["storage"],
696
+ intent.reset ? resetData(scratchBytes) : void 0
697
+ );
698
+ if (!scratch.ok) return scratch;
699
+ const billboardInstances = prepare(
700
+ "billboard-instances",
701
+ state.capacity * Math.max(BILLBOARD_INSTANCE_BYTES, MESH_INSTANCE_BYTES),
702
+ ["storage", "vertex"]
703
+ );
704
+ if (!billboardInstances.ok) return billboardInstances;
705
+ const events = prepare(
706
+ "events",
707
+ eventCapacity(intent.emitter) * VFX_EVENT_BYTES,
708
+ ["storage"],
709
+ intent.reset ? resetData(eventCapacity(intent.emitter) * VFX_EVENT_BYTES) : void 0
710
+ );
711
+ if (!events.ok) return events;
712
+ const initialEventInputs = prepare(
713
+ "event-inputs",
714
+ eventInputCapacity(intent.emitter) * VFX_EVENT_INPUT_BYTES,
715
+ ["storage"],
716
+ encodeEventInputs(intent)
717
+ );
718
+ if (!initialEventInputs.ok) return initialEventInputs;
719
+ state.refs = {
720
+ program: program.value,
721
+ particles: particles.value,
722
+ aliveIndices: aliveIndices.value,
723
+ counters: counters.value,
724
+ indirect: indirect.value,
725
+ scratch: scratch.value,
726
+ billboardInstances: billboardInstances.value,
727
+ eventInputs: initialEventInputs.value,
728
+ events: events.value
729
+ };
730
+ const ringIndex = intent.tick % MAX_TICK_RINGS;
731
+ const runtime = gpu.prepareBuffer(`${base}.runtime.${ringIndex}`, {
732
+ size: RUNTIME_BYTES,
733
+ usage: ["uniform"],
734
+ data: runtimeData(intent, entry.camera, void 0, emitterTransform(entry.world, intent))
735
+ });
736
+ if (!runtime.ok) return runtime;
737
+ const refs = state.refs;
738
+ const updatedEventInputs = gpu.prepareBuffer(`${base}.event-inputs`, {
739
+ size: eventInputCapacity(intent.emitter) * VFX_EVENT_INPUT_BYTES,
740
+ usage: ["storage"],
741
+ data: encodeEventInputs(intent)
742
+ });
743
+ if (!updatedEventInputs.ok) return updatedEventInputs;
744
+ const bindings = gpu.prepareBindings(`${base}.bindings.${ringIndex}`, {
745
+ program: refs.program,
746
+ entries: [
747
+ { binding: 0, buffer: refs.particles },
748
+ { binding: 1, buffer: runtime.value },
749
+ { binding: 2, buffer: refs.aliveIndices },
750
+ { binding: 3, buffer: refs.counters },
751
+ { binding: 4, buffer: refs.indirect },
752
+ { binding: 5, buffer: refs.scratch },
753
+ { binding: 6, buffer: refs.billboardInstances },
754
+ { binding: 8, buffer: updatedEventInputs.value },
755
+ { binding: 9, buffer: refs.events }
756
+ ]
757
+ });
758
+ if (!bindings.ok) return bindings;
759
+ state.rings[ringIndex] = {
760
+ runtime: runtime.value,
761
+ bindings: bindings.value
762
+ };
763
+ }
764
+ for (const [key, state] of states) {
765
+ const extracted = frame.worlds.find((entry) => entry.world === state.world);
766
+ if (extracted === void 0 || !extracted.runtime.hasPlayer(state.player)) {
767
+ states.delete(key);
768
+ continue;
769
+ }
770
+ if (options.playerConsumption?.isEnabled(state.world, state.player) === false) {
771
+ state.projections = [];
772
+ state.draws = [];
773
+ state.depthSampledDraws = [];
774
+ state.colorTarget = void 0;
775
+ state.depthTarget = void 0;
776
+ continue;
777
+ }
778
+ const intent = state.lastIntent;
779
+ const refs = state.refs;
780
+ if (intent === void 0 || refs === void 0) continue;
781
+ const retained = gpu.retainBindings([
782
+ ...state.rings.flatMap((ring) => ring === void 0 ? [] : [ring.bindings]),
783
+ ...state.projections.map((projection) => projection.ring.bindings)
784
+ ]);
785
+ if (!retained.ok) return retained;
786
+ if (!extracted.runtime.isEmitterSessionEnabled(state.player, state.emitterId)) {
787
+ state.projections = [];
788
+ state.draws = [];
789
+ state.depthSampledDraws = [];
790
+ continue;
791
+ }
792
+ const renderers = intent.emitter.renderers;
793
+ if (renderers.length === 0) continue;
794
+ const localToWorld = emitterTransform(state.world, intent);
795
+ const visible = emitterVisible(intent, extracted.camera, localToWorld);
796
+ extracted.runtime.setEmitterCameraVisibility(state.player, state.emitterId, visible);
797
+ const currentIntents = extracted.intents.filter(
798
+ (candidate) => candidate.player === state.player && candidate.emitter.id === state.emitterId
799
+ );
800
+ if (!visible) {
801
+ state.culled = true;
802
+ state.projections = [];
803
+ state.draws = [];
804
+ state.depthSampledDraws = [];
805
+ continue;
806
+ }
807
+ if (state.culled && intent.emitter.simulationWhenCulled === "restart-on-visible" && !currentIntents.some((candidate) => candidate.reset)) {
808
+ state.projections = [];
809
+ state.draws = [];
810
+ state.depthSampledDraws = [];
811
+ continue;
812
+ }
813
+ state.culled = false;
814
+ const colorTarget = target(context.targets, "scene-color");
815
+ const depthTarget = target(context.targets, "scene-depth");
816
+ const softParticle = requiresSceneDepth(intent);
817
+ if (softParticle && depthTarget === void 0) continue;
818
+ const eventRing = state.rings[intent.tick % MAX_TICK_RINGS];
819
+ if (eventRing === void 0) continue;
820
+ const meshes = renderers.map(
821
+ (renderer) => renderer.kind === "mesh" ? options.mesh?.read(state.world, renderer.mesh) : void 0
822
+ );
823
+ const missingMesh = renderers.find(
824
+ (renderer, index) => renderer.kind === "mesh" && meshes[index]?.submeshes[renderer.submesh ?? 0] === void 0
825
+ );
826
+ if (missingMesh?.kind === "mesh") {
827
+ pendingGraphicsError ??= missingMeshPreparation(missingMesh.mesh);
828
+ continue;
829
+ }
830
+ const indirectWords = new Uint32Array(renderers.length * 5);
831
+ for (const [index, renderer] of renderers.entries()) {
832
+ const mesh = meshes[index];
833
+ const submesh = renderer.kind === "mesh" ? mesh?.submeshes[renderer.submesh ?? 0] : void 0;
834
+ const topologyPlan = renderer.kind === "ribbon" || renderer.kind === "trail" || renderer.kind === "beam" ? createTopologyResourcePlan(renderer) : void 0;
835
+ if (topologyPlan !== void 0 && !topologyPlan.ok)
836
+ return err(new RenderFeatureStageFailedError(IDENTITY, -1, "prepare", "next-frame"));
837
+ indirectWords[index * 5] = renderer.kind === "billboard" ? 6 : renderer.kind === "ribbon" || renderer.kind === "trail" || renderer.kind === "beam" ? 6 : mesh?.indices === void 0 ? submesh?.vertexCount ?? 0 : submesh?.indexCount ?? 0;
838
+ indirectWords[index * 5 + 2] = renderer.kind === "mesh" && mesh?.indices !== void 0 ? submesh?.indexOffset ?? 0 : 0;
839
+ }
840
+ const indirectInit = gpu.prepareBuffer(`${state.names}.indirect`, {
841
+ size: Math.max(1, renderers.length) * 20,
842
+ usage: ["storage", "indirect"],
843
+ ...state.indirectInitialized ? {} : { data: indirectWords }
844
+ });
845
+ if (!indirectInit.ok) return indirectInit;
846
+ state.indirectInitialized = true;
847
+ const draws = [];
848
+ const depthSampledDraws = [];
849
+ const projections = [];
850
+ for (const [rendererIndex, renderer] of renderers.entries()) {
851
+ const isBillboard = renderer.kind === "billboard";
852
+ const isTopology = renderer.kind === "ribbon" || renderer.kind === "trail" || renderer.kind === "beam";
853
+ const topologyPlan = isTopology ? createTopologyResourcePlan(renderer) : void 0;
854
+ if (topologyPlan !== void 0 && !topologyPlan.ok)
855
+ return err(new RenderFeatureStageFailedError(IDENTITY, -1, "prepare", "next-frame"));
856
+ const mesh = meshes[rendererIndex];
857
+ const submesh = renderer.kind === "mesh" ? mesh?.submeshes[renderer.submesh ?? 0] : void 0;
858
+ const indexFormat = mesh?.indices instanceof Uint32Array ? "uint32" : "uint16";
859
+ let material = options.material?.read(state.world, renderer.material);
860
+ if (material !== void 0) {
861
+ const resolvedMaterial = context.graphics.resolveMaterialAsset(material);
862
+ if (!resolvedMaterial.ok) {
863
+ pendingGraphicsError ??= resolvedMaterial.error;
864
+ continue;
865
+ }
866
+ material = resolvedMaterial.value;
867
+ }
868
+ const materialPass = particleMaterialPass(renderer.kind, material);
869
+ const materialKey = materialResourceKey(materialPass.shader);
870
+ const samplesSceneDepth = particleMaterialUsesSceneDepth(
871
+ context.graphics.getMaterialShaderBindingContract(materialPass.shader)
872
+ );
873
+ const particleBlend = renderer.kind === "billboard" ? renderer.blend : "alpha";
874
+ const projectionInstances = gpu.prepareBuffer(
875
+ `${state.names}.renderer.${rendererIndex}.instances`,
876
+ {
877
+ size: isTopology ? topologyPlan?.ok ? topologyPlan.value.vertexBytes : 0 : state.capacity * (isBillboard ? BILLBOARD_INSTANCE_BYTES : MESH_INSTANCE_BYTES),
878
+ usage: ["storage", "vertex"]
879
+ }
880
+ );
881
+ if (!projectionInstances.ok) return projectionInstances;
882
+ const projectionHistory = gpu.prepareBuffer(
883
+ `${state.names}.renderer.${rendererIndex}.history`,
884
+ {
885
+ size: renderer.kind === "trail" ? Math.max(16, renderer.capacity * renderer.historyLength * 16) : 16,
886
+ usage: ["storage"],
887
+ ...intent.reset ? {
888
+ data: resetData(
889
+ renderer.kind === "trail" ? Math.max(16, renderer.capacity * renderer.historyLength * 16) : 16
890
+ )
891
+ } : {}
892
+ }
893
+ );
894
+ if (!projectionHistory.ok) return projectionHistory;
895
+ const projectionRuntime = gpu.prepareBuffer(
896
+ `${state.names}.renderer.${rendererIndex}.runtime`,
897
+ {
898
+ size: RUNTIME_BYTES,
899
+ usage: ["uniform"],
900
+ data: runtimeData(
901
+ { ...intent, fixedDelta: 0, spawnCount: 0 },
902
+ extracted.camera,
903
+ material,
904
+ localToWorld,
905
+ renderer,
906
+ rendererIndex
907
+ )
908
+ }
909
+ );
910
+ if (!projectionRuntime.ok) return projectionRuntime;
911
+ const projectionBindings = gpu.prepareBindings(
912
+ `${state.names}.renderer.${rendererIndex}.bindings`,
913
+ {
914
+ program: refs.program,
915
+ entries: [
916
+ { binding: 0, buffer: refs.particles },
917
+ { binding: 1, buffer: projectionRuntime.value },
918
+ { binding: 2, buffer: refs.aliveIndices },
919
+ { binding: 3, buffer: refs.counters },
920
+ { binding: 4, buffer: refs.indirect },
921
+ { binding: 5, buffer: projectionHistory.value },
922
+ { binding: 6, buffer: projectionInstances.value },
923
+ { binding: 8, buffer: refs.eventInputs },
924
+ { binding: 9, buffer: refs.events }
925
+ ]
926
+ }
927
+ );
928
+ if (!projectionBindings.ok) return projectionBindings;
929
+ projections.push({
930
+ kind: renderer.kind,
931
+ instances: projectionInstances.value,
932
+ ring: {
933
+ runtime: projectionRuntime.value,
934
+ bindings: projectionBindings.value
935
+ },
936
+ workgroups: Math.ceil(
937
+ (renderer.kind === "trail" ? renderer.capacity * Math.max(1, renderer.historyLength - 1) : renderer.kind === "ribbon" || renderer.kind === "beam" ? renderer.capacity : state.capacity) / WORKGROUP_SIZE
938
+ ),
939
+ ...renderer.kind === "trail" ? { historyWorkgroups: Math.ceil(renderer.capacity / WORKGROUP_SIZE) } : {},
940
+ ...renderer.kind === "billboard" ? { sorting: renderer.sorting ?? "none" } : {}
941
+ });
942
+ const pipeline = context.graphics.preparePipeline(
943
+ `${state.names}.renderer.${rendererIndex}.${renderer.kind}.pipeline.${materialKey}`,
944
+ {
945
+ shader: materialPass.shader,
946
+ vertexLayout: isBillboard ? RENDER_FEATURE_VERTEX_LAYOUTS.billboardMaterialInstance : isTopology ? RENDER_FEATURE_VERTEX_LAYOUTS.topologySegmentInstance : RENDER_FEATURE_VERTEX_LAYOUTS.meshGeometryMaterialInstance,
947
+ colorFormats: [colorTarget?.format ?? "rgba8unorm-srgb"],
948
+ ...depthTarget === void 0 ? {} : { depthFormat: depthTarget.format },
949
+ sampleCount: colorTarget?.sampleCount ?? 1,
950
+ topology: submesh?.topology ?? "triangle-list",
951
+ ...mesh?.indices === void 0 ? {} : { indexFormat },
952
+ ...materialPass.renderState !== void 0 ? { renderState: materialPass.renderState } : isBillboard || isTopology ? {
953
+ renderState: {
954
+ cullMode: "none",
955
+ depthCompare: "less-equal",
956
+ depthWriteEnabled: softParticle || isTopology ? false : particleBlend === "opaque-cutout",
957
+ ...particleBlend === "opaque-cutout" ? {} : {
958
+ blend: {
959
+ color: {
960
+ srcFactor: "one",
961
+ dstFactor: particleBlend === "additive" ? "one" : "one-minus-src-alpha",
962
+ operation: "add"
963
+ },
964
+ alpha: {
965
+ srcFactor: "one",
966
+ dstFactor: "one-minus-src-alpha",
967
+ operation: "add"
968
+ }
969
+ }
970
+ }
971
+ }
972
+ } : {}
973
+ }
974
+ );
975
+ if (!pipeline.ok) {
976
+ if (pipeline.error.code !== "render-feature-preparation-failed" || pipeline.error.detail.recovery !== "next-frame") {
977
+ return pipeline;
978
+ }
979
+ pendingGraphicsError ??= pipeline.error;
980
+ continue;
981
+ }
982
+ const graphicsBindings = context.graphics.prepareBindings(
983
+ `${state.names}.renderer.${rendererIndex}.${renderer.kind}.binding.${materialKey}`,
984
+ {
985
+ pipeline: pipeline.value,
986
+ values: {
987
+ group: 0,
988
+ shader: materialPass.shader,
989
+ ...samplesSceneDepth && depthTarget !== void 0 ? { sceneDepth: depthTarget } : {}
990
+ }
991
+ }
992
+ );
993
+ if (!graphicsBindings.ok) return graphicsBindings;
994
+ const materialBindings = !particleMaterialUsesBindings(material) ? void 0 : context.graphics.prepareBindings(
995
+ `${state.names}.renderer.${rendererIndex}.${renderer.kind}.material-binding.${materialKey}`,
996
+ {
997
+ pipeline: pipeline.value,
998
+ values: {
999
+ group: 1,
1000
+ material: {
1001
+ world: frame.worlds.findIndex((entry) => entry.world === state.world),
1002
+ guid: renderer.material
1003
+ }
1004
+ }
1005
+ }
1006
+ );
1007
+ if (materialBindings !== void 0 && !materialBindings.ok) return materialBindings;
1008
+ const drawBindings = [
1009
+ graphicsBindings.value,
1010
+ ...materialBindings === void 0 ? [] : [materialBindings.value]
1011
+ ];
1012
+ if (isBillboard || isTopology) {
1013
+ const vertexData2 = context.graphics.prepareVertexData(
1014
+ `${state.names}.${isTopology ? renderer.kind : "billboard"}.vertices`,
1015
+ {
1016
+ layout: isTopology ? RENDER_FEATURE_VERTEX_LAYOUTS.topologySegmentInstance : RENDER_FEATURE_VERTEX_LAYOUTS.billboardMaterialInstance,
1017
+ buffer: projectionInstances.value
1018
+ }
1019
+ );
1020
+ if (!vertexData2.ok) return vertexData2;
1021
+ const draw = {
1022
+ kind: "draw-indirect",
1023
+ pipeline: pipeline.value,
1024
+ bindings: drawBindings,
1025
+ vertexData: [{ slot: 0, resource: vertexData2.value }],
1026
+ command: { buffer: refs.indirect, offset: rendererIndex * 20 }
1027
+ };
1028
+ (samplesSceneDepth ? depthSampledDraws : draws).push(draw);
1029
+ continue;
1030
+ }
1031
+ if (mesh === void 0) {
1032
+ return err(new RenderFeatureStageFailedError(IDENTITY, -1, "prepare", "next-frame"));
1033
+ }
1034
+ const geometryData = canonicalMeshVertices(mesh);
1035
+ const geometryBuffer = gpu.prepareBuffer(
1036
+ `${state.names}.renderer.${rendererIndex}.mesh.geometry-buffer`,
1037
+ {
1038
+ size: geometryData.byteLength,
1039
+ usage: ["vertex"],
1040
+ data: geometryData
1041
+ }
1042
+ );
1043
+ if (!geometryBuffer.ok) return geometryBuffer;
1044
+ const geometry = context.graphics.prepareVertexData(
1045
+ `${state.names}.renderer.${rendererIndex}.mesh.geometry`,
1046
+ {
1047
+ layout: RENDER_FEATURE_VERTEX_LAYOUTS.meshGeometryMaterialInstance,
1048
+ buffer: geometryBuffer.value
1049
+ }
1050
+ );
1051
+ if (!geometry.ok) return geometry;
1052
+ const instances = context.graphics.prepareVertexData(
1053
+ `${state.names}.renderer.${rendererIndex}.mesh.instances`,
1054
+ {
1055
+ layout: RENDER_FEATURE_VERTEX_LAYOUTS.meshGeometryMaterialInstance,
1056
+ buffer: projectionInstances.value
1057
+ }
1058
+ );
1059
+ if (!instances.ok) return instances;
1060
+ const indexBuffer = mesh.indices === void 0 ? void 0 : gpu.prepareBuffer(`${state.names}.renderer.${rendererIndex}.mesh.index-buffer`, {
1061
+ size: mesh.indices.byteLength,
1062
+ usage: ["index"],
1063
+ data: mesh.indices
1064
+ });
1065
+ if (indexBuffer !== void 0 && !indexBuffer.ok) return indexBuffer;
1066
+ const indices = indexBuffer === void 0 ? void 0 : context.graphics.prepareIndexData(
1067
+ `${state.names}.renderer.${rendererIndex}.mesh.indices`,
1068
+ {
1069
+ format: indexFormat,
1070
+ buffer: indexBuffer.value
1071
+ }
1072
+ );
1073
+ if (indices !== void 0 && !indices.ok) return indices;
1074
+ const vertexData = [
1075
+ { slot: 0, resource: geometry.value },
1076
+ { slot: 1, resource: instances.value }
1077
+ ];
1078
+ draws.push(
1079
+ indices === void 0 ? {
1080
+ kind: "draw-indirect",
1081
+ pipeline: pipeline.value,
1082
+ bindings: drawBindings,
1083
+ vertexData,
1084
+ command: { buffer: refs.indirect, offset: rendererIndex * 20 }
1085
+ } : {
1086
+ kind: "draw-indexed-indirect",
1087
+ pipeline: pipeline.value,
1088
+ bindings: drawBindings,
1089
+ vertexData,
1090
+ indexData: { resource: indices.value, format: indexFormat },
1091
+ command: { buffer: refs.indirect, offset: rendererIndex * 20 }
1092
+ }
1093
+ );
1094
+ }
1095
+ state.projections = projections;
1096
+ state.draws = draws;
1097
+ state.depthSampledDraws = depthSampledDraws;
1098
+ state.colorTarget = colorTarget;
1099
+ state.depthTarget = depthTarget;
1100
+ }
1101
+ if (pendingGraphicsError !== void 0) return err(pendingGraphicsError);
1102
+ return ok(void 0);
1103
+ },
1104
+ contribute: (frame, context) => {
1105
+ let dispatchCount = 0;
1106
+ let indirectDrawCount = 0;
1107
+ let subjectOutputCount = 0;
1108
+ for (const state of states.values()) {
1109
+ const refs = state.refs;
1110
+ const firstProjection = state.projections[0];
1111
+ if (refs === void 0) continue;
1112
+ const extracted = frame.worlds.find((entry) => entry.world === state.world);
1113
+ if (extracted === void 0) continue;
1114
+ if (options.playerConsumption?.isEnabled(state.world, state.player) === false) continue;
1115
+ if (!extracted.runtime.isEmitterSessionEnabled(state.player, state.emitterId)) continue;
1116
+ const currentIntents = extracted.intents.filter(
1117
+ (intent) => intent.player === state.player && intent.emitter.id === state.emitterId
1118
+ );
1119
+ const intents = currentIntents.some(
1120
+ (intent) => intent.programFingerprint === state.fingerprint
1121
+ ) ? currentIntents.filter((intent) => intent.programFingerprint === state.fingerprint) : state.lastIntent === void 0 ? [] : [state.lastIntent];
1122
+ const groups = Math.ceil(state.capacity / WORKGROUP_SIZE);
1123
+ const dispatches = intents.flatMap((intent) => {
1124
+ const bindings = state.rings[intent.tick % MAX_TICK_RINGS]?.bindings;
1125
+ if (bindings === void 0) return [];
1126
+ return [
1127
+ { entryPoint: "forgeax_vfx_spawn_main", workgroups: [groups], bindings },
1128
+ { entryPoint: "forgeax_vfx_update_main", workgroups: [groups], bindings },
1129
+ ...stageDispatches(
1130
+ state.stagePlan ?? {
1131
+ stages: [],
1132
+ generation: intent.instanceGeneration
1133
+ },
1134
+ groups,
1135
+ bindings
1136
+ ),
1137
+ { entryPoint: "forgeax_vfx_scan_blocks_main", workgroups: [groups], bindings },
1138
+ {
1139
+ entryPoint: "forgeax_vfx_scan_block_offsets_main",
1140
+ workgroups: [1],
1141
+ bindings
1142
+ },
1143
+ { entryPoint: "forgeax_vfx_add_offsets_main", workgroups: [groups], bindings },
1144
+ { entryPoint: "forgeax_vfx_compact_main", workgroups: [groups], bindings },
1145
+ {
1146
+ entryPoint: "forgeax_vfx_event_main",
1147
+ workgroups: [Math.ceil(eventInputCapacity(intent.emitter) / 64)],
1148
+ bindings
1149
+ }
1150
+ ];
1151
+ });
1152
+ for (const projection of state.projections) {
1153
+ if (projection.kind === "billboard" && projection.sorting === "back-to-front") {
1154
+ dispatches.push({
1155
+ entryPoint: "forgeax_vfx_sort_main",
1156
+ workgroups: [1],
1157
+ bindings: projection.ring.bindings
1158
+ });
1159
+ }
1160
+ if (projection.kind === "trail") {
1161
+ dispatches.push({
1162
+ entryPoint: "forgeax_vfx_trail_history_main",
1163
+ workgroups: [projection.historyWorkgroups ?? 1],
1164
+ bindings: projection.ring.bindings
1165
+ });
1166
+ }
1167
+ dispatches.push({
1168
+ entryPoint: projection.kind === "billboard" ? "forgeax_vfx_billboard_main" : projection.kind === "mesh" ? "forgeax_vfx_mesh_main" : `forgeax_vfx_${projection.kind}_main`,
1169
+ workgroups: [projection.workgroups],
1170
+ bindings: projection.ring.bindings
1171
+ });
1172
+ }
1173
+ const passBindings = firstProjection?.ring.bindings ?? intents.map((intent) => state.rings[intent.tick % MAX_TICK_RINGS]?.bindings).find((bindings) => bindings !== void 0);
1174
+ if (passBindings === void 0 || dispatches.length === 0) continue;
1175
+ const computePassIdentity = `${state.names}.simulate-and-project`;
1176
+ const compute = context.staging.addComputePass(computePassIdentity, {
1177
+ program: refs.program,
1178
+ bindings: passBindings,
1179
+ dispatches
1180
+ });
1181
+ if (!compute.ok) return compute;
1182
+ dispatchCount += dispatches.length;
1183
+ for (const intent of intents) {
1184
+ extracted.runtime.markEventDispatched(state.player, intent.eventCounters);
1185
+ }
1186
+ for (const [drawKind, passDraws] of [
1187
+ ["regular", state.draws],
1188
+ ["depth-sampled", state.depthSampledDraws]
1189
+ ]) {
1190
+ if (passDraws.length === 0) continue;
1191
+ const samplesDepth = drawKind === "depth-sampled";
1192
+ const draw = context.staging.addGraphicsPass(
1193
+ `${state.names}.draw.${drawKind}`,
1194
+ {
1195
+ attachments: {
1196
+ colors: [
1197
+ {
1198
+ resource: state.colorTarget ?? "swapchain",
1199
+ format: state.colorTarget?.format ?? "rgba8unorm-srgb",
1200
+ loadOp: "load",
1201
+ storeOp: "store"
1202
+ }
1203
+ ],
1204
+ ...state.depthTarget === void 0 ? {} : {
1205
+ depthStencil: {
1206
+ resource: state.depthTarget,
1207
+ format: state.depthTarget.format,
1208
+ depthLoadOp: "load",
1209
+ depthStoreOp: "store"
1210
+ }
1211
+ }
1212
+ },
1213
+ ...state.depthTarget === void 0 || !samplesDepth ? {} : { sampledTargets: [state.depthTarget] },
1214
+ temporalCoverage: "reactive",
1215
+ draws: passDraws
1216
+ },
1217
+ { dependsOn: [{ featureIdentity: IDENTITY, passIdentity: computePassIdentity }] }
1218
+ );
1219
+ if (!draw.ok) return draw;
1220
+ indirectDrawCount += passDraws.filter(
1221
+ (record) => record.kind === "draw-indirect" || record.kind === "draw-indexed-indirect"
1222
+ ).length;
1223
+ subjectOutputCount += passDraws.length;
1224
+ }
1225
+ }
1226
+ for (const entry of frame.worlds) {
1227
+ const last = entry.intents.at(-1);
1228
+ if (last !== void 0) entry.runtime.commit(last.sequence);
1229
+ }
1230
+ lastObservation = Object.freeze({
1231
+ frameNumber: frame.frameNumber,
1232
+ dispatches: dispatchCount,
1233
+ indirectDraws: indirectDrawCount,
1234
+ subjectOutputs: subjectOutputCount
1235
+ });
1236
+ return ok(void 0);
1237
+ },
1238
+ recover: () => {
1239
+ const runtimes = /* @__PURE__ */ new Set();
1240
+ for (const state of states.values()) {
1241
+ if (!state.world.hasResource(VFX_GPU_RUNTIME_RESOURCE_KEY)) continue;
1242
+ runtimes.add(state.world.getResource(VFX_GPU_RUNTIME_RESOURCE_KEY));
1243
+ }
1244
+ for (const runtime of runtimes) runtime.recover();
1245
+ states.clear();
1246
+ return ok(void 0);
1247
+ },
1248
+ dispose: () => {
1249
+ states.clear();
1250
+ lastObservation = Object.freeze({
1251
+ frameNumber: -1,
1252
+ dispatches: 0,
1253
+ indirectDraws: 0,
1254
+ subjectOutputs: 0
1255
+ });
1256
+ return ok(void 0);
1257
+ },
1258
+ inspect: () => lastObservation
1259
+ };
1260
+ }
1261
+ function duplicate(token, providerId) {
1262
+ return {
1263
+ code: "vfx-data-interface-duplicate",
1264
+ expected: "at most one provider for each reflected Data Interface token",
1265
+ hint: `remove provider ${providerId} or the existing provider for ${token} and retry`,
1266
+ detail: { token, providerId }
1267
+ };
1268
+ }
1269
+ function availableProvider(token, kind, bindingType, source) {
1270
+ return {
1271
+ id: `${token}-provider`,
1272
+ token,
1273
+ kind,
1274
+ bindingType,
1275
+ provide: (generation) => {
1276
+ if (!source.available(generation)) {
1277
+ return err({
1278
+ code: "vfx-data-interface-missing",
1279
+ expected: `an available ${kind} provider for ${token}`,
1280
+ hint: `make ${token} available for generation ${generation} and retry rendering`,
1281
+ detail: { token, providerId: `${token}-provider` }
1282
+ });
1283
+ }
1284
+ const resource = {
1285
+ token,
1286
+ kind,
1287
+ bindingType,
1288
+ generation
1289
+ };
1290
+ return ok(resource);
1291
+ }
1292
+ };
1293
+ }
1294
+ function createCameraProvider(source) {
1295
+ return availableProvider("vfx:camera", "camera", "uniform", source);
1296
+ }
1297
+ function createSceneDepthProvider(source) {
1298
+ return availableProvider("vfx:scene-depth", "scene-depth", "sampled-depth", source);
1299
+ }
1300
+ function createVfxDataInterfaceRegistry(initialProviders = []) {
1301
+ const entries = /* @__PURE__ */ new Map();
1302
+ let registrationError;
1303
+ let lastSnapshot;
1304
+ const registry = {
1305
+ get providers() {
1306
+ return Object.freeze([...entries.values()]);
1307
+ },
1308
+ get snapshot() {
1309
+ return lastSnapshot;
1310
+ },
1311
+ register(provider) {
1312
+ if (entries.has(provider.token)) {
1313
+ const error = duplicate(provider.token, provider.id);
1314
+ registrationError ??= error;
1315
+ return err(error);
1316
+ }
1317
+ entries.set(provider.token, provider);
1318
+ return ok(void 0);
1319
+ },
1320
+ resolve(requirements, generation) {
1321
+ const result = registrationError === void 0 ? resolveVfxDataInterfaces(requirements, [...entries.values()], generation) : err(registrationError);
1322
+ lastSnapshot = Object.freeze({ generation, result });
1323
+ return result;
1324
+ }
1325
+ };
1326
+ for (const provider of initialProviders) registry.register(provider);
1327
+ return registry;
1328
+ }
1329
+ function installVfxRuntimeDecoder(registry) {
1330
+ return registry.installDecoder(vfxGpuEffectContribution.kind, vfxGpuEffectContribution.decoder);
1331
+ }
1332
+ function failure2(code, expected, hint, cause) {
1333
+ return { code, expected, hint, detail: { cause } };
1334
+ }
1335
+ function controlFailure(code, expected, hint, detail = {}) {
1336
+ return { code, expected, hint, detail };
1337
+ }
1338
+ function createVfxRuntimeHost(options) {
1339
+ const decoderLeases = /* @__PURE__ */ new WeakMap();
1340
+ const worlds = /* @__PURE__ */ new WeakMap();
1341
+ const pausedPlayers = /* @__PURE__ */ new WeakMap();
1342
+ let nextGeneration = 1;
1343
+ const dataInterfaces = createVfxDataInterfaceRegistry(options.providers);
1344
+ const readRenderAsset = (world, guid, kind) => {
1345
+ const attached = worlds.get(world);
1346
+ if (attached === void 0) return void 0;
1347
+ const key = `${kind}:${guid.toLowerCase()}`;
1348
+ const cached = attached.renderAssets.get(key);
1349
+ if (cached?.kind === kind) return cached;
1350
+ const resolved = attached.resolver?.lookup(guid);
1351
+ if (resolved?.kind === kind) {
1352
+ attached.renderAssets.set(key, resolved);
1353
+ return resolved;
1354
+ }
1355
+ if (!attached.pendingRenderAssets.has(key)) {
1356
+ const epoch = attached.catalogEpoch;
1357
+ const request = attached.assets.load(guid, kind).then((result) => {
1358
+ if (!result.ok || worlds.get(world) !== attached || attached.catalogEpoch !== epoch)
1359
+ return;
1360
+ attached.renderAssets.set(key, result.value);
1361
+ }).catch(() => void 0);
1362
+ attached.pendingRenderAssets.set(key, request);
1363
+ void request.finally(() => {
1364
+ if (attached.pendingRenderAssets.get(key) === request) {
1365
+ attached.pendingRenderAssets.delete(key);
1366
+ }
1367
+ });
1368
+ }
1369
+ return void 0;
1370
+ };
1371
+ const feature = gpuParticleRenderFeature({
1372
+ camera: options.camera,
1373
+ dataInterfaces,
1374
+ material: { read: (world, guid) => readRenderAsset(world, guid, "material") },
1375
+ mesh: { read: (world, guid) => readRenderAsset(world, guid, "mesh") },
1376
+ playerConsumption: {
1377
+ isEnabled: (world, player) => !pausedPlayers.get(world)?.has(player)
1378
+ }
1379
+ });
1380
+ return {
1381
+ feature,
1382
+ dataInterfaces,
1383
+ inspect: (world) => {
1384
+ const attached = worlds.get(world);
1385
+ if (attached === void 0 || !world.hasResource(VFX_GPU_RUNTIME_RESOURCE_KEY))
1386
+ return void 0;
1387
+ const runtime = world.getResource(VFX_GPU_RUNTIME_RESOURCE_KEY);
1388
+ return Object.freeze({
1389
+ generation: attached.generation,
1390
+ renderGeneration: runtime.renderGeneration,
1391
+ players: runtime.inspectPlayers(),
1392
+ diagnostics: runtime.diagnostics()
1393
+ });
1394
+ },
1395
+ acquireControl: (world) => {
1396
+ const attached = worlds.get(world);
1397
+ if (attached === void 0) {
1398
+ return err(
1399
+ controlFailure(
1400
+ "vfx-host-control-world-detached",
1401
+ "an attached VFX Runtime World",
1402
+ "attach the World to this VfxRuntimeHost before acquiring controls"
1403
+ )
1404
+ );
1405
+ }
1406
+ const requestedGeneration = attached.generation;
1407
+ const withRuntime = (player, action) => {
1408
+ const current = worlds.get(world);
1409
+ if (current === void 0) {
1410
+ return err(
1411
+ controlFailure(
1412
+ "vfx-host-control-world-detached",
1413
+ `VFX host generation ${requestedGeneration} to remain attached`,
1414
+ "reacquire controls after the World is attached again",
1415
+ { requestedGeneration }
1416
+ )
1417
+ );
1418
+ }
1419
+ if (current.generation !== requestedGeneration) {
1420
+ return err(
1421
+ controlFailure(
1422
+ "vfx-host-control-stale-generation",
1423
+ `VFX host generation ${requestedGeneration}`,
1424
+ "discard this stale control lease and acquire one from the current host generation",
1425
+ { requestedGeneration, currentGeneration: current.generation }
1426
+ )
1427
+ );
1428
+ }
1429
+ if (!world.hasResource(VFX_GPU_RUNTIME_RESOURCE_KEY)) {
1430
+ return err(
1431
+ controlFailure(
1432
+ "vfx-host-control-runtime-unavailable",
1433
+ "the attached World to own its VFX GPU runtime resource",
1434
+ "repair the host attachment before retrying the preview command",
1435
+ { requestedGeneration, currentGeneration: current.generation }
1436
+ )
1437
+ );
1438
+ }
1439
+ if (!world.get(player, ParticleEffectPlayer).ok) {
1440
+ return err(
1441
+ controlFailure(
1442
+ "vfx-host-control-player-unavailable",
1443
+ "a live entity with ParticleEffectPlayer in the attached World",
1444
+ "discard the stale player handle or target a VFX player owned by this World",
1445
+ { requestedGeneration, currentGeneration: current.generation, player }
1446
+ )
1447
+ );
1448
+ }
1449
+ return ok(action(world.getResource(VFX_GPU_RUNTIME_RESOURCE_KEY)));
1450
+ };
1451
+ const control = {
1452
+ generation: requestedGeneration,
1453
+ replay: ({ player, replayInput }) => withRuntime(player, (runtime) => {
1454
+ runtime.replay(player, replayInput);
1455
+ return Object.freeze({ state: "queued", generation: requestedGeneration });
1456
+ }),
1457
+ setEmitterSessionEnabled: ({ player, emitterId, enabled }) => withRuntime(player, (runtime) => {
1458
+ runtime.setEmitterSessionEnabled(player, emitterId, enabled);
1459
+ return Object.freeze({
1460
+ state: enabled ? "enabled" : "disabled",
1461
+ generation: requestedGeneration
1462
+ });
1463
+ }),
1464
+ setPlayerRenderConsumption: ({ player, enabled }) => withRuntime(player, () => {
1465
+ let paused = pausedPlayers.get(world);
1466
+ if (enabled) {
1467
+ paused?.delete(player);
1468
+ if (paused !== void 0 && paused.size === 0) pausedPlayers.delete(world);
1469
+ } else {
1470
+ paused ??= /* @__PURE__ */ new Set();
1471
+ paused.add(player);
1472
+ pausedPlayers.set(world, paused);
1473
+ }
1474
+ return Object.freeze({
1475
+ state: enabled ? "enabled" : "paused",
1476
+ generation: requestedGeneration
1477
+ });
1478
+ })
1479
+ };
1480
+ return ok(Object.freeze(control));
1481
+ },
1482
+ resolveDataInterfaces: ({ requirements, generation }) => dataInterfaces.resolve(requirements, generation),
1483
+ attachWorld: async ({ world, assets }) => {
1484
+ if (worlds.has(world)) return ok({ state: "already-attached" });
1485
+ if (!decoderLeases.has(assets)) {
1486
+ try {
1487
+ decoderLeases.set(assets, installVfxRuntimeDecoder(assets));
1488
+ } catch (cause) {
1489
+ return err(
1490
+ failure2(
1491
+ "vfx-host-loader-install-failed",
1492
+ "the VFX owner decoder to be installed once in the runtime core",
1493
+ "remove a conflicting particle-effect decoder and retry attachWorld",
1494
+ cause
1495
+ )
1496
+ );
1497
+ }
1498
+ }
1499
+ let pluginContext;
1500
+ try {
1501
+ pluginContext = await createWorldContext(world, [
1502
+ vfxGpuRuntimePlugin(
1503
+ options.maxQueuedTicks === void 0 ? {} : { maxQueuedTicks: options.maxQueuedTicks }
1504
+ )
1505
+ ]);
1506
+ } catch (cause) {
1507
+ return err(
1508
+ failure2(
1509
+ "vfx-host-world-attach-failed",
1510
+ "the World FixedUpdate VFX intent producer to install",
1511
+ "repair the reported World registration conflict and retry",
1512
+ cause
1513
+ )
1514
+ );
1515
+ }
1516
+ let resolver;
1517
+ try {
1518
+ resolver = getAssetRegistryResolver(assets);
1519
+ } catch {
1520
+ resolver = void 0;
1521
+ }
1522
+ const attached = {
1523
+ assets,
1524
+ resolver,
1525
+ pluginContext,
1526
+ generation: nextGeneration++,
1527
+ renderAssets: /* @__PURE__ */ new Map(),
1528
+ pendingRenderAssets: /* @__PURE__ */ new Map(),
1529
+ catalogEpoch: typeof assets.snapshot === "function" ? assets.snapshot().epoch : 0,
1530
+ unsubscribeAssets: () => {
1531
+ }
1532
+ };
1533
+ const subscribe = assets.subscribe;
1534
+ if (subscribe !== void 0) {
1535
+ attached.unsubscribeAssets = subscribe((snapshot) => {
1536
+ if (snapshot.epoch === attached.catalogEpoch) return;
1537
+ attached.catalogEpoch = snapshot.epoch;
1538
+ attached.renderAssets.clear();
1539
+ attached.pendingRenderAssets.clear();
1540
+ });
1541
+ }
1542
+ worlds.set(world, attached);
1543
+ return ok({ state: "attached" });
1544
+ },
1545
+ detachWorld: async ({ world }) => {
1546
+ const attached = worlds.get(world);
1547
+ if (attached === void 0) return ok({ state: "not-attached" });
1548
+ try {
1549
+ await attached.pluginContext.fiber.dispose();
1550
+ } catch (cause) {
1551
+ return err(
1552
+ failure2(
1553
+ "vfx-host-world-detach-failed",
1554
+ "the VFX FixedUpdate producer to detach exactly once",
1555
+ "inspect the World schedule and retry detachWorld",
1556
+ cause
1557
+ )
1558
+ );
1559
+ }
1560
+ pausedPlayers.delete(world);
1561
+ attached.unsubscribeAssets();
1562
+ attached.renderAssets.clear();
1563
+ attached.pendingRenderAssets.clear();
1564
+ worlds.delete(world);
1565
+ return ok({ state: "detached" });
1566
+ }
1567
+ };
1568
+ }
1569
+
1570
+ export { PARTICLE_SHADER_IDENTIFIERS, VFX_EVENT_BYTES, VFX_EVENT_COUNTER_BYTES, VFX_EVENT_INPUT_BYTES, createCameraProvider, createSceneDepthProvider, createTopologyResourcePlan, createVfxDataInterfaceRegistry, createVfxRenderInspectSnapshot, createVfxRuntimeHost, encodeEventInputs, eventCapacity, eventCounterData, eventInputCapacity, gpuParticleRenderFeature, installVfxRuntimeDecoder, observeStagePlan, resolveBillboardAdvancedState, stageDispatches, stageRecoveryReadiness, topologyCapacitySnapshot, topologyRecoveryHint, validatedStagePlan };
1571
+ //# sourceMappingURL=index.mjs.map
1572
+ //# sourceMappingURL=index.mjs.map