@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.
Files changed (137) hide show
  1. package/dist/core/elementType.d.ts +2 -0
  2. package/dist/core/elementType.d.ts.map +1 -1
  3. package/dist/core/elementType.js +10 -0
  4. package/dist/core/elementType.js.map +1 -1
  5. package/dist/core/provider.d.ts.map +1 -1
  6. package/dist/core/provider.js +65 -32
  7. package/dist/core/provider.js.map +1 -1
  8. package/dist/core/renderObject.d.ts +18 -0
  9. package/dist/core/renderObject.d.ts.map +1 -1
  10. package/dist/resources/preparedRenderObject.d.ts.map +1 -1
  11. package/dist/resources/preparedRenderObject.js +28 -5
  12. package/dist/resources/preparedRenderObject.js.map +1 -1
  13. package/dist/resources/uniformBuffer.d.ts.map +1 -1
  14. package/dist/resources/uniformBuffer.js +3 -1
  15. package/dist/resources/uniformBuffer.js.map +1 -1
  16. package/dist/runtime/derivedModes/modeKeyCpu.d.ts +2 -12
  17. package/dist/runtime/derivedModes/modeKeyCpu.d.ts.map +1 -1
  18. package/dist/runtime/derivedModes/modeKeyCpu.js +30 -13
  19. package/dist/runtime/derivedModes/modeKeyCpu.js.map +1 -1
  20. package/dist/runtime/derivedModes/partitionDispatcher.d.ts +11 -0
  21. package/dist/runtime/derivedModes/partitionDispatcher.d.ts.map +1 -1
  22. package/dist/runtime/derivedModes/partitionDispatcher.js +31 -0
  23. package/dist/runtime/derivedModes/partitionDispatcher.js.map +1 -1
  24. package/dist/runtime/derivedUniforms/codegen.d.ts.map +1 -1
  25. package/dist/runtime/derivedUniforms/codegen.js +34 -13
  26. package/dist/runtime/derivedUniforms/codegen.js.map +1 -1
  27. package/dist/runtime/derivedUniforms/dispatch.d.ts.map +1 -1
  28. package/dist/runtime/derivedUniforms/dispatch.js +6 -0
  29. package/dist/runtime/derivedUniforms/dispatch.js.map +1 -1
  30. package/dist/runtime/derivedUniforms/records.d.ts +15 -0
  31. package/dist/runtime/derivedUniforms/records.d.ts.map +1 -1
  32. package/dist/runtime/derivedUniforms/records.js +81 -14
  33. package/dist/runtime/derivedUniforms/records.js.map +1 -1
  34. package/dist/runtime/derivedUniforms/sceneIntegration.d.ts +6 -0
  35. package/dist/runtime/derivedUniforms/sceneIntegration.d.ts.map +1 -1
  36. package/dist/runtime/derivedUniforms/sceneIntegration.js +18 -4
  37. package/dist/runtime/derivedUniforms/sceneIntegration.js.map +1 -1
  38. package/dist/runtime/flattenTree.d.ts.map +1 -1
  39. package/dist/runtime/flattenTree.js +46 -2
  40. package/dist/runtime/flattenTree.js.map +1 -1
  41. package/dist/runtime/heapAdapter.d.ts +3 -1
  42. package/dist/runtime/heapAdapter.d.ts.map +1 -1
  43. package/dist/runtime/heapAdapter.js +45 -4
  44. package/dist/runtime/heapAdapter.js.map +1 -1
  45. package/dist/runtime/heapDecoder.d.ts.map +1 -1
  46. package/dist/runtime/heapDecoder.js +8 -3
  47. package/dist/runtime/heapDecoder.js.map +1 -1
  48. package/dist/runtime/heapEffect.d.ts +36 -1
  49. package/dist/runtime/heapEffect.d.ts.map +1 -1
  50. package/dist/runtime/heapEffect.js +29 -2
  51. package/dist/runtime/heapEffect.js.map +1 -1
  52. package/dist/runtime/heapEffectIR.d.ts.map +1 -1
  53. package/dist/runtime/heapEffectIR.js +51 -11
  54. package/dist/runtime/heapEffectIR.js.map +1 -1
  55. package/dist/runtime/heapEligibility.d.ts.map +1 -1
  56. package/dist/runtime/heapEligibility.js +64 -12
  57. package/dist/runtime/heapEligibility.js.map +1 -1
  58. package/dist/runtime/heapIrBuilders.d.ts +8 -0
  59. package/dist/runtime/heapIrBuilders.d.ts.map +1 -1
  60. package/dist/runtime/heapIrBuilders.js +47 -2
  61. package/dist/runtime/heapIrBuilders.js.map +1 -1
  62. package/dist/runtime/heapScene/chunkedArena.d.ts +20 -28
  63. package/dist/runtime/heapScene/chunkedArena.d.ts.map +1 -1
  64. package/dist/runtime/heapScene/chunkedArena.js +28 -91
  65. package/dist/runtime/heapScene/chunkedArena.js.map +1 -1
  66. package/dist/runtime/heapScene/freelist.d.ts +1 -1
  67. package/dist/runtime/heapScene/freelist.js +3 -3
  68. package/dist/runtime/heapScene/freelist.js.map +1 -1
  69. package/dist/runtime/heapScene/pools.d.ts +103 -59
  70. package/dist/runtime/heapScene/pools.d.ts.map +1 -1
  71. package/dist/runtime/heapScene/pools.js +335 -194
  72. package/dist/runtime/heapScene/pools.js.map +1 -1
  73. package/dist/runtime/heapScene.d.ts +125 -1
  74. package/dist/runtime/heapScene.d.ts.map +1 -1
  75. package/dist/runtime/heapScene.js +671 -126
  76. package/dist/runtime/heapScene.js.map +1 -1
  77. package/dist/runtime/heapShaderFamily.d.ts.map +1 -1
  78. package/dist/runtime/heapShaderFamily.js +19 -0
  79. package/dist/runtime/heapShaderFamily.js.map +1 -1
  80. package/dist/runtime/hybridScene.d.ts +6 -0
  81. package/dist/runtime/hybridScene.d.ts.map +1 -1
  82. package/dist/runtime/hybridScene.js +36 -3
  83. package/dist/runtime/hybridScene.js.map +1 -1
  84. package/dist/runtime/index.d.ts +1 -1
  85. package/dist/runtime/index.d.ts.map +1 -1
  86. package/dist/runtime/index.js +1 -1
  87. package/dist/runtime/index.js.map +1 -1
  88. package/dist/runtime/renderTask.d.ts +6 -0
  89. package/dist/runtime/renderTask.d.ts.map +1 -1
  90. package/dist/runtime/renderTask.js +3 -0
  91. package/dist/runtime/renderTask.js.map +1 -1
  92. package/dist/runtime/runtime.d.ts +15 -0
  93. package/dist/runtime/runtime.d.ts.map +1 -1
  94. package/dist/runtime/runtime.js +3 -0
  95. package/dist/runtime/runtime.js.map +1 -1
  96. package/dist/runtime/textureAtlas/atlasMipGutterKernel.d.ts +19 -2
  97. package/dist/runtime/textureAtlas/atlasMipGutterKernel.d.ts.map +1 -1
  98. package/dist/runtime/textureAtlas/atlasMipGutterKernel.js +37 -6
  99. package/dist/runtime/textureAtlas/atlasMipGutterKernel.js.map +1 -1
  100. package/dist/runtime/textureAtlas/atlasPool.d.ts +5 -0
  101. package/dist/runtime/textureAtlas/atlasPool.d.ts.map +1 -1
  102. package/dist/runtime/textureAtlas/atlasPool.js +25 -3
  103. package/dist/runtime/textureAtlas/atlasPool.js.map +1 -1
  104. package/dist/window/loop.d.ts.map +1 -1
  105. package/dist/window/loop.js +31 -4
  106. package/dist/window/loop.js.map +1 -1
  107. package/package.json +3 -3
  108. package/src/core/elementType.ts +10 -0
  109. package/src/core/provider.ts +56 -28
  110. package/src/core/renderObject.ts +18 -0
  111. package/src/resources/preparedRenderObject.ts +29 -6
  112. package/src/resources/uniformBuffer.ts +3 -1
  113. package/src/runtime/derivedModes/modeKeyCpu.ts +26 -11
  114. package/src/runtime/derivedModes/partitionDispatcher.ts +30 -0
  115. package/src/runtime/derivedUniforms/codegen.ts +34 -13
  116. package/src/runtime/derivedUniforms/dispatch.ts +6 -0
  117. package/src/runtime/derivedUniforms/records.ts +70 -15
  118. package/src/runtime/derivedUniforms/sceneIntegration.ts +19 -4
  119. package/src/runtime/flattenTree.ts +47 -2
  120. package/src/runtime/heapAdapter.ts +43 -5
  121. package/src/runtime/heapDecoder.ts +8 -3
  122. package/src/runtime/heapEffect.ts +44 -2
  123. package/src/runtime/heapEffectIR.ts +54 -11
  124. package/src/runtime/heapEligibility.ts +39 -12
  125. package/src/runtime/heapIrBuilders.ts +50 -2
  126. package/src/runtime/heapScene/chunkedArena.ts +29 -94
  127. package/src/runtime/heapScene/freelist.ts +3 -3
  128. package/src/runtime/heapScene/pools.ts +340 -218
  129. package/src/runtime/heapScene.ts +826 -142
  130. package/src/runtime/heapShaderFamily.ts +21 -0
  131. package/src/runtime/hybridScene.ts +43 -3
  132. package/src/runtime/index.ts +5 -0
  133. package/src/runtime/renderTask.ts +9 -0
  134. package/src/runtime/runtime.ts +18 -0
  135. package/src/runtime/textureAtlas/atlasMipGutterKernel.ts +52 -3
  136. package/src/runtime/textureAtlas/atlasPool.ts +28 -2
  137. 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
- for (const u of schema.uniforms) pullUniform(u.name);
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
- const distinctTexAvals = new Set<aval<ITexture>>();
403
- ro.textures.iter((_n, av) => { distinctTexAvals.add(av as aval<ITexture>); });
404
- const distinctSamplerAvals = new Set<aval<ISampler>>();
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.add(av as aval<ISampler>); });
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
- const value = layout.perInstanceUniforms.has(f.name)
311
- ? loadInstanceByRef(refExpr, instIdExpr, wgslType)
312
- : loadUniformByRef(refExpr, wgslType);
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 = "h2";
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
- return { attributes, uniforms, varyings, fragmentOutputs, textures, samplers };
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
- const value = layout.perInstanceUniforms.has(f.name)
282
- ? loadInstanceByRef(refExpr, iidx, f.uniformWgslType ?? "")
283
- : loadUniformByRef(refExpr, f.uniformWgslType ?? "");
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 = layout.perInstanceUniforms.has(name)
424
- ? loadInstanceByRef(refExpr, instIdExpr, f.uniformWgslType ?? "")
425
- : loadUniformByRef(refExpr, f.uniformWgslType ?? "");
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
- const value = layout.perInstanceUniforms.has(name)
545
- ? loadInstanceByRef(refReadFs, readScope("Input", "_iidx", Tu32), f.uniformWgslType ?? "")
546
- : loadUniformByRef(refReadFs, f.uniformWgslType ?? "");
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 => [...s.template.values]);
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
- const distinctTextureAvals = new Set<aval<ITexture>>();
178
- ro.textures.iter((_n, av) => { distinctTextureAvals.add(av); });
179
- if (distinctTextureAvals.size > 1) {
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
- const distinctSamplerAvals = new Set<aval<ISampler>>();
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
- // Reactive AND-fold over all participating avals. We also subscribe
206
- // to drawCall (instanceCount/baseVertex/firstIndex/firstInstance
207
- // can flip; if they violate heap constraints the RO routes to the
208
- // legacy path that frame).
209
- return AVal.custom(token => {
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
- return newVec([
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
- return newVec([
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);