@aardworx/wombat.rendering 0.9.3 → 0.9.5
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/core/buffer.d.ts.map +1 -1
- package/dist/core/buffer.js +65 -5
- package/dist/core/buffer.js.map +1 -1
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/provider.d.ts +38 -0
- package/dist/core/provider.d.ts.map +1 -0
- package/dist/core/provider.js +104 -0
- package/dist/core/provider.js.map +1 -0
- package/dist/core/renderObject.d.ts +17 -10
- package/dist/core/renderObject.d.ts.map +1 -1
- package/dist/core/sampler.d.ts.map +1 -1
- package/dist/core/sampler.js +71 -2
- package/dist/core/sampler.js.map +1 -1
- package/dist/core/texture.d.ts +9 -0
- package/dist/core/texture.d.ts.map +1 -1
- package/dist/core/texture.js +0 -0
- package/dist/core/texture.js.map +1 -1
- package/dist/resources/adaptiveTexture.d.ts.map +1 -1
- package/dist/resources/adaptiveTexture.js +7 -0
- package/dist/resources/adaptiveTexture.js.map +1 -1
- package/dist/resources/preparedRenderObject.d.ts.map +1 -1
- package/dist/resources/preparedRenderObject.js +20 -18
- package/dist/resources/preparedRenderObject.js.map +1 -1
- package/dist/resources/uniformBuffer.d.ts.map +1 -1
- package/dist/resources/uniformBuffer.js +23 -0
- package/dist/resources/uniformBuffer.js.map +1 -1
- package/dist/runtime/derivedUniforms/slots.d.ts.map +1 -1
- package/dist/runtime/derivedUniforms/slots.js +4 -0
- package/dist/runtime/derivedUniforms/slots.js.map +1 -1
- package/dist/runtime/heapAdapter.d.ts.map +1 -1
- package/dist/runtime/heapAdapter.js +72 -29
- package/dist/runtime/heapAdapter.js.map +1 -1
- package/dist/runtime/heapDecoder.d.ts +52 -0
- package/dist/runtime/heapDecoder.d.ts.map +1 -0
- package/dist/runtime/heapDecoder.js +363 -0
- package/dist/runtime/heapDecoder.js.map +1 -0
- package/dist/runtime/heapEffectIR.d.ts.map +1 -1
- package/dist/runtime/heapEffectIR.js +300 -3
- package/dist/runtime/heapEffectIR.js.map +1 -1
- package/dist/runtime/heapEligibility.d.ts.map +1 -1
- package/dist/runtime/heapEligibility.js +42 -5
- package/dist/runtime/heapEligibility.js.map +1 -1
- package/dist/runtime/heapIrBuilders.d.ts +64 -0
- package/dist/runtime/heapIrBuilders.d.ts.map +1 -0
- package/dist/runtime/heapIrBuilders.js +311 -0
- package/dist/runtime/heapIrBuilders.js.map +1 -0
- package/dist/runtime/heapScene.d.ts.map +1 -1
- package/dist/runtime/heapScene.js +286 -97
- package/dist/runtime/heapScene.js.map +1 -1
- package/dist/runtime/textureAtlas/atlasPool.d.ts +67 -4
- package/dist/runtime/textureAtlas/atlasPool.d.ts.map +1 -1
- package/dist/runtime/textureAtlas/atlasPool.js +152 -18
- package/dist/runtime/textureAtlas/atlasPool.js.map +1 -1
- package/package.json +4 -3
- package/src/core/buffer.ts +58 -6
- package/src/core/index.ts +11 -0
- package/src/core/provider.ts +143 -0
- package/src/core/renderObject.ts +17 -10
- package/src/core/sampler.ts +64 -2
- package/src/core/texture.ts +0 -0
- package/src/resources/adaptiveTexture.ts +7 -0
- package/src/resources/preparedRenderObject.ts +21 -16
- package/src/resources/uniformBuffer.ts +21 -0
- package/src/runtime/derivedUniforms/slots.ts +4 -0
- package/src/runtime/heapAdapter.ts +63 -29
- package/src/runtime/heapDecoder.ts +446 -0
- package/src/runtime/heapEffectIR.ts +314 -3
- package/src/runtime/heapEligibility.ts +34 -5
- package/src/runtime/heapIrBuilders.ts +365 -0
- package/src/runtime/heapScene.ts +295 -81
- package/src/runtime/textureAtlas/atlasPool.ts +152 -16
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
// mip generation (canvas-2d) for v1; compute-shader path is future.
|
|
31
31
|
|
|
32
32
|
import { V2f, V2i } from "@aardworx/wombat.base";
|
|
33
|
-
import type
|
|
33
|
+
import { type aval, HashTable } from "@aardworx/wombat.adaptive";
|
|
34
34
|
import type { ITexture, HostTextureSource } from "../../core/texture.js";
|
|
35
35
|
import { TexturePacking } from "./packer.js";
|
|
36
36
|
import { buildMipsAndGutterOnGpu, type MipSlot } from "./atlasMipGutterKernel.js";
|
|
@@ -191,6 +191,8 @@ function describeAtlasTexture(t: ITexture): {
|
|
|
191
191
|
mipLevelCount: tex.mipLevelCount,
|
|
192
192
|
};
|
|
193
193
|
}
|
|
194
|
+
// URL-deferred textures are resolved at the Sg layer, never atlas-routed.
|
|
195
|
+
if (t.kind === "url") return null;
|
|
194
196
|
const src = t.source;
|
|
195
197
|
if (src.kind === "raw") {
|
|
196
198
|
return {
|
|
@@ -225,7 +227,17 @@ function describeAtlasTexture(t: ITexture): {
|
|
|
225
227
|
export class AtlasPool {
|
|
226
228
|
private readonly pagesByFormat = new Map<AtlasPageFormat, AtlasPage[]>();
|
|
227
229
|
private readonly pageAddedListeners = new Map<AtlasPageFormat, Set<(pageId: number) => void>>();
|
|
228
|
-
|
|
230
|
+
/**
|
|
231
|
+
* Keyed by `aval<ITexture>` under the aval equality protocol: a
|
|
232
|
+
* `HashTable` (not a JS `Map`) so two distinct `AVal.constant(tex)`
|
|
233
|
+
* built at different call sites — including ones wrapping
|
|
234
|
+
* structurally-equal `ITexture` values, e.g. `ITexture.fromUrl("X")`
|
|
235
|
+
* twice (which are in fact the *same* interned object) — collapse to
|
|
236
|
+
* one entry. Reactive (non-constant) avals key by reference, which is
|
|
237
|
+
* correct since their value can change. This subsumes the old `§5b`
|
|
238
|
+
* value-key side-table entirely.
|
|
239
|
+
*/
|
|
240
|
+
private readonly entriesByAval = new HashTable<aval<ITexture>, AtlasEntry>();
|
|
229
241
|
private readonly entriesByRef = new Map<number, AtlasEntry>();
|
|
230
242
|
/**
|
|
231
243
|
* §5b value-equality dedup map. Constant avals (`isConstant ===
|
|
@@ -233,12 +245,39 @@ export class AtlasPool {
|
|
|
233
245
|
* HostTextureSource (kind:"host") collapse to one sub-rect.
|
|
234
246
|
* Reactive avals fall through to identity-only since their
|
|
235
247
|
* content can change.
|
|
248
|
+
*
|
|
249
|
+
* NOTE: now mostly vestigial — `entriesByAval` being a content-keyed
|
|
250
|
+
* `HashTable` already collapses constant-aval siblings. Kept for the
|
|
251
|
+
* (rare) case where two constant avals wrap *different* `ITexture`
|
|
252
|
+
* wrapper objects that nevertheless reference the same inner GPU
|
|
253
|
+
* resource AND don't compare equal under the ITexture protocol
|
|
254
|
+
* (shouldn't happen with the factory functions, but a hand-built
|
|
255
|
+
* `{kind:"gpu", texture}` literal wouldn't carry the protocol).
|
|
236
256
|
*/
|
|
237
257
|
private readonly entriesByValueKey = new Map<GPUTexture | HostTextureSource, AtlasEntry>();
|
|
258
|
+
/**
|
|
259
|
+
* LRU of refcount-0 entries. Insertion order = age (oldest first).
|
|
260
|
+
* Released entries land here instead of being freed immediately —
|
|
261
|
+
* this lets a re-`acquire` of the same `aval<ITexture>` (or value-
|
|
262
|
+
* keyed sibling) resurrect the sub-rect in O(1) with no GPU upload.
|
|
263
|
+
* Entries leave the LRU when (a) `acquire` resurrects them, or
|
|
264
|
+
* (b) a fresh acquire can't fit in any existing page and we need
|
|
265
|
+
* to evict oldest-first to make room.
|
|
266
|
+
*
|
|
267
|
+
* Why we don't free on refcount → 0: a cset-driven workload (e.g.
|
|
268
|
+
* the heap-demo-sg toggle) cycles the same texture's atlas slot
|
|
269
|
+
* through 0-refs → fresh-acquire many times per second. The
|
|
270
|
+
* eager-free path repeatedly upload-and-pack the same image; the
|
|
271
|
+
* profiler showed `copyExternalImageToTexture` firing during every
|
|
272
|
+
* toggle. With the LRU it fires only on first acquire.
|
|
273
|
+
*/
|
|
274
|
+
private readonly lru = new Map<number, AtlasEntry>();
|
|
238
275
|
private nextRef = 1;
|
|
239
276
|
|
|
240
277
|
private innerKeyOf(t: ITexture): GPUTexture | HostTextureSource {
|
|
241
|
-
|
|
278
|
+
if (t.kind === "gpu") return t.texture;
|
|
279
|
+
if (t.kind === "host") return t.source;
|
|
280
|
+
throw new Error(`AtlasPool: unsupported ITexture.kind "${t.kind}" (url textures must be resolved by the Sg layer first)`);
|
|
242
281
|
}
|
|
243
282
|
|
|
244
283
|
constructor(private readonly device: GPUDevice) {
|
|
@@ -337,7 +376,9 @@ export class AtlasPool {
|
|
|
337
376
|
): AtlasAcquisition {
|
|
338
377
|
const existing = this.entriesByAval.get(sourceAval);
|
|
339
378
|
if (existing !== undefined) {
|
|
379
|
+
const wasIdle = existing.refcount === 0;
|
|
340
380
|
existing.refcount++;
|
|
381
|
+
if (wasIdle) this.lru.delete(existing.ref);
|
|
341
382
|
return this.makeResult(existing);
|
|
342
383
|
}
|
|
343
384
|
|
|
@@ -361,9 +402,11 @@ export class AtlasPool {
|
|
|
361
402
|
if (valueKey !== undefined) {
|
|
362
403
|
const shared = this.entriesByValueKey.get(valueKey);
|
|
363
404
|
if (shared !== undefined) {
|
|
405
|
+
const wasIdle = shared.refcount === 0;
|
|
364
406
|
shared.refcount++;
|
|
365
407
|
shared.aliases.push(sourceAval);
|
|
366
408
|
this.entriesByAval.set(sourceAval, shared);
|
|
409
|
+
if (wasIdle) this.lru.delete(shared.ref);
|
|
367
410
|
return this.makeResult(shared);
|
|
368
411
|
}
|
|
369
412
|
}
|
|
@@ -403,14 +446,36 @@ export class AtlasPool {
|
|
|
403
446
|
|
|
404
447
|
const pages = this.pagesByFormat.get(format)!;
|
|
405
448
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
449
|
+
const tryFitInExisting = (): AtlasAcquisition | null => {
|
|
450
|
+
for (let i = 0; i < pages.length; i++) {
|
|
451
|
+
const page = pages[i]!;
|
|
452
|
+
const next = page.packing.tryAdd(this.nextRef, size);
|
|
453
|
+
if (next !== null) {
|
|
454
|
+
page.packing = next;
|
|
455
|
+
return this.finalize(sourceAval, format, i, page, opts.source, width, height, numMips, valueKey);
|
|
456
|
+
}
|
|
413
457
|
}
|
|
458
|
+
return null;
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
// Try fitting into existing pages first.
|
|
462
|
+
{
|
|
463
|
+
const fit = tryFitInExisting();
|
|
464
|
+
if (fit !== null) return fit;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Page-fit failed. Try evicting LRU entries (oldest first) on this
|
|
468
|
+
// format's pages until either the new size fits or the LRU is
|
|
469
|
+
// empty. Each evicted entry frees its packer slot; we retry the
|
|
470
|
+
// packer after every eviction since the freed slot may bridge two
|
|
471
|
+
// existing gaps. Only entries with `format` matching the request
|
|
472
|
+
// can help — wrong-format entries live on different pages.
|
|
473
|
+
for (const [ref, entry] of this.lru) {
|
|
474
|
+
if (entry.format !== format) continue;
|
|
475
|
+
this.actuallyFree(entry);
|
|
476
|
+
void ref;
|
|
477
|
+
const fit = tryFitInExisting();
|
|
478
|
+
if (fit !== null) return fit;
|
|
414
479
|
}
|
|
415
480
|
|
|
416
481
|
// Allocate a fresh page if we haven't hit the max.
|
|
@@ -524,12 +589,31 @@ export class AtlasPool {
|
|
|
524
589
|
return acq;
|
|
525
590
|
}
|
|
526
591
|
|
|
527
|
-
/**
|
|
592
|
+
/**
|
|
593
|
+
* Decrement the refcount. On zero we *don't* eagerly free — the entry
|
|
594
|
+
* moves to the LRU instead, so a re-acquire of the same aval (or a
|
|
595
|
+
* value-keyed sibling) can resurrect it without re-uploading the
|
|
596
|
+
* texture. Eviction (actual `actuallyFree`) happens lazily when a
|
|
597
|
+
* fresh acquire can't fit and we need to reclaim packer space.
|
|
598
|
+
*/
|
|
528
599
|
release(ref: number): void {
|
|
529
600
|
const e = this.entriesByRef.get(ref);
|
|
530
601
|
if (e === undefined) return;
|
|
531
602
|
e.refcount--;
|
|
532
603
|
if (e.refcount > 0) return;
|
|
604
|
+
// Hold the entry idle in the LRU. Move-to-end (Map insertion-order
|
|
605
|
+
// semantics): delete + set re-positions a key that was already
|
|
606
|
+
// present, but the refcount-0 path shouldn't see one here — guard
|
|
607
|
+
// anyway in case of double-release.
|
|
608
|
+
this.lru.delete(e.ref);
|
|
609
|
+
this.lru.set(e.ref, e);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Drop an LRU entry for real: remove its packer slot, drop the entry
|
|
614
|
+
* from every lookup map. Caller must guarantee `refcount === 0`.
|
|
615
|
+
*/
|
|
616
|
+
private actuallyFree(e: AtlasEntry): void {
|
|
533
617
|
const pages = this.pagesByFormat.get(e.format)!;
|
|
534
618
|
const page = pages[e.pageId];
|
|
535
619
|
if (page !== undefined) {
|
|
@@ -538,6 +622,23 @@ export class AtlasPool {
|
|
|
538
622
|
this.entriesByRef.delete(e.ref);
|
|
539
623
|
for (const a of e.aliases) this.entriesByAval.delete(a);
|
|
540
624
|
if (e.valueKey !== undefined) this.entriesByValueKey.delete(e.valueKey);
|
|
625
|
+
this.lru.delete(e.ref);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Free every refcount-0 entry held by the LRU. Used by tests that
|
|
630
|
+
* want to assert packer-empty after a release, and by callers that
|
|
631
|
+
* want to release atlas memory pressure on demand. Returns the
|
|
632
|
+
* number of entries freed. Outside of tests, eviction normally
|
|
633
|
+
* happens lazily on packer pressure during a subsequent `acquire`.
|
|
634
|
+
*/
|
|
635
|
+
evictIdle(): number {
|
|
636
|
+
let n = 0;
|
|
637
|
+
for (const [, entry] of this.lru) {
|
|
638
|
+
this.actuallyFree(entry);
|
|
639
|
+
n++;
|
|
640
|
+
}
|
|
641
|
+
return n;
|
|
541
642
|
}
|
|
542
643
|
|
|
543
644
|
/** Destroy every page texture. The pool becomes unusable. */
|
|
@@ -550,6 +651,7 @@ export class AtlasPool {
|
|
|
550
651
|
this.entriesByAval.clear();
|
|
551
652
|
this.entriesByRef.clear();
|
|
552
653
|
this.entriesByValueKey.clear();
|
|
654
|
+
this.lru.clear();
|
|
553
655
|
}
|
|
554
656
|
|
|
555
657
|
private finalize(
|
|
@@ -611,6 +713,33 @@ export class AtlasPool {
|
|
|
611
713
|
h: number,
|
|
612
714
|
numMips: number,
|
|
613
715
|
): void {
|
|
716
|
+
// Fast path — `external` source, no mips: upload mip-0 straight to
|
|
717
|
+
// the page with `copyExternalImageToTexture` (decode happens on the
|
|
718
|
+
// GPU/compositor side, not the main thread) and fill the 2-px
|
|
719
|
+
// gutter ring with tiny intra-page `copyTextureToTexture` ops. This
|
|
720
|
+
// skips `extractPixels`, whose canvas-2d `drawImage`+`getImageData`
|
|
721
|
+
// round-trip per texture was ~8% of cold-boot CPU on the main
|
|
722
|
+
// thread (the heap-demo-sg profile). If the platform rejects
|
|
723
|
+
// same-texture copies (the original reason this path didn't exist —
|
|
724
|
+
// older Dawn) the gutter cells just stay as-is; per the existing
|
|
725
|
+
// tolerance that's "visually invisible" for typical atlas content,
|
|
726
|
+
// and mip-0 itself is correct either way. `numMips > 1` and `raw`
|
|
727
|
+
// sources keep the kernel / CPU path (mip downsampling genuinely
|
|
728
|
+
// needs the pixels; for `raw` `extractPixels` is a free slice).
|
|
729
|
+
if (host.kind === "external" && numMips <= 1) {
|
|
730
|
+
try {
|
|
731
|
+
this.device.queue.copyExternalImageToTexture(
|
|
732
|
+
{ source: host.source as GPUImageCopyExternalImageSource },
|
|
733
|
+
{ texture: page.texture, origin: { x, y, z: 0 } },
|
|
734
|
+
{ width: w, height: h, depthOrArrayLayers: 1 },
|
|
735
|
+
);
|
|
736
|
+
this.fillGutterRing(page.texture, x, y, w, h);
|
|
737
|
+
return;
|
|
738
|
+
} catch {
|
|
739
|
+
// Source not in a copyable state (e.g. a not-ready video) —
|
|
740
|
+
// fall through to the CPU/kernel path below.
|
|
741
|
+
}
|
|
742
|
+
}
|
|
614
743
|
// For storage-capable pages (rgba8unorm): use the GPU compute
|
|
615
744
|
// kernel to build mips + gutter in one pass. Mip 0 interior is
|
|
616
745
|
// uploaded raw via writeTexture; the kernel reads from there to
|
|
@@ -798,13 +927,20 @@ export class AtlasPool {
|
|
|
798
927
|
return true;
|
|
799
928
|
}
|
|
800
929
|
|
|
801
|
-
/**
|
|
802
|
-
|
|
803
|
-
|
|
930
|
+
/**
|
|
931
|
+
* Fill a sub-rect's 2-px gutter ring (inner clamp-replicate ring +
|
|
932
|
+
* outer wrap ring) from the already-uploaded interior pixels, using
|
|
933
|
+
* ~24 tiny intra-page `copyTextureToTexture` ops. `(ix, iy)` is the
|
|
934
|
+
* interior origin (= sub-rect origin + 2,+2), `w×h` the interior
|
|
935
|
+
* size. Used by the `copyExternalImageToTexture` fast path. If the
|
|
936
|
+
* platform rejects same-texture copies these become no-ops (validation
|
|
937
|
+
* errors don't throw synchronously) and the gutter cells stay as-is —
|
|
938
|
+
* which is the documented "visually invisible" tolerance.
|
|
939
|
+
*/
|
|
940
|
+
private fillGutterRing(
|
|
941
|
+
t: GPUTexture,
|
|
804
942
|
ix: number, iy: number, w: number, h: number,
|
|
805
943
|
): void {
|
|
806
|
-
void page; void ix; void iy; void w; void h;
|
|
807
|
-
const t = page.texture;
|
|
808
944
|
const enc = this.device.createCommandEncoder({ label: "atlas/gutter" });
|
|
809
945
|
const cp = (sx: number, sy: number, sw: number, sh: number, dx: number, dy: number): void => {
|
|
810
946
|
enc.copyTextureToTexture(
|