@aardworx/wombat.rendering 0.8.1 → 0.9.1

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 (141) hide show
  1. package/dist/core/command.d.ts +7 -9
  2. package/dist/core/command.d.ts.map +1 -1
  3. package/dist/core/renderTask.d.ts +64 -1
  4. package/dist/core/renderTask.d.ts.map +1 -1
  5. package/dist/core/renderTask.js +3 -1
  6. package/dist/core/renderTask.js.map +1 -1
  7. package/dist/resources/preparedRenderObject.js +3 -3
  8. package/dist/resources/preparedRenderObject.js.map +1 -1
  9. package/dist/resources/uniformBuffer.d.ts.map +1 -1
  10. package/dist/resources/uniformBuffer.js +17 -0
  11. package/dist/resources/uniformBuffer.js.map +1 -1
  12. package/dist/runtime/derivedUniforms/dispatch.d.ts +105 -0
  13. package/dist/runtime/derivedUniforms/dispatch.d.ts.map +1 -0
  14. package/dist/runtime/derivedUniforms/dispatch.js +355 -0
  15. package/dist/runtime/derivedUniforms/dispatch.js.map +1 -0
  16. package/dist/runtime/derivedUniforms/index.d.ts +6 -0
  17. package/dist/runtime/derivedUniforms/index.d.ts.map +1 -0
  18. package/dist/runtime/derivedUniforms/index.js +26 -0
  19. package/dist/runtime/derivedUniforms/index.js.map +1 -0
  20. package/dist/runtime/derivedUniforms/recipes.d.ts +42 -0
  21. package/dist/runtime/derivedUniforms/recipes.d.ts.map +1 -0
  22. package/dist/runtime/derivedUniforms/recipes.js +134 -0
  23. package/dist/runtime/derivedUniforms/recipes.js.map +1 -0
  24. package/dist/runtime/derivedUniforms/sceneIntegration.d.ts +56 -0
  25. package/dist/runtime/derivedUniforms/sceneIntegration.d.ts.map +1 -0
  26. package/dist/runtime/derivedUniforms/sceneIntegration.js +171 -0
  27. package/dist/runtime/derivedUniforms/sceneIntegration.js.map +1 -0
  28. package/dist/runtime/derivedUniforms/slotId.d.ts +15 -0
  29. package/dist/runtime/derivedUniforms/slotId.d.ts.map +1 -0
  30. package/dist/runtime/derivedUniforms/slotId.js +30 -0
  31. package/dist/runtime/derivedUniforms/slotId.js.map +1 -0
  32. package/dist/runtime/derivedUniforms/slots.d.ts +49 -0
  33. package/dist/runtime/derivedUniforms/slots.d.ts.map +1 -0
  34. package/dist/runtime/derivedUniforms/slots.js +123 -0
  35. package/dist/runtime/derivedUniforms/slots.js.map +1 -0
  36. package/dist/runtime/derivedUniforms/uberKernel.wgsl.d.ts +13 -0
  37. package/dist/runtime/derivedUniforms/uberKernel.wgsl.d.ts.map +1 -0
  38. package/dist/runtime/derivedUniforms/uberKernel.wgsl.js +218 -0
  39. package/dist/runtime/derivedUniforms/uberKernel.wgsl.js.map +1 -0
  40. package/dist/runtime/flattenTree.d.ts +5 -0
  41. package/dist/runtime/flattenTree.d.ts.map +1 -0
  42. package/dist/runtime/flattenTree.js +45 -0
  43. package/dist/runtime/flattenTree.js.map +1 -0
  44. package/dist/runtime/heapAdapter.d.ts +16 -0
  45. package/dist/runtime/heapAdapter.d.ts.map +1 -0
  46. package/dist/runtime/heapAdapter.js +263 -0
  47. package/dist/runtime/heapAdapter.js.map +1 -0
  48. package/dist/runtime/heapEffect.d.ts +235 -0
  49. package/dist/runtime/heapEffect.d.ts.map +1 -0
  50. package/dist/runtime/heapEffect.js +529 -0
  51. package/dist/runtime/heapEffect.js.map +1 -0
  52. package/dist/runtime/heapEffectIR.d.ts +34 -0
  53. package/dist/runtime/heapEffectIR.d.ts.map +1 -0
  54. package/dist/runtime/heapEffectIR.js +1161 -0
  55. package/dist/runtime/heapEffectIR.js.map +1 -0
  56. package/dist/runtime/heapEligibility.d.ts +9 -0
  57. package/dist/runtime/heapEligibility.d.ts.map +1 -0
  58. package/dist/runtime/heapEligibility.js +195 -0
  59. package/dist/runtime/heapEligibility.js.map +1 -0
  60. package/dist/runtime/heapScene.d.ts +240 -31
  61. package/dist/runtime/heapScene.d.ts.map +1 -1
  62. package/dist/runtime/heapScene.js +3334 -265
  63. package/dist/runtime/heapScene.js.map +1 -1
  64. package/dist/runtime/heapShaderFamily.d.ts +132 -0
  65. package/dist/runtime/heapShaderFamily.d.ts.map +1 -0
  66. package/dist/runtime/heapShaderFamily.js +866 -0
  67. package/dist/runtime/heapShaderFamily.js.map +1 -0
  68. package/dist/runtime/hybridScene.d.ts +133 -0
  69. package/dist/runtime/hybridScene.d.ts.map +1 -0
  70. package/dist/runtime/hybridScene.js +159 -0
  71. package/dist/runtime/hybridScene.js.map +1 -0
  72. package/dist/runtime/index.d.ts +8 -1
  73. package/dist/runtime/index.d.ts.map +1 -1
  74. package/dist/runtime/index.js +9 -1
  75. package/dist/runtime/index.js.map +1 -1
  76. package/dist/runtime/renderTask.d.ts +21 -4
  77. package/dist/runtime/renderTask.d.ts.map +1 -1
  78. package/dist/runtime/renderTask.js +127 -32
  79. package/dist/runtime/renderTask.js.map +1 -1
  80. package/dist/runtime/runtime.d.ts +33 -3
  81. package/dist/runtime/runtime.d.ts.map +1 -1
  82. package/dist/runtime/runtime.js +12 -3
  83. package/dist/runtime/runtime.js.map +1 -1
  84. package/dist/runtime/scenePass.d.ts +28 -3
  85. package/dist/runtime/scenePass.d.ts.map +1 -1
  86. package/dist/runtime/scenePass.js +37 -4
  87. package/dist/runtime/scenePass.js.map +1 -1
  88. package/dist/runtime/textureAtlas/atlasMipGutterKernel.d.ts +35 -0
  89. package/dist/runtime/textureAtlas/atlasMipGutterKernel.d.ts.map +1 -0
  90. package/dist/runtime/textureAtlas/atlasMipGutterKernel.js +247 -0
  91. package/dist/runtime/textureAtlas/atlasMipGutterKernel.js.map +1 -0
  92. package/dist/runtime/textureAtlas/atlasPool.d.ts +240 -0
  93. package/dist/runtime/textureAtlas/atlasPool.d.ts.map +1 -0
  94. package/dist/runtime/textureAtlas/atlasPool.js +798 -0
  95. package/dist/runtime/textureAtlas/atlasPool.js.map +1 -0
  96. package/dist/runtime/textureAtlas/bvhTree2d.d.ts +33 -0
  97. package/dist/runtime/textureAtlas/bvhTree2d.d.ts.map +1 -0
  98. package/dist/runtime/textureAtlas/bvhTree2d.js +501 -0
  99. package/dist/runtime/textureAtlas/bvhTree2d.js.map +1 -0
  100. package/dist/runtime/textureAtlas/packer.d.ts +50 -0
  101. package/dist/runtime/textureAtlas/packer.d.ts.map +1 -0
  102. package/dist/runtime/textureAtlas/packer.js +390 -0
  103. package/dist/runtime/textureAtlas/packer.js.map +1 -0
  104. package/dist/window/loop.d.ts +14 -0
  105. package/dist/window/loop.d.ts.map +1 -1
  106. package/dist/window/loop.js +18 -5
  107. package/dist/window/loop.js.map +1 -1
  108. package/package.json +6 -5
  109. package/src/core/command.ts +9 -9
  110. package/src/core/renderTask.ts +55 -2
  111. package/src/resources/preparedRenderObject.ts +3 -3
  112. package/src/resources/uniformBuffer.ts +15 -0
  113. package/src/runtime/derivedUniforms/dispatch.ts +407 -0
  114. package/src/runtime/derivedUniforms/index.ts +52 -0
  115. package/src/runtime/derivedUniforms/recipes.ts +159 -0
  116. package/src/runtime/derivedUniforms/sceneIntegration.ts +239 -0
  117. package/src/runtime/derivedUniforms/slotId.ts +35 -0
  118. package/src/runtime/derivedUniforms/slots.ts +155 -0
  119. package/src/runtime/derivedUniforms/uberKernel.wgsl.ts +220 -0
  120. package/src/runtime/flattenTree.ts +46 -0
  121. package/src/runtime/heapAdapter.ts +314 -0
  122. package/src/runtime/heapEffect.ts +728 -0
  123. package/src/runtime/heapEffectIR.ts +1211 -0
  124. package/src/runtime/heapEligibility.ts +189 -0
  125. package/src/runtime/heapScene.ts +3981 -349
  126. package/src/runtime/heapShaderFamily.ts +1102 -0
  127. package/src/runtime/hybridScene.ts +310 -0
  128. package/src/runtime/index.ts +45 -1
  129. package/src/runtime/renderTask.ts +167 -32
  130. package/src/runtime/runtime.ts +37 -4
  131. package/src/runtime/scenePass.ts +39 -4
  132. package/src/runtime/textureAtlas/atlasMipGutterKernel.ts +291 -0
  133. package/src/runtime/textureAtlas/atlasPool.ts +991 -0
  134. package/src/runtime/textureAtlas/bvhTree2d.ts +530 -0
  135. package/src/runtime/textureAtlas/packer.ts +429 -0
  136. package/src/window/loop.ts +32 -6
  137. package/dist/runtime/heapShader.d.ts +0 -12
  138. package/dist/runtime/heapShader.d.ts.map +0 -1
  139. package/dist/runtime/heapShader.js +0 -157
  140. package/dist/runtime/heapShader.js.map +0 -1
  141. package/src/runtime/heapShader.ts +0 -185
@@ -0,0 +1,310 @@
1
+ // hybridScene — composes the heap-bucket fast path with the legacy
2
+ // per-RO path into a single render-pass renderer.
3
+ //
4
+ // Pipeline:
5
+ // 1. `flattenRenderTree(tree)` → `aset<RenderObject>` (drops intra-
6
+ // pass ordering by contract).
7
+ // 2. `partitionA(aset, isHeapEligible)` → (heapAset, legacyAset).
8
+ // Reactive: an RO migrates when its eligibility predicate marks
9
+ // (e.g. an `aval<IBuffer>` flips host↔gpu).
10
+ // 3. `heapAset.map(renderObjectToHeapSpec)` → `aset<HeapDrawSpec>`,
11
+ // fed to `buildHeapScene`. The adapter is memoized per-RO so
12
+ // the same RO always maps to the same spec object — needed for
13
+ // delta-driven removal to identify the right entry.
14
+ // 4. `legacyAset` wrapped as `RenderTree.unorderedFromSet(map(leaf))`
15
+ // and fed to `ScenePass`. Same NodeWalker machinery that the
16
+ // legacy `RenderTask` uses.
17
+ //
18
+ // Per-frame:
19
+ // - `update(token)` runs both backends' update phases (CPU work
20
+ // only — pool repacks, RO preparation).
21
+ // - `encodeIntoPass(passEnc, token)` runs both backends' draw
22
+ // emission into the caller-managed render pass.
23
+ //
24
+ // Order between the two batches inside the pass is irrelevant by
25
+ // the aset contract — the framebuffer's depth/blend ops resolve any
26
+ // apparent ordering. State changes between batches are limited to
27
+ // pipeline + bind-group switches at the boundary; both batches retain
28
+ // their internal amortization.
29
+
30
+ import {
31
+ ASet, AVal, AdaptiveToken, type aset, type aval, type AdaptiveToken as _T,
32
+ } from "@aardworx/wombat.adaptive";
33
+ import type { CompiledEffect, Effect } from "../core/shader.js";
34
+ import type { FramebufferSignature } from "../core/framebufferSignature.js";
35
+ import { RenderTree } from "../core/renderTree.js";
36
+ import type { RenderObject } from "../core/renderObject.js";
37
+ import { ScenePass } from "./scenePass.js";
38
+ import { flattenRenderTree } from "./flattenTree.js";
39
+ import { isHeapEligible } from "./heapEligibility.js";
40
+ import { renderObjectToHeapSpec } from "./heapAdapter.js";
41
+ import { buildHeapScene, type HeapDrawSpec, type HeapScene } from "./heapScene.js";
42
+ import type { FragmentOutputLayout } from "./heapEffect.js";
43
+ import { AtlasPool } from "./textureAtlas/atlasPool.js";
44
+
45
+ void (null as AdaptiveToken | _T | null);
46
+
47
+ export interface CompileHybridSceneOptions {
48
+ /**
49
+ * Maps each fragment-output name an effect emits to its framebuffer
50
+ * attachment location. Used by both backends — the heap path uses
51
+ * it during effect introspection, the legacy path passes it to
52
+ * `Effect.compile`. Omitted ⇒ derive from `signature.colorNames`
53
+ * by index.
54
+ */
55
+ readonly fragmentOutputLayout?: FragmentOutputLayout;
56
+ /**
57
+ * Override `Effect → CompiledEffect`. Defaults to
58
+ * `effect.compile({ target: "wgsl", fragmentOutputLayout })` with
59
+ * the `fragmentOutputLayout` resolved as above. Used only by the
60
+ * legacy path; the heap path calls `compileHeapEffect` itself.
61
+ */
62
+ readonly compileEffect?: (e: Effect, sig: FramebufferSignature) => CompiledEffect;
63
+ /**
64
+ * Global on/off switch for the heap fast path. When `false`, every
65
+ * `RenderObject` routes through the legacy per-RO path regardless
66
+ * of its own eligibility — equivalent to forcing `isHeapEligible`
67
+ * to `false` for the whole scene. Reactive: flipping it migrates
68
+ * ROs between subsets via the existing `filterA` partition.
69
+ *
70
+ * Use cases:
71
+ * - A/B perf comparisons (heap vs legacy on identical scenes).
72
+ * - Quick fallback if a heap-path bug shows up on a specific
73
+ * workload — flip the cval, ship.
74
+ *
75
+ * Default: `AVal.constant(true)` (heap path on for eligible ROs).
76
+ */
77
+ readonly heapEnabled?: aval<boolean>;
78
+ /**
79
+ * §6 family-merge opt-in — pass-through to `BuildHeapSceneOptions`.
80
+ * When true, all effects collapse into one bucket per pipelineState.
81
+ * Default false (per-effect buckets).
82
+ */
83
+ readonly enableFamilyMerge?: boolean;
84
+ /**
85
+ * §7 derived-uniforms opt-in — pass-through to `BuildHeapSceneOptions`.
86
+ */
87
+ readonly enableDerivedUniforms?: boolean;
88
+ }
89
+
90
+ export interface HybridScene {
91
+ /**
92
+ * Run both backends' CPU-side update phases. Idempotent within a
93
+ * frame. Call before opening the render pass.
94
+ */
95
+ update(token: AdaptiveToken): void;
96
+ /**
97
+ * Encode pre-pass compute work the heap path needs (drawTable GPU
98
+ * prefix-sum). Must be called BEFORE `beginRenderPass`. No-op when
99
+ * the heap path has no buckets or none are dirty.
100
+ */
101
+ encodeComputePrep(enc: GPUCommandEncoder, token: AdaptiveToken): void;
102
+ /**
103
+ * Encode draws from both backends into the caller-managed render
104
+ * pass. Heap batch first, legacy batch second — order is contract-
105
+ * irrelevant inside one pass.
106
+ */
107
+ encodeIntoPass(passEnc: GPURenderPassEncoder, token: AdaptiveToken): void;
108
+ /**
109
+ * Cheap check for "is there anything to draw?". Lets the caller
110
+ * skip opening the render pass entirely when both backends are
111
+ * empty AND no clear is requested. Must be called after `update`.
112
+ */
113
+ hasDraws(): boolean;
114
+ /**
115
+ * Number of GPU buckets the heap path is currently emitting. With
116
+ * §6 family-merge in v1 this typically collapses to 1 per
117
+ * pipelineState. Useful for status / dev-overlay text.
118
+ */
119
+ heapBucketCount(): number;
120
+ /** Per-frame breakdown of §7 derived-uniforms work (CPU). */
121
+ heapDerivedTimings(): {
122
+ pullMs: number; uploadMs: number; encodeMs: number; records: number;
123
+ };
124
+ /** Diagnostic: download heap state and verify drawHeader refs land in
125
+ * arena, drawTable rows reference valid indices, prefix sum is
126
+ * consistent, attribute alloc headers (typeId/length) are sane and
127
+ * data is finite. Returns issue strings + counts. */
128
+ validateHeap(): Promise<{
129
+ arenaBytes: number; issues: string[];
130
+ okRefs: number; badRefs: number;
131
+ drawTableRows: number; drawTableErrs: number; prefixSumErrs: number;
132
+ attrAllocsChecked: number; attrAllocsBad: number;
133
+ tilesChecked: number; tilesBad: number;
134
+ vidChecks: number; vidBad: number;
135
+ indicesHash: string;
136
+ }>;
137
+ /** Per-emit CPU draw simulator — samples the global emit space,
138
+ * reproduces the binary-search + (slot, _local, instId, vid)
139
+ * recovery, and verifies every storage-buffer read address is
140
+ * in-bounds and reads finite data. Reports any out-of-bounds. */
141
+ simulateDraws(samples?: number): Promise<{
142
+ emitsChecked: number; oob: number; issues: string[];
143
+ }>;
144
+ /** GPU-side binary-search probe — dispatches a kernel mirroring the
145
+ * render VS preamble; for each sampled emit, compares GPU-resolved
146
+ * slot to CPU-resolved slot. Mismatches indicate the search loop
147
+ * (or one of its inputs as seen by the kernel) miscomputes. */
148
+ probeBinarySearch(samples?: number): Promise<{
149
+ emitsChecked: number; gpuMismatches: number; issues: string[];
150
+ }>;
151
+ /** Triangle-level coherence check — for each sampled triangle base,
152
+ * verifies all three of its emits resolve to the same slot. */
153
+ checkTriangleCoherence(samples?: number): Promise<{
154
+ trianglesChecked: number; crossSlot: number; issues: string[];
155
+ }>;
156
+ dispose(): void;
157
+ }
158
+
159
+ function defaultFragmentOutputLayout(sig: FramebufferSignature): FragmentOutputLayout {
160
+ const locations = new Map<string, number>();
161
+ sig.colorNames.forEach((name, i) => locations.set(name, i));
162
+ return { locations };
163
+ }
164
+
165
+ export function compileHybridScene(
166
+ device: GPUDevice,
167
+ signature: FramebufferSignature,
168
+ tree: RenderTree,
169
+ opts: CompileHybridSceneOptions = {},
170
+ ): HybridScene {
171
+ const fragmentOutputLayout =
172
+ opts.fragmentOutputLayout ?? defaultFragmentOutputLayout(signature);
173
+ const compileEffect =
174
+ opts.compileEffect ?? ((e: Effect, _sig: FramebufferSignature) =>
175
+ e.compile({ target: "wgsl", fragmentOutputLayout }));
176
+
177
+ // ─── Partition ───────────────────────────────────────────────────
178
+ // Memoize the eligibility predicate per-RO so the two filterA calls
179
+ // share one underlying observation. Without this, both calls would
180
+ // build independent custom-avals with overlapping subscriptions.
181
+ // The per-RO eligibility is ANDed with the global `heapEnabled`
182
+ // toggle — flipping that off forces every RO to legacy.
183
+ const heapEnabled = opts.heapEnabled ?? AVal.constant(true);
184
+ const eligCache = new WeakMap<RenderObject, aval<boolean>>();
185
+ const elig = (ro: RenderObject): aval<boolean> => {
186
+ let av = eligCache.get(ro);
187
+ if (av === undefined) {
188
+ const perRO = isHeapEligible(ro);
189
+ av = AVal.custom(t => heapEnabled.getValue(t) && perRO.getValue(t));
190
+ eligCache.set(ro, av);
191
+ }
192
+ return av;
193
+ };
194
+
195
+ const flat = flattenRenderTree(tree);
196
+ const heapAset = flat.filterA(ro => elig(ro));
197
+ const legacyAset = flat.filterA(ro => elig(ro).map(b => !b));
198
+
199
+ // ─── Atlas pool ──────────────────────────────────────────────────
200
+ // Per-scene Tier-S atlas pool. Owned by this hybrid scene; disposed
201
+ // alongside the heap path. The adapter consults it to classify
202
+ // textures into Tier S (atlas-packed) vs Tier L (standalone). MVP:
203
+ // the adapter currently always returns Tier L (see heapAdapter
204
+ // comments) — the pool is wired here so the next PR can add Tier-S
205
+ // classification without touching this file.
206
+ const atlasPool = new AtlasPool(device);
207
+
208
+ // ─── Heap subset → HeapDrawSpec aset ─────────────────────────────
209
+ // Memoize the adapter: aset removal must identify the SAME spec
210
+ // object that addition produced; the underlying HashSet uses
211
+ // identity/equality and HeapDrawSpec is plain (no structural hash).
212
+ // WeakMap keyed by RO ensures the same RO always maps to the same
213
+ // spec instance, even if the RO migrates out and back in.
214
+ const specCache = new WeakMap<RenderObject, HeapDrawSpec>();
215
+ const heapSpecAset = heapAset.map((ro: RenderObject) => {
216
+ let spec = specCache.get(ro);
217
+ if (spec === undefined) {
218
+ spec = renderObjectToHeapSpec(ro, AdaptiveToken.top, atlasPool);
219
+ specCache.set(ro, spec);
220
+ }
221
+ return spec;
222
+ });
223
+
224
+ const heapScene: HeapScene = buildHeapScene(device, signature, heapSpecAset, {
225
+ fragmentOutputLayout,
226
+ atlasPool,
227
+ ...(opts.enableFamilyMerge === true ? { enableFamilyMerge: true } : {}),
228
+ ...(opts.enableDerivedUniforms === true ? { enableDerivedUniforms: true } : {}),
229
+ });
230
+
231
+ // ─── Legacy subset → RenderTree → ScenePass ──────────────────────
232
+ // The unordered-from-set wrapper drops index/order info; the legacy
233
+ // path inherits the same "no order inside a pass" contract. Re-using
234
+ // the existing NodeWalker machinery means RO preparation, caching,
235
+ // and resource ref-counting all behave exactly as the master path.
236
+ const legacyTree: RenderTree =
237
+ RenderTree.unorderedFromSet(legacyAset.map(ro => RenderTree.leaf(ro)));
238
+ const scenePass = new ScenePass(device, signature, legacyTree, compileEffect);
239
+
240
+ return {
241
+ update(token: AdaptiveToken): void {
242
+ heapScene.update(token);
243
+ scenePass.update(token);
244
+ },
245
+ encodeComputePrep(enc: GPUCommandEncoder, token: AdaptiveToken): void {
246
+ heapScene.encodeComputePrep(enc, token);
247
+ },
248
+ encodeIntoPass(passEnc: GPURenderPassEncoder, token: AdaptiveToken): void {
249
+ heapScene.encodeIntoPass(passEnc);
250
+ scenePass.encodeIntoPass(passEnc, token);
251
+ },
252
+ hasDraws(): boolean {
253
+ // Heap path tracks count in stats; legacy path's leaf count is
254
+ // a cheap walker-tree scan when empty (collect into a fresh
255
+ // array). The sentinel here is "both empty" — caller decides
256
+ // whether a clear-only pass is still worth opening.
257
+ if (heapScene.stats.totalDraws > 0) return true;
258
+ return scenePass.collect().length > 0;
259
+ },
260
+ heapBucketCount(): number {
261
+ return heapScene.stats.groups;
262
+ },
263
+ heapDerivedTimings() {
264
+ const s = heapScene.stats;
265
+ return {
266
+ pullMs: s.derivedPullMs,
267
+ uploadMs: s.derivedUploadMs,
268
+ encodeMs: s.derivedEncodeMs,
269
+ records: s.derivedRecords,
270
+ };
271
+ },
272
+ validateHeap() {
273
+ return (heapScene as unknown as { _debug: { validateHeap(): Promise<{
274
+ arenaBytes: number; issues: string[];
275
+ okRefs: number; badRefs: number;
276
+ drawTableRows: number; drawTableErrs: number; prefixSumErrs: number;
277
+ attrAllocsChecked: number; attrAllocsBad: number;
278
+ tilesChecked: number; tilesBad: number;
279
+ vidChecks: number; vidBad: number;
280
+ indicesHash: string;
281
+ }> } })._debug.validateHeap();
282
+ },
283
+ simulateDraws(samples?: number) {
284
+ return (heapScene as unknown as { _debug: { simulateDraws(s?: number): Promise<{
285
+ emitsChecked: number; oob: number; issues: string[];
286
+ }> } })._debug.simulateDraws(samples);
287
+ },
288
+ probeBinarySearch(samples?: number) {
289
+ return (heapScene as unknown as { _debug: { probeBinarySearch(s?: number): Promise<{
290
+ emitsChecked: number; gpuMismatches: number; issues: string[];
291
+ }> } })._debug.probeBinarySearch(samples);
292
+ },
293
+ checkTriangleCoherence(samples?: number) {
294
+ return (heapScene as unknown as { _debug: { checkTriangleCoherence(s?: number): Promise<{
295
+ trianglesChecked: number; crossSlot: number; issues: string[];
296
+ }> } })._debug.checkTriangleCoherence(samples);
297
+ },
298
+ dispose(): void {
299
+ heapScene.dispose();
300
+ scenePass.dispose();
301
+ atlasPool.dispose();
302
+ },
303
+ };
304
+ }
305
+
306
+ // `aset` reference keeps the import live for documentation comments.
307
+ const _asetTypeGuard: aset<unknown> | undefined = undefined;
308
+ void _asetTypeGuard;
309
+ // Same for ASet: re-exported here as a hint (the helper uses ASet.* internally).
310
+ void ASet;
@@ -33,4 +33,48 @@ export {
33
33
  type BuildHeapSceneOptions,
34
34
  } from "./heapScene.js";
35
35
 
36
- export { compileHeapFragment } from "./heapShader.js";
36
+ export {
37
+ compileHeapEffect,
38
+ type CompiledHeapEffect,
39
+ type FragmentOutputLayout,
40
+ } from "./heapEffect.js";
41
+
42
+ export { flattenRenderTree } from "./flattenTree.js";
43
+ export { isHeapEligible } from "./heapEligibility.js";
44
+ export { renderObjectToHeapSpec } from "./heapAdapter.js";
45
+
46
+ export {
47
+ compileHybridScene,
48
+ type CompileHybridSceneOptions,
49
+ type HybridScene,
50
+ } from "./hybridScene.js";
51
+
52
+ // ─── Texture atlas (heap path Tier-S texture sharing) ───────────────
53
+ export {
54
+ BvhTree2d,
55
+ SPLIT_LIMIT_DEFAULT,
56
+ } from "./textureAtlas/bvhTree2d.js";
57
+
58
+ export {
59
+ TexturePacking,
60
+ empty as emptyTexturePacking,
61
+ isEmpty as isEmptyTexturePacking,
62
+ count as countTexturePacking,
63
+ occupancy as texturePackingOccupancy,
64
+ square as squareTexturePacking,
65
+ tryAddMany as texturePackingTryAddMany,
66
+ tryOfArray as texturePackingTryOfArray,
67
+ } from "./textureAtlas/packer.js";
68
+
69
+ export {
70
+ AtlasPool,
71
+ ATLAS_PAGE_SIZE,
72
+ ATLAS_MAX_DIM,
73
+ ATLAS_MAX_PAGES_PER_FORMAT,
74
+ ATLAS_PAGE_FORMATS,
75
+ atlasFormatIndex,
76
+ type AtlasPage,
77
+ type AtlasPageFormat,
78
+ type AtlasAcquisition,
79
+ type AtlasSource,
80
+ } from "./textureAtlas/atlasPool.js";
@@ -26,7 +26,7 @@ import {
26
26
  import { beginPassDescriptor, clear } from "../commands/index.js";
27
27
  import { AdaptiveToken, type alist, type aval } from "@aardworx/wombat.adaptive";
28
28
  import { copy } from "./copy.js";
29
- import { ScenePass } from "./scenePass.js";
29
+ import { compileHybridScene, type HybridScene } from "./hybridScene.js";
30
30
 
31
31
  export interface RuntimeContext {
32
32
  readonly device: GPUDevice;
@@ -37,50 +37,177 @@ export interface RuntimeContext {
37
37
  * `linkFragmentOutputs` pass uses it to re-pin and DCE outputs.
38
38
  */
39
39
  readonly compileEffect: (effect: Effect, signature: FramebufferSignature) => CompiledEffect;
40
+ /**
41
+ * Global heap on/off toggle propagated to every `HybridScene`
42
+ * compiled against this context. Flipping it causes every RO to
43
+ * migrate between the heap subset and the legacy subset.
44
+ * Useful for A/B perf comparisons.
45
+ */
46
+ readonly heapEnabled?: aval<boolean>;
47
+ /**
48
+ * §6 family-merge opt-in — propagated to every HybridScene
49
+ * compiled against this context. Default: merge off.
50
+ */
51
+ readonly enableFamilyMerge?: boolean;
52
+ /**
53
+ * §7 derived-uniforms opt-in — propagated to every HybridScene
54
+ * compiled against this context.
55
+ */
56
+ readonly enableDerivedUniforms?: boolean;
40
57
  }
41
58
 
42
59
  class RenderTask implements IRenderTask {
43
60
  /**
44
- * Cache of `ScenePass`es keyed on the `Render` command itself.
45
- * Holding ScenePass per command means the same `RenderTree`
46
- * reused across frames keeps its incrementally-maintained
47
- * walker state.
61
+ * Cache of `HybridScene`s keyed on the `Render` command itself.
62
+ * Each command compiles to a hybrid scene composing the heap-
63
+ * bucket fast path with the legacy per-RO path; reuse across
64
+ * frames preserves both backends' incremental state.
48
65
  */
49
- private readonly _scenes = new Map<unknown, ScenePass>();
66
+ private readonly _scenes = new Map<unknown, HybridScene>();
50
67
  private _disposed = false;
51
68
 
52
69
  constructor(
53
70
  private readonly ctx: RuntimeContext,
71
+ readonly signature: FramebufferSignature,
54
72
  private readonly commands: alist<Command>,
55
73
  ) {}
56
74
 
57
- run(token: AdaptiveToken): void {
75
+ run(framebuffer: IFramebuffer, token: AdaptiveToken): void {
58
76
  if (this._disposed) throw new Error("RenderTask: run after dispose");
59
77
  const enc = this.ctx.device.createCommandEncoder();
60
- this.encode(enc, token);
78
+ this.encode(enc, framebuffer, token);
61
79
  this.ctx.device.queue.submit([enc.finish()]);
62
80
  }
63
81
 
64
- encode(enc: GPUCommandEncoder, token: AdaptiveToken): void {
82
+ encode(enc: GPUCommandEncoder, framebuffer: IFramebuffer, token: AdaptiveToken): void {
65
83
  if (this._disposed) throw new Error("RenderTask: encode after dispose");
66
84
  RenderContext.withEncoder(enc, () => {
67
85
  const arr: Command[] = [];
68
86
  for (const c of this.commands.content.getValue(token)) arr.push(c);
69
87
  for (let i = 0; i < arr.length; i++) {
70
88
  const c = arr[i]!;
89
+ // Coalesce a Clear immediately followed by a Render: both
90
+ // share the run-arg framebuffer now (no per-cmd output to
91
+ // disambiguate), so the merge is unconditional when the
92
+ // pair is adjacent.
71
93
  if (c.kind === "Clear") {
72
94
  const next = arr[i + 1];
73
- if (next !== undefined && next.kind === "Render" && next.output === c.output) {
74
- this.encodeRenderCommand(enc, next, token, c.values);
95
+ if (next !== undefined && next.kind === "Render") {
96
+ this.encodeRenderCommand(enc, next, framebuffer, token, c.values);
75
97
  i++;
76
98
  continue;
77
99
  }
78
100
  }
79
- this.encodeCommand(enc, c, token);
101
+ this.encodeCommand(enc, c, framebuffer, token);
80
102
  }
81
103
  });
82
104
  }
83
105
 
106
+ heapBucketCount(): number {
107
+ let total = 0;
108
+ for (const s of this._scenes.values()) total += s.heapBucketCount();
109
+ return total;
110
+ }
111
+
112
+ heapDerivedTimings(): {
113
+ pullMs: number; uploadMs: number; encodeMs: number; records: number;
114
+ } {
115
+ let pullMs = 0, uploadMs = 0, encodeMs = 0, records = 0;
116
+ for (const s of this._scenes.values()) {
117
+ const t = s.heapDerivedTimings();
118
+ pullMs += t.pullMs;
119
+ uploadMs += t.uploadMs;
120
+ encodeMs += t.encodeMs;
121
+ records += t.records;
122
+ }
123
+ return { pullMs, uploadMs, encodeMs, records };
124
+ }
125
+
126
+ async validateHeap(): Promise<{
127
+ arenaBytes: number; issues: string[];
128
+ okRefs: number; badRefs: number;
129
+ drawTableRows: number; drawTableErrs: number; prefixSumErrs: number;
130
+ attrAllocsChecked: number; attrAllocsBad: number;
131
+ tilesChecked: number; tilesBad: number;
132
+ vidChecks: number; vidBad: number;
133
+ indicesHash: string;
134
+ }> {
135
+ let arenaBytes = 0, okRefs = 0, badRefs = 0;
136
+ let drawTableRows = 0, drawTableErrs = 0, prefixSumErrs = 0;
137
+ let attrAllocsChecked = 0, attrAllocsBad = 0;
138
+ let tilesChecked = 0, tilesBad = 0;
139
+ let vidChecks = 0, vidBad = 0;
140
+ const indicesHashes: string[] = [];
141
+ const issues: string[] = [];
142
+ for (const s of this._scenes.values()) {
143
+ const r = await s.validateHeap();
144
+ arenaBytes += r.arenaBytes;
145
+ okRefs += r.okRefs;
146
+ badRefs += r.badRefs;
147
+ drawTableRows += r.drawTableRows;
148
+ drawTableErrs += r.drawTableErrs;
149
+ prefixSumErrs += r.prefixSumErrs;
150
+ attrAllocsChecked += r.attrAllocsChecked;
151
+ attrAllocsBad += r.attrAllocsBad;
152
+ tilesChecked += r.tilesChecked;
153
+ tilesBad += r.tilesBad;
154
+ vidChecks += r.vidChecks;
155
+ vidBad += r.vidBad;
156
+ indicesHashes.push(r.indicesHash);
157
+ for (const i of r.issues) issues.push(i);
158
+ }
159
+ return {
160
+ arenaBytes, issues, okRefs, badRefs,
161
+ drawTableRows, drawTableErrs, prefixSumErrs,
162
+ attrAllocsChecked, attrAllocsBad,
163
+ tilesChecked, tilesBad,
164
+ vidChecks, vidBad,
165
+ indicesHash: indicesHashes.join(","),
166
+ };
167
+ }
168
+
169
+ async simulateDraws(samples?: number): Promise<{
170
+ emitsChecked: number; oob: number; issues: string[];
171
+ }> {
172
+ let emitsChecked = 0, oob = 0;
173
+ const issues: string[] = [];
174
+ for (const s of this._scenes.values()) {
175
+ const r = await s.simulateDraws(samples);
176
+ emitsChecked += r.emitsChecked;
177
+ oob += r.oob;
178
+ for (const i of r.issues) issues.push(i);
179
+ }
180
+ return { emitsChecked, oob, issues };
181
+ }
182
+
183
+ async probeBinarySearch(samples?: number): Promise<{
184
+ emitsChecked: number; gpuMismatches: number; issues: string[];
185
+ }> {
186
+ let emitsChecked = 0, gpuMismatches = 0;
187
+ const issues: string[] = [];
188
+ for (const s of this._scenes.values()) {
189
+ const r = await s.probeBinarySearch(samples);
190
+ emitsChecked += r.emitsChecked;
191
+ gpuMismatches += r.gpuMismatches;
192
+ for (const i of r.issues) issues.push(i);
193
+ }
194
+ return { emitsChecked, gpuMismatches, issues };
195
+ }
196
+
197
+ async checkTriangleCoherence(samples?: number): Promise<{
198
+ trianglesChecked: number; crossSlot: number; issues: string[];
199
+ }> {
200
+ let trianglesChecked = 0, crossSlot = 0;
201
+ const issues: string[] = [];
202
+ for (const s of this._scenes.values()) {
203
+ const r = await s.checkTriangleCoherence(samples);
204
+ trianglesChecked += r.trianglesChecked;
205
+ crossSlot += r.crossSlot;
206
+ for (const i of r.issues) issues.push(i);
207
+ }
208
+ return { trianglesChecked, crossSlot, issues };
209
+ }
210
+
84
211
  dispose(): void {
85
212
  if (this._disposed) return;
86
213
  for (const s of this._scenes.values()) s.dispose();
@@ -88,54 +215,62 @@ class RenderTask implements IRenderTask {
88
215
  this._disposed = true;
89
216
  }
90
217
 
91
- private encodeCommand(enc: GPUCommandEncoder, c: Command, token: AdaptiveToken): void {
218
+ private encodeCommand(
219
+ enc: GPUCommandEncoder, c: Command, framebuffer: IFramebuffer, token: AdaptiveToken,
220
+ ): void {
92
221
  switch (c.kind) {
93
- case "Clear": clear(enc, c.output.getValue(token), c.values); return;
222
+ case "Clear": clear(enc, framebuffer, c.values); return;
94
223
  case "Copy": copy(enc, c.copy); return;
95
224
  case "Custom": c.encode(enc); return;
96
- case "Render": this.encodeRenderCommand(enc, c, token); return;
225
+ case "Render": this.encodeRenderCommand(enc, c, framebuffer, token); return;
97
226
  }
98
227
  }
99
228
 
100
229
  private encodeRenderCommand(
101
230
  enc: GPUCommandEncoder,
102
231
  cmd: Extract<Command, { kind: "Render" }>,
232
+ framebuffer: IFramebuffer,
103
233
  token: AdaptiveToken,
104
234
  clearValues?: ClearValues,
105
235
  ): void {
106
- const output = cmd.output.getValue(token);
107
- const scene = this.sceneFor(cmd, output, cmd.tree);
108
- const leaves = scene.resolve(token);
109
- if (leaves.length === 0 && clearValues === undefined) return;
236
+ const scene = this.sceneFor(cmd, cmd.tree);
237
+ scene.update(token);
238
+ if (!scene.hasDraws() && clearValues === undefined) return;
239
+ // Compute prep (drawTable prefix-sum) must happen outside any
240
+ // render pass — encode it before we open the pass.
241
+ scene.encodeComputePrep(enc, token);
110
242
  // Either we have draws or we need to clear — open a single pass.
111
- const pass = enc.beginRenderPass(beginPassDescriptor(output, clearValues));
112
- for (const leaf of leaves) leaf.record(pass, token);
243
+ const pass = enc.beginRenderPass(beginPassDescriptor(framebuffer, clearValues));
244
+ scene.encodeIntoPass(pass, token);
113
245
  pass.end();
114
246
  }
115
247
 
116
248
  private sceneFor(
117
249
  cmd: Extract<Command, { kind: "Render" }>,
118
- output: IFramebuffer,
119
250
  tree: RenderTree,
120
- ): ScenePass {
251
+ ): HybridScene {
121
252
  let s = this._scenes.get(cmd);
122
253
  if (s === undefined) {
123
- s = new ScenePass(this.ctx.device, output.signature, tree, this.ctx.compileEffect);
254
+ s = compileHybridScene(this.ctx.device, this.signature, tree, {
255
+ compileEffect: this.ctx.compileEffect,
256
+ ...(this.ctx.heapEnabled !== undefined ? { heapEnabled: this.ctx.heapEnabled } : {}),
257
+ ...(this.ctx.enableFamilyMerge === true ? { enableFamilyMerge: true } : {}),
258
+ ...(this.ctx.enableDerivedUniforms === true ? { enableDerivedUniforms: true } : {}),
259
+ });
124
260
  this._scenes.set(cmd, s);
125
261
  }
126
262
  return s;
127
- // Note: `output.signature` is read here only to seed the
128
- // ScenePass; subsequent frames continue to use this signature
129
- // even if the framebuffer aval emits a different sig. That
130
- // matches the `(RenderObject, signature)` cache invariant —
131
- // changing signature requires a fresh Render command.
132
263
  }
133
264
  }
134
265
 
135
- export function compileRenderTask(ctx: RuntimeContext, commands: alist<Command>): IRenderTask & {
136
- encode(enc: GPUCommandEncoder, token: AdaptiveToken): void;
266
+ export function compileRenderTask(
267
+ ctx: RuntimeContext,
268
+ signature: FramebufferSignature,
269
+ commands: alist<Command>,
270
+ ): IRenderTask & {
271
+ encode(enc: GPUCommandEncoder, framebuffer: IFramebuffer, token: AdaptiveToken): void;
137
272
  } {
138
- return new RenderTask(ctx, commands);
273
+ return new RenderTask(ctx, signature, commands);
139
274
  }
140
275
 
141
276
  // `RenderTree` carries an `aval<RenderTree>` inside its `Adaptive`