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