@forgeax/engine-vfx-render 0.1.4 → 0.1.7
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 +6 -12
- package/dist/.tsbuildinfo +1 -1
- package/dist/feature/gpu-particle-feature.d.ts +20 -26
- package/dist/feature/gpu-particle-feature.d.ts.map +1 -1
- package/dist/feature/particle-resources.d.ts +0 -3
- package/dist/feature/particle-resources.d.ts.map +1 -1
- package/dist/feature/stage-plan.d.ts +1 -1
- package/dist/feature/stage-plan.d.ts.map +1 -1
- package/dist/host/vfx-runtime-host.d.ts +20 -4
- package/dist/host/vfx-runtime-host.d.ts.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +458 -722
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -8
- package/src/__tests__/billboard-advanced.integration.test.ts +0 -23
- package/src/__tests__/fog-producer.integration.test.ts +4 -3
- package/src/__tests__/gpu-host.integration.test.ts +133 -115
- package/src/__tests__/particle-resources.unit.test.ts +0 -9
- package/src/__tests__/stage-plan.integration.test.ts +132 -0
- package/src/feature/gpu-particle-feature.ts +482 -888
- package/src/feature/particle-resources.ts +0 -8
- package/src/feature/stage-plan.ts +4 -1
- package/src/host/vfx-runtime-host.ts +81 -24
- package/src/index.ts +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { frustum } from '@forgeax/engine-math';
|
|
2
|
-
import { RenderFeatureStageFailedError, RENDER_FEATURE_VERTEX_LAYOUTS, RenderFeaturePreparationFailedError } from '@forgeax/engine-render';
|
|
3
2
|
import { Transform } from '@forgeax/engine-scene';
|
|
4
3
|
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
|
|
4
|
+
import { VFX_GPU_RUNTIME_RESOURCE_KEY, vfxGpuEffectContribution, vfxGpuEffectPackLoader, vfxGpuRuntimePlugin, resolveVfxDataInterfaces, ParticleEffectPlayer } from '@forgeax/engine-vfx';
|
|
5
|
+
import { getAssetRegistryResolver } from '@forgeax/engine-assets-runtime';
|
|
7
6
|
import { createWorldContext } from '@forgeax/engine-ecs';
|
|
8
7
|
|
|
9
8
|
// src/feature/event-resources.ts
|
|
@@ -59,6 +58,34 @@ function encodeEventInputs(intent) {
|
|
|
59
58
|
function eventCounterData() {
|
|
60
59
|
return new Uint8Array(VFX_EVENT_COUNTER_BYTES);
|
|
61
60
|
}
|
|
61
|
+
|
|
62
|
+
// ../render/src/errors/render.ts
|
|
63
|
+
var renderFeatureRecoveryHintByRecovery = {
|
|
64
|
+
"next-frame": (featureIdentity, stage) => `correct '${featureIdentity}' ${stage} data and retry on the next frame`,
|
|
65
|
+
"renderer-recover": (featureIdentity, _stage) => `wait for renderer recovery before retrying '${featureIdentity}'`,
|
|
66
|
+
registration: (featureIdentity, _stage) => `correct '${featureIdentity}' registration before retrying`
|
|
67
|
+
};
|
|
68
|
+
var RenderFeatureStageFailedError = class extends Error {
|
|
69
|
+
code = "render-feature-stage-failed";
|
|
70
|
+
expected;
|
|
71
|
+
hint;
|
|
72
|
+
detail;
|
|
73
|
+
constructor(featureIdentity, order, stage, recovery) {
|
|
74
|
+
const expected = `feature '${featureIdentity}' completes its ${stage} stage without an error`;
|
|
75
|
+
const hint = renderFeatureRecoveryHintByRecovery[recovery](featureIdentity, stage);
|
|
76
|
+
super(`render feature '${featureIdentity}' failed during ${stage}`);
|
|
77
|
+
this.name = "RenderFeatureStageFailedError";
|
|
78
|
+
this.expected = expected;
|
|
79
|
+
this.hint = hint;
|
|
80
|
+
this.detail = { featureIdentity, order, stage, recovery };
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
var RENDER_FEATURE_VERTEX_LAYOUTS = Object.freeze({
|
|
84
|
+
positionSizeColorInstance: "position-size-color-instance",
|
|
85
|
+
billboardMaterialInstance: "billboard-material-instance",
|
|
86
|
+
topologySegmentInstance: "topology-segment-instance",
|
|
87
|
+
meshGeometryMaterialInstance: "mesh-geometry-material-instance"
|
|
88
|
+
});
|
|
62
89
|
var PARTICLE_SHADER_IDENTIFIERS = Object.freeze({
|
|
63
90
|
billboard: "forgeax::vfx-render.particles.billboard",
|
|
64
91
|
mesh: "forgeax::vfx-render.particles.mesh",
|
|
@@ -144,9 +171,6 @@ function particleMaterialPass(kind, material) {
|
|
|
144
171
|
...pass?.renderState === void 0 ? {} : { renderState: pass.renderState }
|
|
145
172
|
};
|
|
146
173
|
}
|
|
147
|
-
function particleMaterialUsesSceneDepth(contract) {
|
|
148
|
-
return contract === "group-0-resource" || contract === "view-with-resource";
|
|
149
|
-
}
|
|
150
174
|
function particleMaterialUsesBindings(material) {
|
|
151
175
|
return (material?.parameters?.length ?? 0) > 0;
|
|
152
176
|
}
|
|
@@ -164,19 +188,19 @@ function canonicalMeshVertices(mesh) {
|
|
|
164
188
|
const tangents = floatAttribute(mesh.attributes.tangent);
|
|
165
189
|
const result = new Float32Array(vertexCount * 12);
|
|
166
190
|
for (let index = 0; index < vertexCount; index += 1) {
|
|
167
|
-
const
|
|
168
|
-
result.set(positions.subarray(index * 3, index * 3 + 3),
|
|
191
|
+
const target = index * 12;
|
|
192
|
+
result.set(positions.subarray(index * 3, index * 3 + 3), target);
|
|
169
193
|
result.set(
|
|
170
194
|
normals.length >= index * 3 + 3 ? normals.subarray(index * 3, index * 3 + 3) : [0, 0, 1],
|
|
171
|
-
|
|
195
|
+
target + 3
|
|
172
196
|
);
|
|
173
197
|
result.set(
|
|
174
198
|
uvs.length >= index * 2 + 2 ? uvs.subarray(index * 2, index * 2 + 2) : [0, 0],
|
|
175
|
-
|
|
199
|
+
target + 6
|
|
176
200
|
);
|
|
177
201
|
result.set(
|
|
178
202
|
tangents.length >= index * 4 + 4 ? tangents.subarray(index * 4, index * 4 + 4) : [1, 0, 0, 1],
|
|
179
|
-
|
|
203
|
+
target + 8
|
|
180
204
|
);
|
|
181
205
|
}
|
|
182
206
|
return result;
|
|
@@ -374,13 +398,13 @@ var BILLBOARD_INSTANCE_BYTES = 31 * 4;
|
|
|
374
398
|
var MESH_INSTANCE_BYTES = 28 * 4;
|
|
375
399
|
var COUNTERS_BYTES = 24;
|
|
376
400
|
var RUNTIME_BYTES = 72 * 4;
|
|
377
|
-
var MAX_TICK_RINGS = 8;
|
|
378
401
|
var IDENTITY_MATRIX = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
|
|
379
402
|
function createVfxRenderInspectSnapshot(input) {
|
|
380
403
|
return {
|
|
381
404
|
topology: input.topology,
|
|
382
405
|
counters: { capacity: input.capacity, produced: input.produced, dropped: input.dropped },
|
|
383
406
|
stageReadiness: input.stageReadiness,
|
|
407
|
+
stageOutput: input.stageOutput ?? "empty",
|
|
384
408
|
providerReadiness: input.providerReadiness,
|
|
385
409
|
gpuTiming: input.gpuTiming
|
|
386
410
|
};
|
|
@@ -486,7 +510,7 @@ function emitterVisible(intent, camera, localToWorld) {
|
|
|
486
510
|
(bounds.max[2] - bounds.min[2]) * 0.5
|
|
487
511
|
);
|
|
488
512
|
const matrix = (index) => localToWorld[index] ?? 0;
|
|
489
|
-
const
|
|
513
|
+
const worldCenter = new Float32Array([
|
|
490
514
|
matrix(0) * center[0] + matrix(4) * center[1] + matrix(8) * center[2] + matrix(12),
|
|
491
515
|
matrix(1) * center[0] + matrix(5) * center[1] + matrix(9) * center[2] + matrix(13),
|
|
492
516
|
matrix(2) * center[0] + matrix(6) * center[1] + matrix(10) * center[2] + matrix(14)
|
|
@@ -497,81 +521,53 @@ function emitterVisible(intent, camera, localToWorld) {
|
|
|
497
521
|
Math.hypot(matrix(8), matrix(9), matrix(10))
|
|
498
522
|
);
|
|
499
523
|
const planes = frustum.fromViewProjection(frustum.create(), camera.viewProjection);
|
|
500
|
-
return frustum.intersectsSphere(planes,
|
|
524
|
+
return frustum.intersectsSphere(planes, worldCenter, radius * scale);
|
|
501
525
|
}
|
|
502
526
|
function resetData(size) {
|
|
503
527
|
return new Uint8Array(size);
|
|
504
528
|
}
|
|
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
529
|
function requiresSceneDepth(intent) {
|
|
523
530
|
return (intent.emitter.reflection.dataInterfaces ?? []).some(
|
|
524
531
|
(requirement) => requirement.kind === "scene-depth"
|
|
525
532
|
);
|
|
526
533
|
}
|
|
527
|
-
function
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
534
|
+
function planFailure() {
|
|
535
|
+
return new RenderFeatureStageFailedError(IDENTITY, -1, "plan", "next-frame");
|
|
536
|
+
}
|
|
537
|
+
function planName(value, maxLength = 24) {
|
|
538
|
+
const normalized = value.toLowerCase().replaceAll(/[^a-z0-9.-]/g, "-");
|
|
539
|
+
return (normalized.length === 0 ? "unnamed" : normalized).slice(0, maxLength);
|
|
540
|
+
}
|
|
541
|
+
function computeBindingEntries(intent, resources) {
|
|
542
|
+
const declared = new Set(
|
|
543
|
+
(intent.emitter.reflection.bindings[0]?.entries ?? []).filter((entry) => entry.buffer !== void 0).map((entry) => entry.binding)
|
|
544
|
+
);
|
|
545
|
+
return Object.entries(resources).flatMap(
|
|
546
|
+
([binding, resource]) => declared.has(Number(binding)) ? [{ binding: Number(binding), resource }] : []
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
function simulationDispatches(intent, stages) {
|
|
550
|
+
const groups = Math.max(1, Math.ceil(intent.emitter.capacity / WORKGROUP_SIZE));
|
|
551
|
+
return [
|
|
552
|
+
{ kind: "direct", entryPoint: "forgeax_vfx_spawn_main", workgroups: [groups] },
|
|
553
|
+
{ kind: "direct", entryPoint: "forgeax_vfx_update_main", workgroups: [groups] },
|
|
554
|
+
...stages.stages.map((stage) => ({
|
|
555
|
+
kind: "direct",
|
|
556
|
+
entryPoint: stage.entryPoint,
|
|
557
|
+
workgroups: [groups]
|
|
558
|
+
})),
|
|
559
|
+
{ kind: "direct", entryPoint: "forgeax_vfx_scan_blocks_main", workgroups: [groups] },
|
|
560
|
+
{ kind: "direct", entryPoint: "forgeax_vfx_scan_block_offsets_main", workgroups: [1] },
|
|
561
|
+
{ kind: "direct", entryPoint: "forgeax_vfx_add_offsets_main", workgroups: [groups] },
|
|
562
|
+
{ kind: "direct", entryPoint: "forgeax_vfx_compact_main", workgroups: [groups] },
|
|
563
|
+
{
|
|
564
|
+
kind: "direct",
|
|
565
|
+
entryPoint: "forgeax_vfx_event_main",
|
|
566
|
+
workgroups: [Math.max(1, Math.ceil(eventInputCapacity(intent.emitter) / 64))]
|
|
551
567
|
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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
|
-
};
|
|
568
|
+
];
|
|
569
|
+
}
|
|
570
|
+
function gpuParticleRenderFeature(options) {
|
|
575
571
|
return {
|
|
576
572
|
identity: IDENTITY,
|
|
577
573
|
requiredCapabilities: ["compute", "indirectDrawing"],
|
|
@@ -586,676 +582,399 @@ function gpuParticleRenderFeature(options) {
|
|
|
586
582
|
const intents = runtime.snapshot().filter((intent) => {
|
|
587
583
|
if (options.playerConsumption?.isEnabled(world, intent.player) === false) return false;
|
|
588
584
|
const requirements = intent.emitter.reflection.dataInterfaces ?? [];
|
|
589
|
-
|
|
590
|
-
return options.dataInterfaces?.resolve(requirements, intent.instanceGeneration).ok === true;
|
|
585
|
+
return requirements.length === 0 || options.dataInterfaces?.resolve(requirements, intent.instanceGeneration).ok === true;
|
|
591
586
|
});
|
|
592
587
|
extracted.push({ world, runtime, camera, intents });
|
|
593
588
|
}
|
|
594
589
|
return ok({ worlds: extracted, frameNumber: context.frameNumber });
|
|
595
590
|
},
|
|
596
|
-
|
|
597
|
-
const
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
const
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
const
|
|
606
|
-
|
|
607
|
-
|
|
591
|
+
plan: (frame, context) => {
|
|
592
|
+
const resources = [];
|
|
593
|
+
const passes = [];
|
|
594
|
+
const dispatchedIntents = /* @__PURE__ */ new Set();
|
|
595
|
+
const colorTarget = context.targets.find((candidate) => candidate.kind === "color") ?? context.targets.find((candidate) => candidate.kind === "swapchain");
|
|
596
|
+
const depthTarget = context.targets.find((candidate) => candidate.kind === "depth");
|
|
597
|
+
for (const [worldIndex, entry] of frame.worlds.entries()) {
|
|
598
|
+
for (const [intentIndex, intent] of entry.intents.entries()) {
|
|
599
|
+
if (!entry.runtime.isEmitterSessionEnabled(intent.player, intent.emitter.id)) continue;
|
|
600
|
+
const localToWorld = emitterTransform(entry.world, intent);
|
|
601
|
+
const visible = emitterVisible(intent, entry.camera, localToWorld);
|
|
602
|
+
entry.runtime.setEmitterCameraVisibility(intent.player, intent.emitter.id, visible);
|
|
603
|
+
if (!visible) continue;
|
|
604
|
+
if (requiresSceneDepth(intent) && depthTarget === void 0) continue;
|
|
605
|
+
const stagePlan = validatedStagePlan(
|
|
608
606
|
intent.emitter.reflection.stages,
|
|
609
607
|
intent.instanceGeneration
|
|
610
608
|
);
|
|
611
|
-
if (!
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
}
|
|
629
|
-
state ??= stateFor(entry.world, intent);
|
|
630
|
-
const observation = observeStagePlan(
|
|
631
|
-
candidate,
|
|
632
|
-
intent.instanceGeneration,
|
|
633
|
-
state.lastKnownGoodStage
|
|
609
|
+
if (!stagePlan.ok) return err(planFailure());
|
|
610
|
+
const prefix = `vfx.w-${worldIndex}.i-${intentIndex}.${planName(intent.emitter.id)}`;
|
|
611
|
+
const program = `${prefix}.compute-program`;
|
|
612
|
+
const particles = `${prefix}.particles`;
|
|
613
|
+
const runtime = `${prefix}.runtime`;
|
|
614
|
+
const aliveIndices = `${prefix}.alive-indices`;
|
|
615
|
+
const counters = `${prefix}.counters`;
|
|
616
|
+
const indirect = `${prefix}.indirect`;
|
|
617
|
+
const scratch = `${prefix}.scratch`;
|
|
618
|
+
const sharedInstances = `${prefix}.shared-instances`;
|
|
619
|
+
const eventInputs = `${prefix}.event-inputs`;
|
|
620
|
+
const events = `${prefix}.events`;
|
|
621
|
+
const bindings = `${prefix}.simulation-bindings`;
|
|
622
|
+
const capacity = intent.emitter.capacity;
|
|
623
|
+
const renderers = intent.emitter.renderers;
|
|
624
|
+
const meshes = renderers.map(
|
|
625
|
+
(renderer) => renderer.kind === "mesh" ? options.mesh?.read(entry.world, renderer.mesh) : void 0
|
|
634
626
|
);
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
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;
|
|
627
|
+
const indirectWords = new Uint32Array(Math.max(1, renderers.length) * 5);
|
|
628
|
+
for (const [rendererIndex, renderer] of renderers.entries()) {
|
|
629
|
+
const mesh = meshes[rendererIndex];
|
|
630
|
+
const submesh = renderer.kind === "mesh" ? mesh?.submeshes[renderer.submesh ?? 0] : void 0;
|
|
631
|
+
if (renderer.kind === "mesh" && submesh === void 0) return err(planFailure());
|
|
632
|
+
if ((renderer.kind === "ribbon" || renderer.kind === "trail" || renderer.kind === "beam") && !createTopologyResourcePlan(renderer).ok) {
|
|
633
|
+
return err(planFailure());
|
|
647
634
|
}
|
|
648
|
-
|
|
635
|
+
indirectWords[rendererIndex * 5] = renderer.kind === "mesh" ? mesh?.indices === void 0 ? submesh?.vertexCount ?? 0 : submesh?.indexCount ?? 0 : 6;
|
|
636
|
+
indirectWords[rendererIndex * 5 + 2] = renderer.kind === "mesh" && mesh?.indices !== void 0 ? submesh?.indexOffset ?? 0 : 0;
|
|
649
637
|
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
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)
|
|
638
|
+
const scratchBytes = (capacity * 2 + Math.ceil(capacity / WORKGROUP_SIZE)) * 4;
|
|
639
|
+
const eventInputBytes = Math.max(
|
|
640
|
+
4,
|
|
641
|
+
eventInputCapacity(intent.emitter) * VFX_EVENT_INPUT_BYTES
|
|
872
642
|
);
|
|
873
|
-
const
|
|
874
|
-
|
|
875
|
-
|
|
643
|
+
const eventBytes = Math.max(4, eventCapacity(intent.emitter) * VFX_EVENT_BYTES);
|
|
644
|
+
resources.push(
|
|
645
|
+
{
|
|
646
|
+
kind: "compute-program",
|
|
647
|
+
name: program,
|
|
648
|
+
program: {
|
|
649
|
+
wgsl: intent.emitter.wgsl,
|
|
650
|
+
entryPoints: intent.emitter.reflection.entryPoints,
|
|
651
|
+
bindings: intent.emitter.reflection.bindings
|
|
652
|
+
}
|
|
653
|
+
},
|
|
654
|
+
{
|
|
655
|
+
kind: "buffer",
|
|
656
|
+
name: particles,
|
|
657
|
+
size: capacity * PARTICLE_BYTES,
|
|
658
|
+
usage: ["storage"],
|
|
659
|
+
...intent.reset ? { data: resetData(capacity * PARTICLE_BYTES) } : {}
|
|
660
|
+
},
|
|
661
|
+
{ kind: "buffer", name: aliveIndices, size: capacity * 4, usage: ["storage"] },
|
|
662
|
+
{
|
|
663
|
+
kind: "buffer",
|
|
664
|
+
name: counters,
|
|
665
|
+
size: COUNTERS_BYTES,
|
|
666
|
+
usage: ["storage"],
|
|
667
|
+
...intent.reset ? { data: resetData(COUNTERS_BYTES) } : {}
|
|
668
|
+
},
|
|
876
669
|
{
|
|
877
|
-
|
|
670
|
+
kind: "buffer",
|
|
671
|
+
name: indirect,
|
|
672
|
+
size: indirectWords.byteLength,
|
|
673
|
+
usage: ["storage", "indirect"],
|
|
674
|
+
data: indirectWords
|
|
675
|
+
},
|
|
676
|
+
{
|
|
677
|
+
kind: "buffer",
|
|
678
|
+
name: scratch,
|
|
679
|
+
size: scratchBytes,
|
|
680
|
+
usage: ["storage"],
|
|
681
|
+
...intent.reset ? { data: resetData(scratchBytes) } : {}
|
|
682
|
+
},
|
|
683
|
+
{
|
|
684
|
+
kind: "buffer",
|
|
685
|
+
name: sharedInstances,
|
|
686
|
+
size: capacity * Math.max(BILLBOARD_INSTANCE_BYTES, MESH_INSTANCE_BYTES),
|
|
878
687
|
usage: ["storage", "vertex"]
|
|
879
|
-
}
|
|
880
|
-
);
|
|
881
|
-
if (!projectionInstances.ok) return projectionInstances;
|
|
882
|
-
const projectionHistory = gpu.prepareBuffer(
|
|
883
|
-
`${state.names}.renderer.${rendererIndex}.history`,
|
|
688
|
+
},
|
|
884
689
|
{
|
|
885
|
-
|
|
690
|
+
kind: "buffer",
|
|
691
|
+
name: eventInputs,
|
|
692
|
+
size: eventInputBytes,
|
|
886
693
|
usage: ["storage"],
|
|
887
|
-
|
|
888
|
-
|
|
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`,
|
|
694
|
+
data: encodeEventInputs(intent)
|
|
695
|
+
},
|
|
897
696
|
{
|
|
697
|
+
kind: "buffer",
|
|
698
|
+
name: events,
|
|
699
|
+
size: eventBytes,
|
|
700
|
+
usage: ["storage"],
|
|
701
|
+
...intent.reset ? { data: resetData(eventBytes) } : {}
|
|
702
|
+
},
|
|
703
|
+
{
|
|
704
|
+
kind: "buffer",
|
|
705
|
+
name: runtime,
|
|
898
706
|
size: RUNTIME_BYTES,
|
|
899
707
|
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
|
|
708
|
+
data: runtimeData(intent, entry.camera, void 0, localToWorld)
|
|
935
709
|
},
|
|
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
710
|
{
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
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
|
-
}
|
|
711
|
+
kind: "compute-bindings",
|
|
712
|
+
name: bindings,
|
|
713
|
+
program,
|
|
714
|
+
entries: computeBindingEntries(intent, {
|
|
715
|
+
0: particles,
|
|
716
|
+
1: runtime,
|
|
717
|
+
2: aliveIndices,
|
|
718
|
+
3: counters,
|
|
719
|
+
4: indirect,
|
|
720
|
+
5: scratch,
|
|
721
|
+
6: sharedInstances,
|
|
722
|
+
8: eventInputs,
|
|
723
|
+
9: events
|
|
724
|
+
})
|
|
991
725
|
}
|
|
992
726
|
);
|
|
993
|
-
|
|
994
|
-
const
|
|
995
|
-
|
|
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
|
-
}
|
|
727
|
+
const entryPoints = new Set(intent.emitter.reflection.entryPoints);
|
|
728
|
+
const dispatches = simulationDispatches(intent, stagePlan.value).filter(
|
|
729
|
+
(dispatch) => entryPoints.has(dispatch.entryPoint)
|
|
1006
730
|
);
|
|
1007
|
-
if (
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
731
|
+
if (dispatches.length > 0) {
|
|
732
|
+
passes.push({
|
|
733
|
+
kind: "compute",
|
|
734
|
+
name: `${prefix}.simulate`,
|
|
735
|
+
program,
|
|
736
|
+
bindings,
|
|
737
|
+
dispatches
|
|
738
|
+
});
|
|
739
|
+
dispatchedIntents.add(intent);
|
|
740
|
+
}
|
|
741
|
+
for (const [rendererIndex, renderer] of renderers.entries()) {
|
|
742
|
+
const rendererPrefix = `${prefix}.renderer-${rendererIndex}`;
|
|
743
|
+
const isBillboard = renderer.kind === "billboard";
|
|
744
|
+
const isTopology = renderer.kind === "ribbon" || renderer.kind === "trail" || renderer.kind === "beam";
|
|
745
|
+
const topologyPlan = isTopology ? createTopologyResourcePlan(renderer) : void 0;
|
|
746
|
+
if (topologyPlan !== void 0 && !topologyPlan.ok) return err(planFailure());
|
|
747
|
+
const material = options.material?.read(entry.world, renderer.material);
|
|
748
|
+
const materialPass = particleMaterialPass(renderer.kind, material);
|
|
749
|
+
const mesh = meshes[rendererIndex];
|
|
750
|
+
const submesh = renderer.kind === "mesh" ? mesh?.submeshes[renderer.submesh ?? 0] : void 0;
|
|
751
|
+
const indexFormat = mesh?.indices instanceof Uint32Array ? "uint32" : "uint16";
|
|
752
|
+
const instances = `${rendererPrefix}.instances`;
|
|
753
|
+
const history = `${rendererPrefix}.history`;
|
|
754
|
+
const projectionRuntime = `${rendererPrefix}.runtime`;
|
|
755
|
+
const projectionBindings = `${rendererPrefix}.compute-bindings`;
|
|
756
|
+
const vertexLayout = isBillboard ? RENDER_FEATURE_VERTEX_LAYOUTS.billboardMaterialInstance : isTopology ? RENDER_FEATURE_VERTEX_LAYOUTS.topologySegmentInstance : RENDER_FEATURE_VERTEX_LAYOUTS.meshGeometryMaterialInstance;
|
|
757
|
+
const instanceBytes = isTopology ? topologyPlan?.value.vertexBytes ?? 16 : capacity * (isBillboard ? BILLBOARD_INSTANCE_BYTES : MESH_INSTANCE_BYTES);
|
|
758
|
+
const historyBytes = renderer.kind === "trail" ? Math.max(16, renderer.capacity * renderer.historyLength * 16) : 16;
|
|
759
|
+
resources.push(
|
|
760
|
+
{
|
|
761
|
+
kind: "buffer",
|
|
762
|
+
name: instances,
|
|
763
|
+
size: instanceBytes,
|
|
764
|
+
usage: ["storage", "vertex"]
|
|
765
|
+
},
|
|
1015
766
|
{
|
|
1016
|
-
|
|
1017
|
-
|
|
767
|
+
kind: "buffer",
|
|
768
|
+
name: history,
|
|
769
|
+
size: historyBytes,
|
|
770
|
+
usage: ["storage"],
|
|
771
|
+
...intent.reset ? { data: resetData(historyBytes) } : {}
|
|
772
|
+
},
|
|
773
|
+
{
|
|
774
|
+
kind: "buffer",
|
|
775
|
+
name: projectionRuntime,
|
|
776
|
+
size: RUNTIME_BYTES,
|
|
777
|
+
usage: ["uniform"],
|
|
778
|
+
data: runtimeData(
|
|
779
|
+
{ ...intent, fixedDelta: 0, spawnCount: 0 },
|
|
780
|
+
entry.camera,
|
|
781
|
+
material,
|
|
782
|
+
localToWorld,
|
|
783
|
+
renderer,
|
|
784
|
+
rendererIndex
|
|
785
|
+
)
|
|
786
|
+
},
|
|
787
|
+
{
|
|
788
|
+
kind: "compute-bindings",
|
|
789
|
+
name: projectionBindings,
|
|
790
|
+
program,
|
|
791
|
+
entries: computeBindingEntries(intent, {
|
|
792
|
+
0: particles,
|
|
793
|
+
1: projectionRuntime,
|
|
794
|
+
2: aliveIndices,
|
|
795
|
+
3: counters,
|
|
796
|
+
4: indirect,
|
|
797
|
+
5: history,
|
|
798
|
+
6: instances,
|
|
799
|
+
8: eventInputs,
|
|
800
|
+
9: events
|
|
801
|
+
})
|
|
1018
802
|
}
|
|
1019
803
|
);
|
|
1020
|
-
|
|
1021
|
-
const
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
804
|
+
const projectionDispatches = [];
|
|
805
|
+
const pushProjection = (entryPoint, workgroups) => {
|
|
806
|
+
if (!entryPoints.has(entryPoint)) return;
|
|
807
|
+
projectionDispatches.push({
|
|
808
|
+
kind: "direct",
|
|
809
|
+
entryPoint,
|
|
810
|
+
workgroups: [Math.max(1, workgroups)]
|
|
811
|
+
});
|
|
1027
812
|
};
|
|
1028
|
-
(
|
|
1029
|
-
|
|
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
|
|
813
|
+
if (isBillboard && renderer.sorting === "back-to-front") {
|
|
814
|
+
pushProjection("forgeax_vfx_sort_main", 1);
|
|
1041
815
|
}
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
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
|
|
816
|
+
if (renderer.kind === "trail") {
|
|
817
|
+
pushProjection(
|
|
818
|
+
"forgeax_vfx_trail_history_main",
|
|
819
|
+
Math.ceil(renderer.capacity / WORKGROUP_SIZE)
|
|
820
|
+
);
|
|
1071
821
|
}
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
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 }
|
|
822
|
+
const projectionCount = renderer.kind === "trail" ? renderer.capacity * Math.max(1, renderer.historyLength - 1) : isTopology ? renderer.capacity : capacity;
|
|
823
|
+
pushProjection(
|
|
824
|
+
renderer.kind === "billboard" ? "forgeax_vfx_billboard_main" : renderer.kind === "mesh" ? "forgeax_vfx_mesh_main" : `forgeax_vfx_${renderer.kind}_main`,
|
|
825
|
+
Math.ceil(projectionCount / WORKGROUP_SIZE)
|
|
826
|
+
);
|
|
827
|
+
if (projectionDispatches.length > 0) {
|
|
828
|
+
passes.push({
|
|
829
|
+
kind: "compute",
|
|
830
|
+
name: `${rendererPrefix}.project`,
|
|
831
|
+
program,
|
|
832
|
+
bindings: projectionBindings,
|
|
833
|
+
dispatches: projectionDispatches
|
|
834
|
+
});
|
|
1092
835
|
}
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
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
|
|
836
|
+
const graphicsProgram = `${rendererPrefix}.graphics-program`;
|
|
837
|
+
const graphicsBindings = `${rendererPrefix}.graphics-bindings`;
|
|
838
|
+
const vertexData = `${rendererPrefix}.vertex-data`;
|
|
839
|
+
const renderState = isBillboard && depthTarget !== void 0 ? { ...materialPass.renderState ?? {}, depthWriteEnabled: false } : materialPass.renderState;
|
|
840
|
+
resources.push(
|
|
841
|
+
{
|
|
842
|
+
kind: "graphics-program",
|
|
843
|
+
name: graphicsProgram,
|
|
844
|
+
program: {
|
|
845
|
+
shader: materialPass.shader,
|
|
846
|
+
vertexLayout,
|
|
847
|
+
colorFormats: [colorTarget?.format ?? "rgba8unorm-srgb"],
|
|
848
|
+
...depthTarget === void 0 ? {} : { depthFormat: depthTarget.format },
|
|
849
|
+
sampleCount: colorTarget?.sampleCount ?? 1,
|
|
850
|
+
topology: submesh?.topology ?? "triangle-list",
|
|
851
|
+
...mesh?.indices === void 0 ? {} : { indexFormat },
|
|
852
|
+
...renderState === void 0 ? {} : { renderState }
|
|
853
|
+
}
|
|
1133
854
|
},
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
855
|
+
{
|
|
856
|
+
kind: "graphics-bindings",
|
|
857
|
+
name: graphicsBindings,
|
|
858
|
+
program: graphicsProgram,
|
|
859
|
+
values: {
|
|
860
|
+
group: 0,
|
|
861
|
+
runtime: projectionRuntime,
|
|
862
|
+
instances,
|
|
863
|
+
...isBillboard ? { sceneDepthBinding: 1 } : {}
|
|
864
|
+
},
|
|
865
|
+
...isBillboard && depthTarget !== void 0 ? { logicalTargets: { sceneDepth: depthTarget.name } } : {}
|
|
866
|
+
},
|
|
867
|
+
{ kind: "vertex-data", name: vertexData, layout: vertexLayout, buffer: instances }
|
|
868
|
+
);
|
|
869
|
+
const drawBindings = [graphicsBindings];
|
|
870
|
+
if (particleMaterialUsesBindings(material)) {
|
|
871
|
+
const materialBindings = `${rendererPrefix}.material-bindings`;
|
|
872
|
+
resources.push({
|
|
873
|
+
kind: "graphics-bindings",
|
|
874
|
+
name: materialBindings,
|
|
875
|
+
program: graphicsProgram,
|
|
876
|
+
values: {
|
|
877
|
+
group: 1,
|
|
878
|
+
material: { world: worldIndex, guid: renderer.material }
|
|
879
|
+
}
|
|
880
|
+
});
|
|
881
|
+
drawBindings.push(materialBindings);
|
|
1149
882
|
}
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
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: [
|
|
883
|
+
const vertexBindings = [];
|
|
884
|
+
let indexData;
|
|
885
|
+
if (renderer.kind === "mesh") {
|
|
886
|
+
if (mesh === void 0) return err(planFailure());
|
|
887
|
+
const geometryBuffer = `${rendererPrefix}.geometry-buffer`;
|
|
888
|
+
const geometry = `${rendererPrefix}.geometry`;
|
|
889
|
+
const geometryData = canonicalMeshVertices(mesh);
|
|
890
|
+
resources.push(
|
|
891
|
+
{
|
|
892
|
+
kind: "buffer",
|
|
893
|
+
name: geometryBuffer,
|
|
894
|
+
size: geometryData.byteLength,
|
|
895
|
+
usage: ["vertex"],
|
|
896
|
+
data: geometryData
|
|
897
|
+
},
|
|
898
|
+
{
|
|
899
|
+
kind: "vertex-data",
|
|
900
|
+
name: geometry,
|
|
901
|
+
layout: vertexLayout,
|
|
902
|
+
buffer: geometryBuffer
|
|
903
|
+
}
|
|
904
|
+
);
|
|
905
|
+
vertexBindings.push(
|
|
906
|
+
{ slot: 0, resource: geometry },
|
|
907
|
+
{ slot: 1, resource: vertexData }
|
|
908
|
+
);
|
|
909
|
+
if (mesh.indices !== void 0) {
|
|
910
|
+
const indexBuffer = `${rendererPrefix}.index-buffer`;
|
|
911
|
+
const indices = `${rendererPrefix}.indices`;
|
|
912
|
+
resources.push(
|
|
1197
913
|
{
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
format:
|
|
1208
|
-
|
|
1209
|
-
depthStoreOp: "store"
|
|
914
|
+
kind: "buffer",
|
|
915
|
+
name: indexBuffer,
|
|
916
|
+
size: mesh.indices.byteLength,
|
|
917
|
+
usage: ["index"],
|
|
918
|
+
data: mesh.indices
|
|
919
|
+
},
|
|
920
|
+
{
|
|
921
|
+
kind: "index-data",
|
|
922
|
+
name: indices,
|
|
923
|
+
format: indexFormat,
|
|
924
|
+
buffer: indexBuffer
|
|
1210
925
|
}
|
|
926
|
+
);
|
|
927
|
+
indexData = { resource: indices, format: indexFormat };
|
|
928
|
+
}
|
|
929
|
+
} else {
|
|
930
|
+
vertexBindings.push({ slot: 0, resource: vertexData });
|
|
931
|
+
}
|
|
932
|
+
passes.push({
|
|
933
|
+
kind: "raster",
|
|
934
|
+
name: `${rendererPrefix}.raster`,
|
|
935
|
+
colorAttachments: [
|
|
936
|
+
{
|
|
937
|
+
target: colorTarget?.name ?? "swapchain",
|
|
938
|
+
loadOp: "load",
|
|
939
|
+
storeOp: "store"
|
|
940
|
+
}
|
|
941
|
+
],
|
|
942
|
+
...depthTarget === void 0 ? {} : {
|
|
943
|
+
depthStencilAttachment: {
|
|
944
|
+
target: depthTarget.name,
|
|
945
|
+
depthLoadOp: "load",
|
|
946
|
+
depthStoreOp: "store"
|
|
1211
947
|
}
|
|
1212
948
|
},
|
|
1213
|
-
...
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
949
|
+
...isBillboard && depthTarget !== void 0 ? { sampledTargets: [depthTarget.name] } : {},
|
|
950
|
+
draws: [
|
|
951
|
+
{
|
|
952
|
+
program: graphicsProgram,
|
|
953
|
+
bindings: drawBindings,
|
|
954
|
+
vertexData: vertexBindings,
|
|
955
|
+
...indexData === void 0 ? {} : { indexData },
|
|
956
|
+
draw: {
|
|
957
|
+
kind: indexData === void 0 ? "draw-indirect" : "draw-indexed-indirect",
|
|
958
|
+
resource: indirect,
|
|
959
|
+
offset: rendererIndex * 20
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
]
|
|
963
|
+
});
|
|
964
|
+
}
|
|
1224
965
|
}
|
|
1225
966
|
}
|
|
1226
967
|
for (const entry of frame.worlds) {
|
|
1227
|
-
const
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
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));
|
|
968
|
+
for (const intent of entry.intents) {
|
|
969
|
+
if (dispatchedIntents.has(intent)) {
|
|
970
|
+
entry.runtime.markEventDispatched(intent.player, intent.eventCounters);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
const lastIntent = entry.intents.at(-1);
|
|
974
|
+
if (lastIntent !== void 0) entry.runtime.commit(lastIntent.sequence);
|
|
1243
975
|
}
|
|
1244
|
-
|
|
1245
|
-
|
|
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
|
|
976
|
+
return ok({ resources, passes });
|
|
977
|
+
}
|
|
1259
978
|
};
|
|
1260
979
|
}
|
|
1261
980
|
function duplicate(token, providerId) {
|
|
@@ -1336,7 +1055,7 @@ function controlFailure(code, expected, hint, detail = {}) {
|
|
|
1336
1055
|
return { code, expected, hint, detail };
|
|
1337
1056
|
}
|
|
1338
1057
|
function createVfxRuntimeHost(options) {
|
|
1339
|
-
const
|
|
1058
|
+
const registries = /* @__PURE__ */ new WeakSet();
|
|
1340
1059
|
const worlds = /* @__PURE__ */ new WeakMap();
|
|
1341
1060
|
const pausedPlayers = /* @__PURE__ */ new WeakMap();
|
|
1342
1061
|
let nextGeneration = 1;
|
|
@@ -1347,14 +1066,23 @@ function createVfxRuntimeHost(options) {
|
|
|
1347
1066
|
const key = `${kind}:${guid.toLowerCase()}`;
|
|
1348
1067
|
const cached = attached.renderAssets.get(key);
|
|
1349
1068
|
if (cached?.kind === kind) return cached;
|
|
1069
|
+
if ("lookup" in attached.assets) {
|
|
1070
|
+
const lookup = attached.assets.lookup;
|
|
1071
|
+
const legacy = lookup.call(attached.assets, guid);
|
|
1072
|
+
if (legacy?.kind === kind) {
|
|
1073
|
+
attached.renderAssets.set(key, legacy);
|
|
1074
|
+
return legacy;
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1350
1077
|
const resolved = attached.resolver?.lookup(guid);
|
|
1351
1078
|
if (resolved?.kind === kind) {
|
|
1352
1079
|
attached.renderAssets.set(key, resolved);
|
|
1353
1080
|
return resolved;
|
|
1354
1081
|
}
|
|
1355
|
-
if (!attached.pendingRenderAssets.has(key)) {
|
|
1082
|
+
if ("load" in attached.assets && typeof attached.assets.load === "function" && !attached.pendingRenderAssets.has(key)) {
|
|
1356
1083
|
const epoch = attached.catalogEpoch;
|
|
1357
|
-
const
|
|
1084
|
+
const load = attached.assets.load;
|
|
1085
|
+
const request = load(guid, kind).then((result) => {
|
|
1358
1086
|
if (!result.ok || worlds.get(world) !== attached || attached.catalogEpoch !== epoch)
|
|
1359
1087
|
return;
|
|
1360
1088
|
attached.renderAssets.set(key, result.value);
|
|
@@ -1482,15 +1210,22 @@ function createVfxRuntimeHost(options) {
|
|
|
1482
1210
|
resolveDataInterfaces: ({ requirements, generation }) => dataInterfaces.resolve(requirements, generation),
|
|
1483
1211
|
attachWorld: async ({ world, assets }) => {
|
|
1484
1212
|
if (worlds.has(world)) return ok({ state: "already-attached" });
|
|
1485
|
-
if (!
|
|
1213
|
+
if (!registries.has(assets)) {
|
|
1486
1214
|
try {
|
|
1487
|
-
|
|
1215
|
+
if ("loaders" in assets) {
|
|
1216
|
+
assets.loaders.registerPackLoader(vfxGpuEffectPackLoader);
|
|
1217
|
+
} else if ("installDecoder" in assets) {
|
|
1218
|
+
assets.installDecoder(vfxGpuEffectContribution.kind, vfxGpuEffectContribution.decoder);
|
|
1219
|
+
} else {
|
|
1220
|
+
throw new TypeError("VFX assets registry exposes neither loaders nor installDecoder");
|
|
1221
|
+
}
|
|
1222
|
+
registries.add(assets);
|
|
1488
1223
|
} catch (cause) {
|
|
1489
1224
|
return err(
|
|
1490
1225
|
failure2(
|
|
1491
1226
|
"vfx-host-loader-install-failed",
|
|
1492
|
-
"the VFX
|
|
1493
|
-
"remove a conflicting particle-effect
|
|
1227
|
+
"the v2 VFX loader to be registered once",
|
|
1228
|
+
"remove a conflicting particle-effect loader and retry attachWorld",
|
|
1494
1229
|
cause
|
|
1495
1230
|
)
|
|
1496
1231
|
);
|
|
@@ -1514,10 +1249,12 @@ function createVfxRuntimeHost(options) {
|
|
|
1514
1249
|
);
|
|
1515
1250
|
}
|
|
1516
1251
|
let resolver;
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1252
|
+
if (!("loaders" in assets) && "installDecoder" in assets) {
|
|
1253
|
+
try {
|
|
1254
|
+
resolver = getAssetRegistryResolver(assets);
|
|
1255
|
+
} catch {
|
|
1256
|
+
resolver = void 0;
|
|
1257
|
+
}
|
|
1521
1258
|
}
|
|
1522
1259
|
const attached = {
|
|
1523
1260
|
assets,
|
|
@@ -1526,13 +1263,12 @@ function createVfxRuntimeHost(options) {
|
|
|
1526
1263
|
generation: nextGeneration++,
|
|
1527
1264
|
renderAssets: /* @__PURE__ */ new Map(),
|
|
1528
1265
|
pendingRenderAssets: /* @__PURE__ */ new Map(),
|
|
1529
|
-
catalogEpoch: typeof assets.snapshot === "function" ? assets.snapshot().epoch : 0,
|
|
1266
|
+
catalogEpoch: "snapshot" in assets && typeof assets.snapshot === "function" ? assets.snapshot().epoch : 0,
|
|
1530
1267
|
unsubscribeAssets: () => {
|
|
1531
1268
|
}
|
|
1532
1269
|
};
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
attached.unsubscribeAssets = subscribe((snapshot) => {
|
|
1270
|
+
if ("subscribe" in assets && typeof assets.subscribe === "function") {
|
|
1271
|
+
attached.unsubscribeAssets = assets.subscribe((snapshot) => {
|
|
1536
1272
|
if (snapshot.epoch === attached.catalogEpoch) return;
|
|
1537
1273
|
attached.catalogEpoch = snapshot.epoch;
|
|
1538
1274
|
attached.renderAssets.clear();
|