@aardworx/wombat.rendering 0.19.24 → 0.19.25
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/elementType.d.ts +2 -0
- package/dist/core/elementType.d.ts.map +1 -1
- package/dist/core/elementType.js +10 -0
- package/dist/core/elementType.js.map +1 -1
- package/dist/core/provider.d.ts.map +1 -1
- package/dist/core/provider.js +65 -32
- package/dist/core/provider.js.map +1 -1
- package/dist/core/renderObject.d.ts +9 -0
- package/dist/core/renderObject.d.ts.map +1 -1
- package/dist/resources/preparedRenderObject.d.ts.map +1 -1
- package/dist/resources/preparedRenderObject.js +19 -2
- package/dist/resources/preparedRenderObject.js.map +1 -1
- package/dist/runtime/derivedModes/modeKeyCpu.d.ts +2 -12
- package/dist/runtime/derivedModes/modeKeyCpu.d.ts.map +1 -1
- package/dist/runtime/derivedModes/modeKeyCpu.js +30 -13
- package/dist/runtime/derivedModes/modeKeyCpu.js.map +1 -1
- package/dist/runtime/derivedModes/partitionDispatcher.d.ts +11 -0
- package/dist/runtime/derivedModes/partitionDispatcher.d.ts.map +1 -1
- package/dist/runtime/derivedModes/partitionDispatcher.js +31 -0
- package/dist/runtime/derivedModes/partitionDispatcher.js.map +1 -1
- package/dist/runtime/derivedUniforms/records.d.ts +15 -0
- package/dist/runtime/derivedUniforms/records.d.ts.map +1 -1
- package/dist/runtime/derivedUniforms/records.js +81 -14
- package/dist/runtime/derivedUniforms/records.js.map +1 -1
- package/dist/runtime/derivedUniforms/sceneIntegration.d.ts +6 -0
- package/dist/runtime/derivedUniforms/sceneIntegration.d.ts.map +1 -1
- package/dist/runtime/derivedUniforms/sceneIntegration.js +9 -0
- package/dist/runtime/derivedUniforms/sceneIntegration.js.map +1 -1
- package/dist/runtime/flattenTree.d.ts.map +1 -1
- package/dist/runtime/flattenTree.js +46 -2
- package/dist/runtime/flattenTree.js.map +1 -1
- package/dist/runtime/heapAdapter.d.ts +3 -1
- package/dist/runtime/heapAdapter.d.ts.map +1 -1
- package/dist/runtime/heapAdapter.js +42 -4
- package/dist/runtime/heapAdapter.js.map +1 -1
- package/dist/runtime/heapDecoder.d.ts.map +1 -1
- package/dist/runtime/heapDecoder.js +8 -3
- package/dist/runtime/heapDecoder.js.map +1 -1
- package/dist/runtime/heapEffect.d.ts +14 -1
- package/dist/runtime/heapEffect.d.ts.map +1 -1
- package/dist/runtime/heapEffect.js +13 -1
- package/dist/runtime/heapEffect.js.map +1 -1
- package/dist/runtime/heapEffectIR.d.ts.map +1 -1
- package/dist/runtime/heapEffectIR.js +18 -9
- package/dist/runtime/heapEffectIR.js.map +1 -1
- package/dist/runtime/heapEligibility.d.ts.map +1 -1
- package/dist/runtime/heapEligibility.js +64 -12
- package/dist/runtime/heapEligibility.js.map +1 -1
- package/dist/runtime/heapIrBuilders.d.ts +8 -0
- package/dist/runtime/heapIrBuilders.d.ts.map +1 -1
- package/dist/runtime/heapIrBuilders.js +47 -2
- package/dist/runtime/heapIrBuilders.js.map +1 -1
- package/dist/runtime/heapScene/chunkedArena.d.ts +20 -28
- package/dist/runtime/heapScene/chunkedArena.d.ts.map +1 -1
- package/dist/runtime/heapScene/chunkedArena.js +28 -91
- package/dist/runtime/heapScene/chunkedArena.js.map +1 -1
- package/dist/runtime/heapScene/freelist.d.ts +1 -1
- package/dist/runtime/heapScene/freelist.js +3 -3
- package/dist/runtime/heapScene/freelist.js.map +1 -1
- package/dist/runtime/heapScene/pools.d.ts +103 -59
- package/dist/runtime/heapScene/pools.d.ts.map +1 -1
- package/dist/runtime/heapScene/pools.js +335 -194
- package/dist/runtime/heapScene/pools.js.map +1 -1
- package/dist/runtime/heapScene.d.ts +116 -1
- package/dist/runtime/heapScene.d.ts.map +1 -1
- package/dist/runtime/heapScene.js +581 -125
- package/dist/runtime/heapScene.js.map +1 -1
- package/dist/runtime/hybridScene.d.ts.map +1 -1
- package/dist/runtime/hybridScene.js +19 -3
- package/dist/runtime/hybridScene.js.map +1 -1
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/index.js.map +1 -1
- package/package.json +1 -1
- package/src/core/elementType.ts +10 -0
- package/src/core/provider.ts +56 -28
- package/src/core/renderObject.ts +9 -0
- package/src/resources/preparedRenderObject.ts +22 -3
- package/src/runtime/derivedModes/modeKeyCpu.ts +26 -11
- package/src/runtime/derivedModes/partitionDispatcher.ts +30 -0
- package/src/runtime/derivedUniforms/records.ts +70 -15
- package/src/runtime/derivedUniforms/sceneIntegration.ts +10 -0
- package/src/runtime/flattenTree.ts +47 -2
- package/src/runtime/heapAdapter.ts +40 -5
- package/src/runtime/heapDecoder.ts +8 -3
- package/src/runtime/heapEffect.ts +15 -1
- package/src/runtime/heapEffectIR.ts +18 -9
- package/src/runtime/heapEligibility.ts +39 -12
- package/src/runtime/heapIrBuilders.ts +50 -2
- package/src/runtime/heapScene/chunkedArena.ts +29 -94
- package/src/runtime/heapScene/freelist.ts +3 -3
- package/src/runtime/heapScene/pools.ts +340 -218
- package/src/runtime/heapScene.ts +717 -141
- package/src/runtime/hybridScene.ts +20 -3
- package/src/runtime/index.ts +5 -0
|
@@ -19,6 +19,49 @@ import { AListBridges } from "@aardworx/wombat.adaptive";
|
|
|
19
19
|
import type { RenderObject } from "../core/renderObject.js";
|
|
20
20
|
import type { RenderTree } from "../core/renderTree.js";
|
|
21
21
|
|
|
22
|
+
/** True when `tree` contains no reactive nodes — its RenderObjects can
|
|
23
|
+
* be enumerated as a plain iterable (no inner aset / reader needed). */
|
|
24
|
+
function isStaticTree(tree: RenderTree): boolean {
|
|
25
|
+
switch (tree.kind) {
|
|
26
|
+
case "Empty":
|
|
27
|
+
case "Leaf":
|
|
28
|
+
return true;
|
|
29
|
+
case "Ordered":
|
|
30
|
+
case "Unordered":
|
|
31
|
+
return tree.children.every(isStaticTree);
|
|
32
|
+
default:
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Enumerate a static subtree's RenderObjects (pre: `isStaticTree`). */
|
|
38
|
+
function* staticObjects(tree: RenderTree): Iterable<RenderObject> {
|
|
39
|
+
switch (tree.kind) {
|
|
40
|
+
case "Empty":
|
|
41
|
+
return;
|
|
42
|
+
case "Leaf":
|
|
43
|
+
yield tree.object;
|
|
44
|
+
return;
|
|
45
|
+
case "Ordered":
|
|
46
|
+
case "Unordered":
|
|
47
|
+
for (const c of tree.children) yield* staticObjects(c);
|
|
48
|
+
return;
|
|
49
|
+
default:
|
|
50
|
+
throw new Error(`staticObjects: reactive node '${tree.kind}'`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Flatten a reactive container of subtrees. The static children go
|
|
55
|
+
* through `collectSeq` (plain iterables — ONE reader for the whole
|
|
56
|
+
* set, instead of a ConstantAset + reader + subscription PER LEAF,
|
|
57
|
+
* which at heap scale was megabytes of ballast); reactive children
|
|
58
|
+
* keep the full `collect`. */
|
|
59
|
+
function flattenChildSet(children: aset<RenderTree>): aset<RenderObject> {
|
|
60
|
+
const staticPart = children.filter(isStaticTree).collectSeq(staticObjects);
|
|
61
|
+
const reactivePart = children.filter(t => !isStaticTree(t)).collect(flattenRenderTree);
|
|
62
|
+
return ASet.union(staticPart, reactivePart);
|
|
63
|
+
}
|
|
64
|
+
|
|
22
65
|
export function flattenRenderTree(tree: RenderTree): aset<RenderObject> {
|
|
23
66
|
switch (tree.kind) {
|
|
24
67
|
case "Empty":
|
|
@@ -27,6 +70,8 @@ export function flattenRenderTree(tree: RenderTree): aset<RenderObject> {
|
|
|
27
70
|
return ASet.single(tree.object);
|
|
28
71
|
case "Ordered":
|
|
29
72
|
case "Unordered": {
|
|
73
|
+
// Fully-static subtree → one constant aset over a plain array.
|
|
74
|
+
if (isStaticTree(tree)) return ASet.ofArray([...staticObjects(tree)]);
|
|
30
75
|
// N-ary union via `unionMany(aset<aset<T>>)`. The naïve
|
|
31
76
|
// `children.reduce(ASet.union)` builds a 2N-deep nested-union
|
|
32
77
|
// tree; the reader's traversal recurses through it and blows
|
|
@@ -39,8 +84,8 @@ export function flattenRenderTree(tree: RenderTree): aset<RenderObject> {
|
|
|
39
84
|
case "Adaptive":
|
|
40
85
|
return ASet.bind(flattenRenderTree, tree.tree);
|
|
41
86
|
case "OrderedFromList":
|
|
42
|
-
return
|
|
87
|
+
return flattenChildSet(AListBridges.toASet(tree.children));
|
|
43
88
|
case "UnorderedFromSet":
|
|
44
|
-
return
|
|
89
|
+
return flattenChildSet(tree.children);
|
|
45
90
|
}
|
|
46
91
|
}
|
|
@@ -113,6 +113,8 @@ import type { HeapDrawSpec, HeapTextureSet } from "./heapScene.js";
|
|
|
113
113
|
import { isBufferView } from "./heapScene/pools.js";
|
|
114
114
|
import { compileHeapEffect } from "./heapEffect.js";
|
|
115
115
|
import { AtlasPool } from "./textureAtlas/atlasPool.js";
|
|
116
|
+
import { STANDARD_DERIVED_RULES, STANDARD_TRAFO_LEAVES } from "./derivedUniforms/recipes.js";
|
|
117
|
+
import { inputsOf } from "./derivedUniforms/flatten.js";
|
|
116
118
|
|
|
117
119
|
/**
|
|
118
120
|
* View `HostBufferSource` as `Uint32Array`. Index buffers are u32 in
|
|
@@ -326,6 +328,7 @@ export function renderObjectToHeapSpec(
|
|
|
326
328
|
ro: RenderObject,
|
|
327
329
|
token: AdaptiveToken,
|
|
328
330
|
pool?: AtlasPool,
|
|
331
|
+
opts?: { readonly enableDerivedUniforms?: boolean },
|
|
329
332
|
): HeapDrawSpec {
|
|
330
333
|
// 1. Inputs map: vertex attributes (BufferView) + uniforms, pulled
|
|
331
334
|
// SHADER-DRIVEN from the providers. We compile `ro.effect` (cached
|
|
@@ -356,12 +359,38 @@ export function renderObjectToHeapSpec(
|
|
|
356
359
|
const av = uProv.tryGet(name);
|
|
357
360
|
if (av !== undefined) inputs[name] = av;
|
|
358
361
|
};
|
|
359
|
-
|
|
362
|
+
// §7-covered derived trafos (ModelViewProjTrafo, NormalMatrix, …) are
|
|
363
|
+
// GPU-computed from the M/V/P constituents — addDraw never reads their
|
|
364
|
+
// `spec.inputs` entry when the rule applies. Deferring the pull keeps
|
|
365
|
+
// the provider from materialising a per-RO derived-aval chain (MapVal +
|
|
366
|
+
// nested closure contexts + a never-forced lazy constant) that would sit
|
|
367
|
+
// in `inputs` as pure ballast. Pulled after all as a fallback only when
|
|
368
|
+
// the recipe's base leaves turn out not to be bound (mirrors
|
|
369
|
+
// heapScene's `ruleForUniform` applicability test).
|
|
370
|
+
const deriveOnGpu = opts?.enableDerivedUniforms !== false;
|
|
371
|
+
const deferredDerived: string[] = [];
|
|
372
|
+
for (const u of schema.uniforms) {
|
|
373
|
+
if (deriveOnGpu && STANDARD_DERIVED_RULES.has(u.name)) {
|
|
374
|
+
deferredDerived.push(u.name);
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
pullUniform(u.name);
|
|
378
|
+
}
|
|
360
379
|
// §7 derived-uniforms constituents — cheap (these are the raw
|
|
361
380
|
// `state.model/view/proj` avals, no `compose`/`inverse`) and the
|
|
362
381
|
// compute pre-pass needs them even when the effect itself doesn't
|
|
363
382
|
// declare them. No-ops if the provider doesn't carry them.
|
|
364
383
|
for (const n of ["ModelTrafo", "ViewTrafo", "ProjTrafo"]) pullUniform(n);
|
|
384
|
+
for (const name of deferredDerived) {
|
|
385
|
+
const rule = STANDARD_DERIVED_RULES.get(name)!;
|
|
386
|
+
let leavesBound = true;
|
|
387
|
+
for (const inp of inputsOf(rule.ir)) {
|
|
388
|
+
if (inp.name === "Model" && ro.modelChain !== undefined) continue;
|
|
389
|
+
const specName = STANDARD_TRAFO_LEAVES.get(inp.name) ?? inp.name;
|
|
390
|
+
if (inputs[specName] === undefined) { leavesBound = false; break; }
|
|
391
|
+
}
|
|
392
|
+
if (!leavesBound) pullUniform(name);
|
|
393
|
+
}
|
|
365
394
|
|
|
366
395
|
// 1b. Instance attributes — provider is map-backed (user-supplied via
|
|
367
396
|
// `Sg.instanced({attributes})`), so enumerate its names. Threaded
|
|
@@ -399,9 +428,11 @@ export function renderObjectToHeapSpec(
|
|
|
399
428
|
// WGSL schema's binding shape doesn't matter at scene time — that
|
|
400
429
|
// leaves us with two HashMap entries pointing at the same aval.
|
|
401
430
|
// Dedupe by identity before applying the single-pair rule.
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
431
|
+
// `let` + nulled after use: the spec's long-lived closures share
|
|
432
|
+
// this scope context — a leftover Set here is per-RO ballast.
|
|
433
|
+
let distinctTexAvals: Set<aval<ITexture>> | undefined = new Set<aval<ITexture>>();
|
|
434
|
+
ro.textures.iter((_n, av) => { distinctTexAvals!.add(av as aval<ITexture>); });
|
|
435
|
+
let distinctSamplerAvals: Set<aval<ISampler>> | undefined = new Set<aval<ISampler>>();
|
|
405
436
|
// Shader-defined sampler state (from a `sampler2d { filter …; addressU … }`
|
|
406
437
|
// builder, carried through the IR) overrides the scene's default sampler.
|
|
407
438
|
const stateBinding = schema.samplers.find(b => b.state !== undefined);
|
|
@@ -418,7 +449,7 @@ export function renderObjectToHeapSpec(
|
|
|
418
449
|
AVal.constant(ISampler.fromDescriptor(desc)) as aval<ISampler>,
|
|
419
450
|
);
|
|
420
451
|
} else {
|
|
421
|
-
ro.samplers.iter((_n, av) => { distinctSamplerAvals
|
|
452
|
+
ro.samplers.iter((_n, av) => { distinctSamplerAvals!.add(av as aval<ISampler>); });
|
|
422
453
|
}
|
|
423
454
|
let textures: HeapTextureSet | undefined;
|
|
424
455
|
if (distinctTexAvals.size === 1 && distinctSamplerAvals.size === 1) {
|
|
@@ -531,6 +562,9 @@ export function renderObjectToHeapSpec(
|
|
|
531
562
|
`single-pair only in v1 (classifier should have caught this)`,
|
|
532
563
|
);
|
|
533
564
|
}
|
|
565
|
+
// Drop the dedup scratch — see the `let` note above.
|
|
566
|
+
distinctTexAvals = undefined;
|
|
567
|
+
distinctSamplerAvals = undefined;
|
|
534
568
|
|
|
535
569
|
// 4. DrawCall geometry. Classifier validates fields are heap-
|
|
536
570
|
// compatible (instanceCount≥1, firstInstance=0, non-indexed firstVertex=0).
|
|
@@ -550,6 +584,7 @@ export function renderObjectToHeapSpec(
|
|
|
550
584
|
|
|
551
585
|
return {
|
|
552
586
|
effect: ro.effect,
|
|
587
|
+
...(ro.pickId !== undefined ? { pickId: ro.pickId } : {}),
|
|
553
588
|
pipelineState: ro.pipelineState,
|
|
554
589
|
inputs,
|
|
555
590
|
...(instanceAttributes !== undefined ? { instanceAttributes } : {}),
|
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
Tu32, Tf32,
|
|
47
47
|
} from "@aardworx/wombat.shader/ir";
|
|
48
48
|
import type { BucketLayout, DrawHeaderField } from "./heapEffect.js";
|
|
49
|
+
import { isInlineHeaderField } from "./heapEffect.js";
|
|
49
50
|
import {
|
|
50
51
|
Tvec2, Tvec3, Tvec4, Tmat4, TarrU32,
|
|
51
52
|
constU32, add, mul, div, item, newVec, readScope,
|
|
@@ -307,9 +308,13 @@ function addUniformOutput(
|
|
|
307
308
|
}
|
|
308
309
|
const irType = wgslTypeToIrType(wgslType);
|
|
309
310
|
const refExpr = loadHeaderRef(drawIdxExpr, f.byteOffset, layout.strideU32);
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
311
|
+
// Inline fields (PickId, __layoutId family): the header word IS the
|
|
312
|
+
// u32 value — no arena indirection, no pool entry per draw.
|
|
313
|
+
const value = isInlineHeaderField(f)
|
|
314
|
+
? refExpr
|
|
315
|
+
: layout.perInstanceUniforms.has(f.name)
|
|
316
|
+
? loadInstanceByRef(refExpr, instIdExpr, wgslType)
|
|
317
|
+
: loadUniformByRef(refExpr, wgslType);
|
|
313
318
|
|
|
314
319
|
outputs.push({
|
|
315
320
|
name: f.name,
|
|
@@ -104,7 +104,21 @@ export interface FragmentOutputLayout {
|
|
|
104
104
|
// and on quota; every failure simply degrades to "recompute".
|
|
105
105
|
|
|
106
106
|
/** Cache-generation stamp for the persistent (localStorage) tier. */
|
|
107
|
-
export const HEAP_PERSIST_VERSION = "
|
|
107
|
+
export const HEAP_PERSIST_VERSION = "h4"; // h4: inline PickId drawHeader read (was h3: packed decode arms)
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Uniforms whose drawHeader word IS the u32 value (no arena
|
|
111
|
+
* indirection, no pool entry, no per-RO aval) — the `__layoutId`
|
|
112
|
+
* mechanism generalised. Values come from first-class RenderObject
|
|
113
|
+
* fields (`ro.pickId`), not the uniform provider. Fields must be
|
|
114
|
+
* u32-typed.
|
|
115
|
+
*/
|
|
116
|
+
export const INLINE_HEADER_UNIFORMS: ReadonlySet<string> = new Set(["PickId"]);
|
|
117
|
+
|
|
118
|
+
/** True for fields read inline from the drawHeader word. */
|
|
119
|
+
export function isInlineHeaderField(f: { kind: string; name: string }): boolean {
|
|
120
|
+
return f.kind === "uniform-ref" && (f.name === "__layoutId" || INLINE_HEADER_UNIFORMS.has(f.name));
|
|
121
|
+
}
|
|
108
122
|
export const HEAP_PERSIST_PREFIX = "wbt.heapfx.";
|
|
109
123
|
|
|
110
124
|
export function persistKey(version: string, kind: string, contentKey: string): string {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
// consumes is identical to the one heapScene.ts already builds.
|
|
19
19
|
|
|
20
20
|
import { compileModule, stage as makeStage, effect as makeEffect } from "@aardworx/wombat.shader";
|
|
21
|
+
import { isInlineHeaderField } from "./heapEffect.js";
|
|
21
22
|
import type { Effect, CompileOptions } from "@aardworx/wombat.shader";
|
|
22
23
|
import { substituteInputsInStage, readInputs, mapExpr, mapStmt, liftReturns, uniformsToInputs } from "@aardworx/wombat.shader/passes";
|
|
23
24
|
import { synthesizeHeapDecoderModule } from "./heapDecoder.js";
|
|
@@ -278,9 +279,12 @@ function buildVertexUniformMap(layout: BucketLayout): Map<string, Expr> {
|
|
|
278
279
|
for (const f of layout.drawHeaderFields) {
|
|
279
280
|
if (f.kind !== "uniform-ref") continue;
|
|
280
281
|
const refExpr = loadHeaderRef(drawIdx, f.byteOffset, stride);
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
282
|
+
// Inline fields: the header word IS the u32 value.
|
|
283
|
+
const value = isInlineHeaderField(f)
|
|
284
|
+
? refExpr
|
|
285
|
+
: layout.perInstanceUniforms.has(f.name)
|
|
286
|
+
? loadInstanceByRef(refExpr, iidx, f.uniformWgslType ?? "")
|
|
287
|
+
: loadUniformByRef(refExpr, f.uniformWgslType ?? "");
|
|
284
288
|
out.set(f.name, value);
|
|
285
289
|
}
|
|
286
290
|
return out;
|
|
@@ -420,9 +424,11 @@ function rewriteFsUniformsDirect(m: Module, layout: BucketLayout): Module {
|
|
|
420
424
|
const f = fieldByName.get(name);
|
|
421
425
|
if (f === undefined || f.kind !== "uniform-ref") continue;
|
|
422
426
|
const refExpr = loadHeaderRef(drawIdxExpr, f.byteOffset, layout.strideU32);
|
|
423
|
-
const value =
|
|
424
|
-
?
|
|
425
|
-
:
|
|
427
|
+
const value = isInlineHeaderField(f)
|
|
428
|
+
? refExpr
|
|
429
|
+
: layout.perInstanceUniforms.has(name)
|
|
430
|
+
? loadInstanceByRef(refExpr, instIdExpr, f.uniformWgslType ?? "")
|
|
431
|
+
: loadUniformByRef(refExpr, f.uniformWgslType ?? "");
|
|
426
432
|
fsSubst.set(name, value);
|
|
427
433
|
}
|
|
428
434
|
return substituteInputsInStage(m, "fragment", "Uniform", n => fsSubst.get(n));
|
|
@@ -541,9 +547,12 @@ function rewriteFsUniforms(m: Module, layout: BucketLayout): Module {
|
|
|
541
547
|
value: { kind: "Expr", value: refExprWriter },
|
|
542
548
|
});
|
|
543
549
|
const refReadFs = readScope("Input", refParamName, Tu32);
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
550
|
+
// Inline fields: the threaded varying already carries the value.
|
|
551
|
+
const value = isInlineHeaderField(f)
|
|
552
|
+
? refReadFs
|
|
553
|
+
: layout.perInstanceUniforms.has(name)
|
|
554
|
+
? loadInstanceByRef(refReadFs, readScope("Input", "_iidx", Tu32), f.uniformWgslType ?? "")
|
|
555
|
+
: loadUniformByRef(refReadFs, f.uniformWgslType ?? "");
|
|
547
556
|
fsSubst.set(name, value);
|
|
548
557
|
}
|
|
549
558
|
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
// grows feature support each becomes a per-buffer eligibility
|
|
35
35
|
// question instead of a blanket "no".
|
|
36
36
|
|
|
37
|
-
import { AVal, type aval } from "@aardworx/wombat.adaptive";
|
|
37
|
+
import { AVal, AdaptiveToken, type aval } from "@aardworx/wombat.adaptive";
|
|
38
38
|
import type { IBuffer } from "../core/buffer.js";
|
|
39
39
|
import type { BufferView } from "../core/bufferView.js";
|
|
40
40
|
import type { ITexture } from "../core/texture.js";
|
|
@@ -174,14 +174,14 @@ export function isHeapEligible(ro: RenderObject): aval<boolean> {
|
|
|
174
174
|
// per-RO path, which at scale (thousands of textured ROs ⇒ thousands
|
|
175
175
|
// of draw calls + bind-group switches) is far slower than the heap
|
|
176
176
|
// megacall sampling one shared atlas page.
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
177
|
+
// NOTE identity-dedup WITHOUT a Set: the long-lived AVal.custom
|
|
178
|
+
// closure below shares this function's V8 scope context, so any
|
|
179
|
+
// temporary Set here would be retained per-RO for the scene's
|
|
180
|
+
// lifetime (measured: 1 Set/RO of pure ballast at heap scale).
|
|
181
|
+
if (countDistinct(ro.textures) > 1) {
|
|
180
182
|
return AVal.constant(false);
|
|
181
183
|
}
|
|
182
|
-
|
|
183
|
-
ro.samplers.iter((_n, av) => { distinctSamplerAvals.add(av); });
|
|
184
|
-
if (distinctSamplerAvals.size > 1) {
|
|
184
|
+
if (countDistinct(ro.samplers) > 1) {
|
|
185
185
|
return AVal.constant(false);
|
|
186
186
|
}
|
|
187
187
|
|
|
@@ -202,11 +202,16 @@ export function isHeapEligible(ro: RenderObject): aval<boolean> {
|
|
|
202
202
|
const textures = textureAvals(ro);
|
|
203
203
|
const samplers = samplerAvals(ro);
|
|
204
204
|
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
|
|
205
|
+
// Constant collapse: when every participating aval is constant the
|
|
206
|
+
// answer can never change — evaluate once and return a plain
|
|
207
|
+
// constant. This keeps the hybrid partition from holding a custom
|
|
208
|
+
// aval + subscriptions per static RO (the overwhelmingly common
|
|
209
|
+
// case at heap scale).
|
|
210
|
+
let allConstant = ro.drawCall.isConstant;
|
|
211
|
+
if (allConstant) for (const av of buffers) { if (!av.isConstant) { allConstant = false; break; } }
|
|
212
|
+
if (allConstant) for (const av of textures) { if (!av.isConstant) { allConstant = false; break; } }
|
|
213
|
+
if (allConstant) for (const av of samplers) { if (!av.isConstant) { allConstant = false; break; } }
|
|
214
|
+
const evaluate = (token: AdaptiveToken): boolean => {
|
|
210
215
|
let payloadBytes = 0;
|
|
211
216
|
for (const av of buffers) {
|
|
212
217
|
const b = av.getValue(token);
|
|
@@ -240,5 +245,27 @@ export function isHeapEligible(ro: RenderObject): aval<boolean> {
|
|
|
240
245
|
if (dc.firstVertex !== 0) return false;
|
|
241
246
|
}
|
|
242
247
|
return true;
|
|
248
|
+
};
|
|
249
|
+
if (allConstant) return AVal.constant(evaluate(AdaptiveToken.top));
|
|
250
|
+
// Reactive AND-fold over all participating avals. We also subscribe
|
|
251
|
+
// to drawCall (instanceCount/baseVertex/firstIndex/firstInstance
|
|
252
|
+
// can flip; if they violate heap constraints the RO routes to the
|
|
253
|
+
// legacy path that frame).
|
|
254
|
+
return AVal.custom(evaluate);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
/** Count distinct avals in a name→aval map by identity, allocation-free
|
|
259
|
+
* for the 0/1/2-distinct cases that matter (we only compare > 1). */
|
|
260
|
+
function countDistinct(m: { iter(f: (n: string, av: aval<unknown>) => void): void }): number {
|
|
261
|
+
let first: aval<unknown> | undefined;
|
|
262
|
+
let second: aval<unknown> | undefined;
|
|
263
|
+
let n = 0;
|
|
264
|
+
m.iter((_n, av) => {
|
|
265
|
+
if (av === first || av === second) return;
|
|
266
|
+
if (first === undefined) { first = av; n = 1; }
|
|
267
|
+
else if (second === undefined) { second = av; n = 2; }
|
|
268
|
+
else n++;
|
|
243
269
|
});
|
|
270
|
+
return n;
|
|
244
271
|
}
|
|
@@ -62,6 +62,23 @@ export const shr = (lhs: Expr, rhs: Expr, type: Type): Expr => ({
|
|
|
62
62
|
export const select = (cond: Expr, ifTrue: Expr, ifFalse: Expr, type: Type): Expr => ({
|
|
63
63
|
kind: "Conditional", cond, ifTrue, ifFalse, type,
|
|
64
64
|
});
|
|
65
|
+
export const bitAnd = (lhs: Expr, rhs: Expr, type: Type): Expr => ({
|
|
66
|
+
kind: "BitAnd", lhs, rhs, type,
|
|
67
|
+
});
|
|
68
|
+
export const ltF32 = (lhs: Expr, rhs: Expr): Expr => ({ kind: "Lt", lhs, rhs, type: Tbool });
|
|
69
|
+
export const geF32 = (lhs: Expr, rhs: Expr): Expr => ({ kind: "Ge", lhs, rhs, type: Tbool });
|
|
70
|
+
/** u32 → f32 numeric conversion. */
|
|
71
|
+
export const toF32 = (value: Expr): Expr => ({ kind: "Convert", value, type: Tf32 } as Expr);
|
|
72
|
+
/** Pure WGSL/GLSL intrinsic call (same emit name unless glsl differs). */
|
|
73
|
+
const callIntrinsic = (wgsl: string, glsl: string, ret: Type, args: Expr[]): Expr => ({
|
|
74
|
+
kind: "CallIntrinsic",
|
|
75
|
+
op: { name: wgsl, returnTypeOf: () => ret, pure: true, emit: { wgsl, glsl } },
|
|
76
|
+
args, type: ret,
|
|
77
|
+
} as Expr);
|
|
78
|
+
export const absF32 = (e: Expr): Expr => callIntrinsic("abs", "abs", Tf32, [e]);
|
|
79
|
+
export const normalize3 = (e: Expr): Expr => callIntrinsic("normalize", "normalize", Tvec3, [e]);
|
|
80
|
+
export const unpack4x8unorm = (e: Expr): Expr =>
|
|
81
|
+
callIntrinsic("unpack4x8unorm", "unpackUnorm4x8", Tvec4, [e]);
|
|
65
82
|
export const newVec = (components: Expr[], type: Type): Expr => ({
|
|
66
83
|
kind: "NewVector", components, type,
|
|
67
84
|
});
|
|
@@ -174,14 +191,40 @@ export function loadAttributeByRef(refIdent: Expr, idx: Expr, wgslType: string):
|
|
|
174
191
|
const length = item(heapU32, add(div(refIdent, constU32(4), Tu32), constU32(1), Tu32), Tu32);
|
|
175
192
|
return mul(mod(idx, length, Tu32), constU32(elemSize), Tu32);
|
|
176
193
|
};
|
|
194
|
+
/** Header typeId (word 0 — same 16-byte cache line as the length the
|
|
195
|
+
* broadcast modulo already loads, so the packed-encoding branch costs
|
|
196
|
+
* no extra memory round-trip and is warp-coherent per field). */
|
|
197
|
+
const typeId = item(heapU32, div(refIdent, constU32(4), Tu32), Tu32);
|
|
198
|
+
/** One u32 element at the cyclic index (packed encodings: 4 B/elt). */
|
|
199
|
+
const packedWord = (): Expr => add(dataF32Base, cyclic(1), Tu32);
|
|
177
200
|
switch (wgslType) {
|
|
178
201
|
case "vec3<f32>": {
|
|
179
202
|
const base = add(dataF32Base, cyclic(3), Tu32);
|
|
180
|
-
|
|
203
|
+
const tight = newVec([
|
|
181
204
|
item(heapF32, base, Tf32),
|
|
182
205
|
item(heapF32, add(base, constU32(1), Tu32), Tf32),
|
|
183
206
|
item(heapF32, add(base, constU32(2), Tu32), Tf32),
|
|
184
207
|
], Tvec3);
|
|
208
|
+
// ENC_OCT32 (typeId 2): one u32/element — x = low unorm16, y = high
|
|
209
|
+
// unorm16, both → [-1,1]; octahedral fold when z<0 (matches
|
|
210
|
+
// build_vienna.py's oct_pack and the CPU decoder in renderbench).
|
|
211
|
+
const u = item(heapU32, packedWord(), Tu32);
|
|
212
|
+
const toSnorm = (word: Expr): Expr =>
|
|
213
|
+
add(mul(toF32(word), constF32(2 / 65535), Tf32), constF32(-1.0), Tf32);
|
|
214
|
+
const fx = toSnorm(bitAnd(u, constU32(0xffff), Tu32));
|
|
215
|
+
const fy = toSnorm(shr(u, constU32(16), Tu32));
|
|
216
|
+
const z = sub(sub(constF32(1.0), absF32(fx), Tf32), absF32(fy), Tf32);
|
|
217
|
+
const neg = ltF32(z, constF32(0.0));
|
|
218
|
+
const signOf = (v: Expr): Expr =>
|
|
219
|
+
select(geF32(v, constF32(0.0)), constF32(1.0), constF32(-1.0), Tf32);
|
|
220
|
+
const fold = (a: Expr, b: Expr): Expr => // (1-|b|) * sign(a)
|
|
221
|
+
mul(sub(constF32(1.0), absF32(b), Tf32), signOf(a), Tf32);
|
|
222
|
+
const oct = normalize3(newVec([
|
|
223
|
+
select(neg, fold(fx, fy), fx, Tf32),
|
|
224
|
+
select(neg, fold(fy, fx), fy, Tf32),
|
|
225
|
+
z,
|
|
226
|
+
], Tvec3));
|
|
227
|
+
return select(eqU32(typeId, constU32(2 /* ENC_OCT32 */)), oct, tight, Tvec3);
|
|
185
228
|
}
|
|
186
229
|
case "vec4<f32>": {
|
|
187
230
|
const strideBytes = item(heapU32, add(div(refIdent, constU32(4), Tu32), constU32(2), Tu32), Tu32);
|
|
@@ -190,12 +233,17 @@ export function loadAttributeByRef(refIdent: Expr, idx: Expr, wgslType: string):
|
|
|
190
233
|
const off = mul(cycled, strideF, Tu32);
|
|
191
234
|
const base = add(dataF32Base, off, Tu32);
|
|
192
235
|
const w = select(eqU32(strideF, constU32(4)), item(heapF32, add(base, constU32(3), Tu32), Tf32), constF32(1.0), Tf32);
|
|
193
|
-
|
|
236
|
+
const tight = newVec([
|
|
194
237
|
item(heapF32, base, Tf32),
|
|
195
238
|
item(heapF32, add(base, constU32(1), Tu32), Tf32),
|
|
196
239
|
item(heapF32, add(base, constU32(2), Tu32), Tf32),
|
|
197
240
|
w,
|
|
198
241
|
], Tvec4);
|
|
242
|
+
// ENC_C4B (typeId 3): one u32/element, RGBA8 unorm — a single
|
|
243
|
+
// hardware unpack. (Vienna colours ship as C4b; storing them raw is
|
|
244
|
+
// 4 B/vertex instead of the 12 B tight-RGB expansion.)
|
|
245
|
+
const c4b = unpack4x8unorm(item(heapU32, packedWord(), Tu32));
|
|
246
|
+
return select(eqU32(typeId, constU32(3 /* ENC_C4B */)), c4b, tight, Tvec4);
|
|
199
247
|
}
|
|
200
248
|
case "vec2<f32>": {
|
|
201
249
|
const base = add(dataF32Base, cyclic(2), Tu32);
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import type { IDisposable } from "@aardworx/wombat.adaptive";
|
|
16
16
|
import { GrowBuffer, DEFAULT_MAX_BUFFER_BYTES } from "./growBuffer.js";
|
|
17
|
-
import { AttributeArena
|
|
17
|
+
import { AttributeArena } from "./pools.js";
|
|
18
18
|
|
|
19
19
|
/** Address inside a chunked arena. */
|
|
20
20
|
export interface ChunkedRef {
|
|
@@ -46,6 +46,15 @@ export class ChunkedAttributeArena {
|
|
|
46
46
|
get chunks(): ReadonlyArray<AttributeArena> { return this._chunks; }
|
|
47
47
|
chunk(i: number): AttributeArena { return this._chunks[i]!; }
|
|
48
48
|
|
|
49
|
+
/** Bump headroom (bytes) of chunk `i` — free space before its cap, used by
|
|
50
|
+
* group placement. Out-of-range → 0. */
|
|
51
|
+
bumpHeadroom(i: number): number {
|
|
52
|
+
return i >= 0 && i < this._chunks.length ? this._chunks[i]!.bumpHeadroom : 0;
|
|
53
|
+
}
|
|
54
|
+
/** Open a fresh empty chunk (page) and return its index. Used by group
|
|
55
|
+
* placement when no existing chunk can hold a draw's whole group. */
|
|
56
|
+
openPage(): number { return this.openChunk(); }
|
|
57
|
+
|
|
49
58
|
/**
|
|
50
59
|
* Allocate `dataBytes` of attribute storage. Tries the `hint`
|
|
51
60
|
* chunk first when given (caller passes the RO's chunk to keep all
|
|
@@ -81,12 +90,12 @@ export class ChunkedAttributeArena {
|
|
|
81
90
|
c.release(off, dataBytes);
|
|
82
91
|
}
|
|
83
92
|
|
|
84
|
-
/**
|
|
93
|
+
/** No-op (mirror-less arena — writes upload immediately). Kept for API compat. */
|
|
85
94
|
flush(device: GPUDevice): void {
|
|
86
95
|
for (const c of this._chunks) c.flush(device);
|
|
87
96
|
}
|
|
88
97
|
|
|
89
|
-
/** Write into a specific chunk
|
|
98
|
+
/** Write into a specific chunk (immediate GPU upload). */
|
|
90
99
|
write(chunkIdx: number, dst: number, data: Uint8Array): void {
|
|
91
100
|
this._chunks[chunkIdx]!.write(dst, data);
|
|
92
101
|
}
|
|
@@ -124,100 +133,25 @@ export class ChunkedAttributeArena {
|
|
|
124
133
|
return s;
|
|
125
134
|
}
|
|
126
135
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
private openChunk(): number {
|
|
133
|
-
const idx = this._chunks.length;
|
|
134
|
-
const buf = new GrowBuffer(
|
|
135
|
-
this.device, `${this.label}/c${idx}`, this.usage,
|
|
136
|
-
this.initialChunkBytes, this.maxChunkBytes,
|
|
137
|
-
);
|
|
138
|
-
this._chunks.push(new AttributeArena(buf));
|
|
139
|
-
for (const cb of this.onChunkAddedCbs) cb(idx);
|
|
140
|
-
return idx;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/** Multi-chunk wrapper around `IndexAllocator`. Element-bump units
|
|
145
|
-
* are u32. Same chunk-open / hint / fallback semantics as
|
|
146
|
-
* `ChunkedAttributeArena`. */
|
|
147
|
-
export class ChunkedIndexAllocator {
|
|
148
|
-
private readonly _chunks: IndexAllocator[] = [];
|
|
149
|
-
private readonly onChunkAddedCbs = new Set<(chunkIdx: number) => void>();
|
|
150
|
-
|
|
151
|
-
constructor(
|
|
152
|
-
private readonly device: GPUDevice,
|
|
153
|
-
private readonly label: string,
|
|
154
|
-
private readonly usage: GPUBufferUsageFlags,
|
|
155
|
-
private readonly initialChunkBytes: number,
|
|
156
|
-
private readonly maxChunkBytes: number = DEFAULT_MAX_BUFFER_BYTES,
|
|
157
|
-
) {
|
|
158
|
-
this.openChunk();
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
get chunkCount(): number { return this._chunks.length; }
|
|
162
|
-
get chunks(): ReadonlyArray<IndexAllocator> { return this._chunks; }
|
|
163
|
-
chunk(i: number): IndexAllocator { return this._chunks[i]!; }
|
|
164
|
-
|
|
165
|
-
alloc(elements: number, hint?: number): ChunkedRef {
|
|
166
|
-
if (hint !== undefined && hint >= 0 && hint < this._chunks.length) {
|
|
167
|
-
const off = this._chunks[hint]!.tryAlloc(elements);
|
|
168
|
-
if (off !== undefined) return { chunkIdx: hint, off };
|
|
169
|
-
}
|
|
170
|
-
for (let i = this._chunks.length - 1; i >= 0; i--) {
|
|
171
|
-
if (i === hint) continue;
|
|
172
|
-
const off = this._chunks[i]!.tryAlloc(elements);
|
|
173
|
-
if (off !== undefined) return { chunkIdx: i, off };
|
|
174
|
-
}
|
|
175
|
-
const newIdx = this.openChunk();
|
|
176
|
-
const off = this._chunks[newIdx]!.tryAlloc(elements);
|
|
177
|
-
if (off === undefined) {
|
|
178
|
-
throw new Error(
|
|
179
|
-
`ChunkedIndexAllocator '${this.label}': allocation of ${elements} elements exceeds maxChunkBytes/4 ${this.maxChunkBytes / 4}`,
|
|
180
|
-
);
|
|
181
|
-
}
|
|
182
|
-
return { chunkIdx: newIdx, off };
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
release(chunkIdx: number, off: number, elements: number): void {
|
|
186
|
-
const c = this._chunks[chunkIdx];
|
|
187
|
-
if (c === undefined) return;
|
|
188
|
-
c.release(off, elements);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
flush(device: GPUDevice): void {
|
|
192
|
-
for (const c of this._chunks) c.flush(device);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
write(chunkIdx: number, dst: number, data: Uint8Array): void {
|
|
196
|
-
this._chunks[chunkIdx]!.write(dst, data);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
onChunkAdded(cb: (chunkIdx: number) => void): IDisposable {
|
|
200
|
-
this.onChunkAddedCbs.add(cb);
|
|
201
|
-
return { dispose: () => { this.onChunkAddedCbs.delete(cb); } };
|
|
136
|
+
/** Total fragmentation waste across all chunks (diagnostic / trigger). */
|
|
137
|
+
totalWasteBytes(): number {
|
|
138
|
+
let s = 0;
|
|
139
|
+
for (const c of this._chunks) s += c.wasteBytes;
|
|
140
|
+
return s;
|
|
202
141
|
}
|
|
203
142
|
|
|
204
|
-
|
|
205
|
-
|
|
143
|
+
/** Waste-triggered compaction across every chunk. Returns one entry per
|
|
144
|
+
* chunk that actually relocated data — `remap` maps OLD→NEW byte offset
|
|
145
|
+
* within that chunk. Empty array when nothing was worth compacting. The
|
|
146
|
+
* caller must have flushed pending arena writes first (the GPU copy reads
|
|
147
|
+
* the live buffers). */
|
|
148
|
+
compact(device: GPUDevice, wasteFloorBytes: number, force = false): Array<{ chunkIdx: number; remap: Map<number, number> }> {
|
|
149
|
+
const out: Array<{ chunkIdx: number; remap: Map<number, number> }> = [];
|
|
206
150
|
for (let i = 0; i < this._chunks.length; i++) {
|
|
207
|
-
const
|
|
208
|
-
|
|
151
|
+
const remap = this._chunks[i]!.compact(device, wasteFloorBytes, force);
|
|
152
|
+
if (remap.size > 0) out.push({ chunkIdx: i, remap });
|
|
209
153
|
}
|
|
210
|
-
|
|
211
|
-
disposables.push(this._chunks[idx]!.onResize(() => cb(idx)));
|
|
212
|
-
cb(idx);
|
|
213
|
-
}));
|
|
214
|
-
return { dispose: () => { for (const d of disposables) d.dispose(); } };
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
totalUsedElements(): number {
|
|
218
|
-
let s = 0;
|
|
219
|
-
for (const c of this._chunks) s += c.usedElements;
|
|
220
|
-
return s;
|
|
154
|
+
return out;
|
|
221
155
|
}
|
|
222
156
|
|
|
223
157
|
destroy(): void {
|
|
@@ -231,8 +165,9 @@ export class ChunkedIndexAllocator {
|
|
|
231
165
|
this.device, `${this.label}/c${idx}`, this.usage,
|
|
232
166
|
this.initialChunkBytes, this.maxChunkBytes,
|
|
233
167
|
);
|
|
234
|
-
this._chunks.push(new
|
|
168
|
+
this._chunks.push(new AttributeArena(this.device, buf));
|
|
235
169
|
for (const cb of this.onChunkAddedCbs) cb(idx);
|
|
236
170
|
return idx;
|
|
237
171
|
}
|
|
238
172
|
}
|
|
173
|
+
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// Freelist — best-fit free-block manager used by AttributeArena
|
|
2
|
-
//
|
|
1
|
+
// Freelist — best-fit free-block manager used by AttributeArena.
|
|
2
|
+
// Replaces the earlier `{off,size}[]` sorted-by-off
|
|
3
3
|
// freelist (whose alloc was O(N) first-fit and release was O(log N)
|
|
4
4
|
// binary-search-then-coalesce).
|
|
5
5
|
//
|
|
@@ -94,7 +94,7 @@ export class Freelist {
|
|
|
94
94
|
/**
|
|
95
95
|
* Take and remove the free block whose end offset is exactly
|
|
96
96
|
* `endOff`, if any exists. Used by `AttributeArena` /
|
|
97
|
-
* `
|
|
97
|
+
* `AttributeArena` to shrink the bump cursor back when releases
|
|
98
98
|
* expose a free tail (§5 — cursor-shrink hygiene for long-lived
|
|
99
99
|
* high-churn scenes).
|
|
100
100
|
*/
|