@driftengine/core 4.9.0 → 4.11.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/CHANGELOG.json +133 -11
- package/README.md +43 -38
- package/dist/index.d.ts +7 -4
- package/dist/index.js +5 -4
- package/dist/render/ambientHarmonics.d.ts +1 -1
- package/dist/render/ambientHarmonics.js +1 -1
- package/dist/render/areaLights.d.ts +4 -4
- package/dist/render/areaLights.js +1 -1
- package/dist/render/backend/api.d.ts +10 -3
- package/dist/render/backend/select.js +11 -5
- package/dist/render/backend/webgl2/boneAnimations.d.ts +29 -0
- package/dist/render/backend/webgl2/boneAnimations.js +80 -0
- package/dist/render/backend/webgl2/instanced.d.ts +5 -1
- package/dist/render/backend/webgl2/instanced.js +13 -1
- package/dist/render/backend/webgl2/renderer.d.ts +189 -4
- package/dist/render/backend/webgl2/renderer.js +614 -66
- package/dist/render/backend/webgl2/skinPaletteTexture.d.ts +5 -3
- package/dist/render/backend/webgl2/skinPaletteTexture.js +6 -4
- package/dist/render/backend/webgpu/boneAnimations.d.ts +34 -0
- package/dist/render/backend/webgpu/boneAnimations.js +76 -0
- package/dist/render/backend/webgpu/buffers.d.ts +24 -4
- package/dist/render/backend/webgpu/buffers.js +116 -59
- package/dist/render/backend/webgpu/clothKernels.d.ts +8 -0
- package/dist/render/backend/webgpu/clothKernels.js +76 -0
- package/dist/render/backend/webgpu/clothOps.d.ts +39 -0
- package/dist/render/backend/webgpu/clothOps.js +78 -0
- package/dist/render/backend/webgpu/clothSetLayout.d.ts +25 -0
- package/dist/render/backend/webgpu/clothSetLayout.js +84 -0
- package/dist/render/backend/webgpu/clothSetPack.d.ts +33 -0
- package/dist/render/backend/webgpu/clothSetPack.js +144 -0
- package/dist/render/backend/webgpu/compressedUpload.d.ts +0 -1
- package/dist/render/backend/webgpu/compressedUpload.js +53 -8
- package/dist/render/backend/webgpu/depthPass.d.ts +59 -3
- package/dist/render/backend/webgpu/depthPass.js +49 -14
- package/dist/render/backend/webgpu/drawCommand.d.ts +20 -4
- package/dist/render/backend/webgpu/drawCommand.js +11 -3
- package/dist/render/backend/webgpu/flatPass.d.ts +85 -42
- package/dist/render/backend/webgpu/flatPass.js +137 -20
- package/dist/render/backend/webgpu/gpuClothSet.d.ts +80 -0
- package/dist/render/backend/webgpu/gpuClothSet.js +462 -0
- package/dist/render/backend/webgpu/instanced.d.ts +14 -1
- package/dist/render/backend/webgpu/instanced.js +24 -1
- package/dist/render/backend/webgpu/latePass.js +1 -0
- package/dist/render/backend/webgpu/materialStore.d.ts +101 -0
- package/dist/render/backend/webgpu/materialStore.js +265 -0
- package/dist/render/backend/webgpu/motionPass.js +15 -6
- package/dist/render/backend/webgpu/pipelineCache.d.ts +28 -1
- package/dist/render/backend/webgpu/pipelineCache.js +46 -1
- package/dist/render/backend/webgpu/postPass.d.ts +1 -1
- package/dist/render/backend/webgpu/renderer.d.ts +200 -35
- package/dist/render/backend/webgpu/renderer.js +993 -168
- package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.d.ts +9 -2
- package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.js +121 -47
- package/dist/render/backend/webgpu/skinPaletteRing.d.ts +7 -3
- package/dist/render/backend/webgpu/skinPaletteRing.js +13 -7
- package/dist/render/backend/webgpu/skinPaletteTexture.d.ts +6 -0
- package/dist/render/backend/webgpu/skinPaletteTexture.js +7 -1
- package/dist/render/backend/webgpu/staticBundles.d.ts +125 -0
- package/dist/render/backend/webgpu/staticBundles.js +242 -0
- package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +10 -3
- package/dist/render/backend/webgpu/surfaceTexturePass.js +20 -2
- package/dist/render/backend/webgpu/uniformRing.d.ts +21 -0
- package/dist/render/backend/webgpu/uniformRing.js +9 -1
- package/dist/render/backend/webgpu/vertexFormats.d.ts +22 -0
- package/dist/render/backend/webgpu/vertexFormats.js +97 -0
- package/dist/render/bloomChain.d.ts +3 -3
- package/dist/render/boneAnimation.d.ts +92 -0
- package/dist/render/boneAnimation.js +133 -0
- package/dist/render/compressedSource.d.ts +33 -13
- package/dist/render/compressedSource.js +70 -25
- package/dist/render/glCompressed.js +83 -0
- package/dist/render/globalMediumPass.d.ts +2 -0
- package/dist/render/globalMediumPass.js +1 -0
- package/dist/render/indexWidth.d.ts +17 -0
- package/dist/render/indexWidth.js +23 -0
- package/dist/render/instances.d.ts +26 -0
- package/dist/render/lightBudget.d.ts +10 -0
- package/dist/render/lightBudget.js +10 -0
- package/dist/render/lightMatrix.d.ts +11 -0
- package/dist/render/lightMatrix.js +14 -0
- package/dist/render/lightmap.d.ts +1 -1
- package/dist/render/mesh.d.ts +14 -0
- package/dist/render/mesh.js +44 -14
- package/dist/render/pointLightSelection.d.ts +4 -4
- package/dist/render/renderQuality.d.ts +30 -0
- package/dist/render/renderQuality.js +5 -0
- package/dist/render/rendererHarness.d.ts +7 -0
- package/dist/render/rendererHarness.js +12 -0
- package/dist/render/screenSpaceReflection.d.ts +2 -1
- package/dist/render/shader.d.ts +15 -0
- package/dist/render/shader.js +41 -0
- package/dist/render/shaders/boneAnimation.d.ts +19 -0
- package/dist/render/shaders/boneAnimation.js +63 -0
- package/dist/render/shaders/depth.d.ts +7 -0
- package/dist/render/shaders/depth.js +35 -10
- package/dist/render/shaders/flat/directionalShadow.js +84 -10
- package/dist/render/shaders/flat/glassTint.d.ts +1 -1
- package/dist/render/shaders/flat/glassTint.js +3 -0
- package/dist/render/shaders/flat/index.d.ts +21 -0
- package/dist/render/shaders/flat/index.js +42 -7
- package/dist/render/shaders/flat/layered.d.ts +19 -0
- package/dist/render/shaders/flat/layered.js +115 -0
- package/dist/render/shaders/flat/main.d.ts +1 -1
- package/dist/render/shaders/flat/main.js +58 -21
- package/dist/render/shaders/flat/mapReads.d.ts +10 -0
- package/dist/render/shaders/flat/mapReads.js +38 -0
- package/dist/render/shaders/flat/models.js +1 -1
- package/dist/render/shaders/flat/movingSun.d.ts +21 -0
- package/dist/render/shaders/flat/movingSun.js +46 -0
- package/dist/render/shaders/flat/normalMap.d.ts +6 -2
- package/dist/render/shaders/flat/normalMap.js +14 -3
- package/dist/render/shaders/flat/overlay.d.ts +3 -2
- package/dist/render/shaders/flat/overlay.js +42 -20
- package/dist/render/shaders/flat/pointShadow.js +2 -2
- package/dist/render/shaders/flat/preamble.js +59 -27
- package/dist/render/shaders/flat/worldUv.d.ts +22 -0
- package/dist/render/shaders/flat/worldUv.js +113 -0
- package/dist/render/shaders/generated/depth.wgsl.d.ts +93 -0
- package/dist/render/shaders/generated/depth.wgsl.js +93 -0
- package/dist/render/shaders/generated/flat.wgsl.d.ts +3616 -3042
- package/dist/render/shaders/generated/flat.wgsl.js +3688 -3113
- package/dist/render/shaders/generated/globalMedium.wgsl.d.ts +8 -3
- package/dist/render/shaders/generated/globalMedium.wgsl.js +8 -3
- package/dist/render/shaders/generated/lightVolume.wgsl.d.ts +15 -10
- package/dist/render/shaders/generated/lightVolume.wgsl.js +16 -11
- package/dist/render/shaders/globalMedium.js +9 -1
- package/dist/render/shaders/lightVolume.js +9 -1
- package/dist/render/shaders/outputTransform.d.ts +7 -0
- package/dist/render/shaders/outputTransform.js +28 -14
- package/dist/render/shaders/tangentFrame.d.ts +1 -1
- package/dist/render/shaders/tangentFrame.js +16 -3
- package/dist/render/shaders/vertexChannel.d.ts +7 -0
- package/dist/render/shaders/vertexChannel.js +26 -0
- package/dist/render/skinnedClothRun.d.ts +28 -0
- package/dist/render/skinnedClothRun.js +103 -35
- package/dist/render/staticDraws.d.ts +47 -0
- package/dist/render/staticDraws.js +43 -0
- package/dist/render/surfaceLayers.d.ts +39 -0
- package/dist/render/surfaceLayers.js +26 -0
- package/dist/render/surfaceOverlay.d.ts +16 -3
- package/dist/render/surfaceOverlay.js +11 -6
- package/dist/render/surfaceProjection.d.ts +34 -0
- package/dist/render/surfaceProjection.js +30 -0
- package/dist/render/surfaceTexture.d.ts +68 -7
- package/dist/render/surfaceTexture.js +56 -17
- package/dist/render/vertexPacking.d.ts +48 -0
- package/dist/render/vertexPacking.js +68 -0
- package/package.json +6 -6
- package/src/index.ts +12 -4
- package/src/render/ambientHarmonics.ts +1 -1
- package/src/render/areaLights.ts +4 -4
- package/src/render/backend/api.ts +13 -3
- package/src/render/backend/select.ts +10 -5
- package/src/render/backend/webgl2/boneAnimations.ts +105 -0
- package/src/render/backend/webgl2/instanced.ts +20 -1
- package/src/render/backend/webgl2/renderer.ts +723 -79
- package/src/render/backend/webgl2/skinPaletteTexture.ts +6 -4
- package/src/render/backend/webgpu/boneAnimations.ts +97 -0
- package/src/render/backend/webgpu/buffers.ts +195 -70
- package/src/render/backend/webgpu/clothKernels.ts +86 -0
- package/src/render/backend/webgpu/clothOps.ts +86 -0
- package/src/render/backend/webgpu/clothSetLayout.ts +102 -0
- package/src/render/backend/webgpu/clothSetPack.ts +197 -0
- package/src/render/backend/webgpu/compressedUpload.ts +59 -9
- package/src/render/backend/webgpu/depthPass.ts +51 -10
- package/src/render/backend/webgpu/drawCommand.ts +25 -4
- package/src/render/backend/webgpu/flatPass.ts +212 -41
- package/src/render/backend/webgpu/gpuClothSet.ts +564 -0
- package/src/render/backend/webgpu/instanced.ts +32 -1
- package/src/render/backend/webgpu/latePass.ts +1 -0
- package/src/render/backend/webgpu/materialStore.ts +276 -0
- package/src/render/backend/webgpu/motionPass.ts +18 -7
- package/src/render/backend/webgpu/pipelineCache.ts +53 -7
- package/src/render/backend/webgpu/renderer.ts +1201 -202
- package/src/render/backend/webgpu/shaders/clothSolve.wgsl.ts +121 -47
- package/src/render/backend/webgpu/skinPaletteRing.ts +13 -7
- package/src/render/backend/webgpu/skinPaletteTexture.ts +7 -1
- package/src/render/backend/webgpu/staticBundles.ts +364 -0
- package/src/render/backend/webgpu/surfaceTexturePass.ts +29 -3
- package/src/render/backend/webgpu/uniformRing.ts +10 -1
- package/src/render/backend/webgpu/vertexFormats.ts +113 -0
- package/src/render/bloomChain.ts +3 -3
- package/src/render/boneAnimation.ts +223 -0
- package/src/render/compressedSource.ts +121 -29
- package/src/render/glCompressed.ts +83 -0
- package/src/render/globalMediumPass.ts +7 -0
- package/src/render/indexWidth.ts +27 -0
- package/src/render/instances.ts +27 -0
- package/src/render/lightBudget.ts +11 -0
- package/src/render/lightMatrix.ts +15 -0
- package/src/render/lightmap.ts +1 -1
- package/src/render/mesh.ts +72 -14
- package/src/render/pointLightSelection.ts +4 -4
- package/src/render/renderQuality.ts +38 -0
- package/src/render/rendererHarness.ts +15 -0
- package/src/render/screenSpaceReflection.ts +2 -1
- package/src/render/shader.ts +59 -0
- package/src/render/shaders/boneAnimation.ts +63 -0
- package/src/render/shaders/depth.ts +45 -10
- package/src/render/shaders/flat/directionalShadow.ts +84 -10
- package/src/render/shaders/flat/glassTint.ts +7 -1
- package/src/render/shaders/flat/index.ts +63 -7
- package/src/render/shaders/flat/layered.ts +115 -0
- package/src/render/shaders/flat/main.ts +58 -21
- package/src/render/shaders/flat/mapReads.ts +38 -0
- package/src/render/shaders/flat/models.ts +1 -1
- package/src/render/shaders/flat/movingSun.ts +46 -0
- package/src/render/shaders/flat/normalMap.ts +14 -3
- package/src/render/shaders/flat/overlay.ts +42 -20
- package/src/render/shaders/flat/pointShadow.ts +2 -2
- package/src/render/shaders/flat/preamble.ts +59 -27
- package/src/render/shaders/flat/worldUv.ts +113 -0
- package/src/render/shaders/generated/depth.wgsl.ts +95 -0
- package/src/render/shaders/generated/flat.wgsl.ts +3688 -3113
- package/src/render/shaders/generated/globalMedium.wgsl.ts +8 -3
- package/src/render/shaders/generated/lightVolume.wgsl.ts +16 -11
- package/src/render/shaders/globalMedium.ts +9 -1
- package/src/render/shaders/lightVolume.ts +9 -1
- package/src/render/shaders/outputTransform.ts +30 -14
- package/src/render/shaders/tangentFrame.ts +16 -3
- package/src/render/shaders/vertexChannel.ts +27 -0
- package/src/render/skinnedClothRun.ts +138 -38
- package/src/render/staticDraws.ts +95 -0
- package/src/render/surfaceLayers.ts +57 -0
- package/src/render/surfaceOverlay.ts +24 -7
- package/src/render/surfaceProjection.ts +57 -0
- package/src/render/surfaceTexture.ts +124 -27
- package/src/render/vertexPacking.ts +109 -0
- package/dist/render/backend/webgpu/gpuSkinnedCloth.d.ts +0 -74
- package/dist/render/backend/webgpu/gpuSkinnedCloth.js +0 -452
- package/src/render/backend/webgpu/gpuSkinnedCloth.ts +0 -502
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skinned cloth solved on the device, a set of garments at a time: `ClothControl`'s schedule for
|
|
3
|
+
* each garment, over one set of buffers, in one compute pass and one submit a frame.
|
|
4
|
+
*
|
|
5
|
+
* **The CPU solver's steps, as dispatches.** A step is `clothBegin`, then per substep
|
|
6
|
+
* `clothPredict`, every colour of every kind of constraint per iteration, `clothLimit` and
|
|
7
|
+
* `clothFinish`; then `clothBlend` while a reset blends back in. The particles never leave the
|
|
8
|
+
* device: a frame ends with `clothPublish` writing each garment's, at the frame's `alpha`, into the
|
|
9
|
+
* particle texture its bound meshes' vertex stage reads — so a garment costs no upload a frame.
|
|
10
|
+
*
|
|
11
|
+
* **A round is one kind of work for every garment that has it next.** Each garment's control asks
|
|
12
|
+
* for poses, resets and steps as it always did; they wait in its queue (`clothOps.ts`) until the
|
|
13
|
+
* set records them a round at a time — the poses of every garment with a pose next, in one
|
|
14
|
+
* dispatch, then the rests, then a step for every garment with a step next, each colour of each kind
|
|
15
|
+
* one dispatch across the set (`clothSetPack.ts`). A garment with nothing of that kind sits the round
|
|
16
|
+
* out. So a step costs one garment's dispatches whatever the set holds: fifteen garments of nine
|
|
17
|
+
* colours at four iterations are about a hundred a step, where a pass a garment was some fourteen
|
|
18
|
+
* hundred.
|
|
19
|
+
*
|
|
20
|
+
* **One submit a frame, everything it needs written before it.** `queue.writeBuffer` lands ahead of
|
|
21
|
+
* the whole submit rather than between two dispatches, which is the trap `AGENTS.md` (2026-08-27)
|
|
22
|
+
* records for draws and is no different here. So every round's records take a slot of their own, a
|
|
23
|
+
* frame that runs out of slots submits what it has and goes on, and a garment posed while work
|
|
24
|
+
* against its last pose is still unsent sends that work first, since its matrices are one region.
|
|
25
|
+
*
|
|
26
|
+
* **What it gives up**: single precision; a colour dispatched over the most records any garment has
|
|
27
|
+
* in it; and garments of one set step together, so they share a step, substeps, iterations and most
|
|
28
|
+
* steps a frame, refused by name otherwise. Self-collision is accepted and not simulated, as on the
|
|
29
|
+
* CPU.
|
|
30
|
+
*/
|
|
31
|
+
import { ClothControl, exactExp, validateClothSetup } from '@driftengine/physics';
|
|
32
|
+
import { clothKernelsFor } from './clothKernels.js';
|
|
33
|
+
import { CLOTH_OP_POSE, CLOTH_OP_REST, CLOTH_OP_STEP, ClothOps, PACE_HEAD_WORDS, PACE_WORDS, } from './clothOps.js';
|
|
34
|
+
import { CLOTH_CONSTANT_BYTES, clothBatchSlots, clothConstants, clothRoundOffsets, writeClothCarry, } from './clothSetLayout.js';
|
|
35
|
+
import { packClothSet } from './clothSetPack.js';
|
|
36
|
+
import { packCloth } from './clothSolvePack.js';
|
|
37
|
+
import { GpuClothParticles } from './clothTextures.js';
|
|
38
|
+
import { CLOTH_WORKGROUP } from './shaders/clothSolve.wgsl.js';
|
|
39
|
+
import { DYNAMIC_ALIGNMENT } from './uniformRing.js';
|
|
40
|
+
const STORAGE = 0x80;
|
|
41
|
+
const UNIFORM = 0x40;
|
|
42
|
+
const COPY_DST = 0x8;
|
|
43
|
+
const COPY_SRC = 0x4;
|
|
44
|
+
const MAP_READ = 0x1;
|
|
45
|
+
/** Rounds a submit holds before it is sent and the slots start again. */
|
|
46
|
+
const ROUNDS = 64;
|
|
47
|
+
const KINDS = [CLOTH_OP_POSE, CLOTH_OP_REST, CLOTH_OP_STEP];
|
|
48
|
+
export class GpuClothSet {
|
|
49
|
+
device;
|
|
50
|
+
garments = [];
|
|
51
|
+
packed;
|
|
52
|
+
kernels;
|
|
53
|
+
buffers = [];
|
|
54
|
+
state;
|
|
55
|
+
frameData;
|
|
56
|
+
paces;
|
|
57
|
+
group;
|
|
58
|
+
/** One round's records, every garment's: the head an op's, the rest each garment's own. */
|
|
59
|
+
staging;
|
|
60
|
+
stagingWords;
|
|
61
|
+
stagingFloats;
|
|
62
|
+
/** Each kind's first batch slot, and the first garment's publishing slot. */
|
|
63
|
+
batchSlot;
|
|
64
|
+
groups;
|
|
65
|
+
substeps;
|
|
66
|
+
iterations;
|
|
67
|
+
encoder = null;
|
|
68
|
+
pass = null;
|
|
69
|
+
round = 0;
|
|
70
|
+
constructor(device, setups) {
|
|
71
|
+
this.device = device;
|
|
72
|
+
if (setups.length === 0)
|
|
73
|
+
throw new Error('skinned cloth set: no garments');
|
|
74
|
+
for (const setup of setups)
|
|
75
|
+
validateClothSetup(setup);
|
|
76
|
+
const packed = packClothSet(setups.map(packCloth));
|
|
77
|
+
this.packed = packed;
|
|
78
|
+
const n = packed.count;
|
|
79
|
+
const count = setups.length;
|
|
80
|
+
this.kernels = clothKernelsFor(device);
|
|
81
|
+
this.groups = Math.ceil(n / CLOTH_WORKGROUP);
|
|
82
|
+
const buffer = (label, bytes, usage) => {
|
|
83
|
+
const made = device.createBuffer({
|
|
84
|
+
label: `cloth.${label}`,
|
|
85
|
+
size: Math.max(16, bytes),
|
|
86
|
+
usage,
|
|
87
|
+
});
|
|
88
|
+
this.buffers.push(made);
|
|
89
|
+
return made;
|
|
90
|
+
};
|
|
91
|
+
this.staging = new ArrayBuffer(count * PACE_WORDS * 4);
|
|
92
|
+
this.stagingWords = new Uint32Array(this.staging);
|
|
93
|
+
this.stagingFloats = new Float32Array(this.staging);
|
|
94
|
+
setups.forEach((setup, g) => this.garments.push(this.garment(setup, g)));
|
|
95
|
+
const first = this.garments[0].control.parameters;
|
|
96
|
+
this.substeps = first.substeps;
|
|
97
|
+
this.iterations = first.iterations;
|
|
98
|
+
this.refuseStrangers(first);
|
|
99
|
+
const constants = buffer('constants', CLOTH_CONSTANT_BYTES, UNIFORM | COPY_DST);
|
|
100
|
+
const rounds = buffer('rounds', ROUNDS * DYNAMIC_ALIGNMENT, UNIFORM | COPY_DST);
|
|
101
|
+
this.paces = buffer('paces', ROUNDS * this.staging.byteLength, STORAGE | COPY_DST);
|
|
102
|
+
const batchWords = clothBatchSlots(packed);
|
|
103
|
+
const batches = buffer('batches', batchWords.byteLength, UNIFORM | COPY_DST);
|
|
104
|
+
this.state = buffer('state', n * 12 * 4, STORAGE | COPY_DST | COPY_SRC);
|
|
105
|
+
const pose = buffer('pose', n * 9 * 4, STORAGE);
|
|
106
|
+
const statics = buffer('statics', packed.statics.byteLength, STORAGE | COPY_DST);
|
|
107
|
+
const constraints = buffer('constraints', packed.constraints.byteLength, STORAGE | COPY_DST);
|
|
108
|
+
const lambda = buffer('lambda', (packed.distances + packed.bendings) * 4, STORAGE);
|
|
109
|
+
this.frameData = buffer('frame', (packed.joints * 16 + packed.colliders * 8) * 4, STORAGE | COPY_DST);
|
|
110
|
+
const queue = device.queue;
|
|
111
|
+
queue.writeBuffer(constants, 0, clothConstants(packed, first.step / first.substeps));
|
|
112
|
+
queue.writeBuffer(rounds, 0, clothRoundOffsets(ROUNDS, count));
|
|
113
|
+
queue.writeBuffer(batches, 0, batchWords);
|
|
114
|
+
queue.writeBuffer(statics, 0, packed.statics);
|
|
115
|
+
if (packed.constraints.byteLength > 0) {
|
|
116
|
+
queue.writeBuffer(constraints, 0, packed.constraints);
|
|
117
|
+
}
|
|
118
|
+
/* The rest pose until the first pose, which resets each garment to its own. */
|
|
119
|
+
const state = new Float32Array(n * 12);
|
|
120
|
+
setups.forEach((setup, g) => {
|
|
121
|
+
const base = this.garments[g].range.particleBase * 3;
|
|
122
|
+
state.set(setup.positions, base);
|
|
123
|
+
state.set(setup.positions, n * 9 + base);
|
|
124
|
+
});
|
|
125
|
+
queue.writeBuffer(this.state, 0, state);
|
|
126
|
+
const slot = (of) => ({ buffer: of, size: DYNAMIC_ALIGNMENT });
|
|
127
|
+
this.group = device.createBindGroup({
|
|
128
|
+
label: 'cloth.solve',
|
|
129
|
+
layout: this.kernels.layout,
|
|
130
|
+
entries: [
|
|
131
|
+
{ binding: 0, resource: { buffer: constants } },
|
|
132
|
+
{ binding: 1, resource: slot(rounds) },
|
|
133
|
+
{ binding: 2, resource: slot(batches) },
|
|
134
|
+
{ binding: 3, resource: { buffer: this.state } },
|
|
135
|
+
{ binding: 4, resource: { buffer: pose } },
|
|
136
|
+
{ binding: 5, resource: { buffer: statics } },
|
|
137
|
+
{ binding: 6, resource: { buffer: constraints } },
|
|
138
|
+
{ binding: 7, resource: { buffer: lambda } },
|
|
139
|
+
{ binding: 8, resource: { buffer: this.frameData } },
|
|
140
|
+
{ binding: 9, resource: { buffer: this.paces } },
|
|
141
|
+
],
|
|
142
|
+
});
|
|
143
|
+
const colours = (batches) => (batches.length - 1) * 2;
|
|
144
|
+
this.batchSlot = {
|
|
145
|
+
distance: 0,
|
|
146
|
+
bending: colours(packed.distanceBatches),
|
|
147
|
+
tether: colours(packed.distanceBatches) + colours(packed.bendingBatches),
|
|
148
|
+
publish: colours(packed.distanceBatches) +
|
|
149
|
+
colours(packed.bendingBatches) +
|
|
150
|
+
colours(packed.tetherBatches),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/** How many garments the set holds. */
|
|
154
|
+
get size() {
|
|
155
|
+
return this.garments.length;
|
|
156
|
+
}
|
|
157
|
+
/** Garment `g`'s particles, what `setCloth` draws its bound meshes by. */
|
|
158
|
+
particles(g) {
|
|
159
|
+
return this.at(g).particles;
|
|
160
|
+
}
|
|
161
|
+
/** Garment `g`'s rig for the steps to come; see `ClothControl.setPose`. */
|
|
162
|
+
setPose(g, globals, model) {
|
|
163
|
+
this.at(g).control.setPose(globals, model);
|
|
164
|
+
}
|
|
165
|
+
/** The frame's sample of the scene's wind for garment `g`. See `SkinnedCloth.setWind`. */
|
|
166
|
+
setWind(g, x, y, z) {
|
|
167
|
+
const wind = this.at(g).wind;
|
|
168
|
+
wind[0] = x;
|
|
169
|
+
wind[1] = y;
|
|
170
|
+
wind[2] = z;
|
|
171
|
+
}
|
|
172
|
+
/** Garment `g` back to its skinned pose, settled; the next `step` publishes it. */
|
|
173
|
+
reset(g) {
|
|
174
|
+
this.at(g).control.reset();
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* `dt` of the caller's time in whole fixed steps for every garment, and each garment's particles
|
|
178
|
+
* at its `alpha` published for the draws that follow, as the particles of `frame`.
|
|
179
|
+
*/
|
|
180
|
+
step(dt, frame) {
|
|
181
|
+
const garments = this.garments;
|
|
182
|
+
for (let g = 0; g < garments.length; g++)
|
|
183
|
+
garments[g].control.advance(dt);
|
|
184
|
+
this.run();
|
|
185
|
+
const at = this.takeRound();
|
|
186
|
+
for (let g = 0; g < garments.length; g++) {
|
|
187
|
+
const base = g * PACE_WORDS;
|
|
188
|
+
const garment = garments[g];
|
|
189
|
+
this.stagingWords.fill(0, base, base + PACE_HEAD_WORDS);
|
|
190
|
+
this.stagingFloats[base + 2] = garment.control.alpha;
|
|
191
|
+
this.stagingFloats.set(garment.wind, base + 28);
|
|
192
|
+
}
|
|
193
|
+
this.device.queue.writeBuffer(this.paces, this.paceOffset(at), this.staging);
|
|
194
|
+
for (let g = 0; g < garments.length; g++) {
|
|
195
|
+
const garment = garments[g];
|
|
196
|
+
const view = garment.particles.publish(frame);
|
|
197
|
+
const slot = (this.batchSlot.publish + g) * DYNAMIC_ALIGNMENT;
|
|
198
|
+
const groups = Math.ceil(garment.range.count / CLOTH_WORKGROUP);
|
|
199
|
+
this.dispatch('clothPublish', at, slot, garment.outputs[view], groups);
|
|
200
|
+
}
|
|
201
|
+
this.flush();
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Garment `g`'s particles where its last step left them, three floats each: for the parity check,
|
|
205
|
+
* which compares them with the CPU solver's positions. Not for a frame — it waits on the device.
|
|
206
|
+
*/
|
|
207
|
+
async read(g) {
|
|
208
|
+
this.run();
|
|
209
|
+
this.flush();
|
|
210
|
+
const { particleBase, count } = this.at(g).range;
|
|
211
|
+
const bytes = count * 12;
|
|
212
|
+
const readback = this.device.createBuffer({
|
|
213
|
+
label: 'cloth.readback',
|
|
214
|
+
size: bytes,
|
|
215
|
+
usage: MAP_READ | COPY_DST,
|
|
216
|
+
});
|
|
217
|
+
const encoder = this.device.createCommandEncoder({ label: 'cloth.read' });
|
|
218
|
+
encoder.copyBufferToBuffer(this.state, particleBase * 12, readback, 0, bytes);
|
|
219
|
+
this.device.queue.submit([encoder.finish()]);
|
|
220
|
+
await readback.mapAsync(MAP_READ);
|
|
221
|
+
const out = new Float32Array(readback.getMappedRange().slice(0));
|
|
222
|
+
readback.unmap();
|
|
223
|
+
readback.destroy();
|
|
224
|
+
return out;
|
|
225
|
+
}
|
|
226
|
+
/** Its buffers; the particle textures are the renderer's to retire with the frame. */
|
|
227
|
+
dispose() {
|
|
228
|
+
this.flush();
|
|
229
|
+
for (const made of this.buffers)
|
|
230
|
+
made.destroy();
|
|
231
|
+
}
|
|
232
|
+
at(g) {
|
|
233
|
+
const garment = this.garments[g];
|
|
234
|
+
if (garment === undefined)
|
|
235
|
+
throw new Error(`skinned cloth set: no garment ${g}`);
|
|
236
|
+
return garment;
|
|
237
|
+
}
|
|
238
|
+
/** Garment `g`: its schedule over a device that queues, its particles and its own numbers. */
|
|
239
|
+
garment(setup, g) {
|
|
240
|
+
const range = this.packed.garments[g];
|
|
241
|
+
const particles = new GpuClothParticles(this.device, range.count, true);
|
|
242
|
+
const ops = new ClothOps();
|
|
243
|
+
const solver = {
|
|
244
|
+
targetsChanged: (change) => this.targetsChanged(garment, change),
|
|
245
|
+
rest: () => this.queueOp(garment, CLOTH_OP_REST),
|
|
246
|
+
step: (fraction, blend) => this.queueStep(garment, fraction, blend),
|
|
247
|
+
};
|
|
248
|
+
const control = new ClothControl(setup, solver, false);
|
|
249
|
+
const output = (k) => this.device.createBindGroup({
|
|
250
|
+
label: 'cloth.publish',
|
|
251
|
+
layout: this.kernels.outputLayout,
|
|
252
|
+
entries: [{ binding: 0, resource: particles.viewOf(k) }],
|
|
253
|
+
});
|
|
254
|
+
const garment = {
|
|
255
|
+
range,
|
|
256
|
+
control,
|
|
257
|
+
particles,
|
|
258
|
+
ops,
|
|
259
|
+
wind: new Float32Array(control.parameters.wind),
|
|
260
|
+
outputs: [output(0), output(1)],
|
|
261
|
+
fromAt: 0,
|
|
262
|
+
toAt: 0,
|
|
263
|
+
recorded: false,
|
|
264
|
+
};
|
|
265
|
+
this.writeOwn(garment, g);
|
|
266
|
+
return garment;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* A garment's own words of its record, which every round carries: its colliders, its texture's
|
|
270
|
+
* width and its constants. Its limits ride in the head beside `live`, written as it joins a round.
|
|
271
|
+
*/
|
|
272
|
+
writeOwn(garment, g) {
|
|
273
|
+
const p = garment.control.parameters;
|
|
274
|
+
const h = p.step / p.substeps;
|
|
275
|
+
const base = g * PACE_WORDS;
|
|
276
|
+
const u = this.stagingWords;
|
|
277
|
+
const f = this.stagingFloats;
|
|
278
|
+
u[base + 32] = garment.range.colliderBase;
|
|
279
|
+
u[base + 33] = garment.range.colliders;
|
|
280
|
+
u[base + 34] = garment.particles.width;
|
|
281
|
+
f[base + 36] = exactExp(p.damping * h);
|
|
282
|
+
f[base + 37] = exactExp(p.drag * h);
|
|
283
|
+
f[base + 38] = p.maxDistanceScale;
|
|
284
|
+
f[base + 39] = p.margin;
|
|
285
|
+
f.set(p.gravity, base + 40);
|
|
286
|
+
}
|
|
287
|
+
/** Garments that cannot step together, refused by the first number they disagree on. */
|
|
288
|
+
refuseStrangers(first) {
|
|
289
|
+
this.garments.forEach((garment, g) => {
|
|
290
|
+
const p = garment.control.parameters;
|
|
291
|
+
for (const name of ['step', 'substeps', 'iterations', 'maxSteps']) {
|
|
292
|
+
if (p[name] !== first[name]) {
|
|
293
|
+
throw new Error(`skinned cloth set: garment ${g} has ${name} ${p[name]} where garment 0 has ` +
|
|
294
|
+
`${first[name]}; a set steps its garments together, so they must agree`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
targetsChanged(garment, change) {
|
|
300
|
+
if (change !== 'posed') {
|
|
301
|
+
garment.fromAt = garment.toAt;
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
/*
|
|
305
|
+
* The pose's matrices and colliders are written ahead of the next submit, so any work of this
|
|
306
|
+
* garment's still waiting or recorded — a reset's settle steps, which read the colliders — is
|
|
307
|
+
* sent first, or it would run against this pose rather than its own.
|
|
308
|
+
*/
|
|
309
|
+
if (garment.ops.pending > 0 || garment.recorded) {
|
|
310
|
+
this.run();
|
|
311
|
+
this.flush();
|
|
312
|
+
}
|
|
313
|
+
this.writeFrameData(garment);
|
|
314
|
+
garment.fromAt = garment.toAt;
|
|
315
|
+
garment.toAt = garment.toAt === 0 ? this.packed.count * 3 : 0;
|
|
316
|
+
this.queueOp(garment, CLOTH_OP_POSE);
|
|
317
|
+
}
|
|
318
|
+
queueOp(garment, kind) {
|
|
319
|
+
const at = garment.ops.push(kind);
|
|
320
|
+
const words = garment.ops.headWords;
|
|
321
|
+
words[at + 4] = garment.fromAt;
|
|
322
|
+
words[at + 5] = garment.toAt;
|
|
323
|
+
garment.ops.headFloats.set(garment.wind, at + 28);
|
|
324
|
+
return at;
|
|
325
|
+
}
|
|
326
|
+
queueStep(garment, fraction, blend) {
|
|
327
|
+
const at = this.queueOp(garment, CLOTH_OP_STEP);
|
|
328
|
+
const f = garment.ops.headFloats;
|
|
329
|
+
f[at] = fraction;
|
|
330
|
+
f[at + 1] = blend;
|
|
331
|
+
const { parameters, targets } = garment.control;
|
|
332
|
+
writeClothCarry(parameters, targets.modelBefore, targets.modelNow, garment.ops.headWords, f, at);
|
|
333
|
+
}
|
|
334
|
+
/** Every op every garment is waiting on, a round of one kind at a time, in each garment's order. */
|
|
335
|
+
run() {
|
|
336
|
+
for (;;) {
|
|
337
|
+
let ran = false;
|
|
338
|
+
for (let k = 0; k < KINDS.length; k++) {
|
|
339
|
+
const kind = KINDS[k];
|
|
340
|
+
if (this.waiting(kind)) {
|
|
341
|
+
this.runRound(kind);
|
|
342
|
+
ran = true;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (!ran)
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
/** Whether any garment has `kind` next. */
|
|
350
|
+
waiting(kind) {
|
|
351
|
+
for (let g = 0; g < this.garments.length; g++) {
|
|
352
|
+
if (this.garments[g].ops.next === kind)
|
|
353
|
+
return true;
|
|
354
|
+
}
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
/** One round: every garment with `kind` next takes part, and the rest sit it out. */
|
|
358
|
+
runRound(kind) {
|
|
359
|
+
const at = this.takeRound();
|
|
360
|
+
const words = this.stagingWords;
|
|
361
|
+
let blends = false;
|
|
362
|
+
for (let g = 0; g < this.garments.length; g++) {
|
|
363
|
+
const garment = this.garments[g];
|
|
364
|
+
const base = g * PACE_WORDS;
|
|
365
|
+
words.fill(0, base, base + PACE_HEAD_WORDS);
|
|
366
|
+
if (garment.ops.next !== kind)
|
|
367
|
+
continue;
|
|
368
|
+
garment.ops.take(words, base);
|
|
369
|
+
words[base + 6] = 1;
|
|
370
|
+
words[base + 7] = garment.range.limits;
|
|
371
|
+
garment.recorded = true;
|
|
372
|
+
if (this.stagingFloats[base + 1] > 0)
|
|
373
|
+
blends = true;
|
|
374
|
+
}
|
|
375
|
+
this.device.queue.writeBuffer(this.paces, this.paceOffset(at), this.staging);
|
|
376
|
+
if (kind === CLOTH_OP_POSE)
|
|
377
|
+
this.dispatch('clothPose', at, 0, null);
|
|
378
|
+
else if (kind === CLOTH_OP_REST)
|
|
379
|
+
this.dispatch('clothRest', at, 0, null);
|
|
380
|
+
else
|
|
381
|
+
this.stepRound(at, blends);
|
|
382
|
+
}
|
|
383
|
+
stepRound(at, blends) {
|
|
384
|
+
this.dispatch('clothBegin', at, 0, null);
|
|
385
|
+
const { distanceBatches, bendingBatches, tetherBatches } = this.packed;
|
|
386
|
+
for (let s = 0; s < this.substeps; s++) {
|
|
387
|
+
this.dispatch('clothPredict', at, 0, null);
|
|
388
|
+
for (let pass = 0; pass < this.iterations; pass++) {
|
|
389
|
+
const first = pass === 0 ? 1 : 0;
|
|
390
|
+
this.batches('clothDistance', distanceBatches, this.batchSlot.distance, first, at);
|
|
391
|
+
this.batches('clothBending', bendingBatches, this.batchSlot.bending, first, at);
|
|
392
|
+
this.batches('clothTether', tetherBatches, this.batchSlot.tether, first, at);
|
|
393
|
+
}
|
|
394
|
+
this.dispatch('clothLimit', at, 0, null);
|
|
395
|
+
this.dispatch('clothFinish', at, 0, null);
|
|
396
|
+
}
|
|
397
|
+
if (blends)
|
|
398
|
+
this.dispatch('clothBlend', at, 0, null);
|
|
399
|
+
}
|
|
400
|
+
/** Every colour of one kind across the set, in colour order, each a dispatch over its range. */
|
|
401
|
+
batches(entry, batches, firstSlot, first, roundAt) {
|
|
402
|
+
for (let b = 0; b + 1 < batches.length; b++) {
|
|
403
|
+
const size = batches[b + 1] - batches[b];
|
|
404
|
+
if (size === 0)
|
|
405
|
+
continue;
|
|
406
|
+
const slot = firstSlot + b * 2 + first;
|
|
407
|
+
this.dispatch(entry, roundAt, slot * DYNAMIC_ALIGNMENT, null, Math.ceil(size / CLOTH_WORKGROUP));
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
dispatch(entry, roundAt, batchAt, output, groups = this.groups) {
|
|
411
|
+
const pass = this.passFor();
|
|
412
|
+
pass.setPipeline(this.kernels.pipelines[entry]);
|
|
413
|
+
pass.setBindGroup(0, this.group, [roundAt, batchAt]);
|
|
414
|
+
if (output !== null)
|
|
415
|
+
pass.setBindGroup(1, output);
|
|
416
|
+
pass.dispatchWorkgroups(groups);
|
|
417
|
+
}
|
|
418
|
+
passFor() {
|
|
419
|
+
if (this.pass === null) {
|
|
420
|
+
this.encoder = this.device.createCommandEncoder({ label: 'cloth' });
|
|
421
|
+
this.pass = this.encoder.beginComputePass({ label: 'cloth' });
|
|
422
|
+
}
|
|
423
|
+
return this.pass;
|
|
424
|
+
}
|
|
425
|
+
/** A round's slot, sending what is recorded first where the slots are spent: its byte offset. */
|
|
426
|
+
takeRound() {
|
|
427
|
+
if (this.round === ROUNDS)
|
|
428
|
+
this.flush();
|
|
429
|
+
return this.round++ * DYNAMIC_ALIGNMENT;
|
|
430
|
+
}
|
|
431
|
+
/** Where the records of the round at `roundAt` sit in `paces`. */
|
|
432
|
+
paceOffset(roundAt) {
|
|
433
|
+
return (roundAt / DYNAMIC_ALIGNMENT) * this.staging.byteLength;
|
|
434
|
+
}
|
|
435
|
+
/** Send what has been recorded: every slot it reads was written ahead of it on the queue. */
|
|
436
|
+
flush() {
|
|
437
|
+
const { pass, encoder } = this;
|
|
438
|
+
if (pass === null || encoder === null)
|
|
439
|
+
return;
|
|
440
|
+
pass.end();
|
|
441
|
+
this.device.queue.submit([encoder.finish()]);
|
|
442
|
+
this.pass = null;
|
|
443
|
+
this.encoder = null;
|
|
444
|
+
this.round = 0;
|
|
445
|
+
for (let g = 0; g < this.garments.length; g++)
|
|
446
|
+
this.garments[g].recorded = false;
|
|
447
|
+
}
|
|
448
|
+
/** A garment's skin matrices — or its model, with no rig — and its colliders, into the set's. */
|
|
449
|
+
writeFrameData(garment) {
|
|
450
|
+
const targets = garment.control.targets;
|
|
451
|
+
const { jointBase, joints, colliderBase, colliders, rigged } = garment.range;
|
|
452
|
+
const queue = this.device.queue;
|
|
453
|
+
const matrices = (rigged && targets.skin !== null ? targets.skin : targets.model);
|
|
454
|
+
queue.writeBuffer(this.frameData, jointBase * 64, matrices, 0, joints * 16);
|
|
455
|
+
if (colliders === 0)
|
|
456
|
+
return;
|
|
457
|
+
const ends = this.packed.joints * 16;
|
|
458
|
+
const radii = ends + this.packed.colliders * 6;
|
|
459
|
+
queue.writeBuffer(this.frameData, (ends + colliderBase * 6) * 4, targets.colliderEnds, 0, colliders * 6);
|
|
460
|
+
queue.writeBuffer(this.frameData, (radii + colliderBase * 2) * 4, targets.colliderRadii, 0, colliders * 2);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CullSlot } from './instanceCullPass.ts';
|
|
2
2
|
import type { MeshInstances } from '../../instances.ts';
|
|
3
3
|
import type { GpuMesh } from './buffers.ts';
|
|
4
|
+
import { GpuInstanceClocks, type GpuBoneAnimation } from './boneAnimations.ts';
|
|
4
5
|
/**
|
|
5
6
|
* One mesh's per-instance buffer on the device.
|
|
6
7
|
*
|
|
@@ -25,7 +26,19 @@ export declare class GpuInstancedBatch {
|
|
|
25
26
|
/** The frame the slots were last taken in, and how many were taken. */
|
|
26
27
|
slotFrame: number;
|
|
27
28
|
slotsTaken: number;
|
|
28
|
-
|
|
29
|
+
/** The bone animation every instance plays, and its clocks; null for a batch that plays none. */
|
|
30
|
+
readonly animation: GpuBoneAnimation | null;
|
|
31
|
+
readonly clocks: GpuInstanceClocks | null;
|
|
32
|
+
/**
|
|
33
|
+
* An animated batch's own groups — its lit twins by the material group they were built beside, its
|
|
34
|
+
* shadow groups by what they cut with — and the renderer's group epoch they were built in, after
|
|
35
|
+
* which every one is dropped. See `WebGPURenderer.animatedGroup`.
|
|
36
|
+
*/
|
|
37
|
+
readonly litGroups: Map<GPUBindGroup, GPUBindGroup>;
|
|
38
|
+
readonly depthGroups: Map<unknown, GPUBindGroup>;
|
|
39
|
+
readonly depthPeelGroups: Map<unknown, GPUBindGroup>;
|
|
40
|
+
groupEpoch: number;
|
|
41
|
+
constructor(device: GPUDevice, mesh: GpuMesh, capacity: number, label: string, cull?: boolean, animation?: GpuBoneAnimation | null);
|
|
29
42
|
/** Last frame's packed slots, for a reconstruction; made on first use by the renderer. */
|
|
30
43
|
previousBuffer: GPUBuffer | null;
|
|
31
44
|
/** How many slots the upload before the last one held, and how many the last one holds. */
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { instancesBox } from '../../instanceCull.js';
|
|
2
2
|
import { INSTANCE_FLOATS, INSTANCE_STRIDE, packInstances } from '../../instances.js';
|
|
3
3
|
import { destroySlot } from './instanceCullPass.js';
|
|
4
|
+
import { packInstanceClocks } from '../../boneAnimation.js';
|
|
5
|
+
import { GpuInstanceClocks } from './boneAnimations.js';
|
|
4
6
|
/** VERTEX | COPY_DST, spelled the way every other pass here spells it. */
|
|
5
7
|
const USAGE_VERTEX = 0x0020 | 0x0008;
|
|
6
8
|
/** A culling batch's source is read by the cull as storage too. */
|
|
@@ -29,10 +31,26 @@ export class GpuInstancedBatch {
|
|
|
29
31
|
/** The frame the slots were last taken in, and how many were taken. */
|
|
30
32
|
slotFrame = -1;
|
|
31
33
|
slotsTaken = 0;
|
|
32
|
-
|
|
34
|
+
/** The bone animation every instance plays, and its clocks; null for a batch that plays none. */
|
|
35
|
+
animation;
|
|
36
|
+
clocks;
|
|
37
|
+
/**
|
|
38
|
+
* An animated batch's own groups — its lit twins by the material group they were built beside, its
|
|
39
|
+
* shadow groups by what they cut with — and the renderer's group epoch they were built in, after
|
|
40
|
+
* which every one is dropped. See `WebGPURenderer.animatedGroup`.
|
|
41
|
+
*/
|
|
42
|
+
litGroups = new Map();
|
|
43
|
+
depthGroups = new Map();
|
|
44
|
+
depthPeelGroups = new Map();
|
|
45
|
+
groupEpoch = -1;
|
|
46
|
+
constructor(device, mesh, capacity, label, cull = false, animation = null) {
|
|
33
47
|
this.mesh = mesh;
|
|
34
48
|
this.capacity = capacity;
|
|
49
|
+
/* An animated batch is culled whole and never by instance: see `submitInstanced`. */
|
|
35
50
|
this.cull = cull;
|
|
51
|
+
this.animation = animation;
|
|
52
|
+
this.clocks =
|
|
53
|
+
animation === null ? null : new GpuInstanceClocks(device, capacity, `${label}.clocks`);
|
|
36
54
|
this.staging = new Float32Array(capacity * INSTANCE_FLOATS);
|
|
37
55
|
this.buffer = device.createBuffer({
|
|
38
56
|
/*
|
|
@@ -76,10 +94,15 @@ export class GpuInstancedBatch {
|
|
|
76
94
|
return;
|
|
77
95
|
packInstances(data, this.staging);
|
|
78
96
|
queue.writeBuffer(this.buffer, 0, this.staging, 0, count * INSTANCE_FLOATS);
|
|
97
|
+
if (this.clocks !== null) {
|
|
98
|
+
packInstanceClocks(data, this.clocks.staging);
|
|
99
|
+
this.clocks.upload(queue, count);
|
|
100
|
+
}
|
|
79
101
|
}
|
|
80
102
|
dispose() {
|
|
81
103
|
this.previousBuffer?.destroy();
|
|
82
104
|
this.buffer.destroy();
|
|
105
|
+
this.clocks?.dispose();
|
|
83
106
|
for (const slot of this.slots)
|
|
84
107
|
destroySlot(slot);
|
|
85
108
|
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { BudgetCounter } from '../budget.ts';
|
|
2
|
+
/**
|
|
3
|
+
* The lit stage's material blocks, each kept in its slot for as long as later draws ask for the
|
|
4
|
+
* same numbers, so a material that has not changed since the last frame costs no upload at all.
|
|
5
|
+
*
|
|
6
|
+
* **Keyed by content, because nothing else names a material.** A material here is whatever the
|
|
7
|
+
* setters left in the block when a draw took it — a consumer never hands over an object that could
|
|
8
|
+
* carry an identity — so the block's own words are the key: hashed to find a bucket, compared word
|
|
9
|
+
* for word to accept a match, and bound by the slot's offset. What it gives up is a hash and a
|
|
10
|
+
* compare of a few hundred bytes per material change, against copying and uploading the whole
|
|
11
|
+
* block every frame, which a stage of four hundred materials measured at megabytes a frame.
|
|
12
|
+
*
|
|
13
|
+
* **A slot the frame has asked for is never given away within it**, for the reason `UniformRing`
|
|
14
|
+
* exists: a queue write does not interleave with recorded draws, so rewriting a slot a recorded draw
|
|
15
|
+
* reads would hand that draw the new numbers. A frame asking for more distinct blocks than there are
|
|
16
|
+
* slots is refused, counted and skipped, as the ring was. Across frames rewriting is safe, because a
|
|
17
|
+
* write issued after a submit is ordered after the commands in it.
|
|
18
|
+
*
|
|
19
|
+
* **What a new block displaces is a slot nobody asked for this frame or the last**, found by a hand
|
|
20
|
+
* that sweeps the slots in turn; only when every slot was asked for recently does it take one the
|
|
21
|
+
* last frame used. What would make that wrong is a scene whose distinct materials a frame approach
|
|
22
|
+
* the store's size: it then rewrites some of its steady materials every frame, which costs their
|
|
23
|
+
* upload and nothing else.
|
|
24
|
+
*/
|
|
25
|
+
export declare class MaterialStore {
|
|
26
|
+
private readonly device;
|
|
27
|
+
private readonly usage;
|
|
28
|
+
private readonly label;
|
|
29
|
+
private readonly budget;
|
|
30
|
+
/** Replaced by `growTo`, so every bind group over it is rebuilt when that says so. */
|
|
31
|
+
buffer: GPUBuffer;
|
|
32
|
+
/** Bytes per slot, rounded up to the alignment a dynamic offset requires. */
|
|
33
|
+
readonly slotSize: number;
|
|
34
|
+
private readonly words;
|
|
35
|
+
private readonly stride;
|
|
36
|
+
private capacity;
|
|
37
|
+
private staging;
|
|
38
|
+
private ints;
|
|
39
|
+
/** Per slot: its block's hash, the next slot in its bucket or -1, and whether it holds one. */
|
|
40
|
+
private hashes;
|
|
41
|
+
private next;
|
|
42
|
+
private held;
|
|
43
|
+
/** Per slot: the frame that last asked for it, and whether it was written since the last flush. */
|
|
44
|
+
private askedIn;
|
|
45
|
+
private pending;
|
|
46
|
+
/** Per slot: how many holders pinned it. A pinned slot is never displaced. */
|
|
47
|
+
private pins;
|
|
48
|
+
/**
|
|
49
|
+
* How many times the store has forgotten everything it held, which growth does: a holder of an
|
|
50
|
+
* offset compares this, because an offset from before is a slot of a buffer that is gone.
|
|
51
|
+
*/
|
|
52
|
+
epoch: number;
|
|
53
|
+
/** Per bucket: its first slot, or -1. A power of two at least twice the slots. */
|
|
54
|
+
private heads;
|
|
55
|
+
private mask;
|
|
56
|
+
private lowPending;
|
|
57
|
+
private highPending;
|
|
58
|
+
private hand;
|
|
59
|
+
/** Starts past zero, so a slot never asked for reads as long unasked. */
|
|
60
|
+
private frame;
|
|
61
|
+
constructor(device: GPUDevice, bytesPerBlock: number, slots: number, usage: number, label?: string, budget?: BudgetCounter | null);
|
|
62
|
+
/** How many distinct blocks a frame may hold. */
|
|
63
|
+
get slots(): number;
|
|
64
|
+
/** Start a frame: every slot the last one asked for may be displaced from here on, last. */
|
|
65
|
+
beginFrame(): void;
|
|
66
|
+
/**
|
|
67
|
+
* The byte offset of a slot holding exactly `block`, written there if no slot did, or null when
|
|
68
|
+
* every slot is one this frame has asked for. `block` is the whole block, word for word.
|
|
69
|
+
*/
|
|
70
|
+
place(block: Int32Array): number | null;
|
|
71
|
+
/** Upload every slot written since the last flush, one write per run of neighbouring slots. */
|
|
72
|
+
flush(): void;
|
|
73
|
+
/**
|
|
74
|
+
* Make room for at least `slots`, forgetting every block held, and say whether the buffer was
|
|
75
|
+
* replaced. Called where `UniformRing.growTo` is, at a frame's start, for the reasons it gives,
|
|
76
|
+
* and refused past the same ceiling.
|
|
77
|
+
*/
|
|
78
|
+
growTo(slots: number): boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Keep the block at `offset` where it is until `unpin`: a holder binding it by offset frame after
|
|
81
|
+
* frame — a list recorded once — whether or not anything asks for it. Counted, so two holders of
|
|
82
|
+
* one block each let go of their own pin.
|
|
83
|
+
*/
|
|
84
|
+
pin(offset: number): void;
|
|
85
|
+
unpin(offset: number): void;
|
|
86
|
+
dispose(): void;
|
|
87
|
+
private holds;
|
|
88
|
+
/**
|
|
89
|
+
* The slot a new block goes in: the first from the hand nobody asked for this frame or the last,
|
|
90
|
+
* else the first this frame has not asked for, else none. One sweep at most.
|
|
91
|
+
*/
|
|
92
|
+
private displace;
|
|
93
|
+
private unlink;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* MurmurHash3's 32-bit mix over the block's words. Not FNV, whose multiply carries a difference only
|
|
97
|
+
* upward: the bucket is the hash's low bits, and a material's words are mostly floats, whose low
|
|
98
|
+
* mantissa bits are zero for every value a person types — 0.5, 1, 2 — so blocks differing only in
|
|
99
|
+
* those would all have shared one bucket. The compare that follows a match is what decides.
|
|
100
|
+
*/
|
|
101
|
+
export declare function hashWords(block: Int32Array, words: number): number;
|