@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
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
|
+
}
|
package/src/core/renderObject.ts
CHANGED
|
@@ -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
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
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
|
|
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). */
|
package/src/core/sampler.ts
CHANGED
|
@@ -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 {
|
|
11
|
-
|
|
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;
|
package/src/core/texture.ts
CHANGED
|
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
|
|
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.
|
|
538
|
-
const fromInstance = instanceMap.
|
|
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:
|
|
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
|
-
|
|
765
|
-
|
|
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
|
-
|
|
768
|
-
|
|
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
|
-
|
|
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
|
-
|
|
176
|
-
|
|
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 —
|
|
179
|
-
//
|
|
180
|
-
//
|
|
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
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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.
|
|
198
|
-
//
|
|
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 (
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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 (
|
|
325
|
+
} else if (distinctTexAvals.size > 0 || distinctSamplerAvals.size > 0) {
|
|
292
326
|
throw new Error(
|
|
293
|
-
`heapAdapter: RO has ${
|
|
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
|
}
|