@forgeax/engine-vfx 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.
- package/LICENSE +202 -0
- package/README.md +214 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/authoring-descriptor.unit.test.d.ts +2 -0
- package/dist/__tests__/authoring-descriptor.unit.test.d.ts.map +1 -0
- package/dist/__tests__/channel-input.unit.test.d.ts +2 -0
- package/dist/__tests__/channel-input.unit.test.d.ts.map +1 -0
- package/dist/__tests__/channel-inspect.unit.test.d.ts +2 -0
- package/dist/__tests__/channel-inspect.unit.test.d.ts.map +1 -0
- package/dist/__tests__/code-source-v2.unit.test.d.ts +2 -0
- package/dist/__tests__/code-source-v2.unit.test.d.ts.map +1 -0
- package/dist/__tests__/data-interface-contract.unit.test.d.ts +2 -0
- package/dist/__tests__/data-interface-contract.unit.test.d.ts.map +1 -0
- package/dist/__tests__/effect-contract.unit.test.d.ts +2 -0
- package/dist/__tests__/effect-contract.unit.test.d.ts.map +1 -0
- package/dist/__tests__/gpu-program-simulation-culling-owner.test-d.d.ts +2 -0
- package/dist/__tests__/gpu-program-simulation-culling-owner.test-d.d.ts.map +1 -0
- package/dist/__tests__/gpu-reflection-vocabulary-owner.test-d.d.ts +2 -0
- package/dist/__tests__/gpu-reflection-vocabulary-owner.test-d.d.ts.map +1 -0
- package/dist/__tests__/gpu-runtime.integration.test.d.ts +2 -0
- package/dist/__tests__/gpu-runtime.integration.test.d.ts.map +1 -0
- package/dist/__tests__/inspect-snapshot.unit.test.d.ts +2 -0
- package/dist/__tests__/inspect-snapshot.unit.test.d.ts.map +1 -0
- package/dist/__tests__/instance-public-api.unit.test.d.ts +2 -0
- package/dist/__tests__/instance-public-api.unit.test.d.ts.map +1 -0
- package/dist/__tests__/instance.unit.test.d.ts +2 -0
- package/dist/__tests__/instance.unit.test.d.ts.map +1 -0
- package/dist/__tests__/loader-v1-rejection.unit.test.d.ts +2 -0
- package/dist/__tests__/loader-v1-rejection.unit.test.d.ts.map +1 -0
- package/dist/__tests__/player-reflection.unit.test.d.ts +2 -0
- package/dist/__tests__/player-reflection.unit.test.d.ts.map +1 -0
- package/dist/__tests__/player-type.test-d.d.ts +2 -0
- package/dist/__tests__/player-type.test-d.d.ts.map +1 -0
- package/dist/__tests__/renderer-source.unit.test.d.ts +2 -0
- package/dist/__tests__/renderer-source.unit.test.d.ts.map +1 -0
- package/dist/__tests__/replay-canonical.unit.test.d.ts +2 -0
- package/dist/__tests__/replay-canonical.unit.test.d.ts.map +1 -0
- package/dist/__tests__/runtime-dist-isolation.test.d.ts +2 -0
- package/dist/__tests__/runtime-dist-isolation.test.d.ts.map +1 -0
- package/dist/__tests__/scriptable-pack-consumer.test-d.d.ts +2 -0
- package/dist/__tests__/scriptable-pack-consumer.test-d.d.ts.map +1 -0
- package/dist/__tests__/stage-source.unit.test.d.ts +2 -0
- package/dist/__tests__/stage-source.unit.test.d.ts.map +1 -0
- package/dist/__tests__/vfx-data-interface-vocabulary-owner.test-d.d.ts +2 -0
- package/dist/__tests__/vfx-data-interface-vocabulary-owner.test-d.d.ts.map +1 -0
- package/dist/__tests__/vfx-performance-contract.unit.test.d.ts +2 -0
- package/dist/__tests__/vfx-performance-contract.unit.test.d.ts.map +1 -0
- package/dist/__tests__/vfx-value-type-owner.test-d.d.ts +2 -0
- package/dist/__tests__/vfx-value-type-owner.test-d.d.ts.map +1 -0
- package/dist/assets/particle-effect-decoder.d.ts +3 -0
- package/dist/assets/particle-effect-decoder.d.ts.map +1 -0
- package/dist/authoring-descriptor.d.ts +60 -0
- package/dist/authoring-descriptor.d.ts.map +1 -0
- package/dist/code-source.d.ts +140 -0
- package/dist/code-source.d.ts.map +1 -0
- package/dist/data-interface.d.ts +53 -0
- package/dist/data-interface.d.ts.map +1 -0
- package/dist/effect-contract.d.ts +57 -0
- package/dist/effect-contract.d.ts.map +1 -0
- package/dist/gpu-loader.d.ts +33 -0
- package/dist/gpu-loader.d.ts.map +1 -0
- package/dist/gpu-program.d.ts +80 -0
- package/dist/gpu-program.d.ts.map +1 -0
- package/dist/gpu-runtime.d.ts +174 -0
- package/dist/gpu-runtime.d.ts.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +1986 -0
- package/dist/index.mjs.map +1 -0
- package/dist/instance.d.ts +86 -0
- package/dist/instance.d.ts.map +1 -0
- package/dist/player.d.ts +17 -0
- package/dist/player.d.ts.map +1 -0
- package/package.json +66 -0
- package/src/__tests__/authoring-descriptor.unit.test.ts +138 -0
- package/src/__tests__/channel-input.unit.test.ts +65 -0
- package/src/__tests__/channel-inspect.unit.test.ts +65 -0
- package/src/__tests__/code-source-v2.unit.test.ts +121 -0
- package/src/__tests__/data-interface-contract.unit.test.ts +92 -0
- package/src/__tests__/effect-contract.unit.test.ts +62 -0
- package/src/__tests__/gpu-program-simulation-culling-owner.test-d.ts +58 -0
- package/src/__tests__/gpu-reflection-vocabulary-owner.test-d.ts +23 -0
- package/src/__tests__/gpu-runtime.integration.test.ts +549 -0
- package/src/__tests__/inspect-snapshot.unit.test.ts +51 -0
- package/src/__tests__/instance-public-api.unit.test.ts +27 -0
- package/src/__tests__/instance.unit.test.ts +107 -0
- package/src/__tests__/loader-v1-rejection.unit.test.ts +125 -0
- package/src/__tests__/player-reflection.unit.test.ts +91 -0
- package/src/__tests__/player-type.test-d.ts +54 -0
- package/src/__tests__/renderer-source.unit.test.ts +58 -0
- package/src/__tests__/replay-canonical.unit.test.ts +68 -0
- package/src/__tests__/runtime-dist-isolation.test.ts +100 -0
- package/src/__tests__/scriptable-pack-consumer.test-d.ts +9 -0
- package/src/__tests__/stage-source.unit.test.ts +64 -0
- package/src/__tests__/vfx-data-interface-vocabulary-owner.test-d.ts +34 -0
- package/src/__tests__/vfx-performance-contract.unit.test.ts +19 -0
- package/src/__tests__/vfx-value-type-owner.test-d.ts +32 -0
- package/src/assets/particle-effect-decoder.ts +34 -0
- package/src/authoring-descriptor.ts +355 -0
- package/src/code-source.ts +827 -0
- package/src/data-interface.ts +181 -0
- package/src/effect-contract.ts +220 -0
- package/src/gpu-loader.ts +279 -0
- package/src/gpu-program.ts +101 -0
- package/src/gpu-runtime.ts +807 -0
- package/src/index.ts +110 -0
- package/src/instance.ts +379 -0
- package/src/player.ts +21 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1986 @@
|
|
|
1
|
+
import { ok, err, toShared } from '@forgeax/engine-types';
|
|
2
|
+
import { defineComponent, FixedUpdate, Entity, FixedTime } from '@forgeax/engine-ecs';
|
|
3
|
+
|
|
4
|
+
// src/assets/particle-effect-decoder.ts
|
|
5
|
+
var particleEffectContribution = {
|
|
6
|
+
kind: { kind: "particle-effect" },
|
|
7
|
+
consumer: "VfxGpuRuntime",
|
|
8
|
+
decoder: {
|
|
9
|
+
async decode({ envelope }) {
|
|
10
|
+
const payload = envelope.payload;
|
|
11
|
+
if (payload.kind === "particle-effect" && payload.schemaVersion === 2 && payload.emitters.length === payload.program.emitters.length && payload.programFingerprint === payload.program.fingerprint) {
|
|
12
|
+
return ok(payload);
|
|
13
|
+
}
|
|
14
|
+
return err({
|
|
15
|
+
code: "asset-package-invalid",
|
|
16
|
+
expected: "a schema v2 particle payload matching its cooked program",
|
|
17
|
+
hint: "recook the particle effect atomically with its GPU program",
|
|
18
|
+
detail: { guid: envelope.guid, reason: "particle owner validation failed" }
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// src/authoring-descriptor.ts
|
|
25
|
+
var CAPABILITIES = Object.freeze([
|
|
26
|
+
Object.freeze({ id: "wgsl-behavior", state: "executable" }),
|
|
27
|
+
Object.freeze({ id: "multi-emitter", state: "executable" }),
|
|
28
|
+
Object.freeze({ id: "deterministic-replay", state: "executable" }),
|
|
29
|
+
Object.freeze({ id: "emitter-visibility", state: "executable" }),
|
|
30
|
+
Object.freeze({
|
|
31
|
+
id: "runtime-parameters",
|
|
32
|
+
state: "partial",
|
|
33
|
+
reason: "reflected and packed, but not yet bound to managed author WGSL"
|
|
34
|
+
}),
|
|
35
|
+
Object.freeze({
|
|
36
|
+
id: "custom-attributes",
|
|
37
|
+
state: "partial",
|
|
38
|
+
reason: "reflected without executable per-particle custom storage"
|
|
39
|
+
}),
|
|
40
|
+
Object.freeze({
|
|
41
|
+
id: "data-interfaces",
|
|
42
|
+
state: "partial",
|
|
43
|
+
reason: "requirements resolve provider readiness without author resource bindings"
|
|
44
|
+
})
|
|
45
|
+
]);
|
|
46
|
+
function isVfxGpuEffectAsset(value) {
|
|
47
|
+
if (typeof value !== "object" || value === null) return false;
|
|
48
|
+
const candidate = value;
|
|
49
|
+
return candidate.kind === "particle-effect" && candidate.schemaVersion === 2 && typeof candidate.guid === "string" && candidate.guid.length > 0 && typeof candidate.program === "object" && candidate.program !== null && candidate.program.format === "forgeax-vfx-program-2" && typeof candidate.program.fingerprint === "string" && Array.isArray(candidate.program.emitters);
|
|
50
|
+
}
|
|
51
|
+
function label(path) {
|
|
52
|
+
const leaf = path.split(".").at(-1) ?? path;
|
|
53
|
+
return leaf.replaceAll(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, (value) => value.toUpperCase());
|
|
54
|
+
}
|
|
55
|
+
function authoringValue(value) {
|
|
56
|
+
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
if (Array.isArray(value)) return Object.freeze(value.map(authoringValue));
|
|
60
|
+
if (typeof value === "object") {
|
|
61
|
+
const out = {};
|
|
62
|
+
for (const [key, child] of Object.entries(value)) out[key] = authoringValue(child);
|
|
63
|
+
return Object.freeze(out);
|
|
64
|
+
}
|
|
65
|
+
return String(value);
|
|
66
|
+
}
|
|
67
|
+
function valueType(value) {
|
|
68
|
+
if (typeof value === "number") return "number";
|
|
69
|
+
if (typeof value === "boolean") return "boolean";
|
|
70
|
+
if (Array.isArray(value) && value.every((entry) => typeof entry === "number")) return "vector";
|
|
71
|
+
if (typeof value === "object" && value !== null) return "object";
|
|
72
|
+
return "text";
|
|
73
|
+
}
|
|
74
|
+
function field(path, value) {
|
|
75
|
+
const normalized = authoringValue(value);
|
|
76
|
+
return Object.freeze({
|
|
77
|
+
path,
|
|
78
|
+
label: label(path),
|
|
79
|
+
value: normalized,
|
|
80
|
+
valueType: valueType(normalized)
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
function fields(path, value, omit = []) {
|
|
84
|
+
const excluded = new Set(omit);
|
|
85
|
+
return Object.freeze(
|
|
86
|
+
Object.entries(value).filter(([key, child]) => !excluded.has(key) && child !== void 0).map(([key, child]) => field(`${path}.${key}`, child))
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
function node(input) {
|
|
90
|
+
return Object.freeze({ ...input, children: Object.freeze([...input.children ?? []]) });
|
|
91
|
+
}
|
|
92
|
+
function reflectionNodes(emitter, path) {
|
|
93
|
+
const nodes = [];
|
|
94
|
+
const layout = emitter.reflection.layout;
|
|
95
|
+
if (layout !== void 0 && layout.parameters.fields.length > 0) {
|
|
96
|
+
nodes.push(
|
|
97
|
+
node({
|
|
98
|
+
id: `parameters:${emitter.id}`,
|
|
99
|
+
role: "parameters",
|
|
100
|
+
label: layout.parameters.name,
|
|
101
|
+
sourcePath: `${path}.program`,
|
|
102
|
+
fields: Object.freeze(
|
|
103
|
+
layout.parameters.fields.map(
|
|
104
|
+
(entry) => field(`parameters.${entry.name}`, {
|
|
105
|
+
type: entry.type,
|
|
106
|
+
offset: entry.offset,
|
|
107
|
+
size: entry.size,
|
|
108
|
+
alignment: entry.alignment,
|
|
109
|
+
defaultValue: entry.defaultValue ?? null
|
|
110
|
+
})
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
})
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
if (layout !== void 0 && layout.custom.fields.length > 0) {
|
|
117
|
+
nodes.push(
|
|
118
|
+
node({
|
|
119
|
+
id: `custom:${emitter.id}`,
|
|
120
|
+
role: "custom",
|
|
121
|
+
label: layout.custom.name,
|
|
122
|
+
sourcePath: `${path}.program`,
|
|
123
|
+
fields: Object.freeze(
|
|
124
|
+
layout.custom.fields.map(
|
|
125
|
+
(entry) => field(`custom.${entry.name}`, {
|
|
126
|
+
type: entry.type,
|
|
127
|
+
offset: entry.offset,
|
|
128
|
+
size: entry.size,
|
|
129
|
+
alignment: entry.alignment,
|
|
130
|
+
defaultValue: entry.defaultValue ?? null
|
|
131
|
+
})
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
})
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
for (const stage of emitter.reflection.stages ?? []) {
|
|
138
|
+
nodes.push(
|
|
139
|
+
node({
|
|
140
|
+
id: `stage:${emitter.id}:${stage.id}`,
|
|
141
|
+
role: "stage",
|
|
142
|
+
label: stage.id,
|
|
143
|
+
sourcePath: `${path}.program`,
|
|
144
|
+
fields: fields(`stages.${stage.id}`, stage, ["id", "entryPoint"])
|
|
145
|
+
})
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
return nodes;
|
|
149
|
+
}
|
|
150
|
+
function emitterNode(emitter, index) {
|
|
151
|
+
const path = `emitters[${index}]`;
|
|
152
|
+
const children = [
|
|
153
|
+
node({
|
|
154
|
+
id: `program:${emitter.id}`,
|
|
155
|
+
role: "program",
|
|
156
|
+
label: emitter.module,
|
|
157
|
+
sourcePath: `${path}.program.module`,
|
|
158
|
+
fields: Object.freeze([
|
|
159
|
+
field(`${path}.program.module`, emitter.module),
|
|
160
|
+
field(`${path}.program.imports`, emitter.reflection.imports)
|
|
161
|
+
])
|
|
162
|
+
}),
|
|
163
|
+
...reflectionNodes(emitter, path),
|
|
164
|
+
...(emitter.channels ?? []).map(
|
|
165
|
+
(channel, channelIndex) => node({
|
|
166
|
+
id: `channel:${emitter.id}:${channel.id}`,
|
|
167
|
+
role: "channel",
|
|
168
|
+
label: channel.id,
|
|
169
|
+
sourcePath: `${path}.channels[${channelIndex}]`,
|
|
170
|
+
fields: fields(`${path}.channels[${channelIndex}]`, channel, ["id"])
|
|
171
|
+
})
|
|
172
|
+
),
|
|
173
|
+
...(emitter.events ?? []).map(
|
|
174
|
+
(event, eventIndex) => node({
|
|
175
|
+
id: `event:${emitter.id}:${event.id}`,
|
|
176
|
+
role: "event",
|
|
177
|
+
label: event.id,
|
|
178
|
+
sourcePath: `${path}.events[${eventIndex}]`,
|
|
179
|
+
fields: fields(`${path}.events[${eventIndex}]`, event, ["id"])
|
|
180
|
+
})
|
|
181
|
+
),
|
|
182
|
+
...emitter.renderers.map(
|
|
183
|
+
(renderer, rendererIndex) => node({
|
|
184
|
+
id: `renderer:${emitter.id}:${rendererIndex}`,
|
|
185
|
+
role: "renderer",
|
|
186
|
+
label: `${renderer.kind} renderer`,
|
|
187
|
+
sourcePath: `${path}.renderers[${rendererIndex}]`,
|
|
188
|
+
fields: fields(`${path}.renderers[${rendererIndex}]`, renderer, ["kind"])
|
|
189
|
+
})
|
|
190
|
+
)
|
|
191
|
+
];
|
|
192
|
+
return Object.freeze({
|
|
193
|
+
id: `emitter:${emitter.id}`,
|
|
194
|
+
role: "emitter",
|
|
195
|
+
label: emitter.id,
|
|
196
|
+
module: emitter.module,
|
|
197
|
+
sourcePath: path,
|
|
198
|
+
fields: Object.freeze([
|
|
199
|
+
field(`${path}.capacity`, emitter.capacity),
|
|
200
|
+
field(`${path}.space`, emitter.space),
|
|
201
|
+
field(`${path}.bounds`, emitter.bounds),
|
|
202
|
+
field(`${path}.simulationWhenCulled`, emitter.simulationWhenCulled)
|
|
203
|
+
]),
|
|
204
|
+
children: Object.freeze(children)
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
function dependencies(effect) {
|
|
208
|
+
const entries = [];
|
|
209
|
+
const seen = /* @__PURE__ */ new Set();
|
|
210
|
+
const add = (entry) => {
|
|
211
|
+
const key = `${entry.kind}:${entry.identity}`;
|
|
212
|
+
if (seen.has(key)) return;
|
|
213
|
+
seen.add(key);
|
|
214
|
+
entries.push(Object.freeze(entry));
|
|
215
|
+
};
|
|
216
|
+
for (const [emitterIndex, emitter] of effect.program.emitters.entries()) {
|
|
217
|
+
const path = `emitters[${emitterIndex}]`;
|
|
218
|
+
add({ kind: "module", identity: emitter.module, sourcePath: `${path}.program.module` });
|
|
219
|
+
for (const identity of emitter.reflection.imports) {
|
|
220
|
+
add({ kind: "module", identity, sourcePath: `${path}.program.imports` });
|
|
221
|
+
}
|
|
222
|
+
for (const [rendererIndex, renderer] of emitter.renderers.entries()) {
|
|
223
|
+
add({
|
|
224
|
+
kind: "asset",
|
|
225
|
+
identity: renderer.material,
|
|
226
|
+
sourcePath: `${path}.renderers[${rendererIndex}].material`
|
|
227
|
+
});
|
|
228
|
+
if (renderer.kind === "mesh") {
|
|
229
|
+
add({
|
|
230
|
+
kind: "asset",
|
|
231
|
+
identity: renderer.mesh,
|
|
232
|
+
sourcePath: `${path}.renderers[${rendererIndex}].mesh`
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
for (const requirement of emitter.reflection.dataInterfaces ?? []) {
|
|
237
|
+
add({ kind: "data-interface", identity: requirement.token, sourcePath: `${path}.program` });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return Object.freeze(entries);
|
|
241
|
+
}
|
|
242
|
+
function describeVfxGpuEffect(effect) {
|
|
243
|
+
return Object.freeze({
|
|
244
|
+
version: 1,
|
|
245
|
+
assetGuid: effect.guid,
|
|
246
|
+
schemaVersion: 2,
|
|
247
|
+
artifactFingerprint: effect.program.fingerprint,
|
|
248
|
+
emitters: Object.freeze(effect.program.emitters.map(emitterNode)),
|
|
249
|
+
timeline: Object.freeze(
|
|
250
|
+
effect.program.emitters.map(
|
|
251
|
+
(emitter) => Object.freeze({
|
|
252
|
+
emitterId: emitter.id,
|
|
253
|
+
rate: emitter.schedule.rate,
|
|
254
|
+
bursts: Object.freeze([...emitter.schedule.bursts ?? []]),
|
|
255
|
+
...emitter.schedule.loopDuration === void 0 ? {} : { loopDuration: emitter.schedule.loopDuration }
|
|
256
|
+
})
|
|
257
|
+
)
|
|
258
|
+
),
|
|
259
|
+
dependencies: dependencies(effect),
|
|
260
|
+
capabilities: CAPABILITIES
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
var PARTICLE_CODE_DEFAULT_MODULE_ID = "forgeax_vfx::default";
|
|
264
|
+
var PARTICLE_STAGE_RESOURCE_NAMES = Object.freeze([
|
|
265
|
+
"particles",
|
|
266
|
+
"runtime",
|
|
267
|
+
"aliveIndices",
|
|
268
|
+
"counters",
|
|
269
|
+
"indirect",
|
|
270
|
+
"scratch",
|
|
271
|
+
"billboardInstances",
|
|
272
|
+
"channelInputs",
|
|
273
|
+
"events",
|
|
274
|
+
"eventCounters"
|
|
275
|
+
]);
|
|
276
|
+
function invalid(path, expected, emitterId, code = "vfx-source-invalid") {
|
|
277
|
+
return err({
|
|
278
|
+
code,
|
|
279
|
+
expected,
|
|
280
|
+
hint: `repair ${path} and recook the particle effect`,
|
|
281
|
+
detail: emitterId === void 0 ? { path } : { path, emitterId }
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
function eventInvalid(code, path, expected, emitterId) {
|
|
285
|
+
return invalid(path, expected, emitterId, code);
|
|
286
|
+
}
|
|
287
|
+
function record(value) {
|
|
288
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
289
|
+
}
|
|
290
|
+
function finite(value) {
|
|
291
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
292
|
+
}
|
|
293
|
+
function positiveInteger(value) {
|
|
294
|
+
return finite(value) && Number.isInteger(value) && value > 0;
|
|
295
|
+
}
|
|
296
|
+
function nonNegativeInteger(value) {
|
|
297
|
+
return finite(value) && Number.isInteger(value) && value >= 0;
|
|
298
|
+
}
|
|
299
|
+
function text(value) {
|
|
300
|
+
return typeof value === "string" && value.length > 0;
|
|
301
|
+
}
|
|
302
|
+
function vector(value, size) {
|
|
303
|
+
return Array.isArray(value) && value.length === size && value.every(finite);
|
|
304
|
+
}
|
|
305
|
+
function allowed(value, keys) {
|
|
306
|
+
const set = new Set(keys);
|
|
307
|
+
return Object.keys(value).find((key) => !set.has(key));
|
|
308
|
+
}
|
|
309
|
+
function stageInvalid(path, expected, stageId, resource) {
|
|
310
|
+
return err({
|
|
311
|
+
code: "vfx-source-stage-invalid",
|
|
312
|
+
expected,
|
|
313
|
+
hint: `repair stage ${path} and recook the stage declaration`,
|
|
314
|
+
detail: {
|
|
315
|
+
path,
|
|
316
|
+
...stageId === void 0 ? {} : { stageId },
|
|
317
|
+
...resource === void 0 ? {} : { resource }
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
var STAGE_FIELDS = ["entry", "domain", "resources", "dependsOn", "iterationBudget"];
|
|
322
|
+
var STAGE_RESOURCE_NAMES = new Set(PARTICLE_STAGE_RESOURCE_NAMES);
|
|
323
|
+
function parseStageResources(value, path, stageId) {
|
|
324
|
+
if (value.length === 0)
|
|
325
|
+
return stageInvalid(path, "at least one explicit stage resource", stageId);
|
|
326
|
+
const resources = [];
|
|
327
|
+
const names = /* @__PURE__ */ new Set();
|
|
328
|
+
for (const item of value.split(",")) {
|
|
329
|
+
const [name, access, extra] = item.split(":");
|
|
330
|
+
if (name === void 0 || access === void 0 || extra !== void 0 || !STAGE_RESOURCE_NAMES.has(name) || access !== "read" && access !== "write" && access !== "read-write" || names.has(name)) {
|
|
331
|
+
return stageInvalid(
|
|
332
|
+
path,
|
|
333
|
+
"known resources with unique read, write, or read-write access",
|
|
334
|
+
stageId,
|
|
335
|
+
name
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
names.add(name);
|
|
339
|
+
resources.push({ name, access });
|
|
340
|
+
}
|
|
341
|
+
return ok(Object.freeze(resources));
|
|
342
|
+
}
|
|
343
|
+
function parseVfxStageDeclarations(source) {
|
|
344
|
+
const stages = [];
|
|
345
|
+
const ids = /* @__PURE__ */ new Set();
|
|
346
|
+
const pattern = /^\s*\/\/\s*#vfx\s+stage\s+([^\s]+)\s+(.+)$/gm;
|
|
347
|
+
for (const match of source.matchAll(pattern)) {
|
|
348
|
+
const id = match[1];
|
|
349
|
+
const fieldsText = match[2];
|
|
350
|
+
if (id === void 0 || fieldsText === void 0 || !/^[A-Za-z_]\w*$/.test(id) || ids.has(id)) {
|
|
351
|
+
return stageInvalid(
|
|
352
|
+
`stage.${id ?? "unknown"}`,
|
|
353
|
+
"a unique stage id and supported stage declaration",
|
|
354
|
+
id
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
const fields2 = {};
|
|
358
|
+
for (const token of fieldsText.trim().split(/\s+/)) {
|
|
359
|
+
const separator = token.indexOf("=");
|
|
360
|
+
const key = separator < 0 ? void 0 : token.slice(0, separator);
|
|
361
|
+
const value = separator < 0 ? void 0 : token.slice(separator + 1);
|
|
362
|
+
if (key === void 0 || value === void 0 || !STAGE_FIELDS.includes(key) || fields2[key] !== void 0) {
|
|
363
|
+
return stageInvalid(
|
|
364
|
+
`stage.${id}`,
|
|
365
|
+
"a supported stage declaration with entry, domain, resources, dependsOn, and iterationBudget fields",
|
|
366
|
+
id
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
fields2[key] = value;
|
|
370
|
+
}
|
|
371
|
+
const entry = fields2.entry;
|
|
372
|
+
const domain = fields2.domain;
|
|
373
|
+
const resourcesValue = fields2.resources;
|
|
374
|
+
const dependsOnValue = fields2.dependsOn;
|
|
375
|
+
const budgetValue = fields2.iterationBudget;
|
|
376
|
+
if (entry === void 0 || !/^[A-Za-z_]\w*$/.test(entry) || entry.startsWith("forgeax_vfx_") || domain !== "particle" || resourcesValue === void 0 || dependsOnValue === void 0 || budgetValue === void 0) {
|
|
377
|
+
return stageInvalid(
|
|
378
|
+
`stage.${id}`,
|
|
379
|
+
"a particle-domain stage with an author entry and explicit fields",
|
|
380
|
+
id
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
const resources = parseStageResources(resourcesValue, `stage.${id}.resources`, id);
|
|
384
|
+
if (!resources.ok) return resources;
|
|
385
|
+
const iterationBudget = Number(budgetValue);
|
|
386
|
+
if (!Number.isInteger(iterationBudget) || iterationBudget < 1 || iterationBudget > 64) {
|
|
387
|
+
return stageInvalid(
|
|
388
|
+
`stage.${id}.iterationBudget`,
|
|
389
|
+
"an integer iteration budget from 1 through 64",
|
|
390
|
+
id
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
const dependsOn = dependsOnValue === "none" ? [] : dependsOnValue.split(",").filter((dependency) => dependency.length > 0);
|
|
394
|
+
if (dependsOn.some((dependency) => !/^[A-Za-z_]\w*$/.test(dependency))) {
|
|
395
|
+
return stageInvalid(`stage.${id}.dependsOn`, "stage identifiers or none", id);
|
|
396
|
+
}
|
|
397
|
+
ids.add(id);
|
|
398
|
+
stages.push({
|
|
399
|
+
id,
|
|
400
|
+
entry,
|
|
401
|
+
domain: "particle",
|
|
402
|
+
resources: resources.value,
|
|
403
|
+
dependsOn: Object.freeze(dependsOn),
|
|
404
|
+
iterationBudget
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
return ok(Object.freeze(stages));
|
|
408
|
+
}
|
|
409
|
+
function parseEmitter(value, index, ids) {
|
|
410
|
+
const path = `emitters[${index}]`;
|
|
411
|
+
if (!record(value)) return invalid(path, "a v2 emitter object");
|
|
412
|
+
const extra = allowed(value, [
|
|
413
|
+
"id",
|
|
414
|
+
"capacity",
|
|
415
|
+
"backend",
|
|
416
|
+
"space",
|
|
417
|
+
"bounds",
|
|
418
|
+
"schedule",
|
|
419
|
+
"program",
|
|
420
|
+
"renderers",
|
|
421
|
+
"channels",
|
|
422
|
+
"events",
|
|
423
|
+
"simulationWhenCulled"
|
|
424
|
+
]);
|
|
425
|
+
if (extra !== void 0) return invalid(`${path}.${extra}`, "a v2 emitter field");
|
|
426
|
+
if (!text(value.id) || ids.has(value.id)) return invalid(`${path}.id`, "a unique non-empty id");
|
|
427
|
+
const id = value.id;
|
|
428
|
+
ids.add(id);
|
|
429
|
+
if (!positiveInteger(value.capacity))
|
|
430
|
+
return invalid(`${path}.capacity`, "a positive integer", id);
|
|
431
|
+
if (!record(value.backend) || value.backend.required !== "gpu") {
|
|
432
|
+
return invalid(`${path}.backend`, "the explicit policy { required: 'gpu' }", id);
|
|
433
|
+
}
|
|
434
|
+
const backendExtra = allowed(value.backend, ["required"]);
|
|
435
|
+
if (backendExtra !== void 0) {
|
|
436
|
+
return invalid(`${path}.backend.${backendExtra}`, "the required GPU policy", id);
|
|
437
|
+
}
|
|
438
|
+
if (value.space !== "local" && value.space !== "world") {
|
|
439
|
+
return invalid(`${path}.space`, "local or world", id);
|
|
440
|
+
}
|
|
441
|
+
if (!record(value.bounds)) return invalid(`${path}.bounds`, "fixed aabb or sphere bounds", id);
|
|
442
|
+
const boundsExtra = allowed(
|
|
443
|
+
value.bounds,
|
|
444
|
+
value.bounds.kind === "aabb" ? ["kind", "min", "max"] : value.bounds.kind === "sphere" ? ["kind", "center", "radius"] : ["kind"]
|
|
445
|
+
);
|
|
446
|
+
if (boundsExtra !== void 0) {
|
|
447
|
+
return invalid(`${path}.bounds.${boundsExtra}`, "a supported bounds field", id);
|
|
448
|
+
}
|
|
449
|
+
const boundsOk = value.bounds.kind === "aabb" && vector(value.bounds.min, 3) && vector(value.bounds.max, 3) || value.bounds.kind === "sphere" && vector(value.bounds.center, 3) && finite(value.bounds.radius) && value.bounds.radius > 0;
|
|
450
|
+
if (!boundsOk) return invalid(`${path}.bounds`, "valid fixed aabb or sphere bounds", id);
|
|
451
|
+
if (!record(value.schedule) || !finite(value.schedule.rate) || value.schedule.rate < 0) {
|
|
452
|
+
return invalid(`${path}.schedule`, "a non-negative spawn schedule", id);
|
|
453
|
+
}
|
|
454
|
+
const scheduleExtra = allowed(value.schedule, ["rate", "bursts", "loopDuration"]);
|
|
455
|
+
if (scheduleExtra !== void 0) {
|
|
456
|
+
return invalid(`${path}.schedule.${scheduleExtra}`, "a supported schedule field", id);
|
|
457
|
+
}
|
|
458
|
+
if (value.schedule.bursts !== void 0 && (!Array.isArray(value.schedule.bursts) || value.schedule.bursts.some(
|
|
459
|
+
(burst) => !record(burst) || !finite(burst.time) || burst.time < 0 || !positiveInteger(burst.count)
|
|
460
|
+
))) {
|
|
461
|
+
return invalid(`${path}.schedule.bursts`, "non-negative timed positive bursts", id);
|
|
462
|
+
}
|
|
463
|
+
if (Array.isArray(value.schedule.bursts)) {
|
|
464
|
+
for (const [burstIndex, burst] of value.schedule.bursts.entries()) {
|
|
465
|
+
if (!record(burst)) continue;
|
|
466
|
+
const burstExtra = allowed(burst, ["time", "count"]);
|
|
467
|
+
if (burstExtra !== void 0) {
|
|
468
|
+
return invalid(
|
|
469
|
+
`${path}.schedule.bursts[${burstIndex}].${burstExtra}`,
|
|
470
|
+
"a supported burst field",
|
|
471
|
+
id
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
if (value.schedule.loopDuration !== void 0 && (!finite(value.schedule.loopDuration) || value.schedule.loopDuration <= 0)) {
|
|
477
|
+
return invalid(`${path}.schedule.loopDuration`, "a positive finite duration", id);
|
|
478
|
+
}
|
|
479
|
+
if (!record(value.program) || !text(value.program.module)) {
|
|
480
|
+
return invalid(`${path}.program.module`, "a WGSL module identity", id);
|
|
481
|
+
}
|
|
482
|
+
const programExtra = allowed(value.program, ["module"]);
|
|
483
|
+
if (programExtra !== void 0) {
|
|
484
|
+
return invalid(`${path}.program.${programExtra}`, "a supported program field", id);
|
|
485
|
+
}
|
|
486
|
+
if (!Array.isArray(value.renderers) || value.renderers.length === 0) {
|
|
487
|
+
return invalid(`${path}.renderers`, "at least one renderer", id);
|
|
488
|
+
}
|
|
489
|
+
for (const [rendererIndex, renderer] of value.renderers.entries()) {
|
|
490
|
+
const rendererPath = `${path}.renderers[${rendererIndex}]`;
|
|
491
|
+
if (!record(renderer) || !text(renderer.material) || !text(renderer.kind))
|
|
492
|
+
return invalid(
|
|
493
|
+
rendererPath,
|
|
494
|
+
"a supported renderer object",
|
|
495
|
+
id,
|
|
496
|
+
"vfx-source-renderer-invalid"
|
|
497
|
+
);
|
|
498
|
+
const common = ["kind", "material", "enabled", "capacity", "overflow", "width"];
|
|
499
|
+
const rendererExtra = allowed(
|
|
500
|
+
renderer,
|
|
501
|
+
renderer.kind === "billboard" ? [...common, "blend", "textureSheet", "pivot", "softParticle", "sorting"] : renderer.kind === "mesh" ? ["kind", "material", "mesh", "submesh", "enabled"] : renderer.kind === "ribbon" ? [...common, "stripKey"] : renderer.kind === "trail" ? [...common, "historyLength"] : renderer.kind === "beam" ? [...common, "endpointField"] : ["kind"]
|
|
502
|
+
);
|
|
503
|
+
if (rendererExtra !== void 0)
|
|
504
|
+
return invalid(
|
|
505
|
+
`${rendererPath}.${rendererExtra}`,
|
|
506
|
+
"a supported renderer field",
|
|
507
|
+
id,
|
|
508
|
+
"vfx-source-renderer-invalid"
|
|
509
|
+
);
|
|
510
|
+
if (renderer.kind === "mesh") {
|
|
511
|
+
if (!text(renderer.mesh) || renderer.submesh !== void 0 && !nonNegativeInteger(renderer.submesh))
|
|
512
|
+
return invalid(
|
|
513
|
+
rendererPath,
|
|
514
|
+
"a mesh renderer with a non-negative submesh",
|
|
515
|
+
id,
|
|
516
|
+
"vfx-source-renderer-invalid"
|
|
517
|
+
);
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
if (renderer.enabled !== void 0 && typeof renderer.enabled !== "boolean")
|
|
521
|
+
return invalid(
|
|
522
|
+
`${rendererPath}.enabled`,
|
|
523
|
+
"a boolean enabled flag",
|
|
524
|
+
id,
|
|
525
|
+
"vfx-source-renderer-invalid"
|
|
526
|
+
);
|
|
527
|
+
if (renderer.capacity !== void 0 && (!positiveInteger(renderer.capacity) || renderer.capacity > 65536))
|
|
528
|
+
return invalid(
|
|
529
|
+
`${rendererPath}.capacity`,
|
|
530
|
+
"a positive capacity no greater than 65536",
|
|
531
|
+
id,
|
|
532
|
+
"vfx-source-renderer-invalid"
|
|
533
|
+
);
|
|
534
|
+
if (renderer.overflow !== void 0 && renderer.overflow !== "drop-newest" && renderer.overflow !== "drop-oldest")
|
|
535
|
+
return invalid(
|
|
536
|
+
`${rendererPath}.overflow`,
|
|
537
|
+
"drop-newest or drop-oldest",
|
|
538
|
+
id,
|
|
539
|
+
"vfx-source-renderer-invalid"
|
|
540
|
+
);
|
|
541
|
+
if (renderer.width !== void 0 && (!finite(renderer.width) || renderer.width <= 0))
|
|
542
|
+
return invalid(
|
|
543
|
+
`${rendererPath}.width`,
|
|
544
|
+
"a positive finite width",
|
|
545
|
+
id,
|
|
546
|
+
"vfx-source-renderer-invalid"
|
|
547
|
+
);
|
|
548
|
+
if (renderer.kind === "billboard") {
|
|
549
|
+
if (renderer.blend !== void 0 && renderer.blend !== "additive" && renderer.blend !== "alpha" && renderer.blend !== "opaque-cutout")
|
|
550
|
+
return invalid(
|
|
551
|
+
`${rendererPath}.blend`,
|
|
552
|
+
"additive, alpha, or opaque-cutout",
|
|
553
|
+
id,
|
|
554
|
+
"vfx-source-renderer-invalid"
|
|
555
|
+
);
|
|
556
|
+
if (renderer.sorting !== void 0 && renderer.sorting !== "none" && renderer.sorting !== "emitter" && renderer.sorting !== "back-to-front")
|
|
557
|
+
return invalid(
|
|
558
|
+
`${rendererPath}.sorting`,
|
|
559
|
+
"none, emitter, or back-to-front",
|
|
560
|
+
id,
|
|
561
|
+
"vfx-source-renderer-invalid"
|
|
562
|
+
);
|
|
563
|
+
if (renderer.pivot !== void 0 && (!vector(renderer.pivot, 2) || renderer.pivot.some((value2) => value2 < -1 || value2 > 1)))
|
|
564
|
+
return invalid(
|
|
565
|
+
`${rendererPath}.pivot`,
|
|
566
|
+
"two finite values in the -1..1 range",
|
|
567
|
+
id,
|
|
568
|
+
"vfx-source-renderer-invalid"
|
|
569
|
+
);
|
|
570
|
+
if (renderer.textureSheet !== void 0 && !record(renderer.textureSheet))
|
|
571
|
+
return invalid(
|
|
572
|
+
`${rendererPath}.textureSheet`,
|
|
573
|
+
"a texture sheet object",
|
|
574
|
+
id,
|
|
575
|
+
"vfx-source-renderer-invalid"
|
|
576
|
+
);
|
|
577
|
+
if (renderer.textureSheet !== void 0 && record(renderer.textureSheet)) {
|
|
578
|
+
const sheet = renderer.textureSheet;
|
|
579
|
+
const sheetExtra = allowed(sheet, ["columns", "rows", "frameRate", "frameCount"]);
|
|
580
|
+
if (sheetExtra !== void 0 || !positiveInteger(sheet.columns) || !positiveInteger(sheet.rows) || sheet.columns > 64 || sheet.rows > 64 || !finite(sheet.frameRate) || sheet.frameRate < 0 || sheet.frameCount !== void 0 && (!positiveInteger(sheet.frameCount) || sheet.frameCount > sheet.columns * sheet.rows))
|
|
581
|
+
return invalid(
|
|
582
|
+
`${rendererPath}.textureSheet`,
|
|
583
|
+
"a bounded texture sheet declaration",
|
|
584
|
+
id,
|
|
585
|
+
"vfx-source-renderer-invalid"
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
if (renderer.softParticle !== void 0 && !record(renderer.softParticle))
|
|
589
|
+
return invalid(
|
|
590
|
+
`${rendererPath}.softParticle`,
|
|
591
|
+
"a soft-particle object",
|
|
592
|
+
id,
|
|
593
|
+
"vfx-source-renderer-invalid"
|
|
594
|
+
);
|
|
595
|
+
if (renderer.softParticle !== void 0 && record(renderer.softParticle)) {
|
|
596
|
+
const softExtra = allowed(renderer.softParticle, ["fadeDistance"]);
|
|
597
|
+
if (softExtra !== void 0 || !finite(renderer.softParticle.fadeDistance) || renderer.softParticle.fadeDistance <= 0)
|
|
598
|
+
return invalid(
|
|
599
|
+
`${rendererPath}.softParticle`,
|
|
600
|
+
"a positive scene-depth fade distance",
|
|
601
|
+
id,
|
|
602
|
+
"vfx-source-renderer-invalid"
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
} else if (renderer.kind === "ribbon") {
|
|
606
|
+
if (renderer.stripKey !== "alive-index" || !positiveInteger(renderer.capacity))
|
|
607
|
+
return invalid(
|
|
608
|
+
rendererPath,
|
|
609
|
+
"stripKey 'alive-index' and positive capacity",
|
|
610
|
+
id,
|
|
611
|
+
"vfx-source-renderer-invalid"
|
|
612
|
+
);
|
|
613
|
+
} else if (renderer.kind === "trail") {
|
|
614
|
+
if (!positiveInteger(renderer.historyLength) || renderer.historyLength > 256 || !positiveInteger(renderer.capacity))
|
|
615
|
+
return invalid(
|
|
616
|
+
rendererPath,
|
|
617
|
+
"a bounded trail historyLength and positive capacity",
|
|
618
|
+
id,
|
|
619
|
+
"vfx-source-renderer-invalid"
|
|
620
|
+
);
|
|
621
|
+
} else if (renderer.kind === "beam") {
|
|
622
|
+
if (renderer.endpointField !== "velocity" || !positiveInteger(renderer.capacity))
|
|
623
|
+
return invalid(
|
|
624
|
+
rendererPath,
|
|
625
|
+
"endpointField 'velocity' and positive capacity",
|
|
626
|
+
id,
|
|
627
|
+
"vfx-source-renderer-invalid"
|
|
628
|
+
);
|
|
629
|
+
} else {
|
|
630
|
+
return invalid(
|
|
631
|
+
rendererPath,
|
|
632
|
+
"billboard, mesh, ribbon, trail, or beam",
|
|
633
|
+
id,
|
|
634
|
+
"vfx-source-renderer-invalid"
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
if (value.channels !== void 0) {
|
|
639
|
+
if (!Array.isArray(value.channels) || value.channels.length === 0) {
|
|
640
|
+
return eventInvalid(
|
|
641
|
+
"vfx-source-channel-invalid",
|
|
642
|
+
`${path}.channels`,
|
|
643
|
+
"a non-empty bounded channel list",
|
|
644
|
+
id
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
const channelIds = /* @__PURE__ */ new Set();
|
|
648
|
+
for (const [channelIndex, channel] of value.channels.entries()) {
|
|
649
|
+
const channelPath = `${path}.channels[${channelIndex}]`;
|
|
650
|
+
if (!record(channel)) {
|
|
651
|
+
return eventInvalid("vfx-source-channel-invalid", channelPath, "a channel object", id);
|
|
652
|
+
}
|
|
653
|
+
const channelExtra = allowed(channel, ["id", "payload", "capacity", "overflow"]);
|
|
654
|
+
if (channelExtra !== void 0) {
|
|
655
|
+
return eventInvalid(
|
|
656
|
+
"vfx-source-channel-invalid",
|
|
657
|
+
`${channelPath}.${channelExtra}`,
|
|
658
|
+
"a supported channel field",
|
|
659
|
+
id
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
if (!text(channel.id) || channelIds.has(channel.id)) {
|
|
663
|
+
return eventInvalid(
|
|
664
|
+
"vfx-source-channel-invalid",
|
|
665
|
+
`${channelPath}.id`,
|
|
666
|
+
"a unique non-empty channel id",
|
|
667
|
+
id
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
if (!positiveInteger(channel.capacity) || channel.capacity > 65536 || channel.payload !== void 0 && channel.payload !== "impact" || channel.overflow !== "drop-newest" && channel.overflow !== "drop-oldest") {
|
|
671
|
+
return eventInvalid(
|
|
672
|
+
"vfx-source-channel-invalid",
|
|
673
|
+
channelPath,
|
|
674
|
+
"an impact channel with capacity 1..65536 and explicit overflow policy",
|
|
675
|
+
id
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
channelIds.add(channel.id);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
if (value.events !== void 0) {
|
|
682
|
+
if (!Array.isArray(value.events)) {
|
|
683
|
+
return eventInvalid("vfx-source-event-invalid", `${path}.events`, "an event list", id);
|
|
684
|
+
}
|
|
685
|
+
const eventIds = /* @__PURE__ */ new Set();
|
|
686
|
+
for (const [eventIndex, event] of value.events.entries()) {
|
|
687
|
+
const eventPath = `${path}.events[${eventIndex}]`;
|
|
688
|
+
if (!record(event)) {
|
|
689
|
+
return eventInvalid("vfx-source-event-invalid", eventPath, "an event object", id);
|
|
690
|
+
}
|
|
691
|
+
const eventExtra = allowed(event, [
|
|
692
|
+
"id",
|
|
693
|
+
"channel",
|
|
694
|
+
"subEmitter",
|
|
695
|
+
"fanOut",
|
|
696
|
+
"recursionDepth"
|
|
697
|
+
]);
|
|
698
|
+
if (eventExtra !== void 0) {
|
|
699
|
+
return eventInvalid(
|
|
700
|
+
"vfx-source-event-invalid",
|
|
701
|
+
`${eventPath}.${eventExtra}`,
|
|
702
|
+
"a supported event field",
|
|
703
|
+
id
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
if (!text(event.id) || eventIds.has(event.id)) {
|
|
707
|
+
return eventInvalid("vfx-source-event-invalid", `${eventPath}.id`, "a unique event id", id);
|
|
708
|
+
}
|
|
709
|
+
if (!text(event.channel) || !text(event.subEmitter) || !positiveInteger(event.fanOut) || event.fanOut > 16 || !positiveInteger(event.recursionDepth) || event.recursionDepth > 8) {
|
|
710
|
+
return eventInvalid(
|
|
711
|
+
"vfx-source-event-invalid",
|
|
712
|
+
eventPath,
|
|
713
|
+
"an event with bounded fanOut 1..16 and recursionDepth 1..8",
|
|
714
|
+
id
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
eventIds.add(event.id);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
if (value.simulationWhenCulled !== void 0 && value.simulationWhenCulled !== "continue" && value.simulationWhenCulled !== "pause" && value.simulationWhenCulled !== "restart-on-visible") {
|
|
721
|
+
return invalid(`${path}.simulationWhenCulled`, "continue, pause, or restart-on-visible", id);
|
|
722
|
+
}
|
|
723
|
+
return ok(value);
|
|
724
|
+
}
|
|
725
|
+
function parseParticleEffectSourceV2(value) {
|
|
726
|
+
if (!record(value)) return invalid("$", "a particle effect source object");
|
|
727
|
+
if (value.schemaVersion !== 2) {
|
|
728
|
+
return err({
|
|
729
|
+
code: "vfx-source-version-unsupported",
|
|
730
|
+
expected: "ParticleEffectSource schemaVersion 2",
|
|
731
|
+
hint: "migrate code behavior to WGSL and recook; v1 is not interpreted at runtime",
|
|
732
|
+
detail: { path: "schemaVersion" }
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
const extra = allowed(value, ["schemaVersion", "emitters"]);
|
|
736
|
+
if (extra !== void 0) return invalid(extra, "a root field: emitters");
|
|
737
|
+
if (!Array.isArray(value.emitters) || value.emitters.length === 0) {
|
|
738
|
+
return invalid("emitters", "at least one v2 emitter");
|
|
739
|
+
}
|
|
740
|
+
const ids = /* @__PURE__ */ new Set();
|
|
741
|
+
const emitters = [];
|
|
742
|
+
for (const [index, emitter] of value.emitters.entries()) {
|
|
743
|
+
const parsed = parseEmitter(emitter, index, ids);
|
|
744
|
+
if (!parsed.ok) return parsed;
|
|
745
|
+
emitters.push(parsed.value);
|
|
746
|
+
}
|
|
747
|
+
const emitterIds = new Set(emitters.map((emitter) => emitter.id));
|
|
748
|
+
for (const emitter of emitters) {
|
|
749
|
+
const channels = new Set((emitter.channels ?? []).map((channel) => channel.id));
|
|
750
|
+
for (const event of emitter.events ?? []) {
|
|
751
|
+
if (!channels.has(event.channel)) {
|
|
752
|
+
return eventInvalid(
|
|
753
|
+
"vfx-source-event-invalid",
|
|
754
|
+
`emitters[${emitters.indexOf(emitter)}].events.${event.id}.channel`,
|
|
755
|
+
"the id of a channel declared on the same emitter",
|
|
756
|
+
emitter.id
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
if (!emitterIds.has(event.subEmitter)) {
|
|
760
|
+
return eventInvalid(
|
|
761
|
+
"vfx-source-event-invalid",
|
|
762
|
+
`emitters[${emitters.indexOf(emitter)}].events.${event.id}.subEmitter`,
|
|
763
|
+
"the id of an emitter in the same cooked effect",
|
|
764
|
+
emitter.id
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
return ok(
|
|
770
|
+
Object.freeze({
|
|
771
|
+
schemaVersion: 2,
|
|
772
|
+
emitters: Object.freeze(emitters)
|
|
773
|
+
})
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
function defineParticleEffectSourceV2(source) {
|
|
777
|
+
const parsed = parseParticleEffectSourceV2(source);
|
|
778
|
+
if (!parsed.ok) throw new TypeError(`${parsed.error.code}: ${parsed.error.expected}`);
|
|
779
|
+
return source;
|
|
780
|
+
}
|
|
781
|
+
function failure(code, requirement, expected, hint, detail = {}) {
|
|
782
|
+
return { code, expected, hint, detail: { token: requirement.token, ...detail } };
|
|
783
|
+
}
|
|
784
|
+
function resolveVfxDataInterfaces(requirements, providers, generation) {
|
|
785
|
+
const byToken = /* @__PURE__ */ new Map();
|
|
786
|
+
for (const provider of providers) {
|
|
787
|
+
const prior = byToken.get(provider.token);
|
|
788
|
+
if (prior !== void 0) {
|
|
789
|
+
const requirement = requirements.find((entry) => entry.token === provider.token) ?? {
|
|
790
|
+
token: provider.token,
|
|
791
|
+
kind: provider.kind,
|
|
792
|
+
bindingType: provider.bindingType};
|
|
793
|
+
return err(
|
|
794
|
+
failure(
|
|
795
|
+
"vfx-data-interface-duplicate",
|
|
796
|
+
requirement,
|
|
797
|
+
"at most one provider for each reflected Data Interface token",
|
|
798
|
+
`remove duplicate providers ${prior.id} and ${provider.id} and retry`,
|
|
799
|
+
{ providerId: provider.id }
|
|
800
|
+
)
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
byToken.set(provider.token, provider);
|
|
804
|
+
}
|
|
805
|
+
const resources = [];
|
|
806
|
+
for (const requirement of requirements) {
|
|
807
|
+
const provider = byToken.get(requirement.token);
|
|
808
|
+
if (provider === void 0) {
|
|
809
|
+
return err(
|
|
810
|
+
failure(
|
|
811
|
+
"vfx-data-interface-missing",
|
|
812
|
+
requirement,
|
|
813
|
+
`a registered ${requirement.kind} provider for ${requirement.token}`,
|
|
814
|
+
`register ${requirement.token} for generation ${generation} before starting the effect`
|
|
815
|
+
)
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
if (provider.kind !== requirement.kind || provider.bindingType !== requirement.bindingType) {
|
|
819
|
+
return err(
|
|
820
|
+
failure(
|
|
821
|
+
"vfx-data-interface-wrong-type",
|
|
822
|
+
requirement,
|
|
823
|
+
`${requirement.kind} with ${requirement.bindingType} binding semantics`,
|
|
824
|
+
`replace provider ${provider.id} with a ${requirement.token} provider matching reflection`,
|
|
825
|
+
{
|
|
826
|
+
providerId: provider.id,
|
|
827
|
+
expectedBindingType: requirement.bindingType,
|
|
828
|
+
actualBindingType: provider.bindingType
|
|
829
|
+
}
|
|
830
|
+
)
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
const provided = provider.provide(generation);
|
|
834
|
+
if (!provided.ok) return provided;
|
|
835
|
+
const resource = provided.value;
|
|
836
|
+
if (resource.token !== requirement.token || resource.kind !== requirement.kind) {
|
|
837
|
+
return err(
|
|
838
|
+
failure(
|
|
839
|
+
"vfx-data-interface-wrong-type",
|
|
840
|
+
requirement,
|
|
841
|
+
`${requirement.kind} resource for ${requirement.token}`,
|
|
842
|
+
`repair provider ${provider.id} so its resource matches its reflected token`,
|
|
843
|
+
{ providerId: provider.id }
|
|
844
|
+
)
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
if (resource.bindingType !== requirement.bindingType) {
|
|
848
|
+
return err(
|
|
849
|
+
failure(
|
|
850
|
+
"vfx-data-interface-wrong-type",
|
|
851
|
+
requirement,
|
|
852
|
+
`resource binding type ${requirement.bindingType}`,
|
|
853
|
+
`repair provider ${provider.id} resource binding metadata`,
|
|
854
|
+
{
|
|
855
|
+
providerId: provider.id,
|
|
856
|
+
expectedBindingType: requirement.bindingType,
|
|
857
|
+
actualBindingType: resource.bindingType
|
|
858
|
+
}
|
|
859
|
+
)
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
if (resource.generation !== generation) {
|
|
863
|
+
return err(
|
|
864
|
+
failure(
|
|
865
|
+
"vfx-data-interface-stale",
|
|
866
|
+
requirement,
|
|
867
|
+
`a resource from generation ${generation}`,
|
|
868
|
+
`refresh provider ${provider.id} for generation ${generation} before rendering`,
|
|
869
|
+
{
|
|
870
|
+
providerId: provider.id,
|
|
871
|
+
expectedGeneration: generation,
|
|
872
|
+
actualGeneration: resource.generation
|
|
873
|
+
}
|
|
874
|
+
)
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
resources.push(resource);
|
|
878
|
+
}
|
|
879
|
+
return ok({ generation, readiness: "ready", resources: Object.freeze(resources) });
|
|
880
|
+
}
|
|
881
|
+
var VECTOR_LENGTH = {
|
|
882
|
+
f32: 1,
|
|
883
|
+
i32: 1,
|
|
884
|
+
u32: 1,
|
|
885
|
+
"vec2<f32>": 2,
|
|
886
|
+
"vec3<f32>": 3,
|
|
887
|
+
"vec4<f32>": 4
|
|
888
|
+
};
|
|
889
|
+
function fieldList(reflection) {
|
|
890
|
+
return [...reflection.parameters.fields, ...reflection.custom.fields];
|
|
891
|
+
}
|
|
892
|
+
function zeroValue(type) {
|
|
893
|
+
const length = VECTOR_LENGTH[type];
|
|
894
|
+
return length === 1 ? 0 : Array.from({ length }, () => 0);
|
|
895
|
+
}
|
|
896
|
+
function valueMatches(type, value) {
|
|
897
|
+
const length = VECTOR_LENGTH[type];
|
|
898
|
+
if (length === 1) return typeof value === "number" && Number.isFinite(value);
|
|
899
|
+
return Array.isArray(value) && value.length === length && value.every((component) => typeof component === "number" && Number.isFinite(component));
|
|
900
|
+
}
|
|
901
|
+
function fail(code, path, expected, hint, actual) {
|
|
902
|
+
return err({
|
|
903
|
+
code,
|
|
904
|
+
expected,
|
|
905
|
+
hint,
|
|
906
|
+
detail: actual === void 0 ? { path } : { path, actual }
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
function validateReflection(reflection) {
|
|
910
|
+
if (reflection.version !== 1 || typeof reflection.fingerprint !== "string" || !reflection.fingerprint.startsWith("sha256:")) {
|
|
911
|
+
return fail(
|
|
912
|
+
"vfx-reflection-invalid",
|
|
913
|
+
"reflection",
|
|
914
|
+
"a version 1 reflection with a sha256 fingerprint",
|
|
915
|
+
"recook the effect with the current VFX compiler"
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
const names = /* @__PURE__ */ new Set();
|
|
919
|
+
for (const field2 of fieldList(reflection)) {
|
|
920
|
+
if (names.has(field2.name)) {
|
|
921
|
+
return fail(
|
|
922
|
+
"vfx-reflection-invalid",
|
|
923
|
+
field2.name,
|
|
924
|
+
"unique field names across parameters and custom data",
|
|
925
|
+
"rename the duplicate WGSL field and recook"
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
names.add(field2.name);
|
|
929
|
+
}
|
|
930
|
+
return ok(true);
|
|
931
|
+
}
|
|
932
|
+
function defaultsFor(reflection) {
|
|
933
|
+
const defaults = {};
|
|
934
|
+
for (const field2 of fieldList(reflection)) {
|
|
935
|
+
defaults[field2.name] = field2.defaultValue === void 0 ? zeroValue(field2.type) : field2.defaultValue;
|
|
936
|
+
}
|
|
937
|
+
return Object.freeze(defaults);
|
|
938
|
+
}
|
|
939
|
+
function validateMap(reflection, values) {
|
|
940
|
+
const fields2 = new Map(fieldList(reflection).map((field2) => [field2.name, field2]));
|
|
941
|
+
for (const name of Object.keys(values)) {
|
|
942
|
+
const field2 = fields2.get(name);
|
|
943
|
+
if (field2 === void 0) {
|
|
944
|
+
return fail(
|
|
945
|
+
"vfx-value-unknown-field",
|
|
946
|
+
name,
|
|
947
|
+
"a field declared by VfxParameters or VfxCustom",
|
|
948
|
+
`remove ${name} or declare it in the authored WGSL struct`,
|
|
949
|
+
values[name]
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
if (!valueMatches(field2.type, values[name])) {
|
|
953
|
+
return fail(
|
|
954
|
+
"vfx-value-type-mismatch",
|
|
955
|
+
name,
|
|
956
|
+
field2.type,
|
|
957
|
+
`provide ${name} as ${field2.type}`,
|
|
958
|
+
values[name]
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
return ok(Object.freeze({ ...values }));
|
|
963
|
+
}
|
|
964
|
+
function writeValue(view, offset, type, value) {
|
|
965
|
+
const values = typeof value === "number" ? [value] : value;
|
|
966
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
967
|
+
const component = values[index] ?? 0;
|
|
968
|
+
if (type === "i32") view.setInt32(offset + index * 4, component, true);
|
|
969
|
+
else if (type === "u32") view.setUint32(offset + index * 4, component, true);
|
|
970
|
+
else view.setFloat32(offset + index * 4, component, true);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
function createVfxEffectContract(reflection) {
|
|
974
|
+
const checked = validateReflection(reflection);
|
|
975
|
+
if (!checked.ok) throw new TypeError(checked.error.hint);
|
|
976
|
+
const defaults = defaultsFor(reflection);
|
|
977
|
+
const fields2 = fieldList(reflection);
|
|
978
|
+
const customBase = reflection.parameters.size;
|
|
979
|
+
const packedSize = customBase + reflection.custom.size;
|
|
980
|
+
return {
|
|
981
|
+
reflection,
|
|
982
|
+
fingerprint: reflection.fingerprint,
|
|
983
|
+
packedSize,
|
|
984
|
+
defaults,
|
|
985
|
+
createValues(initial = {}) {
|
|
986
|
+
const merged = { ...defaults, ...initial };
|
|
987
|
+
return validateMap(reflection, merged);
|
|
988
|
+
},
|
|
989
|
+
validateValues(values) {
|
|
990
|
+
return validateMap(reflection, values);
|
|
991
|
+
},
|
|
992
|
+
pack(values) {
|
|
993
|
+
const checkedValues = validateMap(reflection, values);
|
|
994
|
+
if (!checkedValues.ok) return checkedValues;
|
|
995
|
+
const bytes = new Uint8Array(packedSize);
|
|
996
|
+
const view = new DataView(bytes.buffer);
|
|
997
|
+
for (const field2 of fields2) {
|
|
998
|
+
const value = checkedValues.value[field2.name];
|
|
999
|
+
if (value === void 0) continue;
|
|
1000
|
+
const base = reflection.parameters.fields.includes(field2) ? 0 : customBase;
|
|
1001
|
+
writeValue(view, base + field2.offset, field2.type, value);
|
|
1002
|
+
}
|
|
1003
|
+
return ok(bytes);
|
|
1004
|
+
}
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
function validateVfxEffectValues(reflection, values) {
|
|
1008
|
+
return validateMap(reflection, values);
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// src/gpu-program.ts
|
|
1012
|
+
var VFX_GPU_PROGRAM_FORMAT = "forgeax-vfx-program-2";
|
|
1013
|
+
var VFX_GPU_PROGRAM_ARTIFACT_KEY = "particle-effect/program.json";
|
|
1014
|
+
|
|
1015
|
+
// src/gpu-loader.ts
|
|
1016
|
+
function failure2(code, guid, path, expected, hint) {
|
|
1017
|
+
return err({ code, expected, hint, detail: { guid, path } });
|
|
1018
|
+
}
|
|
1019
|
+
function record2(value) {
|
|
1020
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1021
|
+
}
|
|
1022
|
+
function hex(bytes) {
|
|
1023
|
+
return [...new Uint8Array(bytes)].map((value) => value.toString(16).padStart(2, "0")).join("");
|
|
1024
|
+
}
|
|
1025
|
+
async function fingerprint(bytes) {
|
|
1026
|
+
const source = bytes.buffer.slice(
|
|
1027
|
+
bytes.byteOffset,
|
|
1028
|
+
bytes.byteOffset + bytes.byteLength
|
|
1029
|
+
);
|
|
1030
|
+
return `sha256:${hex(await globalThis.crypto.subtle.digest("SHA-256", source))}`;
|
|
1031
|
+
}
|
|
1032
|
+
function validReflectionLayout(value) {
|
|
1033
|
+
if (value === void 0) return true;
|
|
1034
|
+
if (!record2(value) || value.version !== 1) return false;
|
|
1035
|
+
if (!record2(value.parameters) || !Array.isArray(value.parameters.fields)) return false;
|
|
1036
|
+
if (!record2(value.custom) || !Array.isArray(value.custom.fields)) return false;
|
|
1037
|
+
return typeof value.fingerprint === "string" && value.fingerprint.startsWith("sha256:");
|
|
1038
|
+
}
|
|
1039
|
+
function stableLayouts(emitters) {
|
|
1040
|
+
const fingerprints = emitters.map((emitter) => emitter.reflection.layout?.fingerprint).filter((fingerprint2) => fingerprint2 !== void 0);
|
|
1041
|
+
return fingerprints.every((fingerprint2) => fingerprint2 === fingerprints[0]);
|
|
1042
|
+
}
|
|
1043
|
+
function validEmitter(value) {
|
|
1044
|
+
const reflection = record2(value) && record2(value.reflection) ? value.reflection : void 0;
|
|
1045
|
+
const layout = reflection !== void 0 ? reflection.layout : void 0;
|
|
1046
|
+
return record2(value) && typeof value.id === "string" && typeof value.module === "string" && value.module.length > 0 && Number.isInteger(value.capacity) && value.capacity > 0 && record2(value.backend) && value.backend.required === "gpu" && Object.keys(value.backend).length === 1 && typeof value.wgsl === "string" && value.wgsl.length > 0 && record2(value.reflection) && validReflectionLayout(layout) && Array.isArray(value.reflection.entryPoints) && value.reflection.entryPoints.includes("forgeax_vfx_spawn_main") && value.reflection.entryPoints.includes("forgeax_vfx_compact_main") && value.reflection.entryPoints.includes("forgeax_vfx_billboard_main") && value.reflection.entryPoints.includes("forgeax_vfx_mesh_main") && value.reflection.entryPoints.includes("forgeax_vfx_ribbon_main") && value.reflection.entryPoints.includes("forgeax_vfx_trail_main") && value.reflection.entryPoints.includes("forgeax_vfx_beam_main");
|
|
1047
|
+
}
|
|
1048
|
+
var vfxGpuEffectPackLoader = {
|
|
1049
|
+
kind: "particle-effect",
|
|
1050
|
+
async load(input, _context) {
|
|
1051
|
+
if (input.payload.kind !== "particle-effect" || input.payload.schemaVersion !== 2 || !Array.isArray(input.payload.emitters) || typeof input.payload.programFingerprint !== "string" || !record2(input.payload.program)) {
|
|
1052
|
+
return failure2(
|
|
1053
|
+
"vfx-asset-v2-invalid",
|
|
1054
|
+
input.guid,
|
|
1055
|
+
"payload",
|
|
1056
|
+
"a schemaVersion 2 particle payload with its complete program and fingerprint",
|
|
1057
|
+
"migrate behavior to WGSL and recook; the runtime does not interpret v1"
|
|
1058
|
+
);
|
|
1059
|
+
}
|
|
1060
|
+
const artifact = input.artifacts[VFX_GPU_PROGRAM_ARTIFACT_KEY];
|
|
1061
|
+
if (artifact === void 0) {
|
|
1062
|
+
return failure2(
|
|
1063
|
+
"vfx-asset-v2-program-missing",
|
|
1064
|
+
input.guid,
|
|
1065
|
+
VFX_GPU_PROGRAM_ARTIFACT_KEY,
|
|
1066
|
+
"the asset-local cooked VFX program",
|
|
1067
|
+
"recook the particle effect and retry the load"
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
let decoded;
|
|
1071
|
+
try {
|
|
1072
|
+
decoded = JSON.parse(new TextDecoder().decode(artifact.bytes));
|
|
1073
|
+
} catch {
|
|
1074
|
+
return failure2(
|
|
1075
|
+
"vfx-asset-v2-invalid",
|
|
1076
|
+
input.guid,
|
|
1077
|
+
VFX_GPU_PROGRAM_ARTIFACT_KEY,
|
|
1078
|
+
"canonical JSON program bytes",
|
|
1079
|
+
"recook the particle effect and retry the load"
|
|
1080
|
+
);
|
|
1081
|
+
}
|
|
1082
|
+
if (!record2(decoded) || decoded.format !== VFX_GPU_PROGRAM_FORMAT || !Array.isArray(decoded.emitters) || !decoded.emitters.every(validEmitter) || !stableLayouts(decoded.emitters)) {
|
|
1083
|
+
return failure2(
|
|
1084
|
+
"vfx-asset-v2-invalid",
|
|
1085
|
+
input.guid,
|
|
1086
|
+
VFX_GPU_PROGRAM_ARTIFACT_KEY,
|
|
1087
|
+
`a ${VFX_GPU_PROGRAM_FORMAT} managed GPU program`,
|
|
1088
|
+
"recook with the current VFX compiler ABI"
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
1091
|
+
const decodedEmitters = decoded.emitters;
|
|
1092
|
+
if (input.payload.program.format !== VFX_GPU_PROGRAM_FORMAT || input.payload.program.fingerprint !== input.payload.programFingerprint || !Array.isArray(input.payload.program.emitters)) {
|
|
1093
|
+
return failure2(
|
|
1094
|
+
"vfx-asset-v2-invalid",
|
|
1095
|
+
input.guid,
|
|
1096
|
+
"payload.program",
|
|
1097
|
+
"the complete canonical program matching payload.programFingerprint",
|
|
1098
|
+
"recook the particle effect atomically"
|
|
1099
|
+
);
|
|
1100
|
+
}
|
|
1101
|
+
const actualFingerprint = await fingerprint(artifact.bytes);
|
|
1102
|
+
if (actualFingerprint !== input.payload.programFingerprint) {
|
|
1103
|
+
return failure2(
|
|
1104
|
+
"vfx-asset-v2-fingerprint-mismatch",
|
|
1105
|
+
input.guid,
|
|
1106
|
+
"payload.programFingerprint",
|
|
1107
|
+
"payload and program artifact fingerprints to match",
|
|
1108
|
+
"cold-cook the particle asset so payload and artifact publish atomically"
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
const emitters = input.payload.emitters.map((value) => {
|
|
1112
|
+
if (!record2(value) || typeof value.id !== "string" || !Number.isInteger(value.capacity)) {
|
|
1113
|
+
return void 0;
|
|
1114
|
+
}
|
|
1115
|
+
return { id: value.id, capacity: value.capacity };
|
|
1116
|
+
});
|
|
1117
|
+
if (emitters.some((value) => value === void 0) || emitters.length !== decodedEmitters.length || emitters.some(
|
|
1118
|
+
(value, index) => value?.id !== decodedEmitters[index]?.id || value?.capacity !== decodedEmitters[index]?.capacity
|
|
1119
|
+
)) {
|
|
1120
|
+
return failure2(
|
|
1121
|
+
"vfx-asset-v2-invalid",
|
|
1122
|
+
input.guid,
|
|
1123
|
+
"payload.emitters",
|
|
1124
|
+
"payload emitter identities and capacities to match the program",
|
|
1125
|
+
"recook the particle effect atomically"
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
1128
|
+
return ok(
|
|
1129
|
+
Object.freeze({
|
|
1130
|
+
guid: input.guid,
|
|
1131
|
+
kind: "particle-effect",
|
|
1132
|
+
schemaVersion: 2,
|
|
1133
|
+
programFingerprint: actualFingerprint,
|
|
1134
|
+
emitters: Object.freeze(emitters),
|
|
1135
|
+
program: Object.freeze({
|
|
1136
|
+
format: VFX_GPU_PROGRAM_FORMAT,
|
|
1137
|
+
fingerprint: actualFingerprint,
|
|
1138
|
+
emitters: Object.freeze(decodedEmitters)
|
|
1139
|
+
})
|
|
1140
|
+
})
|
|
1141
|
+
);
|
|
1142
|
+
}
|
|
1143
|
+
};
|
|
1144
|
+
var vfxGpuEffectContribution = {
|
|
1145
|
+
kind: { kind: "particle-effect" },
|
|
1146
|
+
consumer: "VfxGpuRuntime",
|
|
1147
|
+
decoder: {
|
|
1148
|
+
async decode({ envelope, artifacts }) {
|
|
1149
|
+
const descriptor = envelope.artifacts[VFX_GPU_PROGRAM_ARTIFACT_KEY];
|
|
1150
|
+
const bytes = descriptor === void 0 ? void 0 : await artifacts.read(descriptor);
|
|
1151
|
+
if (bytes !== void 0 && !bytes.ok) return bytes;
|
|
1152
|
+
const result = await vfxGpuEffectPackLoader.load(
|
|
1153
|
+
{
|
|
1154
|
+
guid: envelope.guid,
|
|
1155
|
+
kind: envelope.kind,
|
|
1156
|
+
payload: envelope.payload,
|
|
1157
|
+
artifacts: bytes === void 0 || descriptor === void 0 ? {} : {
|
|
1158
|
+
[VFX_GPU_PROGRAM_ARTIFACT_KEY]: {
|
|
1159
|
+
descriptor: { path: descriptor.path, mediaType: descriptor.mediaType },
|
|
1160
|
+
bytes: bytes.value
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
},
|
|
1164
|
+
{}
|
|
1165
|
+
);
|
|
1166
|
+
if (result.ok) return result;
|
|
1167
|
+
const error = {
|
|
1168
|
+
code: "asset-package-invalid",
|
|
1169
|
+
expected: result.error.expected,
|
|
1170
|
+
hint: result.error.hint,
|
|
1171
|
+
detail: { guid: envelope.guid, reason: result.error.code }
|
|
1172
|
+
};
|
|
1173
|
+
return err(error);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
};
|
|
1177
|
+
async function loadVfxGpuEffect(registry, guid) {
|
|
1178
|
+
return registry.load(guid, vfxGpuEffectContribution.kind);
|
|
1179
|
+
}
|
|
1180
|
+
function failure3(code, path, expected, hint, actual) {
|
|
1181
|
+
return err({
|
|
1182
|
+
code,
|
|
1183
|
+
expected,
|
|
1184
|
+
hint,
|
|
1185
|
+
detail: actual === void 0 ? { path } : { path, actual }
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
function allFields(contract) {
|
|
1189
|
+
return [...contract.reflection.parameters.fields, ...contract.reflection.custom.fields];
|
|
1190
|
+
}
|
|
1191
|
+
function normalizeValue(type, value) {
|
|
1192
|
+
const normalize = (component) => {
|
|
1193
|
+
if (type === "i32" || type === "u32") return Math.trunc(component);
|
|
1194
|
+
return Math.fround(component);
|
|
1195
|
+
};
|
|
1196
|
+
return typeof value === "number" ? normalize(value) : value.map(normalize);
|
|
1197
|
+
}
|
|
1198
|
+
function canonicalBytes(contract, values) {
|
|
1199
|
+
const fields2 = allFields(contract).map((field2) => ({ name: field2.name, type: field2.type, value: values[field2.name] })).sort((left, right) => left.name.localeCompare(right.name)).map((field2) => ({
|
|
1200
|
+
name: field2.name,
|
|
1201
|
+
type: field2.type,
|
|
1202
|
+
value: field2.value === void 0 ? void 0 : normalizeValue(field2.type, field2.value)
|
|
1203
|
+
}));
|
|
1204
|
+
return new TextEncoder().encode(JSON.stringify({ fields: fields2 }));
|
|
1205
|
+
}
|
|
1206
|
+
function sameBytes(left, right) {
|
|
1207
|
+
if (left.length !== right.length) return false;
|
|
1208
|
+
for (let index = 0; index < left.length; index += 1) {
|
|
1209
|
+
if (left[index] !== right[index]) return false;
|
|
1210
|
+
}
|
|
1211
|
+
return true;
|
|
1212
|
+
}
|
|
1213
|
+
function cloneValue(value) {
|
|
1214
|
+
return typeof value === "number" ? value : [...value];
|
|
1215
|
+
}
|
|
1216
|
+
function cloneValues(values) {
|
|
1217
|
+
const clone = {};
|
|
1218
|
+
for (const [name, value] of Object.entries(values)) clone[name] = cloneValue(value);
|
|
1219
|
+
return Object.freeze(clone);
|
|
1220
|
+
}
|
|
1221
|
+
function cloneBytes(bytes) {
|
|
1222
|
+
return bytes.slice();
|
|
1223
|
+
}
|
|
1224
|
+
function validChannelPayload(payload) {
|
|
1225
|
+
if (payload === null || typeof payload !== "object" || Array.isArray(payload)) return false;
|
|
1226
|
+
const value = payload;
|
|
1227
|
+
const position = value.position;
|
|
1228
|
+
return Array.isArray(position) && position.length === 3 && position.every((component) => typeof component === "number" && Number.isFinite(component)) && typeof value.strength === "number" && Number.isFinite(value.strength);
|
|
1229
|
+
}
|
|
1230
|
+
function withTick(input, tick) {
|
|
1231
|
+
const result = { ...input };
|
|
1232
|
+
Object.defineProperty(result, "tick", { value: tick, enumerable: false });
|
|
1233
|
+
return Object.freeze(result);
|
|
1234
|
+
}
|
|
1235
|
+
var ParticleEffectInstance = class {
|
|
1236
|
+
contract;
|
|
1237
|
+
#values;
|
|
1238
|
+
#generation = 0;
|
|
1239
|
+
#sequence = 0;
|
|
1240
|
+
#pendingPatches = [];
|
|
1241
|
+
#pendingChannels = [];
|
|
1242
|
+
#channelCapacities = /* @__PURE__ */ new Map([["impact", 32]]);
|
|
1243
|
+
#droppedCount = 0;
|
|
1244
|
+
constructor(contract, options = {}) {
|
|
1245
|
+
this.contract = contract;
|
|
1246
|
+
if (options.parent?.fingerprint !== void 0 && options.parent.fingerprint !== contract.fingerprint) {
|
|
1247
|
+
throw new TypeError(
|
|
1248
|
+
`ParticleEffectInstance parent fingerprint ${options.parent.fingerprint} does not match ${contract.fingerprint}`
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
const parentDefaults = options.parent?.defaults ?? contract.defaults;
|
|
1252
|
+
const initial = { ...parentDefaults, ...options.initialValues };
|
|
1253
|
+
const checked = contract.createValues(initial);
|
|
1254
|
+
if (!checked.ok) throw new TypeError(checked.error.hint);
|
|
1255
|
+
this.#values = cloneValues(checked.value);
|
|
1256
|
+
for (const channel of options.channels ?? []) {
|
|
1257
|
+
this.#channelCapacities.set(channel.id, channel.capacity);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
get values() {
|
|
1261
|
+
return this.#values;
|
|
1262
|
+
}
|
|
1263
|
+
get generation() {
|
|
1264
|
+
return this.#generation;
|
|
1265
|
+
}
|
|
1266
|
+
get pendingPatchCount() {
|
|
1267
|
+
return this.#pendingPatches.length;
|
|
1268
|
+
}
|
|
1269
|
+
setChannelCapacity(channel, capacity) {
|
|
1270
|
+
if (!Number.isInteger(capacity) || capacity <= 0) {
|
|
1271
|
+
throw new RangeError("channel capacity must be a positive integer");
|
|
1272
|
+
}
|
|
1273
|
+
this.#channelCapacities.set(channel, capacity);
|
|
1274
|
+
}
|
|
1275
|
+
submit(input) {
|
|
1276
|
+
if (typeof input.channel !== "string" || input.channel.length === 0 || !Number.isInteger(input.sequence) || input.sequence < 0 || !validChannelPayload(input.payload)) {
|
|
1277
|
+
return failure3(
|
|
1278
|
+
"vfx-channel-invalid",
|
|
1279
|
+
`channels.${input.channel || "unknown"}`,
|
|
1280
|
+
"a named channel with a finite impact payload and non-negative integer sequence",
|
|
1281
|
+
"repair the typed channel payload before the next FixedUpdate"
|
|
1282
|
+
);
|
|
1283
|
+
}
|
|
1284
|
+
const capacity = this.#channelCapacities.get(input.channel) ?? 32;
|
|
1285
|
+
if (this.#pendingChannels.length >= capacity) {
|
|
1286
|
+
this.#droppedCount += 1;
|
|
1287
|
+
return failure3(
|
|
1288
|
+
"vfx-channel-overflow",
|
|
1289
|
+
`channels.${input.channel}`,
|
|
1290
|
+
`at most ${capacity} pending inputs for this channel`,
|
|
1291
|
+
"reduce the input rate or increase the reflected channel capacity and recook",
|
|
1292
|
+
input.sequence
|
|
1293
|
+
);
|
|
1294
|
+
}
|
|
1295
|
+
this.#pendingChannels.push(
|
|
1296
|
+
Object.freeze({
|
|
1297
|
+
channel: input.channel,
|
|
1298
|
+
payload: Object.freeze({
|
|
1299
|
+
position: [...input.payload.position],
|
|
1300
|
+
strength: input.payload.strength
|
|
1301
|
+
}),
|
|
1302
|
+
sequence: input.sequence
|
|
1303
|
+
})
|
|
1304
|
+
);
|
|
1305
|
+
return ok(void 0);
|
|
1306
|
+
}
|
|
1307
|
+
patch(patch) {
|
|
1308
|
+
const candidate = { ...this.#values, ...patch };
|
|
1309
|
+
const checked = this.contract.validateValues(candidate);
|
|
1310
|
+
if (!checked.ok) return checked;
|
|
1311
|
+
this.#pendingPatches.push(Object.freeze({ ...patch }));
|
|
1312
|
+
return ok(void 0);
|
|
1313
|
+
}
|
|
1314
|
+
commit(options) {
|
|
1315
|
+
const candidate = { ...this.#values };
|
|
1316
|
+
for (const patch of this.#pendingPatches) Object.assign(candidate, patch);
|
|
1317
|
+
const checked = this.contract.validateValues(candidate);
|
|
1318
|
+
if (!checked.ok) return checked;
|
|
1319
|
+
const nextValues = cloneValues(checked.value);
|
|
1320
|
+
const packed = this.contract.pack(nextValues);
|
|
1321
|
+
if (!packed.ok) return packed;
|
|
1322
|
+
const patchCount = this.#pendingPatches.length;
|
|
1323
|
+
const droppedCount = this.#droppedCount;
|
|
1324
|
+
const channelInputs = Object.freeze(
|
|
1325
|
+
[...this.#pendingChannels].sort((left, right) => left.sequence - right.sequence).map((input) => withTick(input, options.tick))
|
|
1326
|
+
);
|
|
1327
|
+
this.#pendingPatches = [];
|
|
1328
|
+
this.#pendingChannels = [];
|
|
1329
|
+
this.#droppedCount = 0;
|
|
1330
|
+
if (patchCount > 0) this.#generation += 1;
|
|
1331
|
+
this.#values = nextValues;
|
|
1332
|
+
const canonicalPayload = canonicalBytes(this.contract, nextValues);
|
|
1333
|
+
const sequence = this.#sequence++;
|
|
1334
|
+
const replayInput = Object.freeze({
|
|
1335
|
+
seed: options.seed,
|
|
1336
|
+
tick: options.tick,
|
|
1337
|
+
generation: this.#generation,
|
|
1338
|
+
sequence,
|
|
1339
|
+
fingerprint: this.contract.fingerprint,
|
|
1340
|
+
payload: cloneBytes(canonicalPayload),
|
|
1341
|
+
values: nextValues,
|
|
1342
|
+
channelInputs,
|
|
1343
|
+
droppedCount
|
|
1344
|
+
});
|
|
1345
|
+
return ok({
|
|
1346
|
+
seed: options.seed,
|
|
1347
|
+
tick: options.tick,
|
|
1348
|
+
generation: this.#generation,
|
|
1349
|
+
sequence,
|
|
1350
|
+
values: nextValues,
|
|
1351
|
+
parameterBlock: cloneBytes(packed.value),
|
|
1352
|
+
canonicalPayload,
|
|
1353
|
+
replayInput,
|
|
1354
|
+
patchCount,
|
|
1355
|
+
channelInputs,
|
|
1356
|
+
droppedCount
|
|
1357
|
+
});
|
|
1358
|
+
}
|
|
1359
|
+
replay(input) {
|
|
1360
|
+
if (input.fingerprint !== this.contract.fingerprint) {
|
|
1361
|
+
return failure3(
|
|
1362
|
+
"vfx-instance-replay-mismatch",
|
|
1363
|
+
"replay.fingerprint",
|
|
1364
|
+
this.contract.fingerprint,
|
|
1365
|
+
"replay the input with the matching cooked effect contract",
|
|
1366
|
+
input.fingerprint
|
|
1367
|
+
);
|
|
1368
|
+
}
|
|
1369
|
+
const checked = this.contract.validateValues(input.values);
|
|
1370
|
+
if (!checked.ok) return checked;
|
|
1371
|
+
const canonicalPayload = canonicalBytes(this.contract, checked.value);
|
|
1372
|
+
if (!sameBytes(canonicalPayload, input.payload)) {
|
|
1373
|
+
return failure3(
|
|
1374
|
+
"vfx-instance-replay-mismatch",
|
|
1375
|
+
"replay.payload",
|
|
1376
|
+
"canonical payload bytes for replay.values",
|
|
1377
|
+
"record and replay normalized values from the same fixed-tick input"
|
|
1378
|
+
);
|
|
1379
|
+
}
|
|
1380
|
+
const packed = this.contract.pack(checked.value);
|
|
1381
|
+
if (!packed.ok) return packed;
|
|
1382
|
+
this.#values = cloneValues(checked.value);
|
|
1383
|
+
this.#generation = input.generation;
|
|
1384
|
+
this.#sequence = Math.max(this.#sequence, input.sequence + 1);
|
|
1385
|
+
this.#pendingPatches = [];
|
|
1386
|
+
this.#pendingChannels = [...input.channelInputs].map(
|
|
1387
|
+
(channel) => withTick(channel, input.tick)
|
|
1388
|
+
);
|
|
1389
|
+
this.#droppedCount = 0;
|
|
1390
|
+
const values = this.#values;
|
|
1391
|
+
const replayInput = Object.freeze({
|
|
1392
|
+
...input,
|
|
1393
|
+
payload: cloneBytes(input.payload),
|
|
1394
|
+
values,
|
|
1395
|
+
channelInputs: this.#pendingChannels,
|
|
1396
|
+
droppedCount: input.droppedCount
|
|
1397
|
+
});
|
|
1398
|
+
return ok({
|
|
1399
|
+
seed: input.seed,
|
|
1400
|
+
tick: input.tick,
|
|
1401
|
+
generation: input.generation,
|
|
1402
|
+
sequence: input.sequence,
|
|
1403
|
+
values,
|
|
1404
|
+
parameterBlock: cloneBytes(packed.value),
|
|
1405
|
+
canonicalPayload,
|
|
1406
|
+
replayInput,
|
|
1407
|
+
patchCount: 0,
|
|
1408
|
+
channelInputs: this.#pendingChannels,
|
|
1409
|
+
droppedCount: input.droppedCount
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1412
|
+
};
|
|
1413
|
+
function createParticleEffectInstance(contract, options = {}) {
|
|
1414
|
+
return new ParticleEffectInstance(contract, options);
|
|
1415
|
+
}
|
|
1416
|
+
var ParticleEffectPlayer = defineComponent("ParticleEffectPlayer", {
|
|
1417
|
+
// The VFX render owner re-resolves the compiled effect on the target world;
|
|
1418
|
+
// playback intent remains portable simulation state.
|
|
1419
|
+
effect: { type: "shared<ParticleEffectAsset>" },
|
|
1420
|
+
playing: { type: "bool", default: true },
|
|
1421
|
+
seed: { type: "u32", default: 0 },
|
|
1422
|
+
timeScale: { type: "f32", default: 1 }
|
|
1423
|
+
});
|
|
1424
|
+
|
|
1425
|
+
// src/gpu-runtime.ts
|
|
1426
|
+
var VFX_GPU_RUNTIME_RESOURCE_KEY = "VfxGpuRuntime";
|
|
1427
|
+
function createVfxInspectSnapshot(input) {
|
|
1428
|
+
return {
|
|
1429
|
+
layout: { fingerprint: input.layoutFingerprint },
|
|
1430
|
+
values: { generation: input.parameterGeneration, patchCount: input.patchCount },
|
|
1431
|
+
...input.dataInterfaces === void 0 ? {} : { dataInterfaces: input.dataInterfaces },
|
|
1432
|
+
...input.channels === void 0 ? {} : { channels: input.channels },
|
|
1433
|
+
...input.stages === void 0 ? {} : { stages: input.stages },
|
|
1434
|
+
...input.renderers === void 0 ? {} : { renderers: input.renderers },
|
|
1435
|
+
...input.hmr === void 0 ? {} : { hmr: input.hmr },
|
|
1436
|
+
...input.gpuTiming === void 0 ? {} : { gpuTiming: input.gpuTiming },
|
|
1437
|
+
...input.error === void 0 ? {} : { error: input.error }
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
function eventCounters(intent, dropped, eventSources) {
|
|
1441
|
+
const inputs = intent.channelInputs;
|
|
1442
|
+
const events = [...intent.emitter.events ?? [], ...eventSources];
|
|
1443
|
+
return Object.freeze({
|
|
1444
|
+
queued: inputs.length,
|
|
1445
|
+
produced: (intent.emitter.events?.length ?? 0) > 0 ? inputs.length : 0,
|
|
1446
|
+
consumed: eventSources.length > 0 ? inputs.length * eventSources.reduce((total, event) => total + event.fanOut, 0) : 0,
|
|
1447
|
+
dropped,
|
|
1448
|
+
overflow: dropped > 0 ? 1 : 0,
|
|
1449
|
+
fanOut: events.reduce((total, event) => total + event.fanOut, 0),
|
|
1450
|
+
recursionDepth: events.reduce((depth, event) => Math.max(depth, event.recursionDepth), 0),
|
|
1451
|
+
lastSequence: inputs.at(-1)?.sequence ?? -1
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
var VfxGpuRuntime = class {
|
|
1455
|
+
#maxQueuedTicks;
|
|
1456
|
+
#players = /* @__PURE__ */ new Map();
|
|
1457
|
+
#instances = /* @__PURE__ */ new Map();
|
|
1458
|
+
#seen = /* @__PURE__ */ new Set();
|
|
1459
|
+
#intents = [];
|
|
1460
|
+
#diagnostics = [];
|
|
1461
|
+
#lastCommitted = /* @__PURE__ */ new Map();
|
|
1462
|
+
#lastCommittedByEmitter = /* @__PURE__ */ new Map();
|
|
1463
|
+
#eventCounters = /* @__PURE__ */ new Map();
|
|
1464
|
+
#cameraVisibility = /* @__PURE__ */ new Map();
|
|
1465
|
+
#sessionEnabled = /* @__PURE__ */ new Map();
|
|
1466
|
+
#replayRequests = /* @__PURE__ */ new Set();
|
|
1467
|
+
#replayInputs = /* @__PURE__ */ new Map();
|
|
1468
|
+
#sequence = 0;
|
|
1469
|
+
#renderGeneration = 0;
|
|
1470
|
+
constructor(options = {}) {
|
|
1471
|
+
this.#maxQueuedTicks = options.maxQueuedTicks ?? 8;
|
|
1472
|
+
}
|
|
1473
|
+
/** Resource generation owned by the current VFX render attachment. */
|
|
1474
|
+
get renderGeneration() {
|
|
1475
|
+
return this.#renderGeneration;
|
|
1476
|
+
}
|
|
1477
|
+
snapshot() {
|
|
1478
|
+
return this.#intents;
|
|
1479
|
+
}
|
|
1480
|
+
diagnostics() {
|
|
1481
|
+
return this.#diagnostics;
|
|
1482
|
+
}
|
|
1483
|
+
lastCommitted(player) {
|
|
1484
|
+
return this.#lastCommitted.get(player);
|
|
1485
|
+
}
|
|
1486
|
+
inspectPlayers() {
|
|
1487
|
+
return Object.freeze(
|
|
1488
|
+
[...this.#players.keys()].sort((left, right) => Number(left) - Number(right)).flatMap((player) => {
|
|
1489
|
+
const snapshot = this.inspectPlayer(player);
|
|
1490
|
+
return snapshot === void 0 ? [] : [snapshot];
|
|
1491
|
+
})
|
|
1492
|
+
);
|
|
1493
|
+
}
|
|
1494
|
+
inspectPlayer(player) {
|
|
1495
|
+
const state = this.#players.get(player);
|
|
1496
|
+
if (state === void 0) return void 0;
|
|
1497
|
+
const instance = this.#instances.get(player);
|
|
1498
|
+
const latest = state.emitters.map((emitter) => this.#latestIntent(player, emitter.id));
|
|
1499
|
+
const lastIntent = latest.reduce(
|
|
1500
|
+
(current, intent) => intent !== void 0 && (current === void 0 || intent.sequence > current.sequence) ? intent : current,
|
|
1501
|
+
void 0
|
|
1502
|
+
);
|
|
1503
|
+
const layoutFingerprint = state.emitters.find((emitter) => emitter.reflection.layout !== void 0)?.reflection.layout?.fingerprint ?? state.programFingerprint;
|
|
1504
|
+
const lastCommitted = this.#lastCommitted.get(player);
|
|
1505
|
+
const queuedTicks = /* @__PURE__ */ new Set();
|
|
1506
|
+
let queuedIntents = 0;
|
|
1507
|
+
for (const intent of this.#intents) {
|
|
1508
|
+
if (intent.player !== player) continue;
|
|
1509
|
+
queuedIntents += 1;
|
|
1510
|
+
queuedTicks.add(intent.tick);
|
|
1511
|
+
}
|
|
1512
|
+
return Object.freeze({
|
|
1513
|
+
player,
|
|
1514
|
+
assetGuid: state.assetGuid,
|
|
1515
|
+
programFingerprint: state.programFingerprint,
|
|
1516
|
+
seed: lastIntent?.seed ?? state.seed,
|
|
1517
|
+
fixedDelta: lastIntent?.fixedDelta ?? 0,
|
|
1518
|
+
playing: state.playing,
|
|
1519
|
+
values: Object.freeze({
|
|
1520
|
+
layoutFingerprint,
|
|
1521
|
+
generation: instance?.generation ?? lastIntent?.instanceGeneration ?? 0,
|
|
1522
|
+
pendingPatchCount: instance?.pendingPatchCount ?? 0
|
|
1523
|
+
}),
|
|
1524
|
+
queuedIntents,
|
|
1525
|
+
queuedTicks: queuedTicks.size,
|
|
1526
|
+
lastCommitted: lastCommitted === void 0 ? null : Object.freeze({
|
|
1527
|
+
sequence: lastCommitted.sequence,
|
|
1528
|
+
tick: lastCommitted.tick,
|
|
1529
|
+
phaseTick: lastCommitted.phaseTick,
|
|
1530
|
+
playCycle: lastCommitted.playCycle,
|
|
1531
|
+
spawnCount: lastCommitted.spawnCount,
|
|
1532
|
+
firstParticleId: lastCommitted.firstParticleId,
|
|
1533
|
+
reset: lastCommitted.reset,
|
|
1534
|
+
instanceGeneration: lastCommitted.instanceGeneration,
|
|
1535
|
+
instancePatchCount: lastCommitted.instancePatchCount
|
|
1536
|
+
}),
|
|
1537
|
+
channels: this.eventCounters(player),
|
|
1538
|
+
emitters: Object.freeze(
|
|
1539
|
+
state.emitters.map((emitter, index) => {
|
|
1540
|
+
const intent = latest[index];
|
|
1541
|
+
return Object.freeze({
|
|
1542
|
+
id: emitter.id,
|
|
1543
|
+
module: emitter.module,
|
|
1544
|
+
capacity: emitter.capacity,
|
|
1545
|
+
cameraVisible: this.#cameraVisibility.get(`${player}:${emitter.id}`) ?? true,
|
|
1546
|
+
sessionEnabled: this.isEmitterSessionEnabled(player, emitter.id),
|
|
1547
|
+
phaseTick: intent?.phaseTick ?? null,
|
|
1548
|
+
tick: intent?.tick ?? null,
|
|
1549
|
+
playCycle: intent?.playCycle ?? null,
|
|
1550
|
+
spawnCount: intent?.spawnCount ?? 0,
|
|
1551
|
+
firstParticleId: intent?.firstParticleId ?? 0,
|
|
1552
|
+
reset: intent?.reset ?? false,
|
|
1553
|
+
schedule: emitter.schedule,
|
|
1554
|
+
bounds: emitter.bounds,
|
|
1555
|
+
simulationWhenCulled: emitter.simulationWhenCulled,
|
|
1556
|
+
renderers: Object.freeze(
|
|
1557
|
+
emitter.renderers.map(
|
|
1558
|
+
(renderer, rendererIndex) => Object.freeze({
|
|
1559
|
+
index: rendererIndex,
|
|
1560
|
+
kind: renderer.kind,
|
|
1561
|
+
enabled: renderer.enabled ?? true
|
|
1562
|
+
})
|
|
1563
|
+
)
|
|
1564
|
+
),
|
|
1565
|
+
stages: Object.freeze((emitter.reflection.stages ?? []).map((stage) => stage.id)),
|
|
1566
|
+
dataInterfaces: Object.freeze(
|
|
1567
|
+
(emitter.reflection.dataInterfaces ?? []).map((requirement) => requirement.token)
|
|
1568
|
+
)
|
|
1569
|
+
});
|
|
1570
|
+
})
|
|
1571
|
+
),
|
|
1572
|
+
diagnostics: Object.freeze(
|
|
1573
|
+
this.#diagnostics.filter((diagnostic) => diagnostic.detail.player === player)
|
|
1574
|
+
)
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1577
|
+
eventCounters(player) {
|
|
1578
|
+
return this.#eventCounters.get(player) ?? this.#lastCommitted.get(player)?.eventCounters ?? {
|
|
1579
|
+
queued: 0,
|
|
1580
|
+
produced: 0,
|
|
1581
|
+
consumed: 0,
|
|
1582
|
+
dropped: 0,
|
|
1583
|
+
overflow: 0,
|
|
1584
|
+
fanOut: 0,
|
|
1585
|
+
recursionDepth: 0,
|
|
1586
|
+
lastSequence: -1
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
markEventDispatched(player, counters) {
|
|
1590
|
+
const prior = this.#eventCounters.get(player);
|
|
1591
|
+
if (counters.produced === 0 && prior !== void 0) {
|
|
1592
|
+
this.#eventCounters.set(
|
|
1593
|
+
player,
|
|
1594
|
+
Object.freeze({
|
|
1595
|
+
...prior,
|
|
1596
|
+
queued: 0,
|
|
1597
|
+
consumed: Math.max(prior.consumed, counters.consumed)
|
|
1598
|
+
})
|
|
1599
|
+
);
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1602
|
+
this.#eventCounters.set(
|
|
1603
|
+
player,
|
|
1604
|
+
Object.freeze({ ...counters, queued: 0, consumed: counters.produced })
|
|
1605
|
+
);
|
|
1606
|
+
}
|
|
1607
|
+
hasPlayer(player) {
|
|
1608
|
+
return this.#players.has(player);
|
|
1609
|
+
}
|
|
1610
|
+
attachInstance(player, instance) {
|
|
1611
|
+
this.#instances.set(player, instance);
|
|
1612
|
+
}
|
|
1613
|
+
detachInstance(player) {
|
|
1614
|
+
this.#instances.delete(player);
|
|
1615
|
+
}
|
|
1616
|
+
getInstance(player) {
|
|
1617
|
+
return this.#instances.get(player);
|
|
1618
|
+
}
|
|
1619
|
+
setEmitterCameraVisibility(player, emitterId, visible) {
|
|
1620
|
+
this.#cameraVisibility.set(`${player}:${emitterId}`, visible);
|
|
1621
|
+
}
|
|
1622
|
+
setEmitterSessionEnabled(player, emitterId, enabled) {
|
|
1623
|
+
this.#sessionEnabled.set(`${player}:${emitterId}`, enabled);
|
|
1624
|
+
}
|
|
1625
|
+
isEmitterSessionEnabled(player, emitterId) {
|
|
1626
|
+
return this.#sessionEnabled.get(`${player}:${emitterId}`) ?? true;
|
|
1627
|
+
}
|
|
1628
|
+
/** Restart from tick zero without changing authored `ParticleEffectPlayer.playing`. */
|
|
1629
|
+
replay(player, input) {
|
|
1630
|
+
this.#discardQueuedIntents(player);
|
|
1631
|
+
this.#clearDiagnostics(player, "vfx-intent-queue-overflow");
|
|
1632
|
+
this.#replayInputs.delete(player);
|
|
1633
|
+
if (input === void 0) {
|
|
1634
|
+
this.#instances.delete(player);
|
|
1635
|
+
} else {
|
|
1636
|
+
this.#replayInputs.set(player, input);
|
|
1637
|
+
}
|
|
1638
|
+
this.#replayRequests.add(player);
|
|
1639
|
+
}
|
|
1640
|
+
commit(sequence) {
|
|
1641
|
+
const committedPlayers = /* @__PURE__ */ new Set();
|
|
1642
|
+
for (const intent of this.#intents) {
|
|
1643
|
+
if (intent.sequence > sequence) break;
|
|
1644
|
+
const state = this.#players.get(intent.player);
|
|
1645
|
+
if (state !== void 0) state.hasCommitted = true;
|
|
1646
|
+
this.#lastCommitted.set(intent.player, intent);
|
|
1647
|
+
let emitters = this.#lastCommittedByEmitter.get(intent.player);
|
|
1648
|
+
if (emitters === void 0) {
|
|
1649
|
+
emitters = /* @__PURE__ */ new Map();
|
|
1650
|
+
this.#lastCommittedByEmitter.set(intent.player, emitters);
|
|
1651
|
+
}
|
|
1652
|
+
emitters.set(intent.emitter.id, intent);
|
|
1653
|
+
committedPlayers.add(intent.player);
|
|
1654
|
+
}
|
|
1655
|
+
const retained = this.#intents.findIndex((intent) => intent.sequence > sequence);
|
|
1656
|
+
if (retained < 0) this.#intents.length = 0;
|
|
1657
|
+
else if (retained > 0) this.#intents.splice(0, retained);
|
|
1658
|
+
for (const player of committedPlayers) {
|
|
1659
|
+
this.#clearDiagnostics(player, "vfx-intent-queue-overflow");
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
reset(player) {
|
|
1663
|
+
this.#discardQueuedIntents(player);
|
|
1664
|
+
this.#clearDiagnostics(player);
|
|
1665
|
+
this.#players.delete(player);
|
|
1666
|
+
this.#instances.delete(player);
|
|
1667
|
+
this.#replayRequests.delete(player);
|
|
1668
|
+
this.#lastCommitted.delete(player);
|
|
1669
|
+
this.#lastCommittedByEmitter.delete(player);
|
|
1670
|
+
this.#eventCounters.delete(player);
|
|
1671
|
+
this.#replayInputs.delete(player);
|
|
1672
|
+
const prefix = `${player}:`;
|
|
1673
|
+
for (const key of this.#cameraVisibility.keys()) {
|
|
1674
|
+
if (key.startsWith(prefix)) this.#cameraVisibility.delete(key);
|
|
1675
|
+
}
|
|
1676
|
+
for (const key of this.#sessionEnabled.keys()) {
|
|
1677
|
+
if (key.startsWith(prefix)) this.#sessionEnabled.delete(key);
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
/**
|
|
1681
|
+
* Drop every generation-owned VFX runtime value after device recovery.
|
|
1682
|
+
* Authored ParticleEffectPlayer state remains the restart source on the next
|
|
1683
|
+
* FixedUpdate; no queued intent or stale instance crosses the boundary.
|
|
1684
|
+
*/
|
|
1685
|
+
recover() {
|
|
1686
|
+
this.#renderGeneration += 1;
|
|
1687
|
+
this.#players.clear();
|
|
1688
|
+
this.#instances.clear();
|
|
1689
|
+
this.#seen.clear();
|
|
1690
|
+
this.#intents.length = 0;
|
|
1691
|
+
this.#diagnostics.length = 0;
|
|
1692
|
+
this.#lastCommitted.clear();
|
|
1693
|
+
this.#lastCommittedByEmitter.clear();
|
|
1694
|
+
this.#eventCounters.clear();
|
|
1695
|
+
this.#cameraVisibility.clear();
|
|
1696
|
+
this.#sessionEnabled.clear();
|
|
1697
|
+
this.#replayRequests.clear();
|
|
1698
|
+
this.#replayInputs.clear();
|
|
1699
|
+
}
|
|
1700
|
+
#report(diagnostic) {
|
|
1701
|
+
const alreadyActive = this.#diagnostics.some(
|
|
1702
|
+
(prior) => prior.code === diagnostic.code && prior.detail.player === diagnostic.detail.player
|
|
1703
|
+
);
|
|
1704
|
+
if (alreadyActive) return;
|
|
1705
|
+
if (this.#diagnostics.length === 64) this.#diagnostics.shift();
|
|
1706
|
+
this.#diagnostics.push(diagnostic);
|
|
1707
|
+
}
|
|
1708
|
+
#clearDiagnostics(player, code) {
|
|
1709
|
+
for (let index = this.#diagnostics.length - 1; index >= 0; index -= 1) {
|
|
1710
|
+
const diagnostic = this.#diagnostics[index];
|
|
1711
|
+
if (diagnostic?.detail.player === player && (code === void 0 || diagnostic.code === code)) {
|
|
1712
|
+
this.#diagnostics.splice(index, 1);
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
#discardQueuedIntents(player) {
|
|
1717
|
+
let retained = 0;
|
|
1718
|
+
for (const intent of this.#intents) {
|
|
1719
|
+
if (intent.player !== player) {
|
|
1720
|
+
this.#intents[retained] = intent;
|
|
1721
|
+
retained += 1;
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
this.#intents.length = retained;
|
|
1725
|
+
}
|
|
1726
|
+
advance(world, tick, fixedDelta, players) {
|
|
1727
|
+
this.#seen.clear();
|
|
1728
|
+
for (const input of players) {
|
|
1729
|
+
this.#seen.add(input.player);
|
|
1730
|
+
if (!Number.isFinite(input.timeScale) || input.timeScale < 0) {
|
|
1731
|
+
this.#report({
|
|
1732
|
+
code: "vfx-player-invalid",
|
|
1733
|
+
expected: "a finite non-negative particle timeScale",
|
|
1734
|
+
hint: "repair ParticleEffectPlayer.timeScale and restart the player",
|
|
1735
|
+
detail: { player: input.player }
|
|
1736
|
+
});
|
|
1737
|
+
continue;
|
|
1738
|
+
}
|
|
1739
|
+
this.#clearDiagnostics(input.player, "vfx-player-invalid");
|
|
1740
|
+
const resolved = world.sharedRefs.resolve(
|
|
1741
|
+
input.effect
|
|
1742
|
+
);
|
|
1743
|
+
if (!resolved.ok || resolved.value.schemaVersion !== 2) {
|
|
1744
|
+
this.#report({
|
|
1745
|
+
code: "vfx-effect-unavailable",
|
|
1746
|
+
expected: "a loaded schemaVersion 2 GPU particle effect",
|
|
1747
|
+
hint: "load and recook the effect before the first FixedUpdate",
|
|
1748
|
+
detail: { player: input.player }
|
|
1749
|
+
});
|
|
1750
|
+
continue;
|
|
1751
|
+
}
|
|
1752
|
+
this.#clearDiagnostics(input.player, "vfx-effect-unavailable");
|
|
1753
|
+
const previous = this.#players.get(input.player);
|
|
1754
|
+
const replayRequested = this.#replayRequests.delete(input.player);
|
|
1755
|
+
const restart = previous === void 0 || previous.effect !== input.effect || previous.seed !== input.seed || replayRequested || !previous.playing && input.playing;
|
|
1756
|
+
const state = restart ? {
|
|
1757
|
+
effect: input.effect,
|
|
1758
|
+
assetGuid: resolved.value.guid,
|
|
1759
|
+
programFingerprint: resolved.value.program.fingerprint,
|
|
1760
|
+
emitters: resolved.value.program.emitters,
|
|
1761
|
+
seed: input.seed,
|
|
1762
|
+
playing: input.playing,
|
|
1763
|
+
playCycle: (previous?.playCycle ?? -1) + 1,
|
|
1764
|
+
elapsed: resolved.value.program.emitters.map(() => 0),
|
|
1765
|
+
rateRemainders: resolved.value.program.emitters.map(() => 0),
|
|
1766
|
+
nextParticleIds: resolved.value.program.emitters.map(() => 0),
|
|
1767
|
+
playCycles: resolved.value.program.emitters.map(() => (previous?.playCycle ?? -1) + 1),
|
|
1768
|
+
cameraVisible: resolved.value.program.emitters.map(() => true),
|
|
1769
|
+
phaseTicks: resolved.value.program.emitters.map(() => 0),
|
|
1770
|
+
hasCommitted: false
|
|
1771
|
+
} : previous;
|
|
1772
|
+
this.#players.set(input.player, state);
|
|
1773
|
+
state.playing = input.playing;
|
|
1774
|
+
if (!input.playing && !replayRequested) continue;
|
|
1775
|
+
const queuedForPlayer = this.#intents.reduce(
|
|
1776
|
+
(count, intent) => count + (intent.player === input.player ? 1 : 0),
|
|
1777
|
+
0
|
|
1778
|
+
);
|
|
1779
|
+
if (queuedForPlayer >= this.#maxQueuedTicks * Math.max(1, resolved.value.program.emitters.length)) {
|
|
1780
|
+
if (state.hasCommitted) {
|
|
1781
|
+
this.#report({
|
|
1782
|
+
code: "vfx-intent-queue-overflow",
|
|
1783
|
+
expected: `at most ${this.#maxQueuedTicks} unconsumed fixed ticks`,
|
|
1784
|
+
hint: "recover or restart the renderer; VFX does not silently discard simulation ticks",
|
|
1785
|
+
detail: { player: input.player, maxQueuedTicks: this.#maxQueuedTicks }
|
|
1786
|
+
});
|
|
1787
|
+
}
|
|
1788
|
+
continue;
|
|
1789
|
+
}
|
|
1790
|
+
const instance = this.#instances.get(input.player) ?? this.#createInstance(input.player, resolved.value);
|
|
1791
|
+
if (instance === void 0) continue;
|
|
1792
|
+
const hasActiveEmitter = resolved.value.program.emitters.some((emitter) => {
|
|
1793
|
+
if (!this.isEmitterSessionEnabled(input.player, emitter.id)) return false;
|
|
1794
|
+
const cameraVisible = this.#cameraVisibility.get(`${input.player}:${emitter.id}`) ?? true;
|
|
1795
|
+
return cameraVisible || emitter.simulationWhenCulled === "continue";
|
|
1796
|
+
});
|
|
1797
|
+
if (!hasActiveEmitter) {
|
|
1798
|
+
for (const [index, emitter] of resolved.value.program.emitters.entries()) {
|
|
1799
|
+
state.cameraVisible[index] = this.#cameraVisibility.get(`${input.player}:${emitter.id}`) ?? true;
|
|
1800
|
+
}
|
|
1801
|
+
continue;
|
|
1802
|
+
}
|
|
1803
|
+
const replayInput = this.#replayInputs.get(input.player);
|
|
1804
|
+
this.#replayInputs.delete(input.player);
|
|
1805
|
+
const committed = replayInput === void 0 ? instance.commit({ seed: input.seed, tick }) : instance.replay(replayInput);
|
|
1806
|
+
if (!committed.ok) {
|
|
1807
|
+
this.#report({
|
|
1808
|
+
code: "vfx-instance-commit-failed",
|
|
1809
|
+
expected: "the current typed instance values to pack into the reflected GPU block",
|
|
1810
|
+
hint: "repair the instance patch and retry at the next FixedUpdate",
|
|
1811
|
+
detail: { player: input.player }
|
|
1812
|
+
});
|
|
1813
|
+
continue;
|
|
1814
|
+
}
|
|
1815
|
+
const delta = fixedDelta * input.timeScale;
|
|
1816
|
+
const committedChannels = committed.value.channelInputs;
|
|
1817
|
+
for (const [index, emitter] of resolved.value.program.emitters.entries()) {
|
|
1818
|
+
const cameraVisible = this.#cameraVisibility.get(`${input.player}:${emitter.id}`) ?? true;
|
|
1819
|
+
const becameVisible = cameraVisible && state.cameraVisible[index] === false;
|
|
1820
|
+
state.cameraVisible[index] = cameraVisible;
|
|
1821
|
+
if (!this.isEmitterSessionEnabled(input.player, emitter.id)) continue;
|
|
1822
|
+
if (!cameraVisible && emitter.simulationWhenCulled !== "continue") continue;
|
|
1823
|
+
const visibilityRestart = becameVisible && emitter.simulationWhenCulled === "restart-on-visible";
|
|
1824
|
+
if (visibilityRestart) {
|
|
1825
|
+
state.elapsed[index] = 0;
|
|
1826
|
+
state.rateRemainders[index] = 0;
|
|
1827
|
+
state.nextParticleIds[index] = 0;
|
|
1828
|
+
state.playCycles[index] = (state.playCycles[index] ?? state.playCycle) + 1;
|
|
1829
|
+
state.phaseTicks[index] = 0;
|
|
1830
|
+
}
|
|
1831
|
+
const previousElapsed = state.elapsed[index] ?? 0;
|
|
1832
|
+
const scheduled = spawnCount(
|
|
1833
|
+
emitter,
|
|
1834
|
+
previousElapsed,
|
|
1835
|
+
previousElapsed + delta,
|
|
1836
|
+
restart || visibilityRestart,
|
|
1837
|
+
state.rateRemainders[index] ?? 0
|
|
1838
|
+
);
|
|
1839
|
+
state.rateRemainders[index] = scheduled.remainder;
|
|
1840
|
+
const firstParticleId = state.nextParticleIds[index] ?? 0;
|
|
1841
|
+
state.nextParticleIds[index] = firstParticleId + scheduled.count;
|
|
1842
|
+
const consumesEvent = resolved.value.program.emitters.some(
|
|
1843
|
+
(source) => (source.events ?? []).some((event) => event.subEmitter === emitter.id)
|
|
1844
|
+
);
|
|
1845
|
+
const eventSources = resolved.value.program.emitters.flatMap(
|
|
1846
|
+
(source) => (source.events ?? []).filter((event) => event.subEmitter === emitter.id)
|
|
1847
|
+
);
|
|
1848
|
+
const channelInputs = (emitter.events?.length ?? 0) > 0 || consumesEvent ? committedChannels : [];
|
|
1849
|
+
const phaseTick = state.phaseTicks[index] ?? 0;
|
|
1850
|
+
this.#intents.push(
|
|
1851
|
+
Object.freeze({
|
|
1852
|
+
sequence: this.#sequence++,
|
|
1853
|
+
player: input.player,
|
|
1854
|
+
emitter,
|
|
1855
|
+
programFingerprint: resolved.value.program.fingerprint,
|
|
1856
|
+
reset: restart || visibilityRestart,
|
|
1857
|
+
fixedDelta: delta,
|
|
1858
|
+
phaseTick,
|
|
1859
|
+
tick,
|
|
1860
|
+
seed: input.seed,
|
|
1861
|
+
playCycle: state.playCycles[index] ?? state.playCycle,
|
|
1862
|
+
spawnCount: scheduled.count,
|
|
1863
|
+
firstParticleId,
|
|
1864
|
+
instanceGeneration: committed.value.generation,
|
|
1865
|
+
instancePatchCount: committed.value.patchCount,
|
|
1866
|
+
parameterBlock: committed.value.parameterBlock,
|
|
1867
|
+
canonicalPayload: committed.value.canonicalPayload,
|
|
1868
|
+
replayInput: committed.value.replayInput,
|
|
1869
|
+
channelInputs,
|
|
1870
|
+
eventCounters: eventCounters(
|
|
1871
|
+
{ channelInputs, emitter },
|
|
1872
|
+
channelInputs.length === 0 ? 0 : committed.value.droppedCount,
|
|
1873
|
+
eventSources
|
|
1874
|
+
)
|
|
1875
|
+
})
|
|
1876
|
+
);
|
|
1877
|
+
state.elapsed[index] = previousElapsed + delta;
|
|
1878
|
+
state.phaseTicks[index] = phaseTick + 1;
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
for (const player of this.#players.keys()) {
|
|
1882
|
+
if (!this.#seen.has(player)) this.reset(player);
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
#createInstance(player, effect) {
|
|
1886
|
+
const layout = effect.program.emitters.find(
|
|
1887
|
+
(emitter) => emitter.reflection.layout !== void 0
|
|
1888
|
+
)?.reflection.layout;
|
|
1889
|
+
const reflection = layout ?? {
|
|
1890
|
+
version: 1,
|
|
1891
|
+
parameters: { name: "VfxParameters", fields: [], size: 0, alignment: 1 },
|
|
1892
|
+
custom: { name: "VfxCustom", fields: [], size: 0, alignment: 1 },
|
|
1893
|
+
fingerprint: effect.program.fingerprint.startsWith("sha256:") ? effect.program.fingerprint : `sha256:${effect.program.fingerprint}`
|
|
1894
|
+
};
|
|
1895
|
+
try {
|
|
1896
|
+
const instance = new ParticleEffectInstance(createVfxEffectContract(reflection), {
|
|
1897
|
+
channels: effect.program.emitters.flatMap((emitter) => emitter.channels ?? [])
|
|
1898
|
+
});
|
|
1899
|
+
this.#instances.set(player, instance);
|
|
1900
|
+
return instance;
|
|
1901
|
+
} catch {
|
|
1902
|
+
this.#report({
|
|
1903
|
+
code: "vfx-instance-commit-failed",
|
|
1904
|
+
expected: "a valid reflected VFX instance contract",
|
|
1905
|
+
hint: "recook the effect with a valid reflection layout before starting the player",
|
|
1906
|
+
detail: { player }
|
|
1907
|
+
});
|
|
1908
|
+
return void 0;
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
#latestIntent(player, emitterId) {
|
|
1912
|
+
let latest = this.#lastCommittedByEmitter.get(player)?.get(emitterId);
|
|
1913
|
+
for (let index = this.#intents.length - 1; index >= 0; index -= 1) {
|
|
1914
|
+
const intent = this.#intents[index];
|
|
1915
|
+
if (intent?.player !== player || intent.emitter.id !== emitterId) continue;
|
|
1916
|
+
if (latest === void 0 || intent.sequence > latest.sequence) latest = intent;
|
|
1917
|
+
break;
|
|
1918
|
+
}
|
|
1919
|
+
return latest;
|
|
1920
|
+
}
|
|
1921
|
+
};
|
|
1922
|
+
function spawnCount(emitter, previous, next, firstTick, priorRemainder) {
|
|
1923
|
+
const exactRate = emitter.schedule.rate * Math.max(0, next - previous) + priorRemainder;
|
|
1924
|
+
let count = Math.floor(exactRate);
|
|
1925
|
+
const loop = emitter.schedule.loopDuration;
|
|
1926
|
+
for (const burst of emitter.schedule.bursts ?? []) {
|
|
1927
|
+
if (loop === void 0) {
|
|
1928
|
+
if (firstTick && burst.time === 0 || burst.time > previous && burst.time <= next) {
|
|
1929
|
+
count += burst.count;
|
|
1930
|
+
}
|
|
1931
|
+
continue;
|
|
1932
|
+
}
|
|
1933
|
+
const firstOccurrence = Math.max(0, Math.floor((previous - burst.time) / loop) + 1);
|
|
1934
|
+
const lastOccurrence = Math.floor((next - burst.time) / loop);
|
|
1935
|
+
const occurrences = Math.max(0, lastOccurrence - firstOccurrence + 1);
|
|
1936
|
+
count += occurrences * burst.count;
|
|
1937
|
+
if (firstTick && burst.time === 0) count += burst.count;
|
|
1938
|
+
}
|
|
1939
|
+
return { count, remainder: exactRate - Math.floor(exactRate) };
|
|
1940
|
+
}
|
|
1941
|
+
function vfxGpuRuntimePlugin(options = {}) {
|
|
1942
|
+
const rows = [];
|
|
1943
|
+
return {
|
|
1944
|
+
name: "vfx-gpu-runtime",
|
|
1945
|
+
inject: ["world"],
|
|
1946
|
+
apply(ctx) {
|
|
1947
|
+
const world = ctx.world;
|
|
1948
|
+
if (world.hasResource(VFX_GPU_RUNTIME_RESOURCE_KEY)) {
|
|
1949
|
+
throw new TypeError(`${VFX_GPU_RUNTIME_RESOURCE_KEY} already exists`);
|
|
1950
|
+
}
|
|
1951
|
+
const runtime = new VfxGpuRuntime(options);
|
|
1952
|
+
ctx.effect(() => {
|
|
1953
|
+
world.insertResource(VFX_GPU_RUNTIME_RESOURCE_KEY, runtime);
|
|
1954
|
+
return () => {
|
|
1955
|
+
world.removeResource(VFX_GPU_RUNTIME_RESOURCE_KEY);
|
|
1956
|
+
};
|
|
1957
|
+
}, "vfx/runtime-resource");
|
|
1958
|
+
ctx.effect(() => {
|
|
1959
|
+
world.addSystem(FixedUpdate, {
|
|
1960
|
+
name: "vfx-gpu-runtime",
|
|
1961
|
+
queries: [{ with: [Entity, ParticleEffectPlayer] }],
|
|
1962
|
+
fn: (world2, queryResults) => {
|
|
1963
|
+
rows.length = 0;
|
|
1964
|
+
for (const row of queryResults[0]) {
|
|
1965
|
+
const player = row.get(ParticleEffectPlayer);
|
|
1966
|
+
rows.push({
|
|
1967
|
+
player: row.entity,
|
|
1968
|
+
effect: toShared(player.effect),
|
|
1969
|
+
playing: player.playing,
|
|
1970
|
+
seed: player.seed,
|
|
1971
|
+
timeScale: player.timeScale
|
|
1972
|
+
});
|
|
1973
|
+
}
|
|
1974
|
+
const fixed = world2.getResource(FixedTime);
|
|
1975
|
+
runtime.advance(world2, fixed.tick, fixed.delta, rows);
|
|
1976
|
+
}
|
|
1977
|
+
}).unwrap();
|
|
1978
|
+
return () => world.removeSystem(FixedUpdate, "vfx-gpu-runtime");
|
|
1979
|
+
}, "vfx/tick-system");
|
|
1980
|
+
}
|
|
1981
|
+
};
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
export { PARTICLE_CODE_DEFAULT_MODULE_ID, PARTICLE_STAGE_RESOURCE_NAMES, ParticleEffectInstance, ParticleEffectPlayer, VFX_GPU_PROGRAM_ARTIFACT_KEY, VFX_GPU_PROGRAM_FORMAT, VFX_GPU_RUNTIME_RESOURCE_KEY, VfxGpuRuntime, createParticleEffectInstance, createVfxEffectContract, createVfxInspectSnapshot, defineParticleEffectSourceV2, describeVfxGpuEffect, isVfxGpuEffectAsset, loadVfxGpuEffect, parseParticleEffectSourceV2, parseVfxStageDeclarations, particleEffectContribution, resolveVfxDataInterfaces, validateVfxEffectValues, vfxGpuEffectContribution, vfxGpuEffectPackLoader, vfxGpuRuntimePlugin };
|
|
1985
|
+
//# sourceMappingURL=index.mjs.map
|
|
1986
|
+
//# sourceMappingURL=index.mjs.map
|