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