@aardworx/wombat.rendering 0.19.8 → 0.19.10
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/resources/preparedRenderObject.d.ts.map +1 -1
- package/dist/resources/preparedRenderObject.js +49 -5
- package/dist/resources/preparedRenderObject.js.map +1 -1
- package/dist/runtime/derivedUniforms/cpuEval.d.ts +14 -0
- package/dist/runtime/derivedUniforms/cpuEval.d.ts.map +1 -0
- package/dist/runtime/derivedUniforms/cpuEval.js +287 -0
- package/dist/runtime/derivedUniforms/cpuEval.js.map +1 -0
- package/dist/runtime/heapAdapter.d.ts.map +1 -1
- package/dist/runtime/heapAdapter.js +105 -14
- package/dist/runtime/heapAdapter.js.map +1 -1
- package/dist/runtime/heapEligibility.d.ts.map +1 -1
- package/dist/runtime/heapEligibility.js +22 -46
- package/dist/runtime/heapEligibility.js.map +1 -1
- package/dist/runtime/heapScene.d.ts +11 -0
- package/dist/runtime/heapScene.d.ts.map +1 -1
- package/dist/runtime/heapScene.js +5 -2
- package/dist/runtime/heapScene.js.map +1 -1
- package/package.json +1 -1
- package/src/resources/preparedRenderObject.ts +51 -4
- package/src/runtime/derivedUniforms/cpuEval.ts +249 -0
- package/src/runtime/heapAdapter.ts +102 -17
- package/src/runtime/heapEligibility.ts +21 -39
- package/src/runtime/heapScene.ts +16 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aardworx/wombat.rendering",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.10",
|
|
4
4
|
"description": "WebGPU rendering layer for the Wombat TypeScript stack — RenderObject/RenderTask/runtime + window glue, port of Aardvark.Rendering's lower layers on top of WebGPU and wombat.shader.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "krauthaufen",
|
|
@@ -29,8 +29,12 @@ import {
|
|
|
29
29
|
type aval,
|
|
30
30
|
HashMap,
|
|
31
31
|
cval,
|
|
32
|
+
AVal,
|
|
32
33
|
} from "@aardworx/wombat.adaptive";
|
|
33
34
|
import type { Type } from "@aardworx/wombat.shader/ir";
|
|
35
|
+
import { isDerivedRule } from "../runtime/derivedUniforms/rule.js";
|
|
36
|
+
import { STANDARD_DERIVED_RULES, STANDARD_TRAFO_LEAVES } from "../runtime/derivedUniforms/recipes.js";
|
|
37
|
+
import { interpretExpr } from "../runtime/derivedUniforms/cpuEval.js";
|
|
34
38
|
import { prepareAdaptiveBuffer } from "./adaptiveBuffer.js";
|
|
35
39
|
import { prepareAdaptiveTexture } from "./adaptiveTexture.js";
|
|
36
40
|
import { prepareAdaptiveSampler } from "./adaptiveSampler.js";
|
|
@@ -785,11 +789,14 @@ function mergeUniformInputs(
|
|
|
785
789
|
let merged = HashMap.empty<string, aval<unknown>>();
|
|
786
790
|
for (const f of block.fields) {
|
|
787
791
|
const u = user.tryGet(f.name);
|
|
788
|
-
if (u !== undefined) { merged = merged.add(f.name, u); continue; }
|
|
792
|
+
if (u !== undefined) { merged = merged.add(f.name, resolveUniform(u, user, bindings)); continue; }
|
|
789
793
|
const getter = bindings?.[f.name];
|
|
790
|
-
if (getter
|
|
791
|
-
|
|
792
|
-
|
|
794
|
+
if (getter !== undefined) { merged = merged.add(f.name, resolveUniform(getter(), user, bindings)); continue; }
|
|
795
|
+
// Standard derived recipe by name (ModelViewTrafo, …) — derive it CPU-side
|
|
796
|
+
// here just as the heap path derives it on the GPU, so the legacy path
|
|
797
|
+
// serves the same declared uniforms.
|
|
798
|
+
const recipe = STANDARD_DERIVED_RULES.get(f.name);
|
|
799
|
+
if (recipe !== undefined) merged = merged.add(f.name, ruleToAval(recipe, user, bindings));
|
|
793
800
|
}
|
|
794
801
|
return merged;
|
|
795
802
|
}
|
|
@@ -798,6 +805,46 @@ function isAval(v: unknown): boolean {
|
|
|
798
805
|
return typeof v === "object" && v !== null && typeof (v as { getValue?: unknown }).getValue === "function";
|
|
799
806
|
}
|
|
800
807
|
|
|
808
|
+
// A uniform binding value is either an aval/constant or a derivedUniform RULE.
|
|
809
|
+
// Rules are evaluated on CPU (cpuEval) in double precision and exposed as a
|
|
810
|
+
// derived aval — the legacy per-RO path's answer to the heap's §7 compute pass.
|
|
811
|
+
type UProvider = import("../core/index.js").IUniformProvider;
|
|
812
|
+
type Bindings = Readonly<Record<string, () => unknown>> | undefined;
|
|
813
|
+
|
|
814
|
+
function resolveUniform(v: unknown, user: UProvider, bindings: Bindings): aval<unknown> {
|
|
815
|
+
if (isDerivedRule(v)) return ruleToAval(v, user, bindings);
|
|
816
|
+
return isAval(v) ? (v as aval<unknown>) : cval(v);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function ruleToAval(rule: import("../runtime/derivedUniforms/rule.js").DerivedRule, user: UProvider, bindings: Bindings): aval<unknown> {
|
|
820
|
+
return AVal.custom((token: AdaptiveToken) =>
|
|
821
|
+
interpretExpr(rule.ir, (leaf) => readLeaf(leaf, user, bindings, token)),
|
|
822
|
+
) as aval<unknown>;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// Resolve a `u.<name>` leaf to its current value, recursing through nested
|
|
826
|
+
// rules / standard recipes. Reading the leaf aval's value under `token`
|
|
827
|
+
// registers the dependency, so the derived aval re-evaluates reactively.
|
|
828
|
+
function readLeaf(name: string, user: UProvider, bindings: Bindings, token: AdaptiveToken): unknown {
|
|
829
|
+
const evalRule = (r: unknown): unknown =>
|
|
830
|
+
interpretExpr((r as import("../runtime/derivedUniforms/rule.js").DerivedRule).ir, (n) => readLeaf(n, user, bindings, token));
|
|
831
|
+
const lookup = (n: string): unknown => {
|
|
832
|
+
const u = user.tryGet(n);
|
|
833
|
+
if (u !== undefined) return isDerivedRule(u) ? evalRule(u) : isAval(u) ? (u as aval<unknown>).getValue(token) : u;
|
|
834
|
+
const getter = bindings?.[n];
|
|
835
|
+
if (getter !== undefined) { const v = getter(); return isDerivedRule(v) ? evalRule(v) : isAval(v) ? (v as aval<unknown>).getValue(token) : v; }
|
|
836
|
+
const recipe = STANDARD_DERIVED_RULES.get(n);
|
|
837
|
+
if (recipe !== undefined) return evalRule(recipe);
|
|
838
|
+
return undefined;
|
|
839
|
+
};
|
|
840
|
+
const direct = lookup(name);
|
|
841
|
+
if (direct !== undefined) return direct;
|
|
842
|
+
// Standard recipes read short trafo leaves (Model / View / Proj) that the RO
|
|
843
|
+
// binds under ModelTrafo / ViewTrafo / ProjTrafo — alias them like the heap.
|
|
844
|
+
const alias = STANDARD_TRAFO_LEAVES.get(name);
|
|
845
|
+
return alias !== undefined ? lookup(alias) : undefined;
|
|
846
|
+
}
|
|
847
|
+
|
|
801
848
|
function ubAsBlock(ub: import("../core/index.js").UniformBlockInfo): import("../core/index.js").UniformBlockInfo {
|
|
802
849
|
return ub;
|
|
803
850
|
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
// CPU interpreter for derived-uniform rule IR (a wombat.shader `Expr`).
|
|
2
|
+
//
|
|
3
|
+
// Evaluates a `derivedUniform(u => …)` rule in DOUBLE precision (M44d / V3d /
|
|
4
|
+
// …) — no df32 dual-float trick, no GPU constituent slots. The heap path
|
|
5
|
+
// compiles rules into a §7 GPU compute pass (f32, df32 for the inverse trick);
|
|
6
|
+
// here we just interpret the IR with f64 host math (`M44d.inverse()` is exact
|
|
7
|
+
// enough that the backward-storage trick is unnecessary), and `writeField`
|
|
8
|
+
// narrows the result to f32 at pack time.
|
|
9
|
+
//
|
|
10
|
+
// Used by the legacy per-RO uniform path so derived uniforms work on ROs that
|
|
11
|
+
// aren't heap-eligible (a storage buffer, 2+ textures, …). The evaluator is a
|
|
12
|
+
// standalone `Expr → value` function so other CPU rule paths can reuse it.
|
|
13
|
+
|
|
14
|
+
import type { Expr, Type } from "@aardworx/wombat.shader/ir";
|
|
15
|
+
import { V2d, V3d, V4d, M22d, M33d, M44d } from "@aardworx/wombat.base";
|
|
16
|
+
|
|
17
|
+
/** A value produced by the interpreter (host doubles). */
|
|
18
|
+
export type EvalValue = number | boolean | V2d | V3d | V4d | M22d | M33d | M44d;
|
|
19
|
+
|
|
20
|
+
/** Resolve a `u.<name>` leaf to its current host value (Trafo3d | M44f | M44d |
|
|
21
|
+
* V*f | number | …) — or undefined if unbound. */
|
|
22
|
+
export type LeafReader = (name: string) => unknown;
|
|
23
|
+
|
|
24
|
+
// ── runtime classification ───────────────────────────────────────────────
|
|
25
|
+
function isVec(v: unknown): v is V2d | V3d | V4d {
|
|
26
|
+
return v instanceof V2d || v instanceof V3d || v instanceof V4d;
|
|
27
|
+
}
|
|
28
|
+
function isMat(v: unknown): v is M22d | M33d | M44d {
|
|
29
|
+
return v instanceof M22d || v instanceof M33d || v instanceof M44d;
|
|
30
|
+
}
|
|
31
|
+
function data(v: EvalValue): Float64Array { return (v as unknown as { _data: Float64Array })._data; }
|
|
32
|
+
function num(v: EvalValue): number {
|
|
33
|
+
if (typeof v === "number") return v;
|
|
34
|
+
if (typeof v === "boolean") return v ? 1 : 0;
|
|
35
|
+
throw new Error("derived eval: expected scalar, got " + describe(v));
|
|
36
|
+
}
|
|
37
|
+
function describe(v: unknown): string {
|
|
38
|
+
if (isVec(v)) return "vec" + data(v as EvalValue).length;
|
|
39
|
+
if (isMat(v)) return "mat";
|
|
40
|
+
return typeof v;
|
|
41
|
+
}
|
|
42
|
+
function vecOf(d: ArrayLike<number>): V2d | V3d | V4d {
|
|
43
|
+
switch (d.length) {
|
|
44
|
+
case 2: return new V2d(d[0]!, d[1]!);
|
|
45
|
+
case 3: return new V3d(d[0]!, d[1]!, d[2]!);
|
|
46
|
+
case 4: return new V4d(d[0]!, d[1]!, d[2]!, d[3]!);
|
|
47
|
+
default: throw new Error("derived eval: bad vector arity " + d.length);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── leaf coercion (by the IR leaf type) ──────────────────────────────────
|
|
52
|
+
function toM44(raw: unknown): M44d {
|
|
53
|
+
if (raw instanceof M44d) return raw;
|
|
54
|
+
const o = raw as { forward?: unknown; _data?: ArrayLike<number> };
|
|
55
|
+
if (o && o.forward !== undefined) return toM44(o.forward); // Trafo3d → forward
|
|
56
|
+
if (o && o._data !== undefined && o._data.length >= 16) return M44d.fromArray(o._data); // M44f
|
|
57
|
+
throw new Error("derived eval: leaf is not a mat4: " + describe(raw));
|
|
58
|
+
}
|
|
59
|
+
function upperLeft3(m: M44d): M33d {
|
|
60
|
+
const d = data(m); // row-major r*4+c
|
|
61
|
+
return M33d.fromArray([d[0]!, d[1]!, d[2]!, d[4]!, d[5]!, d[6]!, d[8]!, d[9]!, d[10]!]);
|
|
62
|
+
}
|
|
63
|
+
function toM33(raw: unknown): M33d {
|
|
64
|
+
if (raw instanceof M33d) return raw;
|
|
65
|
+
const o = raw as { forward?: unknown; _data?: ArrayLike<number> };
|
|
66
|
+
if (raw instanceof M44d) return upperLeft3(raw);
|
|
67
|
+
if (o && o.forward !== undefined) return upperLeft3(toM44(o.forward));
|
|
68
|
+
if (o && o._data !== undefined && o._data.length === 9) return M33d.fromArray(o._data);
|
|
69
|
+
if (o && o._data !== undefined && o._data.length >= 16) return upperLeft3(toM44(raw));
|
|
70
|
+
throw new Error("derived eval: leaf is not a mat3: " + describe(raw));
|
|
71
|
+
}
|
|
72
|
+
function toM22(raw: unknown): M22d {
|
|
73
|
+
if (raw instanceof M22d) return raw;
|
|
74
|
+
const o = raw as { _data?: ArrayLike<number> };
|
|
75
|
+
if (o && o._data !== undefined && o._data.length === 4) return M22d.fromArray(o._data);
|
|
76
|
+
throw new Error("derived eval: leaf is not a mat2: " + describe(raw));
|
|
77
|
+
}
|
|
78
|
+
function toVec(raw: unknown, n: 2 | 3 | 4): V2d | V3d | V4d {
|
|
79
|
+
if (isVec(raw)) return raw;
|
|
80
|
+
const o = raw as { _data?: ArrayLike<number> };
|
|
81
|
+
if (o && o._data !== undefined) {
|
|
82
|
+
const d = o._data;
|
|
83
|
+
return vecOf(n === 2 ? [d[0]!, d[1]!] : n === 3 ? [d[0]!, d[1]!, d[2]!] : [d[0]!, d[1]!, d[2]!, d[3]!]);
|
|
84
|
+
}
|
|
85
|
+
throw new Error("derived eval: leaf is not a vec" + n + ": " + describe(raw));
|
|
86
|
+
}
|
|
87
|
+
function coerce(raw: unknown, t: Type): EvalValue {
|
|
88
|
+
switch (t.kind) {
|
|
89
|
+
case "Matrix": return t.rows === 4 ? toM44(raw) : t.rows === 3 ? toM33(raw) : toM22(raw);
|
|
90
|
+
case "Vector": return toVec(raw, t.dim);
|
|
91
|
+
case "Float": return Number(raw);
|
|
92
|
+
case "Int": return Math.trunc(Number(raw));
|
|
93
|
+
case "Bool": return Boolean(raw);
|
|
94
|
+
default: throw new Error("derived eval: unsupported leaf type " + t.kind);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ── arithmetic over scalar / vector / matrix ─────────────────────────────
|
|
99
|
+
function negate(x: EvalValue): EvalValue {
|
|
100
|
+
if (typeof x === "number") return -x;
|
|
101
|
+
if (isVec(x) || isMat(x)) return remap(x, (a) => -a);
|
|
102
|
+
throw new Error("derived eval: cannot negate " + describe(x));
|
|
103
|
+
}
|
|
104
|
+
/** Map a vector/matrix's components through `f`, returning the same shape. */
|
|
105
|
+
function remap(v: V2d | V3d | V4d | M22d | M33d | M44d, f: (x: number) => number): EvalValue {
|
|
106
|
+
const d = data(v); const out = new Array<number>(d.length);
|
|
107
|
+
for (let i = 0; i < d.length; i++) out[i] = f(d[i]!);
|
|
108
|
+
return isMat(v) ? matOf(out) : vecOf(out);
|
|
109
|
+
}
|
|
110
|
+
function matOf(d: ArrayLike<number>): M22d | M33d | M44d {
|
|
111
|
+
switch (d.length) {
|
|
112
|
+
case 4: return M22d.fromArray(d);
|
|
113
|
+
case 9: return M33d.fromArray(d);
|
|
114
|
+
case 16: return M44d.fromArray(d);
|
|
115
|
+
default: throw new Error("derived eval: bad matrix arity " + d.length);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/** Element-wise binary op, broadcasting a scalar against a vector/matrix. */
|
|
119
|
+
function elementwise(a: EvalValue, b: EvalValue, f: (x: number, y: number) => number): EvalValue {
|
|
120
|
+
if (typeof a === "number" && typeof b === "number") return f(a, b);
|
|
121
|
+
if (isVec(a) || isMat(a)) {
|
|
122
|
+
const da = data(a);
|
|
123
|
+
const out = new Array<number>(da.length);
|
|
124
|
+
if (typeof b === "number") { for (let i = 0; i < da.length; i++) out[i] = f(da[i]!, b); }
|
|
125
|
+
else { const db = data(b as EvalValue); for (let i = 0; i < da.length; i++) out[i] = f(da[i]!, db[i]!); }
|
|
126
|
+
return isMat(a) ? matOf(out) : vecOf(out);
|
|
127
|
+
}
|
|
128
|
+
if (typeof a === "number" && (isVec(b) || isMat(b))) {
|
|
129
|
+
const db = data(b); const out = new Array<number>(db.length);
|
|
130
|
+
for (let i = 0; i < db.length; i++) out[i] = f(a, db[i]!);
|
|
131
|
+
return isMat(b) ? matOf(out) : vecOf(out);
|
|
132
|
+
}
|
|
133
|
+
throw new Error(`derived eval: cannot combine ${describe(a)} and ${describe(b)}`);
|
|
134
|
+
}
|
|
135
|
+
/** `Mul` node — scalar·scalar, scalar·(vec|mat) broadcast, or component-wise
|
|
136
|
+
* vec·vec (mat·mat / mat·vec / vec·mat have their own node kinds). */
|
|
137
|
+
function mulNode(a: EvalValue, b: EvalValue): EvalValue {
|
|
138
|
+
if (typeof a === "number" && isMat(b)) return (b as M44d).mul(a) as EvalValue;
|
|
139
|
+
if (typeof b === "number" && isMat(a)) return (a as M44d).mul(b) as EvalValue;
|
|
140
|
+
return elementwise(a, b, (x, y) => x * y);
|
|
141
|
+
}
|
|
142
|
+
function vlen(v: EvalValue): number {
|
|
143
|
+
if (typeof v === "number") return Math.abs(v);
|
|
144
|
+
const d = data(v); let s = 0; for (let i = 0; i < d.length; i++) s += d[i]! * d[i]!;
|
|
145
|
+
return Math.sqrt(s);
|
|
146
|
+
}
|
|
147
|
+
function dot(a: EvalValue, b: EvalValue): number {
|
|
148
|
+
const da = data(a), db = data(b); let s = 0;
|
|
149
|
+
for (let i = 0; i < da.length; i++) s += da[i]! * db[i]!;
|
|
150
|
+
return s;
|
|
151
|
+
}
|
|
152
|
+
function normalize(v: EvalValue): EvalValue {
|
|
153
|
+
const len = vlen(v); return len === 0 ? v : remap(v as V3d, (x) => x / len);
|
|
154
|
+
}
|
|
155
|
+
function swizzle(v: EvalValue, comps: readonly string[]): EvalValue {
|
|
156
|
+
const d = data(v); const idx = (c: string): number => ({ x: 0, y: 1, z: 2, w: 3 }[c] ?? 0);
|
|
157
|
+
if (comps.length === 1) return d[idx(comps[0]!)]!;
|
|
158
|
+
return vecOf(comps.map((c) => d[idx(c)]!));
|
|
159
|
+
}
|
|
160
|
+
function convertMatrix(v: EvalValue, t: Type): EvalValue {
|
|
161
|
+
if (t.kind === "Matrix" && t.rows === 3 && v instanceof M44d) return upperLeft3(v);
|
|
162
|
+
if (isMat(v)) return v; // identity-ish convert
|
|
163
|
+
throw new Error("derived eval: ConvertMatrix on " + describe(v));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── intrinsics (vec-componentwise where natural) ─────────────────────────
|
|
167
|
+
const UNARY: Record<string, (x: number) => number> = {
|
|
168
|
+
sin: Math.sin, cos: Math.cos, tan: Math.tan, abs: Math.abs, floor: Math.floor,
|
|
169
|
+
ceil: Math.ceil, sqrt: Math.sqrt, exp: Math.exp, log: Math.log,
|
|
170
|
+
exp2: (x) => 2 ** x, log2: Math.log2, sign: Math.sign, fract: (x) => x - Math.floor(x),
|
|
171
|
+
};
|
|
172
|
+
const BINARY: Record<string, (x: number, y: number) => number> = {
|
|
173
|
+
min: Math.min, max: Math.max, pow: (x, y) => x ** y, atan2: Math.atan2,
|
|
174
|
+
};
|
|
175
|
+
function intrinsic(name: string, a: EvalValue[]): EvalValue {
|
|
176
|
+
if (UNARY[name]) return typeof a[0] === "number" ? UNARY[name]!(a[0]) : remap(a[0] as V3d, UNARY[name]!);
|
|
177
|
+
if (BINARY[name]) return elementwise(a[0]!, a[1]!, BINARY[name]!);
|
|
178
|
+
switch (name) {
|
|
179
|
+
case "normalize": return normalize(a[0]!);
|
|
180
|
+
case "length": return vlen(a[0]!);
|
|
181
|
+
case "dot": return dot(a[0]!, a[1]!);
|
|
182
|
+
case "cross": return (a[0] as V3d).cross(a[1] as V3d);
|
|
183
|
+
case "distance": return vlen(elementwise(a[0]!, a[1]!, (x, y) => x - y));
|
|
184
|
+
case "step": return elementwise(a[1]!, a[0]!, (x, edge) => (x < edge ? 0 : 1));
|
|
185
|
+
case "clamp": return elementwise(elementwise(a[0]!, a[1]!, Math.max), a[2]!, Math.min);
|
|
186
|
+
case "mix": { // a + (b - a) * t
|
|
187
|
+
const diff = elementwise(a[1]!, a[0]!, (y, x) => y - x);
|
|
188
|
+
const scaled = elementwise(diff, a[2]!, (d, t) => d * t);
|
|
189
|
+
return elementwise(a[0]!, scaled, (x, s) => x + s);
|
|
190
|
+
}
|
|
191
|
+
case "reflect": { const d = dot(a[0]!, a[1]!); return elementwise(a[0]!, elementwise(a[1]!, 2 * d, (n, k) => n * k), (i, s) => i - s); }
|
|
192
|
+
default: throw new Error("derived eval: unsupported intrinsic '" + name + "'");
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Interpret a derived-uniform rule IR, reading `u.<name>` leaves via
|
|
198
|
+
* `readLeaf`. Returns a host double value (`M44d` / `V3d` / number / …) that
|
|
199
|
+
* `writeField` narrows to f32 when packing the UBO.
|
|
200
|
+
*/
|
|
201
|
+
export function interpretExpr(ir: Expr, readLeaf: LeafReader): EvalValue {
|
|
202
|
+
const ev = (e: Expr): EvalValue => {
|
|
203
|
+
switch (e.kind) {
|
|
204
|
+
case "Const": {
|
|
205
|
+
const v = e.value;
|
|
206
|
+
if (v.kind === "Bool") return v.value;
|
|
207
|
+
if (v.kind === "Int" || v.kind === "Float") return v.value;
|
|
208
|
+
throw new Error("derived eval: null const");
|
|
209
|
+
}
|
|
210
|
+
case "ReadInput": {
|
|
211
|
+
if (e.scope !== "Uniform") throw new Error("derived eval: non-uniform leaf scope " + e.scope);
|
|
212
|
+
const raw = readLeaf(e.name);
|
|
213
|
+
if (raw === undefined) throw new Error("derived eval: missing uniform leaf '" + e.name + "'");
|
|
214
|
+
return coerce(raw, e.type);
|
|
215
|
+
}
|
|
216
|
+
case "Neg": return negate(ev(e.value));
|
|
217
|
+
case "Not": return !(ev(e.value) as boolean);
|
|
218
|
+
case "Add": return elementwise(ev(e.lhs), ev(e.rhs), (x, y) => x + y);
|
|
219
|
+
case "Sub": return elementwise(ev(e.lhs), ev(e.rhs), (x, y) => x - y);
|
|
220
|
+
case "Div": return elementwise(ev(e.lhs), ev(e.rhs), (x, y) => x / y);
|
|
221
|
+
case "Mod": return elementwise(ev(e.lhs), ev(e.rhs), (x, y) => x % y);
|
|
222
|
+
case "Mul": return mulNode(ev(e.lhs), ev(e.rhs));
|
|
223
|
+
case "MulMatMat": return (ev(e.lhs) as M44d).mul(ev(e.rhs) as M44d) as EvalValue;
|
|
224
|
+
case "MulMatVec": return (ev(e.lhs) as M44d).mul(ev(e.rhs) as V4d) as EvalValue;
|
|
225
|
+
case "MulVecMat": return (ev(e.rhs) as M44d).transpose().mul(ev(e.lhs) as V4d) as EvalValue;
|
|
226
|
+
case "Transpose": return (ev(e.value) as M44d).transpose();
|
|
227
|
+
case "Inverse": return (ev(e.value) as M44d).inverse();
|
|
228
|
+
case "Determinant": return (ev(e.value) as M44d).determinant();
|
|
229
|
+
case "Dot": return dot(ev(e.lhs), ev(e.rhs));
|
|
230
|
+
case "Cross": return (ev(e.lhs) as V3d).cross(ev(e.rhs) as V3d);
|
|
231
|
+
case "Length": return vlen(ev(e.value));
|
|
232
|
+
case "ConvertMatrix": return convertMatrix(ev(e.value), e.type);
|
|
233
|
+
case "VecSwizzle": return swizzle(ev(e.value), e.comps);
|
|
234
|
+
case "NewVector": return vecOf(e.components.map((c) => num(ev(c))));
|
|
235
|
+
case "And": return (ev(e.lhs) as boolean) && (ev(e.rhs) as boolean);
|
|
236
|
+
case "Or": return (ev(e.lhs) as boolean) || (ev(e.rhs) as boolean);
|
|
237
|
+
case "Eq": return num(ev(e.lhs)) === num(ev(e.rhs));
|
|
238
|
+
case "Neq": return num(ev(e.lhs)) !== num(ev(e.rhs));
|
|
239
|
+
case "Lt": return num(ev(e.lhs)) < num(ev(e.rhs));
|
|
240
|
+
case "Le": return num(ev(e.lhs)) <= num(ev(e.rhs));
|
|
241
|
+
case "Gt": return num(ev(e.lhs)) > num(ev(e.rhs));
|
|
242
|
+
case "Ge": return num(ev(e.lhs)) >= num(ev(e.rhs));
|
|
243
|
+
case "Conditional": return (ev(e.cond) as boolean) ? ev(e.ifTrue) : ev(e.ifFalse);
|
|
244
|
+
case "CallIntrinsic": return intrinsic(e.op.name, e.args.map(ev));
|
|
245
|
+
default: throw new Error("derived eval: unsupported IR node '" + e.kind + "'");
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
return ev(ir);
|
|
249
|
+
}
|
|
@@ -27,13 +27,14 @@
|
|
|
27
27
|
// `ro.samplers.count <= 1` are classifier invariants.
|
|
28
28
|
|
|
29
29
|
import { type aval, type AdaptiveToken } from "@aardworx/wombat.adaptive";
|
|
30
|
-
import
|
|
31
|
-
import
|
|
30
|
+
import { IBuffer, type HostBufferSource } from "../core/buffer.js";
|
|
31
|
+
import { 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
35
|
import { asAttributeProvider, asUniformProvider } from "../core/provider.js";
|
|
36
36
|
import type { HeapDrawSpec, HeapTextureSet } from "./heapScene.js";
|
|
37
|
+
import { isBufferView } from "./heapScene/pools.js";
|
|
37
38
|
import { compileHeapEffect } from "./heapEffect.js";
|
|
38
39
|
import { AtlasPool } from "./textureAtlas/atlasPool.js";
|
|
39
40
|
|
|
@@ -50,13 +51,29 @@ function asUint32(data: HostBufferSource): Uint32Array {
|
|
|
50
51
|
return new Uint32Array(data);
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
// Widen 16-bit indices to u32 (the heap's indexStorage is u32). Copy, not a
|
|
55
|
+
// reinterpret view — each u16 index becomes a u32 element.
|
|
56
|
+
function widenU16(data: HostBufferSource): Uint32Array {
|
|
57
|
+
const u16 = data instanceof Uint16Array
|
|
58
|
+
? data
|
|
59
|
+
: ArrayBuffer.isView(data)
|
|
60
|
+
? new Uint16Array(data.buffer, data.byteOffset, data.byteLength / 2)
|
|
61
|
+
: new Uint16Array(data);
|
|
62
|
+
return Uint32Array.from(u16);
|
|
63
|
+
}
|
|
64
|
+
|
|
53
65
|
// IndexPool keys on aval identity → ROs sharing the same
|
|
54
66
|
// `ro.indices.buffer` must produce the SAME downstream
|
|
55
67
|
// `aval<Uint32Array>`. A fresh `.map(…)` per RO would defeat the
|
|
56
68
|
// pool's identity-based sharing and balloon GPU memory (e.g. 11K
|
|
57
69
|
// spheres each allocating their own copy of the 6144-index buffer
|
|
58
70
|
// → 270 MB).
|
|
59
|
-
|
|
71
|
+
// Keyed by (buffer aval, baseVertex). baseVertex is baked into the index
|
|
72
|
+
// VALUES (index[i] + baseVertex), so a buffer drawn with two different
|
|
73
|
+
// baseVertex offsets needs two distinct transcoded arrays — but the common
|
|
74
|
+
// case (baseVertex 0, or per-primitive index buffers that aren't shared)
|
|
75
|
+
// keeps one entry per buffer and preserves the pool's identity sharing.
|
|
76
|
+
const indicesAvalCache = new WeakMap<aval<IBuffer>, Map<number, aval<Uint32Array>>>();
|
|
60
77
|
|
|
61
78
|
// Per-aval shared "current pool ref" cell. ROs that share an
|
|
62
79
|
// `aval<ITexture>` (and therefore one AtlasPool entry) all read
|
|
@@ -71,20 +88,79 @@ function currentRefCellFor(av: aval<ITexture>, initial: number): { ref: number }
|
|
|
71
88
|
}
|
|
72
89
|
return c;
|
|
73
90
|
}
|
|
74
|
-
function indicesAvalFor(ibAval: aval<IBuffer
|
|
75
|
-
let
|
|
91
|
+
function indicesAvalFor(ibAval: aval<IBuffer>, indexFormat: GPUIndexFormat, baseVertex: number): aval<Uint32Array> {
|
|
92
|
+
let byBase = indicesAvalCache.get(ibAval);
|
|
93
|
+
if (byBase === undefined) {
|
|
94
|
+
byBase = new Map();
|
|
95
|
+
indicesAvalCache.set(ibAval, byBase);
|
|
96
|
+
}
|
|
97
|
+
let av = byBase.get(baseVertex);
|
|
76
98
|
if (av === undefined) {
|
|
77
99
|
av = ibAval.map((ib: IBuffer) => {
|
|
78
100
|
if (ib.kind !== "host") {
|
|
79
101
|
throw new Error("heapAdapter: index buffer flipped to native GPUBuffer; classifier should have caught this");
|
|
80
102
|
}
|
|
81
|
-
|
|
103
|
+
const u32 = indexFormat === "uint16" ? widenU16(ib.data) : asUint32(ib.data);
|
|
104
|
+
if (baseVertex === 0) return u32;
|
|
105
|
+
// Fold baseVertex into the index values so the megacall decode
|
|
106
|
+
// (vid = indexStorage[...]) lands on the right vertex with no
|
|
107
|
+
// extra record field. Copy (don't mutate the shared view).
|
|
108
|
+
const out = new Uint32Array(u32.length);
|
|
109
|
+
for (let i = 0; i < u32.length; i++) out[i] = u32[i]! + baseVertex;
|
|
110
|
+
return out;
|
|
82
111
|
});
|
|
83
|
-
|
|
112
|
+
byBase.set(baseVertex, av);
|
|
84
113
|
}
|
|
85
114
|
return av;
|
|
86
115
|
}
|
|
87
116
|
|
|
117
|
+
// Byte view over a host buffer source (for the de-interleave gather).
|
|
118
|
+
function bytesOf(data: HostBufferSource): Uint8Array {
|
|
119
|
+
if (data instanceof Uint8Array) return data;
|
|
120
|
+
if (ArrayBuffer.isView(data)) return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
121
|
+
return new Uint8Array(data);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// De-interleaved (tight, offset-0) views, keyed by (source buffer aval,
|
|
125
|
+
// offset, stride) so ROs sharing one interleaved buffer + the same attribute
|
|
126
|
+
// share a single gathered allocation.
|
|
127
|
+
const deinterleaveCache = new WeakMap<aval<IBuffer>, Map<string, aval<IBuffer>>>();
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Make a tight, offset-0 BufferView the arena can ingest. Tight/broadcast
|
|
131
|
+
* views pass through unchanged; interleaved or offset views are gathered
|
|
132
|
+
* into a fresh packed host buffer (every `stride` bytes from `offset`,
|
|
133
|
+
* `byteSize` each) wrapped as a new whole-buffer view.
|
|
134
|
+
*/
|
|
135
|
+
function tightenBufferView(bv: BufferView): BufferView {
|
|
136
|
+
if (bv.singleValue !== undefined) return bv; // broadcast → lowered to a uniform
|
|
137
|
+
const byteSize = bv.elementType.byteSize;
|
|
138
|
+
const offset = bv.offset ?? 0;
|
|
139
|
+
const stride = (bv.stride !== undefined && bv.stride > 0) ? bv.stride : byteSize;
|
|
140
|
+
if (offset === 0 && stride === byteSize) return bv; // already tight
|
|
141
|
+
let byKey = deinterleaveCache.get(bv.buffer);
|
|
142
|
+
if (byKey === undefined) { byKey = new Map(); deinterleaveCache.set(bv.buffer, byKey); }
|
|
143
|
+
const key = `${offset}:${stride}:${byteSize}`;
|
|
144
|
+
let tight = byKey.get(key);
|
|
145
|
+
if (tight === undefined) {
|
|
146
|
+
tight = bv.buffer.map((ib: IBuffer): IBuffer => {
|
|
147
|
+
if (ib.kind !== "host") {
|
|
148
|
+
throw new Error("heapAdapter: interleaved attribute buffer flipped to native GPUBuffer; classifier should have caught this");
|
|
149
|
+
}
|
|
150
|
+
const src = bytesOf(ib.data);
|
|
151
|
+
const count = Math.max(0, Math.floor((src.byteLength - offset - byteSize) / stride) + 1);
|
|
152
|
+
const out = new Uint8Array(count * byteSize);
|
|
153
|
+
for (let i = 0; i < count; i++) {
|
|
154
|
+
const s = offset + i * stride;
|
|
155
|
+
out.set(src.subarray(s, s + byteSize), i * byteSize);
|
|
156
|
+
}
|
|
157
|
+
return IBuffer.fromHost(out);
|
|
158
|
+
});
|
|
159
|
+
byKey.set(key, tight);
|
|
160
|
+
}
|
|
161
|
+
return BufferView.ofBuffer(tight, bv.elementType);
|
|
162
|
+
}
|
|
163
|
+
|
|
88
164
|
/**
|
|
89
165
|
* Pull `(format, width, height, mipLevelCount)` out of an ITexture so
|
|
90
166
|
* we can run Tier-S eligibility against it. Both variants expose this:
|
|
@@ -197,7 +273,7 @@ export function renderObjectToHeapSpec(
|
|
|
197
273
|
const inputs: { [name: string]: aval<unknown> | unknown } = {};
|
|
198
274
|
for (const a of schema.attributes) {
|
|
199
275
|
const bv = vAttr.tryGet(a.name);
|
|
200
|
-
if (bv !== undefined) inputs[a.name] = bv;
|
|
276
|
+
if (bv !== undefined) inputs[a.name] = isBufferView(bv) ? tightenBufferView(bv) : bv;
|
|
201
277
|
}
|
|
202
278
|
const pullUniform = (name: string): void => {
|
|
203
279
|
if (Object.prototype.hasOwnProperty.call(inputs, name)) return;
|
|
@@ -222,17 +298,25 @@ export function renderObjectToHeapSpec(
|
|
|
222
298
|
instanceAttributes = {};
|
|
223
299
|
for (const n of names) {
|
|
224
300
|
const bv = iAttr.tryGet(n);
|
|
225
|
-
if (bv !== undefined) instanceAttributes[n] = bv;
|
|
301
|
+
if (bv !== undefined) instanceAttributes[n] = isBufferView(bv) ? tightenBufferView(bv) : bv;
|
|
226
302
|
}
|
|
227
303
|
}
|
|
228
304
|
}
|
|
229
305
|
|
|
306
|
+
// DrawCall: read once (snapshot — like instanceCount/vertexCount).
|
|
307
|
+
// Slice offsets fold in here: firstIndex → record indexStart,
|
|
308
|
+
// indexCount → record slice length, baseVertex → baked into the
|
|
309
|
+
// transcoded index values (so it needs no record field).
|
|
310
|
+
const dc = ro.drawCall.getValue(token);
|
|
311
|
+
const baseVertex = dc.kind === "indexed" ? dc.baseVertex : 0;
|
|
312
|
+
|
|
230
313
|
// 2. Indices: BufferView → aval<Uint32Array>. Map the underlying
|
|
231
|
-
// IBuffer aval; the heap path's IndexPool will key on this aval
|
|
314
|
+
// IBuffer aval; the heap path's IndexPool will key on this aval
|
|
315
|
+
// (and on baseVertex, since it's baked into the values).
|
|
232
316
|
// Non-indexed ROs (no `ro.indices`) carry no index aval — the spec
|
|
233
317
|
// then sets `vertexCount` and the megacall decodes the vertex directly.
|
|
234
318
|
const indices: aval<Uint32Array> | undefined =
|
|
235
|
-
ro.indices !== undefined ? indicesAvalFor(ro.indices.buffer) : undefined;
|
|
319
|
+
ro.indices !== undefined ? indicesAvalFor(ro.indices.buffer, ro.indices.elementType.indexFormat ?? "uint32", baseVertex) : undefined;
|
|
236
320
|
|
|
237
321
|
// 3. Texture/sampler: single-pair v1. The Sg compile layer binds
|
|
238
322
|
// each texture aval under both `name` and `${name}_view` so the
|
|
@@ -355,19 +439,20 @@ export function renderObjectToHeapSpec(
|
|
|
355
439
|
);
|
|
356
440
|
}
|
|
357
441
|
|
|
358
|
-
// 4. DrawCall
|
|
359
|
-
// compatible (
|
|
360
|
-
const dc = ro.drawCall.getValue(token);
|
|
442
|
+
// 4. DrawCall geometry. Classifier validates fields are heap-
|
|
443
|
+
// compatible (instanceCount≥1, firstInstance=0, non-indexed firstVertex=0).
|
|
361
444
|
if (dc.kind === "indexed" && indices === undefined) {
|
|
362
445
|
throw new Error("heapAdapter: indexed drawCall without indices");
|
|
363
446
|
}
|
|
364
447
|
if (dc.kind === "non-indexed" && indices !== undefined) {
|
|
365
448
|
throw new Error("heapAdapter: non-indexed drawCall with an index buffer");
|
|
366
449
|
}
|
|
367
|
-
// Indexed → carry the index aval
|
|
368
|
-
//
|
|
450
|
+
// Indexed → carry the index aval + the firstIndex/indexCount slice (folded
|
|
451
|
+
// into the record's indexStart/indexCount; baseVertex is already baked into
|
|
452
|
+
// the index values). Non-indexed → carry the vertex count; the megacall uses
|
|
453
|
+
// the local vertex index directly (no index lookup).
|
|
369
454
|
const geom = dc.kind === "indexed"
|
|
370
|
-
? { indices: indices
|
|
455
|
+
? { indices: indices!, firstIndex: dc.firstIndex, indexCount: dc.indexCount }
|
|
371
456
|
: { vertexCount: dc.vertexCount };
|
|
372
457
|
|
|
373
458
|
return {
|
|
@@ -185,27 +185,17 @@ export function isHeapEligible(ro: RenderObject): aval<boolean> {
|
|
|
185
185
|
return AVal.constant(false);
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
// 2.
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
// unsupported. Per-instance attributes follow the same rule.
|
|
194
|
-
let strideBlocked = false;
|
|
195
|
-
const checkStride = (v: BufferView): void => {
|
|
196
|
-
const stride = v.stride ?? v.elementType.byteSize;
|
|
197
|
-
const isBroadcast = v.singleValue !== undefined || stride === 0;
|
|
198
|
-
if (!isBroadcast && stride !== v.elementType.byteSize) strideBlocked = true;
|
|
199
|
-
};
|
|
200
|
-
for (const v of attrViews(asAttributeProvider(ro.vertexAttributes))) checkStride(v);
|
|
201
|
-
if (ro.instanceAttributes !== undefined) {
|
|
202
|
-
for (const v of attrViews(asAttributeProvider(ro.instanceAttributes))) checkStride(v);
|
|
203
|
-
}
|
|
204
|
-
if (strideBlocked) return AVal.constant(false);
|
|
188
|
+
// 2. Interleaved / offset attributes are handled by de-interleaving at
|
|
189
|
+
// ingest (heapAdapter.tightenBufferView gathers a tight, offset-0
|
|
190
|
+
// copy keyed by (buffer, offset, stride) so sharing is preserved), so
|
|
191
|
+
// no stride/offset wedge remains. Broadcasts (singleValue / stride 0)
|
|
192
|
+
// still pass through as a degenerate length-1 case.
|
|
205
193
|
|
|
206
|
-
// 3. Index-format wedge — heap stores indices as u32
|
|
207
|
-
|
|
208
|
-
|
|
194
|
+
// 3. Index-format wedge — heap stores indices as u32; u16 is widened
|
|
195
|
+
// to u32 at ingest (heapAdapter.widenU16). Anything else is rejected.
|
|
196
|
+
if (ro.indices !== undefined) {
|
|
197
|
+
const fmt = ro.indices.elementType.indexFormat;
|
|
198
|
+
if (fmt !== "uint32" && fmt !== "uint16") return AVal.constant(false);
|
|
209
199
|
}
|
|
210
200
|
|
|
211
201
|
const buffers = bufferAvals(ro);
|
|
@@ -233,27 +223,19 @@ export function isHeapEligible(ro: RenderObject): aval<boolean> {
|
|
|
233
223
|
if (dc.firstInstance !== 0) return false;
|
|
234
224
|
if (dc.kind === "indexed") {
|
|
235
225
|
if (ro.indices === undefined) return false;
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
// buffer into its draw: the trailing indices reference vertices
|
|
245
|
-
// past this RO's range, painting stray triangles. Until the heap
|
|
246
|
-
// honours the drawCall slice, only ROs that own their whole index
|
|
247
|
-
// buffer are eligible.
|
|
248
|
-
const ib = ro.indices.buffer.getValue(token);
|
|
249
|
-
if (ib.kind === "host") {
|
|
250
|
-
const idxBufCount = ib.sizeBytes >>> 2; // u32 indices
|
|
251
|
-
if (dc.indexCount !== idxBufCount) return false;
|
|
252
|
-
}
|
|
226
|
+
// Offsets and shared sub-buffer slices are all eligible now: the heap
|
|
227
|
+
// still ingests the WHOLE index + vertex buffers (one shared arena
|
|
228
|
+
// allocation per buffer aval — sharing is the heap's whole point), but
|
|
229
|
+
// the record now honours the drawCall slice. firstIndex folds into the
|
|
230
|
+
// record's indexStart, indexCount becomes the slice length (so a single
|
|
231
|
+
// glyph of a multi-glyph run reads only its run of indices), and
|
|
232
|
+
// baseVertex is baked into the transcoded index values at ingest
|
|
233
|
+
// (heapAdapter.indicesAvalFor). No drawCall wedge remains here.
|
|
253
234
|
} else {
|
|
254
235
|
// Non-indexed: the megacall uses the local vertex index directly, so a
|
|
255
|
-
// prefix read is safe (vid = 0..vertexCount-1)
|
|
256
|
-
//
|
|
236
|
+
// prefix read is safe (vid = 0..vertexCount-1). firstVertex must be 0 —
|
|
237
|
+
// a non-indexed vertex offset would need a per-record field (the index
|
|
238
|
+
// bake-in trick doesn't apply with no index buffer), so it's deferred.
|
|
257
239
|
if (ro.indices !== undefined) return false;
|
|
258
240
|
if (dc.firstVertex !== 0) return false;
|
|
259
241
|
}
|
package/src/runtime/heapScene.ts
CHANGED
|
@@ -465,6 +465,17 @@ export interface HeapDrawSpec {
|
|
|
465
465
|
readonly indices?: aval<Uint32Array> | Uint32Array;
|
|
466
466
|
/** Vertex count for a non-indexed draw (when `indices` is omitted). */
|
|
467
467
|
readonly vertexCount?: number;
|
|
468
|
+
/**
|
|
469
|
+
* Optional index slice into a (possibly shared) index buffer. The heap
|
|
470
|
+
* still allocates/uploads the WHOLE `indices` buffer once per aval (so N
|
|
471
|
+
* draws sharing it cost one allocation), but this draw emits only
|
|
472
|
+
* `[firstIndex, firstIndex+indexCount)`. Folds into the record's
|
|
473
|
+
* indexStart (`alloc.firstIndex + firstIndex`) and indexCount. Omit for a
|
|
474
|
+
* whole-buffer draw (defaults: firstIndex 0, indexCount = buffer length).
|
|
475
|
+
* (baseVertex is handled upstream by baking it into the index values.)
|
|
476
|
+
*/
|
|
477
|
+
readonly firstIndex?: number;
|
|
478
|
+
readonly indexCount?: number;
|
|
468
479
|
/**
|
|
469
480
|
* Optional texture set. When present, adds a `texture_2d<f32>` at
|
|
470
481
|
* binding 4 and a sampler at binding 5; the FS must declare them.
|
|
@@ -3025,8 +3036,11 @@ export function buildHeapScene(
|
|
|
3025
3036
|
? indexPool.acquire(device, arena.indices, indicesAval!, bucket.chunkIdx, readPlain(spec.indices!) as Uint32Array)
|
|
3026
3037
|
: undefined;
|
|
3027
3038
|
// Emit count per instance + the firstIndex field written to the record.
|
|
3028
|
-
|
|
3029
|
-
|
|
3039
|
+
// Slice: emit only [firstIndex, firstIndex+indexCount) of the (shared,
|
|
3040
|
+
// whole-buffer) index allocation. firstIndex folds into indexStart;
|
|
3041
|
+
// indexCount defaults to the whole buffer.
|
|
3042
|
+
const emitCount = hasIndices ? (spec.indexCount ?? idxAlloc!.count) : (spec.vertexCount ?? 0);
|
|
3043
|
+
const firstIndexField = hasIndices ? (idxAlloc!.firstIndex + (spec.firstIndex ?? 0)) : HEAP_NONINDEXED;
|
|
3030
3044
|
|
|
3031
3045
|
const localSlot = bucket.drawHeap.alloc();
|
|
3032
3046
|
// Per-RO instancing: read `spec.instanceCount` (defaults to 1).
|