@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.
Files changed (231) hide show
  1. package/CHANGELOG.json +133 -11
  2. package/README.md +43 -38
  3. package/dist/index.d.ts +7 -4
  4. package/dist/index.js +5 -4
  5. package/dist/render/ambientHarmonics.d.ts +1 -1
  6. package/dist/render/ambientHarmonics.js +1 -1
  7. package/dist/render/areaLights.d.ts +4 -4
  8. package/dist/render/areaLights.js +1 -1
  9. package/dist/render/backend/api.d.ts +10 -3
  10. package/dist/render/backend/select.js +11 -5
  11. package/dist/render/backend/webgl2/boneAnimations.d.ts +29 -0
  12. package/dist/render/backend/webgl2/boneAnimations.js +80 -0
  13. package/dist/render/backend/webgl2/instanced.d.ts +5 -1
  14. package/dist/render/backend/webgl2/instanced.js +13 -1
  15. package/dist/render/backend/webgl2/renderer.d.ts +189 -4
  16. package/dist/render/backend/webgl2/renderer.js +614 -66
  17. package/dist/render/backend/webgl2/skinPaletteTexture.d.ts +5 -3
  18. package/dist/render/backend/webgl2/skinPaletteTexture.js +6 -4
  19. package/dist/render/backend/webgpu/boneAnimations.d.ts +34 -0
  20. package/dist/render/backend/webgpu/boneAnimations.js +76 -0
  21. package/dist/render/backend/webgpu/buffers.d.ts +24 -4
  22. package/dist/render/backend/webgpu/buffers.js +116 -59
  23. package/dist/render/backend/webgpu/clothKernels.d.ts +8 -0
  24. package/dist/render/backend/webgpu/clothKernels.js +76 -0
  25. package/dist/render/backend/webgpu/clothOps.d.ts +39 -0
  26. package/dist/render/backend/webgpu/clothOps.js +78 -0
  27. package/dist/render/backend/webgpu/clothSetLayout.d.ts +25 -0
  28. package/dist/render/backend/webgpu/clothSetLayout.js +84 -0
  29. package/dist/render/backend/webgpu/clothSetPack.d.ts +33 -0
  30. package/dist/render/backend/webgpu/clothSetPack.js +144 -0
  31. package/dist/render/backend/webgpu/compressedUpload.d.ts +0 -1
  32. package/dist/render/backend/webgpu/compressedUpload.js +53 -8
  33. package/dist/render/backend/webgpu/depthPass.d.ts +59 -3
  34. package/dist/render/backend/webgpu/depthPass.js +49 -14
  35. package/dist/render/backend/webgpu/drawCommand.d.ts +20 -4
  36. package/dist/render/backend/webgpu/drawCommand.js +11 -3
  37. package/dist/render/backend/webgpu/flatPass.d.ts +85 -42
  38. package/dist/render/backend/webgpu/flatPass.js +137 -20
  39. package/dist/render/backend/webgpu/gpuClothSet.d.ts +80 -0
  40. package/dist/render/backend/webgpu/gpuClothSet.js +462 -0
  41. package/dist/render/backend/webgpu/instanced.d.ts +14 -1
  42. package/dist/render/backend/webgpu/instanced.js +24 -1
  43. package/dist/render/backend/webgpu/latePass.js +1 -0
  44. package/dist/render/backend/webgpu/materialStore.d.ts +101 -0
  45. package/dist/render/backend/webgpu/materialStore.js +265 -0
  46. package/dist/render/backend/webgpu/motionPass.js +15 -6
  47. package/dist/render/backend/webgpu/pipelineCache.d.ts +28 -1
  48. package/dist/render/backend/webgpu/pipelineCache.js +46 -1
  49. package/dist/render/backend/webgpu/postPass.d.ts +1 -1
  50. package/dist/render/backend/webgpu/renderer.d.ts +200 -35
  51. package/dist/render/backend/webgpu/renderer.js +993 -168
  52. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.d.ts +9 -2
  53. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.js +121 -47
  54. package/dist/render/backend/webgpu/skinPaletteRing.d.ts +7 -3
  55. package/dist/render/backend/webgpu/skinPaletteRing.js +13 -7
  56. package/dist/render/backend/webgpu/skinPaletteTexture.d.ts +6 -0
  57. package/dist/render/backend/webgpu/skinPaletteTexture.js +7 -1
  58. package/dist/render/backend/webgpu/staticBundles.d.ts +125 -0
  59. package/dist/render/backend/webgpu/staticBundles.js +242 -0
  60. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +10 -3
  61. package/dist/render/backend/webgpu/surfaceTexturePass.js +20 -2
  62. package/dist/render/backend/webgpu/uniformRing.d.ts +21 -0
  63. package/dist/render/backend/webgpu/uniformRing.js +9 -1
  64. package/dist/render/backend/webgpu/vertexFormats.d.ts +22 -0
  65. package/dist/render/backend/webgpu/vertexFormats.js +97 -0
  66. package/dist/render/bloomChain.d.ts +3 -3
  67. package/dist/render/boneAnimation.d.ts +92 -0
  68. package/dist/render/boneAnimation.js +133 -0
  69. package/dist/render/compressedSource.d.ts +33 -13
  70. package/dist/render/compressedSource.js +70 -25
  71. package/dist/render/glCompressed.js +83 -0
  72. package/dist/render/globalMediumPass.d.ts +2 -0
  73. package/dist/render/globalMediumPass.js +1 -0
  74. package/dist/render/indexWidth.d.ts +17 -0
  75. package/dist/render/indexWidth.js +23 -0
  76. package/dist/render/instances.d.ts +26 -0
  77. package/dist/render/lightBudget.d.ts +10 -0
  78. package/dist/render/lightBudget.js +10 -0
  79. package/dist/render/lightMatrix.d.ts +11 -0
  80. package/dist/render/lightMatrix.js +14 -0
  81. package/dist/render/lightmap.d.ts +1 -1
  82. package/dist/render/mesh.d.ts +14 -0
  83. package/dist/render/mesh.js +44 -14
  84. package/dist/render/pointLightSelection.d.ts +4 -4
  85. package/dist/render/renderQuality.d.ts +30 -0
  86. package/dist/render/renderQuality.js +5 -0
  87. package/dist/render/rendererHarness.d.ts +7 -0
  88. package/dist/render/rendererHarness.js +12 -0
  89. package/dist/render/screenSpaceReflection.d.ts +2 -1
  90. package/dist/render/shader.d.ts +15 -0
  91. package/dist/render/shader.js +41 -0
  92. package/dist/render/shaders/boneAnimation.d.ts +19 -0
  93. package/dist/render/shaders/boneAnimation.js +63 -0
  94. package/dist/render/shaders/depth.d.ts +7 -0
  95. package/dist/render/shaders/depth.js +35 -10
  96. package/dist/render/shaders/flat/directionalShadow.js +84 -10
  97. package/dist/render/shaders/flat/glassTint.d.ts +1 -1
  98. package/dist/render/shaders/flat/glassTint.js +3 -0
  99. package/dist/render/shaders/flat/index.d.ts +21 -0
  100. package/dist/render/shaders/flat/index.js +42 -7
  101. package/dist/render/shaders/flat/layered.d.ts +19 -0
  102. package/dist/render/shaders/flat/layered.js +115 -0
  103. package/dist/render/shaders/flat/main.d.ts +1 -1
  104. package/dist/render/shaders/flat/main.js +58 -21
  105. package/dist/render/shaders/flat/mapReads.d.ts +10 -0
  106. package/dist/render/shaders/flat/mapReads.js +38 -0
  107. package/dist/render/shaders/flat/models.js +1 -1
  108. package/dist/render/shaders/flat/movingSun.d.ts +21 -0
  109. package/dist/render/shaders/flat/movingSun.js +46 -0
  110. package/dist/render/shaders/flat/normalMap.d.ts +6 -2
  111. package/dist/render/shaders/flat/normalMap.js +14 -3
  112. package/dist/render/shaders/flat/overlay.d.ts +3 -2
  113. package/dist/render/shaders/flat/overlay.js +42 -20
  114. package/dist/render/shaders/flat/pointShadow.js +2 -2
  115. package/dist/render/shaders/flat/preamble.js +59 -27
  116. package/dist/render/shaders/flat/worldUv.d.ts +22 -0
  117. package/dist/render/shaders/flat/worldUv.js +113 -0
  118. package/dist/render/shaders/generated/depth.wgsl.d.ts +93 -0
  119. package/dist/render/shaders/generated/depth.wgsl.js +93 -0
  120. package/dist/render/shaders/generated/flat.wgsl.d.ts +3616 -3042
  121. package/dist/render/shaders/generated/flat.wgsl.js +3688 -3113
  122. package/dist/render/shaders/generated/globalMedium.wgsl.d.ts +8 -3
  123. package/dist/render/shaders/generated/globalMedium.wgsl.js +8 -3
  124. package/dist/render/shaders/generated/lightVolume.wgsl.d.ts +15 -10
  125. package/dist/render/shaders/generated/lightVolume.wgsl.js +16 -11
  126. package/dist/render/shaders/globalMedium.js +9 -1
  127. package/dist/render/shaders/lightVolume.js +9 -1
  128. package/dist/render/shaders/outputTransform.d.ts +7 -0
  129. package/dist/render/shaders/outputTransform.js +28 -14
  130. package/dist/render/shaders/tangentFrame.d.ts +1 -1
  131. package/dist/render/shaders/tangentFrame.js +16 -3
  132. package/dist/render/shaders/vertexChannel.d.ts +7 -0
  133. package/dist/render/shaders/vertexChannel.js +26 -0
  134. package/dist/render/skinnedClothRun.d.ts +28 -0
  135. package/dist/render/skinnedClothRun.js +103 -35
  136. package/dist/render/staticDraws.d.ts +47 -0
  137. package/dist/render/staticDraws.js +43 -0
  138. package/dist/render/surfaceLayers.d.ts +39 -0
  139. package/dist/render/surfaceLayers.js +26 -0
  140. package/dist/render/surfaceOverlay.d.ts +16 -3
  141. package/dist/render/surfaceOverlay.js +11 -6
  142. package/dist/render/surfaceProjection.d.ts +34 -0
  143. package/dist/render/surfaceProjection.js +30 -0
  144. package/dist/render/surfaceTexture.d.ts +68 -7
  145. package/dist/render/surfaceTexture.js +56 -17
  146. package/dist/render/vertexPacking.d.ts +48 -0
  147. package/dist/render/vertexPacking.js +68 -0
  148. package/package.json +6 -6
  149. package/src/index.ts +12 -4
  150. package/src/render/ambientHarmonics.ts +1 -1
  151. package/src/render/areaLights.ts +4 -4
  152. package/src/render/backend/api.ts +13 -3
  153. package/src/render/backend/select.ts +10 -5
  154. package/src/render/backend/webgl2/boneAnimations.ts +105 -0
  155. package/src/render/backend/webgl2/instanced.ts +20 -1
  156. package/src/render/backend/webgl2/renderer.ts +723 -79
  157. package/src/render/backend/webgl2/skinPaletteTexture.ts +6 -4
  158. package/src/render/backend/webgpu/boneAnimations.ts +97 -0
  159. package/src/render/backend/webgpu/buffers.ts +195 -70
  160. package/src/render/backend/webgpu/clothKernels.ts +86 -0
  161. package/src/render/backend/webgpu/clothOps.ts +86 -0
  162. package/src/render/backend/webgpu/clothSetLayout.ts +102 -0
  163. package/src/render/backend/webgpu/clothSetPack.ts +197 -0
  164. package/src/render/backend/webgpu/compressedUpload.ts +59 -9
  165. package/src/render/backend/webgpu/depthPass.ts +51 -10
  166. package/src/render/backend/webgpu/drawCommand.ts +25 -4
  167. package/src/render/backend/webgpu/flatPass.ts +212 -41
  168. package/src/render/backend/webgpu/gpuClothSet.ts +564 -0
  169. package/src/render/backend/webgpu/instanced.ts +32 -1
  170. package/src/render/backend/webgpu/latePass.ts +1 -0
  171. package/src/render/backend/webgpu/materialStore.ts +276 -0
  172. package/src/render/backend/webgpu/motionPass.ts +18 -7
  173. package/src/render/backend/webgpu/pipelineCache.ts +53 -7
  174. package/src/render/backend/webgpu/renderer.ts +1201 -202
  175. package/src/render/backend/webgpu/shaders/clothSolve.wgsl.ts +121 -47
  176. package/src/render/backend/webgpu/skinPaletteRing.ts +13 -7
  177. package/src/render/backend/webgpu/skinPaletteTexture.ts +7 -1
  178. package/src/render/backend/webgpu/staticBundles.ts +364 -0
  179. package/src/render/backend/webgpu/surfaceTexturePass.ts +29 -3
  180. package/src/render/backend/webgpu/uniformRing.ts +10 -1
  181. package/src/render/backend/webgpu/vertexFormats.ts +113 -0
  182. package/src/render/bloomChain.ts +3 -3
  183. package/src/render/boneAnimation.ts +223 -0
  184. package/src/render/compressedSource.ts +121 -29
  185. package/src/render/glCompressed.ts +83 -0
  186. package/src/render/globalMediumPass.ts +7 -0
  187. package/src/render/indexWidth.ts +27 -0
  188. package/src/render/instances.ts +27 -0
  189. package/src/render/lightBudget.ts +11 -0
  190. package/src/render/lightMatrix.ts +15 -0
  191. package/src/render/lightmap.ts +1 -1
  192. package/src/render/mesh.ts +72 -14
  193. package/src/render/pointLightSelection.ts +4 -4
  194. package/src/render/renderQuality.ts +38 -0
  195. package/src/render/rendererHarness.ts +15 -0
  196. package/src/render/screenSpaceReflection.ts +2 -1
  197. package/src/render/shader.ts +59 -0
  198. package/src/render/shaders/boneAnimation.ts +63 -0
  199. package/src/render/shaders/depth.ts +45 -10
  200. package/src/render/shaders/flat/directionalShadow.ts +84 -10
  201. package/src/render/shaders/flat/glassTint.ts +7 -1
  202. package/src/render/shaders/flat/index.ts +63 -7
  203. package/src/render/shaders/flat/layered.ts +115 -0
  204. package/src/render/shaders/flat/main.ts +58 -21
  205. package/src/render/shaders/flat/mapReads.ts +38 -0
  206. package/src/render/shaders/flat/models.ts +1 -1
  207. package/src/render/shaders/flat/movingSun.ts +46 -0
  208. package/src/render/shaders/flat/normalMap.ts +14 -3
  209. package/src/render/shaders/flat/overlay.ts +42 -20
  210. package/src/render/shaders/flat/pointShadow.ts +2 -2
  211. package/src/render/shaders/flat/preamble.ts +59 -27
  212. package/src/render/shaders/flat/worldUv.ts +113 -0
  213. package/src/render/shaders/generated/depth.wgsl.ts +95 -0
  214. package/src/render/shaders/generated/flat.wgsl.ts +3688 -3113
  215. package/src/render/shaders/generated/globalMedium.wgsl.ts +8 -3
  216. package/src/render/shaders/generated/lightVolume.wgsl.ts +16 -11
  217. package/src/render/shaders/globalMedium.ts +9 -1
  218. package/src/render/shaders/lightVolume.ts +9 -1
  219. package/src/render/shaders/outputTransform.ts +30 -14
  220. package/src/render/shaders/tangentFrame.ts +16 -3
  221. package/src/render/shaders/vertexChannel.ts +27 -0
  222. package/src/render/skinnedClothRun.ts +138 -38
  223. package/src/render/staticDraws.ts +95 -0
  224. package/src/render/surfaceLayers.ts +57 -0
  225. package/src/render/surfaceOverlay.ts +24 -7
  226. package/src/render/surfaceProjection.ts +57 -0
  227. package/src/render/surfaceTexture.ts +124 -27
  228. package/src/render/vertexPacking.ts +109 -0
  229. package/dist/render/backend/webgpu/gpuSkinnedCloth.d.ts +0 -74
  230. package/dist/render/backend/webgpu/gpuSkinnedCloth.js +0 -452
  231. package/src/render/backend/webgpu/gpuSkinnedCloth.ts +0 -502
@@ -19,9 +19,11 @@ export declare class SkinPaletteTexture {
19
19
  /**
20
20
  * Upload a palette, reallocating only when the joint count changes.
21
21
  *
22
- * A rig's joint count is fixed for its life, so the reallocation path runs once per skeleton and
23
- * the steady state is one `texSubImage2D`. Reallocating per frame would be a GPU allocation in
24
- * the frame loop, which is the same class of mistake as allocating on the CPU there.
22
+ * **The texture grows and never shrinks**, so the steady state is one `texSubImage2D` whatever
23
+ * mix of rigs a frame draws. It was rebuilt whenever the joint count changed, which with one
24
+ * texture for every skinned draw meant a character and a prop drawn in turn reallocated it at
25
+ * every draw: a GPU allocation in the frame loop. The shader reads joints by index, so a texture
26
+ * wider than the rig is read exactly as one that fits.
25
27
  */
26
28
  update(gl: WebGL2RenderingContext, palette: Float32Array): void;
27
29
  /** Bind to a **vertex-stage** unit. See `SKIN_PALETTE_TEXTURE_UNIT` for why that is free. */
@@ -20,15 +20,17 @@ export class SkinPaletteTexture {
20
20
  /**
21
21
  * Upload a palette, reallocating only when the joint count changes.
22
22
  *
23
- * A rig's joint count is fixed for its life, so the reallocation path runs once per skeleton and
24
- * the steady state is one `texSubImage2D`. Reallocating per frame would be a GPU allocation in
25
- * the frame loop, which is the same class of mistake as allocating on the CPU there.
23
+ * **The texture grows and never shrinks**, so the steady state is one `texSubImage2D` whatever
24
+ * mix of rigs a frame draws. It was rebuilt whenever the joint count changed, which with one
25
+ * texture for every skinned draw meant a character and a prop drawn in turn reallocated it at
26
+ * every draw: a GPU allocation in the frame loop. The shader reads joints by index, so a texture
27
+ * wider than the rig is read exactly as one that fits.
26
28
  */
27
29
  update(gl, palette) {
28
30
  validateSkinPalette(palette);
29
31
  const joints = palette.length / 16;
30
32
  const width = paletteTextureWidth(joints);
31
- if (this.texture === null || this.joints !== joints) {
33
+ if (this.texture === null || joints > this.joints) {
32
34
  if (this.texture !== null)
33
35
  gl.deleteTexture(this.texture);
34
36
  const created = gl.createTexture();
@@ -0,0 +1,34 @@
1
+ /**
2
+ * A bone animation's textures on this backend, and a batch's clocks: what
3
+ * `shaders/boneAnimation.ts` reads by index, made once and never filtered.
4
+ *
5
+ * **`rgba32float` places and turns, and `rg32float` clocks**, all read with `textureLoad` and bound
6
+ * `unfilterable-float`, as the joint palette is: a turn in half floats moves a crowd's hands by a
7
+ * thousandth of their reach, a jitter at the frame's rate, and the bytes are a clip's, once.
8
+ */
9
+ import { type BoneAnimationHandle, type BoneAnimationTexels } from '../../boneAnimation.ts';
10
+ /** A clip on the device, from `createBoneAnimation`. */
11
+ export declare class GpuBoneAnimation implements BoneAnimationHandle {
12
+ readonly bones: number;
13
+ readonly frames: number;
14
+ readonly framesPerSecond: number;
15
+ readonly boneScale: number;
16
+ readonly placesView: GPUTextureView;
17
+ readonly turnsView: GPUTextureView;
18
+ disposed: boolean;
19
+ private readonly places;
20
+ private readonly turns;
21
+ constructor(device: GPUDevice, texels: BoneAnimationTexels);
22
+ dispose(): void;
23
+ }
24
+ /** A batch's clocks: two floats an instance, wrapped onto rows `CLOCK_TEXTURE_WIDTH` wide. */
25
+ export declare class GpuInstanceClocks {
26
+ readonly view: GPUTextureView;
27
+ readonly staging: Float32Array;
28
+ private readonly texture;
29
+ private readonly width;
30
+ constructor(device: GPUDevice, capacity: number, label: string);
31
+ /** The rows that hold the first `count` instances, from `staging`. */
32
+ upload(queue: GPUQueue, count: number): void;
33
+ dispose(): void;
34
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * A bone animation's textures on this backend, and a batch's clocks: what
3
+ * `shaders/boneAnimation.ts` reads by index, made once and never filtered.
4
+ *
5
+ * **`rgba32float` places and turns, and `rg32float` clocks**, all read with `textureLoad` and bound
6
+ * `unfilterable-float`, as the joint palette is: a turn in half floats moves a crowd's hands by a
7
+ * thousandth of their reach, a jitter at the frame's rate, and the bytes are a clip's, once.
8
+ */
9
+ import { CLOCK_TEXTURE_WIDTH, } from '../../boneAnimation.js';
10
+ const USAGE_TEXTURE = 0x4 | 0x2; // TEXTURE_BINDING | COPY_DST
11
+ /** A clip on the device, from `createBoneAnimation`. */
12
+ export class GpuBoneAnimation {
13
+ bones;
14
+ frames;
15
+ framesPerSecond;
16
+ boneScale;
17
+ placesView;
18
+ turnsView;
19
+ disposed = false;
20
+ places;
21
+ turns;
22
+ constructor(device, texels) {
23
+ this.bones = texels.bones;
24
+ this.frames = texels.frames;
25
+ this.framesPerSecond = texels.framesPerSecond;
26
+ this.boneScale = texels.boneScale;
27
+ const make = (label, data) => {
28
+ const texture = device.createTexture({
29
+ label,
30
+ size: [texels.bones, texels.frames],
31
+ format: 'rgba32float',
32
+ usage: USAGE_TEXTURE,
33
+ });
34
+ device.queue.writeTexture({ texture }, data, { bytesPerRow: texels.bones * 16, rowsPerImage: texels.frames }, [texels.bones, texels.frames]);
35
+ return texture;
36
+ };
37
+ this.places = make('bones.places', texels.places);
38
+ this.turns = make('bones.turns', texels.turns);
39
+ this.placesView = this.places.createView();
40
+ this.turnsView = this.turns.createView();
41
+ }
42
+ dispose() {
43
+ this.disposed = true;
44
+ this.places.destroy();
45
+ this.turns.destroy();
46
+ }
47
+ }
48
+ /** A batch's clocks: two floats an instance, wrapped onto rows `CLOCK_TEXTURE_WIDTH` wide. */
49
+ export class GpuInstanceClocks {
50
+ view;
51
+ staging;
52
+ texture;
53
+ width;
54
+ constructor(device, capacity, label) {
55
+ this.width = Math.max(1, Math.min(capacity, CLOCK_TEXTURE_WIDTH));
56
+ const rows = Math.max(1, Math.ceil(capacity / this.width));
57
+ this.staging = new Float32Array(this.width * rows * 2);
58
+ this.texture = device.createTexture({
59
+ label,
60
+ size: [this.width, rows],
61
+ format: 'rg32float',
62
+ usage: USAGE_TEXTURE,
63
+ });
64
+ this.view = this.texture.createView();
65
+ }
66
+ /** The rows that hold the first `count` instances, from `staging`. */
67
+ upload(queue, count) {
68
+ if (count <= 0)
69
+ return;
70
+ const rows = Math.ceil(count / this.width);
71
+ queue.writeTexture({ texture: this.texture }, this.staging, { bytesPerRow: this.width * 8, rowsPerImage: rows }, [this.width, rows]);
72
+ }
73
+ dispose() {
74
+ this.texture.destroy();
75
+ }
76
+ }
@@ -75,8 +75,13 @@ export interface GpuMesh {
75
75
  /** The second four influences' offsets, or -1 for a mesh with four. */
76
76
  readonly joints2: number;
77
77
  readonly weights2: number;
78
+ /** How the weights are read: floats, or sixteen-bit fixed point (`vertexPacking.ts`). */
79
+ readonly weightsFormat: GPUVertexFormat;
80
+ readonly weights2Format: GPUVertexFormat;
78
81
  } | null;
79
82
  readonly indexBuffer: GPUBuffer;
83
+ /** Sixteen or thirty-two bits an index: every pass that binds `indexBuffer` says which. */
84
+ readonly indexFormat: GPUIndexFormat;
80
85
  /**
81
86
  * How many indices the mesh draws. **Zero is a mesh with nothing to draw, and no pass draws it**:
82
87
  * a level can hold none of some kind of geometry, and a draw of zero indices does nothing but
@@ -156,18 +161,33 @@ export interface GpuMesh {
156
161
  * changed, reported at 2.8 MB a frame for one character. What the split costs is a pipeline per
157
162
  * layout, keyed by `:dynamic`, and two more buffers bound per draw of such a mesh.
158
163
  */
159
- readonly update: ((device: GPUDevice, positions: Float32Array, normals?: Float32Array, previous?: GPUBuffer | null) => void) | null;
164
+ readonly update: ((device: GPUDevice, positions: Float32Array, normals?: Float32Array, staged?: StagedRewrite | null) => void) | null;
160
165
  /**
161
- * A dynamic mesh's motion state, which the renderer owns: a buffer for last frame's positions,
162
- * made the first time a reconstructing renderer rewrites the mesh, and the frame of the last
163
- * rewrite. Null on a mesh created without `{ dynamic: true }`, which never moves a vertex.
166
+ * A dynamic mesh's motion state, which the renderer owns: a buffer for last frame's positions and
167
+ * one the new positions are staged in, each made the first time a reconstructing renderer
168
+ * rewrites the mesh, and the frame of the last rewrite. Null on a mesh created without
169
+ * `{ dynamic: true }`, which never moves a vertex.
164
170
  */
165
171
  readonly motion: {
166
172
  previous: GPUBuffer | null;
173
+ staging: GPUBuffer | null;
167
174
  changed: number;
168
175
  } | null;
169
176
  dispose(): void;
170
177
  }
178
+ /**
179
+ * A rewrite a reconstructing renderer stages rather than writes, so last frame's positions can be
180
+ * kept with no submit of the rewrite's own: the new positions are written into `staging`, and two
181
+ * copies are recorded into `encoder` — the live positions into `previous` first, where the frame's
182
+ * first rewrite keeps them, then the staged ones in. The renderer submits that encoder at the head
183
+ * of the frame's next submit, which is what keeps both copies ahead of every draw that reads the
184
+ * mesh. A `writeBuffer` straight to the live positions would land before the copy aside could run.
185
+ */
186
+ export interface StagedRewrite {
187
+ encoder: GPUCommandEncoder;
188
+ staging: GPUBuffer;
189
+ previous: GPUBuffer | null;
190
+ }
171
191
  /** A mesh on its way to the device, and the iterator that gets it there. */
172
192
  export interface IncrementalGpuMesh {
173
193
  /**
@@ -4,6 +4,9 @@ import { validateMeshData } from '../../mesh.js';
4
4
  import { ABSENT_ATTRIBUTE } from '../../vertexDefaults.js';
5
5
  import { UPLOAD_BYTES_PER_STEP } from '../../uploadStep.js';
6
6
  import { INSTANCE_STRIDE } from '../../instances.js';
7
+ import { compactIndices } from '../../indexWidth.js';
8
+ import { SNORM16_ONE, UNORM16_ONE, packingFor, sharedValue, vertexPackingOf, } from '../../vertexPacking.js';
9
+ import { formatBytes, interleaveFixed, packedFormat, packingIn } from './vertexFormats.js';
7
10
  /**
8
11
  * Mesh geometry as WebGPU buffers.
9
12
  *
@@ -160,13 +163,33 @@ export function createGpuMeshIncremental(device, data, dynamic = false) {
160
163
  const bounds = boundsOfPositions(data.positions, createBounds());
161
164
  const vertexCount = data.positions.length / 3;
162
165
  const layered = data.layers !== undefined;
163
- const supplied = VERTEX_LAYOUT.filter((attribute) => (!attribute.optional || data[attribute.name] !== undefined) &&
164
- !(dynamic && isStreamed(attribute))).map((attribute) => laidOut(attribute, layered));
166
+ /* How each attribute travels, which the pipeline key says too: `vertexPacking.ts`. */
167
+ const packing = vertexPackingOf(data, dynamic);
165
168
  /* Interleaved: one stride holding every attribute the mesh actually supplied — less a dynamic
166
- mesh's positions and normals, which are buffers of their own. */
167
- const stride = supplied.reduce((sum, attribute) => sum + attribute.components * 4, 0);
168
- const step = stride / 4;
169
- const interleaved = new Float32Array(step * vertexCount);
169
+ mesh's positions and normals, which are buffers of their own, and any attribute every vertex
170
+ shares, which is a constant. */
171
+ const fields = [];
172
+ let stride = 0;
173
+ for (const declared of VERTEX_LAYOUT) {
174
+ if (declared.optional && data[declared.name] === undefined)
175
+ continue;
176
+ if (dynamic && isStreamed(declared))
177
+ continue;
178
+ const attribute = laidOut(declared, layered);
179
+ const how = packingFor(packing, attribute.name);
180
+ if (how === 'constant')
181
+ continue;
182
+ const format = packedFormat(attribute.format, how);
183
+ fields.push({ attribute, packing: how, format, offset: stride });
184
+ stride += formatBytes(format);
185
+ }
186
+ const buffer = new ArrayBuffer(stride * vertexCount);
187
+ const rows = {
188
+ floats: new Float32Array(buffer),
189
+ signed: new Int16Array(buffer),
190
+ unsigned: new Uint16Array(buffer),
191
+ bytes: new Uint8Array(buffer),
192
+ };
170
193
  /*
171
194
  * **Refused here, in one sentence, rather than by the driver a hundred times.**
172
195
  *
@@ -181,7 +204,7 @@ export function createGpuMeshIncremental(device, data, dynamic = false) {
181
204
  * whose adapter caps at the default, and it deserves a sentence naming the model's size and
182
205
  * the device's rather than a wall of validation.
183
206
  */
184
- const byteLength = align4(interleaved.byteLength);
207
+ const byteLength = align4(buffer.byteLength);
185
208
  const ceiling = device.limits.maxBufferSize;
186
209
  if (typeof ceiling === 'number' && byteLength > ceiling) {
187
210
  throw new Error(`this mesh needs a ${Math.round(byteLength / 1024 / 1024)} MB vertex buffer and this GPU ` +
@@ -216,10 +239,20 @@ export function createGpuMeshIncremental(device, data, dynamic = false) {
216
239
  continue;
217
240
  absent.set(value, slot * (CONSTANT_SLOT / 4));
218
241
  }
242
+ /* And what every vertex shares, where a mesh's attribute is one value: `vertexPacking.ts`. */
243
+ for (let slot = 0; slot < VERTEX_LAYOUT.length; slot++) {
244
+ const attribute = VERTEX_LAYOUT[slot];
245
+ if (packingFor(packing, attribute.name) !== 'constant')
246
+ continue;
247
+ const values = data[attribute.name];
248
+ absent.set(sharedValue(values, attribute.components), slot * (CONSTANT_SLOT / 4));
249
+ }
219
250
  device.queue.writeBuffer(constants, 0, absent);
251
+ /* Sixteen bits where every vertex fits, which every draw of this mesh says: indexWidth.ts. */
252
+ const indices = compactIndices(data.indices, vertexCount);
220
253
  const indexBuffer = device.createBuffer({
221
254
  label: 'mesh.indices',
222
- size: align4(data.indices.byteLength),
255
+ size: align4(indices.byteLength),
223
256
  usage: USAGE.INDEX | USAGE.COPY_DST,
224
257
  });
225
258
  /* A dynamic mesh's two moving streams: copied from as well as written, for last frame's positions. */
@@ -243,10 +276,11 @@ export function createGpuMeshIncremental(device, data, dynamic = false) {
243
276
  * buffer never holds a torn one even for the frame between two steps.
244
277
  */
245
278
  const verticesPerStep = Math.max(1, Math.floor(UPLOAD_BYTES_PER_STEP / (stride + (dynamic ? 24 : 0))));
246
- const indexWidth = data.indices.BYTES_PER_ELEMENT;
247
- const indicesPerStep = Math.max(1, Math.floor(UPLOAD_BYTES_PER_STEP / indexWidth));
279
+ /* Whole four-byte words a step, which is all a queue writes: two sixteen-bit indices to one. */
280
+ const indexWidth = indices.BYTES_PER_ELEMENT;
281
+ const indicesPerStep = Math.max(1, Math.floor(UPLOAD_BYTES_PER_STEP / 4)) * (4 / indexWidth);
248
282
  const totalSteps = Math.max(1, Math.ceil(vertexCount / verticesPerStep)) +
249
- Math.max(1, Math.ceil(data.indices.length / indicesPerStep));
283
+ Math.max(1, Math.ceil(indices.length / indicesPerStep));
250
284
  /*
251
285
  * **The mesh is assembled out here, by functions of its own, and that is the fix for a copy
252
286
  * nobody named.** It was an object literal in this scope, and its `complete` getter shared this
@@ -262,6 +296,7 @@ export function createGpuMeshIncremental(device, data, dynamic = false) {
262
296
  constants,
263
297
  streams,
264
298
  indexBuffer,
299
+ indexFormat: indices instanceof Uint16Array ? 'uint16' : 'uint32',
265
300
  stride: streams === null ? stride : 12,
266
301
  indexCount: data.indices.length,
267
302
  bounds,
@@ -271,7 +306,7 @@ export function createGpuMeshIncremental(device, data, dynamic = false) {
271
306
  isSkinnedEight: data.joints !== undefined && data.joints2 !== undefined,
272
307
  vertexCount,
273
308
  hasUvs: data.uvs !== undefined,
274
- skinOffsets: streams === null ? skinOffsetsOf(supplied) : null,
309
+ skinOffsets: streams === null ? skinOffsetsOf(fields) : null,
275
310
  hasChannel: data.channel !== undefined,
276
311
  morph: data.morphTargets === undefined || data.morphTargetCount === undefined
277
312
  ? null
@@ -282,14 +317,14 @@ export function createGpuMeshIncremental(device, data, dynamic = false) {
282
317
  });
283
318
  const job = {
284
319
  data,
285
- interleaved,
320
+ indices,
321
+ rows,
286
322
  device,
287
- supplied,
323
+ fields,
288
324
  streams,
289
325
  vertices,
290
326
  indexBuffer,
291
327
  vertexCount,
292
- step,
293
328
  stride,
294
329
  verticesPerStep,
295
330
  indicesPerStep,
@@ -299,26 +334,35 @@ export function createGpuMeshIncremental(device, data, dynamic = false) {
299
334
  return { mesh, upload: uploadSteps(job) };
300
335
  }
301
336
  function* uploadSteps(job) {
302
- const { device, supplied, vertices, indexBuffer, vertexCount, step, stride } = job;
337
+ const { device, fields, vertices, indexBuffer, vertexCount, stride } = job;
338
+ /* Floats and halves a vertex: every field is whole words, so both divide. */
339
+ const step = stride / 4;
340
+ const halves = stride / 2;
303
341
  let taken = 0;
304
342
  for (let from = 0; from < vertexCount; from += job.verticesPerStep) {
305
343
  const data = job.data;
306
- const interleaved = job.interleaved;
344
+ const rows = job.rows;
307
345
  const to = Math.min(from + job.verticesPerStep, vertexCount);
308
- let fieldOffset = 0;
309
- for (const attribute of supplied) {
346
+ for (const field of fields) {
347
+ const attribute = field.attribute;
310
348
  const source = data[attribute.name];
311
- if (attribute === LAYERED_UVS) {
349
+ if (field.packing === 'snorm16') {
350
+ interleaveFixed(rows.signed, source, attribute.components, halves, field.offset / 2, from, to, SNORM16_ONE);
351
+ }
352
+ else if (field.packing === 'unorm16') {
353
+ interleaveFixed(rows.unsigned, source, attribute.components, halves, field.offset / 2, from, to, UNORM16_ONE);
354
+ }
355
+ else if (attribute === LAYERED_UVS) {
312
356
  /* (u, v) from the coordinates and the layer after them, one field of three. */
313
- interleaveField(interleaved, source, 2, step, fieldOffset, from, to);
314
- interleaveField(interleaved, data.layers, 1, step, fieldOffset + 2, from, to);
357
+ const at = field.offset / 4;
358
+ interleaveField(rows.floats, source, 2, step, at, from, to);
359
+ interleaveField(rows.floats, data.layers, 1, step, at + 2, from, to);
315
360
  }
316
361
  else {
317
- interleaveField(interleaved, source, attribute.components, step, fieldOffset, from, to);
362
+ interleaveField(rows.floats, source, attribute.components, step, field.offset / 4, from, to);
318
363
  }
319
- fieldOffset += attribute.components;
320
364
  }
321
- device.queue.writeBuffer(vertices, from * stride, interleaved, from * step, (to - from) * step);
365
+ device.queue.writeBuffer(vertices, from * stride, rows.bytes, from * stride, (to - from) * stride);
322
366
  if (job.streams !== null) {
323
367
  const count = (to - from) * 3;
324
368
  device.queue.writeBuffer(job.streams.positions, from * 12, data.positions, from * 3, count);
@@ -328,7 +372,7 @@ function* uploadSteps(job) {
328
372
  if (taken < job.totalSteps)
329
373
  yield;
330
374
  }
331
- const indices = job.data.indices;
375
+ const indices = job.indices;
332
376
  const indexWidth = indices.BYTES_PER_ELEMENT;
333
377
  for (let from = 0; from < indices.length; from += job.indicesPerStep) {
334
378
  const to = Math.min(from + job.indicesPerStep, indices.length);
@@ -338,7 +382,8 @@ function* uploadSteps(job) {
338
382
  yield;
339
383
  }
340
384
  job.data = null;
341
- job.interleaved = null;
385
+ job.indices = null;
386
+ job.rows = null;
342
387
  job.progress.uploaded = true;
343
388
  }
344
389
  /**
@@ -383,32 +428,39 @@ function interleaveField(rows, source, width, step, fieldOffset, from, to) {
383
428
  * Where the joints and weights sit in a vertex of `supplied`, the attributes a mesh interleaves in
384
429
  * layout order; null unless it supplied both.
385
430
  */
386
- function skinOffsetsOf(supplied) {
387
- let floats = 0;
431
+ function skinOffsetsOf(fields) {
388
432
  let joints = -1;
389
433
  let weights = -1;
390
434
  let joints2 = -1;
391
435
  let weights2 = -1;
392
- for (const attribute of supplied) {
393
- if (attribute.name === 'joints')
394
- joints = floats * 4;
395
- if (attribute.name === 'weights')
396
- weights = floats * 4;
397
- if (attribute.name === 'joints2')
398
- joints2 = floats * 4;
399
- if (attribute.name === 'weights2')
400
- weights2 = floats * 4;
401
- floats += attribute.components;
436
+ let weightsFormat = 'float32x4';
437
+ let weights2Format = 'float32x4';
438
+ for (const field of fields) {
439
+ const name = field.attribute.name;
440
+ if (name === 'joints')
441
+ joints = field.offset;
442
+ if (name === 'joints2')
443
+ joints2 = field.offset;
444
+ if (name === 'weights') {
445
+ weights = field.offset;
446
+ weightsFormat = field.format;
447
+ }
448
+ if (name === 'weights2') {
449
+ weights2 = field.offset;
450
+ weights2Format = field.format;
451
+ }
402
452
  }
403
453
  if (joints < 0 || weights < 0)
404
454
  return null;
405
455
  return joints2 >= 0 && weights2 >= 0
406
- ? { joints, weights, joints2, weights2 }
407
- : { joints, weights, joints2: -1, weights2: -1 };
456
+ ? { joints, weights, joints2, weights2, weightsFormat, weights2Format }
457
+ : { joints, weights, joints2: -1, weights2: -1, weightsFormat, weights2Format };
408
458
  }
409
459
  /** The handle, made where it can see only what it hands out. */
410
460
  function gpuMeshOf(parts) {
411
- const motion = parts.update === null ? null : { previous: null, changed: -1 };
461
+ const motion = parts.update === null
462
+ ? null
463
+ : { previous: null, staging: null, changed: -1 };
412
464
  const { vertices, constants, streams, indexBuffer, progress } = parts;
413
465
  return {
414
466
  vertexBuffers: streams === null
@@ -417,6 +469,7 @@ function gpuMeshOf(parts) {
417
469
  vertexStride: parts.stride,
418
470
  skinOffsets: parts.skinOffsets,
419
471
  indexBuffer,
472
+ indexFormat: parts.indexFormat,
420
473
  indexCount: parts.indexCount,
421
474
  bounds: parts.bounds,
422
475
  get complete() {
@@ -433,6 +486,7 @@ function gpuMeshOf(parts) {
433
486
  motion,
434
487
  dispose() {
435
488
  motion?.previous?.destroy();
489
+ motion?.staging?.destroy();
436
490
  streams?.positions.destroy();
437
491
  streams?.normals.destroy();
438
492
  vertices.destroy();
@@ -447,12 +501,12 @@ function isStreamed(attribute) {
447
501
  }
448
502
  /**
449
503
  * A dynamic mesh's `update`: the caller's positions, and its normals where it has them, each
450
- * written to its own buffer as it came. Last frame's positions, where a reconstruction wants them,
451
- * are copied on the device before the write — a copy submitted ahead of a `writeBuffer` runs ahead
452
- * of it on the queue — so no CPU copy of anything is kept.
504
+ * written to its own buffer as it came — or, where a reconstruction keeps last frame's positions,
505
+ * the positions staged and copied in on the device behind the copy aside (`StagedRewrite`), so no
506
+ * CPU copy of anything is kept and the rewrite submits nothing of its own.
453
507
  */
454
508
  function streamUpdate(positionsBuffer, normalsBuffer, streamBytes, bounds, vertexCount) {
455
- return (target, positions, normals, previous = null) => {
509
+ return (target, positions, normals, staged = null) => {
456
510
  for (const [name, values] of [
457
511
  ['update', positions],
458
512
  ['normals', normals],
@@ -463,15 +517,19 @@ function streamUpdate(positionsBuffer, normalsBuffer, streamBytes, bounds, verte
463
517
  "A mesh's vertex count is fixed at creation: the index buffer, the pipeline and " +
464
518
  'every other attribute are sized against it.');
465
519
  }
466
- if (previous !== null) {
467
- const encoder = target.createCommandEncoder({ label: 'mesh.previousPositions' });
468
- encoder.copyBufferToBuffer(positionsBuffer, 0, previous, 0, streamBytes);
469
- target.queue.submit([encoder.finish()]);
470
- }
471
520
  /* Everything that decides whether this is on screen starts from the bounds, so a cloth
472
521
  that blew sideways out of its original box would be culled while still visible. */
473
522
  boundsOfPositions(positions, bounds);
474
- target.queue.writeBuffer(positionsBuffer, 0, positions);
523
+ if (staged === null) {
524
+ target.queue.writeBuffer(positionsBuffer, 0, positions);
525
+ }
526
+ else {
527
+ target.queue.writeBuffer(staged.staging, 0, positions);
528
+ if (staged.previous !== null) {
529
+ staged.encoder.copyBufferToBuffer(positionsBuffer, 0, staged.previous, 0, streamBytes);
530
+ }
531
+ staged.encoder.copyBufferToBuffer(staged.staging, 0, positionsBuffer, 0, streamBytes);
532
+ }
475
533
  if (normals !== undefined)
476
534
  target.queue.writeBuffer(normalsBuffer, 0, normals);
477
535
  };
@@ -546,7 +604,9 @@ instanced = false) {
546
604
  * in the interleaved one. A *skinned* mesh interleaves them, so removing them would shift
547
605
  * the stride under every other attribute — which is why `createInstanced` refuses one.
548
606
  */
549
- const supplied = !attribute.optional || present[String(attribute.name)] === true;
607
+ /* One value every vertex shares sits with the absent ones: `vertexPacking.ts`. */
608
+ const packing = packingIn(present, String(attribute.name));
609
+ const supplied = (!attribute.optional || present[String(attribute.name)] === true) && packing !== 'constant';
550
610
  if (instanced && INSTANCE_LOCATIONS.has(attribute.shaderLocation)) {
551
611
  /*
552
612
  * **Free to drop only because the mesh does not supply it.** An absent optional attribute
@@ -566,12 +626,9 @@ instanced = false) {
566
626
  return;
567
627
  }
568
628
  if (supplied) {
569
- interleaved.push({
570
- shaderLocation: attribute.shaderLocation,
571
- offset,
572
- format: attribute.format,
573
- });
574
- offset += attribute.components * 4;
629
+ const format = packedFormat(attribute.format, packing);
630
+ interleaved.push({ shaderLocation: attribute.shaderLocation, offset, format });
631
+ offset += formatBytes(format);
575
632
  return;
576
633
  }
577
634
  constants.push({
@@ -0,0 +1,8 @@
1
+ export declare const CLOTH_ENTRIES: readonly ['clothPose', 'clothRest', 'clothBegin', 'clothPredict', 'clothDistance', 'clothBending', 'clothTether', 'clothLimit', 'clothFinish', 'clothBlend', 'clothPublish'];
2
+ export type ClothEntry = (typeof CLOTH_ENTRIES)[number];
3
+ export interface ClothKernels {
4
+ readonly layout: GPUBindGroupLayout;
5
+ readonly outputLayout: GPUBindGroupLayout;
6
+ readonly pipelines: Readonly<Record<ClothEntry, GPUComputePipeline>>;
7
+ }
8
+ export declare function clothKernelsFor(device: GPUDevice): ClothKernels;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * The cloth solver's layouts and one compute pipeline an entry point of `shaders/clothSolve.wgsl.ts`,
3
+ * built once a device and shared by every set of garments on it.
4
+ */
5
+ import { CLOTH_SOLVE_WGSL } from './shaders/clothSolve.wgsl.js';
6
+ import { shaderModule } from './shaderModules.js';
7
+ const COMPUTE = 0x4;
8
+ export const CLOTH_ENTRIES = [
9
+ 'clothPose',
10
+ 'clothRest',
11
+ 'clothBegin',
12
+ 'clothPredict',
13
+ 'clothDistance',
14
+ 'clothBending',
15
+ 'clothTether',
16
+ 'clothLimit',
17
+ 'clothFinish',
18
+ 'clothBlend',
19
+ 'clothPublish',
20
+ ];
21
+ const kernelsByDevice = new WeakMap();
22
+ export function clothKernelsFor(device) {
23
+ const held = kernelsByDevice.get(device);
24
+ if (held !== undefined)
25
+ return held;
26
+ const uniform = (binding, dynamic) => ({
27
+ binding,
28
+ visibility: COMPUTE,
29
+ buffer: { type: 'uniform', hasDynamicOffset: dynamic },
30
+ });
31
+ const storage = (binding, type) => ({
32
+ binding,
33
+ visibility: COMPUTE,
34
+ buffer: { type },
35
+ });
36
+ /* Seven storage buffers of the eight a stage is granted: the shader's header says why so few. */
37
+ const layout = device.createBindGroupLayout({
38
+ label: 'cloth.solve',
39
+ entries: [
40
+ uniform(0, false),
41
+ uniform(1, true),
42
+ uniform(2, true),
43
+ storage(3, 'storage'),
44
+ storage(4, 'storage'),
45
+ storage(5, 'read-only-storage'),
46
+ storage(6, 'read-only-storage'),
47
+ storage(7, 'storage'),
48
+ storage(8, 'read-only-storage'),
49
+ storage(9, 'read-only-storage'),
50
+ ],
51
+ });
52
+ const outputLayout = device.createBindGroupLayout({
53
+ label: 'cloth.publish',
54
+ entries: [
55
+ {
56
+ binding: 0,
57
+ visibility: COMPUTE,
58
+ storageTexture: { access: 'write-only', format: 'rgba32float' },
59
+ },
60
+ ],
61
+ });
62
+ const module = shaderModule(device, { label: 'cloth.solve', code: CLOTH_SOLVE_WGSL });
63
+ const solve = device.createPipelineLayout({ bindGroupLayouts: [layout] });
64
+ const publish = device.createPipelineLayout({ bindGroupLayouts: [layout, outputLayout] });
65
+ const pipelines = {};
66
+ for (const entry of CLOTH_ENTRIES) {
67
+ pipelines[entry] = device.createComputePipeline({
68
+ label: `cloth.${entry}`,
69
+ layout: entry === 'clothPublish' ? publish : solve,
70
+ compute: { module, entryPoint: entry },
71
+ });
72
+ }
73
+ const kernels = { layout, outputLayout, pipelines };
74
+ kernelsByDevice.set(device, kernels);
75
+ return kernels;
76
+ }