@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.
Files changed (74) hide show
  1. package/dist/core/buffer.d.ts.map +1 -1
  2. package/dist/core/buffer.js +65 -5
  3. package/dist/core/buffer.js.map +1 -1
  4. package/dist/core/index.d.ts +2 -0
  5. package/dist/core/index.d.ts.map +1 -1
  6. package/dist/core/index.js +1 -0
  7. package/dist/core/index.js.map +1 -1
  8. package/dist/core/provider.d.ts +38 -0
  9. package/dist/core/provider.d.ts.map +1 -0
  10. package/dist/core/provider.js +104 -0
  11. package/dist/core/provider.js.map +1 -0
  12. package/dist/core/renderObject.d.ts +17 -10
  13. package/dist/core/renderObject.d.ts.map +1 -1
  14. package/dist/core/sampler.d.ts.map +1 -1
  15. package/dist/core/sampler.js +71 -2
  16. package/dist/core/sampler.js.map +1 -1
  17. package/dist/core/texture.d.ts +9 -0
  18. package/dist/core/texture.d.ts.map +1 -1
  19. package/dist/core/texture.js +0 -0
  20. package/dist/core/texture.js.map +1 -1
  21. package/dist/resources/adaptiveTexture.d.ts.map +1 -1
  22. package/dist/resources/adaptiveTexture.js +7 -0
  23. package/dist/resources/adaptiveTexture.js.map +1 -1
  24. package/dist/resources/preparedRenderObject.d.ts.map +1 -1
  25. package/dist/resources/preparedRenderObject.js +20 -18
  26. package/dist/resources/preparedRenderObject.js.map +1 -1
  27. package/dist/resources/uniformBuffer.d.ts.map +1 -1
  28. package/dist/resources/uniformBuffer.js +23 -0
  29. package/dist/resources/uniformBuffer.js.map +1 -1
  30. package/dist/runtime/derivedUniforms/slots.d.ts.map +1 -1
  31. package/dist/runtime/derivedUniforms/slots.js +4 -0
  32. package/dist/runtime/derivedUniforms/slots.js.map +1 -1
  33. package/dist/runtime/heapAdapter.d.ts.map +1 -1
  34. package/dist/runtime/heapAdapter.js +72 -29
  35. package/dist/runtime/heapAdapter.js.map +1 -1
  36. package/dist/runtime/heapDecoder.d.ts +52 -0
  37. package/dist/runtime/heapDecoder.d.ts.map +1 -0
  38. package/dist/runtime/heapDecoder.js +363 -0
  39. package/dist/runtime/heapDecoder.js.map +1 -0
  40. package/dist/runtime/heapEffectIR.d.ts.map +1 -1
  41. package/dist/runtime/heapEffectIR.js +300 -3
  42. package/dist/runtime/heapEffectIR.js.map +1 -1
  43. package/dist/runtime/heapEligibility.d.ts.map +1 -1
  44. package/dist/runtime/heapEligibility.js +42 -5
  45. package/dist/runtime/heapEligibility.js.map +1 -1
  46. package/dist/runtime/heapIrBuilders.d.ts +64 -0
  47. package/dist/runtime/heapIrBuilders.d.ts.map +1 -0
  48. package/dist/runtime/heapIrBuilders.js +311 -0
  49. package/dist/runtime/heapIrBuilders.js.map +1 -0
  50. package/dist/runtime/heapScene.d.ts.map +1 -1
  51. package/dist/runtime/heapScene.js +286 -97
  52. package/dist/runtime/heapScene.js.map +1 -1
  53. package/dist/runtime/textureAtlas/atlasPool.d.ts +67 -4
  54. package/dist/runtime/textureAtlas/atlasPool.d.ts.map +1 -1
  55. package/dist/runtime/textureAtlas/atlasPool.js +152 -18
  56. package/dist/runtime/textureAtlas/atlasPool.js.map +1 -1
  57. package/package.json +4 -3
  58. package/src/core/buffer.ts +58 -6
  59. package/src/core/index.ts +11 -0
  60. package/src/core/provider.ts +143 -0
  61. package/src/core/renderObject.ts +17 -10
  62. package/src/core/sampler.ts +64 -2
  63. package/src/core/texture.ts +0 -0
  64. package/src/resources/adaptiveTexture.ts +7 -0
  65. package/src/resources/preparedRenderObject.ts +21 -16
  66. package/src/resources/uniformBuffer.ts +21 -0
  67. package/src/runtime/derivedUniforms/slots.ts +4 -0
  68. package/src/runtime/heapAdapter.ts +63 -29
  69. package/src/runtime/heapDecoder.ts +446 -0
  70. package/src/runtime/heapEffectIR.ts +314 -3
  71. package/src/runtime/heapEligibility.ts +34 -5
  72. package/src/runtime/heapIrBuilders.ts +365 -0
  73. package/src/runtime/heapScene.ts +295 -81
  74. 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 { aval } from "@aardworx/wombat.adaptive";
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
- private readonly entriesByAval = new Map<aval<ITexture>, AtlasEntry>();
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
- return t.kind === "gpu" ? t.texture : t.source;
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
- // Try fitting into existing pages first.
407
- for (let i = 0; i < pages.length; i++) {
408
- const page = pages[i]!;
409
- const next = page.packing.tryAdd(this.nextRef, size);
410
- if (next !== null) {
411
- page.packing = next;
412
- return this.finalize(sourceAval, format, i, page, opts.source, width, height, numMips, valueKey);
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
- /** Decrement the refcount; on zero, free the sub-rect. */
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
- /** Unused stub kept for compatibility — see uploadLevelWithGutter. */
802
- private fillClampGutter(
803
- page: AtlasPage,
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(