@aardworx/wombat.rendering 0.8.0 → 0.9.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 (136) hide show
  1. package/dist/core/command.d.ts +7 -9
  2. package/dist/core/command.d.ts.map +1 -1
  3. package/dist/core/renderTask.d.ts +64 -1
  4. package/dist/core/renderTask.d.ts.map +1 -1
  5. package/dist/core/renderTask.js +3 -1
  6. package/dist/core/renderTask.js.map +1 -1
  7. package/dist/resources/preparedRenderObject.js +3 -3
  8. package/dist/resources/preparedRenderObject.js.map +1 -1
  9. package/dist/resources/uniformBuffer.d.ts.map +1 -1
  10. package/dist/resources/uniformBuffer.js +17 -0
  11. package/dist/resources/uniformBuffer.js.map +1 -1
  12. package/dist/runtime/derivedUniforms/dispatch.d.ts +105 -0
  13. package/dist/runtime/derivedUniforms/dispatch.d.ts.map +1 -0
  14. package/dist/runtime/derivedUniforms/dispatch.js +355 -0
  15. package/dist/runtime/derivedUniforms/dispatch.js.map +1 -0
  16. package/dist/runtime/derivedUniforms/index.d.ts +6 -0
  17. package/dist/runtime/derivedUniforms/index.d.ts.map +1 -0
  18. package/dist/runtime/derivedUniforms/index.js +26 -0
  19. package/dist/runtime/derivedUniforms/index.js.map +1 -0
  20. package/dist/runtime/derivedUniforms/recipes.d.ts +42 -0
  21. package/dist/runtime/derivedUniforms/recipes.d.ts.map +1 -0
  22. package/dist/runtime/derivedUniforms/recipes.js +134 -0
  23. package/dist/runtime/derivedUniforms/recipes.js.map +1 -0
  24. package/dist/runtime/derivedUniforms/sceneIntegration.d.ts +56 -0
  25. package/dist/runtime/derivedUniforms/sceneIntegration.d.ts.map +1 -0
  26. package/dist/runtime/derivedUniforms/sceneIntegration.js +171 -0
  27. package/dist/runtime/derivedUniforms/sceneIntegration.js.map +1 -0
  28. package/dist/runtime/derivedUniforms/slotId.d.ts +15 -0
  29. package/dist/runtime/derivedUniforms/slotId.d.ts.map +1 -0
  30. package/dist/runtime/derivedUniforms/slotId.js +30 -0
  31. package/dist/runtime/derivedUniforms/slotId.js.map +1 -0
  32. package/dist/runtime/derivedUniforms/slots.d.ts +49 -0
  33. package/dist/runtime/derivedUniforms/slots.d.ts.map +1 -0
  34. package/dist/runtime/derivedUniforms/slots.js +123 -0
  35. package/dist/runtime/derivedUniforms/slots.js.map +1 -0
  36. package/dist/runtime/derivedUniforms/uberKernel.wgsl.d.ts +13 -0
  37. package/dist/runtime/derivedUniforms/uberKernel.wgsl.d.ts.map +1 -0
  38. package/dist/runtime/derivedUniforms/uberKernel.wgsl.js +218 -0
  39. package/dist/runtime/derivedUniforms/uberKernel.wgsl.js.map +1 -0
  40. package/dist/runtime/flattenTree.d.ts +5 -0
  41. package/dist/runtime/flattenTree.d.ts.map +1 -0
  42. package/dist/runtime/flattenTree.js +45 -0
  43. package/dist/runtime/flattenTree.js.map +1 -0
  44. package/dist/runtime/heapAdapter.d.ts +16 -0
  45. package/dist/runtime/heapAdapter.d.ts.map +1 -0
  46. package/dist/runtime/heapAdapter.js +263 -0
  47. package/dist/runtime/heapAdapter.js.map +1 -0
  48. package/dist/runtime/heapEffect.d.ts +235 -0
  49. package/dist/runtime/heapEffect.d.ts.map +1 -0
  50. package/dist/runtime/heapEffect.js +518 -0
  51. package/dist/runtime/heapEffect.js.map +1 -0
  52. package/dist/runtime/heapEffectIR.d.ts +34 -0
  53. package/dist/runtime/heapEffectIR.d.ts.map +1 -0
  54. package/dist/runtime/heapEffectIR.js +1161 -0
  55. package/dist/runtime/heapEffectIR.js.map +1 -0
  56. package/dist/runtime/heapEligibility.d.ts +9 -0
  57. package/dist/runtime/heapEligibility.d.ts.map +1 -0
  58. package/dist/runtime/heapEligibility.js +195 -0
  59. package/dist/runtime/heapEligibility.js.map +1 -0
  60. package/dist/runtime/heapScene.d.ts +240 -31
  61. package/dist/runtime/heapScene.d.ts.map +1 -1
  62. package/dist/runtime/heapScene.js +3334 -265
  63. package/dist/runtime/heapScene.js.map +1 -1
  64. package/dist/runtime/heapShaderFamily.d.ts +132 -0
  65. package/dist/runtime/heapShaderFamily.d.ts.map +1 -0
  66. package/dist/runtime/heapShaderFamily.js +866 -0
  67. package/dist/runtime/heapShaderFamily.js.map +1 -0
  68. package/dist/runtime/hybridScene.d.ts +133 -0
  69. package/dist/runtime/hybridScene.d.ts.map +1 -0
  70. package/dist/runtime/hybridScene.js +159 -0
  71. package/dist/runtime/hybridScene.js.map +1 -0
  72. package/dist/runtime/index.d.ts +8 -0
  73. package/dist/runtime/index.d.ts.map +1 -1
  74. package/dist/runtime/index.js +9 -0
  75. package/dist/runtime/index.js.map +1 -1
  76. package/dist/runtime/renderTask.d.ts +21 -4
  77. package/dist/runtime/renderTask.d.ts.map +1 -1
  78. package/dist/runtime/renderTask.js +127 -32
  79. package/dist/runtime/renderTask.js.map +1 -1
  80. package/dist/runtime/runtime.d.ts +33 -3
  81. package/dist/runtime/runtime.d.ts.map +1 -1
  82. package/dist/runtime/runtime.js +12 -3
  83. package/dist/runtime/runtime.js.map +1 -1
  84. package/dist/runtime/scenePass.d.ts +28 -3
  85. package/dist/runtime/scenePass.d.ts.map +1 -1
  86. package/dist/runtime/scenePass.js +37 -4
  87. package/dist/runtime/scenePass.js.map +1 -1
  88. package/dist/runtime/textureAtlas/atlasMipGutterKernel.d.ts +35 -0
  89. package/dist/runtime/textureAtlas/atlasMipGutterKernel.d.ts.map +1 -0
  90. package/dist/runtime/textureAtlas/atlasMipGutterKernel.js +247 -0
  91. package/dist/runtime/textureAtlas/atlasMipGutterKernel.js.map +1 -0
  92. package/dist/runtime/textureAtlas/atlasPool.d.ts +240 -0
  93. package/dist/runtime/textureAtlas/atlasPool.d.ts.map +1 -0
  94. package/dist/runtime/textureAtlas/atlasPool.js +798 -0
  95. package/dist/runtime/textureAtlas/atlasPool.js.map +1 -0
  96. package/dist/runtime/textureAtlas/bvhTree2d.d.ts +33 -0
  97. package/dist/runtime/textureAtlas/bvhTree2d.d.ts.map +1 -0
  98. package/dist/runtime/textureAtlas/bvhTree2d.js +501 -0
  99. package/dist/runtime/textureAtlas/bvhTree2d.js.map +1 -0
  100. package/dist/runtime/textureAtlas/packer.d.ts +50 -0
  101. package/dist/runtime/textureAtlas/packer.d.ts.map +1 -0
  102. package/dist/runtime/textureAtlas/packer.js +390 -0
  103. package/dist/runtime/textureAtlas/packer.js.map +1 -0
  104. package/dist/window/loop.d.ts +14 -0
  105. package/dist/window/loop.d.ts.map +1 -1
  106. package/dist/window/loop.js +18 -5
  107. package/dist/window/loop.js.map +1 -1
  108. package/package.json +5 -4
  109. package/src/core/command.ts +9 -9
  110. package/src/core/renderTask.ts +55 -2
  111. package/src/resources/preparedRenderObject.ts +3 -3
  112. package/src/resources/uniformBuffer.ts +15 -0
  113. package/src/runtime/derivedUniforms/dispatch.ts +407 -0
  114. package/src/runtime/derivedUniforms/index.ts +52 -0
  115. package/src/runtime/derivedUniforms/recipes.ts +159 -0
  116. package/src/runtime/derivedUniforms/sceneIntegration.ts +239 -0
  117. package/src/runtime/derivedUniforms/slotId.ts +35 -0
  118. package/src/runtime/derivedUniforms/slots.ts +155 -0
  119. package/src/runtime/derivedUniforms/uberKernel.wgsl.ts +220 -0
  120. package/src/runtime/flattenTree.ts +46 -0
  121. package/src/runtime/heapAdapter.ts +314 -0
  122. package/src/runtime/heapEffect.ts +719 -0
  123. package/src/runtime/heapEffectIR.ts +1211 -0
  124. package/src/runtime/heapEligibility.ts +189 -0
  125. package/src/runtime/heapScene.ts +3981 -349
  126. package/src/runtime/heapShaderFamily.ts +1102 -0
  127. package/src/runtime/hybridScene.ts +310 -0
  128. package/src/runtime/index.ts +46 -0
  129. package/src/runtime/renderTask.ts +167 -32
  130. package/src/runtime/runtime.ts +37 -4
  131. package/src/runtime/scenePass.ts +39 -4
  132. package/src/runtime/textureAtlas/atlasMipGutterKernel.ts +291 -0
  133. package/src/runtime/textureAtlas/atlasPool.ts +991 -0
  134. package/src/runtime/textureAtlas/bvhTree2d.ts +530 -0
  135. package/src/runtime/textureAtlas/packer.ts +429 -0
  136. package/src/window/loop.ts +32 -6
@@ -0,0 +1,798 @@
1
+ // AtlasPool — aval-keyed refcounted sub-rect allocator over shared
2
+ // 4096×4096 atlas pages.
3
+ //
4
+ // Mirrors the UniformPool / IndexPool pattern in heapScene: identity
5
+ // keyed on the source `aval<ITexture>`; multiple ROs referencing the
6
+ // same texture share one sub-rect; refcount drives free-on-release.
7
+ //
8
+ // Storage shape: per format, N independent 2D `GPUTexture` "pages",
9
+ // each its own 4096² image. The shader binds N consecutive single-
10
+ // texture BGL slots per format (linear/srgb) and switches on
11
+ // `pageRef` (0..N-1). Adding a new page = allocate a fresh
12
+ // GPUTexture and slot it into the next bind-group entry — no GPU-
13
+ // side `copyTextureToTexture` of the array, unlike a `texture_2d_array`
14
+ // design.
15
+ //
16
+ // `binding_array<texture_2d<f32>, N>` would be simpler in the shader
17
+ // but requires Chrome's experimental `bindless-textures` feature;
18
+ // WebGPU 1.0's `GPUBindGroupEntry.resource` doesn't accept arrays of
19
+ // views. The N-consecutive-bindings approach is core WebGPU 1.0.
20
+ //
21
+ // MVP scope (per docs/heap-textures-plan.md):
22
+ // - Tier S/M: source ≤ 1024×1024.
23
+ // - Two formats: rgba8unorm, rgba8unorm-srgb.
24
+ // - No eviction. New pages are allocated lazily up to
25
+ // ATLAS_MAX_PAGES_PER_FORMAT; allocations past that throw.
26
+ //
27
+ // Mipped textures use the classic Iliffe / id Tech 1.5×1 layout: the
28
+ // packer reserves a `1.5W × H` rect, mip 0 occupies the left W×H block
29
+ // and mips 1..N stack vertically in the right W/2-wide column. CPU
30
+ // mip generation (canvas-2d) for v1; compute-shader path is future.
31
+ import { V2f, V2i } from "@aardworx/wombat.base";
32
+ import { TexturePacking } from "./packer.js";
33
+ import { buildMipsAndGutterOnGpu } from "./atlasMipGutterKernel.js";
34
+ export const ATLAS_PAGE_SIZE = 4096;
35
+ /** Tier S/M source-side dimension cap. */
36
+ export const ATLAS_MAX_DIM = 1024;
37
+ /**
38
+ * Maximum number of independent pages allowed per format. The shader's
39
+ * `switch pageRef` runs an N-way ladder; the BGL declares N consecutive
40
+ * texture slots per format. Allocations beyond this throw.
41
+ */
42
+ export const ATLAS_MAX_PAGES_PER_FORMAT = 8;
43
+ export const ATLAS_PAGE_FORMATS = ["rgba8unorm", "rgba8unorm-srgb"];
44
+ /** Format index used in the drawHeader: 0 = linear, 1 = srgb. */
45
+ export const atlasFormatIndex = (f) => f === "rgba8unorm-srgb" ? 1 : 0;
46
+ /**
47
+ * Default mip count for a `w×h` source: full chain down to 1×1.
48
+ */
49
+ export const defaultMipCount = (w, h) => Math.floor(Math.log2(Math.max(w, h))) + 1;
50
+ /**
51
+ * Mip-k pixel size given mip-0 size `(w,h)`. Halves each level and
52
+ * floors at 1×1.
53
+ */
54
+ export const mipPixelSize = (w, h, k) => ({
55
+ w: Math.max(1, w >> k),
56
+ h: Math.max(1, h >> k),
57
+ });
58
+ /**
59
+ * Mip-k interior offset from mip-0 interior, both inside the embedded
60
+ * Iliffe pyramid. Mip 0 is at (0,0). Mips 1..N stack vertically in
61
+ * the column at x = W + 4 (mip-0 width + 2 px right gutter of mip-0
62
+ * + 2 px left gutter of the mip column). Each mip k>=1 sits below
63
+ * mip k-1 with 4 px of vertical gutter between them (2 px bottom of
64
+ * mip k-1 + 2 px top of mip k):
65
+ * y_k = sum_{j=1..k-1} (max(1, H >> j) + 4).
66
+ * Caller writes at `(uploadOrigin + mipOffsetInPyramid(w, h, k))`
67
+ * where uploadOrigin is mip-0's interior pixel position.
68
+ */
69
+ export const mipOffsetInPyramid = (w, h, k) => {
70
+ if (k === 0)
71
+ return { x: 0, y: 0 };
72
+ let y = 0;
73
+ for (let j = 1; j < k; j++)
74
+ y += Math.max(1, h >> j) + 4;
75
+ return { x: w + 4, y };
76
+ };
77
+ /**
78
+ * Pull `(format, width, height, mipLevelCount, host?)` out of an
79
+ * ITexture for atlas eligibility. Mirrors `heapAdapter.describeTexture`
80
+ * — kept local so `repack` can validate without importing the adapter.
81
+ * Returns `null` for sources we can't measure.
82
+ */
83
+ function describeAtlasTexture(t) {
84
+ if (t.kind === "gpu") {
85
+ const tex = t.texture;
86
+ return {
87
+ format: tex.format, width: tex.width, height: tex.height,
88
+ mipLevelCount: tex.mipLevelCount,
89
+ };
90
+ }
91
+ const src = t.source;
92
+ if (src.kind === "raw") {
93
+ return {
94
+ format: src.format, width: src.width, height: src.height,
95
+ mipLevelCount: src.mipLevelCount ?? 1, host: src,
96
+ };
97
+ }
98
+ const ext = src.source;
99
+ let w = 0, h = 0;
100
+ if (typeof HTMLVideoElement !== "undefined" && ext instanceof HTMLVideoElement) {
101
+ w = ext.videoWidth;
102
+ h = ext.videoHeight;
103
+ }
104
+ else if (typeof ImageData !== "undefined" && ext instanceof ImageData) {
105
+ w = ext.width;
106
+ h = ext.height;
107
+ }
108
+ else {
109
+ const any = ext;
110
+ w = any.width ?? 0;
111
+ h = any.height ?? 0;
112
+ }
113
+ if (w <= 0 || h <= 0)
114
+ return null;
115
+ return {
116
+ format: src.format ?? "rgba8unorm",
117
+ width: w, height: h,
118
+ mipLevelCount: src.generateMips ? Math.floor(Math.log2(Math.max(w, h))) + 1 : 1,
119
+ host: src,
120
+ };
121
+ }
122
+ /**
123
+ * Aval-identity keyed pool over the per-format page sets. Multiple
124
+ * acquisitions of the same `aval<ITexture>` share one sub-rect; the
125
+ * refcount drives release.
126
+ */
127
+ export class AtlasPool {
128
+ device;
129
+ pagesByFormat = new Map();
130
+ pageAddedListeners = new Map();
131
+ entriesByAval = new Map();
132
+ entriesByRef = new Map();
133
+ /**
134
+ * §5b value-equality dedup map. Constant avals (`isConstant ===
135
+ * true`) wrapping the same inner GPUTexture (kind:"gpu") or
136
+ * HostTextureSource (kind:"host") collapse to one sub-rect.
137
+ * Reactive avals fall through to identity-only since their
138
+ * content can change.
139
+ */
140
+ entriesByValueKey = new Map();
141
+ nextRef = 1;
142
+ innerKeyOf(t) {
143
+ return t.kind === "gpu" ? t.texture : t.source;
144
+ }
145
+ constructor(device) {
146
+ this.device = device;
147
+ for (const f of ATLAS_PAGE_FORMATS) {
148
+ this.pagesByFormat.set(f, []);
149
+ this.pageAddedListeners.set(f, new Set());
150
+ }
151
+ }
152
+ /** All currently allocated pages for a given format. */
153
+ pagesFor(format) {
154
+ return this.pagesByFormat.get(format) ?? [];
155
+ }
156
+ /**
157
+ * Subscribe to page-allocation events. Fires after a fresh
158
+ * `AtlasPage` is appended to the format's page list (its `pageId`
159
+ * is the slot the bind group should now wire up).
160
+ */
161
+ onPageAdded(format, cb) {
162
+ const set = this.pageAddedListeners.get(format);
163
+ set.add(cb);
164
+ return { dispose: () => { set.delete(cb); } };
165
+ }
166
+ /** True for pages we can drive via the compute mip+gutter kernel.
167
+ * The kernel is buffer-based: it allocates a scratch storage
168
+ * buffer matching the sub-rect's bounding box, runs all mip
169
+ * downscale + gutter fill in compute passes over the buffer, then
170
+ * one `copyBufferToTexture` uploads the whole region to the page.
171
+ * No texture storage bindings are needed and no device features
172
+ * are required — works in core WebGPU 1.0. The only constraint is
173
+ * that JS-side pixel extraction succeeds, which falls back to the
174
+ * CPU path on headless platforms without canvas.
175
+ *
176
+ * Disabled in node mock-GPU mode (which lacks queue.onSubmittedWorkDone). */
177
+ canUseGpuKernel(_page) {
178
+ return typeof this.device.queue.onSubmittedWorkDone === "function";
179
+ }
180
+ allocatePage(format, pageId) {
181
+ // rgba8unorm pages gain STORAGE_BINDING for the compute mip+gutter
182
+ // kernel. rgba8unorm-srgb isn't a storage-capable format in WebGPU
183
+ // 1.0 (without an unportable feature), so srgb pages fall back to
184
+ // the CPU buildExtendedWithGutter path. Mixed strategy: linear
185
+ // textures get the fast compute path; srgb textures take the CPU
186
+ // path. Both produce identical visible output — only the kernel
187
+ // path differs.
188
+ const texture = this.device.createTexture({
189
+ label: `atlas/${format}/${pageId}`,
190
+ size: { width: ATLAS_PAGE_SIZE, height: ATLAS_PAGE_SIZE, depthOrArrayLayers: 1 },
191
+ format,
192
+ mipLevelCount: 1,
193
+ usage: GPUTextureUsage.TEXTURE_BINDING |
194
+ GPUTextureUsage.COPY_DST |
195
+ GPUTextureUsage.COPY_SRC |
196
+ GPUTextureUsage.RENDER_ATTACHMENT,
197
+ });
198
+ return {
199
+ format,
200
+ texture,
201
+ packing: TexturePacking.empty(new V2i(ATLAS_PAGE_SIZE, ATLAS_PAGE_SIZE), false),
202
+ pageId,
203
+ };
204
+ }
205
+ /**
206
+ * Tier-S eligibility: dimension threshold + supported format. If
207
+ * this returns null, the caller should route the texture down the
208
+ * Tier L (standalone) path.
209
+ */
210
+ static eligibleFormat(format) {
211
+ if (format === "rgba8unorm")
212
+ return "rgba8unorm";
213
+ if (format === "rgba8unorm-srgb")
214
+ return "rgba8unorm-srgb";
215
+ return null;
216
+ }
217
+ static eligibleSize(width, height) {
218
+ return width > 0 && height > 0 && width <= ATLAS_MAX_DIM && height <= ATLAS_MAX_DIM;
219
+ }
220
+ /**
221
+ * Acquire (or share) a sub-rect. The returned `AtlasAcquisition`
222
+ * gives the page slot index plus the mip-0 `origin`/`size` (in
223
+ * normalized atlas coords) and `numMips`. If the texture is already
224
+ * known (same aval), refcount bumps and we return the existing
225
+ * placement.
226
+ */
227
+ acquire(format, sourceAval, width, height, opts = {}) {
228
+ const existing = this.entriesByAval.get(sourceAval);
229
+ if (existing !== undefined) {
230
+ existing.refcount++;
231
+ return this.makeResult(existing);
232
+ }
233
+ // §5b: value-equality dedup for constant avals. Two distinct
234
+ // constant avals wrapping the same inner GPUTexture or
235
+ // HostTextureSource share one sub-rect. We need a *value* (not
236
+ // just dimensions) to compute the key; pull from `opts.source`
237
+ // if provided, otherwise skip dedup (fresh allocation).
238
+ let valueKey;
239
+ if (sourceAval.isConstant) {
240
+ const sourceVal = opts.source;
241
+ if (sourceVal !== undefined && sourceVal.host !== undefined) {
242
+ valueKey = sourceVal.host;
243
+ }
244
+ // For kind:"gpu" sources, opts.source.host is undefined; the
245
+ // caller (heapAdapter) provides the GPUTexture via a different
246
+ // path and we'd need to thread it through. v1: skip dedup for
247
+ // GPU-backed constants, dedup only host-backed constants. The
248
+ // realistic dedup case is shared ImageBitmap / ImageData; GPU
249
+ // textures are typically already deduped at the user layer.
250
+ if (valueKey !== undefined) {
251
+ const shared = this.entriesByValueKey.get(valueKey);
252
+ if (shared !== undefined) {
253
+ shared.refcount++;
254
+ shared.aliases.push(sourceAval);
255
+ this.entriesByAval.set(sourceAval, shared);
256
+ return this.makeResult(shared);
257
+ }
258
+ }
259
+ }
260
+ const wantsMips = opts.wantsMips === true;
261
+ const numMips = wantsMips
262
+ ? Math.max(1, Math.min(opts.numMips ?? defaultMipCount(width, height), defaultMipCount(width, height)))
263
+ : 1;
264
+ // Every sub-rect (and every mip slot in mipped pyramids) carries a
265
+ // 2-px gutter on each side. Inner ring = clamp-replicate edge
266
+ // texel, outer ring = wrap (opposite-edge texel). Required so the
267
+ // shader's hardware-bilinear at the sub-rect edge doesn't bleed
268
+ // into a neighboring sub-rect and so the repeat-mode `shift ±1`
269
+ // can land on the opposite-edge data.
270
+ //
271
+ // Non-mipped layout: (W+4) × (H+4); mip-0 interior at (+2, +2).
272
+ // Mipped Iliffe layout: mip-0 owns (W+4) × (H+4); mips 1..N stack
273
+ // vertically in the next column starting at x = W + 4, each with
274
+ // its own 4-px gutter padding on each axis. Pyramid bounding rect
275
+ // is `(W + 4 + maxMipW + 4) × max(H+4, sumMipH+gaps)` — sized
276
+ // generously to ensure non-overlap.
277
+ const reservedW = wantsMips
278
+ ? (width + 4) + Math.max(1, width >> 1) + 4
279
+ : width + 4;
280
+ let reservedH = height + 4;
281
+ if (wantsMips) {
282
+ let stackedH = 0;
283
+ for (let k = 1; k < numMips; k++) {
284
+ stackedH += Math.max(1, height >> k) + 4;
285
+ }
286
+ reservedH = Math.max(reservedH, stackedH);
287
+ }
288
+ const size = new V2i(reservedW, reservedH);
289
+ const pages = this.pagesByFormat.get(format);
290
+ // Try fitting into existing pages first.
291
+ for (let i = 0; i < pages.length; i++) {
292
+ const page = pages[i];
293
+ const next = page.packing.tryAdd(this.nextRef, size);
294
+ if (next !== null) {
295
+ page.packing = next;
296
+ return this.finalize(sourceAval, format, i, page, opts.source, width, height, numMips, valueKey);
297
+ }
298
+ }
299
+ // Allocate a fresh page if we haven't hit the max.
300
+ if (pages.length >= ATLAS_MAX_PAGES_PER_FORMAT) {
301
+ throw new Error(`atlas: ATLAS_MAX_PAGES_PER_FORMAT (${ATLAS_MAX_PAGES_PER_FORMAT}) exceeded for format ${format}`);
302
+ }
303
+ const pageId = pages.length;
304
+ const page = this.allocatePage(format, pageId);
305
+ const placed = page.packing.tryAdd(this.nextRef, size);
306
+ if (placed === null) {
307
+ throw new Error(`AtlasPool: ${reservedW}×${reservedH} doesn't fit a fresh ${ATLAS_PAGE_SIZE}² page`);
308
+ }
309
+ page.packing = placed;
310
+ pages.push(page);
311
+ for (const cb of this.pageAddedListeners.get(format))
312
+ cb(pageId);
313
+ return this.finalize(sourceAval, format, pageId, page, opts.source, width, height, numMips, valueKey);
314
+ }
315
+ /**
316
+ * Look up the existing acquisition for a source aval, or `undefined`
317
+ * if the aval is not currently held in the pool. Mirrors
318
+ * `UniformPool.entry` — used by the heap path's reactivity loop to
319
+ * detect "this aval has live refs and just marked".
320
+ */
321
+ entry(sourceAval) {
322
+ const e = this.entriesByAval.get(sourceAval);
323
+ return e === undefined ? undefined : this.makeResult(e);
324
+ }
325
+ /**
326
+ * Re-pack an aval's atlas placement against a new ITexture value.
327
+ * Mirrors `UniformPool.repack(av, newValue)` — used when an atlas-
328
+ * routed `aval<ITexture>` source swaps. Frees the old sub-rect (and
329
+ * the page slot in the packer) and acquires a fresh one for the new
330
+ * texture; the returned `AtlasAcquisition` carries the (potentially
331
+ * different) `pageId`/`origin`/`size`/`numMips`/`ref`. Refcount is
332
+ * preserved across the swap so multi-RO sharing keeps working.
333
+ *
334
+ * Throws if the new texture isn't Tier-S eligible (different format
335
+ * outside rgba8unorm/srgb, or larger than the cap). Callers should
336
+ * rely on the heap eligibility classifier, but this is a safety net.
337
+ */
338
+ repack(sourceAval, newTexture, opts = {}) {
339
+ const old = this.entriesByAval.get(sourceAval);
340
+ if (old === undefined) {
341
+ throw new Error("AtlasPool.repack: source aval has no live entry");
342
+ }
343
+ // Resolve the new texture's eligibility + dimensions.
344
+ const desc = describeAtlasTexture(newTexture);
345
+ if (desc === null) {
346
+ throw new Error("AtlasPool.repack: new ITexture has no measurable dimensions");
347
+ }
348
+ const fmt = AtlasPool.eligibleFormat(desc.format);
349
+ if (fmt === null) {
350
+ throw new Error(`AtlasPool.repack: new texture format ${desc.format} is not Tier-S eligible`);
351
+ }
352
+ if (!AtlasPool.eligibleSize(desc.width, desc.height)) {
353
+ throw new Error(`AtlasPool.repack: new texture ${desc.width}×${desc.height} exceeds Tier-S cap`);
354
+ }
355
+ // Repack on a deduped entry (multiple aliases sharing one
356
+ // sub-rect) is undefined: we'd be free-and-realloc'ing the
357
+ // sub-rect underneath the OTHER aliases. The dedup path only
358
+ // engages for `isConstant` avals, which can't fire mark
359
+ // callbacks → repack(constant) shouldn't ever happen via the
360
+ // reactivity loop. If a caller invokes it manually we'd rather
361
+ // throw than silently corrupt the other aliases.
362
+ if (old.aliases.length > 1) {
363
+ throw new Error("AtlasPool.repack: cannot repack a deduped entry shared by multiple constant avals");
364
+ }
365
+ // 1. Free the old rect from its page's packer + drop the maps.
366
+ const savedRefcount = old.refcount;
367
+ const oldPages = this.pagesByFormat.get(old.format);
368
+ const oldPage = oldPages[old.pageId];
369
+ if (oldPage !== undefined) {
370
+ oldPage.packing = oldPage.packing.remove(old.ref);
371
+ }
372
+ this.entriesByRef.delete(old.ref);
373
+ this.entriesByAval.delete(sourceAval);
374
+ if (old.valueKey !== undefined)
375
+ this.entriesByValueKey.delete(old.valueKey);
376
+ // 2. Acquire a fresh placement for the new texture. Reuse the
377
+ // `acquire` algorithm — entriesByAval no longer has us, so it
378
+ // picks the existing-pages-first path naturally.
379
+ const acqOpts = {
380
+ ...opts,
381
+ ...(opts.source === undefined && desc.host !== undefined
382
+ ? { source: { width: desc.width, height: desc.height, host: desc.host } }
383
+ : {}),
384
+ };
385
+ const acq = this.acquire(fmt, sourceAval, desc.width, desc.height, acqOpts);
386
+ // 3. Restore the original refcount (acquire set it to 1).
387
+ const newEntry = this.entriesByRef.get(acq.ref);
388
+ if (newEntry !== undefined)
389
+ newEntry.refcount = savedRefcount;
390
+ return acq;
391
+ }
392
+ /** Decrement the refcount; on zero, free the sub-rect. */
393
+ release(ref) {
394
+ const e = this.entriesByRef.get(ref);
395
+ if (e === undefined)
396
+ return;
397
+ e.refcount--;
398
+ if (e.refcount > 0)
399
+ return;
400
+ const pages = this.pagesByFormat.get(e.format);
401
+ const page = pages[e.pageId];
402
+ if (page !== undefined) {
403
+ page.packing = page.packing.remove(e.ref);
404
+ }
405
+ this.entriesByRef.delete(e.ref);
406
+ for (const a of e.aliases)
407
+ this.entriesByAval.delete(a);
408
+ if (e.valueKey !== undefined)
409
+ this.entriesByValueKey.delete(e.valueKey);
410
+ }
411
+ /** Destroy every page texture. The pool becomes unusable. */
412
+ dispose() {
413
+ for (const [, pages] of this.pagesByFormat) {
414
+ for (const p of pages)
415
+ p.texture.destroy();
416
+ }
417
+ this.pagesByFormat.clear();
418
+ this.pageAddedListeners.clear();
419
+ this.entriesByAval.clear();
420
+ this.entriesByRef.clear();
421
+ this.entriesByValueKey.clear();
422
+ }
423
+ finalize(aval, format, pageId, page, source, mip0W, mip0H, numMips, valueKey) {
424
+ const ref = this.nextRef++;
425
+ const placed = page.packing.used.get(ref);
426
+ if (placed === undefined) {
427
+ throw new Error("AtlasPool: invariant — placed sub-rect missing from packer");
428
+ }
429
+ const x = placed.min.x;
430
+ const y = placed.min.y;
431
+ const w = placed.max.x - placed.min.x + 1;
432
+ const h = placed.max.y - placed.min.y + 1;
433
+ const entry = {
434
+ aliases: [aval],
435
+ valueKey,
436
+ format,
437
+ pageId,
438
+ subRect: { x, y, w, h },
439
+ mip0: { w: mip0W, h: mip0H },
440
+ numMips,
441
+ refcount: 1,
442
+ ref,
443
+ };
444
+ this.entriesByAval.set(aval, entry);
445
+ this.entriesByRef.set(ref, entry);
446
+ if (valueKey !== undefined)
447
+ this.entriesByValueKey.set(valueKey, entry);
448
+ if (source !== undefined && source.host !== undefined) {
449
+ // Upload places mip-0 at the interior (+2, +2) position. The
450
+ // surrounding 2-px gutter (inner clamp ring + outer wrap ring)
451
+ // should be filled here, but Dawn's WebGPU implementation
452
+ // currently rejects same-texture copyTextureToTexture even
453
+ // when the source and destination regions don't overlap
454
+ // (validation says "overlapping layer ranges" — overly strict
455
+ // vs §22.5.5). Until the planned compute mip+gutter kernel
456
+ // lands, gutter cells stay uninitialized → edge bilinear
457
+ // samples bleed at exact uv=0 / uv=1. For most atlas content
458
+ // (low-frequency icon edges) this is visually invisible.
459
+ this.upload(page, x + 2, y + 2, source.host, mip0W, mip0H, numMips);
460
+ }
461
+ return this.makeResult(entry);
462
+ }
463
+ upload(page, x, y, host, w, h, numMips) {
464
+ // For storage-capable pages (rgba8unorm): use the GPU compute
465
+ // kernel to build mips + gutter in one pass. Mip 0 interior is
466
+ // uploaded raw via writeTexture; the kernel reads from there to
467
+ // both downsample mip 1..N-1 and fill every mip's 2-px gutter
468
+ // ring. No CPU canvas-2d, no per-mip extended-buffer build.
469
+ if (this.canUseGpuKernel(page)) {
470
+ const px = this.extractPixels(host, w, h);
471
+ if (px !== null) {
472
+ // Bounding rect that the sub-rect occupies — caller passed
473
+ // (x, y) as the interior position (sub-rect origin + 2/+2).
474
+ // The scratch buffer covers this bounding rect.
475
+ const boundsX = x - 2;
476
+ const boundsY = y - 2;
477
+ // Reserved size matches the packer's allocation.
478
+ const reservedW = numMips > 1
479
+ ? (w + 4) + Math.max(1, w >> 1) + 4
480
+ : w + 4;
481
+ let reservedH = h + 4;
482
+ if (numMips > 1) {
483
+ let stackedH = 0;
484
+ for (let k = 1; k < numMips; k++)
485
+ stackedH += Math.max(1, h >> k) + 4;
486
+ reservedH = Math.max(reservedH, stackedH);
487
+ }
488
+ // Mip slot offsets, in bounding-rect-relative pixel coords.
489
+ // mip-0 interior is at (+2, +2) inside the bounds.
490
+ const slots = [];
491
+ for (let k = 0; k < numMips; k++) {
492
+ const off = mipOffsetInPyramid(w, h, k);
493
+ slots.push({
494
+ origin: { x: 2 + off.x, y: 2 + off.y },
495
+ size: { w: Math.max(1, w >> k), h: Math.max(1, h >> k) },
496
+ });
497
+ }
498
+ buildMipsAndGutterOnGpu(this.device, page.texture, boundsX, boundsY, reservedW, reservedH, px, w, h, slots);
499
+ return;
500
+ }
501
+ // Pixel extraction failed (headless without canvas for external
502
+ // sources) — fall through to CPU path below.
503
+ }
504
+ // Otherwise (srgb pages): keep the CPU path.
505
+ // Mip 0 lands at (x, y) — already the interior position (caller
506
+ // offset by +2/+2 for the gutter ring). Use the gutter-aware
507
+ // upload path: builds (w+4)×(h+4) with both gutter rings
508
+ // pre-filled CPU-side, then a single writeTexture covers the
509
+ // whole rect. Falls back to plain uploadLevel if pixel
510
+ // extraction fails (headless without canvas).
511
+ if (!this.uploadLevelWithGutter(page, x, y, host, w, h)) {
512
+ this.uploadLevel(page, x, y, host, w, h);
513
+ }
514
+ if (numMips <= 1)
515
+ return;
516
+ // Mip k≥1: downscale via canvas-2d, upload at the pyramid offset.
517
+ const src = this.toDrawable(host, w, h);
518
+ if (src === null) {
519
+ // Headless / unsupported source — skip mip generation; level 0
520
+ // is still in place. Callers that need mips should provide a
521
+ // host source the pool can render-2d.
522
+ return;
523
+ }
524
+ for (let k = 1; k < numMips; k++) {
525
+ const off = mipOffsetInPyramid(w, h, k);
526
+ const mw = Math.max(1, w >> k);
527
+ const mh = Math.max(1, h >> k);
528
+ const mip = this.makeMipCanvas(src, mw, mh);
529
+ if (mip === null)
530
+ continue;
531
+ const mx = x + off.x;
532
+ const my = y + off.y;
533
+ // Render the downsampled mip into a canvas, extract pixels,
534
+ // build gutter-extended buffer, writeTexture once.
535
+ const mipCanvas = this.makeCanvas(mw, mh);
536
+ if (mipCanvas === null) {
537
+ this.device.queue.copyExternalImageToTexture({ source: mip }, { texture: page.texture, origin: { x: mx, y: my, z: 0 } }, { width: mw, height: mh, depthOrArrayLayers: 1 });
538
+ continue;
539
+ }
540
+ const ctx = mipCanvas.getContext("2d");
541
+ if (ctx === null)
542
+ continue;
543
+ ctx.drawImage(mip, 0, 0, mw, mh);
544
+ const mipPx = new Uint8Array(ctx.getImageData(0, 0, mw, mh).data.buffer);
545
+ const ext = AtlasPool.buildExtendedWithGutter(mipPx, mw, mh);
546
+ this.writeRgba8Padded(page.texture, mx - 2, my - 2, ext, mw + 4, mh + 4);
547
+ }
548
+ }
549
+ /**
550
+ * Build a (w+4)×(h+4) extended pixel buffer from `src` (w×h, RGBA8)
551
+ * with the 2-px gutter pre-filled:
552
+ * - Inner ring (1 px) = clamp-replicate (edge texel).
553
+ * - Outer ring (1 px) = wrap (opposite-edge texel).
554
+ * The shader needs both: clamp-replicate absorbs FP-edge bleed for
555
+ * any wrap mode; the outer wrap ring is sampled by the repeat
556
+ * seam-shift ±1 path. By pre-baking these CPU-side we get a single
557
+ * writeTexture call per slot — much faster than per-cell GPU
558
+ * copies, and dodges Dawn's overly-strict same-texture copy
559
+ * validation.
560
+ *
561
+ * Layout matches the shader expectations and `placeWithGutter` in
562
+ * the conformance test. Corner cells are populated by independent
563
+ * per-axis interpretation (e.g. outer-X × outer-Y = diagonal wrap;
564
+ * inner-X × outer-Y = clamp-X × wrap-Y).
565
+ */
566
+ static buildExtendedWithGutter(src, w, h) {
567
+ const ew = w + 4;
568
+ const eh = h + 4;
569
+ const out = new Uint8Array(ew * eh * 4);
570
+ // pickSrcCoord: dx ∈ [-2..w+1] → (srcX, mode) per axis.
571
+ const srcX = (dx) => {
572
+ if (dx === -2)
573
+ return w - 1; // outer wrap left = opposite edge
574
+ if (dx === -1)
575
+ return 0; // inner clamp left = nearest edge
576
+ if (dx === w)
577
+ return w - 1; // inner clamp right
578
+ if (dx === w + 1)
579
+ return 0; // outer wrap right
580
+ return dx; // interior
581
+ };
582
+ const srcY = (dy) => {
583
+ if (dy === -2)
584
+ return h - 1;
585
+ if (dy === -1)
586
+ return 0;
587
+ if (dy === h)
588
+ return h - 1;
589
+ if (dy === h + 1)
590
+ return 0;
591
+ return dy;
592
+ };
593
+ for (let dy = -2; dy < h + 2; dy++) {
594
+ const sy = srcY(dy);
595
+ const ey = dy + 2;
596
+ for (let dx = -2; dx < w + 2; dx++) {
597
+ const sx = srcX(dx);
598
+ const ex = dx + 2;
599
+ const si = (sy * w + sx) * 4;
600
+ const oi = (ey * ew + ex) * 4;
601
+ out[oi + 0] = src[si + 0];
602
+ out[oi + 1] = src[si + 1];
603
+ out[oi + 2] = src[si + 2];
604
+ out[oi + 3] = src[si + 3];
605
+ }
606
+ }
607
+ return out;
608
+ }
609
+ /**
610
+ * Extract raw RGBA8 pixels from `host` at logical size (w, h).
611
+ * Falls back to a canvas draw + getImageData for external sources;
612
+ * raw sources are copied straight. Returns null when the runtime
613
+ * can't synthesise pixels (headless without canvas, etc.).
614
+ */
615
+ extractPixels(host, w, h) {
616
+ if (host.kind !== "external") {
617
+ const ab = host.data instanceof ArrayBuffer
618
+ ? new Uint8Array(host.data)
619
+ : new Uint8Array(host.data.buffer, host.data.byteOffset, host.data.byteLength);
620
+ return ab.slice(0, w * h * 4);
621
+ }
622
+ const c = this.makeCanvas(w, h);
623
+ if (c === null)
624
+ return null;
625
+ const ctx = c.getContext("2d");
626
+ if (ctx === null)
627
+ return null;
628
+ ctx.drawImage(host.source, 0, 0, w, h);
629
+ return new Uint8Array(ctx.getImageData(0, 0, w, h).data.buffer);
630
+ }
631
+ /**
632
+ * Upload a mip level INCLUDING its 2-px gutter ring. Writes a
633
+ * single (w+4)×(h+4) block to (x-2, y-2) — caller passes the
634
+ * interior position (x, y), this routine offsets by -2/-2 to
635
+ * cover the gutter. Returns false if pixel extraction failed (very
636
+ * rare; only on platforms without canvas support).
637
+ */
638
+ uploadLevelWithGutter(page, x, y, host, w, h) {
639
+ const px = this.extractPixels(host, w, h);
640
+ if (px === null)
641
+ return false;
642
+ const ext = AtlasPool.buildExtendedWithGutter(px, w, h);
643
+ this.writeRgba8Padded(page.texture, x - 2, y - 2, ext, w + 4, h + 4);
644
+ return true;
645
+ }
646
+ /** Unused stub kept for compatibility — see uploadLevelWithGutter. */
647
+ fillClampGutter(page, ix, iy, w, h) {
648
+ void page;
649
+ void ix;
650
+ void iy;
651
+ void w;
652
+ void h;
653
+ const t = page.texture;
654
+ const enc = this.device.createCommandEncoder({ label: "atlas/gutter" });
655
+ const cp = (sx, sy, sw, sh, dx, dy) => {
656
+ enc.copyTextureToTexture({ texture: t, origin: { x: sx, y: sy, z: 0 } }, { texture: t, origin: { x: dx, y: dy, z: 0 } }, { width: sw, height: sh, depthOrArrayLayers: 1 });
657
+ };
658
+ // ─── Inner clamp-replicate ring ───────────────────────────────
659
+ // Edge strips (clamp Y or clamp X = nearest interior row/col)
660
+ cp(ix, iy, w, 1, ix, iy - 1); // top : row 0
661
+ cp(ix, iy + h - 1, w, 1, ix, iy + h); // bottom : row h-1
662
+ cp(ix, iy, 1, h, ix - 1, iy); // left : col 0
663
+ cp(ix + w - 1, iy, 1, h, ix + w, iy); // right : col w-1
664
+ // Inner corners (clamp X × clamp Y = the diagonal interior texel)
665
+ cp(ix, iy, 1, 1, ix - 1, iy - 1); // TL T[0, 0]
666
+ cp(ix + w - 1, iy, 1, 1, ix + w, iy - 1); // TR T[0, w-1]
667
+ cp(ix, iy + h - 1, 1, 1, ix - 1, iy + h); // BL T[h-1, 0]
668
+ cp(ix + w - 1, iy + h - 1, 1, 1, ix + w, iy + h); // BR T[h-1, w-1]
669
+ // ─── Outer wrap ring ──────────────────────────────────────────
670
+ // Edge strips (wrap Y or wrap X = opposite interior row/col)
671
+ cp(ix, iy + h - 1, w, 1, ix, iy - 2); // top wrap-Y: row h-1
672
+ cp(ix, iy, w, 1, ix, iy + h + 1); // bot wrap-Y: row 0
673
+ cp(ix + w - 1, iy, 1, h, ix - 2, iy); // left wrap-X: col w-1
674
+ cp(ix, iy, 1, h, ix + w + 1, iy); // right wrap-X: col 0
675
+ // Outer corners — each of 4 outer 2×2 blocks contains 3 outer-ring
676
+ // cells. Source rules per cell:
677
+ // (-2, -2) = wrap-X wrap-Y → diagonal opposite corner = T[h-1, w-1]
678
+ // (-1, -2) = clamp-X wrap-Y → T[h-1, 0]
679
+ // (-2, -1) = wrap-X clamp-Y → T[0, w-1]
680
+ // Top-left
681
+ cp(ix + w - 1, iy + h - 1, 1, 1, ix - 2, iy - 2); // T[h-1, w-1]
682
+ cp(ix, iy + h - 1, 1, 1, ix - 1, iy - 2); // T[h-1, 0]
683
+ cp(ix + w - 1, iy, 1, 1, ix - 2, iy - 1); // T[0, w-1]
684
+ // Top-right
685
+ cp(ix, iy + h - 1, 1, 1, ix + w + 1, iy - 2); // T[h-1, 0]
686
+ cp(ix + w - 1, iy + h - 1, 1, 1, ix + w, iy - 2); // T[h-1, w-1]
687
+ cp(ix, iy, 1, 1, ix + w + 1, iy - 1); // T[0, 0]
688
+ // Bottom-left
689
+ cp(ix + w - 1, iy, 1, 1, ix - 2, iy + h + 1); // T[0, w-1]
690
+ cp(ix, iy, 1, 1, ix - 1, iy + h + 1); // T[0, 0]
691
+ cp(ix + w - 1, iy + h - 1, 1, 1, ix - 2, iy + h); // T[h-1, w-1]
692
+ // Bottom-right
693
+ cp(ix, iy, 1, 1, ix + w + 1, iy + h + 1); // T[0, 0]
694
+ cp(ix + w - 1, iy, 1, 1, ix + w, iy + h + 1); // T[0, w-1]
695
+ cp(ix, iy + h - 1, 1, 1, ix + w + 1, iy + h); // T[h-1, 0]
696
+ this.device.queue.submit([enc.finish()]);
697
+ }
698
+ uploadLevel(page, x, y, host, w, h) {
699
+ if (host.kind === "external") {
700
+ this.device.queue.copyExternalImageToTexture({ source: host.source }, { texture: page.texture, origin: { x, y, z: 0 } }, { width: w, height: h, depthOrArrayLayers: 1 });
701
+ return;
702
+ }
703
+ const data = host.data instanceof ArrayBuffer
704
+ ? new Uint8Array(host.data)
705
+ : new Uint8Array(host.data.buffer, host.data.byteOffset, host.data.byteLength);
706
+ this.writeRgba8Padded(page.texture, x, y, data, w, h);
707
+ }
708
+ /**
709
+ * Upload `src` (RGBA8, w×h) to `texture` at (x, y) via `writeTexture`.
710
+ * `writeTexture` requires `bytesPerRow` to be a multiple of 256 when
711
+ * the source has more than one row, so non-256-aligned widths need
712
+ * a padded buffer.
713
+ */
714
+ writeRgba8Padded(texture, x, y, src, w, h) {
715
+ const srcStride = w * 4;
716
+ if (h === 1 || srcStride % 256 === 0) {
717
+ // Single row or already aligned — pass straight through.
718
+ this.device.queue.writeTexture({ texture, origin: { x, y, z: 0 } }, src, { bytesPerRow: srcStride, rowsPerImage: h }, { width: w, height: h, depthOrArrayLayers: 1 });
719
+ return;
720
+ }
721
+ const dstStride = Math.ceil(srcStride / 256) * 256;
722
+ const padded = new Uint8Array(dstStride * h);
723
+ for (let row = 0; row < h; row++) {
724
+ padded.set(src.subarray(row * srcStride, (row + 1) * srcStride), row * dstStride);
725
+ }
726
+ this.device.queue.writeTexture({ texture, origin: { x, y, z: 0 } }, padded, { bytesPerRow: dstStride, rowsPerImage: h }, { width: w, height: h, depthOrArrayLayers: 1 });
727
+ }
728
+ /**
729
+ * Produce a `CanvasImageSource` we can feed to `drawImage` for
730
+ * downscaling. Returns null when the runtime can't synthesise one
731
+ * (no DOM / OffscreenCanvas / ImageBitmap support).
732
+ */
733
+ toDrawable(host, w, h) {
734
+ if (host.kind === "external") {
735
+ const s = host.source;
736
+ if (typeof ImageData !== "undefined" && s instanceof ImageData) {
737
+ return this.imageDataToCanvas(s);
738
+ }
739
+ return s;
740
+ }
741
+ if (typeof ImageData === "undefined")
742
+ return null;
743
+ const bytes = host.data instanceof ArrayBuffer
744
+ ? new Uint8Array(host.data)
745
+ : new Uint8Array(host.data.buffer, host.data.byteOffset, host.data.byteLength);
746
+ const out = new Uint8ClampedArray(bytes.length);
747
+ out.set(bytes);
748
+ const img = new ImageData(out, w, h);
749
+ return this.imageDataToCanvas(img);
750
+ }
751
+ imageDataToCanvas(img) {
752
+ const c = this.makeCanvas(img.width, img.height);
753
+ if (c === null)
754
+ return null;
755
+ const ctx = c.getContext("2d");
756
+ if (ctx === null)
757
+ return null;
758
+ ctx.putImageData(img, 0, 0);
759
+ return c;
760
+ }
761
+ makeMipCanvas(src, w, h) {
762
+ const c = this.makeCanvas(w, h);
763
+ if (c === null)
764
+ return null;
765
+ const ctx = c.getContext("2d");
766
+ if (ctx === null)
767
+ return null;
768
+ ctx.imageSmoothingEnabled = true;
769
+ ctx.imageSmoothingQuality = "high";
770
+ ctx.drawImage(src, 0, 0, w, h);
771
+ return c;
772
+ }
773
+ makeCanvas(w, h) {
774
+ if (typeof OffscreenCanvas !== "undefined")
775
+ return new OffscreenCanvas(w, h);
776
+ if (typeof document !== "undefined") {
777
+ const c = document.createElement("canvas");
778
+ c.width = w;
779
+ c.height = h;
780
+ return c;
781
+ }
782
+ return null;
783
+ }
784
+ makeResult(e) {
785
+ const page = this.pagesByFormat.get(e.format)[e.pageId];
786
+ // Mip-0 interior sits at +2/+2 inside the reserved rect (skipping
787
+ // the 2-px gutter ring on the top and left).
788
+ return {
789
+ pageId: e.pageId,
790
+ origin: new V2f(e.subRect.x + 2, e.subRect.y + 2),
791
+ size: new V2f(e.mip0.w, e.mip0.h),
792
+ numMips: e.numMips,
793
+ ref: e.ref,
794
+ page,
795
+ };
796
+ }
797
+ }
798
+ //# sourceMappingURL=atlasPool.js.map