@forgeax/engine-vfx-render 0.1.28 → 0.1.29
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/README.md +55 -10
- package/dist/__tests__/persistent-ticks.integration.test.d.ts +2 -0
- package/dist/__tests__/persistent-ticks.integration.test.d.ts.map +1 -0
- package/dist/feature/event-resources.d.ts +10 -3
- package/dist/feature/event-resources.d.ts.map +1 -1
- package/dist/feature/gpu-particle-feature.d.ts +18 -5
- package/dist/feature/gpu-particle-feature.d.ts.map +1 -1
- package/dist/feature/particle-resources.d.ts +43 -6
- package/dist/feature/particle-resources.d.ts.map +1 -1
- package/dist/host/data-interface-providers.d.ts +4 -1
- package/dist/host/data-interface-providers.d.ts.map +1 -1
- package/dist/host/vfx-runtime-host.d.ts +21 -2
- package/dist/host/vfx-runtime-host.d.ts.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +1027 -307
- package/dist/index.mjs.map +1 -1
- package/package.json +11 -9
- package/src/__tests__/billboard-advanced.integration.test.ts +1 -1
- package/src/__tests__/data-interface-providers.unit.test.ts +4 -0
- package/src/__tests__/data-interface-public-api.test-d.ts +1 -0
- package/src/__tests__/gpu-host.integration.test.ts +1471 -59
- package/src/__tests__/particle-resources.unit.test.ts +75 -0
- package/src/__tests__/persistent-ticks.integration.test.ts +162 -0
- package/src/__tests__/render-vocabulary-owner.test-d.ts +7 -7
- package/src/feature/event-resources.ts +18 -4
- package/src/feature/gpu-particle-feature.ts +1153 -376
- package/src/feature/particle-resources.ts +223 -11
- package/src/host/data-interface-providers.ts +19 -0
- package/src/host/vfx-runtime-host.ts +82 -2
- package/src/index.ts +8 -0
- package/src/shaders/beam-inputs.wgsl +40 -0
- package/src/shaders/beam.wgsl +4 -3
- package/src/shaders/billboard-inputs.wgsl +85 -0
- package/src/shaders/mesh-inputs.wgsl +95 -0
- package/src/shaders/mesh-shadow.wgsl +18 -0
- package/src/shaders/mesh.wgsl +64 -40
- package/src/shaders/ribbon-inputs.wgsl +39 -0
- package/src/shaders/trail-inputs.wgsl +45 -0
- package/src/shaders/trail.wgsl +9 -3
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { frustum } from '@forgeax/engine-math';
|
|
2
2
|
import { GlobalTransform } from '@forgeax/engine-scene';
|
|
3
3
|
import { err, ok } from '@forgeax/engine-types';
|
|
4
|
-
import {
|
|
4
|
+
import { vfxGpuEffectContribution, vfxGpuEffectPackLoader, vfxGpuRuntimePlugin, VFX_GPU_RUNTIME_RESOURCE_KEY, resolveVfxDataInterfaces, VFX_PARTICLE_CORE_STRIDE, ParticleEffectPlayer } from '@forgeax/engine-vfx';
|
|
5
5
|
import { getAssetRegistryResolver } from '@forgeax/engine-assets-runtime';
|
|
6
6
|
import { createWorldContext } from '@forgeax/engine-ecs';
|
|
7
7
|
|
|
@@ -55,6 +55,13 @@ function encodeEventInputs(intent) {
|
|
|
55
55
|
}
|
|
56
56
|
return bytes;
|
|
57
57
|
}
|
|
58
|
+
function encodeEventBuffer(intent) {
|
|
59
|
+
const inputBytes = encodeEventInputs(intent);
|
|
60
|
+
const outputBytes = eventCapacity(intent.emitter) * VFX_EVENT_BYTES;
|
|
61
|
+
const data = new Uint8Array(inputBytes.byteLength + outputBytes);
|
|
62
|
+
data.set(inputBytes);
|
|
63
|
+
return data;
|
|
64
|
+
}
|
|
58
65
|
function eventCounterData() {
|
|
59
66
|
return new Uint8Array(VFX_EVENT_COUNTER_BYTES);
|
|
60
67
|
}
|
|
@@ -83,8 +90,11 @@ var RenderFeatureStageFailedError = class extends Error {
|
|
|
83
90
|
var RENDER_FEATURE_VERTEX_LAYOUTS = Object.freeze({
|
|
84
91
|
positionSizeColorInstance: "position-size-color-instance",
|
|
85
92
|
billboardMaterialInstance: "billboard-material-instance",
|
|
93
|
+
billboardMaterialInputInstance: "billboard-material-input-instance",
|
|
86
94
|
topologySegmentInstance: "topology-segment-instance",
|
|
87
|
-
|
|
95
|
+
topologySegmentMaterialInputInstance: "topology-segment-material-input-instance",
|
|
96
|
+
meshGeometryMaterialInstance: "mesh-geometry-material-instance",
|
|
97
|
+
meshGeometryMaterialInputInstance: "mesh-geometry-material-input-instance"
|
|
88
98
|
});
|
|
89
99
|
var PARTICLE_SHADER_IDENTIFIERS = Object.freeze({
|
|
90
100
|
billboard: "forgeax::vfx-render.particles.billboard",
|
|
@@ -93,6 +103,13 @@ var PARTICLE_SHADER_IDENTIFIERS = Object.freeze({
|
|
|
93
103
|
trail: "forgeax::vfx-render.particles.trail",
|
|
94
104
|
beam: "forgeax::vfx-render.particles.beam"
|
|
95
105
|
});
|
|
106
|
+
var PARTICLE_INPUT_SHADER_IDENTIFIERS = Object.freeze({
|
|
107
|
+
billboard: "forgeax::vfx-render.particles.billboard-inputs",
|
|
108
|
+
mesh: "forgeax::vfx-render.particles.mesh-inputs",
|
|
109
|
+
ribbon: "forgeax::vfx-render.particles.ribbon-inputs",
|
|
110
|
+
trail: "forgeax::vfx-render.particles.trail-inputs",
|
|
111
|
+
beam: "forgeax::vfx-render.particles.beam-inputs"
|
|
112
|
+
});
|
|
96
113
|
function createTopologyResourcePlan(renderer) {
|
|
97
114
|
if (renderer === null || typeof renderer !== "object" || Array.isArray(renderer))
|
|
98
115
|
return err({
|
|
@@ -172,13 +189,121 @@ var PARTICLE_ADDITIVE_BLEND = {
|
|
|
172
189
|
color: { srcFactor: "one", dstFactor: "one", operation: "add" },
|
|
173
190
|
alpha: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" }
|
|
174
191
|
};
|
|
175
|
-
function particleMaterialPass(kind, material) {
|
|
192
|
+
function particleMaterialPass(kind, material, hasParticleInputs = false) {
|
|
176
193
|
const pass = material?.passes?.find((candidate) => candidate.name === `particle-${kind}`);
|
|
194
|
+
const renderState = pass?.renderState ?? (kind === "mesh" ? material?.passes?.find((candidate) => {
|
|
195
|
+
const tags = candidate.renderState?.tags;
|
|
196
|
+
return candidate.name === "forward" || typeof tags === "object" && tags !== null && "LightMode" in tags && tags.LightMode === "Forward";
|
|
197
|
+
})?.renderState : void 0);
|
|
177
198
|
return {
|
|
178
|
-
shader: pass?.program.module ?? PARTICLE_SHADER_IDENTIFIERS[kind],
|
|
179
|
-
...
|
|
199
|
+
shader: pass?.program.module ?? (hasParticleInputs ? PARTICLE_INPUT_SHADER_IDENTIFIERS[kind] : PARTICLE_SHADER_IDENTIFIERS[kind]),
|
|
200
|
+
...renderState === void 0 ? {} : { renderState }
|
|
180
201
|
};
|
|
181
202
|
}
|
|
203
|
+
var EMPTY_PARTICLE_MATERIAL_INPUTS = Object.freeze({
|
|
204
|
+
definitions: Object.freeze([]),
|
|
205
|
+
lanes: 0,
|
|
206
|
+
stride: 0
|
|
207
|
+
});
|
|
208
|
+
function particleInputFailure(code, expected, hint, detail) {
|
|
209
|
+
return err({ code, expected, hint, detail });
|
|
210
|
+
}
|
|
211
|
+
function isParticleInputType(value) {
|
|
212
|
+
return value === "f32" || value === "vec2<f32>" || value === "vec3<f32>" || value === "vec4<f32>";
|
|
213
|
+
}
|
|
214
|
+
function isParticleInputVisibility(value) {
|
|
215
|
+
return value === "vertex" || value === "fragment" || value === "vertex-fragment";
|
|
216
|
+
}
|
|
217
|
+
function validParticleInput(value) {
|
|
218
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
219
|
+
const input = value;
|
|
220
|
+
return typeof input.name === "string" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(input.name) && isParticleInputType(input.type) && isParticleInputVisibility(input.visibility) && typeof input.lane === "number" && Number.isInteger(input.lane) && input.lane >= 0 && input.lane < 4;
|
|
221
|
+
}
|
|
222
|
+
function sameParticleInput(left, right) {
|
|
223
|
+
return left.name === right.name && left.type === right.type && left.visibility === right.visibility && left.lane === right.lane;
|
|
224
|
+
}
|
|
225
|
+
function prepareParticleMaterialInputs(renderer, material, reflected) {
|
|
226
|
+
const requested = renderer.materialInputs ?? [];
|
|
227
|
+
if (requested.length === 0) return ok(EMPTY_PARTICLE_MATERIAL_INPUTS);
|
|
228
|
+
if (new Set(requested).size !== requested.length) {
|
|
229
|
+
return particleInputFailure(
|
|
230
|
+
"vfx-material-input-duplicate",
|
|
231
|
+
"unique particle input names per renderer",
|
|
232
|
+
"remove the duplicate renderer material input and recook the effect",
|
|
233
|
+
{ material: renderer.material, path: "renderer.materialInputs" }
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
const definitions = material?.particleInputs;
|
|
237
|
+
if (definitions === void 0) {
|
|
238
|
+
return particleInputFailure(
|
|
239
|
+
"vfx-material-input-missing",
|
|
240
|
+
`material ${renderer.material} to declare particleInputs`,
|
|
241
|
+
"add the requested typed input to MaterialAsset and recook the material before the VFX effect",
|
|
242
|
+
{ material: renderer.material, path: "material.particleInputs" }
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
const names = /* @__PURE__ */ new Set();
|
|
246
|
+
const lanes = /* @__PURE__ */ new Set();
|
|
247
|
+
for (const [index, candidate] of definitions.entries()) {
|
|
248
|
+
if (!validParticleInput(candidate)) {
|
|
249
|
+
return particleInputFailure(
|
|
250
|
+
"vfx-material-input-wrong-type",
|
|
251
|
+
"particleInputs entries with a supported type, visibility, and lane",
|
|
252
|
+
"repair the material particleInputs declaration and recook it",
|
|
253
|
+
{ material: renderer.material, path: `material.particleInputs[${index}]` }
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
if (names.has(candidate.name) || lanes.has(candidate.lane)) {
|
|
257
|
+
return particleInputFailure(
|
|
258
|
+
"vfx-material-input-duplicate",
|
|
259
|
+
"unique particle input names and lanes",
|
|
260
|
+
"assign one lane to one input name and recook the material",
|
|
261
|
+
{ material: renderer.material, name: candidate.name, lane: candidate.lane }
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
names.add(candidate.name);
|
|
265
|
+
lanes.add(candidate.lane);
|
|
266
|
+
}
|
|
267
|
+
const selectedDefinitions = [];
|
|
268
|
+
for (const name of requested) {
|
|
269
|
+
const input = definitions.find((candidate) => candidate.name === name);
|
|
270
|
+
if (input === void 0) {
|
|
271
|
+
return particleInputFailure(
|
|
272
|
+
"vfx-material-input-missing",
|
|
273
|
+
`material ${renderer.material} to declare particle input ${name}`,
|
|
274
|
+
"add the requested input to MaterialAsset.particleInputs and recook both assets",
|
|
275
|
+
{ material: renderer.material, name, path: "renderer.materialInputs" }
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
selectedDefinitions.push(input);
|
|
279
|
+
}
|
|
280
|
+
if (reflected === void 0) {
|
|
281
|
+
return particleInputFailure(
|
|
282
|
+
"vfx-material-input-stale",
|
|
283
|
+
"cooked renderer reflection to carry the material input declarations",
|
|
284
|
+
"recook the VFX effect with the current material artifact catalog",
|
|
285
|
+
{ material: renderer.material, path: "effect.reflection.renderers.materialInputDefinitions" }
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
for (const input of selectedDefinitions) {
|
|
289
|
+
const cooked = reflected.find((candidate) => candidate.name === input.name);
|
|
290
|
+
if (cooked === void 0 || !sameParticleInput(input, cooked)) {
|
|
291
|
+
return particleInputFailure(
|
|
292
|
+
"vfx-material-input-stale",
|
|
293
|
+
`the cooked declaration for material input ${input.name} to match MaterialAsset`,
|
|
294
|
+
"recook the VFX effect and material together so names, types, visibility, and lanes agree",
|
|
295
|
+
{ material: renderer.material, name: input.name, lane: input.lane }
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const lanesUsed = selectedDefinitions.map((input) => input.lane);
|
|
300
|
+
const lanesCount = Math.max(...lanesUsed, -1) + 1;
|
|
301
|
+
return ok({
|
|
302
|
+
definitions: Object.freeze([...selectedDefinitions]),
|
|
303
|
+
lanes: lanesCount,
|
|
304
|
+
stride: lanesCount * 16
|
|
305
|
+
});
|
|
306
|
+
}
|
|
182
307
|
function particleRendererRenderState(kind, blend, authored) {
|
|
183
308
|
if (authored !== void 0) return authored;
|
|
184
309
|
const isTopology = kind === "ribbon" || kind === "trail" || kind === "beam";
|
|
@@ -202,8 +327,9 @@ function particleMaterialUsesBindings(material) {
|
|
|
202
327
|
return (material?.parameters?.length ?? 0) > 0;
|
|
203
328
|
}
|
|
204
329
|
function particleMaterialSceneDepthBinding(contract) {
|
|
205
|
-
if (contract === "group-0-resource") return 0;
|
|
206
|
-
if (contract === "view-and-scene-depth"
|
|
330
|
+
if (contract === "group-0-resource" || contract === "render-material-with-scene-depth") return 0;
|
|
331
|
+
if (contract === "view-and-scene-depth" || contract === "render-material-and-scene-depth")
|
|
332
|
+
return 1;
|
|
207
333
|
return void 0;
|
|
208
334
|
}
|
|
209
335
|
function floatAttribute(value) {
|
|
@@ -425,11 +551,10 @@ function stageRecoveryReadiness(plan, generation, recovery) {
|
|
|
425
551
|
// src/feature/gpu-particle-feature.ts
|
|
426
552
|
var IDENTITY = "forgeax.vfx-render.gpu-particles";
|
|
427
553
|
var WORKGROUP_SIZE = 256;
|
|
428
|
-
var PARTICLE_BYTES = 80;
|
|
429
554
|
var BILLBOARD_INSTANCE_BYTES = 31 * 4;
|
|
430
|
-
var MESH_INSTANCE_BYTES =
|
|
555
|
+
var MESH_INSTANCE_BYTES = 18 * 4;
|
|
431
556
|
var COUNTERS_BYTES = 24;
|
|
432
|
-
var RUNTIME_BYTES =
|
|
557
|
+
var RUNTIME_BYTES = 76 * 4;
|
|
433
558
|
var IDENTITY_MATRIX = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
|
|
434
559
|
function createVfxRenderInspectSnapshot(input) {
|
|
435
560
|
return {
|
|
@@ -467,7 +592,7 @@ function resolveBillboardAdvancedState(renderer, sample) {
|
|
|
467
592
|
frameIndex,
|
|
468
593
|
pivot: renderer.pivot ?? [0, 0],
|
|
469
594
|
softParticleFade,
|
|
470
|
-
sortingKey: renderer.sorting === "
|
|
595
|
+
sortingKey: renderer.sorting === "view-depth" || renderer.sorting === "view-distance" ? sample.particleDepth : 0
|
|
471
596
|
});
|
|
472
597
|
}
|
|
473
598
|
function topologyRecoveryHint(topology, reason) {
|
|
@@ -482,10 +607,25 @@ function topologyRecoveryHint(topology, reason) {
|
|
|
482
607
|
function finite(value, fallback) {
|
|
483
608
|
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
484
609
|
}
|
|
610
|
+
function billboardSortingMode(renderer) {
|
|
611
|
+
if (renderer?.kind !== "billboard") return 0;
|
|
612
|
+
switch (renderer.sorting) {
|
|
613
|
+
case "view-depth":
|
|
614
|
+
return 2;
|
|
615
|
+
case "view-distance":
|
|
616
|
+
return 5;
|
|
617
|
+
case "custom-ascending":
|
|
618
|
+
return 3;
|
|
619
|
+
case "custom-descending":
|
|
620
|
+
return 4;
|
|
621
|
+
default:
|
|
622
|
+
return 0;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
485
625
|
function vector(value, fallback, size) {
|
|
486
626
|
return Array.isArray(value) ? Array.from({ length: size }, (_, index) => finite(value[index], fallback[index] ?? 0)) : fallback;
|
|
487
627
|
}
|
|
488
|
-
function runtimeData(intent, camera, material, localToWorld, renderer, rendererIndex = 0) {
|
|
628
|
+
function runtimeData(intent, camera, material, localToWorld, renderer, rendererIndex = 0, particleInputLanes = renderer?.materialInputs?.length ?? 0, meshDraw) {
|
|
489
629
|
const storage = new ArrayBuffer(RUNTIME_BYTES);
|
|
490
630
|
const floats = new Float32Array(storage);
|
|
491
631
|
const words = new Uint32Array(storage);
|
|
@@ -500,6 +640,7 @@ function runtimeData(intent, camera, material, localToWorld, renderer, rendererI
|
|
|
500
640
|
floats.set(camera.viewProjection, 8);
|
|
501
641
|
floats.set(camera.right, 24);
|
|
502
642
|
floats.set(camera.up, 28);
|
|
643
|
+
floats.set(camera.position, 72);
|
|
503
644
|
const values = material?.values ?? {};
|
|
504
645
|
floats.set(vector(values.baseColor, [1, 1, 1, 1], 4), 32);
|
|
505
646
|
const emissive = vector(values.emissive, [0, 0, 0], 3);
|
|
@@ -511,12 +652,13 @@ function runtimeData(intent, camera, material, localToWorld, renderer, rendererI
|
|
|
511
652
|
floats[43] = finite(values.clearcoatRoughness, 0.5);
|
|
512
653
|
floats.set(localToWorld, 44);
|
|
513
654
|
words[60] = rendererIndex;
|
|
514
|
-
words[61] = renderer?.kind === "trail" ? renderer.historyLength : 0;
|
|
655
|
+
words[61] = renderer === void 0 && ((intent.emitter.reflection.resources ?? []).includes("eventBuffer") || (intent.emitter.reflection.entryPoints ?? []).includes("forgeax_vfx_event_main")) ? eventInputCapacity(intent.emitter) : renderer?.kind === "trail" ? renderer.historyLength : meshDraw?.count ?? 0;
|
|
515
656
|
words[62] = renderer?.kind === "ribbon" || renderer?.kind === "trail" || renderer?.kind === "beam" ? renderer.capacity : intent.emitter.capacity;
|
|
516
|
-
words[63] =
|
|
657
|
+
words[63] = meshDraw?.firstIndex ?? billboardSortingMode(renderer);
|
|
517
658
|
floats[64] = renderer?.kind === "billboard" ? renderer.pivot?.[0] ?? 0 : renderer?.kind === "ribbon" || renderer?.kind === "trail" || renderer?.kind === "beam" ? renderer.width ?? 0.1 : 0.1;
|
|
518
659
|
floats[65] = renderer?.kind === "billboard" ? renderer.pivot?.[1] ?? 0 : 0;
|
|
519
660
|
floats[66] = renderer?.kind === "billboard" ? renderer.softParticle?.fadeDistance ?? 0 : 0;
|
|
661
|
+
floats[67] = particleInputLanes;
|
|
520
662
|
const sheet = renderer?.kind === "billboard" ? renderer.textureSheet : void 0;
|
|
521
663
|
floats[68] = sheet?.columns ?? 1;
|
|
522
664
|
floats[69] = sheet?.rows ?? 1;
|
|
@@ -529,8 +671,8 @@ function emitterTransform(world, intent) {
|
|
|
529
671
|
const transform = world.get(intent.player, GlobalTransform);
|
|
530
672
|
return transform.ok ? transform.value.world : IDENTITY_MATRIX;
|
|
531
673
|
}
|
|
532
|
-
function emitterVisible(
|
|
533
|
-
const bounds =
|
|
674
|
+
function emitterVisible(emitter, camera, localToWorld) {
|
|
675
|
+
const bounds = emitter.bounds;
|
|
534
676
|
const center = bounds.kind === "sphere" ? bounds.center : [
|
|
535
677
|
(bounds.min[0] + bounds.max[0]) * 0.5,
|
|
536
678
|
(bounds.min[1] + bounds.max[1]) * 0.5,
|
|
@@ -563,6 +705,36 @@ function requiresSceneDepth(intent) {
|
|
|
563
705
|
(requirement) => requirement.kind === "scene-depth"
|
|
564
706
|
);
|
|
565
707
|
}
|
|
708
|
+
function dataInterfacesExecutable(intent, registry) {
|
|
709
|
+
const requirements = intent.emitter.reflection.dataInterfaces ?? [];
|
|
710
|
+
if (requirements.length === 0) return true;
|
|
711
|
+
const resolved = registry?.resolve(requirements, intent.instanceGeneration);
|
|
712
|
+
if (resolved === void 0) return true;
|
|
713
|
+
if (!resolved.ok) {
|
|
714
|
+
const error = resolved.error;
|
|
715
|
+
if (error === void 0) return false;
|
|
716
|
+
return error.code === "vfx-data-interface-missing" && (error.detail.providerId === void 0 || error.expected.includes("resident"));
|
|
717
|
+
}
|
|
718
|
+
if (resolved.value === void 0) return true;
|
|
719
|
+
return resolved.value.resources.every((resource) => resource.resource !== void 0);
|
|
720
|
+
}
|
|
721
|
+
function preparedDataInterfaceResources(requirements, generation, prepared) {
|
|
722
|
+
if (prepared === void 0) return void 0;
|
|
723
|
+
const resources = [];
|
|
724
|
+
for (const requirement of requirements) {
|
|
725
|
+
const value = prepared[requirement.kind];
|
|
726
|
+
if (value === void 0) return void 0;
|
|
727
|
+
resources.push({
|
|
728
|
+
token: requirement.token,
|
|
729
|
+
kind: requirement.kind,
|
|
730
|
+
bindingType: requirement.bindingType,
|
|
731
|
+
generation,
|
|
732
|
+
...requirement.sampleCount === void 0 ? {} : { sampleCount: requirement.sampleCount },
|
|
733
|
+
resource: value
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
return resources;
|
|
737
|
+
}
|
|
566
738
|
function planFailure() {
|
|
567
739
|
return new RenderFeatureStageFailedError(IDENTITY, -1, "plan", "next-frame");
|
|
568
740
|
}
|
|
@@ -572,7 +744,9 @@ function planName(value, maxLength = 24) {
|
|
|
572
744
|
}
|
|
573
745
|
function computeBindingEntries(intent, resources) {
|
|
574
746
|
const declared = new Set(
|
|
575
|
-
(intent.emitter.reflection.bindings[0]?.entries ?? []).filter(
|
|
747
|
+
(intent.emitter.reflection.bindings[0]?.entries ?? []).filter(
|
|
748
|
+
(entry) => entry.buffer !== void 0 || entry.texture !== void 0 || entry.sampler !== void 0 || entry.storageTexture !== void 0
|
|
749
|
+
).map((entry) => entry.binding)
|
|
576
750
|
);
|
|
577
751
|
return Object.entries(resources).flatMap(
|
|
578
752
|
([binding, resource]) => declared.has(Number(binding)) ? [{ binding: Number(binding), resource }] : []
|
|
@@ -580,6 +754,12 @@ function computeBindingEntries(intent, resources) {
|
|
|
580
754
|
}
|
|
581
755
|
function simulationDispatches(intent, stages) {
|
|
582
756
|
const groups = Math.max(1, Math.ceil(intent.emitter.capacity / WORKGROUP_SIZE));
|
|
757
|
+
const compact = [
|
|
758
|
+
{ kind: "direct", entryPoint: "forgeax_vfx_scan_blocks_main", workgroups: [groups] },
|
|
759
|
+
{ kind: "direct", entryPoint: "forgeax_vfx_scan_block_offsets_main", workgroups: [1] },
|
|
760
|
+
{ kind: "direct", entryPoint: "forgeax_vfx_add_offsets_main", workgroups: [groups] },
|
|
761
|
+
{ kind: "direct", entryPoint: "forgeax_vfx_compact_main", workgroups: [groups] }
|
|
762
|
+
];
|
|
583
763
|
return [
|
|
584
764
|
{ kind: "direct", entryPoint: "forgeax_vfx_spawn_main", workgroups: [groups] },
|
|
585
765
|
{ kind: "direct", entryPoint: "forgeax_vfx_update_main", workgroups: [groups] },
|
|
@@ -588,19 +768,40 @@ function simulationDispatches(intent, stages) {
|
|
|
588
768
|
entryPoint: stage.entryPoint,
|
|
589
769
|
workgroups: [groups]
|
|
590
770
|
})),
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
771
|
+
...compact,
|
|
772
|
+
...intent.emitter.reflection.entryPoints.includes("forgeax_vfx_event_main") ? [
|
|
773
|
+
{
|
|
774
|
+
kind: "direct",
|
|
775
|
+
entryPoint: "forgeax_vfx_event_main",
|
|
776
|
+
workgroups: [1]
|
|
777
|
+
},
|
|
778
|
+
...compact
|
|
779
|
+
] : []
|
|
600
780
|
];
|
|
601
781
|
}
|
|
602
782
|
function gpuParticleRenderFeature(options) {
|
|
603
|
-
|
|
783
|
+
const pendingFrames = /* @__PURE__ */ new Map();
|
|
784
|
+
const worldIds = /* @__PURE__ */ new WeakMap();
|
|
785
|
+
const runtimeIds = /* @__PURE__ */ new WeakMap();
|
|
786
|
+
const resetEpochByEmitter = /* @__PURE__ */ new WeakMap();
|
|
787
|
+
const resetEpochByIntent = /* @__PURE__ */ new WeakMap();
|
|
788
|
+
let nextWorldId = 0;
|
|
789
|
+
let nextRuntimeId = 0;
|
|
790
|
+
const worldId = (world) => {
|
|
791
|
+
const prior = worldIds.get(world);
|
|
792
|
+
if (prior !== void 0) return prior;
|
|
793
|
+
const assigned = nextWorldId++;
|
|
794
|
+
worldIds.set(world, assigned);
|
|
795
|
+
return assigned;
|
|
796
|
+
};
|
|
797
|
+
const runtimeId = (runtime) => {
|
|
798
|
+
const prior = runtimeIds.get(runtime);
|
|
799
|
+
if (prior !== void 0) return prior;
|
|
800
|
+
const assigned = nextRuntimeId++;
|
|
801
|
+
runtimeIds.set(runtime, assigned);
|
|
802
|
+
return assigned;
|
|
803
|
+
};
|
|
804
|
+
const feature = {
|
|
604
805
|
identity: IDENTITY,
|
|
605
806
|
requiredCapabilities: ["compute", "indirectDrawing"],
|
|
606
807
|
// Generated emitter programs are first-use assets. They must hand a
|
|
@@ -608,372 +809,806 @@ function gpuParticleRenderFeature(options) {
|
|
|
608
809
|
// diagnostic-only getCompilationInfo() round trip; pipeline creation still
|
|
609
810
|
// validates the module before the pass is submitted.
|
|
610
811
|
shaderModuleMode: "immediate",
|
|
611
|
-
requiredMaterialShaders: Object.
|
|
812
|
+
requiredMaterialShaders: Object.freeze([
|
|
813
|
+
...Object.values(PARTICLE_SHADER_IDENTIFIERS),
|
|
814
|
+
...Object.values(PARTICLE_INPUT_SHADER_IDENTIFIERS)
|
|
815
|
+
]),
|
|
612
816
|
extract: (context) => {
|
|
817
|
+
for (const frameNumber of pendingFrames.keys()) {
|
|
818
|
+
if (frameNumber + 4 < context.frameNumber) pendingFrames.delete(frameNumber);
|
|
819
|
+
}
|
|
613
820
|
const extracted = [];
|
|
614
821
|
for (const world of context.worlds) {
|
|
615
822
|
if (!world.hasResource(VFX_GPU_RUNTIME_RESOURCE_KEY)) continue;
|
|
616
823
|
const camera = options.camera.read(world);
|
|
617
824
|
if (camera === void 0) continue;
|
|
618
825
|
const runtime = world.getResource(VFX_GPU_RUNTIME_RESOURCE_KEY);
|
|
619
|
-
const
|
|
620
|
-
|
|
621
|
-
const
|
|
622
|
-
|
|
826
|
+
const retained = [];
|
|
827
|
+
runtime.forEachEmitterSource(({ player, emitter }) => {
|
|
828
|
+
const sourceIntent = runtime.lastCommittedEmitter(player, emitter.id);
|
|
829
|
+
const localToWorld = sourceIntent === void 0 ? emitter.space === "world" ? IDENTITY_MATRIX : (() => {
|
|
830
|
+
const transform = world.get(player, GlobalTransform);
|
|
831
|
+
return transform.ok ? transform.value.world : IDENTITY_MATRIX;
|
|
832
|
+
})() : emitterTransform(world, sourceIntent);
|
|
833
|
+
const visible = emitterVisible(emitter, camera, localToWorld);
|
|
834
|
+
runtime.setEmitterCameraVisibility(player, emitter.id, visible);
|
|
835
|
+
const intent = runtime.lastCommittedEmitter(player, emitter.id);
|
|
836
|
+
if (intent !== void 0) {
|
|
837
|
+
retained.push({ player, emitter, intent, localToWorld, visible });
|
|
838
|
+
}
|
|
623
839
|
});
|
|
624
|
-
|
|
840
|
+
const intents = runtime.snapshot();
|
|
841
|
+
extracted.push({ world, runtime, camera, intents, retained });
|
|
625
842
|
}
|
|
626
843
|
return ok({ worlds: extracted, frameNumber: context.frameNumber });
|
|
627
844
|
},
|
|
628
845
|
plan: (frame, context) => {
|
|
629
846
|
const resources = [];
|
|
630
847
|
const passes = [];
|
|
631
|
-
const dispatchedIntents = /* @__PURE__ */ new Set();
|
|
632
848
|
const colorTarget = context.targets.find((candidate) => candidate.kind === "color") ?? context.targets.find((candidate) => candidate.kind === "swapchain");
|
|
633
849
|
const depthTarget = context.targets.find((candidate) => candidate.kind === "depth");
|
|
850
|
+
const groups = /* @__PURE__ */ new Map();
|
|
851
|
+
const outcomesByEntry = /* @__PURE__ */ new Map();
|
|
634
852
|
for (const [worldIndex, entry] of frame.worlds.entries()) {
|
|
635
|
-
|
|
636
|
-
|
|
853
|
+
const outcomes = [];
|
|
854
|
+
outcomesByEntry.set(entry, outcomes);
|
|
855
|
+
const effectiveEpochByEmitter = /* @__PURE__ */ new Map();
|
|
856
|
+
const renderGeneration = entry.runtime.renderGeneration ?? 0;
|
|
857
|
+
const attachmentId = runtimeId(entry.runtime);
|
|
858
|
+
let emitterEpochs = resetEpochByEmitter.get(entry.runtime);
|
|
859
|
+
if (emitterEpochs === void 0) {
|
|
860
|
+
emitterEpochs = /* @__PURE__ */ new Map();
|
|
861
|
+
resetEpochByEmitter.set(entry.runtime, emitterEpochs);
|
|
862
|
+
}
|
|
863
|
+
let reservations = resetEpochByIntent.get(entry.runtime);
|
|
864
|
+
if (reservations === void 0) {
|
|
865
|
+
reservations = /* @__PURE__ */ new Map();
|
|
866
|
+
resetEpochByIntent.set(entry.runtime, reservations);
|
|
867
|
+
}
|
|
868
|
+
const liveResetSequences = new Set(
|
|
869
|
+
(entry.runtime.snapshot?.() ?? entry.intents).filter((intent) => intent.reset).map((intent) => intent.sequence)
|
|
870
|
+
);
|
|
871
|
+
for (const [sequence, reservation] of reservations) {
|
|
872
|
+
if (reservation.generation !== renderGeneration || !liveResetSequences.has(sequence)) {
|
|
873
|
+
reservations.delete(sequence);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
const blockedEmitters = /* @__PURE__ */ new Set();
|
|
877
|
+
for (const intent of entry.intents) {
|
|
878
|
+
const baseKey = `${worldId(entry.world)}:${renderGeneration}:${Number(intent.player)}:${intent.emitter.id}`;
|
|
879
|
+
let priorReservedEpoch;
|
|
880
|
+
let priorReservedSequence = -1;
|
|
881
|
+
let exactReservation;
|
|
882
|
+
for (const [sequence, reservation] of reservations) {
|
|
883
|
+
if (reservation.generation === renderGeneration && reservation.emitterKey === baseKey) {
|
|
884
|
+
if (sequence === intent.sequence) {
|
|
885
|
+
exactReservation = reservation;
|
|
886
|
+
} else if (sequence < intent.sequence && sequence > priorReservedSequence) {
|
|
887
|
+
priorReservedEpoch = reservation.epoch;
|
|
888
|
+
priorReservedSequence = sequence;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
const committedEpoch = effectiveEpochByEmitter.get(baseKey) ?? Math.max(emitterEpochs.get(baseKey) ?? 0, priorReservedEpoch ?? 0);
|
|
893
|
+
let resetEpoch = committedEpoch;
|
|
894
|
+
if (intent.reset) {
|
|
895
|
+
resetEpoch = exactReservation?.generation === renderGeneration && exactReservation.emitterKey === baseKey ? exactReservation.epoch : committedEpoch + 1;
|
|
896
|
+
reservations.set(intent.sequence, {
|
|
897
|
+
generation: renderGeneration,
|
|
898
|
+
emitterKey: baseKey,
|
|
899
|
+
epoch: resetEpoch
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
if (blockedEmitters.has(baseKey)) {
|
|
903
|
+
outcomes.push({
|
|
904
|
+
intent,
|
|
905
|
+
state: "deferred",
|
|
906
|
+
...intent.reset ? { resetEpoch } : {}
|
|
907
|
+
});
|
|
908
|
+
continue;
|
|
909
|
+
}
|
|
910
|
+
if (options.playerConsumption?.isEnabled(entry.world, intent.player) === false || !entry.runtime.isEmitterSessionEnabled(intent.player, intent.emitter.id)) {
|
|
911
|
+
const state = intent.reset ? "deferred" : "skipped";
|
|
912
|
+
outcomes.push({
|
|
913
|
+
intent,
|
|
914
|
+
state,
|
|
915
|
+
...intent.reset ? { resetEpoch } : {}
|
|
916
|
+
});
|
|
917
|
+
if (intent.reset) blockedEmitters.add(baseKey);
|
|
918
|
+
continue;
|
|
919
|
+
}
|
|
637
920
|
const localToWorld = emitterTransform(entry.world, intent);
|
|
638
|
-
const visible = emitterVisible(intent, entry.camera, localToWorld);
|
|
921
|
+
const visible = emitterVisible(intent.emitter, entry.camera, localToWorld);
|
|
639
922
|
entry.runtime.setEmitterCameraVisibility(intent.player, intent.emitter.id, visible);
|
|
640
|
-
if (!visible)
|
|
641
|
-
|
|
923
|
+
if (!visible && intent.emitter.simulationWhenCulled !== "continue") {
|
|
924
|
+
const state = intent.reset ? "deferred" : "skipped";
|
|
925
|
+
outcomes.push({
|
|
926
|
+
intent,
|
|
927
|
+
state,
|
|
928
|
+
...intent.reset ? { resetEpoch } : {}
|
|
929
|
+
});
|
|
930
|
+
if (intent.reset) blockedEmitters.add(baseKey);
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
933
|
+
if (visible && requiresSceneDepth(intent) && (depthTarget === void 0 || depthTarget.sampleCount !== 1)) {
|
|
934
|
+
outcomes.push({
|
|
935
|
+
intent,
|
|
936
|
+
state: "deferred",
|
|
937
|
+
...intent.reset ? { resetEpoch } : {}
|
|
938
|
+
});
|
|
939
|
+
blockedEmitters.add(baseKey);
|
|
940
|
+
continue;
|
|
941
|
+
}
|
|
942
|
+
const requirements = intent.emitter.reflection.dataInterfaces ?? [];
|
|
943
|
+
if (requirements.length > 0 && !dataInterfacesExecutable(intent, options.dataInterfaces)) {
|
|
944
|
+
outcomes.push({
|
|
945
|
+
intent,
|
|
946
|
+
state: "deferred",
|
|
947
|
+
...intent.reset ? { resetEpoch } : {}
|
|
948
|
+
});
|
|
949
|
+
blockedEmitters.add(baseKey);
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
642
952
|
const stagePlan = validatedStagePlan(
|
|
643
953
|
intent.emitter.reflection.stages,
|
|
644
954
|
intent.instanceGeneration
|
|
645
955
|
);
|
|
646
956
|
if (!stagePlan.ok) return err(planFailure());
|
|
647
|
-
const
|
|
648
|
-
const
|
|
649
|
-
|
|
650
|
-
const runtime = `${prefix}.runtime`;
|
|
651
|
-
const aliveIndices = `${prefix}.alive-indices`;
|
|
652
|
-
const counters = `${prefix}.counters`;
|
|
653
|
-
const indirect = `${prefix}.indirect`;
|
|
654
|
-
const scratch = `${prefix}.scratch`;
|
|
655
|
-
const sharedInstances = `${prefix}.shared-instances`;
|
|
656
|
-
const eventInputs = `${prefix}.event-inputs`;
|
|
657
|
-
const events = `${prefix}.events`;
|
|
658
|
-
const bindings = `${prefix}.simulation-bindings`;
|
|
659
|
-
const capacity = intent.emitter.capacity;
|
|
660
|
-
const renderers = intent.emitter.renderers;
|
|
661
|
-
const meshes = renderers.map(
|
|
662
|
-
(renderer) => renderer.kind === "mesh" ? options.mesh?.read(entry.world, renderer.mesh) : void 0
|
|
957
|
+
const entryPoints = new Set(intent.emitter.reflection.entryPoints);
|
|
958
|
+
const dispatches = simulationDispatches(intent, stagePlan.value).filter(
|
|
959
|
+
(dispatch) => entryPoints.has(dispatch.entryPoint)
|
|
663
960
|
);
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
}
|
|
672
|
-
indirectWords[rendererIndex * 5] = renderer.kind === "mesh" ? mesh?.indices === void 0 ? submesh?.vertexCount ?? 0 : submesh?.indexCount ?? 0 : 6;
|
|
673
|
-
indirectWords[rendererIndex * 5 + 2] = renderer.kind === "mesh" && mesh?.indices !== void 0 ? submesh?.indexOffset ?? 0 : 0;
|
|
961
|
+
if (dispatches.length === 0 && intent.emitter.renderers.length === 0) {
|
|
962
|
+
outcomes.push({
|
|
963
|
+
intent,
|
|
964
|
+
state: "skipped",
|
|
965
|
+
...intent.reset ? { resetEpoch } : {}
|
|
966
|
+
});
|
|
967
|
+
continue;
|
|
674
968
|
}
|
|
675
|
-
|
|
676
|
-
const
|
|
677
|
-
|
|
678
|
-
|
|
969
|
+
effectiveEpochByEmitter.set(baseKey, resetEpoch);
|
|
970
|
+
const key = `vfx.w-${planName(worldId(entry.world).toString(36), 8)}.a-${planName(attachmentId.toString(36), 8)}.r-${entry.runtime.renderGeneration}.p-${planName(Number(intent.player).toString(36), 16)}.${planName(intent.emitter.id)}.g-${resetEpoch}`;
|
|
971
|
+
let group = groups.get(key);
|
|
972
|
+
if (group === void 0) {
|
|
973
|
+
group = {
|
|
974
|
+
key,
|
|
975
|
+
worldIndex,
|
|
976
|
+
entry,
|
|
977
|
+
intents: [],
|
|
978
|
+
// Reset data is a per-frame boundary, not a property of the
|
|
979
|
+
// epoch. Once a reset-created group is warm, later ticks in
|
|
980
|
+
// the same epoch must retain its particle state.
|
|
981
|
+
initialReset: intent.reset
|
|
982
|
+
};
|
|
983
|
+
groups.set(key, group);
|
|
984
|
+
} else if (group.entry !== entry || group.intents[0]?.intent.emitter.wgsl !== intent.emitter.wgsl || group.intents[0]?.intent.emitter.capacity !== intent.emitter.capacity) {
|
|
985
|
+
return err(planFailure());
|
|
986
|
+
}
|
|
987
|
+
group.intents.push({
|
|
988
|
+
intent,
|
|
989
|
+
localToWorld,
|
|
990
|
+
stagePlan: stagePlan.value,
|
|
991
|
+
visible,
|
|
992
|
+
retained: false
|
|
993
|
+
});
|
|
994
|
+
outcomes.push({
|
|
995
|
+
intent,
|
|
996
|
+
state: dispatches.length > 0 ? "dispatched" : "skipped",
|
|
997
|
+
...intent.reset ? { resetEpoch } : {}
|
|
998
|
+
});
|
|
999
|
+
if (dispatches.length === 0) effectiveEpochByEmitter.delete(baseKey);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
for (const [worldIndex, entry] of frame.worlds.entries()) {
|
|
1003
|
+
const renderGeneration = entry.runtime.renderGeneration ?? 0;
|
|
1004
|
+
const attachmentId = runtimeId(entry.runtime);
|
|
1005
|
+
let emitterEpochs = resetEpochByEmitter.get(entry.runtime);
|
|
1006
|
+
if (emitterEpochs === void 0) {
|
|
1007
|
+
emitterEpochs = /* @__PURE__ */ new Map();
|
|
1008
|
+
resetEpochByEmitter.set(entry.runtime, emitterEpochs);
|
|
1009
|
+
}
|
|
1010
|
+
for (const retained of entry.retained ?? []) {
|
|
1011
|
+
const hasAdmittedGroup = [...groups.values()].some(
|
|
1012
|
+
(group) => group.worldIndex === worldIndex && group.entry === entry && group.intents.some(
|
|
1013
|
+
(planned) => planned.intent.player === retained.player && planned.intent.emitter.id === retained.emitter.id
|
|
1014
|
+
)
|
|
679
1015
|
);
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
1016
|
+
if (hasAdmittedGroup) continue;
|
|
1017
|
+
const stagePlan = validatedStagePlan(
|
|
1018
|
+
retained.intent.emitter.reflection.stages,
|
|
1019
|
+
retained.intent.instanceGeneration
|
|
1020
|
+
);
|
|
1021
|
+
if (!stagePlan.ok) return err(planFailure());
|
|
1022
|
+
const baseKey = `${worldId(entry.world)}:${renderGeneration}:${Number(retained.player)}:${retained.emitter.id}`;
|
|
1023
|
+
const resetEpoch = emitterEpochs.get(baseKey) ?? 0;
|
|
1024
|
+
const key = `vfx.w-${planName(worldId(entry.world).toString(36), 8)}.a-${planName(attachmentId.toString(36), 8)}.r-${renderGeneration}.p-${planName(Number(retained.player).toString(36), 16)}.${planName(retained.emitter.id)}.g-${resetEpoch}`;
|
|
1025
|
+
const visible = retained.visible && (options.playerConsumption?.isEnabled(entry.world, retained.player) ?? true) && entry.runtime.isEmitterSessionEnabled(retained.player, retained.emitter.id);
|
|
1026
|
+
groups.set(key, {
|
|
1027
|
+
key,
|
|
1028
|
+
worldIndex,
|
|
1029
|
+
entry,
|
|
1030
|
+
intents: [
|
|
1031
|
+
{
|
|
1032
|
+
intent: retained.intent,
|
|
1033
|
+
localToWorld: retained.localToWorld,
|
|
1034
|
+
stagePlan: stagePlan.value,
|
|
1035
|
+
visible,
|
|
1036
|
+
retained: true
|
|
689
1037
|
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
1038
|
+
],
|
|
1039
|
+
initialReset: false
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
for (const group of groups.values()) {
|
|
1044
|
+
const first = group.intents[0];
|
|
1045
|
+
if (first === void 0) continue;
|
|
1046
|
+
const firstIntent = first.intent;
|
|
1047
|
+
const prefix = group.key;
|
|
1048
|
+
const program = `${prefix}.compute-program`;
|
|
1049
|
+
const particles = `${prefix}.particles`;
|
|
1050
|
+
const aliveIndices = `${prefix}.alive-indices`;
|
|
1051
|
+
const counters = `${prefix}.counters`;
|
|
1052
|
+
const indirect = `${prefix}.indirect`;
|
|
1053
|
+
const scratch = `${prefix}.scratch`;
|
|
1054
|
+
const sharedInstances = `${prefix}.shared-instances`;
|
|
1055
|
+
const projectionEventInputs = `${prefix}.projection-event-inputs`;
|
|
1056
|
+
const capacity = firstIntent.emitter.capacity;
|
|
1057
|
+
const renderers = firstIntent.emitter.renderers;
|
|
1058
|
+
const layout = firstIntent.emitter.reflection.layout;
|
|
1059
|
+
const parameterBytes = layout?.parameters.size ?? 0;
|
|
1060
|
+
const customStride = layout?.customLayout?.stride ?? 0;
|
|
1061
|
+
const hasEvents = (firstIntent.emitter.reflection.resources ?? []).includes("eventBuffer") || (firstIntent.emitter.reflection.entryPoints ?? []).includes("forgeax_vfx_event_main") || (firstIntent.emitter.reflection.bindings[0]?.entries ?? []).some(
|
|
1062
|
+
(entry) => entry.binding === 8
|
|
1063
|
+
);
|
|
1064
|
+
const diBindings = {};
|
|
1065
|
+
const diResources = [];
|
|
1066
|
+
const reflectedBindings = new Set(
|
|
1067
|
+
firstIntent.emitter.reflection.bindings.flatMap(
|
|
1068
|
+
(group2) => group2.entries.map((entry) => entry.binding)
|
|
1069
|
+
)
|
|
1070
|
+
);
|
|
1071
|
+
const requirements = (firstIntent.emitter.reflection.dataInterfaces ?? []).filter(
|
|
1072
|
+
(requirement) => reflectedBindings.has(requirement.binding)
|
|
1073
|
+
);
|
|
1074
|
+
if (requirements.length > 0) {
|
|
1075
|
+
let resolved = options.dataInterfaces?.resolve(
|
|
1076
|
+
requirements,
|
|
1077
|
+
firstIntent.instanceGeneration
|
|
1078
|
+
);
|
|
1079
|
+
if ((resolved === void 0 || !resolved.ok && resolved.error.code === "vfx-data-interface-missing" && (resolved.error.detail.providerId === void 0 || resolved.error.expected.includes("resident"))) && context.preparedDataInterfaces !== void 0) {
|
|
1080
|
+
const fallback = preparedDataInterfaceResources(
|
|
1081
|
+
requirements,
|
|
1082
|
+
firstIntent.instanceGeneration,
|
|
1083
|
+
context.preparedDataInterfaces
|
|
1084
|
+
);
|
|
1085
|
+
if (fallback !== void 0) {
|
|
1086
|
+
resolved = ok({
|
|
1087
|
+
generation: firstIntent.instanceGeneration,
|
|
1088
|
+
readiness: "ready",
|
|
1089
|
+
resources: fallback
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
if (resolved === void 0 || !resolved.ok) return err(planFailure());
|
|
1094
|
+
for (const requirement of requirements) {
|
|
1095
|
+
const resource = resolved.value.resources.find(
|
|
1096
|
+
(candidate) => candidate.token === requirement.token
|
|
1097
|
+
);
|
|
1098
|
+
const prepared = resource?.resource;
|
|
1099
|
+
if (resource === void 0 || prepared === void 0) return err(planFailure());
|
|
1100
|
+
const name = `${prefix}.di-${planName(requirement.kind)}`;
|
|
1101
|
+
if (prepared.kind === "buffer") {
|
|
1102
|
+
if (prepared.size === void 0 || prepared.size <= 0) return err(planFailure());
|
|
1103
|
+
diResources.push({
|
|
1104
|
+
kind: "prepared-gpu-resource",
|
|
1105
|
+
name,
|
|
1106
|
+
resource: {
|
|
1107
|
+
kind: "buffer",
|
|
1108
|
+
value: prepared.value,
|
|
1109
|
+
size: prepared.size,
|
|
1110
|
+
usage: prepared.usage === "storage" ? ["storage"] : ["uniform"]
|
|
1111
|
+
}
|
|
1112
|
+
});
|
|
1113
|
+
} else {
|
|
1114
|
+
const external = prepared.kind === "texture-view" ? { kind: "texture-view", value: prepared.value } : { kind: "sampler", value: prepared.value };
|
|
1115
|
+
diResources.push({
|
|
1116
|
+
kind: "prepared-gpu-resource",
|
|
1117
|
+
name,
|
|
1118
|
+
resource: external,
|
|
1119
|
+
...requirement.kind === "scene-depth" && depthTarget !== void 0 ? { logicalTarget: depthTarget.name } : {}
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
diBindings[requirement.binding] = name;
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
const meshes = renderers.map(
|
|
1126
|
+
(renderer) => renderer.kind === "mesh" ? options.mesh?.read(group.entry.world, renderer.mesh) : void 0
|
|
1127
|
+
);
|
|
1128
|
+
const indirectWords = new Uint32Array(Math.max(1, renderers.length) * 5);
|
|
1129
|
+
for (const [rendererIndex, renderer] of renderers.entries()) {
|
|
1130
|
+
const mesh = meshes[rendererIndex];
|
|
1131
|
+
const submesh = renderer.kind === "mesh" ? mesh?.submeshes[renderer.submesh ?? 0] : void 0;
|
|
1132
|
+
if (renderer.kind === "mesh" && submesh === void 0) return err(planFailure());
|
|
1133
|
+
if ((renderer.kind === "ribbon" || renderer.kind === "trail" || renderer.kind === "beam") && !createTopologyResourcePlan(renderer).ok) {
|
|
1134
|
+
return err(planFailure());
|
|
1135
|
+
}
|
|
1136
|
+
indirectWords[rendererIndex * 5] = renderer.kind === "mesh" ? mesh?.indices === void 0 ? submesh?.vertexCount ?? 0 : submesh?.indexCount ?? 0 : 6;
|
|
1137
|
+
indirectWords[rendererIndex * 5 + 2] = renderer.kind === "mesh" && mesh?.indices !== void 0 ? submesh?.indexOffset ?? 0 : 0;
|
|
1138
|
+
}
|
|
1139
|
+
const scratchBytes = (capacity * 2 + Math.ceil(capacity / WORKGROUP_SIZE)) * 4;
|
|
1140
|
+
const eventBufferBytes = Math.max(
|
|
1141
|
+
4,
|
|
1142
|
+
eventInputCapacity(firstIntent.emitter) * VFX_EVENT_INPUT_BYTES + eventCapacity(firstIntent.emitter) * VFX_EVENT_BYTES
|
|
1143
|
+
);
|
|
1144
|
+
const particleBytes = VFX_PARTICLE_CORE_STRIDE;
|
|
1145
|
+
const maxParticleInputBytes = Math.max(
|
|
1146
|
+
0,
|
|
1147
|
+
...renderers.map((renderer) => (renderer.materialInputs?.length ?? 0) * 16)
|
|
1148
|
+
);
|
|
1149
|
+
resources.push(
|
|
1150
|
+
...diResources,
|
|
1151
|
+
{
|
|
1152
|
+
kind: "compute-program",
|
|
1153
|
+
name: program,
|
|
1154
|
+
program: {
|
|
1155
|
+
wgsl: firstIntent.emitter.wgsl,
|
|
1156
|
+
entryPoints: firstIntent.emitter.reflection.entryPoints,
|
|
1157
|
+
bindings: firstIntent.emitter.reflection.bindings
|
|
1158
|
+
}
|
|
1159
|
+
},
|
|
1160
|
+
{
|
|
1161
|
+
kind: "buffer",
|
|
1162
|
+
name: particles,
|
|
1163
|
+
size: capacity * particleBytes,
|
|
1164
|
+
usage: ["storage"],
|
|
1165
|
+
...group.initialReset ? { data: resetData(capacity * particleBytes) } : {}
|
|
1166
|
+
},
|
|
1167
|
+
...parameterBytes === 0 ? [] : [
|
|
720
1168
|
{
|
|
721
1169
|
kind: "buffer",
|
|
722
|
-
name:
|
|
723
|
-
size:
|
|
724
|
-
usage: ["
|
|
725
|
-
|
|
1170
|
+
name: `${prefix}.parameters`,
|
|
1171
|
+
size: parameterBytes,
|
|
1172
|
+
usage: ["uniform"],
|
|
1173
|
+
data: firstIntent.parameterBlock
|
|
1174
|
+
}
|
|
1175
|
+
],
|
|
1176
|
+
...customStride === 0 ? [] : [
|
|
726
1177
|
{
|
|
727
1178
|
kind: "buffer",
|
|
728
|
-
name:
|
|
729
|
-
size:
|
|
1179
|
+
name: `${prefix}.custom`,
|
|
1180
|
+
size: capacity * customStride,
|
|
730
1181
|
usage: ["storage"],
|
|
731
|
-
data:
|
|
732
|
-
}
|
|
1182
|
+
...group.initialReset ? { data: resetData(capacity * customStride) } : {}
|
|
1183
|
+
}
|
|
1184
|
+
],
|
|
1185
|
+
{ kind: "buffer", name: aliveIndices, size: capacity * 4, usage: ["storage"] },
|
|
1186
|
+
{
|
|
1187
|
+
kind: "buffer",
|
|
1188
|
+
name: counters,
|
|
1189
|
+
size: COUNTERS_BYTES,
|
|
1190
|
+
usage: ["storage"],
|
|
1191
|
+
...group.initialReset ? { data: resetData(COUNTERS_BYTES) } : {}
|
|
1192
|
+
},
|
|
1193
|
+
{
|
|
1194
|
+
kind: "buffer",
|
|
1195
|
+
name: indirect,
|
|
1196
|
+
size: indirectWords.byteLength,
|
|
1197
|
+
usage: ["storage", "indirect"],
|
|
1198
|
+
...group.initialReset ? { data: indirectWords } : {}
|
|
1199
|
+
},
|
|
1200
|
+
{
|
|
1201
|
+
kind: "buffer",
|
|
1202
|
+
name: scratch,
|
|
1203
|
+
size: scratchBytes,
|
|
1204
|
+
usage: ["storage"],
|
|
1205
|
+
...group.initialReset ? { data: resetData(scratchBytes) } : {}
|
|
1206
|
+
},
|
|
1207
|
+
{
|
|
1208
|
+
kind: "buffer",
|
|
1209
|
+
name: sharedInstances,
|
|
1210
|
+
size: capacity * (Math.max(BILLBOARD_INSTANCE_BYTES, MESH_INSTANCE_BYTES) + maxParticleInputBytes),
|
|
1211
|
+
usage: ["storage", "vertex"]
|
|
1212
|
+
},
|
|
1213
|
+
...hasEvents ? [
|
|
733
1214
|
{
|
|
734
1215
|
kind: "buffer",
|
|
735
|
-
name:
|
|
736
|
-
size:
|
|
1216
|
+
name: projectionEventInputs,
|
|
1217
|
+
size: eventBufferBytes,
|
|
737
1218
|
usage: ["storage"],
|
|
738
|
-
|
|
739
|
-
}
|
|
1219
|
+
data: encodeEventBuffer(firstIntent)
|
|
1220
|
+
}
|
|
1221
|
+
] : []
|
|
1222
|
+
);
|
|
1223
|
+
const entryPoints = new Set(firstIntent.emitter.reflection.entryPoints);
|
|
1224
|
+
for (const [tickIndex, planned] of group.intents.entries()) {
|
|
1225
|
+
if (planned.retained) continue;
|
|
1226
|
+
const tickPrefix = `${prefix}.tick-${tickIndex}`;
|
|
1227
|
+
const tickRuntime = `${tickPrefix}.runtime`;
|
|
1228
|
+
const tickEventInputs = `${tickPrefix}.event-inputs`;
|
|
1229
|
+
const tickBindings = `${tickPrefix}.simulation-bindings`;
|
|
1230
|
+
resources.push(
|
|
740
1231
|
{
|
|
741
1232
|
kind: "buffer",
|
|
742
|
-
name:
|
|
1233
|
+
name: tickRuntime,
|
|
743
1234
|
size: RUNTIME_BYTES,
|
|
744
1235
|
usage: ["uniform"],
|
|
745
|
-
data: runtimeData(
|
|
1236
|
+
data: runtimeData(
|
|
1237
|
+
planned.intent,
|
|
1238
|
+
group.entry.camera,
|
|
1239
|
+
void 0,
|
|
1240
|
+
planned.localToWorld
|
|
1241
|
+
)
|
|
746
1242
|
},
|
|
1243
|
+
...hasEvents ? [
|
|
1244
|
+
{
|
|
1245
|
+
kind: "buffer",
|
|
1246
|
+
name: tickEventInputs,
|
|
1247
|
+
size: eventBufferBytes,
|
|
1248
|
+
usage: ["storage"],
|
|
1249
|
+
data: encodeEventBuffer(planned.intent)
|
|
1250
|
+
}
|
|
1251
|
+
] : [],
|
|
1252
|
+
...parameterBytes === 0 ? [] : [
|
|
1253
|
+
{
|
|
1254
|
+
kind: "buffer",
|
|
1255
|
+
name: `${tickPrefix}.parameters`,
|
|
1256
|
+
size: parameterBytes,
|
|
1257
|
+
usage: ["uniform"],
|
|
1258
|
+
data: planned.intent.parameterBlock
|
|
1259
|
+
}
|
|
1260
|
+
],
|
|
747
1261
|
{
|
|
748
1262
|
kind: "compute-bindings",
|
|
749
|
-
name:
|
|
1263
|
+
name: tickBindings,
|
|
750
1264
|
program,
|
|
751
|
-
entries: computeBindingEntries(intent, {
|
|
1265
|
+
entries: computeBindingEntries(planned.intent, {
|
|
752
1266
|
0: particles,
|
|
753
|
-
1:
|
|
1267
|
+
1: tickRuntime,
|
|
754
1268
|
2: aliveIndices,
|
|
755
1269
|
3: counters,
|
|
756
1270
|
4: indirect,
|
|
757
1271
|
5: scratch,
|
|
758
1272
|
6: sharedInstances,
|
|
759
|
-
8:
|
|
760
|
-
|
|
1273
|
+
...hasEvents ? { 8: tickEventInputs } : {},
|
|
1274
|
+
...parameterBytes === 0 ? {} : { 10: `${tickPrefix}.parameters` },
|
|
1275
|
+
...customStride === 0 ? {} : { 11: `${prefix}.custom` },
|
|
1276
|
+
...diBindings
|
|
761
1277
|
})
|
|
762
1278
|
}
|
|
763
1279
|
);
|
|
764
|
-
const
|
|
765
|
-
const dispatches = simulationDispatches(intent, stagePlan.value).filter(
|
|
1280
|
+
const dispatches = simulationDispatches(planned.intent, planned.stagePlan).filter(
|
|
766
1281
|
(dispatch) => entryPoints.has(dispatch.entryPoint)
|
|
767
1282
|
);
|
|
768
|
-
if (dispatches.length
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
dispatchedIntents.add(intent);
|
|
777
|
-
}
|
|
1283
|
+
if (dispatches.length === 0) continue;
|
|
1284
|
+
passes.push({
|
|
1285
|
+
kind: "compute",
|
|
1286
|
+
name: `${tickPrefix}.simulate`,
|
|
1287
|
+
program,
|
|
1288
|
+
bindings: tickBindings,
|
|
1289
|
+
dispatches
|
|
1290
|
+
});
|
|
778
1291
|
for (const [rendererIndex, renderer] of renderers.entries()) {
|
|
1292
|
+
if (renderer.enabled === false) continue;
|
|
1293
|
+
if (renderer.kind !== "trail") continue;
|
|
1294
|
+
if (!entryPoints.has("forgeax_vfx_trail_history_main")) continue;
|
|
779
1295
|
const rendererPrefix = `${prefix}.renderer-${rendererIndex}`;
|
|
780
|
-
const isBillboard = renderer.kind === "billboard";
|
|
781
|
-
const isTopology = renderer.kind === "ribbon" || renderer.kind === "trail" || renderer.kind === "beam";
|
|
782
|
-
const topologyPlan = isTopology ? createTopologyResourcePlan(renderer) : void 0;
|
|
783
|
-
if (topologyPlan !== void 0 && !topologyPlan.ok) return err(planFailure());
|
|
784
|
-
const material = options.material?.read(entry.world, renderer.material);
|
|
785
|
-
const materialPass = particleMaterialPass(renderer.kind, material);
|
|
786
|
-
const mesh = meshes[rendererIndex];
|
|
787
|
-
const submesh = renderer.kind === "mesh" ? mesh?.submeshes[renderer.submesh ?? 0] : void 0;
|
|
788
|
-
const indexFormat = mesh?.indices instanceof Uint32Array ? "uint32" : "uint16";
|
|
789
|
-
const sceneDepthBinding = isBillboard ? particleMaterialSceneDepthBinding(
|
|
790
|
-
context.materialShaderBindingContract?.(materialPass.shader) ?? (materialPass.shader === PARTICLE_SHADER_IDENTIFIERS.billboard ? "view-and-scene-depth" : void 0)
|
|
791
|
-
) : void 0;
|
|
792
|
-
const instances = `${rendererPrefix}.instances`;
|
|
793
1296
|
const history = `${rendererPrefix}.history`;
|
|
794
|
-
const
|
|
795
|
-
const
|
|
796
|
-
const vertexLayout = isBillboard ? RENDER_FEATURE_VERTEX_LAYOUTS.billboardMaterialInstance : isTopology ? RENDER_FEATURE_VERTEX_LAYOUTS.topologySegmentInstance : RENDER_FEATURE_VERTEX_LAYOUTS.meshGeometryMaterialInstance;
|
|
797
|
-
const instanceBytes = isTopology ? topologyPlan?.value.vertexBytes ?? 16 : capacity * (isBillboard ? BILLBOARD_INSTANCE_BYTES : MESH_INSTANCE_BYTES);
|
|
798
|
-
const historyBytes = renderer.kind === "trail" ? Math.max(16, renderer.capacity * renderer.historyLength * 16) : 16;
|
|
1297
|
+
const historyRuntime = `${tickPrefix}.renderer-${rendererIndex}.history-runtime`;
|
|
1298
|
+
const historyBindings = `${tickPrefix}.renderer-${rendererIndex}.history-bindings`;
|
|
799
1299
|
resources.push(
|
|
800
1300
|
{
|
|
801
1301
|
kind: "buffer",
|
|
802
|
-
name:
|
|
803
|
-
size: instanceBytes,
|
|
804
|
-
usage: ["storage", "vertex"]
|
|
805
|
-
},
|
|
806
|
-
{
|
|
807
|
-
kind: "buffer",
|
|
808
|
-
name: history,
|
|
809
|
-
size: historyBytes,
|
|
810
|
-
usage: ["storage"],
|
|
811
|
-
...intent.reset ? { data: resetData(historyBytes) } : {}
|
|
812
|
-
},
|
|
813
|
-
{
|
|
814
|
-
kind: "buffer",
|
|
815
|
-
name: projectionRuntime,
|
|
1302
|
+
name: historyRuntime,
|
|
816
1303
|
size: RUNTIME_BYTES,
|
|
817
1304
|
usage: ["uniform"],
|
|
818
1305
|
data: runtimeData(
|
|
819
|
-
|
|
820
|
-
entry.camera,
|
|
821
|
-
|
|
822
|
-
localToWorld,
|
|
1306
|
+
planned.intent,
|
|
1307
|
+
group.entry.camera,
|
|
1308
|
+
void 0,
|
|
1309
|
+
planned.localToWorld,
|
|
823
1310
|
renderer,
|
|
824
1311
|
rendererIndex
|
|
825
1312
|
)
|
|
826
1313
|
},
|
|
827
1314
|
{
|
|
828
1315
|
kind: "compute-bindings",
|
|
829
|
-
name:
|
|
1316
|
+
name: historyBindings,
|
|
830
1317
|
program,
|
|
831
|
-
entries: computeBindingEntries(intent, {
|
|
1318
|
+
entries: computeBindingEntries(planned.intent, {
|
|
832
1319
|
0: particles,
|
|
833
|
-
1:
|
|
1320
|
+
1: historyRuntime,
|
|
834
1321
|
2: aliveIndices,
|
|
835
1322
|
3: counters,
|
|
836
1323
|
4: indirect,
|
|
837
1324
|
5: history,
|
|
838
|
-
6:
|
|
839
|
-
8:
|
|
840
|
-
|
|
1325
|
+
6: sharedInstances,
|
|
1326
|
+
...hasEvents ? { 8: tickEventInputs } : {},
|
|
1327
|
+
...parameterBytes === 0 ? {} : { 10: `${tickPrefix}.parameters` },
|
|
1328
|
+
...customStride === 0 ? {} : { 11: `${prefix}.custom` },
|
|
1329
|
+
...diBindings
|
|
841
1330
|
})
|
|
842
1331
|
}
|
|
843
1332
|
);
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
1333
|
+
passes.push({
|
|
1334
|
+
kind: "compute",
|
|
1335
|
+
name: `${historyBindings}.write`,
|
|
1336
|
+
program,
|
|
1337
|
+
bindings: historyBindings,
|
|
1338
|
+
dispatches: [
|
|
1339
|
+
{
|
|
1340
|
+
kind: "direct",
|
|
1341
|
+
entryPoint: "forgeax_vfx_trail_history_main",
|
|
1342
|
+
workgroups: [Math.max(1, Math.ceil(renderer.capacity / WORKGROUP_SIZE))]
|
|
1343
|
+
}
|
|
1344
|
+
]
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
const latest = group.intents.at(-1) ?? first;
|
|
1349
|
+
for (const [rendererIndex, renderer] of renderers.entries()) {
|
|
1350
|
+
if (renderer.enabled === false) continue;
|
|
1351
|
+
const rendererPrefix = `${prefix}.renderer-${rendererIndex}`;
|
|
1352
|
+
const isBillboard = renderer.kind === "billboard";
|
|
1353
|
+
const isTopology = renderer.kind === "ribbon" || renderer.kind === "trail" || renderer.kind === "beam";
|
|
1354
|
+
const topologyPlan = isTopology ? createTopologyResourcePlan(renderer) : void 0;
|
|
1355
|
+
if (topologyPlan !== void 0 && !topologyPlan.ok) return err(planFailure());
|
|
1356
|
+
const material = options.material?.read(group.entry.world, renderer.material);
|
|
1357
|
+
const reflectedRenderer = firstIntent.emitter.reflection.renderers?.[rendererIndex];
|
|
1358
|
+
const preparedInputs = prepareParticleMaterialInputs(
|
|
1359
|
+
renderer,
|
|
1360
|
+
material,
|
|
1361
|
+
reflectedRenderer?.materialInputDefinitions
|
|
1362
|
+
);
|
|
1363
|
+
if (!preparedInputs.ok) return err(planFailure());
|
|
1364
|
+
const hasParticleInputs = preparedInputs.value.lanes > 0;
|
|
1365
|
+
const materialPass = particleMaterialPass(renderer.kind, material, hasParticleInputs);
|
|
1366
|
+
const mesh = meshes[rendererIndex];
|
|
1367
|
+
const submesh = renderer.kind === "mesh" ? mesh?.submeshes[renderer.submesh ?? 0] : void 0;
|
|
1368
|
+
const indexFormat = mesh?.indices instanceof Uint32Array ? "uint32" : "uint16";
|
|
1369
|
+
const sceneDepthBinding = particleMaterialSceneDepthBinding(
|
|
1370
|
+
context.materialShaderBindingContract?.(materialPass.shader) ?? (materialPass.shader === PARTICLE_SHADER_IDENTIFIERS.billboard ? "view-and-scene-depth" : void 0)
|
|
1371
|
+
);
|
|
1372
|
+
const instances = `${rendererPrefix}.instances`;
|
|
1373
|
+
const history = `${rendererPrefix}.history`;
|
|
1374
|
+
const projectionRuntime = `${rendererPrefix}.runtime`;
|
|
1375
|
+
const projectionBindings = `${rendererPrefix}.compute-bindings`;
|
|
1376
|
+
const vertexLayout = isBillboard ? hasParticleInputs ? RENDER_FEATURE_VERTEX_LAYOUTS.billboardMaterialInputInstance : RENDER_FEATURE_VERTEX_LAYOUTS.billboardMaterialInstance : isTopology ? hasParticleInputs ? RENDER_FEATURE_VERTEX_LAYOUTS.topologySegmentMaterialInputInstance : RENDER_FEATURE_VERTEX_LAYOUTS.topologySegmentInstance : hasParticleInputs ? RENDER_FEATURE_VERTEX_LAYOUTS.meshGeometryMaterialInputInstance : RENDER_FEATURE_VERTEX_LAYOUTS.meshGeometryMaterialInstance;
|
|
1377
|
+
const inputBytes = preparedInputs.value.stride;
|
|
1378
|
+
const instanceBytes = isTopology ? (() => {
|
|
1379
|
+
const vertexCount = Math.max(
|
|
1380
|
+
1,
|
|
1381
|
+
Math.floor((topologyPlan?.value.vertexBytes ?? 16) / 48)
|
|
1382
|
+
);
|
|
1383
|
+
return vertexCount * (48 + inputBytes);
|
|
1384
|
+
})() : capacity * ((isBillboard ? BILLBOARD_INSTANCE_BYTES : MESH_INSTANCE_BYTES) + inputBytes);
|
|
1385
|
+
const historyBytes = renderer.kind === "trail" ? capacity * (Math.max(2, renderer.historyLength) + 1) * 16 : 16;
|
|
1386
|
+
resources.push(
|
|
1387
|
+
{
|
|
1388
|
+
kind: "buffer",
|
|
1389
|
+
name: instances,
|
|
1390
|
+
size: instanceBytes,
|
|
1391
|
+
usage: ["storage", "vertex"]
|
|
1392
|
+
},
|
|
1393
|
+
{
|
|
1394
|
+
kind: "buffer",
|
|
1395
|
+
name: history,
|
|
1396
|
+
size: historyBytes,
|
|
1397
|
+
usage: ["storage"],
|
|
1398
|
+
...group.initialReset ? { data: resetData(historyBytes) } : {}
|
|
855
1399
|
}
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
1400
|
+
);
|
|
1401
|
+
const castsShadow = renderer.kind === "mesh" && renderer.castShadows;
|
|
1402
|
+
if (!latest.visible && !castsShadow) continue;
|
|
1403
|
+
resources.push(
|
|
1404
|
+
{
|
|
1405
|
+
kind: "buffer",
|
|
1406
|
+
name: projectionRuntime,
|
|
1407
|
+
size: RUNTIME_BYTES,
|
|
1408
|
+
usage: ["uniform"],
|
|
1409
|
+
data: runtimeData(
|
|
1410
|
+
{ ...latest.intent, fixedDelta: 0, spawnCount: 0 },
|
|
1411
|
+
group.entry.camera,
|
|
1412
|
+
material,
|
|
1413
|
+
latest.localToWorld,
|
|
1414
|
+
renderer,
|
|
1415
|
+
rendererIndex,
|
|
1416
|
+
preparedInputs.value.lanes,
|
|
1417
|
+
renderer.kind === "mesh" && submesh !== void 0 ? {
|
|
1418
|
+
count: mesh?.indices === void 0 ? submesh.vertexCount : submesh.indexCount,
|
|
1419
|
+
firstIndex: mesh?.indices === void 0 ? 0 : submesh.indexOffset
|
|
1420
|
+
} : void 0
|
|
1421
|
+
)
|
|
1422
|
+
},
|
|
1423
|
+
{
|
|
1424
|
+
kind: "compute-bindings",
|
|
1425
|
+
name: projectionBindings,
|
|
1426
|
+
program,
|
|
1427
|
+
entries: computeBindingEntries(latest.intent, {
|
|
1428
|
+
0: particles,
|
|
1429
|
+
1: projectionRuntime,
|
|
1430
|
+
2: aliveIndices,
|
|
1431
|
+
3: counters,
|
|
1432
|
+
4: indirect,
|
|
1433
|
+
5: history,
|
|
1434
|
+
6: instances,
|
|
1435
|
+
...hasEvents ? { 8: projectionEventInputs } : {},
|
|
1436
|
+
...parameterBytes === 0 ? {} : { 10: `${prefix}.parameters` },
|
|
1437
|
+
...customStride === 0 ? {} : { 11: `${prefix}.custom` },
|
|
1438
|
+
...diBindings
|
|
1439
|
+
})
|
|
861
1440
|
}
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
1441
|
+
);
|
|
1442
|
+
const projectionDispatches = [];
|
|
1443
|
+
const pushProjection = (entryPoint, workgroups) => {
|
|
1444
|
+
if (!entryPoints.has(entryPoint)) return;
|
|
1445
|
+
projectionDispatches.push({
|
|
1446
|
+
kind: "direct",
|
|
1447
|
+
entryPoint,
|
|
1448
|
+
workgroups: [Math.max(1, workgroups)]
|
|
1449
|
+
});
|
|
1450
|
+
};
|
|
1451
|
+
if (isBillboard && (renderer.sorting === "view-depth" || renderer.sorting === "view-distance" || renderer.sorting === "custom-ascending" || renderer.sorting === "custom-descending")) {
|
|
1452
|
+
pushProjection("forgeax_vfx_sort_main", 1);
|
|
1453
|
+
}
|
|
1454
|
+
if (renderer.kind === "trail") pushProjection("forgeax_vfx_trail_offsets_main", 1);
|
|
1455
|
+
const projectionCount = renderer.kind === "trail" ? renderer.capacity * Math.max(1, renderer.historyLength - 1) : isTopology ? renderer.capacity : capacity;
|
|
1456
|
+
pushProjection(
|
|
1457
|
+
renderer.kind === "billboard" ? "forgeax_vfx_billboard_main" : renderer.kind === "mesh" ? "forgeax_vfx_mesh_main" : `forgeax_vfx_${renderer.kind}_main`,
|
|
1458
|
+
Math.ceil(projectionCount / WORKGROUP_SIZE)
|
|
1459
|
+
);
|
|
1460
|
+
if (projectionDispatches.length > 0) {
|
|
1461
|
+
passes.push({
|
|
1462
|
+
kind: "compute",
|
|
1463
|
+
name: `${rendererPrefix}.project`,
|
|
1464
|
+
program,
|
|
1465
|
+
bindings: projectionBindings,
|
|
1466
|
+
dispatches: projectionDispatches
|
|
1467
|
+
});
|
|
1468
|
+
}
|
|
1469
|
+
const graphicsProgram = `${rendererPrefix}.graphics-program`;
|
|
1470
|
+
const graphicsBindings = `${rendererPrefix}.graphics-bindings`;
|
|
1471
|
+
const vertexData = `${rendererPrefix}.vertex-data`;
|
|
1472
|
+
const defaultRenderState = particleRendererRenderState(
|
|
1473
|
+
renderer.kind,
|
|
1474
|
+
renderer.kind === "billboard" ? renderer.blend : void 0,
|
|
1475
|
+
materialPass.renderState
|
|
1476
|
+
);
|
|
1477
|
+
const renderState = isBillboard && depthTarget !== void 0 ? { ...defaultRenderState ?? {}, depthWriteEnabled: false } : defaultRenderState;
|
|
1478
|
+
resources.push(
|
|
1479
|
+
{
|
|
1480
|
+
kind: "graphics-program",
|
|
1481
|
+
name: graphicsProgram,
|
|
1482
|
+
program: {
|
|
1483
|
+
shader: materialPass.shader,
|
|
1484
|
+
vertexLayout,
|
|
1485
|
+
...hasParticleInputs ? { particleInputLanes: preparedInputs.value.lanes } : {},
|
|
1486
|
+
colorFormats: [colorTarget?.format ?? "rgba8unorm-srgb"],
|
|
1487
|
+
...depthTarget === void 0 ? {} : { depthFormat: depthTarget.format },
|
|
1488
|
+
sampleCount: colorTarget?.sampleCount ?? 1,
|
|
1489
|
+
topology: submesh?.topology ?? "triangle-list",
|
|
1490
|
+
...mesh?.indices === void 0 ? {} : { indexFormat },
|
|
1491
|
+
...renderState === void 0 ? {} : { renderState }
|
|
1492
|
+
}
|
|
1493
|
+
},
|
|
1494
|
+
{
|
|
1495
|
+
kind: "graphics-bindings",
|
|
1496
|
+
name: graphicsBindings,
|
|
1497
|
+
program: graphicsProgram,
|
|
1498
|
+
values: {
|
|
1499
|
+
group: 0,
|
|
1500
|
+
runtime: projectionRuntime,
|
|
1501
|
+
instances,
|
|
1502
|
+
...sceneDepthBinding === void 0 ? {} : { sceneDepthBinding }
|
|
1503
|
+
},
|
|
1504
|
+
...sceneDepthBinding !== void 0 && depthTarget !== void 0 ? { logicalTargets: { sceneDepth: depthTarget.name } } : {}
|
|
1505
|
+
},
|
|
1506
|
+
{ kind: "vertex-data", name: vertexData, layout: vertexLayout, buffer: instances }
|
|
1507
|
+
);
|
|
1508
|
+
const drawBindings = [graphicsBindings];
|
|
1509
|
+
if (particleMaterialUsesBindings(material) || materialPass.shader === PARTICLE_SHADER_IDENTIFIERS.mesh || materialPass.shader === PARTICLE_INPUT_SHADER_IDENTIFIERS.mesh) {
|
|
1510
|
+
const materialBindings = `${rendererPrefix}.material-bindings.w-${group.worldIndex}`;
|
|
1511
|
+
resources.push({
|
|
1512
|
+
kind: "graphics-bindings",
|
|
1513
|
+
name: materialBindings,
|
|
1514
|
+
program: graphicsProgram,
|
|
1515
|
+
values: {
|
|
1516
|
+
group: 1,
|
|
1517
|
+
material: { world: group.worldIndex, guid: renderer.material }
|
|
1518
|
+
}
|
|
1519
|
+
});
|
|
1520
|
+
drawBindings.push(materialBindings);
|
|
1521
|
+
}
|
|
1522
|
+
const vertexBindings = [];
|
|
1523
|
+
let indexData;
|
|
1524
|
+
if (renderer.kind === "mesh") {
|
|
1525
|
+
if (mesh === void 0) return err(planFailure());
|
|
1526
|
+
const geometryBuffer = `${rendererPrefix}.geometry-buffer`;
|
|
1527
|
+
const geometry = `${rendererPrefix}.geometry`;
|
|
1528
|
+
const geometryData = canonicalMeshVertices(mesh);
|
|
1529
|
+
resources.push(
|
|
1530
|
+
{
|
|
1531
|
+
kind: "buffer",
|
|
1532
|
+
name: geometryBuffer,
|
|
1533
|
+
size: geometryData.byteLength,
|
|
1534
|
+
usage: ["vertex"],
|
|
1535
|
+
data: geometryData
|
|
1536
|
+
},
|
|
1537
|
+
{
|
|
1538
|
+
kind: "vertex-data",
|
|
1539
|
+
name: geometry,
|
|
1540
|
+
layout: vertexLayout,
|
|
1541
|
+
buffer: geometryBuffer
|
|
1542
|
+
}
|
|
866
1543
|
);
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
1544
|
+
vertexBindings.push({ slot: 0, resource: geometry }, { slot: 1, resource: vertexData });
|
|
1545
|
+
if (mesh.indices !== void 0) {
|
|
1546
|
+
const indexBuffer = `${rendererPrefix}.index-buffer`;
|
|
1547
|
+
const indices = `${rendererPrefix}.indices`;
|
|
1548
|
+
resources.push(
|
|
1549
|
+
{
|
|
1550
|
+
kind: "buffer",
|
|
1551
|
+
name: indexBuffer,
|
|
1552
|
+
size: mesh.indices.byteLength,
|
|
1553
|
+
usage: ["index"],
|
|
1554
|
+
data: mesh.indices
|
|
1555
|
+
},
|
|
1556
|
+
{
|
|
1557
|
+
kind: "index-data",
|
|
1558
|
+
name: indices,
|
|
1559
|
+
format: indexFormat,
|
|
1560
|
+
buffer: indexBuffer
|
|
1561
|
+
}
|
|
1562
|
+
);
|
|
1563
|
+
indexData = { resource: indices, format: indexFormat };
|
|
875
1564
|
}
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
materialPass.renderState
|
|
883
|
-
);
|
|
884
|
-
const renderState = isBillboard && depthTarget !== void 0 ? { ...defaultRenderState ?? {}, depthWriteEnabled: false } : defaultRenderState;
|
|
1565
|
+
} else {
|
|
1566
|
+
vertexBindings.push({ slot: 0, resource: vertexData });
|
|
1567
|
+
}
|
|
1568
|
+
if (castsShadow) {
|
|
1569
|
+
const shadowProgram = `${rendererPrefix}.shadow-program`;
|
|
1570
|
+
const shadowBindings = `${rendererPrefix}.shadow-bindings`;
|
|
885
1571
|
resources.push(
|
|
886
1572
|
{
|
|
887
1573
|
kind: "graphics-program",
|
|
888
|
-
name:
|
|
1574
|
+
name: shadowProgram,
|
|
889
1575
|
program: {
|
|
890
|
-
shader:
|
|
1576
|
+
shader: "forgeax::vfx-render.particles.mesh-shadow",
|
|
891
1577
|
vertexLayout,
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
1578
|
+
...hasParticleInputs ? { particleInputLanes: preparedInputs.value.lanes } : {},
|
|
1579
|
+
colorFormats: [],
|
|
1580
|
+
depthFormat: "depth32float",
|
|
895
1581
|
topology: submesh?.topology ?? "triangle-list",
|
|
896
1582
|
...mesh?.indices === void 0 ? {} : { indexFormat },
|
|
897
|
-
|
|
1583
|
+
renderState: { depthWriteEnabled: true, cullMode: "none" }
|
|
898
1584
|
}
|
|
899
1585
|
},
|
|
900
1586
|
{
|
|
901
1587
|
kind: "graphics-bindings",
|
|
902
|
-
name:
|
|
903
|
-
program:
|
|
904
|
-
values: {
|
|
905
|
-
|
|
906
|
-
runtime: projectionRuntime,
|
|
907
|
-
instances,
|
|
908
|
-
...sceneDepthBinding === void 0 ? {} : { sceneDepthBinding }
|
|
909
|
-
},
|
|
910
|
-
...sceneDepthBinding !== void 0 && depthTarget !== void 0 ? { logicalTargets: { sceneDepth: depthTarget.name } } : {}
|
|
911
|
-
},
|
|
912
|
-
{ kind: "vertex-data", name: vertexData, layout: vertexLayout, buffer: instances }
|
|
1588
|
+
name: shadowBindings,
|
|
1589
|
+
program: shadowProgram,
|
|
1590
|
+
values: { group: 0, runtime: projectionRuntime, instances }
|
|
1591
|
+
}
|
|
913
1592
|
);
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
kind: "graphics-bindings",
|
|
919
|
-
name: materialBindings,
|
|
920
|
-
program: graphicsProgram,
|
|
921
|
-
values: {
|
|
922
|
-
group: 1,
|
|
923
|
-
material: { world: worldIndex, guid: renderer.material }
|
|
924
|
-
}
|
|
925
|
-
});
|
|
926
|
-
drawBindings.push(materialBindings);
|
|
927
|
-
}
|
|
928
|
-
const vertexBindings = [];
|
|
929
|
-
let indexData;
|
|
930
|
-
if (renderer.kind === "mesh") {
|
|
931
|
-
if (mesh === void 0) return err(planFailure());
|
|
932
|
-
const geometryBuffer = `${rendererPrefix}.geometry-buffer`;
|
|
933
|
-
const geometry = `${rendererPrefix}.geometry`;
|
|
934
|
-
const geometryData = canonicalMeshVertices(mesh);
|
|
935
|
-
resources.push(
|
|
936
|
-
{
|
|
937
|
-
kind: "buffer",
|
|
938
|
-
name: geometryBuffer,
|
|
939
|
-
size: geometryData.byteLength,
|
|
940
|
-
usage: ["vertex"],
|
|
941
|
-
data: geometryData
|
|
942
|
-
},
|
|
1593
|
+
passes.push({
|
|
1594
|
+
kind: "shadow-caster",
|
|
1595
|
+
name: `${rendererPrefix}.shadow`,
|
|
1596
|
+
draws: [
|
|
943
1597
|
{
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
{ slot: 1, resource: vertexData }
|
|
953
|
-
);
|
|
954
|
-
if (mesh.indices !== void 0) {
|
|
955
|
-
const indexBuffer = `${rendererPrefix}.index-buffer`;
|
|
956
|
-
const indices = `${rendererPrefix}.indices`;
|
|
957
|
-
resources.push(
|
|
958
|
-
{
|
|
959
|
-
kind: "buffer",
|
|
960
|
-
name: indexBuffer,
|
|
961
|
-
size: mesh.indices.byteLength,
|
|
962
|
-
usage: ["index"],
|
|
963
|
-
data: mesh.indices
|
|
964
|
-
},
|
|
965
|
-
{
|
|
966
|
-
kind: "index-data",
|
|
967
|
-
name: indices,
|
|
968
|
-
format: indexFormat,
|
|
969
|
-
buffer: indexBuffer
|
|
1598
|
+
program: shadowProgram,
|
|
1599
|
+
bindings: [shadowBindings],
|
|
1600
|
+
vertexData: vertexBindings,
|
|
1601
|
+
...indexData === void 0 ? {} : { indexData },
|
|
1602
|
+
draw: {
|
|
1603
|
+
kind: indexData === void 0 ? "draw-indirect" : "draw-indexed-indirect",
|
|
1604
|
+
resource: indirect,
|
|
1605
|
+
offset: rendererIndex * 20
|
|
970
1606
|
}
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
}
|
|
1607
|
+
}
|
|
1608
|
+
]
|
|
1609
|
+
});
|
|
1610
|
+
}
|
|
1611
|
+
if (latest.visible)
|
|
977
1612
|
passes.push({
|
|
978
1613
|
kind: "raster",
|
|
979
1614
|
name: `${rendererPrefix}.raster`,
|
|
@@ -1006,21 +1641,50 @@ function gpuParticleRenderFeature(options) {
|
|
|
1006
1641
|
}
|
|
1007
1642
|
]
|
|
1008
1643
|
});
|
|
1009
|
-
}
|
|
1010
1644
|
}
|
|
1011
1645
|
}
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1646
|
+
pendingFrames.set(
|
|
1647
|
+
frame.frameNumber,
|
|
1648
|
+
frame.worlds.map((entry) => ({
|
|
1649
|
+
world: entry.world,
|
|
1650
|
+
runtime: entry.runtime,
|
|
1651
|
+
outcomes: outcomesByEntry.get(entry) ?? entry.intents.map((intent) => ({ intent, state: "deferred" }))
|
|
1652
|
+
}))
|
|
1653
|
+
);
|
|
1654
|
+
return ok({ resources, passes });
|
|
1655
|
+
},
|
|
1656
|
+
onFrameSubmitted: (frame) => {
|
|
1657
|
+
const pending = pendingFrames.get(frame.frameNumber);
|
|
1658
|
+
if (pending === void 0) return;
|
|
1659
|
+
pendingFrames.delete(frame.frameNumber);
|
|
1660
|
+
for (const entry of pending) {
|
|
1661
|
+
const acknowledgedSequences = [];
|
|
1662
|
+
const publishedSequences = [];
|
|
1663
|
+
for (const outcome of entry.outcomes) {
|
|
1664
|
+
if (outcome.state === "deferred") continue;
|
|
1665
|
+
acknowledgedSequences.push(outcome.intent.sequence);
|
|
1666
|
+
if (outcome.state === "dispatched") {
|
|
1667
|
+
publishedSequences.push(outcome.intent.sequence);
|
|
1668
|
+
entry.runtime.markEventDispatched(outcome.intent.player, outcome.intent.eventCounters);
|
|
1669
|
+
if (outcome.resetEpoch !== void 0) {
|
|
1670
|
+
const baseKey = `${worldId(entry.world)}:${entry.runtime.renderGeneration ?? 0}:${Number(outcome.intent.player)}:${outcome.intent.emitter.id}`;
|
|
1671
|
+
resetEpochByEmitter.get(entry.runtime)?.set(baseKey, outcome.resetEpoch);
|
|
1672
|
+
resetEpochByIntent.get(entry.runtime)?.delete(outcome.intent.sequence);
|
|
1673
|
+
}
|
|
1674
|
+
} else if (outcome.state === "skipped" && outcome.resetEpoch !== void 0) {
|
|
1675
|
+
resetEpochByIntent.get(entry.runtime)?.delete(outcome.intent.sequence);
|
|
1016
1676
|
}
|
|
1017
1677
|
}
|
|
1018
|
-
|
|
1019
|
-
|
|
1678
|
+
if (acknowledgedSequences.length > 0) {
|
|
1679
|
+
entry.runtime.commit(acknowledgedSequences, publishedSequences);
|
|
1680
|
+
}
|
|
1020
1681
|
}
|
|
1021
|
-
|
|
1682
|
+
},
|
|
1683
|
+
onFrameAborted: (frame) => {
|
|
1684
|
+
pendingFrames.delete(frame.frameNumber);
|
|
1022
1685
|
}
|
|
1023
1686
|
};
|
|
1687
|
+
return feature;
|
|
1024
1688
|
}
|
|
1025
1689
|
function duplicate(token, providerId) {
|
|
1026
1690
|
return {
|
|
@@ -1045,11 +1709,22 @@ function availableProvider(token, kind, bindingType, source) {
|
|
|
1045
1709
|
detail: { token, providerId: `${token}-provider` }
|
|
1046
1710
|
});
|
|
1047
1711
|
}
|
|
1712
|
+
const prepared = source.resource?.(generation);
|
|
1713
|
+
if (prepared === void 0) {
|
|
1714
|
+
return err({
|
|
1715
|
+
code: "vfx-data-interface-missing",
|
|
1716
|
+
expected: `a resident ${kind} resource for ${token}`,
|
|
1717
|
+
hint: `provide the generation-owned ${kind} resource before rendering`,
|
|
1718
|
+
detail: { token, providerId: `${token}-provider` }
|
|
1719
|
+
});
|
|
1720
|
+
}
|
|
1048
1721
|
const resource = {
|
|
1049
1722
|
token,
|
|
1050
1723
|
kind,
|
|
1051
1724
|
bindingType,
|
|
1052
|
-
generation
|
|
1725
|
+
generation,
|
|
1726
|
+
...source.sampleCount === void 0 ? {} : { sampleCount: source.sampleCount },
|
|
1727
|
+
resource: prepared
|
|
1053
1728
|
};
|
|
1054
1729
|
return ok(resource);
|
|
1055
1730
|
}
|
|
@@ -1061,6 +1736,9 @@ function createCameraProvider(source) {
|
|
|
1061
1736
|
function createSceneDepthProvider(source) {
|
|
1062
1737
|
return availableProvider("vfx:scene-depth", "scene-depth", "sampled-depth", source);
|
|
1063
1738
|
}
|
|
1739
|
+
function createNoiseProvider(source) {
|
|
1740
|
+
return availableProvider("vfx:noise", "noise", "sampled-float", source);
|
|
1741
|
+
}
|
|
1064
1742
|
function createVfxDataInterfaceRegistry(initialProviders = []) {
|
|
1065
1743
|
const entries = /* @__PURE__ */ new Map();
|
|
1066
1744
|
let registrationError;
|
|
@@ -1221,6 +1899,33 @@ function createVfxRuntimeHost(options) {
|
|
|
1221
1899
|
}
|
|
1222
1900
|
return ok(action(world.getResource(VFX_GPU_RUNTIME_RESOURCE_KEY)));
|
|
1223
1901
|
};
|
|
1902
|
+
const withInstance = (player, action) => {
|
|
1903
|
+
const result = withRuntime(player, (runtime) => {
|
|
1904
|
+
const instance = runtime.getInstance(player);
|
|
1905
|
+
if (instance === void 0) {
|
|
1906
|
+
return { kind: "error", causeCode: "vfx-instance-unavailable" };
|
|
1907
|
+
}
|
|
1908
|
+
const outcome = action(instance);
|
|
1909
|
+
return outcome.ok ? { kind: "value", value: outcome.value } : { kind: "error", causeCode: outcome.error.code };
|
|
1910
|
+
});
|
|
1911
|
+
if (!result.ok) return result;
|
|
1912
|
+
if (result.value.kind === "error") {
|
|
1913
|
+
return err(
|
|
1914
|
+
controlFailure(
|
|
1915
|
+
"vfx-host-control-instance-rejected",
|
|
1916
|
+
"the live typed VFX instance to accept this control input",
|
|
1917
|
+
"run one fixed tick or repair the reflected instance contract before retrying",
|
|
1918
|
+
{
|
|
1919
|
+
requestedGeneration,
|
|
1920
|
+
currentGeneration: requestedGeneration,
|
|
1921
|
+
player,
|
|
1922
|
+
causeCode: result.value.causeCode
|
|
1923
|
+
}
|
|
1924
|
+
)
|
|
1925
|
+
);
|
|
1926
|
+
}
|
|
1927
|
+
return ok(result.value.value);
|
|
1928
|
+
};
|
|
1224
1929
|
const control = {
|
|
1225
1930
|
generation: requestedGeneration,
|
|
1226
1931
|
replay: ({ player, replayInput }) => withRuntime(player, (runtime) => {
|
|
@@ -1248,6 +1953,21 @@ function createVfxRuntimeHost(options) {
|
|
|
1248
1953
|
state: enabled ? "enabled" : "paused",
|
|
1249
1954
|
generation: requestedGeneration
|
|
1250
1955
|
});
|
|
1956
|
+
}),
|
|
1957
|
+
patchPlayerParameters: ({ player, values }) => withInstance(player, (instance) => {
|
|
1958
|
+
const patched = instance.patch(values);
|
|
1959
|
+
if (!patched.ok) return patched;
|
|
1960
|
+
return ok({
|
|
1961
|
+
state: "queued",
|
|
1962
|
+
generation: requestedGeneration,
|
|
1963
|
+
parameterGeneration: instance.generation,
|
|
1964
|
+
pendingPatchCount: instance.pendingPatchCount
|
|
1965
|
+
});
|
|
1966
|
+
}),
|
|
1967
|
+
submitChannel: ({ player, channel, payload, sequence }) => withInstance(player, (instance) => {
|
|
1968
|
+
const submitted = instance.submit({ channel, payload, sequence });
|
|
1969
|
+
if (!submitted.ok) return submitted;
|
|
1970
|
+
return ok({ state: "queued", generation: requestedGeneration });
|
|
1251
1971
|
})
|
|
1252
1972
|
};
|
|
1253
1973
|
return ok(Object.freeze(control));
|
|
@@ -1269,7 +1989,7 @@ function createVfxRuntimeHost(options) {
|
|
|
1269
1989
|
return err(
|
|
1270
1990
|
failure2(
|
|
1271
1991
|
"vfx-host-loader-install-failed",
|
|
1272
|
-
"the
|
|
1992
|
+
"the Program v3 VFX loader to be registered once",
|
|
1273
1993
|
"remove a conflicting particle-effect loader and retry attachWorld",
|
|
1274
1994
|
cause
|
|
1275
1995
|
)
|
|
@@ -1348,6 +2068,6 @@ function createVfxRuntimeHost(options) {
|
|
|
1348
2068
|
};
|
|
1349
2069
|
}
|
|
1350
2070
|
|
|
1351
|
-
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 };
|
|
2071
|
+
export { PARTICLE_INPUT_SHADER_IDENTIFIERS, PARTICLE_SHADER_IDENTIFIERS, VFX_EVENT_BYTES, VFX_EVENT_COUNTER_BYTES, VFX_EVENT_INPUT_BYTES, createCameraProvider, createNoiseProvider, createSceneDepthProvider, createTopologyResourcePlan, createVfxDataInterfaceRegistry, createVfxRenderInspectSnapshot, createVfxRuntimeHost, encodeEventBuffer, encodeEventInputs, eventCapacity, eventCounterData, eventInputCapacity, gpuParticleRenderFeature, installVfxRuntimeDecoder, observeStagePlan, prepareParticleMaterialInputs, resolveBillboardAdvancedState, stageDispatches, stageRecoveryReadiness, topologyCapacitySnapshot, topologyRecoveryHint, validatedStagePlan };
|
|
1352
2072
|
//# sourceMappingURL=index.mjs.map
|
|
1353
2073
|
//# sourceMappingURL=index.mjs.map
|