@aardworx/wombat.rendering 0.21.23 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/runtime/heapEffect.d.ts +19 -16
- package/dist/runtime/heapEffect.d.ts.map +1 -1
- package/dist/runtime/heapEffect.js +35 -37
- package/dist/runtime/heapEffect.js.map +1 -1
- package/dist/runtime/heapScene.d.ts.map +1 -1
- package/dist/runtime/heapScene.js +23 -31
- package/dist/runtime/heapScene.js.map +1 -1
- package/dist/runtime/runtime.d.ts +3 -0
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +3 -1
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/textureAtlas/atlasMipGutterKernel.d.ts +2 -2
- package/dist/runtime/textureAtlas/atlasMipGutterKernel.d.ts.map +1 -1
- package/dist/runtime/textureAtlas/atlasMipGutterKernel.js +6 -6
- package/dist/runtime/textureAtlas/atlasMipGutterKernel.js.map +1 -1
- package/dist/runtime/textureAtlas/atlasPool.d.ts +30 -8
- package/dist/runtime/textureAtlas/atlasPool.d.ts.map +1 -1
- package/dist/runtime/textureAtlas/atlasPool.js +82 -38
- package/dist/runtime/textureAtlas/atlasPool.js.map +1 -1
- package/package.json +1 -1
- package/src/runtime/heapEffect.ts +35 -37
- package/src/runtime/heapScene.ts +23 -31
- package/src/runtime/runtime.ts +5 -1
- package/src/runtime/textureAtlas/atlasMipGutterKernel.ts +6 -3
- package/src/runtime/textureAtlas/atlasPool.ts +95 -40
|
@@ -46,14 +46,24 @@ export const ATLAS_MAX_DIM = 1024;
|
|
|
46
46
|
* `switch pageRef` runs an N-way ladder; the BGL declares N consecutive
|
|
47
47
|
* texture slots per format. Allocations beyond this throw.
|
|
48
48
|
*/
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
// (
|
|
55
|
-
//
|
|
56
|
-
export const ATLAS_MAX_PAGES_PER_FORMAT =
|
|
49
|
+
// Pages are LAYERS of one `texture_2d_array` per format: the shader
|
|
50
|
+
// indexes `pageRef` at runtime, so the count is no longer pinned to a
|
|
51
|
+
// binding ladder. Layers grow on demand (new array texture + GPU-side
|
|
52
|
+
// layer copy + bind-group rebuild via onPageAdded); this cap only
|
|
53
|
+
// bounds the growth so a runaway streaming scene can't eat unbounded
|
|
54
|
+
// VRAM (16 × 4096² × rgba8 ≈ 1 GB per format family, lazily reached).
|
|
55
|
+
// Configurable via RuntimeOptions.atlasMaxPages.
|
|
56
|
+
export const ATLAS_MAX_PAGES_PER_FORMAT = 16;
|
|
57
|
+
|
|
58
|
+
/** Runtime-configurable layer cap (RuntimeOptions.atlasMaxPages).
|
|
59
|
+
* Routed through globalThis like the page size — bundlers can
|
|
60
|
+
* duplicate this module across entry points. */
|
|
61
|
+
export function atlasMaxPagesNow(): number {
|
|
62
|
+
return ((globalThis as { __wombatAtlasMaxPages?: number }).__wombatAtlasMaxPages ?? ATLAS_MAX_PAGES_PER_FORMAT);
|
|
63
|
+
}
|
|
64
|
+
export function setAtlasMaxPages(n: number): void {
|
|
65
|
+
(globalThis as { __wombatAtlasMaxPages?: number }).__wombatAtlasMaxPages = n;
|
|
66
|
+
}
|
|
57
67
|
|
|
58
68
|
/** Page edge in texels. Default suits small-memory devices; desktops
|
|
59
69
|
* pass RuntimeOptions.atlasPageSize = 8192 (must be set BEFORE any
|
|
@@ -80,17 +90,19 @@ export const atlasFormatIndex = (f: AtlasPageFormat): number =>
|
|
|
80
90
|
export interface AtlasPage {
|
|
81
91
|
readonly format: AtlasPageFormat;
|
|
82
92
|
/**
|
|
83
|
-
* The
|
|
84
|
-
*
|
|
93
|
+
* The format family's shared `texture_2d_array` — this page is layer
|
|
94
|
+
* `pageId` of it. NOT stable across growth: adding pages past the
|
|
95
|
+
* current layer count reallocates the array (existing layers are
|
|
96
|
+
* GPU-copied across and `onPageAdded` fires so bind groups rebuild).
|
|
97
|
+
* Always read through this getter, never cache the GPUTexture.
|
|
85
98
|
*/
|
|
86
99
|
readonly texture: GPUTexture;
|
|
87
100
|
/** Immutable packer state — `tryAdd`/`remove` produce new instances. */
|
|
88
101
|
packing: TexturePacking<number>;
|
|
89
102
|
/**
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* `atlasSrgb<i>` declaration.
|
|
103
|
+
* Layer index in the format's array texture (0..N-1). Same value
|
|
104
|
+
* flows into the drawHeader `pageRef` field; the shader indexes the
|
|
105
|
+
* array with it at runtime.
|
|
94
106
|
*/
|
|
95
107
|
readonly pageId: number;
|
|
96
108
|
}
|
|
@@ -251,6 +263,8 @@ function describeAtlasTexture(t: ITexture): {
|
|
|
251
263
|
export class AtlasPool {
|
|
252
264
|
private readonly pagesByFormat = new Map<AtlasPageFormat, AtlasPage[]>();
|
|
253
265
|
private readonly pageAddedListeners = new Map<AtlasPageFormat, Set<(pageId: number) => void>>();
|
|
266
|
+
/** Per-format shared array texture (pages = layers). */
|
|
267
|
+
private readonly storesByFormat = new Map<AtlasPageFormat, { texture: GPUTexture; layers: number }>();
|
|
254
268
|
/**
|
|
255
269
|
* Keyed by `aval<ITexture>` under the aval equality protocol: a
|
|
256
270
|
* `HashTable` (not a JS `Map`) so two distinct `AVal.constant(tex)`
|
|
@@ -301,7 +315,7 @@ export class AtlasPool {
|
|
|
301
315
|
/** True while every page of `format` is packed with live (referenced) entries. */
|
|
302
316
|
isFull(format: AtlasPageFormat): boolean {
|
|
303
317
|
const pages = this.pagesByFormat.get(format);
|
|
304
|
-
if (pages === undefined || pages.length <
|
|
318
|
+
if (pages === undefined || pages.length < atlasMaxPagesNow()) return false;
|
|
305
319
|
for (const e of this.lru.values()) if (e.format === format) return false; // something is evictable
|
|
306
320
|
return true;
|
|
307
321
|
}
|
|
@@ -351,19 +365,32 @@ export class AtlasPool {
|
|
|
351
365
|
return typeof (this.device.queue as { onSubmittedWorkDone?: () => Promise<void> }).onSubmittedWorkDone === "function";
|
|
352
366
|
}
|
|
353
367
|
|
|
354
|
-
|
|
368
|
+
/**
|
|
369
|
+
* Ensure the format's array texture has at least `layers` layers.
|
|
370
|
+
* Growth allocates a fresh array (pow2 steps up to 8, then +2, capped
|
|
371
|
+
* at `atlasMaxPagesNow()`), copies every existing layer across in one
|
|
372
|
+
* `copyTextureToTexture`, and destroys the old texture (safe right
|
|
373
|
+
* after submit — already-submitted work completes before destruction).
|
|
374
|
+
* Callers fire `onPageAdded` afterwards, which rebuilds bind groups —
|
|
375
|
+
* so the stale-texture window never reaches a render pass.
|
|
376
|
+
*/
|
|
377
|
+
private ensureLayers(format: AtlasPageFormat, layers: number): { texture: GPUTexture; layers: number } {
|
|
378
|
+
const cap = atlasMaxPagesNow();
|
|
379
|
+
if (layers > cap) {
|
|
380
|
+
throw new Error(`AtlasPool: layer request ${layers} exceeds atlasMaxPages ${cap}`);
|
|
381
|
+
}
|
|
382
|
+
const store = this.storesByFormat.get(format);
|
|
383
|
+
if (store !== undefined && store.layers >= layers) return store;
|
|
384
|
+
let next = store?.layers ?? 0;
|
|
385
|
+
while (next < layers) next = next === 0 ? 1 : next < 8 ? next * 2 : next + 2;
|
|
386
|
+
next = Math.min(next, cap);
|
|
355
387
|
// The compute mip+gutter kernel works for *all* page formats: it
|
|
356
|
-
// builds the pyramid + gutters in a scratch storage *buffer*
|
|
357
|
-
//
|
|
358
|
-
//
|
|
359
|
-
// included (the copy reinterprets the bytes; no colour conversion).
|
|
360
|
-
// Pages therefore only need TEXTURE_BINDING | COPY_DST | COPY_SRC |
|
|
361
|
-
// RENDER_ATTACHMENT (no STORAGE_BINDING). The CPU `buildExtended-
|
|
362
|
-
// WithGutter` path remains only as a fallback for the node mock-GPU
|
|
363
|
-
// device and headless-without-canvas edge cases.
|
|
388
|
+
// builds the pyramid + gutters in a scratch storage *buffer* and
|
|
389
|
+
// finishes with `copyBufferToTexture` into the layer, so pages
|
|
390
|
+
// never need STORAGE_BINDING — srgb included.
|
|
364
391
|
const texture = this.device.createTexture({
|
|
365
|
-
label: `atlas/${format}
|
|
366
|
-
size: { width: atlasPageSizeNow(), height: atlasPageSizeNow(), depthOrArrayLayers:
|
|
392
|
+
label: `atlas/${format}[${next}]`,
|
|
393
|
+
size: { width: atlasPageSizeNow(), height: atlasPageSizeNow(), depthOrArrayLayers: next },
|
|
367
394
|
format,
|
|
368
395
|
mipLevelCount: 1,
|
|
369
396
|
usage:
|
|
@@ -372,14 +399,39 @@ export class AtlasPool {
|
|
|
372
399
|
GPUTextureUsage.COPY_SRC |
|
|
373
400
|
GPUTextureUsage.RENDER_ATTACHMENT,
|
|
374
401
|
});
|
|
402
|
+
if (store !== undefined && store.layers > 0) {
|
|
403
|
+
const enc = this.device.createCommandEncoder({ label: `atlas/${format}/grow` });
|
|
404
|
+
enc.copyTextureToTexture(
|
|
405
|
+
{ texture: store.texture },
|
|
406
|
+
{ texture },
|
|
407
|
+
{ width: atlasPageSizeNow(), height: atlasPageSizeNow(), depthOrArrayLayers: store.layers },
|
|
408
|
+
);
|
|
409
|
+
this.device.queue.submit([enc.finish()]);
|
|
410
|
+
store.texture.destroy();
|
|
411
|
+
}
|
|
412
|
+
const fresh = { texture, layers: next };
|
|
413
|
+
this.storesByFormat.set(format, fresh);
|
|
414
|
+
return fresh;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
private allocatePage(format: AtlasPageFormat, pageId: number): AtlasPage {
|
|
418
|
+
const pool = this;
|
|
419
|
+
this.ensureLayers(format, pageId + 1);
|
|
375
420
|
return {
|
|
376
421
|
format,
|
|
377
|
-
|
|
422
|
+
// getter: growth reallocates the array; pages must never cache it
|
|
423
|
+
get texture(): GPUTexture { return pool.storesByFormat.get(format)!.texture; },
|
|
378
424
|
packing: TexturePacking.empty<number>(new V2i(atlasPageSizeNow(), atlasPageSizeNow()), false),
|
|
379
425
|
pageId,
|
|
380
426
|
};
|
|
381
427
|
}
|
|
382
428
|
|
|
429
|
+
/** The format family's current array texture (undefined before the
|
|
430
|
+
* first page). Bind-group building reads this — pages are layers. */
|
|
431
|
+
textureFor(format: AtlasPageFormat): GPUTexture | undefined {
|
|
432
|
+
return this.storesByFormat.get(format)?.texture;
|
|
433
|
+
}
|
|
434
|
+
|
|
383
435
|
/**
|
|
384
436
|
* Tier-S eligibility: dimension threshold + supported format. If
|
|
385
437
|
* this returns null, the caller should route the texture down the
|
|
@@ -515,8 +567,8 @@ export class AtlasPool {
|
|
|
515
567
|
if (fit !== null) return fit;
|
|
516
568
|
}
|
|
517
569
|
|
|
518
|
-
// Allocate a fresh page if we haven't hit the
|
|
519
|
-
if (pages.length >=
|
|
570
|
+
// Allocate a fresh page (array layer) if we haven't hit the cap.
|
|
571
|
+
if (pages.length >= atlasMaxPagesNow()) {
|
|
520
572
|
// The atlas is genuinely full: every page is packed with entries that are
|
|
521
573
|
// still referenced, so there is nothing to evict. That is resource
|
|
522
574
|
// pressure, NOT a program error — and throwing turned it into a fatal
|
|
@@ -720,11 +772,10 @@ export class AtlasPool {
|
|
|
720
772
|
return n;
|
|
721
773
|
}
|
|
722
774
|
|
|
723
|
-
/** Destroy
|
|
775
|
+
/** Destroy the per-format array textures. The pool becomes unusable. */
|
|
724
776
|
dispose(): void {
|
|
725
|
-
for (const [,
|
|
726
|
-
|
|
727
|
-
}
|
|
777
|
+
for (const [, store] of this.storesByFormat) store.texture.destroy();
|
|
778
|
+
this.storesByFormat.clear();
|
|
728
779
|
this.pagesByFormat.clear();
|
|
729
780
|
this.pageAddedListeners.clear();
|
|
730
781
|
this.entriesByAval.clear();
|
|
@@ -884,6 +935,7 @@ export class AtlasPool {
|
|
|
884
935
|
boundsX, boundsY, reservedW, reservedH,
|
|
885
936
|
staging, w, h, slots,
|
|
886
937
|
this.uploadBatch ?? undefined,
|
|
938
|
+
page.pageId,
|
|
887
939
|
);
|
|
888
940
|
if (this.uploadBatch !== null) this.uploadBatch.trashTextures.push(staging);
|
|
889
941
|
else void this.device.queue.onSubmittedWorkDone().then(() => staging.destroy());
|
|
@@ -899,6 +951,8 @@ export class AtlasPool {
|
|
|
899
951
|
this.device, page.texture,
|
|
900
952
|
boundsX, boundsY, reservedW, reservedH,
|
|
901
953
|
px, w, h, slots,
|
|
954
|
+
undefined,
|
|
955
|
+
page.pageId,
|
|
902
956
|
);
|
|
903
957
|
return;
|
|
904
958
|
}
|
|
@@ -938,7 +992,7 @@ export class AtlasPool {
|
|
|
938
992
|
if (mipCanvas === null) {
|
|
939
993
|
this.device.queue.copyExternalImageToTexture(
|
|
940
994
|
{ source: mip as GPUImageCopyExternalImageSource },
|
|
941
|
-
{ texture: page.texture, origin: { x: mx, y: my, z:
|
|
995
|
+
{ texture: page.texture, origin: { x: mx, y: my, z: page.pageId } },
|
|
942
996
|
{ width: mw, height: mh, depthOrArrayLayers: 1 },
|
|
943
997
|
);
|
|
944
998
|
continue;
|
|
@@ -949,7 +1003,7 @@ export class AtlasPool {
|
|
|
949
1003
|
ctx.drawImage(mip as CanvasImageSource, 0, 0, mw, mh);
|
|
950
1004
|
const mipPx = new Uint8Array(ctx.getImageData(0, 0, mw, mh).data.buffer);
|
|
951
1005
|
const ext = AtlasPool.buildExtendedWithGutter(mipPx, mw, mh);
|
|
952
|
-
this.writeRgba8Padded(page.texture, mx - 2, my - 2, ext, mw + 4, mh + 4);
|
|
1006
|
+
this.writeRgba8Padded(page.texture, mx - 2, my - 2, ext, mw + 4, mh + 4, page.pageId);
|
|
953
1007
|
}
|
|
954
1008
|
}
|
|
955
1009
|
|
|
@@ -1044,7 +1098,7 @@ export class AtlasPool {
|
|
|
1044
1098
|
const px = this.extractPixels(host, w, h);
|
|
1045
1099
|
if (px === null) return false;
|
|
1046
1100
|
const ext = AtlasPool.buildExtendedWithGutter(px, w, h);
|
|
1047
|
-
this.writeRgba8Padded(page.texture, x - 2, y - 2, ext, w + 4, h + 4);
|
|
1101
|
+
this.writeRgba8Padded(page.texture, x - 2, y - 2, ext, w + 4, h + 4, page.pageId);
|
|
1048
1102
|
return true;
|
|
1049
1103
|
}
|
|
1050
1104
|
|
|
@@ -1059,7 +1113,7 @@ export class AtlasPool {
|
|
|
1059
1113
|
if (host.kind === "external") {
|
|
1060
1114
|
this.device.queue.copyExternalImageToTexture(
|
|
1061
1115
|
{ source: host.source as GPUImageCopyExternalImageSource },
|
|
1062
|
-
{ texture: page.texture, origin: { x, y, z:
|
|
1116
|
+
{ texture: page.texture, origin: { x, y, z: page.pageId } },
|
|
1063
1117
|
{ width: w, height: h, depthOrArrayLayers: 1 },
|
|
1064
1118
|
);
|
|
1065
1119
|
return;
|
|
@@ -1067,7 +1121,7 @@ export class AtlasPool {
|
|
|
1067
1121
|
const data = host.data instanceof ArrayBuffer
|
|
1068
1122
|
? new Uint8Array(host.data)
|
|
1069
1123
|
: new Uint8Array(host.data.buffer, host.data.byteOffset, host.data.byteLength);
|
|
1070
|
-
this.writeRgba8Padded(page.texture, x, y, data, w, h);
|
|
1124
|
+
this.writeRgba8Padded(page.texture, x, y, data, w, h, page.pageId);
|
|
1071
1125
|
}
|
|
1072
1126
|
|
|
1073
1127
|
/**
|
|
@@ -1079,12 +1133,13 @@ export class AtlasPool {
|
|
|
1079
1133
|
private writeRgba8Padded(
|
|
1080
1134
|
texture: GPUTexture, x: number, y: number,
|
|
1081
1135
|
src: Uint8Array, w: number, h: number,
|
|
1136
|
+
layer = 0,
|
|
1082
1137
|
): void {
|
|
1083
1138
|
const srcStride = w * 4;
|
|
1084
1139
|
if (h === 1 || srcStride % 256 === 0) {
|
|
1085
1140
|
// Single row or already aligned — pass straight through.
|
|
1086
1141
|
this.device.queue.writeTexture(
|
|
1087
|
-
{ texture, origin: { x, y, z:
|
|
1142
|
+
{ texture, origin: { x, y, z: layer } },
|
|
1088
1143
|
src as unknown as GPUAllowSharedBufferSource,
|
|
1089
1144
|
{ bytesPerRow: srcStride, rowsPerImage: h },
|
|
1090
1145
|
{ width: w, height: h, depthOrArrayLayers: 1 },
|
|
@@ -1097,7 +1152,7 @@ export class AtlasPool {
|
|
|
1097
1152
|
padded.set(src.subarray(row * srcStride, (row + 1) * srcStride), row * dstStride);
|
|
1098
1153
|
}
|
|
1099
1154
|
this.device.queue.writeTexture(
|
|
1100
|
-
{ texture, origin: { x, y, z:
|
|
1155
|
+
{ texture, origin: { x, y, z: layer } },
|
|
1101
1156
|
padded as unknown as GPUAllowSharedBufferSource,
|
|
1102
1157
|
{ bytesPerRow: dstStride, rowsPerImage: h },
|
|
1103
1158
|
{ width: w, height: h, depthOrArrayLayers: 1 },
|