@expofp/renderer 0.0.0-experimental.cf063fc

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 (280) hide show
  1. package/README.md +145 -0
  2. package/dist/Renderer.d.ts +102 -0
  3. package/dist/Renderer.js +96 -0
  4. package/dist/controls/CameraControls.d.ts +108 -0
  5. package/dist/controls/CameraControls.js +203 -0
  6. package/dist/controls/Controls.d.ts +53 -0
  7. package/dist/controls/Controls.js +54 -0
  8. package/dist/controls/camera-api.d.ts +226 -0
  9. package/dist/controls/camera-api.js +179 -0
  10. package/dist/controls/gestures.d.ts +71 -0
  11. package/dist/controls/gestures.js +98 -0
  12. package/dist/controls/handlers/DoubleTapZoom.d.ts +23 -0
  13. package/dist/controls/handlers/DoubleTapZoom.js +82 -0
  14. package/dist/controls/handlers/Inertia.d.ts +60 -0
  15. package/dist/controls/handlers/Inertia.js +222 -0
  16. package/dist/controls/handlers/Pan.d.ts +23 -0
  17. package/dist/controls/handlers/Pan.js +212 -0
  18. package/dist/controls/handlers/Pitch.d.ts +19 -0
  19. package/dist/controls/handlers/Pitch.js +200 -0
  20. package/dist/controls/handlers/Roll.d.ts +30 -0
  21. package/dist/controls/handlers/Roll.js +196 -0
  22. package/dist/controls/handlers/Zoom.d.ts +28 -0
  23. package/dist/controls/handlers/Zoom.js +34 -0
  24. package/dist/controls/hooks.d.ts +136 -0
  25. package/dist/controls/hooks.js +200 -0
  26. package/dist/controls/index.d.ts +9 -0
  27. package/dist/controls/index.js +7 -0
  28. package/dist/controls/zoom-to.d.ts +89 -0
  29. package/dist/controls/zoom-to.js +91 -0
  30. package/dist/core/InstanceUniforms.d.ts +108 -0
  31. package/dist/core/InstanceUniforms.js +298 -0
  32. package/dist/core/SquareDataTexture.d.ts +277 -0
  33. package/dist/core/SquareDataTexture.js +476 -0
  34. package/dist/core/batch/BatchedMesh.d.ts +74 -0
  35. package/dist/core/batch/BatchedMesh.js +155 -0
  36. package/dist/core/instanced/FrustumCulling.d.ts +141 -0
  37. package/dist/core/instanced/FrustumCulling.js +246 -0
  38. package/dist/core/instanced/InstancedMesh.d.ts +302 -0
  39. package/dist/core/instanced/InstancedMesh.js +497 -0
  40. package/dist/core/instanced/Instances.d.ts +119 -0
  41. package/dist/core/instanced/Instances.js +230 -0
  42. package/dist/core/instanced/Raycasting.d.ts +118 -0
  43. package/dist/core/instanced/Raycasting.js +134 -0
  44. package/dist/core/instanced/instanced-material.d.ts +77 -0
  45. package/dist/core/instanced/instanced-material.js +228 -0
  46. package/dist/core/lines/BatchedLines.d.ts +137 -0
  47. package/dist/core/lines/BatchedLines.js +215 -0
  48. package/dist/core/lines/FrustumCullingLines.d.ts +44 -0
  49. package/dist/core/lines/FrustumCullingLines.js +85 -0
  50. package/dist/core/lines/RaycastingLines.d.ts +45 -0
  51. package/dist/core/lines/RaycastingLines.js +91 -0
  52. package/dist/core/lines/lines-material.d.ts +47 -0
  53. package/dist/core/lines/lines-material.js +124 -0
  54. package/dist/core/shader-source.d.ts +70 -0
  55. package/dist/core/shader-source.js +110 -0
  56. package/dist/core/sprites/BatchedSprites.d.ts +115 -0
  57. package/dist/core/sprites/BatchedSprites.js +173 -0
  58. package/dist/core/sprites/RaycastingSprites.d.ts +41 -0
  59. package/dist/core/sprites/RaycastingSprites.js +71 -0
  60. package/dist/core/sprites/sprite-material.d.ts +59 -0
  61. package/dist/core/sprites/sprite-material.js +192 -0
  62. package/dist/core/text/BatchedText.d.ts +233 -0
  63. package/dist/core/text/BatchedText.js +473 -0
  64. package/dist/core/text/MemberTexture.d.ts +90 -0
  65. package/dist/core/text/MemberTexture.js +289 -0
  66. package/dist/core/text/Text.d.ts +88 -0
  67. package/dist/core/text/Text.js +230 -0
  68. package/dist/core/text/batched-text-material.d.ts +56 -0
  69. package/dist/core/text/batched-text-material.js +123 -0
  70. package/dist/core/text/glyph-packing.d.ts +17 -0
  71. package/dist/core/text/glyph-packing.js +61 -0
  72. package/dist/core/typed-array.d.ts +16 -0
  73. package/dist/core/typed-array.js +20 -0
  74. package/dist/debug/components/DebugPanelProvider.d.ts +20 -0
  75. package/dist/debug/components/DebugPanelProvider.js +50 -0
  76. package/dist/debug/components/DebugTools.d.ts +22 -0
  77. package/dist/debug/components/DebugTools.js +28 -0
  78. package/dist/debug/components/Gui.d.ts +22 -0
  79. package/dist/debug/components/Gui.js +42 -0
  80. package/dist/debug/components/MemoryInfo.d.ts +22 -0
  81. package/dist/debug/components/MemoryInfo.js +42 -0
  82. package/dist/debug/components/Panel.d.ts +31 -0
  83. package/dist/debug/components/Panel.js +104 -0
  84. package/dist/debug/components/Stats.d.ts +9 -0
  85. package/dist/debug/components/Stats.js +24 -0
  86. package/dist/debug/debug-chunk.d.ts +26 -0
  87. package/dist/debug/debug-chunk.js +32 -0
  88. package/dist/debug/debug-context.d.ts +41 -0
  89. package/dist/debug/debug-context.js +59 -0
  90. package/dist/debug/index.d.ts +41 -0
  91. package/dist/debug/index.js +43 -0
  92. package/dist/events/LayoutDriver.d.ts +36 -0
  93. package/dist/events/LayoutDriver.js +192 -0
  94. package/dist/events/PointerMissed.d.ts +23 -0
  95. package/dist/events/PointerMissed.js +58 -0
  96. package/dist/events/def-culling.d.ts +46 -0
  97. package/dist/events/def-culling.js +69 -0
  98. package/dist/events/layout-targets.d.ts +151 -0
  99. package/dist/events/layout-targets.js +397 -0
  100. package/dist/events/placement/box.d.ts +59 -0
  101. package/dist/events/placement/box.js +96 -0
  102. package/dist/events/placement/grid.d.ts +161 -0
  103. package/dist/events/placement/grid.js +235 -0
  104. package/dist/events/placement/index.d.ts +2 -0
  105. package/dist/events/placement/index.js +1 -0
  106. package/dist/events/placement/pass.d.ts +70 -0
  107. package/dist/events/placement/pass.js +617 -0
  108. package/dist/events/pointer.d.ts +194 -0
  109. package/dist/events/pointer.js +356 -0
  110. package/dist/external/ExternalCanvas.d.ts +56 -0
  111. package/dist/external/ExternalCanvas.js +177 -0
  112. package/dist/external/external-camera.d.ts +18 -0
  113. package/dist/external/external-camera.js +103 -0
  114. package/dist/external/external-events.d.ts +14 -0
  115. package/dist/external/external-events.js +33 -0
  116. package/dist/index.d.ts +13 -0
  117. package/dist/index.js +14 -0
  118. package/dist/objects/Background.d.ts +31 -0
  119. package/dist/objects/Background.js +37 -0
  120. package/dist/objects/containers/SlotContainer.d.ts +312 -0
  121. package/dist/objects/containers/SlotContainer.js +311 -0
  122. package/dist/objects/containers/hooks.d.ts +61 -0
  123. package/dist/objects/containers/hooks.js +196 -0
  124. package/dist/objects/containers/index.d.ts +6 -0
  125. package/dist/objects/containers/index.js +4 -0
  126. package/dist/objects/containers/keys.d.ts +10 -0
  127. package/dist/objects/containers/keys.js +18 -0
  128. package/dist/objects/containers/mesh-writes.d.ts +106 -0
  129. package/dist/objects/containers/mesh-writes.js +97 -0
  130. package/dist/objects/containers/props.d.ts +33 -0
  131. package/dist/objects/containers/props.js +1 -0
  132. package/dist/objects/images/ImageInstance.d.ts +29 -0
  133. package/dist/objects/images/ImageInstance.js +152 -0
  134. package/dist/objects/images/Images.d.ts +49 -0
  135. package/dist/objects/images/Images.js +394 -0
  136. package/dist/objects/images/atlas-packer.d.ts +129 -0
  137. package/dist/objects/images/atlas-packer.js +287 -0
  138. package/dist/objects/images/image-container.d.ts +74 -0
  139. package/dist/objects/images/image-container.js +100 -0
  140. package/dist/objects/images/index.d.ts +2 -0
  141. package/dist/objects/images/index.js +1 -0
  142. package/dist/objects/images/pack-sources.d.ts +190 -0
  143. package/dist/objects/images/pack-sources.js +272 -0
  144. package/dist/objects/images/texture-budget.d.ts +120 -0
  145. package/dist/objects/images/texture-budget.js +215 -0
  146. package/dist/objects/images/worker/atlas-worker.d.ts +56 -0
  147. package/dist/objects/images/worker/atlas-worker.js +123 -0
  148. package/dist/objects/images/worker/client.d.ts +62 -0
  149. package/dist/objects/images/worker/client.js +249 -0
  150. package/dist/objects/images/worker/index.d.ts +10 -0
  151. package/dist/objects/images/worker/index.js +1 -0
  152. package/dist/objects/layer/Layer.d.ts +28 -0
  153. package/dist/objects/layer/Layer.js +134 -0
  154. package/dist/objects/layer/index.d.ts +2 -0
  155. package/dist/objects/layer/index.js +1 -0
  156. package/dist/objects/layer/layer-context.d.ts +74 -0
  157. package/dist/objects/layer/layer-context.js +82 -0
  158. package/dist/objects/layer/mount-scheduler.d.ts +131 -0
  159. package/dist/objects/layer/mount-scheduler.js +289 -0
  160. package/dist/objects/layer/visibility-gui.d.ts +19 -0
  161. package/dist/objects/layer/visibility-gui.js +93 -0
  162. package/dist/objects/lighting/Lighting.d.ts +92 -0
  163. package/dist/objects/lighting/Lighting.js +384 -0
  164. package/dist/objects/lighting/OcclusionPasses.d.ts +23 -0
  165. package/dist/objects/lighting/OcclusionPasses.js +271 -0
  166. package/dist/objects/lighting/index.d.ts +11 -0
  167. package/dist/objects/lighting/index.js +10 -0
  168. package/dist/objects/lighting/lighting-gui.d.ts +24 -0
  169. package/dist/objects/lighting/lighting-gui.js +114 -0
  170. package/dist/objects/lighting/occluder.d.ts +34 -0
  171. package/dist/objects/lighting/occluder.js +62 -0
  172. package/dist/objects/lighting/occlusion-targets.d.ts +143 -0
  173. package/dist/objects/lighting/occlusion-targets.js +276 -0
  174. package/dist/objects/lines/LineInstance.d.ts +21 -0
  175. package/dist/objects/lines/LineInstance.js +73 -0
  176. package/dist/objects/lines/Lines.d.ts +17 -0
  177. package/dist/objects/lines/Lines.js +31 -0
  178. package/dist/objects/lines/index.d.ts +2 -0
  179. package/dist/objects/lines/index.js +1 -0
  180. package/dist/objects/lines/line-container.d.ts +35 -0
  181. package/dist/objects/lines/line-container.js +40 -0
  182. package/dist/objects/materials/ColorMaterial.d.ts +92 -0
  183. package/dist/objects/materials/ColorMaterial.js +124 -0
  184. package/dist/objects/materials/LineMaterial.d.ts +44 -0
  185. package/dist/objects/materials/LineMaterial.js +74 -0
  186. package/dist/objects/materials/ModelMaterial.d.ts +53 -0
  187. package/dist/objects/materials/ModelMaterial.js +111 -0
  188. package/dist/objects/materials/TextureMaterial.d.ts +33 -0
  189. package/dist/objects/materials/TextureMaterial.js +48 -0
  190. package/dist/objects/materials/effects/ao/ambient-occlusion-pass.d.ts +77 -0
  191. package/dist/objects/materials/effects/ao/ambient-occlusion-pass.js +296 -0
  192. package/dist/objects/materials/effects/ao/ambient-occlusion-patch.d.ts +29 -0
  193. package/dist/objects/materials/effects/ao/ambient-occlusion-patch.js +115 -0
  194. package/dist/objects/materials/effects/ao/blur-pass.d.ts +61 -0
  195. package/dist/objects/materials/effects/ao/blur-pass.js +101 -0
  196. package/dist/objects/materials/effects/ao/index.d.ts +129 -0
  197. package/dist/objects/materials/effects/ao/index.js +72 -0
  198. package/dist/objects/materials/effects/color-dim.d.ts +115 -0
  199. package/dist/objects/materials/effects/color-dim.js +206 -0
  200. package/dist/objects/materials/effects/contact-shadow.d.ts +100 -0
  201. package/dist/objects/materials/effects/contact-shadow.js +167 -0
  202. package/dist/objects/materials/effects/effects-context.d.ts +53 -0
  203. package/dist/objects/materials/effects/effects-context.js +24 -0
  204. package/dist/objects/materials/shared.d.ts +24 -0
  205. package/dist/objects/materials/shared.js +72 -0
  206. package/dist/objects/models/ModelInstance.d.ts +22 -0
  207. package/dist/objects/models/ModelInstance.js +33 -0
  208. package/dist/objects/models/Models.d.ts +22 -0
  209. package/dist/objects/models/Models.js +186 -0
  210. package/dist/objects/models/gltf-cache.d.ts +17 -0
  211. package/dist/objects/models/gltf-cache.js +50 -0
  212. package/dist/objects/models/index.d.ts +2 -0
  213. package/dist/objects/models/index.js +1 -0
  214. package/dist/objects/models/model-asset.d.ts +48 -0
  215. package/dist/objects/models/model-asset.js +77 -0
  216. package/dist/objects/models/model-container.d.ts +123 -0
  217. package/dist/objects/models/model-container.js +207 -0
  218. package/dist/objects/shapes/ShapeInstance.d.ts +26 -0
  219. package/dist/objects/shapes/ShapeInstance.js +39 -0
  220. package/dist/objects/shapes/Shapes.d.ts +21 -0
  221. package/dist/objects/shapes/Shapes.js +131 -0
  222. package/dist/objects/shapes/index.d.ts +2 -0
  223. package/dist/objects/shapes/index.js +1 -0
  224. package/dist/objects/shapes/shape-container.d.ts +141 -0
  225. package/dist/objects/shapes/shape-container.js +242 -0
  226. package/dist/objects/shapes/shape-geometry.d.ts +27 -0
  227. package/dist/objects/shapes/shape-geometry.js +94 -0
  228. package/dist/objects/text/TextInstance.d.ts +22 -0
  229. package/dist/objects/text/TextInstance.js +95 -0
  230. package/dist/objects/text/Texts.d.ts +30 -0
  231. package/dist/objects/text/Texts.js +43 -0
  232. package/dist/objects/text/index.d.ts +2 -0
  233. package/dist/objects/text/index.js +1 -0
  234. package/dist/objects/text/text-container.d.ts +121 -0
  235. package/dist/objects/text/text-container.js +229 -0
  236. package/dist/objects/text/text-layout.d.ts +92 -0
  237. package/dist/objects/text/text-layout.js +180 -0
  238. package/dist/space/Camera.d.ts +103 -0
  239. package/dist/space/Camera.js +321 -0
  240. package/dist/space/Scene.d.ts +47 -0
  241. package/dist/space/Scene.js +127 -0
  242. package/dist/space/Space.d.ts +325 -0
  243. package/dist/space/Space.js +359 -0
  244. package/dist/space/VisibleRect.d.ts +45 -0
  245. package/dist/space/VisibleRect.js +129 -0
  246. package/dist/space/camera-handle.d.ts +67 -0
  247. package/dist/space/camera-handle.js +63 -0
  248. package/dist/space/camera-kind.d.ts +26 -0
  249. package/dist/space/camera-kind.js +30 -0
  250. package/dist/space/canvas-size.d.ts +24 -0
  251. package/dist/space/canvas-size.js +20 -0
  252. package/dist/space/clipping.d.ts +69 -0
  253. package/dist/space/clipping.js +130 -0
  254. package/dist/space/layout-measure.d.ts +67 -0
  255. package/dist/space/layout-measure.js +175 -0
  256. package/dist/space/viewport.d.ts +112 -0
  257. package/dist/space/viewport.js +67 -0
  258. package/dist/types/defs.d.ts +234 -0
  259. package/dist/types/defs.js +1 -0
  260. package/dist/types/events.d.ts +198 -0
  261. package/dist/types/events.js +1 -0
  262. package/dist/types/guards.d.ts +21 -0
  263. package/dist/types/guards.js +52 -0
  264. package/dist/update/index.d.ts +3 -0
  265. package/dist/update/index.js +2 -0
  266. package/dist/update/slot-writers.d.ts +30 -0
  267. package/dist/update/slot-writers.js +76 -0
  268. package/dist/update/store.d.ts +41 -0
  269. package/dist/update/store.js +104 -0
  270. package/dist/utils/arrays.d.ts +13 -0
  271. package/dist/utils/arrays.js +20 -0
  272. package/dist/utils/colors.d.ts +19 -0
  273. package/dist/utils/colors.js +27 -0
  274. package/dist/utils/hot-state.d.ts +41 -0
  275. package/dist/utils/hot-state.js +37 -0
  276. package/dist/utils/logging.d.ts +29 -0
  277. package/dist/utils/logging.js +55 -0
  278. package/dist/utils/math.d.ts +52 -0
  279. package/dist/utils/math.js +76 -0
  280. package/package.json +79 -0
@@ -0,0 +1,215 @@
1
+ import { createContext, useEffect, useSyncExternalStore } from 'react';
2
+ import { createLogger } from '../../utils/logging';
3
+ const logger = createLogger('texture-budget');
4
+ /** Bytes one RGBA texel occupies. */
5
+ const BYTES_PER_PIXEL = 4;
6
+ /**
7
+ * How much a full mipmap chain adds over the base level: the 1/4 + 1/16 + … geometric series sums to
8
+ * 1/3, so a mipmapped texture costs ~4/3 its base size. The atlas textures upload with mipmaps on
9
+ * (see `makeAtlasTexture`), so the budget counts them, or it would under-report GPU use by a third.
10
+ */
11
+ const MIPMAP_FACTOR = 4 / 3;
12
+ /** A megabyte, the unit `memoryLimit` is given in. */
13
+ const BYTES_PER_MB = 1024 * 1024;
14
+ /**
15
+ * How much the resize factor must move before subscribers are told. A change this small shifts an
16
+ * atlas edge by well under a pixel, so re-deriving every texture for it would be churn with no
17
+ * visible effect — the point of the gate is to keep a burst of atlas mounts, each nudging the total,
18
+ * from re-scaling the whole scene once per mount.
19
+ */
20
+ const FACTOR_EPSILON = 1e-3;
21
+ /**
22
+ * The GPU footprint of an atlas of the given pixel size: RGBA, with the mipmap chain counted. Reads
23
+ * the atlas's full (packed) dimensions — the resize factor scales this down, so budgeting against
24
+ * the full size is what the factor is solved from.
25
+ * @param width - Atlas width in pixels.
26
+ * @param height - Atlas height in pixels.
27
+ * @returns The size in bytes.
28
+ */
29
+ export function getAtlasSizeBytes(width, height) {
30
+ return width * height * BYTES_PER_PIXEL * MIPMAP_FACTOR;
31
+ }
32
+ /**
33
+ * The scene's shared texture-memory budget. Every image atlas registers its full size here, and the
34
+ * store solves for one linear resize factor that brings the resizable atlases within `memoryLimit` —
35
+ * shared by all of them, so their relative sizes are preserved with no priority.
36
+ *
37
+ * The factor is exposed through `useSyncExternalStore`: an atlas that mounts, unmounts, or a limit
38
+ * that changes recomputes it, and a material move re-derives only the resizable atlases' textures —
39
+ * never a repack, since uv regions are normalized and so invariant to the canvas's pixel size.
40
+ * Recompute runs synchronously per registration; the render churn that would cause is left to
41
+ * React's automatic batching to coalesce, and the `FACTOR_EPSILON` gate keeps sub-pixel nudges from
42
+ * notifying at all. When no cap is set the whole apparatus stands down: {@link TextureBudget.isActive}
43
+ * is false, atlases skip registration entirely (see {@link useResizeFactor}), and the factor is 1.
44
+ */
45
+ export class TextureBudget {
46
+ /** The cap in megabytes, or undefined/`0` for no cap — a falsy value scales nothing. */
47
+ memoryLimitMb;
48
+ /** Every registered atlas; the entry object is its own handle. */
49
+ atlases = new Set();
50
+ /** Subscribers to the resize factor — the mounted resizable atlases. */
51
+ listeners = new Set();
52
+ /** The current shared linear resize factor; `1` means no scaling. */
53
+ factor = 1;
54
+ /**
55
+ * @param memoryLimitMb - The cap in megabytes, or undefined/`0` for no cap.
56
+ */
57
+ constructor(memoryLimitMb) {
58
+ this.memoryLimitMb = memoryLimitMb;
59
+ }
60
+ /**
61
+ * Whether a cap is in force. False for an absent or zero limit — nothing can be compressed, so
62
+ * consumers skip the budget entirely rather than do work that resolves to "no scaling".
63
+ * @returns Whether a positive limit is set.
64
+ */
65
+ isActive() {
66
+ return !!this.memoryLimitMb;
67
+ }
68
+ /**
69
+ * Changes the cap. Called when the renderer's `memoryLimit` prop changes, so a caller can raise,
70
+ * lower, or clear it at runtime and the resizable atlases re-derive their textures live.
71
+ * @param memoryLimitMb - The new cap, or undefined/`0` for no cap.
72
+ */
73
+ setMemoryLimit(memoryLimitMb) {
74
+ if (memoryLimitMb === this.memoryLimitMb)
75
+ return;
76
+ const wasActive = this.isActive();
77
+ this.memoryLimitMb = memoryLimitMb;
78
+ if (this.isActive() !== wasActive) {
79
+ // The cap turned on or off: recompute and always notify, so every atlas hook re-runs to
80
+ // register or drop itself and re-reads the factor, even when its numeric value did not move.
81
+ this.factor = this.computeResizeFactor();
82
+ this.notify();
83
+ }
84
+ else {
85
+ this.recompute();
86
+ }
87
+ }
88
+ /**
89
+ * Registers one atlas's full size, recomputing the factor. Only called while {@link isActive}, so
90
+ * an uncapped scene never populates this set.
91
+ * @param entry - The atlas's dimensions and kind; pass the same object back to unregister.
92
+ */
93
+ register(entry) {
94
+ this.atlases.add(entry);
95
+ this.recompute();
96
+ }
97
+ /**
98
+ * Drops an atlas's contribution, recomputing the factor.
99
+ * @param entry - The object {@link register} was given.
100
+ */
101
+ unregister(entry) {
102
+ if (this.atlases.delete(entry))
103
+ this.recompute();
104
+ }
105
+ /**
106
+ * The current shared resize factor.
107
+ * @returns The factor in `(0, 1]`.
108
+ */
109
+ getResizeFactor() {
110
+ return this.factor;
111
+ }
112
+ /**
113
+ * Subscribes to factor and active-state changes.
114
+ * @param listener - Called when the factor moves past {@link FACTOR_EPSILON} or the cap toggles.
115
+ * @returns The unsubscribe function.
116
+ */
117
+ subscribe(listener) {
118
+ this.listeners.add(listener);
119
+ return () => {
120
+ this.listeners.delete(listener);
121
+ };
122
+ }
123
+ /** Recomputes the factor and, if it moved materially, notifies subscribers. */
124
+ recompute() {
125
+ const next = this.computeResizeFactor();
126
+ if (Math.abs(next - this.factor) < FACTOR_EPSILON)
127
+ return;
128
+ this.factor = next;
129
+ this.notify();
130
+ }
131
+ /** Notifies every subscriber. */
132
+ notify() {
133
+ for (const listener of this.listeners)
134
+ listener();
135
+ }
136
+ /**
137
+ * Solves for the linear factor that fits the resizable atlases in the budget the fixed ones leave.
138
+ *
139
+ * Every degenerate case falls back to `1` — no scaling — rather than forcing a fit: no cap, nothing
140
+ * resizable to scale, or a cap the fixed atlases alone already exceed (canvases cannot be
141
+ * compressed, so there is nothing to give). The factor is only ever clamped up to `1`, never down
142
+ * to a floor: an image is scaled by however much the budget demands.
143
+ * @returns The factor in `(0, 1]`.
144
+ */
145
+ computeResizeFactor() {
146
+ // Silent: reached only when a cap is cleared at runtime, since an atlas never registers without
147
+ // one (see useResizeFactor, which logs the no-cap case at the point it skips the work).
148
+ if (!this.memoryLimitMb)
149
+ return 1;
150
+ let totalResizable = 0;
151
+ let totalFixed = 0;
152
+ for (const entry of this.atlases) {
153
+ const bytes = getAtlasSizeBytes(entry.width, entry.height);
154
+ if (entry.resizable)
155
+ totalResizable += bytes;
156
+ else
157
+ totalFixed += bytes;
158
+ }
159
+ if (totalResizable === 0) {
160
+ logger.debug('no resizable atlases, atlases will not be scaled');
161
+ return 1;
162
+ }
163
+ const budget = this.memoryLimitMb * BYTES_PER_MB - totalFixed;
164
+ if (budget <= 0) {
165
+ logger.debug('memory limit too low to resize textures, atlases will not be scaled');
166
+ return 1;
167
+ }
168
+ const factor = Math.sqrt(budget / totalResizable);
169
+ if (factor >= 1)
170
+ return 1;
171
+ logger.debug('resize factor: %s (budget %d bytes, resizable %d bytes)', factor.toFixed(3), budget, totalResizable);
172
+ return factor;
173
+ }
174
+ }
175
+ /**
176
+ * The scene's texture budget, provided by `Renderer`. The default is an uncapped budget, so an image
177
+ * layer rendered outside a `Renderer` (a test, a story) behaves exactly as before this existed —
178
+ * everything uploads at full size.
179
+ */
180
+ export const TextureBudgetContext = createContext(new TextureBudget());
181
+ /**
182
+ * Registers an atlas with the scene budget for its mounted lifetime and returns the resize factor it
183
+ * should upload at: the shared factor for a resizable atlas, always `1` for a fixed one (its pixels
184
+ * still count toward the budget, they just never scale).
185
+ *
186
+ * When the budget is inactive — no `memoryLimit` — the atlas is not registered at all: there is
187
+ * nothing to solve, so an uncapped scene does no per-atlas budget work. Turning a cap on at runtime
188
+ * flips `isActive`, which re-runs the registration effect.
189
+ * @param budget - The scene budget from context.
190
+ * @param atlas - The atlas to register and read a factor for.
191
+ * @returns The linear factor this atlas's canvas should be scaled by before upload, in `(0, 1]`.
192
+ */
193
+ export function useResizeFactor(budget, atlas) {
194
+ // The inline getters/subscribe close over `budget`; the React Compiler memoizes them on it, so
195
+ // `useSyncExternalStore` sees stable references and does not re-subscribe every render. They must
196
+ // be arrows (not `budget.subscribe`) so the call keeps its `this`.
197
+ const active = useSyncExternalStore((listener) => budget.subscribe(listener), () => budget.isActive());
198
+ const factor = useSyncExternalStore((listener) => budget.subscribe(listener), () => budget.getResizeFactor());
199
+ useEffect(() => {
200
+ // No cap in force: registering would only feed a computation that resolves to "no scaling", so
201
+ // an uncapped scene does no per-atlas budget work. A later `setMemoryLimit` flips `active` and
202
+ // re-runs this to register. The no-cap case is logged once by `Renderer`, not here per atlas.
203
+ if (!active)
204
+ return;
205
+ const entry = {
206
+ width: atlas.image.width,
207
+ height: atlas.image.height,
208
+ resizable: atlas.resizable,
209
+ };
210
+ budget.register(entry);
211
+ return () => budget.unregister(entry);
212
+ }, [budget, atlas, active]);
213
+ // A fixed atlas never scales; an uncapped budget leaves the factor at 1, so this is 1 then too.
214
+ return atlas.resizable ? factor : 1;
215
+ }
@@ -0,0 +1,56 @@
1
+ import { type Placement, type SourceReport } from '../pack-sources';
2
+ /**
3
+ * The one source the worker takes: a `Blob`.
4
+ *
5
+ * The worker could draw a bitmap or an offscreen canvas too, but it never receives one. A `Blob` is
6
+ * the only source that crosses by copy. Its handle clones and the bytes stay put, so the def that
7
+ * owns the source keeps it. Any other source would have to move, a moved source is dead on the main
8
+ * thread, and the next repack of that layer would then find nothing to draw. Tier selection keeps
9
+ * those sources on the main thread, where they pack inline.
10
+ */
11
+ export type WorkerSource = Blob;
12
+ /**
13
+ * What the main thread sends.
14
+ *
15
+ * Every message carries the `id` of the pack it belongs to. One worker serves every pack, so two
16
+ * layers can be in flight at once and the worker must keep their sources apart.
17
+ */
18
+ export type AtlasWorkerRequest =
19
+ /** One batch of sources, in the order they were collected. Sent as many times as needed. */
20
+ {
21
+ type: 'sources';
22
+ id: number;
23
+ sources: WorkerSource[];
24
+ }
25
+ /** No more batches: decode what has arrived, pack it, and reply. */
26
+ | {
27
+ type: 'pack';
28
+ id: number;
29
+ maxDimension: number;
30
+ }
31
+ /** Nobody wants this pack any more. Drop it and reply nothing. */
32
+ | {
33
+ type: 'cancel';
34
+ id: number;
35
+ };
36
+ /** One atlas the worker packed. The bitmap is transferred out, not cloned. */
37
+ export interface WorkerAtlas {
38
+ /** The atlas's pixels, moved out of the offscreen canvas they were drawn into. */
39
+ bitmap: ImageBitmap;
40
+ /** Every placement on this atlas, addressing sources by their index in the batches. */
41
+ placements: Placement[];
42
+ /** Whether these pixels may be downscaled to fit a memory budget. */
43
+ resizable: boolean;
44
+ }
45
+ /** What the worker sends back — at most one of these per pack, carrying that pack's `id`. */
46
+ export type AtlasWorkerResponse = {
47
+ type: 'packed';
48
+ id: number;
49
+ atlases: WorkerAtlas[];
50
+ reports: SourceReport[];
51
+ } | {
52
+ type: 'failed';
53
+ id: number;
54
+ message: string;
55
+ };
56
+ //# sourceMappingURL=atlas-worker.d.ts.map
@@ -0,0 +1,123 @@
1
+ import { packSources, } from '../pack-sources';
2
+ /**
3
+ * Waits for every batch to decode, packs them, and moves each atlas's pixels out of its canvas.
4
+ * @param id - Which pack to run.
5
+ * @param maxDimension - Largest atlas edge in pixels.
6
+ * @returns The reply to post back, or null when the pack was canceled and nothing is owed.
7
+ */
8
+ async function pack(id, maxDimension) {
9
+ // `requestFor` and not `requests.get`, because a layer with no sources sends its closing message
10
+ // and nothing else. That is an empty pack, and it still owes a reply: without one the layer
11
+ // waits forever and draws nothing. The only other way to reach this with no state is a cancel
12
+ // that arrived first, and that happens only when the closing message was never sent, so the
13
+ // empty reply it produces goes to a caller that has already forgotten the id and is discarded.
14
+ const request = requestFor(id);
15
+ request.packing = true;
16
+ const decoded = await Promise.all(request.decoding);
17
+ requests.delete(id);
18
+ // The only yield point in a pack. `packSources` runs straight through once it starts, so this is
19
+ // the last moment a cancel can save the worker's time for the next pack.
20
+ if (request.canceled) {
21
+ closeDecoded(decoded);
22
+ return null;
23
+ }
24
+ // `createAtlasCanvas` returns an `OffscreenCanvas` here and never takes its fallback branch. A
25
+ // worker runs at all only because `selectTier` found `OffscreenCanvas` on the main thread, and
26
+ // the constructor is exposed to both globals or to neither. No fallback is worth writing: a
27
+ // worker has no `document`, so without `OffscreenCanvas` it could build no canvas at all.
28
+ const { atlases, reports } = packSources(decoded, maxDimension);
29
+ // `transferToImageBitmap` moves the canvas's backing bitmap out instead of copying it, which is
30
+ // what makes an atlas cheap to return. The canvas is left blank, and this code drops it anyway.
31
+ const packed = atlases.map((atlas) => ({
32
+ bitmap: atlas.canvas.transferToImageBitmap(),
33
+ placements: atlas.placements,
34
+ resizable: atlas.resizable,
35
+ }));
36
+ // The atlases hold the pixels now, so every decoded bitmap is dead. This worker decoded all of
37
+ // them and owns all of them. Close them rather than leaving a thousand to the collector.
38
+ closeDecoded(decoded);
39
+ return { type: 'packed', id, atlases: packed, reports };
40
+ }
41
+ /**
42
+ * Abandons one pack.
43
+ *
44
+ * A pack that has not reached {@link pack} yet is dropped here, because nothing else will ever read
45
+ * its decodes. A pack that is already running is only marked: it reads the mark at its one yield
46
+ * point and cleans up there.
47
+ * @param id - Which pack to abandon.
48
+ */
49
+ function cancel(id) {
50
+ const request = requests.get(id);
51
+ // Already replied, or never started. Either way this worker holds nothing for that pack.
52
+ if (!request)
53
+ return;
54
+ request.canceled = true;
55
+ if (request.packing)
56
+ return;
57
+ requests.delete(id);
58
+ void Promise.all(request.decoding).then(closeDecoded);
59
+ }
60
+ /**
61
+ * Frees decoded pixels this worker owns.
62
+ *
63
+ * An `ImageBitmap` holds its pixels outside the JavaScript heap, where collection does not reclaim
64
+ * them promptly. A canceled pack of a thousand logos would otherwise sit on a thousand of them.
65
+ * @param decoded - What the sources decoded into.
66
+ */
67
+ function closeDecoded(decoded) {
68
+ for (const source of decoded)
69
+ if (source && 'close' in source)
70
+ source.close();
71
+ }
72
+ /**
73
+ * Finds a pack's state, starting it on the first batch that arrives.
74
+ * @param id - Which pack the batch belongs to.
75
+ * @returns That pack's state.
76
+ */
77
+ function requestFor(id) {
78
+ let request = requests.get(id);
79
+ if (!request) {
80
+ request = { decoding: [], canceled: false, packing: false };
81
+ requests.set(id, request);
82
+ }
83
+ return request;
84
+ }
85
+ /**
86
+ * Every pack this worker is working on, by request id.
87
+ *
88
+ * The main thread does not terminate this worker once it has returned an atlas, because those
89
+ * atlases are backed by GPU memory the worker owns. It stops a worker only when that worker broke
90
+ * before it produced anything. So one worker serves every pack of every layer, for the life of the
91
+ * page, and each pack's state has to be kept apart from the others'. An entry is deleted as soon
92
+ * as its pack replies or is canceled.
93
+ */
94
+ const requests = new Map();
95
+ /** This worker's global scope. */
96
+ const scope = self;
97
+ scope.onmessage = (event) => {
98
+ const message = event.data;
99
+ if (message.type === 'sources') {
100
+ const request = requestFor(message.id);
101
+ // Null rather than a rejection, so one set of bytes that is not an image drops its own def
102
+ // instead of failing the whole layer's pack. `packSources` skips it as a source with no pixels.
103
+ for (const source of message.sources) {
104
+ request.decoding.push(createImageBitmap(source).catch(() => null));
105
+ }
106
+ return;
107
+ }
108
+ if (message.type === 'cancel') {
109
+ cancel(message.id);
110
+ return;
111
+ }
112
+ const { id } = message;
113
+ pack(id, message.maxDimension).then((response) => {
114
+ // Nothing is owed for a canceled pack. The main thread has already settled it.
115
+ if (!response)
116
+ return;
117
+ // The bitmaps move rather than copy. After this the worker holds nothing for this pack.
118
+ scope.postMessage(response, response.atlases.map((atlas) => atlas.bitmap));
119
+ }, (error) => {
120
+ requests.delete(id);
121
+ scope.postMessage({ type: 'failed', id, message: String(error) });
122
+ });
123
+ };
@@ -0,0 +1,62 @@
1
+ import { type SourceReport } from '../pack-sources';
2
+ import { type WorkerAtlas, type WorkerSource } from './atlas-worker';
3
+ /** Options for {@link packInWorker}. */
4
+ export interface PackInWorkerOptions {
5
+ /** Abandons the pack. The worker is told to drop it, and a reply that still arrives is thrown away. */
6
+ signal?: AbortSignal;
7
+ /**
8
+ * Makes the pooled worker, on the first pack that needs it. Defaults to the bundled atlas worker,
9
+ * and exists so a spec can drive the protocol against a fake one without a browser. It is ignored
10
+ * once a worker is pooled.
11
+ */
12
+ createWorker?: () => Worker;
13
+ }
14
+ /**
15
+ * Packs a set of sources in a web worker.
16
+ *
17
+ * **One worker serves every pack, and no pack ever stops it.** The atlases come back as
18
+ * `ImageBitmap`s that `OffscreenCanvas.transferToImageBitmap()` produced, and Chrome backs those
19
+ * with GPU memory the worker owns. `Images` re-reads an atlas's pixels on every texture-budget
20
+ * move, and disposing a texture forces a fresh upload, so the producing worker has to outlive every
21
+ * bitmap it made, not only the first upload of it. Terminating the worker used to run before this
22
+ * function resolved, which destroyed those pixels and left booth logos blank on real GPUs.
23
+ *
24
+ * The one `terminate()` in this file is in `retire`, and it only ever stops a worker that broke
25
+ * before it handed out an atlas. A worker that has produced pixels is dropped from the pool and
26
+ * left running. The worker script is inlined into the bundle, so starting one resolves no URL at
27
+ * runtime, which matters because the SDK is loaded from origins it does not control.
28
+ *
29
+ * Each pack carries an `id`, because a shared worker can hold two layers' packs at once and a reply
30
+ * has to find its own caller. An abandoned pack is settled here and its `id` forgotten, so a reply
31
+ * that still arrives is discarded — and its atlases are closed, or each superseded pack would leak
32
+ * a layer's worth of GPU memory.
33
+ *
34
+ * The sources go over in batches so the main thread is never blocked for long by one structured
35
+ * clone, and so the worker can decode the early batches while the later ones are still being sent.
36
+ * A closing message tells it every source has arrived.
37
+ * @param sources - The unique sources to pack, in the caller's order; placements address them by
38
+ * their index in this array.
39
+ * @param maxDimension - Largest atlas edge in pixels.
40
+ * @param options - The abort signal, and the worker factory a spec overrides.
41
+ * @returns The packed atlases and one report per source.
42
+ */
43
+ export declare function packInWorker(sources: readonly WorkerSource[], maxDimension: number, options?: PackInWorkerOptions): Promise<{
44
+ atlases: WorkerAtlas[];
45
+ reports: SourceReport[];
46
+ }>;
47
+ /**
48
+ * Forgets the pooled worker without stopping it, and abandons every pack waiting on it.
49
+ *
50
+ * Only a spec calls this. Each spec drives its own fake worker, and the pool would otherwise hand
51
+ * the first spec's fake to every spec after it.
52
+ */
53
+ export declare function resetAtlasWorkerPool(): void;
54
+ /**
55
+ * How many sources one `postMessage` carries.
56
+ *
57
+ * Sending every source in one message left 18 dropped frames at a thousand logos on a 4x-throttled
58
+ * CPU; sending them in batches of 25 left one. Chunking is what removes the last stall, so this is
59
+ * the number that matters most in this file — tune it against the benchmark, not by taste.
60
+ */
61
+ export declare const SOURCE_CHUNK_SIZE = 25;
62
+ //# sourceMappingURL=client.d.ts.map