@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
|
@@ -1,17 +1,6 @@
|
|
|
1
1
|
import type { EntityHandle, World } from '@forgeax/engine-ecs';
|
|
2
2
|
import { frustum } from '@forgeax/engine-math';
|
|
3
|
-
import {
|
|
4
|
-
RENDER_FEATURE_VERTEX_LAYOUTS,
|
|
5
|
-
type RenderError,
|
|
6
|
-
type RenderFeature,
|
|
7
|
-
type RenderFeatureDrawRecord,
|
|
8
|
-
type RenderFeatureGpuBindingsRef,
|
|
9
|
-
type RenderFeatureGpuBufferRef,
|
|
10
|
-
type RenderFeatureGpuProgramRef,
|
|
11
|
-
RenderFeaturePreparationFailedError,
|
|
12
|
-
RenderFeatureStageFailedError,
|
|
13
|
-
type RenderFeatureTargetHandle,
|
|
14
|
-
} from '@forgeax/engine-render';
|
|
3
|
+
import type { RenderFeature, RenderFeaturePlan } from '@forgeax/engine-render';
|
|
15
4
|
import { Transform } from '@forgeax/engine-scene';
|
|
16
5
|
import { err, type MaterialAsset, type MeshAsset, ok } from '@forgeax/engine-types';
|
|
17
6
|
import type { ParticleRendererSource } from '@forgeax/engine-vfx';
|
|
@@ -20,6 +9,8 @@ import {
|
|
|
20
9
|
type VfxGpuRuntime,
|
|
21
10
|
type VfxGpuTickIntent,
|
|
22
11
|
} from '@forgeax/engine-vfx';
|
|
12
|
+
import { RenderFeatureStageFailedError } from '../../../render/src/errors/render';
|
|
13
|
+
import { RENDER_FEATURE_VERTEX_LAYOUTS } from '../../../render/src/features/prepared-graphics';
|
|
23
14
|
import type { VfxDataInterfaceRegistry } from '../host/data-interface-providers.js';
|
|
24
15
|
import type { ParticleRenderCamera } from './camera.js';
|
|
25
16
|
import {
|
|
@@ -35,16 +26,9 @@ import {
|
|
|
35
26
|
PARTICLE_SHADER_IDENTIFIERS,
|
|
36
27
|
particleMaterialPass,
|
|
37
28
|
particleMaterialUsesBindings,
|
|
38
|
-
particleMaterialUsesSceneDepth,
|
|
39
29
|
} from './particle-resources.js';
|
|
40
|
-
import {
|
|
41
|
-
|
|
42
|
-
stageDispatches,
|
|
43
|
-
type VfxStagePlanObservation,
|
|
44
|
-
type VfxStageReadiness,
|
|
45
|
-
type VfxValidatedStagePlan,
|
|
46
|
-
validatedStagePlan,
|
|
47
|
-
} from './stage-plan.js';
|
|
30
|
+
import type { VfxStagePlanObservation, VfxValidatedStagePlan } from './stage-plan.js';
|
|
31
|
+
import { validatedStagePlan } from './stage-plan.js';
|
|
48
32
|
|
|
49
33
|
const IDENTITY = 'forgeax.vfx-render.gpu-particles';
|
|
50
34
|
const WORKGROUP_SIZE = 256;
|
|
@@ -53,7 +37,6 @@ const BILLBOARD_INSTANCE_BYTES = 31 * 4;
|
|
|
53
37
|
const MESH_INSTANCE_BYTES = 28 * 4;
|
|
54
38
|
const COUNTERS_BYTES = 24;
|
|
55
39
|
const RUNTIME_BYTES = 72 * 4;
|
|
56
|
-
const MAX_TICK_RINGS = 8;
|
|
57
40
|
const IDENTITY_MATRIX = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
|
|
58
41
|
|
|
59
42
|
type ParticleRendererKind = ParticleRendererSource['kind'];
|
|
@@ -61,21 +44,41 @@ type ParticleTopologyRenderer = Extract<ParticleRendererSource, { readonly capac
|
|
|
61
44
|
type ParticleTopologyKind = ParticleTopologyRenderer['kind'];
|
|
62
45
|
type VfxStageOutput = VfxStagePlanObservation['stageOutput'];
|
|
63
46
|
|
|
47
|
+
interface VfxRenderStageState {
|
|
48
|
+
readonly stageOutput: VfxStageOutput;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface VfxRenderInspectSnapshot extends VfxRenderStageState {
|
|
52
|
+
readonly topology: ParticleRendererKind;
|
|
53
|
+
readonly counters: {
|
|
54
|
+
readonly capacity: number;
|
|
55
|
+
readonly produced: number;
|
|
56
|
+
readonly dropped: number;
|
|
57
|
+
};
|
|
58
|
+
readonly stageReadiness: readonly unknown[];
|
|
59
|
+
readonly providerReadiness: unknown;
|
|
60
|
+
readonly gpuTiming: unknown;
|
|
61
|
+
}
|
|
62
|
+
|
|
64
63
|
export interface VfxRenderInspectInput {
|
|
65
64
|
readonly topology: ParticleRendererKind;
|
|
66
65
|
readonly capacity: number;
|
|
67
66
|
readonly produced: number;
|
|
68
67
|
readonly dropped: number;
|
|
69
68
|
readonly stageReadiness: readonly unknown[];
|
|
69
|
+
readonly stageOutput?: VfxStageOutput;
|
|
70
70
|
readonly providerReadiness: unknown;
|
|
71
71
|
readonly gpuTiming: unknown;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
export function createVfxRenderInspectSnapshot(
|
|
74
|
+
export function createVfxRenderInspectSnapshot(
|
|
75
|
+
input: VfxRenderInspectInput,
|
|
76
|
+
): VfxRenderInspectSnapshot {
|
|
75
77
|
return {
|
|
76
78
|
topology: input.topology,
|
|
77
79
|
counters: { capacity: input.capacity, produced: input.produced, dropped: input.dropped },
|
|
78
80
|
stageReadiness: input.stageReadiness,
|
|
81
|
+
stageOutput: input.stageOutput ?? 'empty',
|
|
79
82
|
providerReadiness: input.providerReadiness,
|
|
80
83
|
gpuTiming: input.gpuTiming,
|
|
81
84
|
} as const;
|
|
@@ -151,18 +154,6 @@ interface GpuParticleFeatureOptions {
|
|
|
151
154
|
};
|
|
152
155
|
}
|
|
153
156
|
|
|
154
|
-
/**
|
|
155
|
-
* GPU work observed from the producer-owned VFX feature for the last frame.
|
|
156
|
-
* Counts are published only after the feature stages its real compute and
|
|
157
|
-
* indirect graphics passes; preview consumers must not manufacture them.
|
|
158
|
-
*/
|
|
159
|
-
export interface VfxGpuRenderObservation {
|
|
160
|
-
readonly frameNumber: number;
|
|
161
|
-
readonly dispatches: number;
|
|
162
|
-
readonly indirectDraws: number;
|
|
163
|
-
readonly subjectOutputs: number;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
157
|
interface ExtractedWorld {
|
|
167
158
|
readonly world: World;
|
|
168
159
|
readonly runtime: VfxGpuRuntime;
|
|
@@ -175,55 +166,6 @@ interface ExtractedFrame {
|
|
|
175
166
|
readonly frameNumber: number;
|
|
176
167
|
}
|
|
177
168
|
|
|
178
|
-
interface GpuRefs {
|
|
179
|
-
readonly program: RenderFeatureGpuProgramRef;
|
|
180
|
-
readonly particles: RenderFeatureGpuBufferRef;
|
|
181
|
-
readonly aliveIndices: RenderFeatureGpuBufferRef;
|
|
182
|
-
readonly counters: RenderFeatureGpuBufferRef;
|
|
183
|
-
readonly indirect: RenderFeatureGpuBufferRef;
|
|
184
|
-
readonly scratch: RenderFeatureGpuBufferRef;
|
|
185
|
-
readonly billboardInstances: RenderFeatureGpuBufferRef;
|
|
186
|
-
readonly eventInputs: RenderFeatureGpuBufferRef;
|
|
187
|
-
readonly events: RenderFeatureGpuBufferRef;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
interface TickRing {
|
|
191
|
-
readonly runtime: RenderFeatureGpuBufferRef;
|
|
192
|
-
readonly bindings: RenderFeatureGpuBindingsRef;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
interface RendererProjection {
|
|
196
|
-
readonly kind: ParticleRendererSource['kind'];
|
|
197
|
-
readonly ring: TickRing;
|
|
198
|
-
readonly instances: RenderFeatureGpuBufferRef;
|
|
199
|
-
readonly workgroups: number;
|
|
200
|
-
readonly historyWorkgroups?: number;
|
|
201
|
-
readonly sorting?: 'none' | 'emitter' | 'back-to-front';
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
interface EmitterState {
|
|
205
|
-
readonly world: World;
|
|
206
|
-
readonly player: EntityHandle;
|
|
207
|
-
readonly emitterId: string;
|
|
208
|
-
readonly fingerprint: string;
|
|
209
|
-
readonly capacity: number;
|
|
210
|
-
readonly names: string;
|
|
211
|
-
refs?: GpuRefs;
|
|
212
|
-
rings: TickRing[];
|
|
213
|
-
projections: RendererProjection[];
|
|
214
|
-
colorTarget: RenderFeatureTargetHandle | undefined;
|
|
215
|
-
depthTarget: RenderFeatureTargetHandle | undefined;
|
|
216
|
-
indirectInitialized: boolean;
|
|
217
|
-
culled: boolean;
|
|
218
|
-
lastIntent?: VfxGpuTickIntent;
|
|
219
|
-
draws: RenderFeatureDrawRecord[];
|
|
220
|
-
depthSampledDraws: RenderFeatureDrawRecord[];
|
|
221
|
-
stagePlan?: VfxValidatedStagePlan;
|
|
222
|
-
lastKnownGoodStage?: VfxValidatedStagePlan;
|
|
223
|
-
stageReadiness: readonly VfxStageReadiness[];
|
|
224
|
-
stageOutput: VfxStageOutput;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
169
|
function finite(value: unknown, fallback: number): number {
|
|
228
170
|
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
229
171
|
}
|
|
@@ -323,7 +265,7 @@ function emitterVisible(
|
|
|
323
265
|
(bounds.max[2] - bounds.min[2]) * 0.5,
|
|
324
266
|
);
|
|
325
267
|
const matrix = (index: number): number => localToWorld[index] ?? 0;
|
|
326
|
-
const
|
|
268
|
+
const worldCenter = new Float32Array([
|
|
327
269
|
matrix(0) * center[0] + matrix(4) * center[1] + matrix(8) * center[2] + matrix(12),
|
|
328
270
|
matrix(1) * center[0] + matrix(5) * center[1] + matrix(9) * center[2] + matrix(13),
|
|
329
271
|
matrix(2) * center[0] + matrix(6) * center[1] + matrix(10) * center[2] + matrix(14),
|
|
@@ -334,93 +276,73 @@ function emitterVisible(
|
|
|
334
276
|
Math.hypot(matrix(8), matrix(9), matrix(10)),
|
|
335
277
|
);
|
|
336
278
|
const planes = frustum.fromViewProjection(frustum.create(), camera.viewProjection);
|
|
337
|
-
return frustum.intersectsSphere(planes,
|
|
279
|
+
return frustum.intersectsSphere(planes, worldCenter, radius * scale);
|
|
338
280
|
}
|
|
339
281
|
|
|
340
282
|
function resetData(size: number): Uint8Array {
|
|
341
283
|
return new Uint8Array(size);
|
|
342
284
|
}
|
|
343
285
|
|
|
344
|
-
function
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
)
|
|
348
|
-
return targets.find((entry) => entry.kind === kind);
|
|
286
|
+
function requiresSceneDepth(intent: VfxGpuTickIntent): boolean {
|
|
287
|
+
return (intent.emitter.reflection.dataInterfaces ?? []).some(
|
|
288
|
+
(requirement) => requirement.kind === 'scene-depth',
|
|
289
|
+
);
|
|
349
290
|
}
|
|
350
291
|
|
|
351
|
-
function
|
|
352
|
-
return new
|
|
353
|
-
IDENTITY,
|
|
354
|
-
-1,
|
|
355
|
-
'asset-load',
|
|
356
|
-
'vertex-data',
|
|
357
|
-
`mesh:${guid}`,
|
|
358
|
-
'asset-not-ready',
|
|
359
|
-
'next-frame',
|
|
360
|
-
);
|
|
292
|
+
function planFailure(): RenderFeatureStageFailedError {
|
|
293
|
+
return new RenderFeatureStageFailedError(IDENTITY, -1, 'plan', 'next-frame');
|
|
361
294
|
}
|
|
362
295
|
|
|
363
|
-
|
|
364
|
-
|
|
296
|
+
type PlanResource = RenderFeaturePlan['resources'][number];
|
|
297
|
+
type PlanPass = RenderFeaturePlan['passes'][number];
|
|
298
|
+
|
|
299
|
+
function planName(value: string, maxLength = 24): string {
|
|
300
|
+
const normalized = value.toLowerCase().replaceAll(/[^a-z0-9.-]/g, '-');
|
|
301
|
+
return (normalized.length === 0 ? 'unnamed' : normalized).slice(0, maxLength);
|
|
365
302
|
}
|
|
366
303
|
|
|
367
|
-
function
|
|
368
|
-
|
|
369
|
-
|
|
304
|
+
function computeBindingEntries(
|
|
305
|
+
intent: VfxGpuTickIntent,
|
|
306
|
+
resources: Readonly<Record<number, string>>,
|
|
307
|
+
): readonly { readonly binding: number; readonly resource: string }[] {
|
|
308
|
+
const declared = new Set(
|
|
309
|
+
(intent.emitter.reflection.bindings[0]?.entries ?? [])
|
|
310
|
+
.filter((entry) => entry.buffer !== undefined)
|
|
311
|
+
.map((entry) => entry.binding),
|
|
312
|
+
);
|
|
313
|
+
return Object.entries(resources).flatMap(([binding, resource]) =>
|
|
314
|
+
declared.has(Number(binding)) ? [{ binding: Number(binding), resource }] : [],
|
|
370
315
|
);
|
|
371
316
|
}
|
|
372
317
|
|
|
318
|
+
function simulationDispatches(
|
|
319
|
+
intent: VfxGpuTickIntent,
|
|
320
|
+
stages: VfxValidatedStagePlan,
|
|
321
|
+
): Extract<PlanPass, { readonly kind: 'compute' }>['dispatches'] {
|
|
322
|
+
const groups = Math.max(1, Math.ceil(intent.emitter.capacity / WORKGROUP_SIZE));
|
|
323
|
+
return [
|
|
324
|
+
{ kind: 'direct', entryPoint: 'forgeax_vfx_spawn_main', workgroups: [groups] },
|
|
325
|
+
{ kind: 'direct', entryPoint: 'forgeax_vfx_update_main', workgroups: [groups] },
|
|
326
|
+
...stages.stages.map((stage) => ({
|
|
327
|
+
kind: 'direct' as const,
|
|
328
|
+
entryPoint: stage.entryPoint,
|
|
329
|
+
workgroups: [groups] as const,
|
|
330
|
+
})),
|
|
331
|
+
{ kind: 'direct', entryPoint: 'forgeax_vfx_scan_blocks_main', workgroups: [groups] },
|
|
332
|
+
{ kind: 'direct', entryPoint: 'forgeax_vfx_scan_block_offsets_main', workgroups: [1] },
|
|
333
|
+
{ kind: 'direct', entryPoint: 'forgeax_vfx_add_offsets_main', workgroups: [groups] },
|
|
334
|
+
{ kind: 'direct', entryPoint: 'forgeax_vfx_compact_main', workgroups: [groups] },
|
|
335
|
+
{
|
|
336
|
+
kind: 'direct',
|
|
337
|
+
entryPoint: 'forgeax_vfx_event_main',
|
|
338
|
+
workgroups: [Math.max(1, Math.ceil(eventInputCapacity(intent.emitter) / 64))],
|
|
339
|
+
},
|
|
340
|
+
];
|
|
341
|
+
}
|
|
342
|
+
|
|
373
343
|
export function gpuParticleRenderFeature(
|
|
374
344
|
options: GpuParticleFeatureOptions,
|
|
375
|
-
): RenderFeature<ExtractedFrame>
|
|
376
|
-
const worldIds = new WeakMap<World, number>();
|
|
377
|
-
let nextWorldId = 0;
|
|
378
|
-
const states = new Map<string, EmitterState>();
|
|
379
|
-
let lastObservation: VfxGpuRenderObservation = Object.freeze({
|
|
380
|
-
frameNumber: -1,
|
|
381
|
-
dispatches: 0,
|
|
382
|
-
indirectDraws: 0,
|
|
383
|
-
subjectOutputs: 0,
|
|
384
|
-
});
|
|
385
|
-
const keyOf = (world: World, intent: VfxGpuTickIntent): string => {
|
|
386
|
-
let worldId = worldIds.get(world);
|
|
387
|
-
if (worldId === undefined) {
|
|
388
|
-
worldId = nextWorldId++;
|
|
389
|
-
worldIds.set(world, worldId);
|
|
390
|
-
}
|
|
391
|
-
return `${worldId}:${intent.player}:${intent.emitter.id}`;
|
|
392
|
-
};
|
|
393
|
-
const stateFor = (world: World, intent: VfxGpuTickIntent): EmitterState => {
|
|
394
|
-
const key = keyOf(world, intent);
|
|
395
|
-
let state = states.get(key);
|
|
396
|
-
if (state !== undefined && state.fingerprint !== intent.programFingerprint) {
|
|
397
|
-
states.delete(key);
|
|
398
|
-
state = undefined;
|
|
399
|
-
}
|
|
400
|
-
if (state !== undefined) return state;
|
|
401
|
-
const fingerprint = intent.programFingerprint.slice(0, 12).replaceAll(':', '_');
|
|
402
|
-
state = {
|
|
403
|
-
world,
|
|
404
|
-
player: intent.player,
|
|
405
|
-
emitterId: intent.emitter.id,
|
|
406
|
-
fingerprint: intent.programFingerprint,
|
|
407
|
-
capacity: intent.emitter.capacity,
|
|
408
|
-
names: `gpu.${key.replaceAll(':', '.')}.${fingerprint}`,
|
|
409
|
-
rings: [],
|
|
410
|
-
projections: [],
|
|
411
|
-
draws: [],
|
|
412
|
-
depthSampledDraws: [],
|
|
413
|
-
colorTarget: undefined,
|
|
414
|
-
depthTarget: undefined,
|
|
415
|
-
indirectInitialized: false,
|
|
416
|
-
culled: false,
|
|
417
|
-
stageReadiness: [],
|
|
418
|
-
stageOutput: 'empty',
|
|
419
|
-
};
|
|
420
|
-
states.set(key, state);
|
|
421
|
-
return state;
|
|
422
|
-
};
|
|
423
|
-
|
|
345
|
+
): RenderFeature<ExtractedFrame> {
|
|
424
346
|
return {
|
|
425
347
|
identity: IDENTITY,
|
|
426
348
|
requiredCapabilities: ['compute', 'indirectDrawing'],
|
|
@@ -435,8 +357,8 @@ export function gpuParticleRenderFeature(
|
|
|
435
357
|
const intents = runtime.snapshot().filter((intent) => {
|
|
436
358
|
if (options.playerConsumption?.isEnabled(world, intent.player) === false) return false;
|
|
437
359
|
const requirements = intent.emitter.reflection.dataInterfaces ?? [];
|
|
438
|
-
if (requirements.length === 0) return true;
|
|
439
360
|
return (
|
|
361
|
+
requirements.length === 0 ||
|
|
440
362
|
options.dataInterfaces?.resolve(requirements, intent.instanceGeneration).ok === true
|
|
441
363
|
);
|
|
442
364
|
});
|
|
@@ -444,782 +366,454 @@ export function gpuParticleRenderFeature(
|
|
|
444
366
|
}
|
|
445
367
|
return ok({ worlds: extracted, frameNumber: context.frameNumber });
|
|
446
368
|
},
|
|
447
|
-
|
|
448
|
-
const
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
const candidate = validatedStagePlan(
|
|
369
|
+
plan: (frame, context) => {
|
|
370
|
+
const resources: PlanResource[] = [];
|
|
371
|
+
const passes: PlanPass[] = [];
|
|
372
|
+
const dispatchedIntents = new Set<VfxGpuTickIntent>();
|
|
373
|
+
const colorTarget =
|
|
374
|
+
context.targets.find((candidate) => candidate.kind === 'color') ??
|
|
375
|
+
context.targets.find((candidate) => candidate.kind === 'swapchain');
|
|
376
|
+
const depthTarget = context.targets.find((candidate) => candidate.kind === 'depth');
|
|
377
|
+
|
|
378
|
+
for (const [worldIndex, entry] of frame.worlds.entries()) {
|
|
379
|
+
for (const [intentIndex, intent] of entry.intents.entries()) {
|
|
380
|
+
if (!entry.runtime.isEmitterSessionEnabled(intent.player, intent.emitter.id)) continue;
|
|
381
|
+
const localToWorld = emitterTransform(entry.world, intent);
|
|
382
|
+
const visible = emitterVisible(intent, entry.camera, localToWorld);
|
|
383
|
+
entry.runtime.setEmitterCameraVisibility(intent.player, intent.emitter.id, visible);
|
|
384
|
+
if (!visible) continue;
|
|
385
|
+
if (requiresSceneDepth(intent) && depthTarget === undefined) continue;
|
|
386
|
+
|
|
387
|
+
const stagePlan = validatedStagePlan(
|
|
467
388
|
intent.emitter.reflection.stages,
|
|
468
389
|
intent.instanceGeneration,
|
|
469
390
|
);
|
|
470
|
-
if (!
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
state ??= stateFor(entry.world, intent);
|
|
489
|
-
const observation = observeStagePlan(
|
|
490
|
-
candidate,
|
|
491
|
-
intent.instanceGeneration,
|
|
492
|
-
state.lastKnownGoodStage,
|
|
391
|
+
if (!stagePlan.ok) return err(planFailure());
|
|
392
|
+
|
|
393
|
+
const prefix = `vfx.w-${worldIndex}.i-${intentIndex}.${planName(intent.emitter.id)}`;
|
|
394
|
+
const program = `${prefix}.compute-program`;
|
|
395
|
+
const particles = `${prefix}.particles`;
|
|
396
|
+
const runtime = `${prefix}.runtime`;
|
|
397
|
+
const aliveIndices = `${prefix}.alive-indices`;
|
|
398
|
+
const counters = `${prefix}.counters`;
|
|
399
|
+
const indirect = `${prefix}.indirect`;
|
|
400
|
+
const scratch = `${prefix}.scratch`;
|
|
401
|
+
const sharedInstances = `${prefix}.shared-instances`;
|
|
402
|
+
const eventInputs = `${prefix}.event-inputs`;
|
|
403
|
+
const events = `${prefix}.events`;
|
|
404
|
+
const bindings = `${prefix}.simulation-bindings`;
|
|
405
|
+
const capacity = intent.emitter.capacity;
|
|
406
|
+
const renderers = intent.emitter.renderers;
|
|
407
|
+
const meshes = renderers.map((renderer) =>
|
|
408
|
+
renderer.kind === 'mesh' ? options.mesh?.read(entry.world, renderer.mesh) : undefined,
|
|
493
409
|
);
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
bindings: intent.emitter.reflection.bindings,
|
|
502
|
-
});
|
|
503
|
-
if (!program.ok) {
|
|
410
|
+
const indirectWords = new Uint32Array(Math.max(1, renderers.length) * 5);
|
|
411
|
+
|
|
412
|
+
for (const [rendererIndex, renderer] of renderers.entries()) {
|
|
413
|
+
const mesh = meshes[rendererIndex];
|
|
414
|
+
const submesh =
|
|
415
|
+
renderer.kind === 'mesh' ? mesh?.submeshes[renderer.submesh ?? 0] : undefined;
|
|
416
|
+
if (renderer.kind === 'mesh' && submesh === undefined) return err(planFailure());
|
|
504
417
|
if (
|
|
505
|
-
|
|
506
|
-
|
|
418
|
+
(renderer.kind === 'ribbon' ||
|
|
419
|
+
renderer.kind === 'trail' ||
|
|
420
|
+
renderer.kind === 'beam') &&
|
|
421
|
+
!createTopologyResourcePlan(renderer).ok
|
|
507
422
|
) {
|
|
508
|
-
return
|
|
423
|
+
return err(planFailure());
|
|
509
424
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
if (pendingProgramError !== undefined) return err(pendingProgramError);
|
|
516
|
-
let pendingGraphicsError: RenderError | undefined;
|
|
517
|
-
for (const { entry, intent, state } of preparedIntents) {
|
|
518
|
-
if (intent.reset) state.indirectInitialized = false;
|
|
519
|
-
state.lastIntent = intent;
|
|
520
|
-
const base = state.names;
|
|
521
|
-
const program = gpu.prepareProgram(`${base}.program`, {
|
|
522
|
-
wgsl: intent.emitter.wgsl,
|
|
523
|
-
entryPoints: intent.emitter.reflection.entryPoints,
|
|
524
|
-
bindings: intent.emitter.reflection.bindings,
|
|
525
|
-
});
|
|
526
|
-
if (!program.ok) return program;
|
|
527
|
-
const prepare = (
|
|
528
|
-
name: string,
|
|
529
|
-
size: number,
|
|
530
|
-
usage: readonly ('storage' | 'uniform' | 'indirect' | 'vertex')[],
|
|
531
|
-
data?: ArrayBufferView,
|
|
532
|
-
) =>
|
|
533
|
-
gpu.prepareBuffer(`${base}.${name}`, {
|
|
534
|
-
size,
|
|
535
|
-
usage,
|
|
536
|
-
...(data === undefined ? {} : { data }),
|
|
537
|
-
});
|
|
538
|
-
const particles = prepare(
|
|
539
|
-
'particles',
|
|
540
|
-
state.capacity * PARTICLE_BYTES,
|
|
541
|
-
['storage'],
|
|
542
|
-
intent.reset ? resetData(state.capacity * PARTICLE_BYTES) : undefined,
|
|
543
|
-
);
|
|
544
|
-
if (!particles.ok) return particles;
|
|
545
|
-
const aliveIndices = prepare('alive-indices', state.capacity * 4, ['storage']);
|
|
546
|
-
if (!aliveIndices.ok) return aliveIndices;
|
|
547
|
-
const counters = prepare(
|
|
548
|
-
'counters',
|
|
549
|
-
COUNTERS_BYTES,
|
|
550
|
-
['storage'],
|
|
551
|
-
intent.reset ? resetData(COUNTERS_BYTES) : undefined,
|
|
552
|
-
);
|
|
553
|
-
if (!counters.ok) return counters;
|
|
554
|
-
const indirect = prepare('indirect', Math.max(1, intent.emitter.renderers.length) * 20, [
|
|
555
|
-
'storage',
|
|
556
|
-
'indirect',
|
|
557
|
-
]);
|
|
558
|
-
if (!indirect.ok) return indirect;
|
|
559
|
-
const scratchBytes = (state.capacity * 2 + Math.ceil(state.capacity / WORKGROUP_SIZE)) * 4;
|
|
560
|
-
const scratch = prepare(
|
|
561
|
-
'scratch',
|
|
562
|
-
scratchBytes,
|
|
563
|
-
['storage'],
|
|
564
|
-
intent.reset ? resetData(scratchBytes) : undefined,
|
|
565
|
-
);
|
|
566
|
-
if (!scratch.ok) return scratch;
|
|
567
|
-
const billboardInstances = prepare(
|
|
568
|
-
'billboard-instances',
|
|
569
|
-
state.capacity * Math.max(BILLBOARD_INSTANCE_BYTES, MESH_INSTANCE_BYTES),
|
|
570
|
-
['storage', 'vertex'],
|
|
571
|
-
);
|
|
572
|
-
if (!billboardInstances.ok) return billboardInstances;
|
|
573
|
-
const events = prepare(
|
|
574
|
-
'events',
|
|
575
|
-
eventCapacity(intent.emitter) * VFX_EVENT_BYTES,
|
|
576
|
-
['storage'],
|
|
577
|
-
intent.reset ? resetData(eventCapacity(intent.emitter) * VFX_EVENT_BYTES) : undefined,
|
|
578
|
-
);
|
|
579
|
-
if (!events.ok) return events;
|
|
580
|
-
const initialEventInputs = prepare(
|
|
581
|
-
'event-inputs',
|
|
582
|
-
eventInputCapacity(intent.emitter) * VFX_EVENT_INPUT_BYTES,
|
|
583
|
-
['storage'],
|
|
584
|
-
encodeEventInputs(intent),
|
|
585
|
-
);
|
|
586
|
-
if (!initialEventInputs.ok) return initialEventInputs;
|
|
587
|
-
state.refs = {
|
|
588
|
-
program: program.value,
|
|
589
|
-
particles: particles.value,
|
|
590
|
-
aliveIndices: aliveIndices.value,
|
|
591
|
-
counters: counters.value,
|
|
592
|
-
indirect: indirect.value,
|
|
593
|
-
scratch: scratch.value,
|
|
594
|
-
billboardInstances: billboardInstances.value,
|
|
595
|
-
eventInputs: initialEventInputs.value,
|
|
596
|
-
events: events.value,
|
|
597
|
-
};
|
|
598
|
-
const ringIndex = intent.tick % MAX_TICK_RINGS;
|
|
599
|
-
const runtime = gpu.prepareBuffer(`${base}.runtime.${ringIndex}`, {
|
|
600
|
-
size: RUNTIME_BYTES,
|
|
601
|
-
usage: ['uniform'],
|
|
602
|
-
data: runtimeData(intent, entry.camera, undefined, emitterTransform(entry.world, intent)),
|
|
603
|
-
});
|
|
604
|
-
if (!runtime.ok) return runtime;
|
|
605
|
-
const refs = state.refs;
|
|
606
|
-
const updatedEventInputs = gpu.prepareBuffer(`${base}.event-inputs`, {
|
|
607
|
-
size: eventInputCapacity(intent.emitter) * VFX_EVENT_INPUT_BYTES,
|
|
608
|
-
usage: ['storage'],
|
|
609
|
-
data: encodeEventInputs(intent),
|
|
610
|
-
});
|
|
611
|
-
if (!updatedEventInputs.ok) return updatedEventInputs;
|
|
612
|
-
const bindings = gpu.prepareBindings(`${base}.bindings.${ringIndex}`, {
|
|
613
|
-
program: refs.program,
|
|
614
|
-
entries: [
|
|
615
|
-
{ binding: 0, buffer: refs.particles },
|
|
616
|
-
{ binding: 1, buffer: runtime.value },
|
|
617
|
-
{ binding: 2, buffer: refs.aliveIndices },
|
|
618
|
-
{ binding: 3, buffer: refs.counters },
|
|
619
|
-
{ binding: 4, buffer: refs.indirect },
|
|
620
|
-
{ binding: 5, buffer: refs.scratch },
|
|
621
|
-
{ binding: 6, buffer: refs.billboardInstances },
|
|
622
|
-
{ binding: 8, buffer: updatedEventInputs.value },
|
|
623
|
-
{ binding: 9, buffer: refs.events },
|
|
624
|
-
],
|
|
625
|
-
});
|
|
626
|
-
if (!bindings.ok) return bindings;
|
|
627
|
-
state.rings[ringIndex] = {
|
|
628
|
-
runtime: runtime.value,
|
|
629
|
-
bindings: bindings.value,
|
|
630
|
-
};
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
for (const [key, state] of states) {
|
|
634
|
-
const extracted = frame.worlds.find((entry) => entry.world === state.world);
|
|
635
|
-
if (extracted === undefined || !extracted.runtime.hasPlayer(state.player)) {
|
|
636
|
-
states.delete(key);
|
|
637
|
-
continue;
|
|
638
|
-
}
|
|
639
|
-
if (options.playerConsumption?.isEnabled(state.world, state.player) === false) {
|
|
640
|
-
// A render-consumption fence must also suppress the retained
|
|
641
|
-
// lastIntent fallback below. The authored player may be despawned
|
|
642
|
-
// after FixedUpdate, so reading its Transform here would otherwise
|
|
643
|
-
// produce an identity matrix and one final frame at world origin.
|
|
644
|
-
state.projections = [];
|
|
645
|
-
state.draws = [];
|
|
646
|
-
state.depthSampledDraws = [];
|
|
647
|
-
state.colorTarget = undefined;
|
|
648
|
-
state.depthTarget = undefined;
|
|
649
|
-
continue;
|
|
650
|
-
}
|
|
651
|
-
const intent = state.lastIntent;
|
|
652
|
-
const refs = state.refs;
|
|
653
|
-
if (intent === undefined || refs === undefined) continue;
|
|
654
|
-
const retained = gpu.retainBindings([
|
|
655
|
-
...state.rings.flatMap((ring) => (ring === undefined ? [] : [ring.bindings])),
|
|
656
|
-
...state.projections.map((projection) => projection.ring.bindings),
|
|
657
|
-
]);
|
|
658
|
-
if (!retained.ok) return retained;
|
|
659
|
-
if (!extracted.runtime.isEmitterSessionEnabled(state.player, state.emitterId)) {
|
|
660
|
-
// Session isolation is transient preview state, not a resource
|
|
661
|
-
// lifetime boundary. Keep prepared bindings warm while suppressing
|
|
662
|
-
// every contribution: a paused player may be re-enabled without a
|
|
663
|
-
// fresh simulation intent that could rebuild those bindings.
|
|
664
|
-
state.projections = [];
|
|
665
|
-
state.draws = [];
|
|
666
|
-
state.depthSampledDraws = [];
|
|
667
|
-
continue;
|
|
668
|
-
}
|
|
669
|
-
const renderers = intent.emitter.renderers;
|
|
670
|
-
if (renderers.length === 0) continue;
|
|
671
|
-
const localToWorld = emitterTransform(state.world, intent);
|
|
672
|
-
const visible = emitterVisible(intent, extracted.camera, localToWorld);
|
|
673
|
-
extracted.runtime.setEmitterCameraVisibility(state.player, state.emitterId, visible);
|
|
674
|
-
const currentIntents = extracted.intents.filter(
|
|
675
|
-
(candidate) =>
|
|
676
|
-
candidate.player === state.player && candidate.emitter.id === state.emitterId,
|
|
677
|
-
);
|
|
678
|
-
if (!visible) {
|
|
679
|
-
state.culled = true;
|
|
680
|
-
state.projections = [];
|
|
681
|
-
state.draws = [];
|
|
682
|
-
state.depthSampledDraws = [];
|
|
683
|
-
continue;
|
|
684
|
-
}
|
|
685
|
-
if (
|
|
686
|
-
state.culled &&
|
|
687
|
-
intent.emitter.simulationWhenCulled === 'restart-on-visible' &&
|
|
688
|
-
!currentIntents.some((candidate) => candidate.reset)
|
|
689
|
-
) {
|
|
690
|
-
state.projections = [];
|
|
691
|
-
state.draws = [];
|
|
692
|
-
state.depthSampledDraws = [];
|
|
693
|
-
continue;
|
|
694
|
-
}
|
|
695
|
-
state.culled = false;
|
|
696
|
-
const colorTarget = target(context.targets, 'scene-color');
|
|
697
|
-
const depthTarget = target(context.targets, 'scene-depth');
|
|
698
|
-
const softParticle = requiresSceneDepth(intent);
|
|
699
|
-
if (softParticle && depthTarget === undefined) continue;
|
|
700
|
-
const eventRing = state.rings[intent.tick % MAX_TICK_RINGS];
|
|
701
|
-
if (eventRing === undefined) continue;
|
|
702
|
-
const meshes = renderers.map((renderer) =>
|
|
703
|
-
renderer.kind === 'mesh' ? options.mesh?.read(state.world, renderer.mesh) : undefined,
|
|
704
|
-
);
|
|
705
|
-
const missingMesh = renderers.find(
|
|
706
|
-
(renderer, index) =>
|
|
707
|
-
renderer.kind === 'mesh' &&
|
|
708
|
-
meshes[index]?.submeshes[renderer.submesh ?? 0] === undefined,
|
|
709
|
-
);
|
|
710
|
-
if (missingMesh?.kind === 'mesh') {
|
|
711
|
-
pendingGraphicsError ??= missingMeshPreparation(missingMesh.mesh);
|
|
712
|
-
continue;
|
|
713
|
-
}
|
|
714
|
-
const indirectWords = new Uint32Array(renderers.length * 5);
|
|
715
|
-
for (const [index, renderer] of renderers.entries()) {
|
|
716
|
-
const mesh = meshes[index];
|
|
717
|
-
const submesh =
|
|
718
|
-
renderer.kind === 'mesh' ? mesh?.submeshes[renderer.submesh ?? 0] : undefined;
|
|
719
|
-
const topologyPlan =
|
|
720
|
-
renderer.kind === 'ribbon' || renderer.kind === 'trail' || renderer.kind === 'beam'
|
|
721
|
-
? createTopologyResourcePlan(renderer)
|
|
722
|
-
: undefined;
|
|
723
|
-
if (topologyPlan !== undefined && !topologyPlan.ok)
|
|
724
|
-
return err(new RenderFeatureStageFailedError(IDENTITY, -1, 'prepare', 'next-frame'));
|
|
725
|
-
indirectWords[index * 5] =
|
|
726
|
-
renderer.kind === 'billboard'
|
|
727
|
-
? 6
|
|
728
|
-
: renderer.kind === 'ribbon' || renderer.kind === 'trail' || renderer.kind === 'beam'
|
|
729
|
-
? 6
|
|
730
|
-
: mesh?.indices === undefined
|
|
425
|
+
indirectWords[rendererIndex * 5] =
|
|
426
|
+
renderer.kind === 'mesh'
|
|
427
|
+
? mesh?.indices === undefined
|
|
731
428
|
? (submesh?.vertexCount ?? 0)
|
|
732
|
-
: (submesh?.indexCount ?? 0)
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
const indirectInit = gpu.prepareBuffer(`${state.names}.indirect`, {
|
|
739
|
-
size: Math.max(1, renderers.length) * 20,
|
|
740
|
-
usage: ['storage', 'indirect'],
|
|
741
|
-
...(state.indirectInitialized ? {} : { data: indirectWords }),
|
|
742
|
-
});
|
|
743
|
-
if (!indirectInit.ok) return indirectInit;
|
|
744
|
-
state.indirectInitialized = true;
|
|
745
|
-
const draws: RenderFeatureDrawRecord[] = [];
|
|
746
|
-
const depthSampledDraws: RenderFeatureDrawRecord[] = [];
|
|
747
|
-
const projections: RendererProjection[] = [];
|
|
748
|
-
for (const [rendererIndex, renderer] of renderers.entries()) {
|
|
749
|
-
const isBillboard = renderer.kind === 'billboard';
|
|
750
|
-
const isTopology =
|
|
751
|
-
renderer.kind === 'ribbon' || renderer.kind === 'trail' || renderer.kind === 'beam';
|
|
752
|
-
const topologyPlan = isTopology ? createTopologyResourcePlan(renderer) : undefined;
|
|
753
|
-
if (topologyPlan !== undefined && !topologyPlan.ok)
|
|
754
|
-
return err(new RenderFeatureStageFailedError(IDENTITY, -1, 'prepare', 'next-frame'));
|
|
755
|
-
const mesh = meshes[rendererIndex];
|
|
756
|
-
const submesh =
|
|
757
|
-
renderer.kind === 'mesh' ? mesh?.submeshes[renderer.submesh ?? 0] : undefined;
|
|
758
|
-
const indexFormat =
|
|
759
|
-
mesh?.indices instanceof Uint32Array ? ('uint32' as const) : ('uint16' as const);
|
|
760
|
-
let material = options.material?.read(state.world, renderer.material);
|
|
761
|
-
if (material !== undefined) {
|
|
762
|
-
const resolvedMaterial = context.graphics.resolveMaterialAsset(material);
|
|
763
|
-
if (!resolvedMaterial.ok) {
|
|
764
|
-
pendingGraphicsError ??= resolvedMaterial.error;
|
|
765
|
-
continue;
|
|
766
|
-
}
|
|
767
|
-
material = resolvedMaterial.value;
|
|
429
|
+
: (submesh?.indexCount ?? 0)
|
|
430
|
+
: 6;
|
|
431
|
+
indirectWords[rendererIndex * 5 + 2] =
|
|
432
|
+
renderer.kind === 'mesh' && mesh?.indices !== undefined
|
|
433
|
+
? (submesh?.indexOffset ?? 0)
|
|
434
|
+
: 0;
|
|
768
435
|
}
|
|
769
|
-
|
|
770
|
-
const
|
|
771
|
-
const
|
|
772
|
-
|
|
436
|
+
|
|
437
|
+
const scratchBytes = (capacity * 2 + Math.ceil(capacity / WORKGROUP_SIZE)) * 4;
|
|
438
|
+
const eventInputBytes = Math.max(
|
|
439
|
+
4,
|
|
440
|
+
eventInputCapacity(intent.emitter) * VFX_EVENT_INPUT_BYTES,
|
|
773
441
|
);
|
|
774
|
-
const
|
|
775
|
-
|
|
776
|
-
`${state.names}.renderer.${rendererIndex}.instances`,
|
|
442
|
+
const eventBytes = Math.max(4, eventCapacity(intent.emitter) * VFX_EVENT_BYTES);
|
|
443
|
+
resources.push(
|
|
777
444
|
{
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
:
|
|
783
|
-
|
|
445
|
+
kind: 'compute-program',
|
|
446
|
+
name: program,
|
|
447
|
+
program: {
|
|
448
|
+
wgsl: intent.emitter.wgsl,
|
|
449
|
+
entryPoints: intent.emitter.reflection.entryPoints,
|
|
450
|
+
bindings: intent.emitter.reflection.bindings,
|
|
451
|
+
},
|
|
784
452
|
},
|
|
785
|
-
);
|
|
786
|
-
if (!projectionInstances.ok) return projectionInstances;
|
|
787
|
-
const projectionHistory = gpu.prepareBuffer(
|
|
788
|
-
`${state.names}.renderer.${rendererIndex}.history`,
|
|
789
453
|
{
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
: 16,
|
|
454
|
+
kind: 'buffer',
|
|
455
|
+
name: particles,
|
|
456
|
+
size: capacity * PARTICLE_BYTES,
|
|
794
457
|
usage: ['storage'],
|
|
795
|
-
...(intent.reset
|
|
796
|
-
? {
|
|
797
|
-
data: resetData(
|
|
798
|
-
renderer.kind === 'trail'
|
|
799
|
-
? Math.max(16, renderer.capacity * renderer.historyLength * 16)
|
|
800
|
-
: 16,
|
|
801
|
-
),
|
|
802
|
-
}
|
|
803
|
-
: {}),
|
|
458
|
+
...(intent.reset ? { data: resetData(capacity * PARTICLE_BYTES) } : {}),
|
|
804
459
|
},
|
|
805
|
-
|
|
806
|
-
if (!projectionHistory.ok) return projectionHistory;
|
|
807
|
-
const projectionRuntime = gpu.prepareBuffer(
|
|
808
|
-
`${state.names}.renderer.${rendererIndex}.runtime`,
|
|
460
|
+
{ kind: 'buffer', name: aliveIndices, size: capacity * 4, usage: ['storage'] },
|
|
809
461
|
{
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
material,
|
|
816
|
-
localToWorld,
|
|
817
|
-
renderer,
|
|
818
|
-
rendererIndex,
|
|
819
|
-
),
|
|
462
|
+
kind: 'buffer',
|
|
463
|
+
name: counters,
|
|
464
|
+
size: COUNTERS_BYTES,
|
|
465
|
+
usage: ['storage'],
|
|
466
|
+
...(intent.reset ? { data: resetData(COUNTERS_BYTES) } : {}),
|
|
820
467
|
},
|
|
821
|
-
);
|
|
822
|
-
if (!projectionRuntime.ok) return projectionRuntime;
|
|
823
|
-
const projectionBindings = gpu.prepareBindings(
|
|
824
|
-
`${state.names}.renderer.${rendererIndex}.bindings`,
|
|
825
468
|
{
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
{ binding: 3, buffer: refs.counters },
|
|
832
|
-
{ binding: 4, buffer: refs.indirect },
|
|
833
|
-
{ binding: 5, buffer: projectionHistory.value },
|
|
834
|
-
{ binding: 6, buffer: projectionInstances.value },
|
|
835
|
-
{ binding: 8, buffer: refs.eventInputs },
|
|
836
|
-
{ binding: 9, buffer: refs.events },
|
|
837
|
-
],
|
|
469
|
+
kind: 'buffer',
|
|
470
|
+
name: indirect,
|
|
471
|
+
size: indirectWords.byteLength,
|
|
472
|
+
usage: ['storage', 'indirect'],
|
|
473
|
+
data: indirectWords,
|
|
838
474
|
},
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
runtime: projectionRuntime.value,
|
|
846
|
-
bindings: projectionBindings.value,
|
|
475
|
+
{
|
|
476
|
+
kind: 'buffer',
|
|
477
|
+
name: scratch,
|
|
478
|
+
size: scratchBytes,
|
|
479
|
+
usage: ['storage'],
|
|
480
|
+
...(intent.reset ? { data: resetData(scratchBytes) } : {}),
|
|
847
481
|
},
|
|
848
|
-
workgroups: Math.ceil(
|
|
849
|
-
(renderer.kind === 'trail'
|
|
850
|
-
? renderer.capacity * Math.max(1, renderer.historyLength - 1)
|
|
851
|
-
: renderer.kind === 'ribbon' || renderer.kind === 'beam'
|
|
852
|
-
? renderer.capacity
|
|
853
|
-
: state.capacity) / WORKGROUP_SIZE,
|
|
854
|
-
),
|
|
855
|
-
...(renderer.kind === 'trail'
|
|
856
|
-
? { historyWorkgroups: Math.ceil(renderer.capacity / WORKGROUP_SIZE) }
|
|
857
|
-
: {}),
|
|
858
|
-
...(renderer.kind === 'billboard' ? { sorting: renderer.sorting ?? 'none' } : {}),
|
|
859
|
-
});
|
|
860
|
-
const pipeline = context.graphics.preparePipeline(
|
|
861
|
-
`${state.names}.renderer.${rendererIndex}.${renderer.kind}.pipeline.${materialKey}`,
|
|
862
482
|
{
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
? RENDER_FEATURE_VERTEX_LAYOUTS.topologySegmentInstance
|
|
868
|
-
: RENDER_FEATURE_VERTEX_LAYOUTS.meshGeometryMaterialInstance,
|
|
869
|
-
colorFormats: [colorTarget?.format ?? 'rgba8unorm-srgb'],
|
|
870
|
-
...(depthTarget === undefined ? {} : { depthFormat: depthTarget.format }),
|
|
871
|
-
sampleCount: colorTarget?.sampleCount ?? 1,
|
|
872
|
-
topology: submesh?.topology ?? 'triangle-list',
|
|
873
|
-
...(mesh?.indices === undefined ? {} : { indexFormat }),
|
|
874
|
-
...(materialPass.renderState !== undefined
|
|
875
|
-
? { renderState: materialPass.renderState }
|
|
876
|
-
: isBillboard || isTopology
|
|
877
|
-
? {
|
|
878
|
-
renderState: {
|
|
879
|
-
cullMode: 'none',
|
|
880
|
-
depthCompare: 'less-equal',
|
|
881
|
-
depthWriteEnabled:
|
|
882
|
-
softParticle || isTopology ? false : particleBlend === 'opaque-cutout',
|
|
883
|
-
...(particleBlend === 'opaque-cutout'
|
|
884
|
-
? {}
|
|
885
|
-
: {
|
|
886
|
-
blend: {
|
|
887
|
-
color: {
|
|
888
|
-
srcFactor: 'one',
|
|
889
|
-
dstFactor:
|
|
890
|
-
particleBlend === 'additive' ? 'one' : 'one-minus-src-alpha',
|
|
891
|
-
operation: 'add',
|
|
892
|
-
},
|
|
893
|
-
alpha: {
|
|
894
|
-
srcFactor: 'one',
|
|
895
|
-
dstFactor: 'one-minus-src-alpha',
|
|
896
|
-
operation: 'add',
|
|
897
|
-
},
|
|
898
|
-
},
|
|
899
|
-
}),
|
|
900
|
-
},
|
|
901
|
-
}
|
|
902
|
-
: {}),
|
|
483
|
+
kind: 'buffer',
|
|
484
|
+
name: sharedInstances,
|
|
485
|
+
size: capacity * Math.max(BILLBOARD_INSTANCE_BYTES, MESH_INSTANCE_BYTES),
|
|
486
|
+
usage: ['storage', 'vertex'],
|
|
903
487
|
},
|
|
904
|
-
);
|
|
905
|
-
if (!pipeline.ok) {
|
|
906
|
-
if (
|
|
907
|
-
pipeline.error.code !== 'render-feature-preparation-failed' ||
|
|
908
|
-
pipeline.error.detail.recovery !== 'next-frame'
|
|
909
|
-
) {
|
|
910
|
-
return pipeline;
|
|
911
|
-
}
|
|
912
|
-
pendingGraphicsError ??= pipeline.error;
|
|
913
|
-
continue;
|
|
914
|
-
}
|
|
915
|
-
const graphicsBindings = context.graphics.prepareBindings(
|
|
916
|
-
`${state.names}.renderer.${rendererIndex}.${renderer.kind}.binding.${materialKey}`,
|
|
917
488
|
{
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
? { sceneDepth: depthTarget }
|
|
924
|
-
: {}),
|
|
925
|
-
},
|
|
489
|
+
kind: 'buffer',
|
|
490
|
+
name: eventInputs,
|
|
491
|
+
size: eventInputBytes,
|
|
492
|
+
usage: ['storage'],
|
|
493
|
+
data: encodeEventInputs(intent),
|
|
926
494
|
},
|
|
927
|
-
);
|
|
928
|
-
if (!graphicsBindings.ok) return graphicsBindings;
|
|
929
|
-
const materialBindings = !particleMaterialUsesBindings(material)
|
|
930
|
-
? undefined
|
|
931
|
-
: context.graphics.prepareBindings(
|
|
932
|
-
`${state.names}.renderer.${rendererIndex}.${renderer.kind}.material-binding.${materialKey}`,
|
|
933
|
-
{
|
|
934
|
-
pipeline: pipeline.value,
|
|
935
|
-
values: {
|
|
936
|
-
group: 1,
|
|
937
|
-
material: {
|
|
938
|
-
world: frame.worlds.findIndex((entry) => entry.world === state.world),
|
|
939
|
-
guid: renderer.material,
|
|
940
|
-
},
|
|
941
|
-
},
|
|
942
|
-
},
|
|
943
|
-
);
|
|
944
|
-
if (materialBindings !== undefined && !materialBindings.ok) return materialBindings;
|
|
945
|
-
const drawBindings = [
|
|
946
|
-
graphicsBindings.value,
|
|
947
|
-
...(materialBindings === undefined ? [] : [materialBindings.value]),
|
|
948
|
-
];
|
|
949
|
-
if (isBillboard || isTopology) {
|
|
950
|
-
const vertexData = context.graphics.prepareVertexData(
|
|
951
|
-
`${state.names}.${isTopology ? renderer.kind : 'billboard'}.vertices`,
|
|
952
|
-
{
|
|
953
|
-
layout: isTopology
|
|
954
|
-
? RENDER_FEATURE_VERTEX_LAYOUTS.topologySegmentInstance
|
|
955
|
-
: RENDER_FEATURE_VERTEX_LAYOUTS.billboardMaterialInstance,
|
|
956
|
-
buffer: projectionInstances.value,
|
|
957
|
-
},
|
|
958
|
-
);
|
|
959
|
-
if (!vertexData.ok) return vertexData;
|
|
960
|
-
const draw: RenderFeatureDrawRecord = {
|
|
961
|
-
kind: 'draw-indirect',
|
|
962
|
-
pipeline: pipeline.value,
|
|
963
|
-
bindings: drawBindings,
|
|
964
|
-
vertexData: [{ slot: 0, resource: vertexData.value }],
|
|
965
|
-
command: { buffer: refs.indirect, offset: rendererIndex * 20 },
|
|
966
|
-
};
|
|
967
|
-
(samplesSceneDepth ? depthSampledDraws : draws).push(draw);
|
|
968
|
-
continue;
|
|
969
|
-
}
|
|
970
|
-
if (mesh === undefined) {
|
|
971
|
-
return err(new RenderFeatureStageFailedError(IDENTITY, -1, 'prepare', 'next-frame'));
|
|
972
|
-
}
|
|
973
|
-
const geometryData = canonicalMeshVertices(mesh);
|
|
974
|
-
const geometryBuffer = gpu.prepareBuffer(
|
|
975
|
-
`${state.names}.renderer.${rendererIndex}.mesh.geometry-buffer`,
|
|
976
495
|
{
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
496
|
+
kind: 'buffer',
|
|
497
|
+
name: events,
|
|
498
|
+
size: eventBytes,
|
|
499
|
+
usage: ['storage'],
|
|
500
|
+
...(intent.reset ? { data: resetData(eventBytes) } : {}),
|
|
980
501
|
},
|
|
981
|
-
);
|
|
982
|
-
if (!geometryBuffer.ok) return geometryBuffer;
|
|
983
|
-
const geometry = context.graphics.prepareVertexData(
|
|
984
|
-
`${state.names}.renderer.${rendererIndex}.mesh.geometry`,
|
|
985
502
|
{
|
|
986
|
-
|
|
987
|
-
|
|
503
|
+
kind: 'buffer',
|
|
504
|
+
name: runtime,
|
|
505
|
+
size: RUNTIME_BYTES,
|
|
506
|
+
usage: ['uniform'],
|
|
507
|
+
data: runtimeData(intent, entry.camera, undefined, localToWorld),
|
|
988
508
|
},
|
|
989
|
-
);
|
|
990
|
-
if (!geometry.ok) return geometry;
|
|
991
|
-
const instances = context.graphics.prepareVertexData(
|
|
992
|
-
`${state.names}.renderer.${rendererIndex}.mesh.instances`,
|
|
993
509
|
{
|
|
994
|
-
|
|
995
|
-
|
|
510
|
+
kind: 'compute-bindings',
|
|
511
|
+
name: bindings,
|
|
512
|
+
program,
|
|
513
|
+
entries: computeBindingEntries(intent, {
|
|
514
|
+
0: particles,
|
|
515
|
+
1: runtime,
|
|
516
|
+
2: aliveIndices,
|
|
517
|
+
3: counters,
|
|
518
|
+
4: indirect,
|
|
519
|
+
5: scratch,
|
|
520
|
+
6: sharedInstances,
|
|
521
|
+
8: eventInputs,
|
|
522
|
+
9: events,
|
|
523
|
+
}),
|
|
996
524
|
},
|
|
997
525
|
);
|
|
998
|
-
|
|
999
|
-
const
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
: gpu.prepareBuffer(`${state.names}.renderer.${rendererIndex}.mesh.index-buffer`, {
|
|
1003
|
-
size: mesh.indices.byteLength,
|
|
1004
|
-
usage: ['index'],
|
|
1005
|
-
data: mesh.indices,
|
|
1006
|
-
});
|
|
1007
|
-
if (indexBuffer !== undefined && !indexBuffer.ok) return indexBuffer;
|
|
1008
|
-
const indices =
|
|
1009
|
-
indexBuffer === undefined
|
|
1010
|
-
? undefined
|
|
1011
|
-
: context.graphics.prepareIndexData(
|
|
1012
|
-
`${state.names}.renderer.${rendererIndex}.mesh.indices`,
|
|
1013
|
-
{
|
|
1014
|
-
format: indexFormat,
|
|
1015
|
-
buffer: indexBuffer.value,
|
|
1016
|
-
},
|
|
1017
|
-
);
|
|
1018
|
-
if (indices !== undefined && !indices.ok) return indices;
|
|
1019
|
-
const vertexData = [
|
|
1020
|
-
{ slot: 0, resource: geometry.value },
|
|
1021
|
-
{ slot: 1, resource: instances.value },
|
|
1022
|
-
];
|
|
1023
|
-
draws.push(
|
|
1024
|
-
indices === undefined
|
|
1025
|
-
? {
|
|
1026
|
-
kind: 'draw-indirect',
|
|
1027
|
-
pipeline: pipeline.value,
|
|
1028
|
-
bindings: drawBindings,
|
|
1029
|
-
vertexData,
|
|
1030
|
-
command: { buffer: refs.indirect, offset: rendererIndex * 20 },
|
|
1031
|
-
}
|
|
1032
|
-
: {
|
|
1033
|
-
kind: 'draw-indexed-indirect',
|
|
1034
|
-
pipeline: pipeline.value,
|
|
1035
|
-
bindings: drawBindings,
|
|
1036
|
-
vertexData,
|
|
1037
|
-
indexData: { resource: indices.value, format: indexFormat },
|
|
1038
|
-
command: { buffer: refs.indirect, offset: rendererIndex * 20 },
|
|
1039
|
-
},
|
|
526
|
+
|
|
527
|
+
const entryPoints = new Set(intent.emitter.reflection.entryPoints);
|
|
528
|
+
const dispatches = simulationDispatches(intent, stagePlan.value).filter((dispatch) =>
|
|
529
|
+
entryPoints.has(dispatch.entryPoint),
|
|
1040
530
|
);
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
state.depthTarget = depthTarget;
|
|
1047
|
-
}
|
|
1048
|
-
if (pendingGraphicsError !== undefined) return err(pendingGraphicsError);
|
|
1049
|
-
return ok(undefined);
|
|
1050
|
-
},
|
|
1051
|
-
contribute: (frame, context) => {
|
|
1052
|
-
let dispatchCount = 0;
|
|
1053
|
-
let indirectDrawCount = 0;
|
|
1054
|
-
let subjectOutputCount = 0;
|
|
1055
|
-
for (const state of states.values()) {
|
|
1056
|
-
const refs = state.refs;
|
|
1057
|
-
const firstProjection = state.projections[0];
|
|
1058
|
-
if (refs === undefined) continue;
|
|
1059
|
-
const extracted = frame.worlds.find((entry) => entry.world === state.world);
|
|
1060
|
-
if (extracted === undefined) continue;
|
|
1061
|
-
if (options.playerConsumption?.isEnabled(state.world, state.player) === false) continue;
|
|
1062
|
-
if (!extracted.runtime.isEmitterSessionEnabled(state.player, state.emitterId)) continue;
|
|
1063
|
-
const currentIntents = extracted.intents.filter(
|
|
1064
|
-
(intent) => intent.player === state.player && intent.emitter.id === state.emitterId,
|
|
1065
|
-
);
|
|
1066
|
-
const intents = currentIntents.some(
|
|
1067
|
-
(intent) => intent.programFingerprint === state.fingerprint,
|
|
1068
|
-
)
|
|
1069
|
-
? currentIntents.filter((intent) => intent.programFingerprint === state.fingerprint)
|
|
1070
|
-
: state.lastIntent === undefined
|
|
1071
|
-
? []
|
|
1072
|
-
: [state.lastIntent];
|
|
1073
|
-
const groups = Math.ceil(state.capacity / WORKGROUP_SIZE);
|
|
1074
|
-
const dispatches = intents.flatMap((intent) => {
|
|
1075
|
-
const bindings = state.rings[intent.tick % MAX_TICK_RINGS]?.bindings;
|
|
1076
|
-
if (bindings === undefined) return [];
|
|
1077
|
-
return [
|
|
1078
|
-
{ entryPoint: 'forgeax_vfx_spawn_main', workgroups: [groups] as const, bindings },
|
|
1079
|
-
{ entryPoint: 'forgeax_vfx_update_main', workgroups: [groups] as const, bindings },
|
|
1080
|
-
...stageDispatches(
|
|
1081
|
-
state.stagePlan ?? {
|
|
1082
|
-
stages: [],
|
|
1083
|
-
fingerprint: '',
|
|
1084
|
-
generation: intent.instanceGeneration,
|
|
1085
|
-
},
|
|
1086
|
-
groups,
|
|
1087
|
-
bindings,
|
|
1088
|
-
),
|
|
1089
|
-
{ entryPoint: 'forgeax_vfx_scan_blocks_main', workgroups: [groups] as const, bindings },
|
|
1090
|
-
{
|
|
1091
|
-
entryPoint: 'forgeax_vfx_scan_block_offsets_main',
|
|
1092
|
-
workgroups: [1] as const,
|
|
1093
|
-
bindings,
|
|
1094
|
-
},
|
|
1095
|
-
{ entryPoint: 'forgeax_vfx_add_offsets_main', workgroups: [groups] as const, bindings },
|
|
1096
|
-
{ entryPoint: 'forgeax_vfx_compact_main', workgroups: [groups] as const, bindings },
|
|
1097
|
-
{
|
|
1098
|
-
entryPoint: 'forgeax_vfx_event_main',
|
|
1099
|
-
workgroups: [Math.ceil(eventInputCapacity(intent.emitter) / 64)] as const,
|
|
531
|
+
if (dispatches.length > 0) {
|
|
532
|
+
passes.push({
|
|
533
|
+
kind: 'compute',
|
|
534
|
+
name: `${prefix}.simulate`,
|
|
535
|
+
program,
|
|
1100
536
|
bindings,
|
|
1101
|
-
|
|
1102
|
-
];
|
|
1103
|
-
});
|
|
1104
|
-
for (const projection of state.projections) {
|
|
1105
|
-
if (projection.kind === 'billboard' && projection.sorting === 'back-to-front') {
|
|
1106
|
-
dispatches.push({
|
|
1107
|
-
entryPoint: 'forgeax_vfx_sort_main',
|
|
1108
|
-
workgroups: [1],
|
|
1109
|
-
bindings: projection.ring.bindings,
|
|
1110
|
-
});
|
|
1111
|
-
}
|
|
1112
|
-
if (projection.kind === 'trail') {
|
|
1113
|
-
dispatches.push({
|
|
1114
|
-
entryPoint: 'forgeax_vfx_trail_history_main',
|
|
1115
|
-
workgroups: [projection.historyWorkgroups ?? 1],
|
|
1116
|
-
bindings: projection.ring.bindings,
|
|
537
|
+
dispatches,
|
|
1117
538
|
});
|
|
539
|
+
dispatchedIntents.add(intent);
|
|
1118
540
|
}
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
541
|
+
|
|
542
|
+
for (const [rendererIndex, renderer] of renderers.entries()) {
|
|
543
|
+
const rendererPrefix = `${prefix}.renderer-${rendererIndex}`;
|
|
544
|
+
const isBillboard = renderer.kind === 'billboard';
|
|
545
|
+
const isTopology =
|
|
546
|
+
renderer.kind === 'ribbon' || renderer.kind === 'trail' || renderer.kind === 'beam';
|
|
547
|
+
const topologyPlan = isTopology ? createTopologyResourcePlan(renderer) : undefined;
|
|
548
|
+
if (topologyPlan !== undefined && !topologyPlan.ok) return err(planFailure());
|
|
549
|
+
const material = options.material?.read(entry.world, renderer.material);
|
|
550
|
+
const materialPass = particleMaterialPass(renderer.kind, material);
|
|
551
|
+
const mesh = meshes[rendererIndex];
|
|
552
|
+
const submesh =
|
|
553
|
+
renderer.kind === 'mesh' ? mesh?.submeshes[renderer.submesh ?? 0] : undefined;
|
|
554
|
+
const indexFormat = mesh?.indices instanceof Uint32Array ? 'uint32' : 'uint16';
|
|
555
|
+
const instances = `${rendererPrefix}.instances`;
|
|
556
|
+
const history = `${rendererPrefix}.history`;
|
|
557
|
+
const projectionRuntime = `${rendererPrefix}.runtime`;
|
|
558
|
+
const projectionBindings = `${rendererPrefix}.compute-bindings`;
|
|
559
|
+
const vertexLayout = isBillboard
|
|
560
|
+
? RENDER_FEATURE_VERTEX_LAYOUTS.billboardMaterialInstance
|
|
561
|
+
: isTopology
|
|
562
|
+
? RENDER_FEATURE_VERTEX_LAYOUTS.topologySegmentInstance
|
|
563
|
+
: RENDER_FEATURE_VERTEX_LAYOUTS.meshGeometryMaterialInstance;
|
|
564
|
+
const instanceBytes = isTopology
|
|
565
|
+
? (topologyPlan?.value.vertexBytes ?? 16)
|
|
566
|
+
: capacity * (isBillboard ? BILLBOARD_INSTANCE_BYTES : MESH_INSTANCE_BYTES);
|
|
567
|
+
const historyBytes =
|
|
568
|
+
renderer.kind === 'trail'
|
|
569
|
+
? Math.max(16, renderer.capacity * renderer.historyLength * 16)
|
|
570
|
+
: 16;
|
|
571
|
+
|
|
572
|
+
resources.push(
|
|
573
|
+
{
|
|
574
|
+
kind: 'buffer',
|
|
575
|
+
name: instances,
|
|
576
|
+
size: instanceBytes,
|
|
577
|
+
usage: ['storage', 'vertex'],
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
kind: 'buffer',
|
|
581
|
+
name: history,
|
|
582
|
+
size: historyBytes,
|
|
583
|
+
usage: ['storage'],
|
|
584
|
+
...(intent.reset ? { data: resetData(historyBytes) } : {}),
|
|
585
|
+
},
|
|
586
|
+
{
|
|
587
|
+
kind: 'buffer',
|
|
588
|
+
name: projectionRuntime,
|
|
589
|
+
size: RUNTIME_BYTES,
|
|
590
|
+
usage: ['uniform'],
|
|
591
|
+
data: runtimeData(
|
|
592
|
+
{ ...intent, fixedDelta: 0, spawnCount: 0 },
|
|
593
|
+
entry.camera,
|
|
594
|
+
material,
|
|
595
|
+
localToWorld,
|
|
596
|
+
renderer,
|
|
597
|
+
rendererIndex,
|
|
598
|
+
),
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
kind: 'compute-bindings',
|
|
602
|
+
name: projectionBindings,
|
|
603
|
+
program,
|
|
604
|
+
entries: computeBindingEntries(intent, {
|
|
605
|
+
0: particles,
|
|
606
|
+
1: projectionRuntime,
|
|
607
|
+
2: aliveIndices,
|
|
608
|
+
3: counters,
|
|
609
|
+
4: indirect,
|
|
610
|
+
5: history,
|
|
611
|
+
6: instances,
|
|
612
|
+
8: eventInputs,
|
|
613
|
+
9: events,
|
|
614
|
+
}),
|
|
615
|
+
},
|
|
616
|
+
);
|
|
617
|
+
|
|
618
|
+
const projectionDispatches: Extract<
|
|
619
|
+
PlanPass,
|
|
620
|
+
{ readonly kind: 'compute' }
|
|
621
|
+
>['dispatches'][number][] = [];
|
|
622
|
+
const pushProjection = (entryPoint: string, workgroups: number): void => {
|
|
623
|
+
if (!entryPoints.has(entryPoint)) return;
|
|
624
|
+
projectionDispatches.push({
|
|
625
|
+
kind: 'direct',
|
|
626
|
+
entryPoint,
|
|
627
|
+
workgroups: [Math.max(1, workgroups)],
|
|
628
|
+
});
|
|
629
|
+
};
|
|
630
|
+
if (isBillboard && renderer.sorting === 'back-to-front') {
|
|
631
|
+
pushProjection('forgeax_vfx_sort_main', 1);
|
|
632
|
+
}
|
|
633
|
+
if (renderer.kind === 'trail') {
|
|
634
|
+
pushProjection(
|
|
635
|
+
'forgeax_vfx_trail_history_main',
|
|
636
|
+
Math.ceil(renderer.capacity / WORKGROUP_SIZE),
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
const projectionCount =
|
|
640
|
+
renderer.kind === 'trail'
|
|
641
|
+
? renderer.capacity * Math.max(1, renderer.historyLength - 1)
|
|
642
|
+
: isTopology
|
|
643
|
+
? renderer.capacity
|
|
644
|
+
: capacity;
|
|
645
|
+
pushProjection(
|
|
646
|
+
renderer.kind === 'billboard'
|
|
1122
647
|
? 'forgeax_vfx_billboard_main'
|
|
1123
|
-
:
|
|
648
|
+
: renderer.kind === 'mesh'
|
|
1124
649
|
? 'forgeax_vfx_mesh_main'
|
|
1125
|
-
: `forgeax_vfx_${
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
650
|
+
: `forgeax_vfx_${renderer.kind}_main`,
|
|
651
|
+
Math.ceil(projectionCount / WORKGROUP_SIZE),
|
|
652
|
+
);
|
|
653
|
+
if (projectionDispatches.length > 0) {
|
|
654
|
+
passes.push({
|
|
655
|
+
kind: 'compute',
|
|
656
|
+
name: `${rendererPrefix}.project`,
|
|
657
|
+
program,
|
|
658
|
+
bindings: projectionBindings,
|
|
659
|
+
dispatches: projectionDispatches,
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
const graphicsProgram = `${rendererPrefix}.graphics-program`;
|
|
664
|
+
const graphicsBindings = `${rendererPrefix}.graphics-bindings`;
|
|
665
|
+
const vertexData = `${rendererPrefix}.vertex-data`;
|
|
666
|
+
const renderState =
|
|
667
|
+
isBillboard && depthTarget !== undefined
|
|
668
|
+
? { ...(materialPass.renderState ?? {}), depthWriteEnabled: false }
|
|
669
|
+
: materialPass.renderState;
|
|
670
|
+
resources.push(
|
|
671
|
+
{
|
|
672
|
+
kind: 'graphics-program',
|
|
673
|
+
name: graphicsProgram,
|
|
674
|
+
program: {
|
|
675
|
+
shader: materialPass.shader,
|
|
676
|
+
vertexLayout,
|
|
677
|
+
colorFormats: [colorTarget?.format ?? 'rgba8unorm-srgb'],
|
|
678
|
+
...(depthTarget === undefined ? {} : { depthFormat: depthTarget.format }),
|
|
679
|
+
sampleCount: colorTarget?.sampleCount ?? 1,
|
|
680
|
+
topology: submesh?.topology ?? 'triangle-list',
|
|
681
|
+
...(mesh?.indices === undefined ? {} : { indexFormat }),
|
|
682
|
+
...(renderState === undefined ? {} : { renderState }),
|
|
683
|
+
},
|
|
684
|
+
},
|
|
685
|
+
{
|
|
686
|
+
kind: 'graphics-bindings',
|
|
687
|
+
name: graphicsBindings,
|
|
688
|
+
program: graphicsProgram,
|
|
689
|
+
values: {
|
|
690
|
+
group: 0,
|
|
691
|
+
runtime: projectionRuntime,
|
|
692
|
+
instances,
|
|
693
|
+
...(isBillboard ? { sceneDepthBinding: 1 } : {}),
|
|
694
|
+
},
|
|
695
|
+
...(isBillboard && depthTarget !== undefined
|
|
696
|
+
? { logicalTargets: { sceneDepth: depthTarget.name } }
|
|
697
|
+
: {}),
|
|
698
|
+
},
|
|
699
|
+
{ kind: 'vertex-data', name: vertexData, layout: vertexLayout, buffer: instances },
|
|
700
|
+
);
|
|
701
|
+
const drawBindings = [graphicsBindings];
|
|
702
|
+
if (particleMaterialUsesBindings(material)) {
|
|
703
|
+
const materialBindings = `${rendererPrefix}.material-bindings`;
|
|
704
|
+
resources.push({
|
|
705
|
+
kind: 'graphics-bindings',
|
|
706
|
+
name: materialBindings,
|
|
707
|
+
program: graphicsProgram,
|
|
708
|
+
values: {
|
|
709
|
+
group: 1,
|
|
710
|
+
material: { world: worldIndex, guid: renderer.material },
|
|
711
|
+
},
|
|
712
|
+
});
|
|
713
|
+
drawBindings.push(materialBindings);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const vertexBindings: { readonly slot: number; readonly resource: string }[] = [];
|
|
717
|
+
let indexData:
|
|
718
|
+
| { readonly resource: string; readonly format: 'uint16' | 'uint32' }
|
|
719
|
+
| undefined;
|
|
720
|
+
if (renderer.kind === 'mesh') {
|
|
721
|
+
if (mesh === undefined) return err(planFailure());
|
|
722
|
+
const geometryBuffer = `${rendererPrefix}.geometry-buffer`;
|
|
723
|
+
const geometry = `${rendererPrefix}.geometry`;
|
|
724
|
+
const geometryData = canonicalMeshVertices(mesh);
|
|
725
|
+
resources.push(
|
|
726
|
+
{
|
|
727
|
+
kind: 'buffer',
|
|
728
|
+
name: geometryBuffer,
|
|
729
|
+
size: geometryData.byteLength,
|
|
730
|
+
usage: ['vertex'],
|
|
731
|
+
data: geometryData,
|
|
732
|
+
},
|
|
733
|
+
{
|
|
734
|
+
kind: 'vertex-data',
|
|
735
|
+
name: geometry,
|
|
736
|
+
layout: vertexLayout,
|
|
737
|
+
buffer: geometryBuffer,
|
|
738
|
+
},
|
|
739
|
+
);
|
|
740
|
+
vertexBindings.push(
|
|
741
|
+
{ slot: 0, resource: geometry },
|
|
742
|
+
{ slot: 1, resource: vertexData },
|
|
743
|
+
);
|
|
744
|
+
if (mesh.indices !== undefined) {
|
|
745
|
+
const indexBuffer = `${rendererPrefix}.index-buffer`;
|
|
746
|
+
const indices = `${rendererPrefix}.indices`;
|
|
747
|
+
resources.push(
|
|
1158
748
|
{
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
749
|
+
kind: 'buffer',
|
|
750
|
+
name: indexBuffer,
|
|
751
|
+
size: mesh.indices.byteLength,
|
|
752
|
+
usage: ['index'],
|
|
753
|
+
data: mesh.indices,
|
|
1163
754
|
},
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
755
|
+
{
|
|
756
|
+
kind: 'index-data',
|
|
757
|
+
name: indices,
|
|
758
|
+
format: indexFormat,
|
|
759
|
+
buffer: indexBuffer,
|
|
760
|
+
},
|
|
761
|
+
);
|
|
762
|
+
indexData = { resource: indices, format: indexFormat };
|
|
763
|
+
}
|
|
764
|
+
} else {
|
|
765
|
+
vertexBindings.push({ slot: 0, resource: vertexData });
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
passes.push({
|
|
769
|
+
kind: 'raster',
|
|
770
|
+
name: `${rendererPrefix}.raster`,
|
|
771
|
+
colorAttachments: [
|
|
772
|
+
{
|
|
773
|
+
target: colorTarget?.name ?? 'swapchain',
|
|
774
|
+
loadOp: 'load',
|
|
775
|
+
storeOp: 'store',
|
|
776
|
+
},
|
|
777
|
+
],
|
|
778
|
+
...(depthTarget === undefined
|
|
1177
779
|
? {}
|
|
1178
|
-
: {
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
780
|
+
: {
|
|
781
|
+
depthStencilAttachment: {
|
|
782
|
+
target: depthTarget.name,
|
|
783
|
+
depthLoadOp: 'load',
|
|
784
|
+
depthStoreOp: 'store',
|
|
785
|
+
},
|
|
786
|
+
}),
|
|
787
|
+
...(isBillboard && depthTarget !== undefined
|
|
788
|
+
? { sampledTargets: [depthTarget.name] }
|
|
789
|
+
: {}),
|
|
790
|
+
draws: [
|
|
791
|
+
{
|
|
792
|
+
program: graphicsProgram,
|
|
793
|
+
bindings: drawBindings,
|
|
794
|
+
vertexData: vertexBindings,
|
|
795
|
+
...(indexData === undefined ? {} : { indexData }),
|
|
796
|
+
draw: {
|
|
797
|
+
kind: indexData === undefined ? 'draw-indirect' : 'draw-indexed-indirect',
|
|
798
|
+
resource: indirect,
|
|
799
|
+
offset: rendererIndex * 20,
|
|
800
|
+
},
|
|
801
|
+
},
|
|
802
|
+
],
|
|
803
|
+
});
|
|
804
|
+
}
|
|
1189
805
|
}
|
|
1190
806
|
}
|
|
1191
807
|
for (const entry of frame.worlds) {
|
|
1192
|
-
const
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
subjectOutputs: subjectOutputCount,
|
|
1200
|
-
});
|
|
1201
|
-
return ok(undefined);
|
|
1202
|
-
},
|
|
1203
|
-
recover: () => {
|
|
1204
|
-
const runtimes = new Set<VfxGpuRuntime>();
|
|
1205
|
-
for (const state of states.values()) {
|
|
1206
|
-
if (!state.world.hasResource(VFX_GPU_RUNTIME_RESOURCE_KEY)) continue;
|
|
1207
|
-
runtimes.add(state.world.getResource<VfxGpuRuntime>(VFX_GPU_RUNTIME_RESOURCE_KEY));
|
|
808
|
+
for (const intent of entry.intents) {
|
|
809
|
+
if (dispatchedIntents.has(intent)) {
|
|
810
|
+
entry.runtime.markEventDispatched(intent.player, intent.eventCounters);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
const lastIntent = entry.intents.at(-1);
|
|
814
|
+
if (lastIntent !== undefined) entry.runtime.commit(lastIntent.sequence);
|
|
1208
815
|
}
|
|
1209
|
-
|
|
1210
|
-
states.clear();
|
|
1211
|
-
return ok(undefined);
|
|
1212
|
-
},
|
|
1213
|
-
dispose: () => {
|
|
1214
|
-
states.clear();
|
|
1215
|
-
lastObservation = Object.freeze({
|
|
1216
|
-
frameNumber: -1,
|
|
1217
|
-
dispatches: 0,
|
|
1218
|
-
indirectDraws: 0,
|
|
1219
|
-
subjectOutputs: 0,
|
|
1220
|
-
});
|
|
1221
|
-
return ok(undefined);
|
|
816
|
+
return ok<RenderFeaturePlan>({ resources, passes });
|
|
1222
817
|
},
|
|
1223
|
-
inspect: () => lastObservation,
|
|
1224
818
|
};
|
|
1225
819
|
}
|