@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.
- package/dist/core/command.d.ts +7 -9
- package/dist/core/command.d.ts.map +1 -1
- package/dist/core/renderTask.d.ts +64 -1
- package/dist/core/renderTask.d.ts.map +1 -1
- package/dist/core/renderTask.js +3 -1
- package/dist/core/renderTask.js.map +1 -1
- package/dist/resources/preparedRenderObject.js +3 -3
- package/dist/resources/preparedRenderObject.js.map +1 -1
- package/dist/resources/uniformBuffer.d.ts.map +1 -1
- package/dist/resources/uniformBuffer.js +17 -0
- package/dist/resources/uniformBuffer.js.map +1 -1
- package/dist/runtime/derivedUniforms/dispatch.d.ts +105 -0
- package/dist/runtime/derivedUniforms/dispatch.d.ts.map +1 -0
- package/dist/runtime/derivedUniforms/dispatch.js +355 -0
- package/dist/runtime/derivedUniforms/dispatch.js.map +1 -0
- package/dist/runtime/derivedUniforms/index.d.ts +6 -0
- package/dist/runtime/derivedUniforms/index.d.ts.map +1 -0
- package/dist/runtime/derivedUniforms/index.js +26 -0
- package/dist/runtime/derivedUniforms/index.js.map +1 -0
- package/dist/runtime/derivedUniforms/recipes.d.ts +42 -0
- package/dist/runtime/derivedUniforms/recipes.d.ts.map +1 -0
- package/dist/runtime/derivedUniforms/recipes.js +134 -0
- package/dist/runtime/derivedUniforms/recipes.js.map +1 -0
- package/dist/runtime/derivedUniforms/sceneIntegration.d.ts +56 -0
- package/dist/runtime/derivedUniforms/sceneIntegration.d.ts.map +1 -0
- package/dist/runtime/derivedUniforms/sceneIntegration.js +171 -0
- package/dist/runtime/derivedUniforms/sceneIntegration.js.map +1 -0
- package/dist/runtime/derivedUniforms/slotId.d.ts +15 -0
- package/dist/runtime/derivedUniforms/slotId.d.ts.map +1 -0
- package/dist/runtime/derivedUniforms/slotId.js +30 -0
- package/dist/runtime/derivedUniforms/slotId.js.map +1 -0
- package/dist/runtime/derivedUniforms/slots.d.ts +49 -0
- package/dist/runtime/derivedUniforms/slots.d.ts.map +1 -0
- package/dist/runtime/derivedUniforms/slots.js +123 -0
- package/dist/runtime/derivedUniforms/slots.js.map +1 -0
- package/dist/runtime/derivedUniforms/uberKernel.wgsl.d.ts +13 -0
- package/dist/runtime/derivedUniforms/uberKernel.wgsl.d.ts.map +1 -0
- package/dist/runtime/derivedUniforms/uberKernel.wgsl.js +218 -0
- package/dist/runtime/derivedUniforms/uberKernel.wgsl.js.map +1 -0
- package/dist/runtime/flattenTree.d.ts +5 -0
- package/dist/runtime/flattenTree.d.ts.map +1 -0
- package/dist/runtime/flattenTree.js +45 -0
- package/dist/runtime/flattenTree.js.map +1 -0
- package/dist/runtime/heapAdapter.d.ts +16 -0
- package/dist/runtime/heapAdapter.d.ts.map +1 -0
- package/dist/runtime/heapAdapter.js +263 -0
- package/dist/runtime/heapAdapter.js.map +1 -0
- package/dist/runtime/heapEffect.d.ts +235 -0
- package/dist/runtime/heapEffect.d.ts.map +1 -0
- package/dist/runtime/heapEffect.js +529 -0
- package/dist/runtime/heapEffect.js.map +1 -0
- package/dist/runtime/heapEffectIR.d.ts +34 -0
- package/dist/runtime/heapEffectIR.d.ts.map +1 -0
- package/dist/runtime/heapEffectIR.js +1161 -0
- package/dist/runtime/heapEffectIR.js.map +1 -0
- package/dist/runtime/heapEligibility.d.ts +9 -0
- package/dist/runtime/heapEligibility.d.ts.map +1 -0
- package/dist/runtime/heapEligibility.js +195 -0
- package/dist/runtime/heapEligibility.js.map +1 -0
- package/dist/runtime/heapScene.d.ts +240 -31
- package/dist/runtime/heapScene.d.ts.map +1 -1
- package/dist/runtime/heapScene.js +3334 -265
- package/dist/runtime/heapScene.js.map +1 -1
- package/dist/runtime/heapShaderFamily.d.ts +132 -0
- package/dist/runtime/heapShaderFamily.d.ts.map +1 -0
- package/dist/runtime/heapShaderFamily.js +866 -0
- package/dist/runtime/heapShaderFamily.js.map +1 -0
- package/dist/runtime/hybridScene.d.ts +133 -0
- package/dist/runtime/hybridScene.d.ts.map +1 -0
- package/dist/runtime/hybridScene.js +159 -0
- package/dist/runtime/hybridScene.js.map +1 -0
- package/dist/runtime/index.d.ts +8 -1
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +9 -1
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/renderTask.d.ts +21 -4
- package/dist/runtime/renderTask.d.ts.map +1 -1
- package/dist/runtime/renderTask.js +127 -32
- package/dist/runtime/renderTask.js.map +1 -1
- package/dist/runtime/runtime.d.ts +33 -3
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +12 -3
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/scenePass.d.ts +28 -3
- package/dist/runtime/scenePass.d.ts.map +1 -1
- package/dist/runtime/scenePass.js +37 -4
- package/dist/runtime/scenePass.js.map +1 -1
- package/dist/runtime/textureAtlas/atlasMipGutterKernel.d.ts +35 -0
- package/dist/runtime/textureAtlas/atlasMipGutterKernel.d.ts.map +1 -0
- package/dist/runtime/textureAtlas/atlasMipGutterKernel.js +247 -0
- package/dist/runtime/textureAtlas/atlasMipGutterKernel.js.map +1 -0
- package/dist/runtime/textureAtlas/atlasPool.d.ts +240 -0
- package/dist/runtime/textureAtlas/atlasPool.d.ts.map +1 -0
- package/dist/runtime/textureAtlas/atlasPool.js +798 -0
- package/dist/runtime/textureAtlas/atlasPool.js.map +1 -0
- package/dist/runtime/textureAtlas/bvhTree2d.d.ts +33 -0
- package/dist/runtime/textureAtlas/bvhTree2d.d.ts.map +1 -0
- package/dist/runtime/textureAtlas/bvhTree2d.js +501 -0
- package/dist/runtime/textureAtlas/bvhTree2d.js.map +1 -0
- package/dist/runtime/textureAtlas/packer.d.ts +50 -0
- package/dist/runtime/textureAtlas/packer.d.ts.map +1 -0
- package/dist/runtime/textureAtlas/packer.js +390 -0
- package/dist/runtime/textureAtlas/packer.js.map +1 -0
- package/dist/window/loop.d.ts +14 -0
- package/dist/window/loop.d.ts.map +1 -1
- package/dist/window/loop.js +18 -5
- package/dist/window/loop.js.map +1 -1
- package/package.json +6 -5
- package/src/core/command.ts +9 -9
- package/src/core/renderTask.ts +55 -2
- package/src/resources/preparedRenderObject.ts +3 -3
- package/src/resources/uniformBuffer.ts +15 -0
- package/src/runtime/derivedUniforms/dispatch.ts +407 -0
- package/src/runtime/derivedUniforms/index.ts +52 -0
- package/src/runtime/derivedUniforms/recipes.ts +159 -0
- package/src/runtime/derivedUniforms/sceneIntegration.ts +239 -0
- package/src/runtime/derivedUniforms/slotId.ts +35 -0
- package/src/runtime/derivedUniforms/slots.ts +155 -0
- package/src/runtime/derivedUniforms/uberKernel.wgsl.ts +220 -0
- package/src/runtime/flattenTree.ts +46 -0
- package/src/runtime/heapAdapter.ts +314 -0
- package/src/runtime/heapEffect.ts +728 -0
- package/src/runtime/heapEffectIR.ts +1211 -0
- package/src/runtime/heapEligibility.ts +189 -0
- package/src/runtime/heapScene.ts +3981 -349
- package/src/runtime/heapShaderFamily.ts +1102 -0
- package/src/runtime/hybridScene.ts +310 -0
- package/src/runtime/index.ts +45 -1
- package/src/runtime/renderTask.ts +167 -32
- package/src/runtime/runtime.ts +37 -4
- package/src/runtime/scenePass.ts +39 -4
- package/src/runtime/textureAtlas/atlasMipGutterKernel.ts +291 -0
- package/src/runtime/textureAtlas/atlasPool.ts +991 -0
- package/src/runtime/textureAtlas/bvhTree2d.ts +530 -0
- package/src/runtime/textureAtlas/packer.ts +429 -0
- package/src/window/loop.ts +32 -6
- package/dist/runtime/heapShader.d.ts +0 -12
- package/dist/runtime/heapShader.d.ts.map +0 -1
- package/dist/runtime/heapShader.js +0 -157
- package/dist/runtime/heapShader.js.map +0 -1
- 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;
|
package/src/runtime/index.ts
CHANGED
|
@@ -33,4 +33,48 @@ export {
|
|
|
33
33
|
type BuildHeapSceneOptions,
|
|
34
34
|
} from "./heapScene.js";
|
|
35
35
|
|
|
36
|
-
export {
|
|
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 {
|
|
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 `
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
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,
|
|
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"
|
|
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(
|
|
218
|
+
private encodeCommand(
|
|
219
|
+
enc: GPUCommandEncoder, c: Command, framebuffer: IFramebuffer, token: AdaptiveToken,
|
|
220
|
+
): void {
|
|
92
221
|
switch (c.kind) {
|
|
93
|
-
case "Clear": clear(enc,
|
|
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
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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(
|
|
112
|
-
|
|
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
|
-
):
|
|
251
|
+
): HybridScene {
|
|
121
252
|
let s = this._scenes.get(cmd);
|
|
122
253
|
if (s === undefined) {
|
|
123
|
-
s =
|
|
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(
|
|
136
|
-
|
|
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`
|