@woosh/meep-engine 3.14.3 → 3.14.5
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/core/geom/3d/mat4/m4_rigidity_defect.d.ts +30 -0
- package/src/core/geom/3d/mat4/m4_rigidity_defect.d.ts.map +1 -0
- package/src/core/geom/3d/mat4/m4_rigidity_defect.js +71 -0
- package/src/engine/Engine.d.ts.map +1 -1
- package/src/engine/Engine.js +733 -726
- package/src/engine/network/NetworkSession.d.ts +24 -4
- package/src/engine/network/NetworkSession.d.ts.map +1 -1
- package/src/engine/network/NetworkSession.js +28 -4
- package/src/engine/network/orchestrator/NetworkPeer.d.ts +96 -7
- package/src/engine/network/orchestrator/NetworkPeer.d.ts.map +1 -1
- package/src/engine/network/orchestrator/NetworkPeer.js +392 -68
- package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts +6 -1
- package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts.map +1 -1
- package/src/engine/network/orchestrator/ServerAuthoritativeClient.js +6 -0
- package/src/engine/network/orchestrator/ServerAuthoritativeServer.d.ts +105 -13
- package/src/engine/network/orchestrator/ServerAuthoritativeServer.d.ts.map +1 -1
- package/src/engine/network/orchestrator/ServerAuthoritativeServer.js +306 -59
- package/src/engine/network/replication/Replicator.d.ts +43 -7
- package/src/engine/network/replication/Replicator.d.ts.map +1 -1
- package/src/engine/network/replication/Replicator.js +309 -111
- package/src/engine/network/sim/ActionLog.d.ts +23 -3
- package/src/engine/network/sim/ActionLog.d.ts.map +1 -1
- package/src/engine/network/sim/ActionLog.js +26 -3
- package/src/engine/network/sim/ActionRecordCursor.d.ts +82 -0
- package/src/engine/network/sim/ActionRecordCursor.d.ts.map +1 -0
- package/src/engine/network/sim/ActionRecordCursor.js +120 -0
- package/src/engine/network/sim/RewindEngine.d.ts.map +1 -1
- package/src/engine/network/sim/RewindEngine.js +13 -29
- package/src/engine/network/sim/SimAction.d.ts +15 -0
- package/src/engine/network/sim/SimAction.d.ts.map +1 -1
- package/src/engine/network/sim/SimAction.js +15 -0
- package/src/engine/network/sim/SimActionExecutor.d.ts +51 -2
- package/src/engine/network/sim/SimActionExecutor.d.ts.map +1 -1
- package/src/engine/network/sim/SimActionExecutor.js +74 -7
- package/src/engine/network/transport/Channel.d.ts +1 -1
- package/src/engine/network/transport/Channel.js +1 -1
- package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
- package/src/engine/simulation/Ticker.d.ts.map +1 -1
- package/src/engine/simulation/Ticker.js +4 -1
- package/src/engine/ui/GUIEngine.d.ts.map +1 -1
- package/src/engine/ui/GUIEngine.js +6 -0
- package/src/shade/playground/particle_system/README.md +98 -0
- package/src/shade/playground/particle_system/index.html +124 -0
- package/src/shade/playground/particle_system/main.d.ts +2 -0
- package/src/shade/playground/particle_system/main.d.ts.map +1 -0
- package/src/shade/playground/particle_system/main.js +180 -0
- package/src/shade/playground/particle_system/particle_scene.d.ts +52 -0
- package/src/shade/playground/particle_system/particle_scene.d.ts.map +1 -0
- package/src/shade/playground/particle_system/particle_scene.js +553 -0
- package/src/shade/playground/skinned_mesh_soup/README.md +143 -0
- package/src/shade/playground/skinned_mesh_soup/index.html +202 -0
- package/src/shade/playground/skinned_mesh_soup/main.d.ts +7 -0
- package/src/shade/playground/skinned_mesh_soup/main.d.ts.map +1 -0
- package/src/shade/playground/skinned_mesh_soup/main.js +567 -0
- package/src/shade/playground/skinned_mesh_soup/verify_clone_vertices.d.ts +29 -0
- package/src/shade/playground/skinned_mesh_soup/verify_clone_vertices.d.ts.map +1 -0
- package/src/shade/playground/skinned_mesh_soup/verify_clone_vertices.js +428 -0
- package/src/shade/renderer/animation/GPUAnimationManager.d.ts.map +1 -1
- package/src/shade/renderer/animation/GPUAnimationManager.js +1958 -1796
- package/src/shade/renderer/animation/SKINNING_FRAME_PLAN.md +357 -0
- package/src/shade/renderer/animation/skin_bind_transform.d.ts +25 -0
- package/src/shade/renderer/animation/skin_bind_transform.d.ts.map +1 -0
- package/src/shade/renderer/animation/skin_bind_transform.js +33 -0
- package/src/shade/renderer/animation/skin_clip_scale_conflicts.d.ts +67 -0
- package/src/shade/renderer/animation/skin_clip_scale_conflicts.d.ts.map +1 -0
- package/src/shade/renderer/animation/skin_clip_scale_conflicts.js +199 -0
- package/src/shade/renderer/animation/skin_frame_defect.d.ts +34 -0
- package/src/shade/renderer/animation/skin_frame_defect.d.ts.map +1 -0
- package/src/shade/renderer/animation/skin_frame_defect.js +132 -0
- package/src/shade/renderer/animation/skin_joint_matrix.d.ts +24 -0
- package/src/shade/renderer/animation/skin_joint_matrix.d.ts.map +1 -0
- package/src/shade/renderer/animation/skin_joint_matrix.js +46 -0
- package/src/shade/renderer/animation/skin_normalize_mesh_frame.d.ts +70 -0
- package/src/shade/renderer/animation/skin_normalize_mesh_frame.d.ts.map +1 -0
- package/src/shade/renderer/animation/skin_normalize_mesh_frame.js +216 -0
- package/src/shade/renderer/animation/skin_test_fixtures.d.ts +86 -0
- package/src/shade/renderer/animation/skin_test_fixtures.d.ts.map +1 -0
- package/src/shade/renderer/animation/skin_test_fixtures.js +250 -0
- package/src/shade/renderer/animation/skinning/chunk_skin_blend_mesh_local.d.ts +18 -4
- package/src/shade/renderer/animation/skinning/chunk_skin_blend_mesh_local.d.ts.map +1 -1
- package/src/shade/renderer/animation/skinning/chunk_skin_blend_mesh_local.js +18 -4
- package/src/shade/renderer/particles/DESIGN.md +5 -5
- package/src/shade/renderer/particles/cull/chunk_particle_cull.d.ts.map +1 -1
- package/src/shade/renderer/particles/cull/chunk_particle_cull.js +3 -1
- package/src/shade/renderer/particles/data/chunk_particle_emitter.js +1 -1
- package/src/shade/renderer/particles/graph/ParticleNodeDescription.d.ts +43 -0
- package/src/shade/renderer/particles/graph/ParticleNodeDescription.d.ts.map +1 -0
- package/src/shade/renderer/particles/graph/ParticleNodeDescription.js +44 -0
- package/src/shade/renderer/particles/graph/ParticleNodeRegistry.d.ts +33 -59
- package/src/shade/renderer/particles/graph/ParticleNodeRegistry.d.ts.map +1 -1
- package/src/shade/renderer/particles/graph/ParticleNodeRegistry.js +185 -118
- package/src/shade/renderer/particles/graph/compile_particle_graph.d.ts +15 -10
- package/src/shade/renderer/particles/graph/compile_particle_graph.d.ts.map +1 -1
- package/src/shade/renderer/particles/graph/compile_particle_graph.js +205 -170
- package/src/shade/renderer/particles/graph/particle_graph_authoring.d.ts +44 -0
- package/src/shade/renderer/particles/graph/particle_graph_authoring.d.ts.map +1 -0
- package/src/shade/renderer/particles/graph/particle_graph_authoring.js +108 -0
- package/src/shade/renderer/particles/isa/InstructionStream.d.ts +89 -0
- package/src/shade/renderer/particles/isa/InstructionStream.d.ts.map +1 -0
- package/src/shade/renderer/particles/isa/InstructionStream.js +162 -0
- package/src/shade/renderer/particles/isa/ParticleAssembler.d.ts +1 -48
- package/src/shade/renderer/particles/isa/ParticleAssembler.d.ts.map +1 -1
- package/src/shade/renderer/particles/isa/ParticleAssembler.js +25 -114
- package/src/shade/renderer/particles/particle_prototype.d.ts.map +1 -1
- package/src/shade/renderer/particles/particle_prototype.js +181 -180
- package/src/shade/renderer/particles/runtime/create_particle_effect.d.ts +4 -4
- package/src/shade/renderer/particles/runtime/create_particle_effect.d.ts.map +1 -1
- package/src/shade/renderer/particles/runtime/create_particle_effect.js +19 -19
- package/src/shade/renderer/particles/shaders/chunk_particle_curve_animation.d.ts.map +1 -1
- package/src/shade/renderer/particles/shaders/chunk_particle_curve_animation.js +4 -2
- package/src/shade/renderer/particles/shaders/chunk_particle_lighting.d.ts +4 -4
- package/src/shade/renderer/particles/shaders/chunk_particle_lighting.d.ts.map +1 -1
- package/src/shade/renderer/particles/shaders/chunk_particle_lighting.js +7 -5
- 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 +3 -1
- package/src/shade/renderer/particles/shaders/shader_particle_emit.d.ts.map +1 -1
- package/src/shade/renderer/particles/shaders/shader_particle_emit.js +3 -1
- package/src/shade/renderer/particles/shaders/shader_particle_finalize.d.ts.map +1 -1
- package/src/shade/renderer/particles/shaders/shader_particle_finalize.js +5 -2
- package/src/shade/renderer/particles/shaders/shader_particle_render.d.ts.map +1 -1
- package/src/shade/renderer/particles/shaders/shader_particle_render.js +9 -2
- package/src/shade/renderer/particles/shaders/shader_particle_simulate.d.ts.map +1 -1
- package/src/shade/renderer/particles/shaders/shader_particle_simulate.js +3 -1
- package/src/shade/renderer/particles/vm/chunk_particle_vm.js +1 -1
- package/src/shade/renderer/scene/SkinnedMesh.d.ts +11 -0
- package/src/shade/renderer/scene/SkinnedMesh.d.ts.map +1 -1
- package/src/shade/renderer/scene/SkinnedMesh.js +11 -0
- package/src/shade/renderer/shader/chunk/geometry/quaternion/dual/chunk_dual_quat_from_m4.d.ts +33 -3
- package/src/shade/renderer/shader/chunk/geometry/quaternion/dual/chunk_dual_quat_from_m4.d.ts.map +1 -1
- package/src/shade/renderer/shader/chunk/geometry/quaternion/dual/chunk_dual_quat_from_m4.js +58 -37
- package/src/shade/renderer/particles/graph/ParticleGraph.d.ts +0 -54
- package/src/shade/renderer/particles/graph/ParticleGraph.d.ts.map +0 -1
- package/src/shade/renderer/particles/graph/ParticleGraph.js +0 -99
- package/src/shade/renderer/particles/prototypeParticleSystem.d.ts +0 -2
- package/src/shade/renderer/particles/prototypeParticleSystem.d.ts.map +0 -1
- package/src/shade/renderer/particles/prototypeParticleSystem.js +0 -448
|
@@ -278,7 +278,10 @@ class Ticker {
|
|
|
278
278
|
|
|
279
279
|
this.#addLifecycleListeners();
|
|
280
280
|
|
|
281
|
-
|
|
281
|
+
//recorded, so a stop() taken before the first frame lands can cancel it. The generation guard
|
|
282
|
+
//already makes the callback a no-op, but an uncancelled frame request keeps the closures (and
|
|
283
|
+
//everything the tick listeners hold) alive until it fires
|
|
284
|
+
this.#animationFrameHandle = requestAnimationFrame(animationFrameCallback);
|
|
282
285
|
|
|
283
286
|
//if we are starting while the page is already in the background, suspend right away
|
|
284
287
|
const doc = globalThis.document;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GUIEngine.d.ts","sourceRoot":"","sources":["../../../../src/engine/ui/GUIEngine.js"],"names":[],"mappings":";AA4BA;IACI,mBAAqB;IAErB;;;OAGG;IACH,eAFU,gBAAc,IAAI,CAEP;IAErB;;;OAGG;IACH,eAAc;IAEd;;;OAGG;IACH,eAFU,eAAe,eAAe,CAAC,CAEX;IAE9B;;;OAGG;IACH,UAFU,cAAc,CAEQ;IAEhC;;;OAGG;IACH,cAFU,iBAAiB,CAEyB;IAGpD;;;OAGG;IACH,QAFU,MAAM,CAEM;IAEtB,gBAOG;IAEH;;;OAGG;IACH,KAFU,SAAS,CAEG;IAEtB;;;OAGG;IACH,QAFU,cAAc,CAEuB;IAG/C;;;OAGG;IACH,cAFU,eAAa,IAAI,CAEP;IAqBpB;;;;;;OAMG;IACH,mDANW,OAAO,GAIL,MAAM,CAoClB;IAED;;;;;;OAMG;IACH,0CALW,IAAI,GAGF,eAAe,CA+D3B;IAED;;;;;;OAMG;IACH,iEALW,MAAM,GAGJ,QAAQ,GAAG,CAAC,CA0DxB;IAED;;;;OAIG;IACH,mCAJW,MAAM,gBAWhB;IAED;;;;;OAKG;IACH,iCAJW,MAAM,gBAUhB;IAED;;;;;;;;OAQG;IACH,gEAPW,IAAI,gBA0Dd;IAED;;;;OAIG;IACH,kCAFY,eAAe,CAgB1B;IAED;;OAEG;IACH,yBAFa,eAAe,GAAC,IAAI,CAkBhC;IAED;;;OAGG;IACH,8BAKC;IAED;;;OAGG;IACH,wCAiEC;IAED,
|
|
1
|
+
{"version":3,"file":"GUIEngine.d.ts","sourceRoot":"","sources":["../../../../src/engine/ui/GUIEngine.js"],"names":[],"mappings":";AA4BA;IACI,mBAAqB;IAErB;;;OAGG;IACH,eAFU,gBAAc,IAAI,CAEP;IAErB;;;OAGG;IACH,eAAc;IAEd;;;OAGG;IACH,eAFU,eAAe,eAAe,CAAC,CAEX;IAE9B;;;OAGG;IACH,UAFU,cAAc,CAEQ;IAEhC;;;OAGG;IACH,cAFU,iBAAiB,CAEyB;IAGpD;;;OAGG;IACH,QAFU,MAAM,CAEM;IAEtB,gBAOG;IAEH;;;OAGG;IACH,KAFU,SAAS,CAEG;IAEtB;;;OAGG;IACH,QAFU,cAAc,CAEuB;IAG/C;;;OAGG;IACH,cAFU,eAAa,IAAI,CAEP;IAqBpB;;;;;;OAMG;IACH,mDANW,OAAO,GAIL,MAAM,CAoClB;IAED;;;;;;OAMG;IACH,0CALW,IAAI,GAGF,eAAe,CA+D3B;IAED;;;;;;OAMG;IACH,iEALW,MAAM,GAGJ,QAAQ,GAAG,CAAC,CA0DxB;IAED;;;;OAIG;IACH,mCAJW,MAAM,gBAWhB;IAED;;;;;OAKG;IACH,iCAJW,MAAM,gBAUhB;IAED;;;;;;;;OAQG;IACH,gEAPW,IAAI,gBA0Dd;IAED;;;;OAIG;IACH,kCAFY,eAAe,CAgB1B;IAED;;OAEG;IACH,yBAFa,eAAe,GAAC,IAAI,CAkBhC;IAED;;;OAGG;IACH,8BAKC;IAED;;;OAGG;IACH,wCAiEC;IAED,2BAkBC;CACJ;iBA9gBgB,oCAAoC;gCAyBrB,4BAA4B;+BAf7B,sCAAsC;kCAFnC,yCAAyC;mBAexD,yBAAyB;sBAjBtB,kCAAkC;0BAG9B,qCAAqC;2BAPpC,oCAAoC;mBAiB5C,kBAAkB;iBAPpB,oBAAoB;gCATqB,uCAAuC"}
|
|
@@ -506,6 +506,12 @@ class GUIEngine {
|
|
|
506
506
|
}
|
|
507
507
|
|
|
508
508
|
shutdown() {
|
|
509
|
+
// startup() armed this loop, so shutdown() is what has to disarm it. Left running it keeps
|
|
510
|
+
// animating views against a GUI that has been torn down, and its frame/timer chain outlives
|
|
511
|
+
// the host it belonged to - under a test environment that means calling
|
|
512
|
+
// requestAnimationFrame after the environment that provided it is gone.
|
|
513
|
+
this.ticker.stop();
|
|
514
|
+
|
|
509
515
|
this.windows.reset();
|
|
510
516
|
this.entityManager = null;
|
|
511
517
|
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# GPU particle system — playground
|
|
2
|
+
|
|
3
|
+
The whole GPU particle path running in a real scene: emitters, the VM, the alive/dead lists and the
|
|
4
|
+
billboard renderer, driven end to end by the GPU with no renderer, no assets and no `Renderer`
|
|
5
|
+
dependency. It moved here from `renderer/particles/prototypeParticleSystem.js`, which had no page to
|
|
6
|
+
open it with.
|
|
7
|
+
|
|
8
|
+
## Running it
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm run dev --workspace @woosh/meep-engine
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
then open <http://localhost:5173/src/shade/playground/particle_system/index.html>.
|
|
15
|
+
|
|
16
|
+
It has to be `http://localhost` rather than `file://`: WebGPU needs a secure context, and module
|
|
17
|
+
imports from `file://` are blocked by CORS anyway. The page imports nothing but meep's own relative
|
|
18
|
+
ESM, so any static server rooted at the repo also serves it.
|
|
19
|
+
|
|
20
|
+
**It needs immediate data**, and refuses to start without it. Emit, simulate and build-indirect each
|
|
21
|
+
carry their per-dispatch state (`u_dt`, `u_time`, `u_frame`, and for emit the request count and the
|
|
22
|
+
pool capacity) as an immediate block rather than as a transient uniform buffer, so an adapter whose
|
|
23
|
+
WGSL lacks the `immediate` address space cannot build any of the three. The refusal names the byte
|
|
24
|
+
counts. Chrome/Edge 113+ with immediates; Claude Code's in-app browser pane is **not** one — it
|
|
25
|
+
reports no adapter at all today, and no `immediate_address_space` in `wgslLanguageFeatures`.
|
|
26
|
+
|
|
27
|
+
## What is on screen
|
|
28
|
+
|
|
29
|
+
Four emitters sharing one 32k pool, chosen so each renderer path is visible at once:
|
|
30
|
+
|
|
31
|
+
| emitter | blend | projection | what it is there to show |
|
|
32
|
+
|---|---|---|---|
|
|
33
|
+
| fire | additive | billboard | `fire_effect()` straight out of `particle_prototype.js`, at the foot of the pillar |
|
|
34
|
+
| smoke | alpha | billboard, soft-depth | a rising column that fades where it meets the pillar rather than cutting into it |
|
|
35
|
+
| sparks | additive | **stretched** | short-lived, gravity-fed, stretched along velocity — the one non-billboard projection |
|
|
36
|
+
| dust | alpha | billboard, soft-depth | a wide slow drift, so the ground plane and the low wall both get a soft intersection |
|
|
37
|
+
|
|
38
|
+
Ground plane and three boxes are drawn first into a reverse-Z `depth32float` buffer. The billboard
|
|
39
|
+
pass loads that colour target and depth-tests against the same buffer read-only, so particles are
|
|
40
|
+
**occluded** by the boxes and the soft-depth emitters sample the depth they were occluded by.
|
|
41
|
+
|
|
42
|
+
Things worth looking at:
|
|
43
|
+
|
|
44
|
+
- particles disappearing behind the pillar as the camera orbits — that is the depth test,
|
|
45
|
+
- smoke and dust softening where they touch a surface instead of showing a hard intersection line —
|
|
46
|
+
that is `PARTICLE_FLAG_SOFT_DEPTH` and the reverse-Z linearisation,
|
|
47
|
+
- sparks leaning into their own velocity — `EMITTER_PROJECTION.STRETCHED`,
|
|
48
|
+
- both blend modes in **one** draw with one pipeline: `particle_premultiply` picks per particle, so
|
|
49
|
+
there is no additive pass and no alpha pass.
|
|
50
|
+
|
|
51
|
+
The overlay shows fps and, every 30 frames, the alive count, the free-list length and the two
|
|
52
|
+
indirect arg buffers. Those are read back **to look at**. Nothing on the CPU sizes a pass.
|
|
53
|
+
|
|
54
|
+
## The property the page exists to demonstrate
|
|
55
|
+
|
|
56
|
+
The frame is GPU-driven end to end. `shader_particle_build_indirect` writes both the simulate
|
|
57
|
+
dispatch args and the billboard draw args on the GPU; the next frame's simulate runs through
|
|
58
|
+
`dispatchWorkgroupsIndirect` and this frame's draw through `drawIndirect`. The host's only sizing
|
|
59
|
+
decision is how many *new* particles to spawn, which is a per-emitter rate times `dt` and has
|
|
60
|
+
nothing to do with how many are already alive.
|
|
61
|
+
|
|
62
|
+
Sizing either from a CPU readback instead would be a correctness bug rather than a latency one: a
|
|
63
|
+
stale count that is too small leaves the tail of the alive list unsimulated, so those slots are
|
|
64
|
+
never freed, and one that is too large re-simulates entries past the end, pushing a dead slot onto
|
|
65
|
+
the free list twice — it is then popped twice and two live particles share one record.
|
|
66
|
+
|
|
67
|
+
## Verifying it
|
|
68
|
+
|
|
69
|
+
`particle_scene.js` holds the scene with every reference to the page removed, so
|
|
70
|
+
`particle_scene.spec.js` can encode the same frames against `SoftwareGPUDevice`:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
npx vitest run src/shade/playground/particle_system/particle_scene.spec.js
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
That mock runs no shaders, so the spec asserts nothing about what a pass computed. What it does
|
|
77
|
+
assert is that the frame is legal and wired the way the design requires — pass order and
|
|
78
|
+
attachments, the alive lists ping-ponging so no frame reads the list it appends to, the immediate
|
|
79
|
+
blocks written before every dispatch that declares them, and simulate and the draw both taking their
|
|
80
|
+
counts from the indirect buffers. Any WebGPU validation failure anywhere in the frame arrives as an
|
|
81
|
+
uncaptured error, which every test fails on.
|
|
82
|
+
|
|
83
|
+
The picture is the browser's job; the spec cannot see it.
|
|
84
|
+
|
|
85
|
+
### Stepping frames by hand
|
|
86
|
+
|
|
87
|
+
Add `?debug` to the URL. `requestAnimationFrame` is then not used at all, and `window.__step(n)`
|
|
88
|
+
runs exactly `n` frames and returns the frame counter — which is what makes the loop drivable in a
|
|
89
|
+
hidden or headless tab, where rAF is suspended. `window.__stats` carries the last counters snapshot.
|
|
90
|
+
|
|
91
|
+
## What this page does not cover
|
|
92
|
+
|
|
93
|
+
Depth sorting. `EMITTER_FLAG.SORT` and the bucketed depth sort in `particles/sort/` (histogram →
|
|
94
|
+
csdldf scan → scatter) are not wired in here: the billboard pass draws the alive list directly, so
|
|
95
|
+
alpha-blended emitters are in emission order rather than back-to-front. The two soft-depth emitters are faint and diffuse enough that it
|
|
96
|
+
does not read as wrong, which is exactly why it should not be taken as evidence that sorting works.
|
|
97
|
+
Lighting is the unlit hook, too — `chunk_particle_lighting_unlit`, not the shading-chunk-backed one
|
|
98
|
+
the renderer feature swaps in.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>meep — GPU particle system</title>
|
|
6
|
+
<style>
|
|
7
|
+
:root {
|
|
8
|
+
color-scheme: dark;
|
|
9
|
+
--bg: #05070a;
|
|
10
|
+
--panel: #1c1f25e0;
|
|
11
|
+
--line: #2b3038;
|
|
12
|
+
--text: #d6dae0;
|
|
13
|
+
--muted: #8b939f;
|
|
14
|
+
--fail: #ff6b6b;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
* { box-sizing: border-box; }
|
|
18
|
+
|
|
19
|
+
html, body { height: 100%; }
|
|
20
|
+
|
|
21
|
+
body {
|
|
22
|
+
margin: 0;
|
|
23
|
+
overflow: hidden;
|
|
24
|
+
background: var(--bg);
|
|
25
|
+
color: var(--text);
|
|
26
|
+
font: 14px/1.5 system-ui, -apple-system, Segoe UI, sans-serif;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
#view {
|
|
30
|
+
display: block;
|
|
31
|
+
width: 100%;
|
|
32
|
+
height: 100%;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
#hud {
|
|
36
|
+
position: fixed;
|
|
37
|
+
top: 12px;
|
|
38
|
+
left: 12px;
|
|
39
|
+
min-width: 210px;
|
|
40
|
+
padding: 10px 12px;
|
|
41
|
+
border: 1px solid var(--line);
|
|
42
|
+
border-radius: 8px;
|
|
43
|
+
background: var(--panel);
|
|
44
|
+
backdrop-filter: blur(6px);
|
|
45
|
+
font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
|
|
46
|
+
font-size: 12px;
|
|
47
|
+
pointer-events: none;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#hud h1 {
|
|
51
|
+
font: inherit;
|
|
52
|
+
font-weight: 600;
|
|
53
|
+
margin: 0 0 6px;
|
|
54
|
+
letter-spacing: 0.04em;
|
|
55
|
+
text-transform: uppercase;
|
|
56
|
+
color: var(--muted);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
#hud div { display: flex; gap: 10px; justify-content: space-between; }
|
|
60
|
+
#hud .key { color: var(--muted); }
|
|
61
|
+
|
|
62
|
+
#legend {
|
|
63
|
+
position: fixed;
|
|
64
|
+
bottom: 12px;
|
|
65
|
+
left: 12px;
|
|
66
|
+
max-width: 62ch;
|
|
67
|
+
padding: 10px 12px;
|
|
68
|
+
border: 1px solid var(--line);
|
|
69
|
+
border-radius: 8px;
|
|
70
|
+
background: var(--panel);
|
|
71
|
+
backdrop-filter: blur(6px);
|
|
72
|
+
font-size: 12.5px;
|
|
73
|
+
color: var(--muted);
|
|
74
|
+
pointer-events: none;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
#legend b { color: var(--text); font-weight: 600; }
|
|
78
|
+
#legend p { margin: 0 0 4px; }
|
|
79
|
+
#legend p:last-child { margin-bottom: 0; }
|
|
80
|
+
|
|
81
|
+
#message {
|
|
82
|
+
position: fixed;
|
|
83
|
+
inset: 0;
|
|
84
|
+
display: none;
|
|
85
|
+
align-items: center;
|
|
86
|
+
justify-content: center;
|
|
87
|
+
padding: 32px;
|
|
88
|
+
text-align: center;
|
|
89
|
+
background: #05070ae6;
|
|
90
|
+
color: var(--fail);
|
|
91
|
+
font-size: 16px;
|
|
92
|
+
white-space: pre-wrap;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
#message:not(:empty) { display: flex; }
|
|
96
|
+
</style>
|
|
97
|
+
</head>
|
|
98
|
+
<body>
|
|
99
|
+
|
|
100
|
+
<canvas id="view"></canvas>
|
|
101
|
+
|
|
102
|
+
<div id="hud">
|
|
103
|
+
<h1>GPU particles</h1>
|
|
104
|
+
<div><span class="key">fps</span><span id="stat-fps">—</span></div>
|
|
105
|
+
<div><span class="key">alive</span><span id="stat-alive">—</span></div>
|
|
106
|
+
<div><span class="key">free</span><span id="stat-dead">—</span></div>
|
|
107
|
+
<div><span class="key">draw instances</span><span id="stat-draw">—</span></div>
|
|
108
|
+
<div><span class="key">sim workgroups</span><span id="stat-dispatch">—</span></div>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<div id="legend">
|
|
112
|
+
<p><b>Four emitters, one pool.</b> Additive fire at the pillar, an alpha soft-depth smoke column
|
|
113
|
+
above it, velocity-stretched sparks by the block, and a wide soft-depth dust drift.</p>
|
|
114
|
+
<p>Emit, simulate and the billboard draw are all GPU-sized — the simulate dispatch and the draw
|
|
115
|
+
read indirect args the build-indirect pass writes. The counters above are read back purely to
|
|
116
|
+
display; nothing on the CPU sizes a pass.</p>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<div id="message"></div>
|
|
120
|
+
|
|
121
|
+
<script type="module" src="./main.js"></script>
|
|
122
|
+
|
|
123
|
+
</body>
|
|
124
|
+
</html>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../../../../src/shade/playground/particle_system/main.js"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { ShadeGPUCommandContext } from "../../device/ShadeGPUCommandContext.js";
|
|
2
|
+
import {
|
|
3
|
+
device_supports_immediate_size,
|
|
4
|
+
immediate_data_supported,
|
|
5
|
+
} from "../../device/immediate_data_supported.js";
|
|
6
|
+
import { GraphicsContext } from "../../renderer/GraphicsContext.js";
|
|
7
|
+
import {
|
|
8
|
+
PARTICLE_IMMEDIATE_BYTES,
|
|
9
|
+
PARTICLE_STATS_BYTES,
|
|
10
|
+
create_particle_scene,
|
|
11
|
+
} from "./particle_scene.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The page for the GPU particle playground: acquire a device, build the scene
|
|
15
|
+
* ({@link ./particle_scene.js}, which is where the interesting part lives), and drive it from
|
|
16
|
+
* `requestAnimationFrame`.
|
|
17
|
+
*
|
|
18
|
+
* Serve the workspace with `npm run dev --workspace @woosh/meep-engine` and open
|
|
19
|
+
* `/src/shade/playground/particle_system/index.html`. See README.md for what to look at, and for
|
|
20
|
+
* the `?debug` query flag that swaps rAF for manual stepping.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** How often the counters are copied back for the overlay. Display only — nothing sizes a pass. */
|
|
24
|
+
const STATS_INTERVAL_FRAMES = 30;
|
|
25
|
+
|
|
26
|
+
/** Device pixels per CSS pixel, capped: 32k billboards at 3x cost more than they show. */
|
|
27
|
+
const MAX_PIXEL_RATIO = 2;
|
|
28
|
+
|
|
29
|
+
async function main() {
|
|
30
|
+
const canvas = document.getElementById("view");
|
|
31
|
+
|
|
32
|
+
if (navigator.gpu === undefined) {
|
|
33
|
+
return show_message("WebGPU is not available in this browser.");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const adapter = await navigator.gpu.requestAdapter();
|
|
37
|
+
|
|
38
|
+
if (adapter === null) {
|
|
39
|
+
return show_message("No WebGPU adapter — this browser exposes the API but has no usable GPU.");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const device = await adapter.requestDevice();
|
|
43
|
+
device.addEventListener?.("uncapturederror", (e) => console.error("WebGPU error:", e.error.message));
|
|
44
|
+
|
|
45
|
+
// Every compute pass here carries its per-dispatch state as immediate data, an optional
|
|
46
|
+
// capability. Refusing by name beats the pipeline-creation error a device without it raises,
|
|
47
|
+
// which names neither the shader nor the feature.
|
|
48
|
+
if (!immediate_data_supported() || !device_supports_immediate_size(device, PARTICLE_IMMEDIATE_BYTES)) {
|
|
49
|
+
return show_message(
|
|
50
|
+
"This browser's WebGPU offers no usable immediate data "
|
|
51
|
+
+ `(needs ${PARTICLE_IMMEDIATE_BYTES} bytes, offers ${device.limits.maxImmediateSize ?? 0}).\n`
|
|
52
|
+
+ "The particle emit / simulate / build-indirect passes all carry their per-dispatch\n"
|
|
53
|
+
+ "state that way, so none of them can be built here."
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const graphics = new GraphicsContext(device);
|
|
58
|
+
const context = canvas.getContext("webgpu");
|
|
59
|
+
const format = navigator.gpu.getPreferredCanvasFormat();
|
|
60
|
+
context.configure({ device, format, alphaMode: "opaque" });
|
|
61
|
+
|
|
62
|
+
const scene = create_particle_scene(graphics, { color_format: format });
|
|
63
|
+
|
|
64
|
+
// Manual frame stepping instead of rAF — opt in with ?debug. Useful for driving the loop a
|
|
65
|
+
// known number of frames in a headless / hidden tab, where rAF is suspended.
|
|
66
|
+
const debug = new URLSearchParams(location.search).has("debug");
|
|
67
|
+
|
|
68
|
+
const stats_buffer = device.createBuffer({
|
|
69
|
+
label: "particles/stats",
|
|
70
|
+
size: PARTICLE_STATS_BYTES,
|
|
71
|
+
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
|
|
72
|
+
});
|
|
73
|
+
let stats_pending = false;
|
|
74
|
+
|
|
75
|
+
let last = performance.now();
|
|
76
|
+
let frame_index = 0;
|
|
77
|
+
let fps = 0;
|
|
78
|
+
|
|
79
|
+
function frame(now) {
|
|
80
|
+
const dt = Math.min((now - last) / 1000, 0.05);
|
|
81
|
+
last = now;
|
|
82
|
+
// A rAF-driven loop steps by wall clock; ?debug steps by hand, where dt can be 0.
|
|
83
|
+
fps = dt > 0 ? fps * 0.9 + 0.1 / dt : fps;
|
|
84
|
+
|
|
85
|
+
// Back the canvas with device pixels, so a HiDPI display gets a sharp image rather than an
|
|
86
|
+
// upscaled CSS-pixel one.
|
|
87
|
+
const ratio = Math.min(window.devicePixelRatio || 1, MAX_PIXEL_RATIO);
|
|
88
|
+
const width = Math.max(1, Math.round(canvas.clientWidth * ratio));
|
|
89
|
+
const height = Math.max(1, Math.round(canvas.clientHeight * ratio));
|
|
90
|
+
|
|
91
|
+
if (canvas.width !== width || canvas.height !== height) {
|
|
92
|
+
canvas.width = width;
|
|
93
|
+
canvas.height = height;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const cmd = ShadeGPUCommandContext.create(graphics, "particles/frame");
|
|
97
|
+
|
|
98
|
+
scene.encode_frame(cmd, {
|
|
99
|
+
color_view: context.getCurrentTexture().createView(),
|
|
100
|
+
width,
|
|
101
|
+
height,
|
|
102
|
+
dt,
|
|
103
|
+
time: now / 1000,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const capture_stats = !stats_pending && frame_index % STATS_INTERVAL_FRAMES === 0;
|
|
107
|
+
if (capture_stats) {
|
|
108
|
+
scene.encode_stats_copy(cmd, stats_buffer);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
cmd.finish();
|
|
112
|
+
|
|
113
|
+
frame_index++;
|
|
114
|
+
|
|
115
|
+
if (capture_stats) {
|
|
116
|
+
stats_pending = true;
|
|
117
|
+
stats_buffer.mapAsync(GPUMapMode.READ).then(() => {
|
|
118
|
+
const bytes = stats_buffer.getMappedRange().slice(0);
|
|
119
|
+
stats_buffer.unmap();
|
|
120
|
+
stats_pending = false;
|
|
121
|
+
|
|
122
|
+
window.__stats = { frame: frame_index, fps, ...scene.read_stats(bytes) };
|
|
123
|
+
write_hud(window.__stats);
|
|
124
|
+
}).catch(() => {
|
|
125
|
+
stats_pending = false;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!debug) {
|
|
130
|
+
requestAnimationFrame(frame);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (debug) {
|
|
135
|
+
// Drive frames by hand when the tab is hidden (rAF suspended) — headless verification.
|
|
136
|
+
// Nothing is scheduled: a stray rAF frame interleaved with __step would make the count the
|
|
137
|
+
// caller asked for and the count that ran two different numbers.
|
|
138
|
+
window.__step = (n = 1) => {
|
|
139
|
+
for (let i = 0; i < n; i++) {
|
|
140
|
+
frame(performance.now());
|
|
141
|
+
}
|
|
142
|
+
return frame_index;
|
|
143
|
+
};
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
requestAnimationFrame(frame);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Push one stats snapshot into the overlay. Absent elements are simply skipped. */
|
|
151
|
+
function write_hud({ fps, alive, dead, dispatch, draw }) {
|
|
152
|
+
const put = (id, value) => {
|
|
153
|
+
const node = document.getElementById(id);
|
|
154
|
+
if (node !== null) {
|
|
155
|
+
node.textContent = value;
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
put("stat-fps", fps.toFixed(0));
|
|
160
|
+
put("stat-alive", alive.toLocaleString());
|
|
161
|
+
put("stat-dead", dead.toLocaleString());
|
|
162
|
+
put("stat-draw", draw[1].toLocaleString());
|
|
163
|
+
put("stat-dispatch", dispatch[0].toLocaleString());
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function show_message(text) {
|
|
167
|
+
const existing = document.getElementById("message");
|
|
168
|
+
const node = existing ?? document.body.appendChild(document.createElement("div"));
|
|
169
|
+
|
|
170
|
+
if (existing === null) {
|
|
171
|
+
node.style.cssText = "position:fixed;inset:0;display:flex;align-items:center;justify-content:center;color:#fff;font-family:sans-serif;font-size:16px;white-space:pre-wrap;text-align:center";
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
node.textContent = text;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
main().catch((e) => {
|
|
178
|
+
console.error(e);
|
|
179
|
+
show_message("The particle playground failed to start — see the console.\n\n" + e.message);
|
|
180
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {object} ParticleSceneStats
|
|
3
|
+
* @property {number} alive particles alive at the end of the captured frame
|
|
4
|
+
* @property {number} dead free slots left in the pool
|
|
5
|
+
* @property {number} alive_in the snapshot next frame's simulate reads as its input length
|
|
6
|
+
* @property {number[]} dispatch simulate's indirect dispatch args, `[x, y, z]` workgroups
|
|
7
|
+
* @property {number[]} draw the billboard draw's indirect args, `[vertices, instances]`
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Build the scene against a live (or mock) device.
|
|
11
|
+
*
|
|
12
|
+
* @param {GraphicsContext} graphics
|
|
13
|
+
* @param {object} options
|
|
14
|
+
* @param {GPUTextureFormat} options.color_format format of the colour target frames are encoded into
|
|
15
|
+
* @param {number} [options.capacity] particle pool size
|
|
16
|
+
* @returns {object} the scene, see the returned object's own members
|
|
17
|
+
*/
|
|
18
|
+
export function create_particle_scene(graphics: GraphicsContext, { color_format, capacity }: {
|
|
19
|
+
color_format: GPUTextureFormat;
|
|
20
|
+
capacity?: number;
|
|
21
|
+
}): object;
|
|
22
|
+
/**
|
|
23
|
+
* The largest immediate block any of the three compute passes declares (emit's 20-byte settings
|
|
24
|
+
* struct). Immediate data is an optional WebGPU capability, so callers check for it up front rather
|
|
25
|
+
* than letting pipeline creation fail with an error that names neither pass nor feature.
|
|
26
|
+
*/
|
|
27
|
+
export const PARTICLE_IMMEDIATE_BYTES: 20;
|
|
28
|
+
/** Bytes of stats {@link ParticleScene#encode_stats_copy} writes: 8 counters, then both arg buffers. */
|
|
29
|
+
export const PARTICLE_STATS_BYTES: 64;
|
|
30
|
+
export type ParticleSceneStats = {
|
|
31
|
+
/**
|
|
32
|
+
* particles alive at the end of the captured frame
|
|
33
|
+
*/
|
|
34
|
+
alive: number;
|
|
35
|
+
/**
|
|
36
|
+
* free slots left in the pool
|
|
37
|
+
*/
|
|
38
|
+
dead: number;
|
|
39
|
+
/**
|
|
40
|
+
* the snapshot next frame's simulate reads as its input length
|
|
41
|
+
*/
|
|
42
|
+
alive_in: number;
|
|
43
|
+
/**
|
|
44
|
+
* simulate's indirect dispatch args, `[x, y, z]` workgroups
|
|
45
|
+
*/
|
|
46
|
+
dispatch: number[];
|
|
47
|
+
/**
|
|
48
|
+
* the billboard draw's indirect args, `[vertices, instances]`
|
|
49
|
+
*/
|
|
50
|
+
draw: number[];
|
|
51
|
+
};
|
|
52
|
+
//# sourceMappingURL=particle_scene.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"particle_scene.d.ts","sourceRoot":"","sources":["../../../../../src/shade/playground/particle_system/particle_scene.js"],"names":[],"mappings":"AAyEA;;;;;;;GAOG;AAEH;;;;;;;;GAQG;AACH;IAJqC,YAAY;IACrB,QAAQ,GAAzB,MAAM;IACJ,MAAM,CA8OlB;AA3QD;;;;GAIG;AACH,0CAA2C;AAE3C,wGAAwG;AACxG,sCAAuC;;;;;WAOzB,MAAM;;;;UACN,MAAM;;;;cACN,MAAM;;;;cACN,MAAM,EAAE;;;;UACR,MAAM,EAAE"}
|