@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
package/src/core/index.ts CHANGED
@@ -74,6 +74,17 @@ export type {
74
74
  RenderObject,
75
75
  } from "./renderObject.js";
76
76
 
77
+ export type {
78
+ IUniformProvider,
79
+ IAttributeProvider,
80
+ } from "./provider.js";
81
+ export {
82
+ UniformProvider,
83
+ AttributeProvider,
84
+ asUniformProvider,
85
+ asAttributeProvider,
86
+ } from "./provider.js";
87
+
77
88
  export {
78
89
  RenderTree,
79
90
  } from "./renderTree.js";
@@ -0,0 +1,143 @@
1
+ // IUniformProvider / IAttributeProvider — lazy, by-name lookup of a
2
+ // RenderObject's uniform values and vertex/instance attribute views.
3
+ //
4
+ // Modelled on Aardvark.Rendering's `IUniformProvider` /
5
+ // `IAttributeProvider`: the binding layer always pulls *shader-driven*
6
+ // — for each name the compiled shader actually declares it calls
7
+ // `tryGet(name)`. A name no shader reads is therefore never
8
+ // materialised by a lazy provider. This is what lets the scene-graph
9
+ // auto-inject all ~15 derived trafo uniforms (`ModelViewProjTrafo`,
10
+ // `NormalMatrix`, the inverses, …) per leaf for free: their `compose`
11
+ // / `inverse` aval chains are only built when some effect's interface
12
+ // references the name.
13
+ //
14
+ // `names()` is the (cheap) enumeration surface — a lazy provider lists
15
+ // the names it *could* produce without materialising their values. The
16
+ // only consumers that enumerate (rather than pull by shader name) are
17
+ // the texture-split in the Sg compile layer (operates on the user map,
18
+ // not a provider) and the heap-eligibility stride check (iterates the
19
+ // attribute provider's names — always map-backed in practice).
20
+
21
+ import type { HashMap, aval } from "@aardworx/wombat.adaptive";
22
+ import type { BufferView } from "./bufferView.js";
23
+
24
+ export interface IUniformProvider {
25
+ /** Uniform value for `name`, or `undefined` if not provided. Pulled
26
+ * once per shader-declared uniform; lazy providers materialise the
27
+ * value here, on first request. */
28
+ tryGet(name: string): aval<unknown> | undefined;
29
+ /** Names this provider knows about (for enumeration / diagnostics).
30
+ * A lazy provider lists derivable names without building them. */
31
+ names(): Iterable<string>;
32
+ }
33
+
34
+ export interface IAttributeProvider {
35
+ /** Vertex/instance attribute view for `name`, or `undefined`. */
36
+ tryGet(name: string): BufferView | undefined;
37
+ names(): Iterable<string>;
38
+ }
39
+
40
+ // ─── shared impl helpers ───────────────────────────────────────────────
41
+
42
+ function mapKeys<K, V>(m: HashMap<K, V>): K[] {
43
+ const out: K[] = [];
44
+ for (const [k] of m) out.push(k);
45
+ return out;
46
+ }
47
+
48
+ function hasOwn(o: object, k: string): boolean {
49
+ return Object.prototype.hasOwnProperty.call(o, k);
50
+ }
51
+
52
+ const EMPTY_NAMES: readonly string[] = [];
53
+
54
+ function makeProvider<T>(): {
55
+ empty: { tryGet(name: string): T | undefined; names(): Iterable<string> };
56
+ ofMap(m: HashMap<string, T>): { tryGet(name: string): T | undefined; names(): Iterable<string> };
57
+ ofObject(o: Readonly<Record<string, T>>): { tryGet(name: string): T | undefined; names(): Iterable<string> };
58
+ union(...ps: { tryGet(name: string): T | undefined; names(): Iterable<string> }[]): { tryGet(name: string): T | undefined; names(): Iterable<string> };
59
+ lazy(known: readonly string[], compute: (name: string) => T | undefined): { tryGet(name: string): T | undefined; names(): Iterable<string> };
60
+ } {
61
+ type P = { tryGet(name: string): T | undefined; names(): Iterable<string> };
62
+ const empty: P = { tryGet: () => undefined, names: () => EMPTY_NAMES };
63
+ return {
64
+ empty,
65
+ ofMap(m: HashMap<string, T>): P {
66
+ if (m.containsKey === undefined) return empty; // defensive
67
+ return { tryGet: (n) => m.tryFind(n), names: () => mapKeys(m) };
68
+ },
69
+ ofObject(o: Readonly<Record<string, T>>): P {
70
+ return { tryGet: (n) => (hasOwn(o, n) ? o[n] : undefined), names: () => Object.keys(o) };
71
+ },
72
+ /** First non-`undefined` wins. Pass providers in priority order —
73
+ * e.g. `[leafAttrs, scopeAttrs]` so a leaf shadows the scope, or
74
+ * `[userUniforms, autoInjected]` so the user overrides defaults. */
75
+ union(...ps: P[]): P {
76
+ if (ps.length === 0) return empty;
77
+ if (ps.length === 1) return ps[0]!;
78
+ return {
79
+ tryGet: (n) => {
80
+ for (let i = 0; i < ps.length; i++) {
81
+ const v = ps[i]!.tryGet(n);
82
+ if (v !== undefined) return v;
83
+ }
84
+ return undefined;
85
+ },
86
+ names: () => {
87
+ const s = new Set<string>();
88
+ for (const p of ps) for (const n of p.names()) s.add(n);
89
+ return s;
90
+ },
91
+ };
92
+ },
93
+ /** `compute(name)` runs the first time `name` is requested; the
94
+ * result (incl. `undefined`) is memoised. `known` is the cheap
95
+ * list of names `compute` can produce. */
96
+ lazy(known: readonly string[], compute: (name: string) => T | undefined): P {
97
+ const cache = new Map<string, T | undefined>();
98
+ return {
99
+ tryGet: (n) => {
100
+ const hit = cache.get(n);
101
+ if (hit !== undefined || cache.has(n)) return hit;
102
+ const v = compute(n);
103
+ cache.set(n, v);
104
+ return v;
105
+ },
106
+ names: () => known,
107
+ };
108
+ },
109
+ };
110
+ }
111
+
112
+ export const UniformProvider = makeProvider<aval<unknown>>() as {
113
+ empty: IUniformProvider;
114
+ ofMap(m: HashMap<string, aval<unknown>>): IUniformProvider;
115
+ ofObject(o: Readonly<Record<string, aval<unknown>>>): IUniformProvider;
116
+ union(...ps: IUniformProvider[]): IUniformProvider;
117
+ lazy(known: readonly string[], compute: (name: string) => aval<unknown> | undefined): IUniformProvider;
118
+ };
119
+
120
+ export const AttributeProvider = makeProvider<BufferView>() as {
121
+ empty: IAttributeProvider;
122
+ ofMap(m: HashMap<string, BufferView>): IAttributeProvider;
123
+ ofObject(o: Readonly<Record<string, BufferView>>): IAttributeProvider;
124
+ union(...ps: IAttributeProvider[]): IAttributeProvider;
125
+ lazy(known: readonly string[], compute: (name: string) => BufferView | undefined): IAttributeProvider;
126
+ };
127
+
128
+ function isProvider(x: unknown): x is { tryGet(n: string): unknown; names(): Iterable<string> } {
129
+ return x !== null && typeof x === "object" && typeof (x as { tryGet?: unknown }).tryGet === "function";
130
+ }
131
+
132
+ /** Normalise a `RenderObject.uniforms` value: a real provider passes
133
+ * through; a plain `HashMap<string, aval>` (legacy shape, still used
134
+ * by some hand-built test fixtures) is wrapped as a map-backed
135
+ * provider. Consumers call this at the boundary so both shapes work. */
136
+ export function asUniformProvider(x: IUniformProvider | HashMap<string, aval<unknown>>): IUniformProvider {
137
+ return isProvider(x) ? x : UniformProvider.ofMap(x);
138
+ }
139
+
140
+ /** Likewise for `vertexAttributes` / `instanceAttributes`. */
141
+ export function asAttributeProvider(x: IAttributeProvider | HashMap<string, BufferView>): IAttributeProvider {
142
+ return isProvider(x) ? x : AttributeProvider.ofMap(x);
143
+ }
@@ -16,6 +16,7 @@ import type { ISampler } from "./sampler.js";
16
16
  import type { ITexture } from "./texture.js";
17
17
  import type { PipelineState } from "./pipelineState.js";
18
18
  import type { Effect } from "./shader.js";
19
+ import type { IAttributeProvider, IUniformProvider } from "./provider.js";
19
20
 
20
21
  export interface RenderObject {
21
22
  /**
@@ -39,17 +40,23 @@ export interface RenderObject {
39
40
  readonly pipelineState: PipelineState;
40
41
 
41
42
  /**
42
- * name vertex buffer view; e.g. "position", "normal", "uv".
43
- *
44
- * The set of attribute names is fixed structurally; only the
45
- * individual buffer values are reactive. The shader's required-input
46
- * set is fixed at compile; the map must contain at least those names.
43
+ * Vertex attributes, looked up by name. The binding layer pulls
44
+ * shader-driven (one `tryGet(name)` per declared vertex input), so a
45
+ * provider may compute views lazily though in practice attribute
46
+ * providers are map-backed. `undefined` from `tryGet` for a name the
47
+ * shader requires is an error at prepare time.
48
+ */
49
+ readonly vertexAttributes: IAttributeProvider;
50
+ /** Instance attributes, looked up by name (same contract). */
51
+ readonly instanceAttributes?: IAttributeProvider;
52
+ /**
53
+ * Uniform values, looked up by name. The runtime packs the UBO from
54
+ * the shader's declared layout — for each declared uniform it calls
55
+ * `uniforms.tryGet(name)`. Names no shader reads are never pulled, so
56
+ * a lazy provider (e.g. the Sg layer's auto-injected derived trafos)
57
+ * never builds their aval chains.
47
58
  */
48
- readonly vertexAttributes: HashMap<string, BufferView>;
49
- /** name → instance buffer view; e.g. "modelMatrix", "instanceColor". */
50
- readonly instanceAttributes?: HashMap<string, BufferView>;
51
- /** name → uniform value; runtime packs into UBO based on shader layout. */
52
- readonly uniforms: HashMap<string, aval<unknown>>;
59
+ readonly uniforms: IUniformProvider;
53
60
  /** name → texture source (CPU image or pre-built GPUTexture). */
54
61
  readonly textures: HashMap<string, aval<ITexture>>;
55
62
  /** name → sampler source (descriptor or pre-built GPUSampler). */
@@ -1,12 +1,74 @@
1
1
  // ISampler — sampler source. Either a pre-built `GPUSampler` or a
2
2
  // descriptor for the runtime to materialise (samplers are cheap
3
3
  // and dedupable, so descriptors are the common path).
4
+ //
5
+ // Equality / hash protocol: `desc` samplers compare structurally over
6
+ // the descriptor fields (all primitives) so two `fromDescriptor({...})`
7
+ // with the same options collapse in `aval<ISampler>`-keyed caches;
8
+ // `gpu` samplers compare by reference on the opaque `GPUSampler`.
9
+ // `gpu` samplers are additionally interned by the handle so two
10
+ // `fromGPU(sameSampler)` return the same object.
4
11
 
5
12
  export type ISampler =
6
13
  | { readonly kind: "gpu"; readonly sampler: GPUSampler }
7
14
  | { readonly kind: "desc"; readonly descriptor: GPUSamplerDescriptor };
8
15
 
16
+ let _idCounter = 0;
17
+ const _idHashes = new WeakMap<object, number>();
18
+ function idHash(o: object): number {
19
+ let v = _idHashes.get(o);
20
+ if (v === undefined) { v = (++_idCounter) | 0; _idHashes.set(o, v); }
21
+ return v;
22
+ }
23
+ function hashStr(s: string): number {
24
+ let h = 5381;
25
+ for (let i = 0; i < s.length; i++) h = ((h << 5) + h + s.charCodeAt(i)) | 0;
26
+ return h;
27
+ }
28
+ // Stable serialisation of a GPUSamplerDescriptor (sorted keys, only
29
+ // own enumerable primitive fields — the descriptor never contains
30
+ // objects). Used for both equality and hashing.
31
+ function descKey(d: GPUSamplerDescriptor): string {
32
+ const keys = Object.keys(d).sort();
33
+ let out = "";
34
+ for (const k of keys) {
35
+ const v = (d as Record<string, unknown>)[k];
36
+ out += `${k}=${String(v)};`;
37
+ }
38
+ return out;
39
+ }
40
+
41
+ function iSamplerEquals(a: ISampler, o: unknown): boolean {
42
+ if (a === o) return true;
43
+ if (o === null || typeof o !== "object") return false;
44
+ const b = o as ISampler;
45
+ if (a.kind !== b.kind) return false;
46
+ if (a.kind === "gpu") return a.sampler === (b as { sampler: GPUSampler }).sampler;
47
+ return descKey(a.descriptor) === descKey((b as { descriptor: GPUSamplerDescriptor }).descriptor);
48
+ }
49
+ function iSamplerHash(a: ISampler): number {
50
+ if (a.kind === "gpu") return idHash(a.sampler);
51
+ return hashStr("desc:" + descKey(a.descriptor));
52
+ }
53
+ const ISAMPLER_PROTO = {
54
+ equals(this: ISampler, o: unknown): boolean { return iSamplerEquals(this, o); },
55
+ getHashCode(this: ISampler): number { return iSamplerHash(this) | 0; },
56
+ };
57
+ function withEq<T extends ISampler>(t: T): T {
58
+ return Object.assign(Object.create(ISAMPLER_PROTO) as object, t) as T;
59
+ }
60
+
61
+ const _gpuIntern = new WeakMap<GPUSampler, ISampler>();
62
+
9
63
  export const ISampler = {
10
- fromGPU(sampler: GPUSampler): ISampler { return { kind: "gpu", sampler }; },
11
- fromDescriptor(descriptor: GPUSamplerDescriptor): ISampler { return { kind: "desc", descriptor }; },
64
+ fromGPU(sampler: GPUSampler): ISampler {
65
+ const cached = _gpuIntern.get(sampler);
66
+ if (cached !== undefined) return cached;
67
+ const t = withEq({ kind: "gpu" as const, sampler });
68
+ _gpuIntern.set(sampler, t);
69
+ return t;
70
+ },
71
+ fromDescriptor(descriptor: GPUSamplerDescriptor): ISampler {
72
+ return withEq({ kind: "desc" as const, descriptor });
73
+ },
12
74
  } as const;
Binary file
@@ -127,6 +127,13 @@ class AdaptiveTexture extends AdaptiveResource<GPUTexture> {
127
127
  }
128
128
  return src.texture;
129
129
  }
130
+ if (src.kind === "url") {
131
+ // URL textures are an authoring shorthand the wombat.dom Sg layer
132
+ // resolves before binding. Reaching adaptiveTexture means the
133
+ // caller bypassed that — give them a clear error instead of a
134
+ // cryptic TypeError from `src.source.kind`.
135
+ throw new Error(`adaptiveTexture: ITexture.kind === "url" must be resolved before binding (url: ${src.url})`);
136
+ }
130
137
  const desc = descFor(src.source);
131
138
  const usage =
132
139
  (this.opts.usage ?? 0)
@@ -14,8 +14,11 @@
14
14
 
15
15
  import {
16
16
  AdaptiveResource,
17
+ AttributeProvider,
17
18
  BufferView,
18
19
  ElementType,
20
+ asAttributeProvider,
21
+ asUniformProvider,
19
22
  type CompiledEffect,
20
23
  type FramebufferSignature,
21
24
  type ProgramInterface,
@@ -508,8 +511,10 @@ export function prepareRenderObject(
508
511
  // The set of attribute names is fixed structurally; only the
509
512
  // per-attribute BufferView avals are reactive. We resolve names →
510
513
  // step-mode now (vertex vs instance) from the maps directly.
511
- const vertexMap = obj.vertexAttributes;
512
- const instanceMap = obj.instanceAttributes ?? emptyAttrMap();
514
+ const vertexMap = asAttributeProvider(obj.vertexAttributes);
515
+ const instanceMap = obj.instanceAttributes !== undefined
516
+ ? asAttributeProvider(obj.instanceAttributes)
517
+ : AttributeProvider.empty;
513
518
 
514
519
  // Per-attribute resolution + grouping. Multiple attributes can
515
520
  // share one underlying GPUBuffer slot when they all point at the
@@ -534,8 +539,8 @@ export function prepareRenderObject(
534
539
  }
535
540
  const resolved: ResolvedBinding[] = [];
536
541
  for (const vb of vertexBindings) {
537
- const fromVertex = vertexMap.tryFind(vb.name);
538
- const fromInstance = instanceMap.tryFind(vb.name);
542
+ const fromVertex = vertexMap.tryGet(vb.name);
543
+ const fromInstance = instanceMap.tryGet(vb.name);
539
544
  if (fromVertex === undefined && fromInstance === undefined) {
540
545
  throw new Error(`prepareRenderObject: missing vertex attribute "${vb.name}"`);
541
546
  }
@@ -665,7 +670,7 @@ export function prepareRenderObject(
665
670
  for (let g = 0; g <= maxGroup; g++) perGroup.push([]);
666
671
 
667
672
  for (const ub of iface.uniformBlocks) {
668
- const merged = mergeUniformInputs(obj.uniforms, effect.avalBindings, ub);
673
+ const merged = mergeUniformInputs(asUniformProvider(obj.uniforms), effect.avalBindings, ub);
669
674
  const block = ubAsBlock(ub);
670
675
  const res = prepareUniformBuffer(device, block, merged, {
671
676
  ...(opts.label !== undefined ? { label: `${opts.label}.${ub.name}` } : {}),
@@ -739,10 +744,6 @@ export function prepareRenderObject(
739
744
  );
740
745
  }
741
746
 
742
- function emptyAttrMap(): HashMap<string, BufferView> {
743
- return HashMap.empty<string, BufferView>();
744
- }
745
-
746
747
  /**
747
748
  * Sentinel pipeline placeholder. PreparedRenderObject's `pipeline`
748
749
  * field is non-null for ergonomic typing; before the first
@@ -757,19 +758,23 @@ const SENTINEL_PIPELINE = { __sentinel: "PreparedRenderObject.pipeline" } as unk
757
758
  // ---------------------------------------------------------------------------
758
759
 
759
760
  function mergeUniformInputs(
760
- user: HashMap<string, aval<unknown>>,
761
+ user: import("../core/index.js").IUniformProvider,
761
762
  bindings: Readonly<Record<string, () => unknown>> | undefined,
762
763
  block: import("../core/index.js").UniformBlockInfo,
763
764
  ): HashMap<string, aval<unknown>> {
764
- let merged = user;
765
- if (bindings === undefined) return merged;
765
+ // Shader-driven: walk the block's declared fields and pull each from
766
+ // the user provider (lazy providers materialise only the names a
767
+ // shader actually declares), falling back to the effect's
768
+ // closure-captured `avalBindings` default. Uniforms the user provides
769
+ // but the shader doesn't declare are correctly dropped.
770
+ let merged = HashMap.empty<string, aval<unknown>>();
766
771
  for (const f of block.fields) {
767
- if (merged.tryFind(f.name) !== undefined) continue;
768
- const getter = bindings[f.name];
772
+ const u = user.tryGet(f.name);
773
+ if (u !== undefined) { merged = merged.add(f.name, u); continue; }
774
+ const getter = bindings?.[f.name];
769
775
  if (getter === undefined) continue;
770
776
  const v = getter();
771
- if (isAval(v)) merged = merged.add(f.name, v as aval<unknown>);
772
- else merged = merged.add(f.name, cval(v));
777
+ merged = merged.add(f.name, isAval(v) ? (v as aval<unknown>) : cval(v));
773
778
  }
774
779
  return merged;
775
780
  }
@@ -153,6 +153,27 @@ export function writeField(
153
153
  view.u32.set(value, offset >> 2);
154
154
  return;
155
155
  }
156
+ // Trafo3d: pack its forward matrix. The runtime treats `Trafo3d`
157
+ // uniforms uniformly as their forward-mat4 — callers who want the
158
+ // inverse should bind a derived uniform that exposes `.backward`
159
+ // (or use the derived-uniforms compute pass which handles both).
160
+ // Recognised via duck-type on `.forward._data` to stay decoupled
161
+ // from the wombat.base import.
162
+ if (
163
+ value !== null && typeof value === "object" &&
164
+ "forward" in (value as object) &&
165
+ (value as { forward: unknown }).forward !== null &&
166
+ typeof (value as { forward: unknown }).forward === "object" &&
167
+ "_data" in ((value as { forward: object }).forward as object)
168
+ ) {
169
+ const data = ((value as { forward: { _data: unknown } }).forward._data);
170
+ if (data instanceof Float32Array) { view.f32.set(data, offset >> 2); return; }
171
+ if (data instanceof Float64Array) {
172
+ const dst = view.f32.subarray(offset >> 2, (offset >> 2) + data.length);
173
+ for (let i = 0; i < data.length; i++) dst[i] = data[i]!;
174
+ return;
175
+ }
176
+ }
156
177
  if (value !== null && typeof value === "object" && "_data" in (value as object)) {
157
178
  const data = (value as { _data: unknown })._data;
158
179
  if (data instanceof Float32Array) { view.f32.set(data, offset >> 2); return; }
@@ -73,6 +73,10 @@ function packMat4ToSlot(mirror: Float32Array, slotIdx: number, m: M44d): void {
73
73
 
74
74
  export class ConstituentSlots {
75
75
  private readonly pool = new IndexPool();
76
+ // Keyed by `aval<Trafo3d>` *by reference* (plain JS `Map`). Keys
77
+ // here are per-object `cval<Trafo3d>`s in practice; content-keying
78
+ // would buy nothing and cost the reactive-aval identity-hash detour
79
+ // (see the note on `UniformPool.byAval`).
76
80
  private readonly byAval = new Map<aval<Trafo3d>, ConstituentEntry>();
77
81
  private readonly dirtyAvals = new Set<aval<Trafo3d>>();
78
82
  private mirror: Float32Array;
@@ -32,6 +32,7 @@ import type { BufferView } from "../core/bufferView.js";
32
32
  import { ITexture, type HostTextureSource } from "../core/texture.js";
33
33
  import { ISampler } from "../core/sampler.js";
34
34
  import type { RenderObject } from "../core/renderObject.js";
35
+ import { asAttributeProvider, asUniformProvider } from "../core/provider.js";
35
36
  import type { HeapDrawSpec, HeapTextureSet } from "./heapScene.js";
36
37
  import { AtlasPool } from "./textureAtlas/atlasPool.js";
37
38
 
@@ -120,6 +121,9 @@ function describeTexture(t: ITexture): TextureDescriptor | null {
120
121
  mipLevelCount: tex.mipLevelCount,
121
122
  };
122
123
  }
124
+ // URL-deferred textures are resolved at the Sg layer (placeholder
125
+ // checker until ready); they should never reach the heap adapter.
126
+ if (t.kind === "url") return null;
123
127
  const src = t.source;
124
128
  if (src.kind === "raw") {
125
129
  return {
@@ -170,21 +174,52 @@ export function renderObjectToHeapSpec(
170
174
  token: AdaptiveToken,
171
175
  pool?: AtlasPool,
172
176
  ): HeapDrawSpec {
173
- // 1. Inputs map: vertex attributes (BufferView) + uniforms.
177
+ // 1. Inputs map: vertex attributes (BufferView) + uniforms, pulled
178
+ // SHADER-DRIVEN from the providers. We compile `ro.effect` (cached
179
+ // per Effect by the module-level compile cache) to discover the
180
+ // names it declares, then `tryGet` exactly those — so a lazy
181
+ // uniform provider (the Sg layer's auto-injected derived trafos)
182
+ // only ever materialises the ~2-4 trafos an effect actually reads,
183
+ // not all ~15. Compiling without a `fragmentOutputLayout` yields
184
+ // the *unreduced* interface — a superset of what the heap
185
+ // drawHeader ends up with — so we never under-provide; the
186
+ // over-provided handful (uniforms that feed only pruned outputs)
187
+ // is harmless (no drawHeader field → ignored).
188
+ const vAttr = asAttributeProvider(ro.vertexAttributes);
189
+ const uProv = asUniformProvider(ro.uniforms);
190
+ const iface = ro.effect.compile({ target: "wgsl" }).interface;
174
191
  const inputs: { [name: string]: aval<unknown> | unknown } = {};
175
- ro.vertexAttributes.iter((name, bv: BufferView) => { inputs[name] = bv; });
176
- ro.uniforms.iter((name, av) => { inputs[name] = av; });
192
+ for (const a of iface.attributes) {
193
+ const bv = vAttr.tryGet(a.name);
194
+ if (bv !== undefined) inputs[a.name] = bv;
195
+ }
196
+ const pullUniform = (name: string): void => {
197
+ if (Object.prototype.hasOwnProperty.call(inputs, name)) return;
198
+ const av = uProv.tryGet(name);
199
+ if (av !== undefined) inputs[name] = av;
200
+ };
201
+ for (const b of iface.uniformBlocks) for (const f of b.fields) pullUniform(f.name);
202
+ for (const u of iface.uniforms) pullUniform(u.name);
203
+ // §7 derived-uniforms constituents — cheap (these are the raw
204
+ // `state.model/view/proj` avals, no `compose`/`inverse`) and the
205
+ // compute pre-pass needs them even when the effect itself doesn't
206
+ // declare them. No-ops if the provider doesn't carry them.
207
+ for (const n of ["ModelTrafo", "ViewTrafo", "ProjTrafo"]) pullUniform(n);
177
208
 
178
- // 1b. Instance attributes — same shape, threaded via the heap path's
179
- // per-RO instancing fast path (one record / one drawIndirect for
180
- // `instanceCount > 1`, with per-instance attribute reads indexed
181
- // by the in-RO instance idx).
209
+ // 1b. Instance attributes — provider is map-backed (user-supplied via
210
+ // `Sg.instanced({attributes})`), so enumerate its names. Threaded
211
+ // via the heap path's per-RO instancing fast path.
182
212
  let instanceAttributes: { [name: string]: aval<unknown> | unknown } | undefined;
183
- if (ro.instanceAttributes !== undefined && ro.instanceAttributes.count > 0) {
184
- instanceAttributes = {};
185
- ro.instanceAttributes.iter((name, bv: BufferView) => {
186
- instanceAttributes![name] = bv;
187
- });
213
+ if (ro.instanceAttributes !== undefined) {
214
+ const iAttr = asAttributeProvider(ro.instanceAttributes);
215
+ const names = [...iAttr.names()];
216
+ if (names.length > 0) {
217
+ instanceAttributes = {};
218
+ for (const n of names) {
219
+ const bv = iAttr.tryGet(n);
220
+ if (bv !== undefined) instanceAttributes[n] = bv;
221
+ }
222
+ }
188
223
  }
189
224
 
190
225
  // 2. Indices: BufferView → aval<Uint32Array>. Map the underlying
@@ -194,22 +229,21 @@ export function renderObjectToHeapSpec(
194
229
  }
195
230
  const indices: aval<Uint32Array> = indicesAvalFor(ro.indices.buffer);
196
231
 
197
- // 3. Texture/sampler: single-pair v1. Classifier already capped
198
- // counts at 1 each.
232
+ // 3. Texture/sampler: single-pair v1. The Sg compile layer binds
233
+ // each texture aval under both `name` and `${name}_view` so the
234
+ // WGSL schema's binding shape doesn't matter at scene time — that
235
+ // leaves us with two HashMap entries pointing at the same aval.
236
+ // Dedupe by identity before applying the single-pair rule.
237
+ const distinctTexAvals = new Set<aval<ITexture>>();
238
+ ro.textures.iter((_n, av) => { distinctTexAvals.add(av as aval<ITexture>); });
239
+ const distinctSamplerAvals = new Set<aval<ISampler>>();
240
+ ro.samplers.iter((_n, av) => { distinctSamplerAvals.add(av as aval<ISampler>); });
199
241
  let textures: HeapTextureSet | undefined;
200
- if (ro.textures.count === 1 && ro.samplers.count === 1) {
201
- let texAval: aval<unknown> | undefined;
202
- let texVal: ITexture | undefined;
203
- let samplerAval: aval<unknown> | undefined;
204
- let samplerVal: ISampler | undefined;
205
- ro.textures.iter((_n, av) => {
206
- texAval = av;
207
- texVal = av.getValue(token) as ITexture;
208
- });
209
- ro.samplers.iter((_n, av) => {
210
- samplerAval = av;
211
- samplerVal = av.getValue(token) as ISampler;
212
- });
242
+ if (distinctTexAvals.size === 1 && distinctSamplerAvals.size === 1) {
243
+ const texAval = [...distinctTexAvals][0]!;
244
+ const samplerAval = [...distinctSamplerAvals][0]!;
245
+ const texVal = texAval.getValue(token);
246
+ const samplerVal = samplerAval.getValue(token);
213
247
  void samplerAval;
214
248
  if (texVal === undefined || samplerVal === undefined) {
215
249
  throw new Error("heapAdapter: missing texture/sampler value");
@@ -288,9 +322,9 @@ export function renderObjectToHeapSpec(
288
322
  sampler: ISampler.fromGPU(samplerVal.sampler),
289
323
  };
290
324
  }
291
- } else if (ro.textures.count > 0 || ro.samplers.count > 0) {
325
+ } else if (distinctTexAvals.size > 0 || distinctSamplerAvals.size > 0) {
292
326
  throw new Error(
293
- `heapAdapter: RO has ${ro.textures.count} texture(s) and ${ro.samplers.count} sampler(s); ` +
327
+ `heapAdapter: RO has ${distinctTexAvals.size} distinct texture aval(s) and ${distinctSamplerAvals.size} distinct sampler aval(s); ` +
294
328
  `single-pair only in v1 (classifier should have caught this)`,
295
329
  );
296
330
  }