@woosh/meep-engine 3.34.0 → 3.36.0
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/package.json +1 -1
- package/src/engine/Engine.d.ts +2 -0
- package/src/engine/Engine.d.ts.map +1 -1
- package/src/engine/Engine.js +5 -1
- package/src/engine/input/devices/XRInputDevice.d.ts +63 -0
- package/src/engine/input/devices/XRInputDevice.d.ts.map +1 -0
- package/src/engine/input/devices/XRInputDevice.js +386 -0
- package/src/engine/input/devices/xr/XRInputAction.d.ts +37 -0
- package/src/engine/input/devices/xr/XRInputAction.d.ts.map +1 -0
- package/src/engine/input/devices/xr/XRInputAction.js +85 -0
- package/src/engine/input/devices/xr/XRInputButton.d.ts +28 -0
- package/src/engine/input/devices/xr/XRInputButton.d.ts.map +1 -0
- package/src/engine/input/devices/xr/XRInputButton.js +43 -0
- package/src/engine/input/devices/xr/XRInputCoordinates.d.ts +15 -0
- package/src/engine/input/devices/xr/XRInputCoordinates.d.ts.map +1 -0
- package/src/engine/input/devices/xr/XRInputCoordinates.js +21 -0
- package/src/engine/input/devices/xr/XRInputHandle.d.ts +63 -0
- package/src/engine/input/devices/xr/XRInputHandle.d.ts.map +1 -0
- package/src/engine/input/devices/xr/XRInputHandle.js +171 -0
- package/src/engine/input/devices/xr/XRInputPose.d.ts +23 -0
- package/src/engine/input/devices/xr/XRInputPose.d.ts.map +1 -0
- package/src/engine/input/devices/xr/XRInputPose.js +36 -0
- package/src/engine/input/devices/xr/XRInputTypes.d.ts +76 -0
- package/src/engine/input/devices/xr/XRInputTypes.d.ts.map +1 -0
- package/src/engine/input/devices/xr/XRInputTypes.js +31 -0
- package/src/engine/input/devices/xr/XRInputValueSource.d.ts +11 -0
- package/src/engine/input/devices/xr/XRInputValueSource.d.ts.map +1 -0
- package/src/engine/input/devices/xr/XRInputValueSource.js +11 -0
- package/src/engine/input/ecs/ism/coordinate/InputCoordinateChannel.d.ts +15 -1
- package/src/engine/input/ecs/ism/coordinate/InputCoordinateChannel.d.ts.map +1 -1
- package/src/engine/input/ecs/ism/coordinate/InputCoordinateChannel.js +9 -0
- package/src/engine/input/ecs/ism/coordinate/InputCoordinateDevice.d.ts +15 -4
- package/src/engine/input/ecs/ism/coordinate/InputCoordinateDevice.d.ts.map +1 -1
- package/src/engine/input/ecs/ism/coordinate/InputCoordinateDevice.js +22 -0
- package/src/engine/input/ecs/ism/coordinate/InputCoordinateSwitches.d.ts +17 -2
- package/src/engine/input/ecs/ism/coordinate/InputCoordinateSwitches.d.ts.map +1 -1
- package/src/engine/input/ecs/ism/device/GamepadInputDeviceAdapter.d.ts +5 -0
- package/src/engine/input/ecs/ism/device/GamepadInputDeviceAdapter.d.ts.map +1 -1
- package/src/engine/input/ecs/ism/device/InputDeviceAdapter.d.ts +2 -0
- package/src/engine/input/ecs/ism/device/InputDeviceAdapter.d.ts.map +1 -1
- package/src/engine/input/ecs/ism/device/InputDeviceAdapter.js +2 -0
- package/src/engine/input/ecs/ism/device/KeyboardInputDeviceAdapter.d.ts +7 -0
- package/src/engine/input/ecs/ism/device/KeyboardInputDeviceAdapter.d.ts.map +1 -1
- package/src/engine/input/ecs/ism/device/ManualInputDeviceAdapter.d.ts +5 -0
- package/src/engine/input/ecs/ism/device/ManualInputDeviceAdapter.d.ts.map +1 -1
- package/src/engine/input/ecs/ism/device/PointerInputDeviceAdapter.d.ts +5 -0
- package/src/engine/input/ecs/ism/device/PointerInputDeviceAdapter.d.ts.map +1 -1
- package/src/engine/input/ecs/ism/device/XRInputDeviceAdapter.d.ts +16 -0
- package/src/engine/input/ecs/ism/device/XRInputDeviceAdapter.d.ts.map +1 -0
- package/src/engine/input/ecs/ism/device/XRInputDeviceAdapter.js +54 -0
- package/src/engine/input/ecs/ism/device/XR_INPUT_DEVICE_IDS.d.ts +9 -0
- package/src/engine/input/ecs/ism/device/XR_INPUT_DEVICE_IDS.d.ts.map +1 -0
- package/src/engine/input/ecs/ism/device/XR_INPUT_DEVICE_IDS.js +12 -0
- package/src/engine/input/ecs/ism/trigger/InputTrigger.d.ts +11 -1
- package/src/engine/input/ecs/ism/trigger/InputTrigger.d.ts.map +1 -1
- package/src/engine/input/ecs/ism/trigger/InputTrigger.js +7 -0
- package/src/engine/input/ecs/ism/trigger/InputTriggerChord.d.ts +16 -2
- package/src/engine/input/ecs/ism/trigger/InputTriggerChord.d.ts.map +1 -1
- package/src/engine/input/ecs/ism/trigger/InputTriggerHold.d.ts +22 -2
- package/src/engine/input/ecs/ism/trigger/InputTriggerHold.d.ts.map +1 -1
- package/src/engine/input/ecs/ism/trigger/InputTriggerRelease.d.ts +16 -2
- package/src/engine/input/ecs/ism/trigger/InputTriggerRelease.d.ts.map +1 -1
- package/src/engine/input/ecs/ism/trigger/InputTriggerSequence.d.ts +24 -4
- package/src/engine/input/ecs/ism/trigger/InputTriggerSequence.d.ts.map +1 -1
- package/src/engine/input/ecs/ism/trigger/InputTriggerSwitch.d.ts +6 -8
- package/src/engine/input/ecs/ism/trigger/InputTriggerSwitch.d.ts.map +1 -1
- package/src/engine/input/ecs/ism/trigger/InputTriggerSwitch.js +5 -0
- package/src/engine/input/ecs/ism/trigger/InputTriggerXRButton.d.ts +7 -0
- package/src/engine/input/ecs/ism/trigger/InputTriggerXRButton.d.ts.map +1 -0
- package/src/engine/input/ecs/ism/trigger/InputTriggerXRButton.js +15 -0
- package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
- package/src/shade/device/timing/profile/GPUProfileSession.d.ts +25 -11
- package/src/shade/device/timing/profile/GPUProfileSession.d.ts.map +1 -1
- package/src/shade/device/timing/profile/GPUProfileSession.js +553 -474
- package/src/shade/playground/avboit/main.d.ts.map +1 -1
- package/src/shade/playground/avboit/main.js +888 -916
- package/src/shade/playground/particle_ecs/README.md +13 -7
- package/src/shade/playground/particle_ecs/bonfire_effects.d.ts +5 -5
- package/src/shade/playground/particle_ecs/bonfire_effects.js +5 -5
- package/src/shade/playground/particle_ecs/effects/smoke.json +1859 -1859
- package/src/shade/playground/particle_ecs/effects/soot.json +1605 -1605
- package/src/shade/playground/particle_system/README.md +1 -2
- package/src/shade/playground/profile_hotkey.d.ts.map +1 -1
- package/src/shade/playground/profile_hotkey.js +4 -40
- package/src/shade/playground/skinned_blas_refit/main.d.ts.map +1 -1
- package/src/shade/playground/skinned_blas_refit/main.js +4 -39
- package/src/shade/playground/volumetrics_froxel/main.d.ts.map +1 -1
- package/src/shade/playground/volumetrics_froxel/main.js +4 -39
- package/src/shade/playground/volumetrics_froxel/measure.d.ts.map +1 -1
- package/src/shade/playground/volumetrics_froxel/measure.js +2 -4
- package/src/shade/renderer/Renderer.d.ts +1 -1
- package/src/shade/renderer/Renderer.d.ts.map +1 -1
- package/src/shade/renderer/Renderer.js +3 -1
- package/src/shade/renderer/light/GPULightCollection.d.ts +9 -1
- package/src/shade/renderer/light/GPULightCollection.d.ts.map +1 -1
- package/src/shade/renderer/light/GPULightCollection.js +41 -0
- package/src/shade/renderer/light/environment/shader_project_environment_sh.d.ts +37 -0
- package/src/shade/renderer/light/environment/shader_project_environment_sh.d.ts.map +1 -0
- package/src/shade/renderer/light/environment/shader_project_environment_sh.js +105 -0
- package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
- package/src/shade/renderer/particles/DESIGN.md +65 -8
- package/src/shade/renderer/particles/GPUParticleSystem.d.ts +7 -1
- package/src/shade/renderer/particles/GPUParticleSystem.d.ts.map +1 -1
- package/src/shade/renderer/particles/GPUParticleSystem.js +1247 -1244
- package/src/shade/renderer/particles/graph_particles_avboit.d.ts +7 -1
- package/src/shade/renderer/particles/graph_particles_avboit.d.ts.map +1 -1
- package/src/shade/renderer/particles/graph_particles_avboit.js +9 -0
- package/src/shade/renderer/particles/shaders/chunk_particle_billboard_vertex.d.ts.map +1 -1
- package/src/shade/renderer/particles/shaders/chunk_particle_billboard_vertex.js +4 -2
- package/src/shade/renderer/particles/shaders/chunk_particle_lighting.d.ts +15 -9
- package/src/shade/renderer/particles/shaders/chunk_particle_lighting.d.ts.map +1 -1
- package/src/shade/renderer/particles/shaders/chunk_particle_lighting.js +29 -11
- package/src/shade/renderer/particles/shaders/chunk_particle_lighting_model.d.ts +24 -0
- package/src/shade/renderer/particles/shaders/chunk_particle_lighting_model.d.ts.map +1 -0
- package/src/shade/renderer/particles/shaders/chunk_particle_lighting_model.js +45 -0
- package/src/shade/renderer/particles/shaders/chunk_particle_render_math.d.ts +3 -0
- package/src/shade/renderer/particles/shaders/chunk_particle_render_math.d.ts.map +1 -1
- package/src/shade/renderer/particles/shaders/chunk_particle_render_math.js +38 -0
- package/src/shade/renderer/particles/shaders/shader_particle_avboit_draw.d.ts.map +1 -1
- package/src/shade/renderer/particles/shaders/shader_particle_avboit_draw.js +190 -185
- package/src/shade/renderer/rasterize/native/avboit/AVBOITSideChannel.d.ts +8 -0
- package/src/shade/renderer/rasterize/native/avboit/AVBOITSideChannel.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/native/avboit/AVBOITSideChannel.js +5 -0
- package/src/shade/renderer/rasterize/native/avboit/graph_rasterize_meshes_transparent_avboit.d.ts +8 -1
- package/src/shade/renderer/rasterize/native/avboit/graph_rasterize_meshes_transparent_avboit.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/native/avboit/graph_rasterize_meshes_transparent_avboit.js +9 -0
- package/src/shade/renderer/volumetrics/GPUViewVolumetrics.d.ts +13 -3
- package/src/shade/renderer/volumetrics/GPUViewVolumetrics.d.ts.map +1 -1
- package/src/shade/renderer/volumetrics/GPUViewVolumetrics.js +9 -1
- package/src/shade/renderer/volumetrics/chunk_volumetrics_irradiance_sh.d.ts +39 -0
- package/src/shade/renderer/volumetrics/chunk_volumetrics_irradiance_sh.d.ts.map +1 -0
- package/src/shade/renderer/volumetrics/chunk_volumetrics_irradiance_sh.js +107 -0
- package/src/shade/renderer/volumetrics/gi/chunk_volumetrics_froxel_centre_ws.d.ts +15 -0
- package/src/shade/renderer/volumetrics/gi/chunk_volumetrics_froxel_centre_ws.d.ts.map +1 -0
- package/src/shade/renderer/volumetrics/gi/chunk_volumetrics_froxel_centre_ws.js +44 -0
- package/src/shade/renderer/volumetrics/gi/chunk_volumetrics_gi.d.ts +19 -0
- package/src/shade/renderer/volumetrics/gi/chunk_volumetrics_gi.d.ts.map +1 -0
- package/src/shade/renderer/volumetrics/gi/chunk_volumetrics_gi.js +48 -0
- package/src/shade/renderer/volumetrics/gi/graph_build_froxel_gi.d.ts +29 -0
- package/src/shade/renderer/volumetrics/gi/graph_build_froxel_gi.d.ts.map +1 -0
- package/src/shade/renderer/volumetrics/gi/graph_build_froxel_gi.js +90 -0
- package/src/shade/renderer/volumetrics/gi/shader_volumetrics_gi_brick4.d.ts +10 -0
- package/src/shade/renderer/volumetrics/gi/shader_volumetrics_gi_brick4.d.ts.map +1 -0
- package/src/shade/renderer/volumetrics/gi/shader_volumetrics_gi_brick4.js +55 -0
- package/src/shade/renderer/volumetrics/gi/shader_volumetrics_gi_ibl.d.ts +10 -0
- package/src/shade/renderer/volumetrics/gi/shader_volumetrics_gi_ibl.d.ts.map +1 -0
- package/src/shade/renderer/volumetrics/gi/shader_volumetrics_gi_ibl.js +47 -0
- package/src/shade/renderer/volumetrics/gi/shader_volumetrics_gi_lpv.d.ts +10 -0
- package/src/shade/renderer/volumetrics/gi/shader_volumetrics_gi_lpv.d.ts.map +1 -0
- package/src/shade/renderer/volumetrics/gi/shader_volumetrics_gi_lpv.js +63 -0
- package/src/shade/renderer/volumetrics/gi/volumetrics_gi_resources.d.ts +17 -0
- package/src/shade/renderer/volumetrics/gi/volumetrics_gi_resources.d.ts.map +1 -0
- package/src/shade/renderer/volumetrics/gi/volumetrics_gi_resources.js +26 -0
- package/src/shade/renderer/volumetrics/graph_build_volumetrics.d.ts +8 -1
- package/src/shade/renderer/volumetrics/graph_build_volumetrics.d.ts.map +1 -1
- package/src/shade/renderer/volumetrics/graph_build_volumetrics.js +50 -2
- package/src/shade/renderer/volumetrics/shader_volumetrics_build_lighting.d.ts.map +1 -1
- package/src/shade/renderer/volumetrics/shader_volumetrics_build_lighting.js +488 -436
- package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.d.ts +1 -1
- package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.d.ts.map +1 -1
- package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.js +254 -63
|
@@ -1,1244 +1,1247 @@
|
|
|
1
|
-
import { assert } from "../../../core/assert.js";
|
|
2
|
-
import { ShadeGPUCommandContext } from "../../device/ShadeGPUCommandContext.js";
|
|
3
|
-
import { BufferResourceDescriptor } from "../../device/graph/BufferResourceDescriptor.js";
|
|
4
|
-
import { warn_limited } from "../../util/warn_limited.js";
|
|
5
|
-
import { hash_mix2 } from "../../../core/math/hash/hash_mix2.js";
|
|
6
|
-
import {
|
|
7
|
-
PARTICLE_EMITTER_TICK_WORKGROUP_SIZE,
|
|
8
|
-
PARTICLE_RECORD_WORD_COUNT,
|
|
9
|
-
PARTICLE_SIMULATE_WORKGROUP_SIZE,
|
|
10
|
-
PARTICLE_SPAWN_COMMAND_WORDS,
|
|
11
|
-
PARTICLE_VM_FAST_REGISTER_SLOTS,
|
|
12
|
-
PARTICLE_WARMUP_EMITTER_JITTER,
|
|
13
|
-
PARTICLE_WARMUP_QUEUE_WORDS,
|
|
14
|
-
} from "./ParticleConstants.js";
|
|
15
|
-
import {
|
|
16
|
-
PARTICLE_BOUNDS_WORDS,
|
|
17
|
-
PARTICLE_COUNTER,
|
|
18
|
-
PARTICLE_COUNTER_COUNT,
|
|
19
|
-
particle_bounds_empty_words,
|
|
20
|
-
particle_counters_word_count,
|
|
21
|
-
} from "./data/PARTICLE_COUNTERS.js";
|
|
22
|
-
import { graph_particles_simulate } from "./graph_particles.js";
|
|
23
|
-
import {
|
|
24
|
-
PARTICLE_INIT_FREE_LIST_WORKGROUP_SIZE,
|
|
25
|
-
shader_particle_init_free_list,
|
|
26
|
-
} from "./shaders/shader_particle_init_free_list.js";
|
|
27
|
-
import {
|
|
28
|
-
graph_particle_avboit_draw,
|
|
29
|
-
graph_particle_avboit_occupancy,
|
|
30
|
-
graph_particle_avboit_splat,
|
|
31
|
-
} from "./graph_particles_avboit.js";
|
|
32
|
-
import { EmitterRegistry } from "./runtime/EmitterRegistry.js";
|
|
33
|
-
import { WARMUP_QUEUE } from "./warmup/shader_particle_warmup.js";
|
|
34
|
-
import { warmup_schedule } from "./warmup/warmup_schedule.js";
|
|
35
|
-
import { GPUTextureContext } from "../texture/GPUTextureContext.js";
|
|
36
|
-
import { inspect_gpu_buffer } from "../buffer/inspect_gpu_buffer.js";
|
|
37
|
-
|
|
38
|
-
const WORD = Uint32Array.BYTES_PER_ELEMENT;
|
|
39
|
-
|
|
40
|
-
/*
|
|
41
|
-
A wide usage mask costs bandwidth: the driver places a buffer by what it was told the buffer might
|
|
42
|
-
have to do, and the flags below are what each one actually does. `pool` and the alive lists are the
|
|
43
|
-
big ones and are touched by nothing but shaders. Adding a flag "just in case" moves 64 MiB onto a
|
|
44
|
-
slower path for a copy nobody makes — see `BufferResourceDescriptor`.
|
|
45
|
-
*/
|
|
46
|
-
/** Written only by shaders. The free list is one of these: `init_free_list` fills it on-device. */
|
|
47
|
-
const STORAGE = GPUBufferUsage.STORAGE;
|
|
48
|
-
/** Seeded or refilled from the host with `writeBuffer`. */
|
|
49
|
-
const STORAGE_UPLOAD = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST;
|
|
50
|
-
/** Uploaded, and copied forward into its replacement when the emitter set outgrows it. */
|
|
51
|
-
const STORAGE_GROWABLE = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC;
|
|
52
|
-
/**
|
|
53
|
-
* An indirect command the GPU writes and the GPU consumes. COPY_SRC is for {@link
|
|
54
|
-
* GPUParticleSystem#read_stats}, which reads the args back to show that the dispatch really is
|
|
55
|
-
* sized on-device — twelve bytes.
|
|
56
|
-
*/
|
|
57
|
-
const INDIRECT = GPUBufferUsage.STORAGE | GPUBufferUsage.INDIRECT | GPUBufferUsage.COPY_SRC;
|
|
58
|
-
|
|
59
|
-
const DEFAULT_CAPACITY = 524288;
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* What {@link GPUParticleSystem#read_stats} decodes: the counts the GPU keeps between frames, named.
|
|
63
|
-
*
|
|
64
|
-
* @typedef {object} ParticleSystemStats
|
|
65
|
-
* @property {number} alive live particles the frame ended with
|
|
66
|
-
* @property {number} dead length of the free list
|
|
67
|
-
* @property {number} spawn_budget free slots the frame never promised to a spawn — its headroom
|
|
68
|
-
* @property {number} alive_in entries the frame's simulate consumed — the padded, program-coherent
|
|
69
|
-
* list the frame before it built, so never fewer than that frame's `alive`
|
|
70
|
-
* @property {number} sim_padded length of the padded, program-coherent simulate list built this
|
|
71
|
-
* frame; the gap to `alive` is what keeping each wave on one VM program costs in lanes
|
|
72
|
-
* @property {number} sim_workgroups workgroups the NEXT simulate will dispatch — read out of the
|
|
73
|
-
* indirect args rather than derived, so it is what the GPU will actually launch
|
|
74
|
-
*/
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* The GPU particle system: a feature of a scene, in the mould of `ReSTIRDI` — construct once
|
|
78
|
-
* against a {@link GPUSceneContext}, and contribute the frame's passes through
|
|
79
|
-
* {@link execute}.
|
|
80
|
-
*
|
|
81
|
-
* ## What it owns
|
|
82
|
-
*
|
|
83
|
-
* Persistent GPU state, and only that: a buffer is reserved here if and only if its contents have
|
|
84
|
-
* to survive to the next frame. That is the particle pool, the alive list, the dead
|
|
85
|
-
* free-list, the counters, the simulation-order (coherence) list, simulate's indirect dispatch args
|
|
86
|
-
* (written at the end of one frame, read by the next), the emitter database (the emitter table and
|
|
87
|
-
* the GPU-owned state table, one buffer), the packed program buffer, and the two CPU-uploaded
|
|
88
|
-
* queues — the spawn-command ring and the warm-up queue. Buffers that depend on the emitter set —
|
|
89
|
-
* the coherence list on the number of distinct programs, the program buffer on the heap, the
|
|
90
|
-
* database on its own pages, the counters on the emitter table's element capacity (they carry the
|
|
91
|
-
* per-emitter bounds accumulator, see `bounds/`) — are grown inside {@link execute}, the way
|
|
92
|
-
* ReSTIR's reservoirs follow the resolution.
|
|
93
|
-
*
|
|
94
|
-
* Everything else a frame touches is the frame graph's. The bucketing's bins, cursors and keys, the
|
|
95
|
-
* draw args, the spawn counts and the VM's wide register file are produced and consumed inside one
|
|
96
|
-
* graph, so the pass that first writes each one declares it and the graph allocates it just in time
|
|
97
|
-
* and releases it after its last reader — into the pool the rest of the renderer allocates from.
|
|
98
|
-
* Reserving them here instead would be megabytes at the default capacity that no other feature
|
|
99
|
-
* could ever borrow, and most of them are cleared by a reset pass every frame regardless.
|
|
100
|
-
*
|
|
101
|
-
* ## The alive list is borrowed, not reserved
|
|
102
|
-
*
|
|
103
|
-
* The alive list crosses the frame boundary and so cannot be the graph's — but it does not have to
|
|
104
|
-
* be the feature's either. It is taken from the graphics context's buffer allocator and given back
|
|
105
|
-
* every frame ({@link #swap_alive_list}), which is the difference between one list and two.
|
|
106
|
-
*
|
|
107
|
-
* Two was the obvious reading of the ping-pong: the frame builds a new list while the previous one
|
|
108
|
-
* is still there to be read. But nothing reads the previous one. Emit and simulate append to the
|
|
109
|
-
* new list from a counter the frame resets to zero, the draws walk that same new list, and the only
|
|
110
|
-
* pass that ever looks at the one before it is the bucketing rebuild on a frame whose program
|
|
111
|
-
* assignments changed — a live program edit, not a frame. So on an ordinary frame the previous list
|
|
112
|
-
* goes back to the pool BEFORE the new one is asked for, the pool answers with the same memory, and
|
|
113
|
-
* the feature's reserved footprint is one list rather than two. On the rare frame that does rebuild,
|
|
114
|
-
* the old list is held one frame longer — until the graph recorded around it has been submitted —
|
|
115
|
-
* and the feature briefly costs what it used to cost always.
|
|
116
|
-
*
|
|
117
|
-
* Every pass stays within the eight storage buffers a stage gets by default: the emitter record
|
|
118
|
-
* and the GPU-owned state are two tables of one database buffer, and the VM's constants and code
|
|
119
|
-
* are one packed buffer. The one exception is the wide-register fallback (below), whose emit and
|
|
120
|
-
* simulate passes bind a ninth for the register file — inside the ten the engine already demands of
|
|
121
|
-
* an adapter, but past the default.
|
|
122
|
-
*
|
|
123
|
-
* ## Two interpreters
|
|
124
|
-
*
|
|
125
|
-
* The VM's register file normally lives in shader registers, and is therefore small — see
|
|
126
|
-
* {@link ./ParticleConstants.js PARTICLE_VM_FAST_REGISTER_SLOTS}. An effect whose compiled program
|
|
127
|
-
* needs more than it holds does not fail: the whole system swaps emit and simulate for variants
|
|
128
|
-
* that keep the file in a storage buffer, which is far slower and effectively unbounded. The choice
|
|
129
|
-
* is per-frame, whole-system, and made here — the registry reports `max_register_count` over the
|
|
130
|
-
* effects it holds and this is where that number meets the VM's; nothing about authoring a graph has
|
|
131
|
-
* a register ceiling.
|
|
132
|
-
*
|
|
133
|
-
* ## Emitters are registered at rows
|
|
134
|
-
*
|
|
135
|
-
* {@link add_emitter} gives a {@link ParticleEmitter} a row in the emitter table pointed at a
|
|
136
|
-
* `transforms` row — an entity's, one on, held through the scene context's rows by whoever owns
|
|
137
|
-
* the entity — and it starts spawning on the next {@link execute}; under a joint's row it moves
|
|
138
|
-
* with the joint. {@link remove_emitter} retires the row and frees its particles. Nothing here
|
|
139
|
-
* walks the scene: an emitter is where its owner says it is, and per-frame upkeep is proportional
|
|
140
|
-
* to what changed rather than to how many emitters there are.
|
|
141
|
-
*
|
|
142
|
-
* ## Spawning is the GPU's decision
|
|
143
|
-
*
|
|
144
|
-
* The CPU never counts particles or walks emitters per frame. The emitter tick pass integrates
|
|
145
|
-
* every emitter's rate on the GPU, and the emit dispatch is sized by a prefix scan of what it
|
|
146
|
-
* decided. The host's one way to ask for particles is {@link spawn}: a burst queued into a small
|
|
147
|
-
* command list that a pass adds to the emitter's pending spawns, on the same path as everything
|
|
148
|
-
* else. See `graph_particles.js` for the frame.
|
|
149
|
-
*
|
|
150
|
-
* ## Drawing
|
|
151
|
-
*
|
|
152
|
-
* {@link execute} is the whole per-frame call, once per frame, and it records only the simulation:
|
|
153
|
-
* everything that moves particles, leaving the alive list and the indirect draw args as graph
|
|
154
|
-
* handles. It draws nothing.
|
|
155
|
-
*
|
|
156
|
-
* The drawing is the transparency pipeline's. The system is an `AVBOITSideChannel`, and the AVBOIT
|
|
157
|
-
* orchestrator asks it for its occupancy, its extinction and its draw at the three points where the
|
|
158
|
-
* transparent meshes have contributed theirs ({@link avboit_occupancy}, {@link avboit_splat},
|
|
159
|
-
* {@link avboit_draw}). Those read the handles `execute` produced and composite into a voxel volume
|
|
160
|
-
* and a set of accumulators that are sums — so the particles need no depth ordering at all, and
|
|
161
|
-
* none is ever built.
|
|
162
|
-
*
|
|
163
|
-
* ## Allocation
|
|
164
|
-
*
|
|
165
|
-
* The per-frame path allocates the frame graph's records and nothing else: emitter rows are
|
|
166
|
-
* staged through one shared record, spawn commands go into a preallocated ring, and the
|
|
167
|
-
* per-dispatch settings are reused objects. A command context is opened only on frames that have
|
|
168
|
-
* something to upload.
|
|
169
|
-
*/
|
|
170
|
-
export class GPUParticleSystem {
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Engine GraphicsContext.
|
|
174
|
-
* @type {object}
|
|
175
|
-
*/
|
|
176
|
-
graphics;
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* The scene whose rows the emitters are placed at, and whose database their world matrices
|
|
180
|
-
* come from.
|
|
181
|
-
* @type {import("../scene/GPUSceneContext.js").GPUSceneContext}
|
|
182
|
-
*/
|
|
183
|
-
scene_context;
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Maximum number of simultaneously live particles.
|
|
187
|
-
* @type {number}
|
|
188
|
-
*/
|
|
189
|
-
capacity;
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* @type {EmitterRegistry}
|
|
193
|
-
*/
|
|
194
|
-
registry;
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Frames elapsed. The VM's frame builtin.
|
|
198
|
-
* @type {number}
|
|
199
|
-
*/
|
|
200
|
-
frame = 0;
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Seconds since start, the VM's `TIME` builtin. Advanced by `dt` each {@link execute}.
|
|
204
|
-
* @type {number}
|
|
205
|
-
*/
|
|
206
|
-
time = 0;
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Cap on how much missed time a culled emitter makes up for when it comes back into view.
|
|
210
|
-
* @type {number}
|
|
211
|
-
*/
|
|
212
|
-
max_catchup_seconds = 1;
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Sprite atlas the billboards sample, and the sampler they sample it with. A 1x1 white texel by
|
|
216
|
-
* default, so an emitter with no art draws as a plain quad rather than nothing.
|
|
217
|
-
* @type {GPUTextureView}
|
|
218
|
-
*/
|
|
219
|
-
atlas;
|
|
220
|
-
|
|
221
|
-
/** @type {GPUSampler} */
|
|
222
|
-
atlas_sampler;
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* The persistent GPU buffers, by the names the graph functions bind them under. Exposed for
|
|
226
|
-
* inspection — {@link bufferSizes} measures them, {@link read_stats} reads the counts out of
|
|
227
|
-
* them — never for the frame loop, which reads nothing back. Entries that grow with the
|
|
228
|
-
* emitter set are replaced in place.
|
|
229
|
-
*
|
|
230
|
-
* `alive` and `alive_write` are one buffer under two names — what the feature holds, and what
|
|
231
|
-
* the frame writes — and `alive_read` is the previous frame's list, present only on the frames
|
|
232
|
-
* that read one. The list itself belongs to the allocator, not here; see {@link #swap_alive_list}.
|
|
233
|
-
*
|
|
234
|
-
* @type {object}
|
|
235
|
-
*/
|
|
236
|
-
buffers = {};
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Distinct programs the coherence scratch is currently sized for.
|
|
240
|
-
* @type {number}
|
|
241
|
-
*/
|
|
242
|
-
#sim_bucket_capacity = 0;
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* What the alive list is asked for under: one `u32` per pool slot, touched by nothing but
|
|
246
|
-
* shaders, and deliberately not cleared — every entry a reader sees was appended this frame,
|
|
247
|
-
* from a counter the frame's first pass resets to zero, so whatever the last borrower left in
|
|
248
|
-
* it is unreadable by construction. One instance, reused: the pool reads a descriptor and does
|
|
249
|
-
* not keep it.
|
|
250
|
-
*
|
|
251
|
-
* @type {BufferResourceDescriptor}
|
|
252
|
-
*/
|
|
253
|
-
#alive_descriptor;
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* The alive list the last recorded frame built — the next frame's `alive_read`. Borrowed from
|
|
257
|
-
* `graphics.buffer_allocator_main`, so it is released rather than destroyed.
|
|
258
|
-
* @type {GPUBuffer|null}
|
|
259
|
-
*/
|
|
260
|
-
#alive = null;
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* An alive list a recorded frame still reads: the bucketing rebuild's input on a frame whose
|
|
264
|
-
* program assignments changed. Handed back on the next {@link execute}, by which point that
|
|
265
|
-
* frame's graph has been executed and submitted.
|
|
266
|
-
* @type {GPUBuffer|null}
|
|
267
|
-
*/
|
|
268
|
-
#alive_retired = null;
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* The spawn-command ring: `[row, count]` pairs, filled by {@link spawn}, merged and uploaded on
|
|
272
|
-
* the next {@link execute}.
|
|
273
|
-
* @type {Uint32Array}
|
|
274
|
-
*/
|
|
275
|
-
#spawn_commands;
|
|
276
|
-
|
|
277
|
-
/**
|
|
278
|
-
* The emitter each ring entry was issued for, so an entry whose emitter has since left the
|
|
279
|
-
* scene — or whose row has gone to somebody else — is dropped at upload rather than delivered
|
|
280
|
-
* to the row's next tenant.
|
|
281
|
-
* @type {import("./runtime/ParticleEmitter.js").ParticleEmitter[]}
|
|
282
|
-
*/
|
|
283
|
-
#spawn_command_emitters;
|
|
284
|
-
|
|
285
|
-
/** @type {number} */
|
|
286
|
-
#spawn_command_count = 0;
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* Scratch for merging the ring: pending count by row. Grown with the emitter table, never per
|
|
290
|
-
* frame.
|
|
291
|
-
* @type {Uint32Array}
|
|
292
|
-
*/
|
|
293
|
-
#spawn_by_row = new Uint32Array(0);
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* Emitters registered since the last frame with a pre-warm to run — the input of the warm-up
|
|
297
|
-
* pipeline (`warmup/`), consumed by the frame that records it. Filled where the emitter is
|
|
298
|
-
* registered ({@link add_emitter}), which is the one moment the host knows an emitter is
|
|
299
|
-
* new: the GPU decides everything else about spawning, but "this one has just arrived and asks
|
|
300
|
-
* to arrive warm" is a host-side fact.
|
|
301
|
-
*
|
|
302
|
-
* @type {import("./runtime/ParticleEmitter.js").ParticleEmitter[]}
|
|
303
|
-
*/
|
|
304
|
-
#warmup_queue = [];
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
* The queue as the GPU reads it, `PARTICLE_WARMUP_QUEUE_WORDS` per entry — see
|
|
308
|
-
* `warmup/shader_particle_warmup.js`. Grown to fit, never per frame.
|
|
309
|
-
* @type {Uint32Array}
|
|
310
|
-
*/
|
|
311
|
-
#warmup_words = new Uint32Array(0);
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
/**
|
|
315
|
-
* The 1x1 white texture {@link atlas} points at until a caller replaces it. Held so
|
|
316
|
-
* {@link destroy} can free it; a caller that assigns its own atlas owns that one.
|
|
317
|
-
* @type {GPUTextureContext|null}
|
|
318
|
-
*/
|
|
319
|
-
#default_atlas = null;
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* Program assignments used to build the previous frame's coherence list.
|
|
324
|
-
* @type {number}
|
|
325
|
-
*/
|
|
326
|
-
#program_assignment_version = 0;
|
|
327
|
-
|
|
328
|
-
/**
|
|
329
|
-
* Per-frame scalars handed to {@link graph_particles_simulate}, reused rather than rebuilt.
|
|
330
|
-
* @type {object}
|
|
331
|
-
*/
|
|
332
|
-
#frame = {
|
|
333
|
-
dt: 0, time: 0, index: 0, max_catchup: 1, spawn_command_count: 0,
|
|
334
|
-
warmup_count: 0, warmup_capacity: 0, warmup_schedule: null,
|
|
335
|
-
rebucket: false,
|
|
336
|
-
};
|
|
337
|
-
|
|
338
|
-
/** @type {{capacity: number, group_count: number}} */
|
|
339
|
-
#emitters = { capacity: 0, group_count: 0 };
|
|
340
|
-
|
|
341
|
-
/**
|
|
342
|
-
* This frame's simulation, for the side channels that draw it later in the frame: the graph it
|
|
343
|
-
* was recorded into, the scene database handle it read, and the handles it produced. Reused,
|
|
344
|
-
* never reallocated.
|
|
345
|
-
* @type {{graph: object|null, scene_database: number, emitter_group_count: number, handles: object|null}}
|
|
346
|
-
*/
|
|
347
|
-
#simulated = { graph: null, scene_database: -1, emitter_group_count: 0, handles: null };
|
|
348
|
-
|
|
349
|
-
/**
|
|
350
|
-
* @param {object} graphics engine GraphicsContext (has `.device`)
|
|
351
|
-
* @param {import("../scene/GPUSceneContext.js").GPUSceneContext} scene_context
|
|
352
|
-
* @param {object} [options]
|
|
353
|
-
* @param {number} [options.capacity] max simultaneous particles
|
|
354
|
-
* @param {number} [options.spawn_command_capacity] most {@link spawn} calls one frame keeps
|
|
355
|
-
*/
|
|
356
|
-
constructor(graphics, scene_context, { capacity = DEFAULT_CAPACITY, spawn_command_capacity = 256 } = {}) {
|
|
357
|
-
assert.defined(graphics, "graphics");
|
|
358
|
-
assert.defined(scene_context, "scene_context");
|
|
359
|
-
assert.equal(scene_context.isGPUSceneContext, true, "scene_context.isGPUSceneContext !== true");
|
|
360
|
-
assert.isPositiveInteger(capacity, "capacity");
|
|
361
|
-
assert.isPositiveInteger(spawn_command_capacity, "spawn_command_capacity");
|
|
362
|
-
|
|
363
|
-
this.graphics = graphics;
|
|
364
|
-
this.scene_context = scene_context;
|
|
365
|
-
this.capacity = capacity;
|
|
366
|
-
|
|
367
|
-
this.registry = new EmitterRegistry(graphics.device);
|
|
368
|
-
|
|
369
|
-
this.#spawn_commands = new Uint32Array(spawn_command_capacity * PARTICLE_SPAWN_COMMAND_WORDS);
|
|
370
|
-
this.#spawn_command_emitters = new Array(spawn_command_capacity).fill(null);
|
|
371
|
-
|
|
372
|
-
this.#create_buffers();
|
|
373
|
-
this.#create_default_atlas();
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
/**
|
|
377
|
-
* The buffer the emitter table lives in — what every emitter-reading pass binds.
|
|
378
|
-
* @returns {GPUBuffer}
|
|
379
|
-
*/
|
|
380
|
-
get emitter_database() {
|
|
381
|
-
return this.registry.database.buffer;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
/**
|
|
385
|
-
* The live emitters, in no particular order.
|
|
386
|
-
* @returns {import("./runtime/ParticleEmitter.js").ParticleEmitter[]}
|
|
387
|
-
*/
|
|
388
|
-
get emitters() {
|
|
389
|
-
return this.registry.emitters;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
/**
|
|
393
|
-
* Number of simulation-order buckets — one per program id the registry can hand out, which is
|
|
394
|
-
* its peak number of distinct compiled programs rather than its current one.
|
|
395
|
-
*
|
|
396
|
-
* This is the coherence pass's only host-provided number, and it is static topology rather than
|
|
397
|
-
* per-frame state: it changes when an effect the set has never held is registered, not as
|
|
398
|
-
* particles are born and die. Everything the pass measures per frame stays on the GPU. Sizing it
|
|
399
|
-
* from the peak rather than the live count is what keeps it from moving every time an effect is
|
|
400
|
-
* unregistered — the bins of released ids are empty, and an empty bin rounds up to zero
|
|
401
|
-
* workgroups and shifts nothing after it.
|
|
402
|
-
*
|
|
403
|
-
* @returns {number}
|
|
404
|
-
*/
|
|
405
|
-
bucketCount() {
|
|
406
|
-
return Math.max(1, this.registry.program_id_capacity);
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
/**
|
|
410
|
-
* Ask for `count` particles from an emitter, on top of its rate, on the next frame. Goes
|
|
411
|
-
* through the GPU path — the tick pass folds it into the emitter's accumulator, the budget and
|
|
412
|
-
* the culling apply — so it is a request, not a guarantee.
|
|
413
|
-
*
|
|
414
|
-
* @param {import("./runtime/ParticleEmitter.js").ParticleEmitter} emitter a registered emitter
|
|
415
|
-
* @param {number} count
|
|
416
|
-
*/
|
|
417
|
-
spawn(emitter, count) {
|
|
418
|
-
assert.defined(emitter, "emitter");
|
|
419
|
-
assert.isNonNegativeInteger(count, "count");
|
|
420
|
-
|
|
421
|
-
const context = this.registry.context(emitter);
|
|
422
|
-
|
|
423
|
-
assert.defined(context, "emitter is not registered");
|
|
424
|
-
|
|
425
|
-
const ring = this.#spawn_commands;
|
|
426
|
-
const base = this.#spawn_command_count * PARTICLE_SPAWN_COMMAND_WORDS;
|
|
427
|
-
|
|
428
|
-
if (base >= ring.length) {
|
|
429
|
-
// the text is the key warn_limited counts on, so it carries no burst size: one message
|
|
430
|
-
// per ring size, not one per number a caller happens to ask for
|
|
431
|
-
warn_limited(`GPUParticleSystem: spawn command ring is full (${ring.length / PARTICLE_SPAWN_COMMAND_WORDS} per frame); dropping this frame's remaining bursts`);
|
|
432
|
-
return;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
ring[base] = context.row;
|
|
436
|
-
ring[base + 1] = count;
|
|
437
|
-
this.#spawn_command_emitters[this.#spawn_command_count] = emitter;
|
|
438
|
-
this.#spawn_command_count++;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
/**
|
|
442
|
-
* The system's whole contribution to the frame, once per frame: reconcile the emitter set with
|
|
443
|
-
* the scene, push what changed to the GPU, and record every pass that moves particles — through
|
|
444
|
-
* to the indirect draw args.
|
|
445
|
-
*
|
|
446
|
-
* It draws nothing and touches no colour target. The handles it returns are what the AVBOIT
|
|
447
|
-
* side channels ({@link avboit_occupancy}, {@link avboit_splat}, {@link avboit_draw}) consume
|
|
448
|
-
* later in the same frame, off this same graph, once the transparent meshes have contributed
|
|
449
|
-
* theirs.
|
|
450
|
-
*
|
|
451
|
-
* Call after the scene context has built for the frame (`GPUSceneContext#update`), so every
|
|
452
|
-
* emitter's `transforms` row is uploaded, and after the hierarchy pass has run, so the rows'
|
|
453
|
-
* `global` matrices are this frame's.
|
|
454
|
-
*
|
|
455
|
-
* **Once per frame, not once per view.** This advances the simulation by `dt` and hands out
|
|
456
|
-
* handles into the graph it recorded into; a second view of the same scene records a second
|
|
457
|
-
* graph and would step the simulation twice. The renderer has one view per `render` call today,
|
|
458
|
-
* so this holds by construction; a second view of one scene needs the step and the handles
|
|
459
|
-
* separated — record the passes for the first graph, import the same buffers into the rest.
|
|
460
|
-
*
|
|
461
|
-
* @param {object} params
|
|
462
|
-
* @param {import("../../../engine/graphics/render/frame_graph/FrameGraph.js").FrameGraph} params.graph
|
|
463
|
-
* @param {number} params.camera graph handle of the camera uniform buffer
|
|
464
|
-
* @param {number} params.scene_database graph handle of the scene database — the one the
|
|
465
|
-
* hierarchy pass returned, so this reads the matrices it composed
|
|
466
|
-
* @param {number} params.dt seconds since the previous frame
|
|
467
|
-
* @returns {import("./graph_particles.js").ParticleSimulationHandles}
|
|
468
|
-
*/
|
|
469
|
-
execute({ graph, camera, scene_database, dt }) {
|
|
470
|
-
assert.defined(graph, "graph");
|
|
471
|
-
assert.isNumber(dt, "dt");
|
|
472
|
-
|
|
473
|
-
this.#upload();
|
|
474
|
-
|
|
475
|
-
const frame = this.#frame;
|
|
476
|
-
frame.dt = dt;
|
|
477
|
-
frame.time = this.time;
|
|
478
|
-
frame.index = this.frame;
|
|
479
|
-
frame.max_catchup = this.max_catchup_seconds;
|
|
480
|
-
frame.spawn_command_count = this.#spawn_command_count;
|
|
481
|
-
frame.rebucket = this.#program_assignment_version !== this.registry.program_assignment_version;
|
|
482
|
-
this.#program_assignment_version = this.registry.program_assignment_version;
|
|
483
|
-
|
|
484
|
-
const table = this.registry.table;
|
|
485
|
-
const emitters = this.#emitters;
|
|
486
|
-
emitters.capacity = table.element_capacity;
|
|
487
|
-
emitters.group_count = table.dispatch_group_count(PARTICLE_EMITTER_TICK_WORKGROUP_SIZE);
|
|
488
|
-
|
|
489
|
-
const buffers = this.buffers;
|
|
490
|
-
|
|
491
|
-
this.#swap_alive_list(frame.rebucket);
|
|
492
|
-
|
|
493
|
-
// The table's database owns and grows its own buffer; it is re-read every frame for that
|
|
494
|
-
// reason.
|
|
495
|
-
buffers.emitter_database = this.registry.database.buffer;
|
|
496
|
-
|
|
497
|
-
const simulated = graph_particles_simulate({
|
|
498
|
-
graph,
|
|
499
|
-
camera,
|
|
500
|
-
scene_database,
|
|
501
|
-
gpu: buffers,
|
|
502
|
-
emitters,
|
|
503
|
-
capacity: this.capacity,
|
|
504
|
-
bucket_count: this.bucketCount(),
|
|
505
|
-
// Whose register file the VM runs on is decided here, once, for the whole system: the
|
|
506
|
-
// registry reports what the hungriest registered effect needs and this compares it
|
|
507
|
-
// against what the fast file holds. Re-read every frame because registering an emitter —
|
|
508
|
-
// or unregistering the one hungry effect — can flip it either way.
|
|
509
|
-
wide_registers: this.registry.max_register_count > PARTICLE_VM_FAST_REGISTER_SLOTS,
|
|
510
|
-
frame,
|
|
511
|
-
});
|
|
512
|
-
|
|
513
|
-
// Kept for the side channels that draw later in this frame, and for nothing past it.
|
|
514
|
-
const current = this.#simulated;
|
|
515
|
-
current.graph = graph;
|
|
516
|
-
current.scene_database = scene_database;
|
|
517
|
-
current.emitter_group_count = emitters.group_count;
|
|
518
|
-
current.handles = simulated;
|
|
519
|
-
|
|
520
|
-
// Consumed: the counts were recorded into the graph, the words were uploaded.
|
|
521
|
-
this.#spawn_command_count = 0;
|
|
522
|
-
this.#warmup_queue.length = 0;
|
|
523
|
-
|
|
524
|
-
this.frame++;
|
|
525
|
-
this.time += dt;
|
|
526
|
-
|
|
527
|
-
return simulated;
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
/**
|
|
531
|
-
* What this frame's {@link execute} left, checked against the graph a side-channel call names:
|
|
532
|
-
* the AVBOIT orchestrator records its passes into the same graph, later in the same frame.
|
|
533
|
-
*
|
|
534
|
-
* @param {import("../../../engine/graphics/render/frame_graph/FrameGraph.js").FrameGraph} graph
|
|
535
|
-
* @returns {{graph: object, scene_database: number, emitter_group_count: number, handles: import("./graph_particles.js").ParticleSimulationHandles}}
|
|
536
|
-
*/
|
|
537
|
-
#simulated_for(graph) {
|
|
538
|
-
const current = this.#simulated;
|
|
539
|
-
|
|
540
|
-
assert.equal(current.graph, graph, "the particles were not simulated into this graph this frame — call execute() before the transparency pipeline records");
|
|
541
|
-
|
|
542
|
-
return current;
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
/**
|
|
546
|
-
* `AVBOITSideChannel#avboit_occupancy`: the emitters' measured bounding boxes, as depth slices.
|
|
547
|
-
*
|
|
548
|
-
* @param {object} args see `AVBOITSideChannel`
|
|
549
|
-
* @param {import("../../../engine/graphics/render/frame_graph/FrameGraph.js").FrameGraph} args.graph
|
|
550
|
-
* @param {number} args.camera graph handle of the camera uniform buffer
|
|
551
|
-
* @param {ArrayLike<number>} args.cluster_parameters the frame's depth curve
|
|
552
|
-
* @param {number} args.occupancy graph handle of the occupancy bits
|
|
553
|
-
* @returns {number} the occupancy handle after the write
|
|
554
|
-
*/
|
|
555
|
-
avboit_occupancy({ graph, camera, cluster_parameters, occupancy }) {
|
|
556
|
-
const current = this.#simulated_for(graph);
|
|
557
|
-
|
|
558
|
-
return graph_particle_avboit_occupancy({
|
|
559
|
-
graph,
|
|
560
|
-
camera,
|
|
561
|
-
cluster_parameters,
|
|
562
|
-
occupancy,
|
|
563
|
-
emitter_database: current.handles.emitter_database,
|
|
564
|
-
emitter_group_count: current.emitter_group_count,
|
|
565
|
-
});
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
/**
|
|
569
|
-
* `AVBOITSideChannel#avboit_splat`: the live particles' extinction, into the volume.
|
|
570
|
-
*
|
|
571
|
-
* @param {object} args see `AVBOITSideChannel`
|
|
572
|
-
* @param {import("../../../engine/graphics/render/frame_graph/FrameGraph.js").FrameGraph} args.graph
|
|
573
|
-
* @param {number} args.camera graph handle of the camera uniform buffer
|
|
574
|
-
* @param {number} args.warp graph handle of the warp LUT
|
|
575
|
-
* @param {number} args.depth_texture graph handle of the scene depth
|
|
576
|
-
* @param {{extinction: number, overflow: number, dummy: number}} args.volume the voxel volume
|
|
577
|
-
* @returns {{extinction: number, overflow: number, dummy: number}} the volume after the write
|
|
578
|
-
*/
|
|
579
|
-
avboit_splat({ graph, camera, warp, depth_texture, volume }) {
|
|
580
|
-
const { handles } = this.#simulated_for(graph);
|
|
581
|
-
|
|
582
|
-
return graph_particle_avboit_splat({
|
|
583
|
-
graph,
|
|
584
|
-
camera,
|
|
585
|
-
warp,
|
|
586
|
-
depth_texture,
|
|
587
|
-
volume,
|
|
588
|
-
atlas: this.atlas,
|
|
589
|
-
atlas_sampler: this.atlas_sampler,
|
|
590
|
-
pool: handles.pool,
|
|
591
|
-
alive: handles.alive,
|
|
592
|
-
emitter_database: handles.emitter_database,
|
|
593
|
-
draw_args: handles.draw_args,
|
|
594
|
-
});
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
/**
|
|
598
|
-
* `AVBOITSideChannel#avboit_draw`: the live particles, into the accumulators.
|
|
599
|
-
*
|
|
600
|
-
* @param {object} args see `AVBOITSideChannel`
|
|
601
|
-
* @param {import("../../../engine/graphics/render/frame_graph/FrameGraph.js").FrameGraph} args.graph
|
|
602
|
-
* @param {import("../view/GPUViewContext.js").GPUViewContext} args.view_ctx
|
|
603
|
-
* @param {number} args.camera graph handle of the camera uniform buffer
|
|
604
|
-
* @param {number} args.depth_texture graph handle of the scene depth
|
|
605
|
-
* @param {number} args.warp graph handle of the warp LUT
|
|
606
|
-
* @param {number} args.integral graph handle of the integral texture
|
|
607
|
-
* @param {number} args.volumetrics_lut graph handle
|
|
608
|
-
* @param {number} args.volumetrics_metadata graph handle
|
|
609
|
-
* @param {{
|
|
610
|
-
*
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
*
|
|
639
|
-
*
|
|
640
|
-
*
|
|
641
|
-
*
|
|
642
|
-
* @
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
*
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
registry.
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
this.#
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
*
|
|
731
|
-
*
|
|
732
|
-
*
|
|
733
|
-
*
|
|
734
|
-
*
|
|
735
|
-
*
|
|
736
|
-
*
|
|
737
|
-
*
|
|
738
|
-
*
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
*
|
|
817
|
-
*
|
|
818
|
-
*
|
|
819
|
-
*
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
const
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
merged
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
*
|
|
875
|
-
*
|
|
876
|
-
*
|
|
877
|
-
*
|
|
878
|
-
*
|
|
879
|
-
*
|
|
880
|
-
*
|
|
881
|
-
*
|
|
882
|
-
*
|
|
883
|
-
*
|
|
884
|
-
*
|
|
885
|
-
*
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
const
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
*
|
|
920
|
-
*
|
|
921
|
-
*
|
|
922
|
-
*
|
|
923
|
-
*
|
|
924
|
-
*
|
|
925
|
-
*
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
*
|
|
959
|
-
*
|
|
960
|
-
*
|
|
961
|
-
*
|
|
962
|
-
*
|
|
963
|
-
*
|
|
964
|
-
*
|
|
965
|
-
*
|
|
966
|
-
*
|
|
967
|
-
*
|
|
968
|
-
*
|
|
969
|
-
* The
|
|
970
|
-
*
|
|
971
|
-
*
|
|
972
|
-
*
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
if
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
buffers.
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
*
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
*
|
|
1035
|
-
*
|
|
1036
|
-
*
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
buffers.
|
|
1073
|
-
|
|
1074
|
-
//
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
//
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
buffers.
|
|
1084
|
-
|
|
1085
|
-
// The
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
counters
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
texture
|
|
1106
|
-
texture.descriptor.
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
texture.
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
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
|
-
*
|
|
1158
|
-
*
|
|
1159
|
-
*
|
|
1160
|
-
*
|
|
1161
|
-
*
|
|
1162
|
-
*
|
|
1163
|
-
*
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
const
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
this
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
*
|
|
1230
|
-
*
|
|
1231
|
-
*
|
|
1232
|
-
*
|
|
1233
|
-
* @param {
|
|
1234
|
-
* @
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1
|
+
import { assert } from "../../../core/assert.js";
|
|
2
|
+
import { ShadeGPUCommandContext } from "../../device/ShadeGPUCommandContext.js";
|
|
3
|
+
import { BufferResourceDescriptor } from "../../device/graph/BufferResourceDescriptor.js";
|
|
4
|
+
import { warn_limited } from "../../util/warn_limited.js";
|
|
5
|
+
import { hash_mix2 } from "../../../core/math/hash/hash_mix2.js";
|
|
6
|
+
import {
|
|
7
|
+
PARTICLE_EMITTER_TICK_WORKGROUP_SIZE,
|
|
8
|
+
PARTICLE_RECORD_WORD_COUNT,
|
|
9
|
+
PARTICLE_SIMULATE_WORKGROUP_SIZE,
|
|
10
|
+
PARTICLE_SPAWN_COMMAND_WORDS,
|
|
11
|
+
PARTICLE_VM_FAST_REGISTER_SLOTS,
|
|
12
|
+
PARTICLE_WARMUP_EMITTER_JITTER,
|
|
13
|
+
PARTICLE_WARMUP_QUEUE_WORDS,
|
|
14
|
+
} from "./ParticleConstants.js";
|
|
15
|
+
import {
|
|
16
|
+
PARTICLE_BOUNDS_WORDS,
|
|
17
|
+
PARTICLE_COUNTER,
|
|
18
|
+
PARTICLE_COUNTER_COUNT,
|
|
19
|
+
particle_bounds_empty_words,
|
|
20
|
+
particle_counters_word_count,
|
|
21
|
+
} from "./data/PARTICLE_COUNTERS.js";
|
|
22
|
+
import { graph_particles_simulate } from "./graph_particles.js";
|
|
23
|
+
import {
|
|
24
|
+
PARTICLE_INIT_FREE_LIST_WORKGROUP_SIZE,
|
|
25
|
+
shader_particle_init_free_list,
|
|
26
|
+
} from "./shaders/shader_particle_init_free_list.js";
|
|
27
|
+
import {
|
|
28
|
+
graph_particle_avboit_draw,
|
|
29
|
+
graph_particle_avboit_occupancy,
|
|
30
|
+
graph_particle_avboit_splat,
|
|
31
|
+
} from "./graph_particles_avboit.js";
|
|
32
|
+
import { EmitterRegistry } from "./runtime/EmitterRegistry.js";
|
|
33
|
+
import { WARMUP_QUEUE } from "./warmup/shader_particle_warmup.js";
|
|
34
|
+
import { warmup_schedule } from "./warmup/warmup_schedule.js";
|
|
35
|
+
import { GPUTextureContext } from "../texture/GPUTextureContext.js";
|
|
36
|
+
import { inspect_gpu_buffer } from "../buffer/inspect_gpu_buffer.js";
|
|
37
|
+
|
|
38
|
+
const WORD = Uint32Array.BYTES_PER_ELEMENT;
|
|
39
|
+
|
|
40
|
+
/*
|
|
41
|
+
A wide usage mask costs bandwidth: the driver places a buffer by what it was told the buffer might
|
|
42
|
+
have to do, and the flags below are what each one actually does. `pool` and the alive lists are the
|
|
43
|
+
big ones and are touched by nothing but shaders. Adding a flag "just in case" moves 64 MiB onto a
|
|
44
|
+
slower path for a copy nobody makes — see `BufferResourceDescriptor`.
|
|
45
|
+
*/
|
|
46
|
+
/** Written only by shaders. The free list is one of these: `init_free_list` fills it on-device. */
|
|
47
|
+
const STORAGE = GPUBufferUsage.STORAGE;
|
|
48
|
+
/** Seeded or refilled from the host with `writeBuffer`. */
|
|
49
|
+
const STORAGE_UPLOAD = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST;
|
|
50
|
+
/** Uploaded, and copied forward into its replacement when the emitter set outgrows it. */
|
|
51
|
+
const STORAGE_GROWABLE = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC;
|
|
52
|
+
/**
|
|
53
|
+
* An indirect command the GPU writes and the GPU consumes. COPY_SRC is for {@link
|
|
54
|
+
* GPUParticleSystem#read_stats}, which reads the args back to show that the dispatch really is
|
|
55
|
+
* sized on-device — twelve bytes.
|
|
56
|
+
*/
|
|
57
|
+
const INDIRECT = GPUBufferUsage.STORAGE | GPUBufferUsage.INDIRECT | GPUBufferUsage.COPY_SRC;
|
|
58
|
+
|
|
59
|
+
const DEFAULT_CAPACITY = 524288;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* What {@link GPUParticleSystem#read_stats} decodes: the counts the GPU keeps between frames, named.
|
|
63
|
+
*
|
|
64
|
+
* @typedef {object} ParticleSystemStats
|
|
65
|
+
* @property {number} alive live particles the frame ended with
|
|
66
|
+
* @property {number} dead length of the free list
|
|
67
|
+
* @property {number} spawn_budget free slots the frame never promised to a spawn — its headroom
|
|
68
|
+
* @property {number} alive_in entries the frame's simulate consumed — the padded, program-coherent
|
|
69
|
+
* list the frame before it built, so never fewer than that frame's `alive`
|
|
70
|
+
* @property {number} sim_padded length of the padded, program-coherent simulate list built this
|
|
71
|
+
* frame; the gap to `alive` is what keeping each wave on one VM program costs in lanes
|
|
72
|
+
* @property {number} sim_workgroups workgroups the NEXT simulate will dispatch — read out of the
|
|
73
|
+
* indirect args rather than derived, so it is what the GPU will actually launch
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The GPU particle system: a feature of a scene, in the mould of `ReSTIRDI` — construct once
|
|
78
|
+
* against a {@link GPUSceneContext}, and contribute the frame's passes through
|
|
79
|
+
* {@link execute}.
|
|
80
|
+
*
|
|
81
|
+
* ## What it owns
|
|
82
|
+
*
|
|
83
|
+
* Persistent GPU state, and only that: a buffer is reserved here if and only if its contents have
|
|
84
|
+
* to survive to the next frame. That is the particle pool, the alive list, the dead
|
|
85
|
+
* free-list, the counters, the simulation-order (coherence) list, simulate's indirect dispatch args
|
|
86
|
+
* (written at the end of one frame, read by the next), the emitter database (the emitter table and
|
|
87
|
+
* the GPU-owned state table, one buffer), the packed program buffer, and the two CPU-uploaded
|
|
88
|
+
* queues — the spawn-command ring and the warm-up queue. Buffers that depend on the emitter set —
|
|
89
|
+
* the coherence list on the number of distinct programs, the program buffer on the heap, the
|
|
90
|
+
* database on its own pages, the counters on the emitter table's element capacity (they carry the
|
|
91
|
+
* per-emitter bounds accumulator, see `bounds/`) — are grown inside {@link execute}, the way
|
|
92
|
+
* ReSTIR's reservoirs follow the resolution.
|
|
93
|
+
*
|
|
94
|
+
* Everything else a frame touches is the frame graph's. The bucketing's bins, cursors and keys, the
|
|
95
|
+
* draw args, the spawn counts and the VM's wide register file are produced and consumed inside one
|
|
96
|
+
* graph, so the pass that first writes each one declares it and the graph allocates it just in time
|
|
97
|
+
* and releases it after its last reader — into the pool the rest of the renderer allocates from.
|
|
98
|
+
* Reserving them here instead would be megabytes at the default capacity that no other feature
|
|
99
|
+
* could ever borrow, and most of them are cleared by a reset pass every frame regardless.
|
|
100
|
+
*
|
|
101
|
+
* ## The alive list is borrowed, not reserved
|
|
102
|
+
*
|
|
103
|
+
* The alive list crosses the frame boundary and so cannot be the graph's — but it does not have to
|
|
104
|
+
* be the feature's either. It is taken from the graphics context's buffer allocator and given back
|
|
105
|
+
* every frame ({@link #swap_alive_list}), which is the difference between one list and two.
|
|
106
|
+
*
|
|
107
|
+
* Two was the obvious reading of the ping-pong: the frame builds a new list while the previous one
|
|
108
|
+
* is still there to be read. But nothing reads the previous one. Emit and simulate append to the
|
|
109
|
+
* new list from a counter the frame resets to zero, the draws walk that same new list, and the only
|
|
110
|
+
* pass that ever looks at the one before it is the bucketing rebuild on a frame whose program
|
|
111
|
+
* assignments changed — a live program edit, not a frame. So on an ordinary frame the previous list
|
|
112
|
+
* goes back to the pool BEFORE the new one is asked for, the pool answers with the same memory, and
|
|
113
|
+
* the feature's reserved footprint is one list rather than two. On the rare frame that does rebuild,
|
|
114
|
+
* the old list is held one frame longer — until the graph recorded around it has been submitted —
|
|
115
|
+
* and the feature briefly costs what it used to cost always.
|
|
116
|
+
*
|
|
117
|
+
* Every pass stays within the eight storage buffers a stage gets by default: the emitter record
|
|
118
|
+
* and the GPU-owned state are two tables of one database buffer, and the VM's constants and code
|
|
119
|
+
* are one packed buffer. The one exception is the wide-register fallback (below), whose emit and
|
|
120
|
+
* simulate passes bind a ninth for the register file — inside the ten the engine already demands of
|
|
121
|
+
* an adapter, but past the default.
|
|
122
|
+
*
|
|
123
|
+
* ## Two interpreters
|
|
124
|
+
*
|
|
125
|
+
* The VM's register file normally lives in shader registers, and is therefore small — see
|
|
126
|
+
* {@link ./ParticleConstants.js PARTICLE_VM_FAST_REGISTER_SLOTS}. An effect whose compiled program
|
|
127
|
+
* needs more than it holds does not fail: the whole system swaps emit and simulate for variants
|
|
128
|
+
* that keep the file in a storage buffer, which is far slower and effectively unbounded. The choice
|
|
129
|
+
* is per-frame, whole-system, and made here — the registry reports `max_register_count` over the
|
|
130
|
+
* effects it holds and this is where that number meets the VM's; nothing about authoring a graph has
|
|
131
|
+
* a register ceiling.
|
|
132
|
+
*
|
|
133
|
+
* ## Emitters are registered at rows
|
|
134
|
+
*
|
|
135
|
+
* {@link add_emitter} gives a {@link ParticleEmitter} a row in the emitter table pointed at a
|
|
136
|
+
* `transforms` row — an entity's, one on, held through the scene context's rows by whoever owns
|
|
137
|
+
* the entity — and it starts spawning on the next {@link execute}; under a joint's row it moves
|
|
138
|
+
* with the joint. {@link remove_emitter} retires the row and frees its particles. Nothing here
|
|
139
|
+
* walks the scene: an emitter is where its owner says it is, and per-frame upkeep is proportional
|
|
140
|
+
* to what changed rather than to how many emitters there are.
|
|
141
|
+
*
|
|
142
|
+
* ## Spawning is the GPU's decision
|
|
143
|
+
*
|
|
144
|
+
* The CPU never counts particles or walks emitters per frame. The emitter tick pass integrates
|
|
145
|
+
* every emitter's rate on the GPU, and the emit dispatch is sized by a prefix scan of what it
|
|
146
|
+
* decided. The host's one way to ask for particles is {@link spawn}: a burst queued into a small
|
|
147
|
+
* command list that a pass adds to the emitter's pending spawns, on the same path as everything
|
|
148
|
+
* else. See `graph_particles.js` for the frame.
|
|
149
|
+
*
|
|
150
|
+
* ## Drawing
|
|
151
|
+
*
|
|
152
|
+
* {@link execute} is the whole per-frame call, once per frame, and it records only the simulation:
|
|
153
|
+
* everything that moves particles, leaving the alive list and the indirect draw args as graph
|
|
154
|
+
* handles. It draws nothing.
|
|
155
|
+
*
|
|
156
|
+
* The drawing is the transparency pipeline's. The system is an `AVBOITSideChannel`, and the AVBOIT
|
|
157
|
+
* orchestrator asks it for its occupancy, its extinction and its draw at the three points where the
|
|
158
|
+
* transparent meshes have contributed theirs ({@link avboit_occupancy}, {@link avboit_splat},
|
|
159
|
+
* {@link avboit_draw}). Those read the handles `execute` produced and composite into a voxel volume
|
|
160
|
+
* and a set of accumulators that are sums — so the particles need no depth ordering at all, and
|
|
161
|
+
* none is ever built.
|
|
162
|
+
*
|
|
163
|
+
* ## Allocation
|
|
164
|
+
*
|
|
165
|
+
* The per-frame path allocates the frame graph's records and nothing else: emitter rows are
|
|
166
|
+
* staged through one shared record, spawn commands go into a preallocated ring, and the
|
|
167
|
+
* per-dispatch settings are reused objects. A command context is opened only on frames that have
|
|
168
|
+
* something to upload.
|
|
169
|
+
*/
|
|
170
|
+
export class GPUParticleSystem {
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Engine GraphicsContext.
|
|
174
|
+
* @type {object}
|
|
175
|
+
*/
|
|
176
|
+
graphics;
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The scene whose rows the emitters are placed at, and whose database their world matrices
|
|
180
|
+
* come from.
|
|
181
|
+
* @type {import("../scene/GPUSceneContext.js").GPUSceneContext}
|
|
182
|
+
*/
|
|
183
|
+
scene_context;
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Maximum number of simultaneously live particles.
|
|
187
|
+
* @type {number}
|
|
188
|
+
*/
|
|
189
|
+
capacity;
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* @type {EmitterRegistry}
|
|
193
|
+
*/
|
|
194
|
+
registry;
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Frames elapsed. The VM's frame builtin.
|
|
198
|
+
* @type {number}
|
|
199
|
+
*/
|
|
200
|
+
frame = 0;
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Seconds since start, the VM's `TIME` builtin. Advanced by `dt` each {@link execute}.
|
|
204
|
+
* @type {number}
|
|
205
|
+
*/
|
|
206
|
+
time = 0;
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Cap on how much missed time a culled emitter makes up for when it comes back into view.
|
|
210
|
+
* @type {number}
|
|
211
|
+
*/
|
|
212
|
+
max_catchup_seconds = 1;
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Sprite atlas the billboards sample, and the sampler they sample it with. A 1x1 white texel by
|
|
216
|
+
* default, so an emitter with no art draws as a plain quad rather than nothing.
|
|
217
|
+
* @type {GPUTextureView}
|
|
218
|
+
*/
|
|
219
|
+
atlas;
|
|
220
|
+
|
|
221
|
+
/** @type {GPUSampler} */
|
|
222
|
+
atlas_sampler;
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* The persistent GPU buffers, by the names the graph functions bind them under. Exposed for
|
|
226
|
+
* inspection — {@link bufferSizes} measures them, {@link read_stats} reads the counts out of
|
|
227
|
+
* them — never for the frame loop, which reads nothing back. Entries that grow with the
|
|
228
|
+
* emitter set are replaced in place.
|
|
229
|
+
*
|
|
230
|
+
* `alive` and `alive_write` are one buffer under two names — what the feature holds, and what
|
|
231
|
+
* the frame writes — and `alive_read` is the previous frame's list, present only on the frames
|
|
232
|
+
* that read one. The list itself belongs to the allocator, not here; see {@link #swap_alive_list}.
|
|
233
|
+
*
|
|
234
|
+
* @type {object}
|
|
235
|
+
*/
|
|
236
|
+
buffers = {};
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Distinct programs the coherence scratch is currently sized for.
|
|
240
|
+
* @type {number}
|
|
241
|
+
*/
|
|
242
|
+
#sim_bucket_capacity = 0;
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* What the alive list is asked for under: one `u32` per pool slot, touched by nothing but
|
|
246
|
+
* shaders, and deliberately not cleared — every entry a reader sees was appended this frame,
|
|
247
|
+
* from a counter the frame's first pass resets to zero, so whatever the last borrower left in
|
|
248
|
+
* it is unreadable by construction. One instance, reused: the pool reads a descriptor and does
|
|
249
|
+
* not keep it.
|
|
250
|
+
*
|
|
251
|
+
* @type {BufferResourceDescriptor}
|
|
252
|
+
*/
|
|
253
|
+
#alive_descriptor;
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* The alive list the last recorded frame built — the next frame's `alive_read`. Borrowed from
|
|
257
|
+
* `graphics.buffer_allocator_main`, so it is released rather than destroyed.
|
|
258
|
+
* @type {GPUBuffer|null}
|
|
259
|
+
*/
|
|
260
|
+
#alive = null;
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* An alive list a recorded frame still reads: the bucketing rebuild's input on a frame whose
|
|
264
|
+
* program assignments changed. Handed back on the next {@link execute}, by which point that
|
|
265
|
+
* frame's graph has been executed and submitted.
|
|
266
|
+
* @type {GPUBuffer|null}
|
|
267
|
+
*/
|
|
268
|
+
#alive_retired = null;
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* The spawn-command ring: `[row, count]` pairs, filled by {@link spawn}, merged and uploaded on
|
|
272
|
+
* the next {@link execute}.
|
|
273
|
+
* @type {Uint32Array}
|
|
274
|
+
*/
|
|
275
|
+
#spawn_commands;
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* The emitter each ring entry was issued for, so an entry whose emitter has since left the
|
|
279
|
+
* scene — or whose row has gone to somebody else — is dropped at upload rather than delivered
|
|
280
|
+
* to the row's next tenant.
|
|
281
|
+
* @type {import("./runtime/ParticleEmitter.js").ParticleEmitter[]}
|
|
282
|
+
*/
|
|
283
|
+
#spawn_command_emitters;
|
|
284
|
+
|
|
285
|
+
/** @type {number} */
|
|
286
|
+
#spawn_command_count = 0;
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Scratch for merging the ring: pending count by row. Grown with the emitter table, never per
|
|
290
|
+
* frame.
|
|
291
|
+
* @type {Uint32Array}
|
|
292
|
+
*/
|
|
293
|
+
#spawn_by_row = new Uint32Array(0);
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Emitters registered since the last frame with a pre-warm to run — the input of the warm-up
|
|
297
|
+
* pipeline (`warmup/`), consumed by the frame that records it. Filled where the emitter is
|
|
298
|
+
* registered ({@link add_emitter}), which is the one moment the host knows an emitter is
|
|
299
|
+
* new: the GPU decides everything else about spawning, but "this one has just arrived and asks
|
|
300
|
+
* to arrive warm" is a host-side fact.
|
|
301
|
+
*
|
|
302
|
+
* @type {import("./runtime/ParticleEmitter.js").ParticleEmitter[]}
|
|
303
|
+
*/
|
|
304
|
+
#warmup_queue = [];
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* The queue as the GPU reads it, `PARTICLE_WARMUP_QUEUE_WORDS` per entry — see
|
|
308
|
+
* `warmup/shader_particle_warmup.js`. Grown to fit, never per frame.
|
|
309
|
+
* @type {Uint32Array}
|
|
310
|
+
*/
|
|
311
|
+
#warmup_words = new Uint32Array(0);
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* The 1x1 white texture {@link atlas} points at until a caller replaces it. Held so
|
|
316
|
+
* {@link destroy} can free it; a caller that assigns its own atlas owns that one.
|
|
317
|
+
* @type {GPUTextureContext|null}
|
|
318
|
+
*/
|
|
319
|
+
#default_atlas = null;
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Program assignments used to build the previous frame's coherence list.
|
|
324
|
+
* @type {number}
|
|
325
|
+
*/
|
|
326
|
+
#program_assignment_version = 0;
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Per-frame scalars handed to {@link graph_particles_simulate}, reused rather than rebuilt.
|
|
330
|
+
* @type {object}
|
|
331
|
+
*/
|
|
332
|
+
#frame = {
|
|
333
|
+
dt: 0, time: 0, index: 0, max_catchup: 1, spawn_command_count: 0,
|
|
334
|
+
warmup_count: 0, warmup_capacity: 0, warmup_schedule: null,
|
|
335
|
+
rebucket: false,
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
/** @type {{capacity: number, group_count: number}} */
|
|
339
|
+
#emitters = { capacity: 0, group_count: 0 };
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* This frame's simulation, for the side channels that draw it later in the frame: the graph it
|
|
343
|
+
* was recorded into, the scene database handle it read, and the handles it produced. Reused,
|
|
344
|
+
* never reallocated.
|
|
345
|
+
* @type {{graph: object|null, scene_database: number, emitter_group_count: number, handles: object|null}}
|
|
346
|
+
*/
|
|
347
|
+
#simulated = { graph: null, scene_database: -1, emitter_group_count: 0, handles: null };
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* @param {object} graphics engine GraphicsContext (has `.device`)
|
|
351
|
+
* @param {import("../scene/GPUSceneContext.js").GPUSceneContext} scene_context
|
|
352
|
+
* @param {object} [options]
|
|
353
|
+
* @param {number} [options.capacity] max simultaneous particles
|
|
354
|
+
* @param {number} [options.spawn_command_capacity] most {@link spawn} calls one frame keeps
|
|
355
|
+
*/
|
|
356
|
+
constructor(graphics, scene_context, { capacity = DEFAULT_CAPACITY, spawn_command_capacity = 256 } = {}) {
|
|
357
|
+
assert.defined(graphics, "graphics");
|
|
358
|
+
assert.defined(scene_context, "scene_context");
|
|
359
|
+
assert.equal(scene_context.isGPUSceneContext, true, "scene_context.isGPUSceneContext !== true");
|
|
360
|
+
assert.isPositiveInteger(capacity, "capacity");
|
|
361
|
+
assert.isPositiveInteger(spawn_command_capacity, "spawn_command_capacity");
|
|
362
|
+
|
|
363
|
+
this.graphics = graphics;
|
|
364
|
+
this.scene_context = scene_context;
|
|
365
|
+
this.capacity = capacity;
|
|
366
|
+
|
|
367
|
+
this.registry = new EmitterRegistry(graphics.device);
|
|
368
|
+
|
|
369
|
+
this.#spawn_commands = new Uint32Array(spawn_command_capacity * PARTICLE_SPAWN_COMMAND_WORDS);
|
|
370
|
+
this.#spawn_command_emitters = new Array(spawn_command_capacity).fill(null);
|
|
371
|
+
|
|
372
|
+
this.#create_buffers();
|
|
373
|
+
this.#create_default_atlas();
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* The buffer the emitter table lives in — what every emitter-reading pass binds.
|
|
378
|
+
* @returns {GPUBuffer}
|
|
379
|
+
*/
|
|
380
|
+
get emitter_database() {
|
|
381
|
+
return this.registry.database.buffer;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* The live emitters, in no particular order.
|
|
386
|
+
* @returns {import("./runtime/ParticleEmitter.js").ParticleEmitter[]}
|
|
387
|
+
*/
|
|
388
|
+
get emitters() {
|
|
389
|
+
return this.registry.emitters;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Number of simulation-order buckets — one per program id the registry can hand out, which is
|
|
394
|
+
* its peak number of distinct compiled programs rather than its current one.
|
|
395
|
+
*
|
|
396
|
+
* This is the coherence pass's only host-provided number, and it is static topology rather than
|
|
397
|
+
* per-frame state: it changes when an effect the set has never held is registered, not as
|
|
398
|
+
* particles are born and die. Everything the pass measures per frame stays on the GPU. Sizing it
|
|
399
|
+
* from the peak rather than the live count is what keeps it from moving every time an effect is
|
|
400
|
+
* unregistered — the bins of released ids are empty, and an empty bin rounds up to zero
|
|
401
|
+
* workgroups and shifts nothing after it.
|
|
402
|
+
*
|
|
403
|
+
* @returns {number}
|
|
404
|
+
*/
|
|
405
|
+
bucketCount() {
|
|
406
|
+
return Math.max(1, this.registry.program_id_capacity);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Ask for `count` particles from an emitter, on top of its rate, on the next frame. Goes
|
|
411
|
+
* through the GPU path — the tick pass folds it into the emitter's accumulator, the budget and
|
|
412
|
+
* the culling apply — so it is a request, not a guarantee.
|
|
413
|
+
*
|
|
414
|
+
* @param {import("./runtime/ParticleEmitter.js").ParticleEmitter} emitter a registered emitter
|
|
415
|
+
* @param {number} count
|
|
416
|
+
*/
|
|
417
|
+
spawn(emitter, count) {
|
|
418
|
+
assert.defined(emitter, "emitter");
|
|
419
|
+
assert.isNonNegativeInteger(count, "count");
|
|
420
|
+
|
|
421
|
+
const context = this.registry.context(emitter);
|
|
422
|
+
|
|
423
|
+
assert.defined(context, "emitter is not registered");
|
|
424
|
+
|
|
425
|
+
const ring = this.#spawn_commands;
|
|
426
|
+
const base = this.#spawn_command_count * PARTICLE_SPAWN_COMMAND_WORDS;
|
|
427
|
+
|
|
428
|
+
if (base >= ring.length) {
|
|
429
|
+
// the text is the key warn_limited counts on, so it carries no burst size: one message
|
|
430
|
+
// per ring size, not one per number a caller happens to ask for
|
|
431
|
+
warn_limited(`GPUParticleSystem: spawn command ring is full (${ring.length / PARTICLE_SPAWN_COMMAND_WORDS} per frame); dropping this frame's remaining bursts`);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
ring[base] = context.row;
|
|
436
|
+
ring[base + 1] = count;
|
|
437
|
+
this.#spawn_command_emitters[this.#spawn_command_count] = emitter;
|
|
438
|
+
this.#spawn_command_count++;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* The system's whole contribution to the frame, once per frame: reconcile the emitter set with
|
|
443
|
+
* the scene, push what changed to the GPU, and record every pass that moves particles — through
|
|
444
|
+
* to the indirect draw args.
|
|
445
|
+
*
|
|
446
|
+
* It draws nothing and touches no colour target. The handles it returns are what the AVBOIT
|
|
447
|
+
* side channels ({@link avboit_occupancy}, {@link avboit_splat}, {@link avboit_draw}) consume
|
|
448
|
+
* later in the same frame, off this same graph, once the transparent meshes have contributed
|
|
449
|
+
* theirs.
|
|
450
|
+
*
|
|
451
|
+
* Call after the scene context has built for the frame (`GPUSceneContext#update`), so every
|
|
452
|
+
* emitter's `transforms` row is uploaded, and after the hierarchy pass has run, so the rows'
|
|
453
|
+
* `global` matrices are this frame's.
|
|
454
|
+
*
|
|
455
|
+
* **Once per frame, not once per view.** This advances the simulation by `dt` and hands out
|
|
456
|
+
* handles into the graph it recorded into; a second view of the same scene records a second
|
|
457
|
+
* graph and would step the simulation twice. The renderer has one view per `render` call today,
|
|
458
|
+
* so this holds by construction; a second view of one scene needs the step and the handles
|
|
459
|
+
* separated — record the passes for the first graph, import the same buffers into the rest.
|
|
460
|
+
*
|
|
461
|
+
* @param {object} params
|
|
462
|
+
* @param {import("../../../engine/graphics/render/frame_graph/FrameGraph.js").FrameGraph} params.graph
|
|
463
|
+
* @param {number} params.camera graph handle of the camera uniform buffer
|
|
464
|
+
* @param {number} params.scene_database graph handle of the scene database — the one the
|
|
465
|
+
* hierarchy pass returned, so this reads the matrices it composed
|
|
466
|
+
* @param {number} params.dt seconds since the previous frame
|
|
467
|
+
* @returns {import("./graph_particles.js").ParticleSimulationHandles}
|
|
468
|
+
*/
|
|
469
|
+
execute({ graph, camera, scene_database, dt }) {
|
|
470
|
+
assert.defined(graph, "graph");
|
|
471
|
+
assert.isNumber(dt, "dt");
|
|
472
|
+
|
|
473
|
+
this.#upload();
|
|
474
|
+
|
|
475
|
+
const frame = this.#frame;
|
|
476
|
+
frame.dt = dt;
|
|
477
|
+
frame.time = this.time;
|
|
478
|
+
frame.index = this.frame;
|
|
479
|
+
frame.max_catchup = this.max_catchup_seconds;
|
|
480
|
+
frame.spawn_command_count = this.#spawn_command_count;
|
|
481
|
+
frame.rebucket = this.#program_assignment_version !== this.registry.program_assignment_version;
|
|
482
|
+
this.#program_assignment_version = this.registry.program_assignment_version;
|
|
483
|
+
|
|
484
|
+
const table = this.registry.table;
|
|
485
|
+
const emitters = this.#emitters;
|
|
486
|
+
emitters.capacity = table.element_capacity;
|
|
487
|
+
emitters.group_count = table.dispatch_group_count(PARTICLE_EMITTER_TICK_WORKGROUP_SIZE);
|
|
488
|
+
|
|
489
|
+
const buffers = this.buffers;
|
|
490
|
+
|
|
491
|
+
this.#swap_alive_list(frame.rebucket);
|
|
492
|
+
|
|
493
|
+
// The table's database owns and grows its own buffer; it is re-read every frame for that
|
|
494
|
+
// reason.
|
|
495
|
+
buffers.emitter_database = this.registry.database.buffer;
|
|
496
|
+
|
|
497
|
+
const simulated = graph_particles_simulate({
|
|
498
|
+
graph,
|
|
499
|
+
camera,
|
|
500
|
+
scene_database,
|
|
501
|
+
gpu: buffers,
|
|
502
|
+
emitters,
|
|
503
|
+
capacity: this.capacity,
|
|
504
|
+
bucket_count: this.bucketCount(),
|
|
505
|
+
// Whose register file the VM runs on is decided here, once, for the whole system: the
|
|
506
|
+
// registry reports what the hungriest registered effect needs and this compares it
|
|
507
|
+
// against what the fast file holds. Re-read every frame because registering an emitter —
|
|
508
|
+
// or unregistering the one hungry effect — can flip it either way.
|
|
509
|
+
wide_registers: this.registry.max_register_count > PARTICLE_VM_FAST_REGISTER_SLOTS,
|
|
510
|
+
frame,
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
// Kept for the side channels that draw later in this frame, and for nothing past it.
|
|
514
|
+
const current = this.#simulated;
|
|
515
|
+
current.graph = graph;
|
|
516
|
+
current.scene_database = scene_database;
|
|
517
|
+
current.emitter_group_count = emitters.group_count;
|
|
518
|
+
current.handles = simulated;
|
|
519
|
+
|
|
520
|
+
// Consumed: the counts were recorded into the graph, the words were uploaded.
|
|
521
|
+
this.#spawn_command_count = 0;
|
|
522
|
+
this.#warmup_queue.length = 0;
|
|
523
|
+
|
|
524
|
+
this.frame++;
|
|
525
|
+
this.time += dt;
|
|
526
|
+
|
|
527
|
+
return simulated;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* What this frame's {@link execute} left, checked against the graph a side-channel call names:
|
|
532
|
+
* the AVBOIT orchestrator records its passes into the same graph, later in the same frame.
|
|
533
|
+
*
|
|
534
|
+
* @param {import("../../../engine/graphics/render/frame_graph/FrameGraph.js").FrameGraph} graph
|
|
535
|
+
* @returns {{graph: object, scene_database: number, emitter_group_count: number, handles: import("./graph_particles.js").ParticleSimulationHandles}}
|
|
536
|
+
*/
|
|
537
|
+
#simulated_for(graph) {
|
|
538
|
+
const current = this.#simulated;
|
|
539
|
+
|
|
540
|
+
assert.equal(current.graph, graph, "the particles were not simulated into this graph this frame — call execute() before the transparency pipeline records");
|
|
541
|
+
|
|
542
|
+
return current;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* `AVBOITSideChannel#avboit_occupancy`: the emitters' measured bounding boxes, as depth slices.
|
|
547
|
+
*
|
|
548
|
+
* @param {object} args see `AVBOITSideChannel`
|
|
549
|
+
* @param {import("../../../engine/graphics/render/frame_graph/FrameGraph.js").FrameGraph} args.graph
|
|
550
|
+
* @param {number} args.camera graph handle of the camera uniform buffer
|
|
551
|
+
* @param {ArrayLike<number>} args.cluster_parameters the frame's depth curve
|
|
552
|
+
* @param {number} args.occupancy graph handle of the occupancy bits
|
|
553
|
+
* @returns {number} the occupancy handle after the write
|
|
554
|
+
*/
|
|
555
|
+
avboit_occupancy({ graph, camera, cluster_parameters, occupancy }) {
|
|
556
|
+
const current = this.#simulated_for(graph);
|
|
557
|
+
|
|
558
|
+
return graph_particle_avboit_occupancy({
|
|
559
|
+
graph,
|
|
560
|
+
camera,
|
|
561
|
+
cluster_parameters,
|
|
562
|
+
occupancy,
|
|
563
|
+
emitter_database: current.handles.emitter_database,
|
|
564
|
+
emitter_group_count: current.emitter_group_count,
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* `AVBOITSideChannel#avboit_splat`: the live particles' extinction, into the volume.
|
|
570
|
+
*
|
|
571
|
+
* @param {object} args see `AVBOITSideChannel`
|
|
572
|
+
* @param {import("../../../engine/graphics/render/frame_graph/FrameGraph.js").FrameGraph} args.graph
|
|
573
|
+
* @param {number} args.camera graph handle of the camera uniform buffer
|
|
574
|
+
* @param {number} args.warp graph handle of the warp LUT
|
|
575
|
+
* @param {number} args.depth_texture graph handle of the scene depth
|
|
576
|
+
* @param {{extinction: number, overflow: number, dummy: number}} args.volume the voxel volume
|
|
577
|
+
* @returns {{extinction: number, overflow: number, dummy: number}} the volume after the write
|
|
578
|
+
*/
|
|
579
|
+
avboit_splat({ graph, camera, warp, depth_texture, volume }) {
|
|
580
|
+
const { handles } = this.#simulated_for(graph);
|
|
581
|
+
|
|
582
|
+
return graph_particle_avboit_splat({
|
|
583
|
+
graph,
|
|
584
|
+
camera,
|
|
585
|
+
warp,
|
|
586
|
+
depth_texture,
|
|
587
|
+
volume,
|
|
588
|
+
atlas: this.atlas,
|
|
589
|
+
atlas_sampler: this.atlas_sampler,
|
|
590
|
+
pool: handles.pool,
|
|
591
|
+
alive: handles.alive,
|
|
592
|
+
emitter_database: handles.emitter_database,
|
|
593
|
+
draw_args: handles.draw_args,
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* `AVBOITSideChannel#avboit_draw`: the live particles, into the accumulators.
|
|
599
|
+
*
|
|
600
|
+
* @param {object} args see `AVBOITSideChannel`
|
|
601
|
+
* @param {import("../../../engine/graphics/render/frame_graph/FrameGraph.js").FrameGraph} args.graph
|
|
602
|
+
* @param {import("../view/GPUViewContext.js").GPUViewContext} args.view_ctx
|
|
603
|
+
* @param {number} args.camera graph handle of the camera uniform buffer
|
|
604
|
+
* @param {number} args.depth_texture graph handle of the scene depth
|
|
605
|
+
* @param {number} args.warp graph handle of the warp LUT
|
|
606
|
+
* @param {number} args.integral graph handle of the integral texture
|
|
607
|
+
* @param {number} args.volumetrics_lut graph handle
|
|
608
|
+
* @param {number} args.volumetrics_metadata graph handle
|
|
609
|
+
* @param {{Y: number, CoCg: number}} args.volumetrics_irradiance graph handles of the froxel
|
|
610
|
+
* irradiance volume, what an emitter flagged `EMITTER_FLAG.LIGHTING` is lit by
|
|
611
|
+
* @param {{color: number, norm: number, extinction: number, emission: number}} args.accumulators
|
|
612
|
+
* @returns {{color: number, norm: number, extinction: number, emission: number}} after the draw
|
|
613
|
+
*/
|
|
614
|
+
avboit_draw({ graph, view_ctx, camera, depth_texture, warp, integral, volumetrics_lut, volumetrics_metadata, volumetrics_irradiance, accumulators }) {
|
|
615
|
+
const { handles } = this.#simulated_for(graph);
|
|
616
|
+
|
|
617
|
+
return graph_particle_avboit_draw({
|
|
618
|
+
graph,
|
|
619
|
+
view_ctx,
|
|
620
|
+
camera,
|
|
621
|
+
depth_texture,
|
|
622
|
+
warp,
|
|
623
|
+
integral,
|
|
624
|
+
volumetrics_lut,
|
|
625
|
+
volumetrics_metadata,
|
|
626
|
+
volumetrics_irradiance,
|
|
627
|
+
accumulators,
|
|
628
|
+
atlas: this.atlas,
|
|
629
|
+
atlas_sampler: this.atlas_sampler,
|
|
630
|
+
pool: handles.pool,
|
|
631
|
+
alive: handles.alive,
|
|
632
|
+
emitter_database: handles.emitter_database,
|
|
633
|
+
draw_args: handles.draw_args,
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Register an emitter at a `transforms` row, and if it asks, warm it up before its first frame.
|
|
639
|
+
*
|
|
640
|
+
* The row is an entity's, one on (`row_of_entity`), and whoever owns the entity holds it through
|
|
641
|
+
* the scene context's rows for as long as the emitter is registered; this only records which
|
|
642
|
+
* row the emitter's particles are born at. Nothing is uploaded until the next {@link execute}.
|
|
643
|
+
*
|
|
644
|
+
* @param {import("./runtime/ParticleEmitter.js").ParticleEmitter} emitter
|
|
645
|
+
* @param {number} transform_row
|
|
646
|
+
*/
|
|
647
|
+
add_emitter(emitter, transform_row) {
|
|
648
|
+
assert.defined(emitter, 'emitter');
|
|
649
|
+
assert.equal(emitter.isParticleEmitter, true, 'emitter.isParticleEmitter !== true');
|
|
650
|
+
assert.isNonNegativeInteger(transform_row, 'transform_row');
|
|
651
|
+
|
|
652
|
+
this.registry.add(emitter, transform_row);
|
|
653
|
+
|
|
654
|
+
// Just arrived, and asked to arrive warm: the warm-up pipeline runs its simulation forward
|
|
655
|
+
// this frame, before it is first drawn. A reused row is a new emitter as far as this is
|
|
656
|
+
// concerned — it is the emitter that asks, not the row.
|
|
657
|
+
if (emitter.prewarm > 0) {
|
|
658
|
+
this.#warmup_queue.push(emitter);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Retire an emitter's row; its particles go within a frame, carrying the row's generation.
|
|
664
|
+
*
|
|
665
|
+
* @param {import("./runtime/ParticleEmitter.js").ParticleEmitter} emitter
|
|
666
|
+
*/
|
|
667
|
+
remove_emitter(emitter) {
|
|
668
|
+
assert.defined(emitter, 'emitter');
|
|
669
|
+
|
|
670
|
+
this.registry.remove(emitter);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Point a registered emitter at another row.
|
|
675
|
+
*
|
|
676
|
+
* @param {import("./runtime/ParticleEmitter.js").ParticleEmitter} emitter
|
|
677
|
+
* @param {number} transform_row
|
|
678
|
+
*/
|
|
679
|
+
set_transform_row(emitter, transform_row) {
|
|
680
|
+
assert.isNonNegativeInteger(transform_row, 'transform_row');
|
|
681
|
+
|
|
682
|
+
this.registry.set_transform_row(emitter, transform_row);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Push everything the CPU changed to the GPU: dirty emitter rows, the state buffer's growth,
|
|
687
|
+
* rebuilt program buffers, the spawn-command ring, and the coherence scratch's growth. A
|
|
688
|
+
* command context is opened only if something needs one.
|
|
689
|
+
*/
|
|
690
|
+
#upload() {
|
|
691
|
+
const registry = this.registry;
|
|
692
|
+
const device = this.graphics.device;
|
|
693
|
+
|
|
694
|
+
registry.flush();
|
|
695
|
+
|
|
696
|
+
// Both tables stage into the one database: the emitter rows the author changed, and the
|
|
697
|
+
// state rows claimed or released with them.
|
|
698
|
+
const rows_staged = registry.table.element_upload_buffer.position > 0
|
|
699
|
+
|| registry.state_table.element_upload_buffer.position > 0;
|
|
700
|
+
|
|
701
|
+
if (rows_staged) {
|
|
702
|
+
const cmd = ShadeGPUCommandContext.create(this.graphics, "particles/upload");
|
|
703
|
+
|
|
704
|
+
registry.database.update(cmd);
|
|
705
|
+
|
|
706
|
+
cmd.finish();
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
if (registry.programs_dirty) {
|
|
710
|
+
this.buffers.program = replace_data_buffer(device, this.buffers.program, "particles/program", registry.build_program());
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
this.#merge_spawn_commands();
|
|
714
|
+
|
|
715
|
+
if (this.#spawn_command_count > 0) {
|
|
716
|
+
device.queue.writeBuffer(
|
|
717
|
+
this.buffers.spawn_commands, 0,
|
|
718
|
+
this.#spawn_commands.buffer, 0,
|
|
719
|
+
this.#spawn_command_count * PARTICLE_SPAWN_COMMAND_WORDS * WORD,
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
this.#upload_warmup_queue();
|
|
724
|
+
|
|
725
|
+
this.#ensure_sim_buffers(this.bucketCount());
|
|
726
|
+
this.#ensure_counters(registry.table.element_capacity);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Turn the emitters queued for a warm-up into the words the warm-up tick reads, and the numbers
|
|
731
|
+
* the frame graph sizes the loop by; upload the words. Nothing when nothing is queued, which is
|
|
732
|
+
* nearly every frame.
|
|
733
|
+
*
|
|
734
|
+
* Two per-emitter numbers are hashed here rather than left at zero, both to keep emitters warmed
|
|
735
|
+
* in one batch off each other's beat: the accumulator's starting fraction, so their first whole
|
|
736
|
+
* particles fall on different ticks, and the clock each one's tick runs at. See
|
|
737
|
+
* `warmup/shader_particle_warmup.js`.
|
|
738
|
+
*
|
|
739
|
+
* The transient pool is sized by what the batch can spawn in all — every emitter's rate over its
|
|
740
|
+
* own pre-warm, plus one each for the rounding — and no larger than the main pool it is bound
|
|
741
|
+
* for: what would not fit there is dropped at migration anyway.
|
|
742
|
+
*/
|
|
743
|
+
#upload_warmup_queue() {
|
|
744
|
+
const queue = this.#warmup_queue;
|
|
745
|
+
const frame = this.#frame;
|
|
746
|
+
|
|
747
|
+
// A queued emitter that has since left the scene has no row to warm; drop it here.
|
|
748
|
+
let count = 0;
|
|
749
|
+
|
|
750
|
+
for (let i = 0; i < queue.length; i++) {
|
|
751
|
+
const emitter = queue[i];
|
|
752
|
+
|
|
753
|
+
if (this.registry.context(emitter) !== undefined) {
|
|
754
|
+
queue[count++] = emitter;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
queue.length = count;
|
|
759
|
+
|
|
760
|
+
if (count === 0) {
|
|
761
|
+
frame.warmup_count = 0;
|
|
762
|
+
frame.warmup_capacity = 0;
|
|
763
|
+
frame.warmup_schedule = null;
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
let words = this.#warmup_words;
|
|
768
|
+
const needed = count * PARTICLE_WARMUP_QUEUE_WORDS;
|
|
769
|
+
|
|
770
|
+
if (words.length < needed) {
|
|
771
|
+
words = new Uint32Array(Math.max(needed, words.length * 2));
|
|
772
|
+
this.#warmup_words = words;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const floats = new Float32Array(words.buffer, words.byteOffset, words.length);
|
|
776
|
+
|
|
777
|
+
let max_prewarm = 0;
|
|
778
|
+
let spawned = 0;
|
|
779
|
+
|
|
780
|
+
for (let i = 0; i < count; i++) {
|
|
781
|
+
const emitter = queue[i];
|
|
782
|
+
const context = this.registry.context(emitter);
|
|
783
|
+
const base = i * PARTICLE_WARMUP_QUEUE_WORDS;
|
|
784
|
+
|
|
785
|
+
// Two unit fractions from one hash of the row and the emitter's seed: the same emitter
|
|
786
|
+
// in the same row warms the same way twice, and two emitters do not.
|
|
787
|
+
const hash = hash_mix2(context.row, context.generation) >>> 0;
|
|
788
|
+
const phase = (hash >>> 8) / 16777216;
|
|
789
|
+
const beat = ((hash & 0xff) / 255) - 0.5;
|
|
790
|
+
|
|
791
|
+
words[base + WARMUP_QUEUE.ROW] = context.row;
|
|
792
|
+
floats[base + WARMUP_QUEUE.PREWARM] = emitter.prewarm;
|
|
793
|
+
floats[base + WARMUP_QUEUE.ACCUMULATOR] = phase;
|
|
794
|
+
floats[base + WARMUP_QUEUE.CLOCK] = 1 + PARTICLE_WARMUP_EMITTER_JITTER * beat;
|
|
795
|
+
|
|
796
|
+
max_prewarm = Math.max(max_prewarm, emitter.prewarm);
|
|
797
|
+
spawned += Math.ceil(emitter.spawn_rate * emitter.prewarm) + 1;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
const device = this.graphics.device;
|
|
801
|
+
const bytes = needed * WORD;
|
|
802
|
+
|
|
803
|
+
if (this.buffers.warmup_queue.size < bytes) {
|
|
804
|
+
this.buffers.warmup_queue.destroy();
|
|
805
|
+
this.buffers.warmup_queue = device.createBuffer({ label: "particles/warmup_queue", size: bytes, usage: STORAGE_UPLOAD });
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
device.queue.writeBuffer(this.buffers.warmup_queue, 0, words.buffer, words.byteOffset, bytes);
|
|
809
|
+
|
|
810
|
+
frame.warmup_count = count;
|
|
811
|
+
frame.warmup_capacity = Math.max(1, Math.min(this.capacity, spawned));
|
|
812
|
+
frame.warmup_schedule = warmup_schedule(max_prewarm);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* Fold the ring down to one entry per row, dropping entries whose emitter no longer holds the
|
|
817
|
+
* row it was issued against.
|
|
818
|
+
*
|
|
819
|
+
* The spawn-command pass adds to a state row with a plain read-modify-write, so two entries
|
|
820
|
+
* naming one row would lose one; and a burst issued to an emitter that left the scene this
|
|
821
|
+
* frame must not land on the row's next tenant. Both are settled here, in place, over a few
|
|
822
|
+
* dozen entries at most.
|
|
823
|
+
*/
|
|
824
|
+
#merge_spawn_commands() {
|
|
825
|
+
const count = this.#spawn_command_count;
|
|
826
|
+
|
|
827
|
+
if (count === 0) {
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
const ring = this.#spawn_commands;
|
|
832
|
+
const emitters = this.#spawn_command_emitters;
|
|
833
|
+
|
|
834
|
+
let rows = this.#spawn_by_row;
|
|
835
|
+
const needed = Math.max(1, this.registry.table.element_capacity);
|
|
836
|
+
|
|
837
|
+
if (rows.length < needed) {
|
|
838
|
+
rows = new Uint32Array(needed);
|
|
839
|
+
this.#spawn_by_row = rows;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
for (let i = 0; i < count; i++) {
|
|
843
|
+
const emitter = emitters[i];
|
|
844
|
+
const row = ring[i * PARTICLE_SPAWN_COMMAND_WORDS];
|
|
845
|
+
const context = this.registry.context(emitter);
|
|
846
|
+
|
|
847
|
+
if (context !== undefined && context.row === row) {
|
|
848
|
+
rows[row] += ring[i * PARTICLE_SPAWN_COMMAND_WORDS + 1];
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
emitters[i] = null;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
let merged = 0;
|
|
855
|
+
|
|
856
|
+
for (let i = 0; i < count; i++) {
|
|
857
|
+
const row = ring[i * PARTICLE_SPAWN_COMMAND_WORDS];
|
|
858
|
+
const pending = rows[row];
|
|
859
|
+
|
|
860
|
+
if (pending === 0) {
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
ring[merged * PARTICLE_SPAWN_COMMAND_WORDS] = row;
|
|
865
|
+
ring[merged * PARTICLE_SPAWN_COMMAND_WORDS + 1] = pending;
|
|
866
|
+
rows[row] = 0;
|
|
867
|
+
merged++;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
this.#spawn_command_count = merged;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* The counters buffer holds the per-emitter bounds accumulators after the counters themselves
|
|
875
|
+
* (`data/PARTICLE_COUNTERS.js`), so it follows the emitter table's element capacity.
|
|
876
|
+
*
|
|
877
|
+
* Grown by copy, not by reallocation: the counters at the head of it are live GPU state — the
|
|
878
|
+
* free-list length above all, which no CPU value can reconstruct — and the accumulators are this
|
|
879
|
+
* frame's measurements of emitters that have not moved. A fresh buffer would zero the free list
|
|
880
|
+
* and strand the whole pool.
|
|
881
|
+
*
|
|
882
|
+
* The rows past the old end are the only ones the copy does not answer for, and zero is the
|
|
883
|
+
* wrong answer for them: a zero accumulator reads back as a degenerate box at the world origin
|
|
884
|
+
* rather than as an absence, and the first box measured for such a row would be stretched to
|
|
885
|
+
* include the origin. They get the empty interval written over them instead — the same two words
|
|
886
|
+
* the publish pass resets a row to.
|
|
887
|
+
*
|
|
888
|
+
* @param {number} emitter_capacity element capacity of the emitter table
|
|
889
|
+
*/
|
|
890
|
+
#ensure_counters(emitter_capacity) {
|
|
891
|
+
const size = particle_counters_word_count(emitter_capacity) * WORD;
|
|
892
|
+
const existing = this.buffers.counters;
|
|
893
|
+
|
|
894
|
+
if (existing.size >= size) {
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
const device = this.graphics.device;
|
|
899
|
+
const grown = device.createBuffer({ label: "particles/counters", size, usage: STORAGE_GROWABLE });
|
|
900
|
+
|
|
901
|
+
const cmd = ShadeGPUCommandContext.create(this.graphics, "particles/counters grow");
|
|
902
|
+
|
|
903
|
+
cmd.gpu_encoder.copyBufferToBuffer(existing, 0, grown, 0, existing.size);
|
|
904
|
+
|
|
905
|
+
cmd.finish();
|
|
906
|
+
|
|
907
|
+
// The new region is a whole number of accumulators: the counters are the head of the buffer
|
|
908
|
+
// and every growth adds rows behind them.
|
|
909
|
+
const added_rows = (size - existing.size) / (PARTICLE_BOUNDS_WORDS * WORD);
|
|
910
|
+
|
|
911
|
+
device.queue.writeBuffer(grown, existing.size, particle_bounds_empty_words(added_rows));
|
|
912
|
+
|
|
913
|
+
existing.destroy();
|
|
914
|
+
|
|
915
|
+
this.buffers.counters = grown;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* The coherence list follows the peak number of program ids. It is the one thing the bucketing
|
|
920
|
+
* leaves behind: the next simulate consumes its entries and padding with the previous frame's
|
|
921
|
+
* saved count, so it cannot be a transient and it cannot be reallocated empty. Growth copies it
|
|
922
|
+
* forward; the old count and dispatch stay valid because the entries keep their places.
|
|
923
|
+
*
|
|
924
|
+
* The bins, cursors, keys and dispatch args the bucketing needs alongside it live and die inside
|
|
925
|
+
* one frame graph, so `coherence/` declares them on the passes that write them and the graph
|
|
926
|
+
* owns them.
|
|
927
|
+
*
|
|
928
|
+
* @param {number} buckets
|
|
929
|
+
*/
|
|
930
|
+
#ensure_sim_buffers(buckets) {
|
|
931
|
+
if (buckets <= this.#sim_bucket_capacity) {
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
const device = this.graphics.device;
|
|
936
|
+
const buffers = this.buffers;
|
|
937
|
+
|
|
938
|
+
// Padding rounds each bucket up to a whole workgroup, so it can add at most `workgroup - 1`
|
|
939
|
+
// entries per bucket; one whole workgroup per bucket is the bound.
|
|
940
|
+
const previous = buffers.sim_list;
|
|
941
|
+
buffers.sim_list = device.createBuffer({
|
|
942
|
+
label: "particles/sim_list",
|
|
943
|
+
size: (this.capacity + buckets * PARTICLE_SIMULATE_WORKGROUP_SIZE) * WORD,
|
|
944
|
+
usage: STORAGE_GROWABLE,
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
if (previous !== undefined) {
|
|
948
|
+
const cmd = ShadeGPUCommandContext.create(this.graphics, "particles/coherence grow");
|
|
949
|
+
cmd.gpu_encoder.copyBufferToBuffer(previous, 0, buffers.sim_list, 0, previous.size);
|
|
950
|
+
cmd.finish();
|
|
951
|
+
previous.destroy();
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
this.#sim_bucket_capacity = buckets;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
/**
|
|
958
|
+
* Give this frame its alive list, and hand back the ones nothing reads any more.
|
|
959
|
+
*
|
|
960
|
+
* The list is the allocator's memory on loan rather than a pair of buffers reserved here — see
|
|
961
|
+
* the class doc. Three things happen, in this order:
|
|
962
|
+
*
|
|
963
|
+
* 1. A list an earlier frame retired goes back. That frame's graph has been recorded, executed
|
|
964
|
+
* and submitted since, so the bucketing pass that was reading it is done.
|
|
965
|
+
* 2. The previous frame's list goes back too — BEFORE the new one is asked for, so that the
|
|
966
|
+
* pool can answer with the same memory and the steady-state footprint is one list. Skipped
|
|
967
|
+
* when this frame rebuilds the buckets, because that pass reads it; it is retired instead
|
|
968
|
+
* and goes back on the next call, one step behind.
|
|
969
|
+
* 3. The new list is taken. Uncleared: the frame appends to it from a counter its first pass
|
|
970
|
+
* zeroes, so no reader can reach what the last borrower left.
|
|
971
|
+
*
|
|
972
|
+
* The pool may answer with a larger buffer than asked for, which changes nothing — every
|
|
973
|
+
* reader is bounded by a count, never by the buffer's length.
|
|
974
|
+
*
|
|
975
|
+
* @param {boolean} rebucket whether this frame's bucketing rebuild reads the previous list
|
|
976
|
+
*/
|
|
977
|
+
#swap_alive_list(rebucket) {
|
|
978
|
+
const allocator = this.graphics.buffer_allocator_main;
|
|
979
|
+
const buffers = this.buffers;
|
|
980
|
+
const previous = this.#alive;
|
|
981
|
+
|
|
982
|
+
// One is taken at construction and none is ever let go without another taking its place, so
|
|
983
|
+
// there is always a previous list — including on the first frame, which can already be a
|
|
984
|
+
// rebucket if a program was edited before it.
|
|
985
|
+
assert.notNull(previous, "the previous alive list");
|
|
986
|
+
|
|
987
|
+
if (this.#alive_retired !== null) {
|
|
988
|
+
allocator.release(this.#alive_retired);
|
|
989
|
+
this.#alive_retired = null;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
if (rebucket) {
|
|
993
|
+
this.#alive_retired = previous;
|
|
994
|
+
} else {
|
|
995
|
+
allocator.release(previous);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
const alive = allocator.get(this.#alive_descriptor);
|
|
999
|
+
|
|
1000
|
+
this.#alive = alive;
|
|
1001
|
+
|
|
1002
|
+
buffers.alive = alive;
|
|
1003
|
+
buffers.alive_write = alive;
|
|
1004
|
+
// Named only on the frames that read one: on every other frame `previous` has already gone
|
|
1005
|
+
// back to the pool, and may be somebody else's by the time the graph runs.
|
|
1006
|
+
buffers.alive_read = rebucket ? previous : undefined;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
/**
|
|
1010
|
+
* Hand every alive list back to the allocator and stop naming them.
|
|
1011
|
+
*
|
|
1012
|
+
* {@link destroy} cannot simply destroy them with the rest: the memory is the pool's, and a
|
|
1013
|
+
* pooled buffer destroyed behind the pool's back is a handle it will hand to the next caller.
|
|
1014
|
+
*/
|
|
1015
|
+
#release_alive_lists() {
|
|
1016
|
+
const allocator = this.graphics.buffer_allocator_main;
|
|
1017
|
+
|
|
1018
|
+
if (this.#alive !== null) {
|
|
1019
|
+
allocator.release(this.#alive);
|
|
1020
|
+
this.#alive = null;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
if (this.#alive_retired !== null) {
|
|
1024
|
+
allocator.release(this.#alive_retired);
|
|
1025
|
+
this.#alive_retired = null;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
delete this.buffers.alive;
|
|
1029
|
+
delete this.buffers.alive_write;
|
|
1030
|
+
delete this.buffers.alive_read;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
/**
|
|
1034
|
+
* Fill the free list with every slot, on the GPU — see `shaders/shader_particle_init_free_list.js`
|
|
1035
|
+
* for why this is a dispatch and not a host write.
|
|
1036
|
+
*
|
|
1037
|
+
* The launch is clamped to what a dispatch dimension holds and the shader strides through the
|
|
1038
|
+
* rest: at the default capacity this is a couple of thousand workgroups, but the capacity is
|
|
1039
|
+
* the caller's to choose and a dispatch past `maxComputeWorkgroupsPerDimension` is refused.
|
|
1040
|
+
*/
|
|
1041
|
+
#init_free_list() {
|
|
1042
|
+
const device = this.graphics.device;
|
|
1043
|
+
|
|
1044
|
+
const cmd = ShadeGPUCommandContext.create(this.graphics, "particles/free list init");
|
|
1045
|
+
|
|
1046
|
+
shader_particle_init_free_list.dispatch({
|
|
1047
|
+
encoder: cmd,
|
|
1048
|
+
group_count_x: Math.min(
|
|
1049
|
+
Math.ceil(this.capacity / PARTICLE_INIT_FREE_LIST_WORKGROUP_SIZE),
|
|
1050
|
+
device.limits.maxComputeWorkgroupsPerDimension,
|
|
1051
|
+
),
|
|
1052
|
+
bindings: { dead: this.buffers.dead, u_capacity: this.capacity },
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
cmd.finish();
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
/**
|
|
1059
|
+
* The buffers whose size is fixed by the pool capacity, and the free list's initial contents.
|
|
1060
|
+
*/
|
|
1061
|
+
#create_buffers() {
|
|
1062
|
+
const device = this.graphics.device;
|
|
1063
|
+
const capacity = this.capacity;
|
|
1064
|
+
const buffers = this.buffers;
|
|
1065
|
+
|
|
1066
|
+
buffers.pool = device.createBuffer({ label: "particles/pool", size: capacity * PARTICLE_RECORD_WORD_COUNT * WORD, usage: STORAGE });
|
|
1067
|
+
|
|
1068
|
+
this.#alive_descriptor = BufferResourceDescriptor.from(capacity * WORD, STORAGE);
|
|
1069
|
+
this.#alive = this.graphics.buffer_allocator_main.get(this.#alive_descriptor);
|
|
1070
|
+
buffers.alive = this.#alive;
|
|
1071
|
+
|
|
1072
|
+
buffers.dead = device.createBuffer({ label: "particles/dead", size: capacity * WORD, usage: STORAGE });
|
|
1073
|
+
// Just the counters to begin with. The per-emitter bounds accumulator that shares this
|
|
1074
|
+
// buffer follows the emitter table's capacity, and there is no table yet.
|
|
1075
|
+
buffers.counters = device.createBuffer({ label: "particles/counters", size: PARTICLE_COUNTER_COUNT * WORD, usage: STORAGE_GROWABLE });
|
|
1076
|
+
|
|
1077
|
+
// Simulate's workgroup count, written by the END of one frame's build-indirect and read by
|
|
1078
|
+
// the NEXT frame's dispatch — the one indirect buffer that crosses the boundary, and so the
|
|
1079
|
+
// one the graph cannot own. Zero-initialised (WebGPU spec), so the first frame's indirect
|
|
1080
|
+
// dispatch is a no-op until the first build-indirect has run.
|
|
1081
|
+
buffers.dispatch_args = device.createBuffer({ label: "particles/dispatch_args", size: 3 * WORD, usage: INDIRECT });
|
|
1082
|
+
|
|
1083
|
+
buffers.spawn_commands = device.createBuffer({ label: "particles/spawn_commands", size: this.#spawn_commands.byteLength, usage: STORAGE_UPLOAD });
|
|
1084
|
+
|
|
1085
|
+
// The warm-up queue: one entry to start with, grown on upload to what a frame registers.
|
|
1086
|
+
buffers.warmup_queue = device.createBuffer({ label: "particles/warmup_queue", size: PARTICLE_WARMUP_QUEUE_WORDS * WORD, usage: STORAGE_UPLOAD });
|
|
1087
|
+
|
|
1088
|
+
// The counters say every slot is free; the pass below makes it so. A handful of words is
|
|
1089
|
+
// the whole of what the host ever writes into either — the list itself is the GPU's.
|
|
1090
|
+
const counters = new Uint32Array(PARTICLE_COUNTER_COUNT);
|
|
1091
|
+
counters[PARTICLE_COUNTER.DEAD] = capacity;
|
|
1092
|
+
|
|
1093
|
+
device.queue.writeBuffer(buffers.counters, 0, counters);
|
|
1094
|
+
|
|
1095
|
+
this.#init_free_list();
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
/**
|
|
1099
|
+
* A 1x1 white atlas and a linear sampler, so the system draws before anyone hands it art.
|
|
1100
|
+
* @returns {void}
|
|
1101
|
+
*/
|
|
1102
|
+
#create_default_atlas() {
|
|
1103
|
+
const device = this.graphics.device;
|
|
1104
|
+
|
|
1105
|
+
const texture = new GPUTextureContext(device);
|
|
1106
|
+
texture.descriptor.label = "particles/default_atlas";
|
|
1107
|
+
texture.descriptor.size = [1, 1];
|
|
1108
|
+
texture.descriptor.format = "rgba8unorm";
|
|
1109
|
+
texture.descriptor.usage = GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST;
|
|
1110
|
+
|
|
1111
|
+
device.queue.writeTexture({ texture: texture.gpu_texture }, new Uint8Array([255, 255, 255, 255]), { bytesPerRow: 4 }, [1, 1]);
|
|
1112
|
+
texture.incrementVersion();
|
|
1113
|
+
|
|
1114
|
+
// kept, not just viewed: a texture nothing holds is a texture nothing can free
|
|
1115
|
+
this.#default_atlas = texture;
|
|
1116
|
+
this.atlas = texture.obtainView();
|
|
1117
|
+
this.atlas_sampler = device.createSampler({
|
|
1118
|
+
label: "particles/atlas_sampler",
|
|
1119
|
+
magFilter: "linear",
|
|
1120
|
+
minFilter: "linear",
|
|
1121
|
+
addressModeU: "clamp-to-edge",
|
|
1122
|
+
addressModeV: "clamp-to-edge",
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
/**
|
|
1127
|
+
* Sizes of the persistent buffers, in bytes, as they stand — one entry per distinct buffer, so
|
|
1128
|
+
* this is the feature's footprint and not a list of the names it binds them under.
|
|
1129
|
+
* `alive_write` is `alive` under the frame's name for it and would otherwise be counted twice;
|
|
1130
|
+
* `alive_read` is a second list, and appears only on the frames that hold one. Exposed for tests.
|
|
1131
|
+
*
|
|
1132
|
+
* @returns {Object<string, number>}
|
|
1133
|
+
*/
|
|
1134
|
+
bufferSizes() {
|
|
1135
|
+
const sizes = {};
|
|
1136
|
+
const counted = new Set();
|
|
1137
|
+
|
|
1138
|
+
for (const [name, buffer] of Object.entries(this.buffers)) {
|
|
1139
|
+
if (buffer === undefined || buffer === null || counted.has(buffer)) {
|
|
1140
|
+
continue;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
counted.add(buffer);
|
|
1144
|
+
sizes[name] = buffer.size;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
return sizes;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
/**
|
|
1151
|
+
* Copy the counts the GPU keeps back to the host and decode them, for display and diagnostics.
|
|
1152
|
+
*
|
|
1153
|
+
* Read back to *look at*: nothing here may size a pass. Every number describes a frame that has
|
|
1154
|
+
* already been submitted, so by the time one arrives it is at best one frame stale — and sizing
|
|
1155
|
+
* a dispatch or a draw from a stale count is a correctness bug, not an inaccuracy, for the
|
|
1156
|
+
* reasons `shader_particle_build_indirect` sets out. This is the whole of what the feature ever
|
|
1157
|
+
* reads back, and the only reason {@link INDIRECT} carries `COPY_SRC` at all.
|
|
1158
|
+
*
|
|
1159
|
+
* The draw args are not here and cannot be: they never leave the frame graph, which allocates
|
|
1160
|
+
* them at the build-indirect pass and releases them after the last draw that reads them. Their
|
|
1161
|
+
* instance count is {@link ParticleSystemStats#alive}, which the counters carry anyway.
|
|
1162
|
+
*
|
|
1163
|
+
* Two copies, two submissions and a map round trip, so a caller polling this for an overlay
|
|
1164
|
+
* wants to rate-limit itself and to hold off while a previous read is still outstanding.
|
|
1165
|
+
*
|
|
1166
|
+
* @returns {Promise<ParticleSystemStats>}
|
|
1167
|
+
*/
|
|
1168
|
+
async read_stats() {
|
|
1169
|
+
const device = this.graphics.device;
|
|
1170
|
+
const buffers = this.buffers;
|
|
1171
|
+
|
|
1172
|
+
const read = (buffer, words) => inspect_gpu_buffer({
|
|
1173
|
+
device,
|
|
1174
|
+
buffer,
|
|
1175
|
+
buffer_size: words * WORD,
|
|
1176
|
+
// The mapped range is only valid for the length of the callback, so the decode either
|
|
1177
|
+
// happens inside it or takes a copy. A copy, so one `Promise.all` sees both buffers.
|
|
1178
|
+
callback: bytes => new Uint32Array(bytes.slice(0)),
|
|
1179
|
+
});
|
|
1180
|
+
|
|
1181
|
+
const [counters, dispatch] = await Promise.all([
|
|
1182
|
+
// The head of the buffer only: what follows the counters is the per-emitter bounds
|
|
1183
|
+
// accumulator, which is boxes rather than counts and grows with the emitter table.
|
|
1184
|
+
read(buffers.counters, PARTICLE_COUNTER_COUNT),
|
|
1185
|
+
read(buffers.dispatch_args, 3),
|
|
1186
|
+
]);
|
|
1187
|
+
|
|
1188
|
+
return {
|
|
1189
|
+
alive: counters[PARTICLE_COUNTER.ALIVE],
|
|
1190
|
+
dead: counters[PARTICLE_COUNTER.DEAD],
|
|
1191
|
+
spawn_budget: counters[PARTICLE_COUNTER.SPAWN_BUDGET],
|
|
1192
|
+
alive_in: counters[PARTICLE_COUNTER.ALIVE_IN],
|
|
1193
|
+
sim_padded: counters[PARTICLE_COUNTER.SIM_PADDED],
|
|
1194
|
+
sim_workgroups: dispatch[0],
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
destroy() {
|
|
1199
|
+
/*
|
|
1200
|
+
The emitters go first, and they matter more than the buffers: they belong to their owners,
|
|
1201
|
+
they outlive this system, and each is holding a row of the table about to be freed.
|
|
1202
|
+
Retiring them here puts every emitter back where it was before this system saw it, so its
|
|
1203
|
+
owner can register it with the next system.
|
|
1204
|
+
*/
|
|
1205
|
+
for (const emitter of this.registry.emitters.slice()) {
|
|
1206
|
+
this.registry.remove(emitter);
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
// Before the loop below, and not in it: the alive lists are the allocator's and go back to
|
|
1210
|
+
// it. Destroying one would leave the pool holding a handle it will hand to the next caller.
|
|
1211
|
+
this.#release_alive_lists();
|
|
1212
|
+
|
|
1213
|
+
for (const buffer of Object.values(this.buffers)) {
|
|
1214
|
+
if (buffer !== undefined && buffer !== null) {
|
|
1215
|
+
buffer.destroy();
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
this.buffers = {};
|
|
1220
|
+
this.registry.database.destroy();
|
|
1221
|
+
|
|
1222
|
+
// the default atlas is this system's own; a page that replaced it owns what it put there
|
|
1223
|
+
this.#default_atlas?.destroy();
|
|
1224
|
+
this.#default_atlas = null;
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
/**
|
|
1229
|
+
* A storage buffer holding `data`, replacing `previous` (destroyed) if it exists. The program
|
|
1230
|
+
* buffer is monolithic — a program is variable-length, so there is no table to page it into — and
|
|
1231
|
+
* is rebuilt whole on the rare frame the program heap changes.
|
|
1232
|
+
*
|
|
1233
|
+
* @param {GPUDevice} device
|
|
1234
|
+
* @param {GPUBuffer|undefined} previous
|
|
1235
|
+
* @param {string} label
|
|
1236
|
+
* @param {Uint32Array} data
|
|
1237
|
+
* @returns {GPUBuffer}
|
|
1238
|
+
*/
|
|
1239
|
+
function replace_data_buffer(device, previous, label, data) {
|
|
1240
|
+
previous?.destroy();
|
|
1241
|
+
|
|
1242
|
+
const buffer = device.createBuffer({ label, size: Math.max(WORD, data.byteLength), usage: STORAGE_UPLOAD });
|
|
1243
|
+
|
|
1244
|
+
device.queue.writeBuffer(buffer, 0, data);
|
|
1245
|
+
|
|
1246
|
+
return buffer;
|
|
1247
|
+
}
|