@aardworx/wombat.rendering 0.19.24 → 0.20.0
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 +18 -0
- package/dist/core/renderObject.d.ts.map +1 -1
- package/dist/resources/preparedRenderObject.d.ts.map +1 -1
- package/dist/resources/preparedRenderObject.js +28 -5
- package/dist/resources/preparedRenderObject.js.map +1 -1
- package/dist/resources/uniformBuffer.d.ts.map +1 -1
- package/dist/resources/uniformBuffer.js +3 -1
- package/dist/resources/uniformBuffer.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/codegen.d.ts.map +1 -1
- package/dist/runtime/derivedUniforms/codegen.js +34 -13
- package/dist/runtime/derivedUniforms/codegen.js.map +1 -1
- package/dist/runtime/derivedUniforms/dispatch.d.ts.map +1 -1
- package/dist/runtime/derivedUniforms/dispatch.js +6 -0
- package/dist/runtime/derivedUniforms/dispatch.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 +18 -4
- 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 +45 -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 +36 -1
- package/dist/runtime/heapEffect.d.ts.map +1 -1
- package/dist/runtime/heapEffect.js +29 -2
- package/dist/runtime/heapEffect.js.map +1 -1
- package/dist/runtime/heapEffectIR.d.ts.map +1 -1
- package/dist/runtime/heapEffectIR.js +51 -11
- 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 +125 -1
- package/dist/runtime/heapScene.d.ts.map +1 -1
- package/dist/runtime/heapScene.js +671 -126
- package/dist/runtime/heapScene.js.map +1 -1
- package/dist/runtime/heapShaderFamily.d.ts.map +1 -1
- package/dist/runtime/heapShaderFamily.js +19 -0
- package/dist/runtime/heapShaderFamily.js.map +1 -1
- package/dist/runtime/hybridScene.d.ts +6 -0
- package/dist/runtime/hybridScene.d.ts.map +1 -1
- package/dist/runtime/hybridScene.js +36 -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/dist/runtime/renderTask.d.ts +6 -0
- package/dist/runtime/renderTask.d.ts.map +1 -1
- package/dist/runtime/renderTask.js +3 -0
- package/dist/runtime/renderTask.js.map +1 -1
- package/dist/runtime/runtime.d.ts +15 -0
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +3 -0
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/textureAtlas/atlasMipGutterKernel.d.ts +19 -2
- package/dist/runtime/textureAtlas/atlasMipGutterKernel.d.ts.map +1 -1
- package/dist/runtime/textureAtlas/atlasMipGutterKernel.js +37 -6
- package/dist/runtime/textureAtlas/atlasMipGutterKernel.js.map +1 -1
- package/dist/runtime/textureAtlas/atlasPool.d.ts +5 -0
- package/dist/runtime/textureAtlas/atlasPool.d.ts.map +1 -1
- package/dist/runtime/textureAtlas/atlasPool.js +25 -3
- package/dist/runtime/textureAtlas/atlasPool.js.map +1 -1
- package/dist/window/loop.d.ts.map +1 -1
- package/dist/window/loop.js +31 -4
- package/dist/window/loop.js.map +1 -1
- package/package.json +3 -3
- package/src/core/elementType.ts +10 -0
- package/src/core/provider.ts +56 -28
- package/src/core/renderObject.ts +18 -0
- package/src/resources/preparedRenderObject.ts +29 -6
- package/src/resources/uniformBuffer.ts +3 -1
- package/src/runtime/derivedModes/modeKeyCpu.ts +26 -11
- package/src/runtime/derivedModes/partitionDispatcher.ts +30 -0
- package/src/runtime/derivedUniforms/codegen.ts +34 -13
- package/src/runtime/derivedUniforms/dispatch.ts +6 -0
- package/src/runtime/derivedUniforms/records.ts +70 -15
- package/src/runtime/derivedUniforms/sceneIntegration.ts +19 -4
- package/src/runtime/flattenTree.ts +47 -2
- package/src/runtime/heapAdapter.ts +43 -5
- package/src/runtime/heapDecoder.ts +8 -3
- package/src/runtime/heapEffect.ts +44 -2
- package/src/runtime/heapEffectIR.ts +54 -11
- 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 +826 -142
- package/src/runtime/heapShaderFamily.ts +21 -0
- package/src/runtime/hybridScene.ts +43 -3
- package/src/runtime/index.ts +5 -0
- package/src/runtime/renderTask.ts +9 -0
- package/src/runtime/runtime.ts +18 -0
- package/src/runtime/textureAtlas/atlasMipGutterKernel.ts +52 -3
- package/src/runtime/textureAtlas/atlasPool.ts +28 -2
- package/src/window/loop.ts +32 -4
|
@@ -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,12 +584,16 @@ 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 } : {}),
|
|
556
591
|
...(dc.instanceCount > 1 ? { instanceCount: dc.instanceCount } : {}),
|
|
557
592
|
...geom,
|
|
558
593
|
...(textures !== undefined ? { textures } : {}),
|
|
594
|
+
...(ro.injectedStorage !== undefined && ro.injectedStorage.count > 0
|
|
595
|
+
? { storageBuffers: (() => { const m = new Map<string, aval<IBuffer>>(); ro.injectedStorage!.iter((n, av) => { m.set(n, av); }); return m; })() }
|
|
596
|
+
: {}),
|
|
559
597
|
...(ro.modeRules !== undefined ? { modeRules: ro.modeRules } : {}),
|
|
560
598
|
...(ro.modelChain !== undefined ? { modelChain: ro.modelChain } : {}),
|
|
561
599
|
// Pass `active` through unforced so the heap path can subscribe
|
|
@@ -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,
|
|
@@ -75,6 +75,10 @@ export interface HeapEffectSchema {
|
|
|
75
75
|
readonly fragmentOutputs: readonly { readonly name: string; readonly location: number; readonly wgslType: string }[];
|
|
76
76
|
readonly textures: readonly HeapTextureBinding[];
|
|
77
77
|
readonly samplers: readonly HeapSamplerBinding[];
|
|
78
|
+
/** User-declared storage buffers (the OIT node pool & co) — bound at
|
|
79
|
+
* bucket level from scene-shared avals (`HeapDrawSpec.storageBuffers`).
|
|
80
|
+
* Absent/empty for ordinary effects. */
|
|
81
|
+
readonly storageBuffers?: readonly { readonly name: string; readonly access: "read" | "read_write" }[];
|
|
78
82
|
}
|
|
79
83
|
|
|
80
84
|
export interface CompiledHeapEffect {
|
|
@@ -104,7 +108,21 @@ export interface FragmentOutputLayout {
|
|
|
104
108
|
// and on quota; every failure simply degrades to "recompute".
|
|
105
109
|
|
|
106
110
|
/** Cache-generation stamp for the persistent (localStorage) tier. */
|
|
107
|
-
export const HEAP_PERSIST_VERSION = "
|
|
111
|
+
export const HEAP_PERSIST_VERSION = "h7"; // h7: OIT resolve early-exit
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Uniforms whose drawHeader word IS the u32 value (no arena
|
|
115
|
+
* indirection, no pool entry, no per-RO aval) — the `__layoutId`
|
|
116
|
+
* mechanism generalised. Values come from first-class RenderObject
|
|
117
|
+
* fields (`ro.pickId`), not the uniform provider. Fields must be
|
|
118
|
+
* u32-typed.
|
|
119
|
+
*/
|
|
120
|
+
export const INLINE_HEADER_UNIFORMS: ReadonlySet<string> = new Set(["PickId"]);
|
|
121
|
+
|
|
122
|
+
/** True for fields read inline from the drawHeader word. */
|
|
123
|
+
export function isInlineHeaderField(f: { kind: string; name: string }): boolean {
|
|
124
|
+
return f.kind === "uniform-ref" && (f.name === "__layoutId" || INLINE_HEADER_UNIFORMS.has(f.name));
|
|
125
|
+
}
|
|
108
126
|
export const HEAP_PERSIST_PREFIX = "wbt.heapfx.";
|
|
109
127
|
|
|
110
128
|
export function persistKey(version: string, kind: string, contentKey: string): string {
|
|
@@ -261,7 +279,11 @@ function buildSchema(iface: ProgramInterface): HeapEffectSchema {
|
|
|
261
279
|
name: s.name, wgslType: isComparison(s.type) ? "sampler_comparison" : "sampler",
|
|
262
280
|
...(s.state !== undefined ? { state: s.state } : {}),
|
|
263
281
|
}));
|
|
264
|
-
|
|
282
|
+
const storageBuffers = iface.storageBuffers.map(sb => ({ name: sb.name, access: sb.access }));
|
|
283
|
+
return {
|
|
284
|
+
attributes, uniforms, varyings, fragmentOutputs, textures, samplers,
|
|
285
|
+
...(storageBuffers.length > 0 ? { storageBuffers } : {}),
|
|
286
|
+
};
|
|
265
287
|
}
|
|
266
288
|
|
|
267
289
|
function irTypeToWgsl(t: IrType): string {
|
|
@@ -375,6 +397,15 @@ export interface BucketLayout {
|
|
|
375
397
|
* `atlasSample(...)` helper for these names only.
|
|
376
398
|
*/
|
|
377
399
|
readonly atlasTextureBindings: ReadonlySet<string>;
|
|
400
|
+
/**
|
|
401
|
+
* User storage-buffer bindings (allocated from
|
|
402
|
+
* HEAP_USER_STORAGE_BINDING_BASE, above the atlas range). Bound at
|
|
403
|
+
* bucket level from `HeapDrawSpec.storageBuffers` — every RO in the
|
|
404
|
+
* bucket shares the same buffers (they are scene-level injections
|
|
405
|
+
* like the OIT node pool). FRAGMENT visibility only (WebGPU forbids
|
|
406
|
+
* writable storage in vertex stages).
|
|
407
|
+
*/
|
|
408
|
+
readonly storageBindings: readonly { readonly name: string; readonly access: "read" | "read_write"; readonly binding: number }[];
|
|
378
409
|
}
|
|
379
410
|
|
|
380
411
|
/**
|
|
@@ -398,6 +429,8 @@ export const ATLAS_LINEAR_BINDING_BASE = 11;
|
|
|
398
429
|
export const ATLAS_SRGB_BINDING_BASE = ATLAS_LINEAR_BINDING_BASE + ATLAS_ARRAY_SIZE;
|
|
399
430
|
/** Atlas sampler binding (= linear base + 2N). */
|
|
400
431
|
export const ATLAS_SAMPLER_BINDING = ATLAS_LINEAR_BINDING_BASE + 2 * ATLAS_ARRAY_SIZE;
|
|
432
|
+
/** First user storage-buffer binding (above the whole atlas range). */
|
|
433
|
+
export const HEAP_USER_STORAGE_BINDING_BASE = ATLAS_SAMPLER_BINDING + 1;
|
|
401
434
|
|
|
402
435
|
export function buildBucketLayout(
|
|
403
436
|
schema: HeapEffectSchema,
|
|
@@ -522,10 +555,18 @@ export function buildBucketLayout(
|
|
|
522
555
|
isAtlasBucket,
|
|
523
556
|
);
|
|
524
557
|
|
|
558
|
+
// User storage buffers: deterministic order (by name) starting above
|
|
559
|
+
// the atlas binding range.
|
|
560
|
+
let nextStorageBinding = HEAP_USER_STORAGE_BINDING_BASE;
|
|
561
|
+
const storageBindings = [...(schema.storageBuffers ?? [])]
|
|
562
|
+
.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))
|
|
563
|
+
.map(sb => ({ name: sb.name, access: sb.access, binding: nextStorageBinding++ }));
|
|
564
|
+
|
|
525
565
|
const base = {
|
|
526
566
|
drawHeaderFields, drawHeaderBytes, preludeWgsl, strideU32,
|
|
527
567
|
perInstanceUniforms, perInstanceAttributes,
|
|
528
568
|
textureBindings, samplerBindings, atlasTextureBindings,
|
|
569
|
+
storageBindings,
|
|
529
570
|
};
|
|
530
571
|
return { id: bucketLayoutId(base), ...base };
|
|
531
572
|
}
|
|
@@ -553,6 +594,7 @@ export function bucketLayoutId(l: Omit<BucketLayout, "id">): string {
|
|
|
553
594
|
parts.push("PIA:" + [...l.perInstanceAttributes].sort().join(","));
|
|
554
595
|
for (const t of l.textureBindings) parts.push(`T:${t.name}|${t.wgslType}|${t.binding}`);
|
|
555
596
|
for (const s of l.samplerBindings) parts.push(`S:${s.name}|${s.wgslType}|${s.binding}`);
|
|
597
|
+
for (const sb of l.storageBindings) parts.push(`SB:${sb.name}|${sb.access}|${sb.binding}`);
|
|
556
598
|
parts.push("ATB:" + [...l.atlasTextureBindings].sort().join(","));
|
|
557
599
|
// The prelude is derived from the above, but fold its hash in too —
|
|
558
600
|
// cheap insurance against any generator path the field list misses.
|
|
@@ -18,8 +18,10 @@
|
|
|
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
|
-
import { substituteInputsInStage, readInputs, mapExpr, mapStmt, liftReturns, uniformsToInputs } from "@aardworx/wombat.shader/passes";
|
|
23
|
+
import { substituteInputsInStage, readInputs, mapExpr, mapStmt, liftReturns, uniformsToInputs, resolveHoles } from "@aardworx/wombat.shader/passes";
|
|
24
|
+
import type { HoleValue } from "@aardworx/wombat.shader/passes";
|
|
23
25
|
import { synthesizeHeapDecoderModule } from "./heapDecoder.js";
|
|
24
26
|
import {
|
|
25
27
|
type Module, type Expr, type Stmt, type Type, type ValueDef,
|
|
@@ -278,9 +280,12 @@ function buildVertexUniformMap(layout: BucketLayout): Map<string, Expr> {
|
|
|
278
280
|
for (const f of layout.drawHeaderFields) {
|
|
279
281
|
if (f.kind !== "uniform-ref") continue;
|
|
280
282
|
const refExpr = loadHeaderRef(drawIdx, f.byteOffset, stride);
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
283
|
+
// Inline fields: the header word IS the u32 value.
|
|
284
|
+
const value = isInlineHeaderField(f)
|
|
285
|
+
? refExpr
|
|
286
|
+
: layout.perInstanceUniforms.has(f.name)
|
|
287
|
+
? loadInstanceByRef(refExpr, iidx, f.uniformWgslType ?? "")
|
|
288
|
+
: loadUniformByRef(refExpr, f.uniformWgslType ?? "");
|
|
284
289
|
out.set(f.name, value);
|
|
285
290
|
}
|
|
286
291
|
return out;
|
|
@@ -420,9 +425,11 @@ function rewriteFsUniformsDirect(m: Module, layout: BucketLayout): Module {
|
|
|
420
425
|
const f = fieldByName.get(name);
|
|
421
426
|
if (f === undefined || f.kind !== "uniform-ref") continue;
|
|
422
427
|
const refExpr = loadHeaderRef(drawIdxExpr, f.byteOffset, layout.strideU32);
|
|
423
|
-
const value =
|
|
424
|
-
?
|
|
425
|
-
:
|
|
428
|
+
const value = isInlineHeaderField(f)
|
|
429
|
+
? refExpr
|
|
430
|
+
: layout.perInstanceUniforms.has(name)
|
|
431
|
+
? loadInstanceByRef(refExpr, instIdExpr, f.uniformWgslType ?? "")
|
|
432
|
+
: loadUniformByRef(refExpr, f.uniformWgslType ?? "");
|
|
426
433
|
fsSubst.set(name, value);
|
|
427
434
|
}
|
|
428
435
|
return substituteInputsInStage(m, "fragment", "Uniform", n => fsSubst.get(n));
|
|
@@ -541,9 +548,12 @@ function rewriteFsUniforms(m: Module, layout: BucketLayout): Module {
|
|
|
541
548
|
value: { kind: "Expr", value: refExprWriter },
|
|
542
549
|
});
|
|
543
550
|
const refReadFs = readScope("Input", refParamName, Tu32);
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
551
|
+
// Inline fields: the threaded varying already carries the value.
|
|
552
|
+
const value = isInlineHeaderField(f)
|
|
553
|
+
? refReadFs
|
|
554
|
+
: layout.perInstanceUniforms.has(name)
|
|
555
|
+
? loadInstanceByRef(refReadFs, readScope("Input", "_iidx", Tu32), f.uniformWgslType ?? "")
|
|
556
|
+
: loadUniformByRef(refReadFs, f.uniformWgslType ?? "");
|
|
547
557
|
fsSubst.set(name, value);
|
|
548
558
|
}
|
|
549
559
|
|
|
@@ -1011,6 +1021,22 @@ function compileHeapEffectIRViaDecoder(
|
|
|
1011
1021
|
// from the carrier varyings the decoder wrote).
|
|
1012
1022
|
combined = rewriteFsAtlasTexturesViaCarrier(combined, layout);
|
|
1013
1023
|
|
|
1024
|
+
// 6b. USER storage buffers (the OIT node pool & co): pin each to the
|
|
1025
|
+
// slot the bucket's BGL committed to. `keep: true` makes the emitter
|
|
1026
|
+
// use it verbatim (and stops `reduceUniforms` dropping an unreferenced
|
|
1027
|
+
// one). Without this the emitter auto-assigns sequentially and collides
|
|
1028
|
+
// with the heap's own reserved bindings.
|
|
1029
|
+
if (layout.storageBindings.length > 0) {
|
|
1030
|
+
const userStorageSlot = new Map(layout.storageBindings.map(b => [b.name, b.binding]));
|
|
1031
|
+
combined = {
|
|
1032
|
+
...combined,
|
|
1033
|
+
values: combined.values.map(v =>
|
|
1034
|
+
v.kind === "StorageBuffer" && userStorageSlot.has(v.name)
|
|
1035
|
+
? { ...v, binding: { group: 0, slot: userStorageSlot.get(v.name)! }, keep: true }
|
|
1036
|
+
: v),
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1014
1040
|
// 7. Compile.
|
|
1015
1041
|
const compiled = compileModule(combined, compileOptions);
|
|
1016
1042
|
if ((globalThis as { __HEAP_DEBUG_FULL__?: boolean }).__HEAP_DEBUG_FULL__) {
|
|
@@ -1557,7 +1583,24 @@ function mergeStages(eff: Effect): Module {
|
|
|
1557
1583
|
// Combine all stages' templates into one module by concatenating
|
|
1558
1584
|
// their values + types arrays. The compile pipeline's composeStages
|
|
1559
1585
|
// pass will fuse same-stage entries afterwards.
|
|
1586
|
+
//
|
|
1587
|
+
// Closure holes are resolved here (per stage, with that stage's
|
|
1588
|
+
// getters) — Effect.compile does this on the classic path, but the
|
|
1589
|
+
// heap pipeline compiles the raw templates directly, and an
|
|
1590
|
+
// unresolved `ReadInput("Closure", …)` emits as a bare identifier
|
|
1591
|
+
// ("unresolved value" at pipeline compile). Constant captures like
|
|
1592
|
+
// an OIT writer's node-pool capacity are the norm in composed
|
|
1593
|
+
// effects.
|
|
1560
1594
|
const types = eff.stages.flatMap(s => [...s.template.types]);
|
|
1561
|
-
const values = eff.stages.flatMap(s =>
|
|
1595
|
+
const values = eff.stages.flatMap(s => {
|
|
1596
|
+
let hasHoles = false;
|
|
1597
|
+
const sampled: Record<string, HoleValue> = {};
|
|
1598
|
+
for (const [name, getter] of Object.entries(s.holes ?? {})) {
|
|
1599
|
+
sampled[name] = getter();
|
|
1600
|
+
hasHoles = true;
|
|
1601
|
+
}
|
|
1602
|
+
const filled = hasHoles ? resolveHoles(s.template, sampled) : s.template;
|
|
1603
|
+
return [...filled.values];
|
|
1604
|
+
});
|
|
1562
1605
|
return { types, values };
|
|
1563
1606
|
}
|
|
@@ -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);
|