@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
@@ -0,0 +1,125 @@
1
+ /**
2
+ * A static list on this backend: its draws recorded once, through the renderer's own draw path,
3
+ * into slots the list owns, and replayed as one render bundle per view.
4
+ *
5
+ * **What a recording holds is the frame's draws as the renderer would have recorded them**, taken
6
+ * from a pool of the list's instead of the frame graph's: a pipeline, a bind group, vertex buffers
7
+ * and four dynamic offsets each. Three of the four never change once recorded — the draw's own
8
+ * slot in `entries`, written once, and its material's slot in the store, pinned. The other two are
9
+ * the view's: the pass block and the view block, which differ between the frame, a mirror and a
10
+ * capture of the same frame. So each view a frame draws the list into takes a *copy* — a slot of
11
+ * `passes` and one of `views`, written for that view — and a bundle is encoded per copy from the
12
+ * same commands with that copy's two offsets. A frame that draws a list twice uploads two blocks
13
+ * and executes two bundles, and records nothing.
14
+ *
15
+ * **Its own buffers, and bind groups over them.** A bundle bakes the buffers its groups bind, and
16
+ * the frame's rings are reset every frame, so a list binds rings of its own: the renderer swaps
17
+ * them in while it records the list's draws, and the groups that recording builds go into the
18
+ * list's cache rather than the frame's.
19
+ *
20
+ * **Recorded again when what it baked has moved**: the renderer's groups (a texture forgotten, a
21
+ * frame texture rebound, a ring grown), the pipeline cache's lit set, the store's slots, or the
22
+ * list's own rings. Each is a number compared at replay, so a stale recording is never replayed.
23
+ *
24
+ * What it gives up: culling, since a bundle draws everything recorded into it; and memory, a pass
25
+ * block a view a list, which is a few kilobytes.
26
+ */
27
+ import type { StaticDrawsList, StaticEntry } from '../../staticDraws.ts';
28
+ import type { CommandPool, DrawCommand } from './drawCommand.ts';
29
+ import type { PipelineCache } from './pipelineCache.ts';
30
+ import { UniformRing } from './uniformRing.ts';
31
+ /** What one recording was made against, and what it recorded. */
32
+ export interface StaticRecording {
33
+ readonly cache: PipelineCache;
34
+ /** The material block's words the list started from: the pass's state, which entries patch. */
35
+ readonly start: Int32Array;
36
+ readonly startHash: number;
37
+ readonly cutout: string;
38
+ readonly surfaceKept: boolean;
39
+ /** Stamps compared at replay; any that moved makes the recording stale. */
40
+ cacheVersion: number;
41
+ groupEpoch: number;
42
+ storeEpoch: number;
43
+ ringEpoch: number;
44
+ /** The frame pass's commands, and the reflection pass's surface halves, with the record's offsets. */
45
+ readonly frame: CommandPool;
46
+ readonly surface: CommandPool;
47
+ /** Material slots pinned for these commands, and the material changes recording them asked for. */
48
+ readonly pins: number[];
49
+ materialAsks: number;
50
+ /** Under the screen-space skin blur a skin entry splits into halves no bundle replays. */
51
+ immediate: boolean;
52
+ /** Per copy, the bundles `executeBundles` takes; null until a copy is first replayed. */
53
+ readonly frameBundles: (readonly GPURenderBundle[] | null)[];
54
+ readonly surfaceBundles: (readonly GPURenderBundle[] | null)[];
55
+ /** The frame it was last replayed in, which picks the one to let go. */
56
+ usedIn: number;
57
+ }
58
+ export declare class GpuStaticDraws implements StaticDrawsList {
59
+ private readonly device;
60
+ readonly list: StaticDrawsList;
61
+ private readonly passSize;
62
+ private readonly viewSize;
63
+ disposed: boolean;
64
+ /** The draws' own slots, one a draw, written by the recording. */
65
+ readonly drawRing: UniformRing;
66
+ /** A pass block and a view block a copy. */
67
+ passes: UniformRing;
68
+ views: UniformRing;
69
+ /** Bumped whenever the three rings are made again, which invalidates every recording. */
70
+ ringEpoch: number;
71
+ /** The list's own lit groups and blank group, built over its rings by the renderer. */
72
+ groups: Map<unknown, unknown>;
73
+ blank: GPUBindGroup | null;
74
+ /** The group epoch the cache above was built in. */
75
+ groupEpoch: number;
76
+ /** The store epoch the list's pins were taken in: pins from before a forgetting are let go of. */
77
+ pinEpoch: number;
78
+ readonly recordings: StaticRecording[];
79
+ private copies;
80
+ private copyFrame;
81
+ constructor(device: GPUDevice, list: StaticDrawsList, passSize: number, viewSize: number, drawSize: number);
82
+ /** The captured entries, which the renderer replays to record them. */
83
+ get entries(): readonly StaticEntry[];
84
+ get draws(): number;
85
+ /**
86
+ * The next view's copy in frame `frame`, growing the copies when a frame asks for more than there
87
+ * are — which makes every recording stale, since the groups bound the old rings.
88
+ */
89
+ takeCopy(frame: number): number;
90
+ /** The two offsets copy `copy` binds. */
91
+ passOffset(copy: number): number;
92
+ viewOffset(copy: number): number;
93
+ /** Copy `copy`'s pass block, written whole. Uploaded by `uploadCopy`. */
94
+ writePass(copy: number, block: Int32Array): void;
95
+ /** Copy `copy`'s two blocks to the device, and no other copy's: each view writes only its own. */
96
+ uploadCopy(copy: number): void;
97
+ /** The draws' own slots, after a recording wrote them. */
98
+ uploadDraws(): void;
99
+ /** A recording that matches, if one does: the same target, starting state and side passes. */
100
+ find(cache: PipelineCache, start: Int32Array, startHash: number, cutout: string, surfaceKept: boolean): StaticRecording | null;
101
+ /** A new, empty recording, letting the least recently used go past the most kept. */
102
+ open(cache: PipelineCache, start: Int32Array, startHash: number, cutout: string, surfaceKept: boolean, unpin: (offset: number) => void): StaticRecording;
103
+ /** Empty a recording to record it again: its pins let go, its commands and bundles dropped. */
104
+ clear(recording: StaticRecording, unpin: (offset: number) => void): void;
105
+ /** Every recording, let go; what disposal and a forgetting store both do. */
106
+ forget(unpin: ((offset: number) => void) | null): void;
107
+ dispose(unpin: ((offset: number) => void) | null): void;
108
+ private growCopies;
109
+ /** Every copy's slot, taken once: a copy is a fixed offset, never handed out again. */
110
+ private claimCopies;
111
+ }
112
+ /** A command the recording takes in place of the frame graph's. */
113
+ export declare function takeRecorded(pool: CommandPool): DrawCommand;
114
+ /** What a render bundle must agree with: the pass it is executed in. */
115
+ export interface BundleTarget {
116
+ readonly colorFormats: (GPUTextureFormat | null)[];
117
+ readonly depthStencilFormat: GPUTextureFormat;
118
+ readonly sampleCount: number;
119
+ readonly depthReadOnly: boolean;
120
+ }
121
+ /**
122
+ * One copy's bundle of a pool's commands: each lit command as recorded but for the pass and view
123
+ * offsets, which are the copy's. Allocates, and runs once a copy a recording, never per frame.
124
+ */
125
+ export declare function encodeBundle(device: GPUDevice, target: BundleTarget, pool: CommandPool, passOffset: number, viewOffset: number, label: string): readonly GPURenderBundle[];
@@ -0,0 +1,242 @@
1
+ import { createCommandPool, resetPool, takeCommand } from './drawCommand.js';
2
+ import { UniformRing } from './uniformRing.js';
3
+ const USAGE_UNIFORM_DST = 0x0040 | 0x0008;
4
+ /** Views a list starts with room for: the frame's and one capture's. Doubles when a frame asks more. */
5
+ const FIRST_COPIES = 2;
6
+ /** Recordings a list keeps before letting the oldest go: a target and a starting state each. */
7
+ const MOST_RECORDINGS = 4;
8
+ export class GpuStaticDraws {
9
+ device;
10
+ list;
11
+ passSize;
12
+ viewSize;
13
+ disposed = false;
14
+ /** The draws' own slots, one a draw, written by the recording. */
15
+ drawRing;
16
+ /** A pass block and a view block a copy. */
17
+ passes;
18
+ views;
19
+ /** Bumped whenever the three rings are made again, which invalidates every recording. */
20
+ ringEpoch = 0;
21
+ /** The list's own lit groups and blank group, built over its rings by the renderer. */
22
+ groups = new Map();
23
+ blank = null;
24
+ /** The group epoch the cache above was built in. */
25
+ groupEpoch = -1;
26
+ /** The store epoch the list's pins were taken in: pins from before a forgetting are let go of. */
27
+ pinEpoch = -1;
28
+ recordings = [];
29
+ copies = 0;
30
+ copyFrame = -1;
31
+ constructor(device, list, passSize, viewSize, drawSize) {
32
+ this.device = device;
33
+ this.list = list;
34
+ this.passSize = passSize;
35
+ this.viewSize = viewSize;
36
+ this.drawRing = new UniformRing(device, drawSize, Math.max(1, list.draws), USAGE_UNIFORM_DST, 'static.draws');
37
+ this.passes = new UniformRing(device, passSize, FIRST_COPIES, USAGE_UNIFORM_DST, 'static.passes');
38
+ this.views = new UniformRing(device, viewSize, FIRST_COPIES, USAGE_UNIFORM_DST, 'static.views');
39
+ this.claimCopies();
40
+ }
41
+ /** The captured entries, which the renderer replays to record them. */
42
+ get entries() {
43
+ return this.list.entries;
44
+ }
45
+ get draws() {
46
+ return this.list.draws;
47
+ }
48
+ /**
49
+ * The next view's copy in frame `frame`, growing the copies when a frame asks for more than there
50
+ * are — which makes every recording stale, since the groups bound the old rings.
51
+ */
52
+ takeCopy(frame) {
53
+ if (frame !== this.copyFrame) {
54
+ this.copyFrame = frame;
55
+ this.copies = 0;
56
+ }
57
+ const copy = this.copies;
58
+ this.copies += 1;
59
+ if (copy >= this.passes.slots)
60
+ this.growCopies(this.passes.slots * 2);
61
+ return copy;
62
+ }
63
+ /** The two offsets copy `copy` binds. */
64
+ passOffset(copy) {
65
+ return copy * this.passes.slotSize;
66
+ }
67
+ viewOffset(copy) {
68
+ return copy * this.views.slotSize;
69
+ }
70
+ /** Copy `copy`'s pass block, written whole. Uploaded by `uploadCopy`. */
71
+ writePass(copy, block) {
72
+ this.passes.writeBlock(this.passOffset(copy), block);
73
+ }
74
+ /** Copy `copy`'s two blocks to the device, and no other copy's: each view writes only its own. */
75
+ uploadCopy(copy) {
76
+ this.passes.uploadSlot(this.passOffset(copy));
77
+ this.views.uploadSlot(this.viewOffset(copy));
78
+ }
79
+ /** The draws' own slots, after a recording wrote them. */
80
+ uploadDraws() {
81
+ this.drawRing.flush();
82
+ }
83
+ /** A recording that matches, if one does: the same target, starting state and side passes. */
84
+ find(cache, start, startHash, cutout, surfaceKept) {
85
+ for (const recording of this.recordings) {
86
+ if (recording.cache === cache &&
87
+ recording.startHash === startHash &&
88
+ recording.cutout === cutout &&
89
+ recording.surfaceKept === surfaceKept &&
90
+ sameWords(recording.start, start)) {
91
+ return recording;
92
+ }
93
+ }
94
+ return null;
95
+ }
96
+ /** A new, empty recording, letting the least recently used go past the most kept. */
97
+ open(cache, start, startHash, cutout, surfaceKept, unpin) {
98
+ if (this.recordings.length >= MOST_RECORDINGS) {
99
+ let oldest = 0;
100
+ for (let i = 1; i < this.recordings.length; i++) {
101
+ if (this.recordings[i].usedIn <
102
+ this.recordings[oldest].usedIn)
103
+ oldest = i;
104
+ }
105
+ const [gone] = this.recordings.splice(oldest, 1);
106
+ if (gone !== undefined)
107
+ release(gone, unpin);
108
+ }
109
+ const recording = {
110
+ cache,
111
+ start: new Int32Array(start),
112
+ startHash,
113
+ cutout,
114
+ surfaceKept,
115
+ cacheVersion: -1,
116
+ groupEpoch: -1,
117
+ storeEpoch: -1,
118
+ ringEpoch: -1,
119
+ frame: createCommandPool(this.list.draws),
120
+ surface: createCommandPool(0),
121
+ pins: [],
122
+ materialAsks: 0,
123
+ immediate: false,
124
+ frameBundles: [],
125
+ surfaceBundles: [],
126
+ usedIn: -1,
127
+ };
128
+ this.recordings.push(recording);
129
+ return recording;
130
+ }
131
+ /** Empty a recording to record it again: its pins let go, its commands and bundles dropped. */
132
+ clear(recording, unpin) {
133
+ release(recording, unpin);
134
+ resetPool(recording.frame);
135
+ resetPool(recording.surface);
136
+ recording.frameBundles.length = 0;
137
+ recording.surfaceBundles.length = 0;
138
+ recording.immediate = false;
139
+ recording.materialAsks = 0;
140
+ }
141
+ /** Every recording, let go; what disposal and a forgetting store both do. */
142
+ forget(unpin) {
143
+ for (const recording of this.recordings)
144
+ release(recording, unpin);
145
+ this.recordings.length = 0;
146
+ }
147
+ dispose(unpin) {
148
+ this.disposed = true;
149
+ this.forget(unpin);
150
+ this.drawRing.dispose();
151
+ this.passes.dispose();
152
+ this.views.dispose();
153
+ }
154
+ growCopies(copies) {
155
+ this.passes.dispose();
156
+ this.views.dispose();
157
+ this.passes = new UniformRing(this.device, this.passSize, copies, USAGE_UNIFORM_DST, 'static.passes');
158
+ this.views = new UniformRing(this.device, this.viewSize, copies, USAGE_UNIFORM_DST, 'static.views');
159
+ this.claimCopies();
160
+ this.ringEpoch += 1;
161
+ this.groups = new Map();
162
+ this.blank = null;
163
+ }
164
+ /** Every copy's slot, taken once: a copy is a fixed offset, never handed out again. */
165
+ claimCopies() {
166
+ for (let copy = 0; copy < this.passes.slots; copy++) {
167
+ this.passes.allocate();
168
+ this.views.allocate();
169
+ }
170
+ }
171
+ }
172
+ /** A command the recording takes in place of the frame graph's. */
173
+ export function takeRecorded(pool) {
174
+ const command = pool.commands[takeCommand(pool)];
175
+ command.offsetCount = 0;
176
+ command.vertexCount = 0;
177
+ command.indexBuffer = null;
178
+ command.indexFormat = 'uint32';
179
+ command.indexed = false;
180
+ command.instances = 1;
181
+ command.indirect = null;
182
+ return command;
183
+ }
184
+ /**
185
+ * One copy's bundle of a pool's commands: each lit command as recorded but for the pass and view
186
+ * offsets, which are the copy's. Allocates, and runs once a copy a recording, never per frame.
187
+ */
188
+ export function encodeBundle(device, target, pool, passOffset, viewOffset, label) {
189
+ const encoder = device.createRenderBundleEncoder({
190
+ label,
191
+ colorFormats: target.colorFormats,
192
+ depthStencilFormat: target.depthStencilFormat,
193
+ sampleCount: target.sampleCount,
194
+ depthReadOnly: target.depthReadOnly,
195
+ stencilReadOnly: target.depthReadOnly,
196
+ });
197
+ for (let i = 0; i < pool.taken; i++) {
198
+ const command = pool.commands[i];
199
+ if (command.pipeline === null || command.bindGroup === null)
200
+ continue;
201
+ if (command.offsetCount !== 4) {
202
+ throw new Error(`static draws: a recorded command bound ${command.offsetCount} offsets, not a lit draw's four`);
203
+ }
204
+ encoder.setPipeline(command.pipeline);
205
+ encoder.setBindGroup(0, command.bindGroup, [
206
+ command.offsetA,
207
+ passOffset,
208
+ command.offsetC,
209
+ viewOffset,
210
+ ]);
211
+ for (let slot = 0; slot < command.vertexCount; slot++) {
212
+ const buffer = command.vertexBuffers[slot];
213
+ if (buffer != null)
214
+ encoder.setVertexBuffer(slot, buffer);
215
+ }
216
+ if (command.indexed && command.indexBuffer !== null) {
217
+ encoder.setIndexBuffer(command.indexBuffer, command.indexFormat);
218
+ if (command.indirect !== null)
219
+ encoder.drawIndexedIndirect(command.indirect, 0);
220
+ else
221
+ encoder.drawIndexed(command.count, command.instances);
222
+ }
223
+ else {
224
+ encoder.draw(command.count, command.instances);
225
+ }
226
+ }
227
+ return [encoder.finish({ label })];
228
+ }
229
+ function release(recording, unpin) {
230
+ if (unpin !== null)
231
+ for (const offset of recording.pins)
232
+ unpin(offset);
233
+ recording.pins.length = 0;
234
+ }
235
+ function sameWords(a, b) {
236
+ if (a.length !== b.length)
237
+ return false;
238
+ for (let i = 0; i < a.length; i++)
239
+ if (a[i] !== b[i])
240
+ return false;
241
+ return true;
242
+ }
@@ -1,7 +1,7 @@
1
1
  import type { SurfaceTextureOptions } from '../../surfaceTexture.ts';
2
2
  import type { LightmapTexels } from '../../lightmap.ts';
3
3
  import type { SceneCaptureTexels } from '../../sceneCapture.ts';
4
- import type { CompressedTextureFormat, SurfaceSource } from '../../compressedSource.ts';
4
+ import type { CompressedTextureFormat, CompressedTextureSource, SurfaceSource } from '../../compressedSource.ts';
5
5
  import type { PipelineCache } from './pipelineCache.ts';
6
6
  /** How many levels an image of this size has, counting the one it arrives with. */
7
7
  export declare function mipLevelCount(width: number, height: number): number;
@@ -63,7 +63,9 @@ export declare class GpuSurfaceTexture {
63
63
  /** Its view, which the flat group binds beside this image when it is a material's albedo. */
64
64
  effectsView: GPUTextureView | null;
65
65
  /** The block format a compressed texture holds, or null for one uploaded from images. */
66
- private readonly blockFormat;
66
+ private blockFormat;
67
+ /** Whether the image is colour, decoded from sRGB as it is sampled: what blocks swapped in keep. */
68
+ private readonly srgb;
67
69
  constructor(device: GPUDevice, pipelines: PipelineCache, source: SurfaceSource | readonly SurfaceSource[] | LightmapTexels | SceneCaptureTexels, options?: SurfaceTextureOptions, compressed?: readonly CompressedTextureFormat[]);
68
70
  /** A view of every layer, as the shaders' `texture_2d_array` bindings read it. */
69
71
  private arrayView;
@@ -84,8 +86,13 @@ export declare class GpuSurfaceTexture {
84
86
  * hit that whenever the preview's decode won the race. The view is new then, and the texture it
85
87
  * replaced is handed back rather than destroyed: a draw recorded earlier in this frame may still
86
88
  * read it, so the renderer destroys it once nothing can, and drops every binding of the old view.
89
+ *
90
+ * **Blocks replace an image the same way**, at any size: a texture of their format holds them,
91
+ * read in the colour space this one was made for, and the image's is handed back. Refused, as a
92
+ * texture made from them would be, where `compressed` — the formats the device samples — does not
93
+ * take them. The way back is refused: an image cannot replace blocks.
87
94
  */
88
- update(source: TexImageSource): GPUTexture | null;
95
+ update(source: TexImageSource | CompressedTextureSource, compressed?: readonly CompressedTextureFormat[]): GPUTexture | null;
89
96
  private allocate;
90
97
  dispose(): void;
91
98
  private upload;
@@ -3,7 +3,7 @@ import { toHalfFloats } from '../../halfFloat.js';
3
3
  import { isLightmapTexels } from '../../lightmap.js';
4
4
  import { isSceneCaptureTexels } from '../../sceneCapture.js';
5
5
  import { layerSize, refuseArrayUpdate, sourceSize } from '../../textureSource.js';
6
- import { compressedLayers, planBlocks, refuseBlockUpdate } from '../../compressedSource.js';
6
+ import { compressedLayers, isCompressedSource, planBlocks, refuseBlockUpdate, } from '../../compressedSource.js';
7
7
  import { createCompressedTexture } from './compressedUpload.js';
8
8
  import { shaderModule } from './shaderModules.js';
9
9
  /**
@@ -154,6 +154,8 @@ export class GpuSurfaceTexture {
154
154
  effectsView = null;
155
155
  /** The block format a compressed texture holds, or null for one uploaded from images. */
156
156
  blockFormat;
157
+ /** Whether the image is colour, decoded from sRGB as it is sampled: what blocks swapped in keep. */
158
+ srgb;
157
159
  constructor(device, pipelines, source, options = {}, compressed = []) {
158
160
  this.device = device;
159
161
  this.pipelines = pipelines;
@@ -164,6 +166,7 @@ export class GpuSurfaceTexture {
164
166
  const listed = (baked !== null || capture !== null ? [] : Array.isArray(source) ? source : [source]);
165
167
  const blocks = compressedLayers(listed);
166
168
  const srgb = (options.colorSpace ?? 'linear') === 'srgb';
169
+ this.srgb = srgb;
167
170
  /* `SRGB8_ALPHA8`'s equivalent. Decoded in the sampler, before filtering, which is the only
168
171
  place it is correct — `surfaceTexture.ts` makes the argument in full. A capture takes the
169
172
  world's own format, which its pass resolves into. */
@@ -294,12 +297,27 @@ export class GpuSurfaceTexture {
294
297
  * hit that whenever the preview's decode won the race. The view is new then, and the texture it
295
298
  * replaced is handed back rather than destroyed: a draw recorded earlier in this frame may still
296
299
  * read it, so the renderer destroys it once nothing can, and drops every binding of the old view.
300
+ *
301
+ * **Blocks replace an image the same way**, at any size: a texture of their format holds them,
302
+ * read in the colour space this one was made for, and the image's is handed back. Refused, as a
303
+ * texture made from them would be, where `compressed` — the formats the device samples — does not
304
+ * take them. The way back is refused: an image cannot replace blocks.
297
305
  */
298
- update(source) {
306
+ update(source, compressed = []) {
299
307
  const replaced = this.texture;
300
308
  if (replaced === null)
301
309
  return null;
302
310
  refuseArrayUpdate(this.layers);
311
+ if (isCompressedSource(source)) {
312
+ const plan = planBlocks([source], this.srgb, compressed);
313
+ this.blockFormat = source.format;
314
+ this.width = source.width;
315
+ this.height = source.height;
316
+ this.levels = this.mipmapped ? plan.levels : 1;
317
+ this.texture = createCompressedTexture(this.device, plan.name, [source], this.levels);
318
+ this.current = this.arrayView(this.texture);
319
+ return replaced;
320
+ }
303
321
  refuseBlockUpdate(this.blockFormat);
304
322
  const { width, height } = sourceSize(source);
305
323
  if (width === this.width && height === this.height) {
@@ -7,6 +7,21 @@ import type { BudgetCounter } from '../budget.ts';
7
7
  * an alignment restated somewhere else is an alignment that can disagree.
8
8
  */
9
9
  export declare const DYNAMIC_ALIGNMENT = 256;
10
+ /**
11
+ * The most staging one ring may hold, and therefore the most device memory it may take.
12
+ *
13
+ * **A ceiling on the growth, because a runaway consumer should degrade rather than crash.** Past
14
+ * this the ring refuses to grow and goes on skipping work and reporting it, which is the right end
15
+ * state for a frame asking for something no device should be asked for. What growth changes is
16
+ * where that line sits: at a number chosen against a phone's memory rather than at whatever the
17
+ * ring happened to be constructed with.
18
+ *
19
+ * Sixty-four mebibytes is far past any legitimate scene — the flat vertex ring's slot is 1,540
20
+ * bytes, so this is forty-three thousand draws, and the fragment ring's is at most 13,312, which
21
+ * is five thousand material changes in one frame. The device buffer matches the staging, so a ring
22
+ * at this ceiling holds 128 MiB in total and only ever reaches it by being asked to.
23
+ */
24
+ export declare const MAX_RING_BYTES: number;
10
25
  /**
11
26
  * Per-draw uniforms, in slots, written once and bound by offset.
12
27
  *
@@ -159,5 +174,11 @@ export declare class UniformRing {
159
174
  * staging without passing a method that lowers `clean` — which is why the staging stays private.
160
175
  */
161
176
  flush(): void;
177
+ /**
178
+ * Upload one slot now, and nothing else: for a ring whose slots are all claimed once and written
179
+ * one at a time — a static list's copies — where `flush` would send every slot above the lowest
180
+ * one written. Leaves the watermark as it was, so a later `flush` still owes what it owed.
181
+ */
182
+ uploadSlot(slot: number): void;
162
183
  dispose(): void;
163
184
  }
@@ -20,7 +20,7 @@ export const DYNAMIC_ALIGNMENT = 256;
20
20
  * is five thousand material changes in one frame. The device buffer matches the staging, so a ring
21
21
  * at this ceiling holds 128 MiB in total and only ever reaches it by being asked to.
22
22
  */
23
- const MAX_RING_BYTES = 64 * 1024 * 1024;
23
+ export const MAX_RING_BYTES = 64 * 1024 * 1024;
24
24
  /**
25
25
  * Per-draw uniforms, in slots, written once and bound by offset.
26
26
  *
@@ -252,6 +252,14 @@ export class UniformRing {
252
252
  this.device.queue.writeBuffer(this.buffer, from, this.staging, from, end - from);
253
253
  this.clean = end;
254
254
  }
255
+ /**
256
+ * Upload one slot now, and nothing else: for a ring whose slots are all claimed once and written
257
+ * one at a time — a static list's copies — where `flush` would send every slot above the lowest
258
+ * one written. Leaves the watermark as it was, so a later `flush` still owes what it owed.
259
+ */
260
+ uploadSlot(slot) {
261
+ this.device.queue.writeBuffer(this.buffer, slot, this.staging, slot, this.slotSize);
262
+ }
255
263
  dispose() {
256
264
  this.buffer.destroy();
257
265
  }
@@ -0,0 +1,22 @@
1
+ import { type AttributePacking, type VertexPacking } from '../../vertexPacking.ts';
2
+ /** The key's suffix for this packing: one word each attribute not travelling as floats. */
3
+ export declare function packingKey(packing: VertexPacking): string;
4
+ /** Mark this packing in a pipeline's `present`, which is what `vertexBufferLayouts` reads. */
5
+ export declare function markPacking(present: Record<string, boolean>, packing: VertexPacking): void;
6
+ /** Mark the packing a mesh's key names, for a pass that has the mesh and not its data. */
7
+ export declare function markPackingOfKey(present: Record<string, boolean>, key: string): void;
8
+ /** How `present` says this attribute travels: floats unless a word says otherwise. */
9
+ export declare function packingIn(present: Readonly<Record<string, boolean>>, name: string): AttributePacking;
10
+ /** The format an attribute is read with, packed this way, or `float` as given. */
11
+ export declare function packedFormat(float: GPUVertexFormat, packing: AttributePacking): GPUVertexFormat;
12
+ /** Bytes a vertex spends on one attribute read with `format`. */
13
+ export declare function formatBytes(format: GPUVertexFormat): number;
14
+ /**
15
+ * One attribute of vertices `from` to `to` into its sixteen-bit field of the interleaved rows,
16
+ * `scale` being `SNORM16_ONE` or `UNORM16_ONE`. Rounded by `toFixed16`, as `fixedArray` rounds
17
+ * for the other backend; the range was checked when the packing was chosen.
18
+ *
19
+ * Unrolled by width, as `interleaveField` is and for its reason: the general loop was most of a
20
+ * heavy load's upload. Three and four are the widths a packed attribute has.
21
+ */
22
+ export declare function interleaveFixed(rows: Int16Array | Uint16Array, source: Float32Array, width: number, step: number, fieldOffset: number, from: number, to: number, scale: number): void;
@@ -0,0 +1,97 @@
1
+ import { PACKED_ATTRIBUTES, toFixed16, } from '../../vertexPacking.js';
2
+ /**
3
+ * A mesh's packed attributes as a WebGPU vertex reads them: each one's format and bytes, the words
4
+ * a pipeline key carries for them, and the writers that put sixteen-bit fixed point into the
5
+ * interleaved rows. What is packed how is `vertexPacking.ts`'s decision; this is only its binding.
6
+ *
7
+ * **Sixteen-bit fields are four wide, never three**, because WebGPU has no three-component
8
+ * sixteen-bit format: a normal or a colour takes `x4` and leaves the fourth zero, and the shader's
9
+ * `vec3` input reads the first three. So a normal costs eight bytes here against six on WebGL2,
10
+ * which reads three. Every field stays a whole number of four-byte words, so a vertex does too.
11
+ */
12
+ /** The ways an attribute can travel other than as floats, as the key words name them. */
13
+ const PACKINGS = ['snorm16', 'unorm16', 'constant'];
14
+ /** The pipeline key's word for one attribute's packing. Not a prefix of any other word. */
15
+ function word(name, packing) {
16
+ return `${name}~${packing}`;
17
+ }
18
+ /** The key's suffix for this packing: one word each attribute not travelling as floats. */
19
+ export function packingKey(packing) {
20
+ let key = '';
21
+ for (const name of PACKED_ATTRIBUTES) {
22
+ if (packing[name] !== 'float')
23
+ key += `:${word(name, packing[name])}`;
24
+ }
25
+ return key;
26
+ }
27
+ /** Mark this packing in a pipeline's `present`, which is what `vertexBufferLayouts` reads. */
28
+ export function markPacking(present, packing) {
29
+ for (const name of PACKED_ATTRIBUTES) {
30
+ if (packing[name] !== 'float')
31
+ present[word(name, packing[name])] = true;
32
+ }
33
+ }
34
+ /** Mark the packing a mesh's key names, for a pass that has the mesh and not its data. */
35
+ export function markPackingOfKey(present, key) {
36
+ for (const name of PACKED_ATTRIBUTES) {
37
+ for (const packing of PACKINGS) {
38
+ if (key.includes(`:${word(name, packing)}`))
39
+ present[word(name, packing)] = true;
40
+ }
41
+ }
42
+ }
43
+ /** How `present` says this attribute travels: floats unless a word says otherwise. */
44
+ export function packingIn(present, name) {
45
+ for (const packing of PACKINGS) {
46
+ if (present[word(name, packing)] === true)
47
+ return packing;
48
+ }
49
+ return 'float';
50
+ }
51
+ /** The format an attribute is read with, packed this way, or `float` as given. */
52
+ export function packedFormat(float, packing) {
53
+ if (packing === 'snorm16')
54
+ return 'snorm16x4';
55
+ if (packing === 'unorm16')
56
+ return 'unorm16x4';
57
+ return float;
58
+ }
59
+ /** Bytes a vertex spends on one attribute read with `format`. */
60
+ export function formatBytes(format) {
61
+ if (format === 'snorm16x4' || format === 'unorm16x4')
62
+ return 8;
63
+ if (format === 'float32x4')
64
+ return 16;
65
+ if (format === 'float32x3')
66
+ return 12;
67
+ if (format === 'float32x2')
68
+ return 8;
69
+ return 4;
70
+ }
71
+ /**
72
+ * One attribute of vertices `from` to `to` into its sixteen-bit field of the interleaved rows,
73
+ * `scale` being `SNORM16_ONE` or `UNORM16_ONE`. Rounded by `toFixed16`, as `fixedArray` rounds
74
+ * for the other backend; the range was checked when the packing was chosen.
75
+ *
76
+ * Unrolled by width, as `interleaveField` is and for its reason: the general loop was most of a
77
+ * heavy load's upload. Three and four are the widths a packed attribute has.
78
+ */
79
+ export function interleaveFixed(rows, source, width, step, fieldOffset, from, to, scale) {
80
+ let write = from * step + fieldOffset;
81
+ const end = to * width;
82
+ if (width === 3) {
83
+ for (let read = from * 3; read < end; read += 3, write += step) {
84
+ rows[write] = toFixed16(source[read], scale);
85
+ rows[write + 1] = toFixed16(source[read + 1], scale);
86
+ rows[write + 2] = toFixed16(source[read + 2], scale);
87
+ }
88
+ }
89
+ else {
90
+ for (let read = from * 4; read < end; read += 4, write += step) {
91
+ rows[write] = toFixed16(source[read], scale);
92
+ rows[write + 1] = toFixed16(source[read + 1], scale);
93
+ rows[write + 2] = toFixed16(source[read + 2], scale);
94
+ rows[write + 3] = toFixed16(source[read + 3], scale);
95
+ }
96
+ }
97
+ }
@@ -62,7 +62,7 @@ export declare function bloomProfileWarning(quality: {
62
62
  * already takes. Absent, or a field absent, is the response every frame had before 4.8.6.
63
63
  *
64
64
  * **Why it exists: one band over the colour less the threshold washes a stage built for another
65
- * engine.** Unreal's standard bloom keeps `saturate((L − threshold) / 2)` of a colour rather than
65
+ * engine.** A ramped bloom keeps `saturate((L − threshold) / 2)` of a colour rather than
66
66
  * the colour less the threshold, and spreads it over six bands, each in a tint of its own. A
67
67
  * stage authored against that — a threshold of 0.1 and tints summing below one — came out white
68
68
  * here, because subtracting so low a threshold keeps nearly every light in the frame at full
@@ -72,8 +72,8 @@ export interface BloomResponse {
72
72
  /**
73
73
  * Scene units over which a colour comes in, from none of it at the threshold to all of it this
74
74
  * far past. **0, the default, subtracts instead**: a colour keeps what it has past the threshold,
75
- * the response since bloom shipped. Unreal's is 2 after exposure, so a frame exposed by `e` that
76
- * wants it passes `2 / e`, as it passes the threshold over the exposure.
75
+ * the response since bloom shipped. A ramped bloom's is 2 after exposure, so a frame exposed by
76
+ * `e` that wants it passes `2 / e`, as it passes the threshold over the exposure.
77
77
  *
78
78
  * Brightness is still the largest channel rather than luminance, for the reason
79
79
  * `shaders/bloom.ts` gives; on a neutral colour the two are equal.