@aardworx/wombat.rendering 0.19.24 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/elementType.d.ts +2 -0
- package/dist/core/elementType.d.ts.map +1 -1
- package/dist/core/elementType.js +10 -0
- package/dist/core/elementType.js.map +1 -1
- package/dist/core/provider.d.ts.map +1 -1
- package/dist/core/provider.js +65 -32
- package/dist/core/provider.js.map +1 -1
- package/dist/core/renderObject.d.ts +18 -0
- package/dist/core/renderObject.d.ts.map +1 -1
- package/dist/resources/preparedRenderObject.d.ts.map +1 -1
- package/dist/resources/preparedRenderObject.js +28 -5
- package/dist/resources/preparedRenderObject.js.map +1 -1
- package/dist/resources/uniformBuffer.d.ts.map +1 -1
- package/dist/resources/uniformBuffer.js +3 -1
- package/dist/resources/uniformBuffer.js.map +1 -1
- package/dist/runtime/derivedModes/modeKeyCpu.d.ts +2 -12
- package/dist/runtime/derivedModes/modeKeyCpu.d.ts.map +1 -1
- package/dist/runtime/derivedModes/modeKeyCpu.js +30 -13
- package/dist/runtime/derivedModes/modeKeyCpu.js.map +1 -1
- package/dist/runtime/derivedModes/partitionDispatcher.d.ts +11 -0
- package/dist/runtime/derivedModes/partitionDispatcher.d.ts.map +1 -1
- package/dist/runtime/derivedModes/partitionDispatcher.js +31 -0
- package/dist/runtime/derivedModes/partitionDispatcher.js.map +1 -1
- package/dist/runtime/derivedUniforms/codegen.d.ts.map +1 -1
- package/dist/runtime/derivedUniforms/codegen.js +34 -13
- package/dist/runtime/derivedUniforms/codegen.js.map +1 -1
- package/dist/runtime/derivedUniforms/dispatch.d.ts.map +1 -1
- package/dist/runtime/derivedUniforms/dispatch.js +6 -0
- package/dist/runtime/derivedUniforms/dispatch.js.map +1 -1
- package/dist/runtime/derivedUniforms/records.d.ts +15 -0
- package/dist/runtime/derivedUniforms/records.d.ts.map +1 -1
- package/dist/runtime/derivedUniforms/records.js +81 -14
- package/dist/runtime/derivedUniforms/records.js.map +1 -1
- package/dist/runtime/derivedUniforms/sceneIntegration.d.ts +6 -0
- package/dist/runtime/derivedUniforms/sceneIntegration.d.ts.map +1 -1
- package/dist/runtime/derivedUniforms/sceneIntegration.js +18 -4
- package/dist/runtime/derivedUniforms/sceneIntegration.js.map +1 -1
- package/dist/runtime/flattenTree.d.ts.map +1 -1
- package/dist/runtime/flattenTree.js +46 -2
- package/dist/runtime/flattenTree.js.map +1 -1
- package/dist/runtime/heapAdapter.d.ts +3 -1
- package/dist/runtime/heapAdapter.d.ts.map +1 -1
- package/dist/runtime/heapAdapter.js +45 -4
- package/dist/runtime/heapAdapter.js.map +1 -1
- package/dist/runtime/heapDecoder.d.ts.map +1 -1
- package/dist/runtime/heapDecoder.js +8 -3
- package/dist/runtime/heapDecoder.js.map +1 -1
- package/dist/runtime/heapEffect.d.ts +36 -1
- package/dist/runtime/heapEffect.d.ts.map +1 -1
- package/dist/runtime/heapEffect.js +29 -2
- package/dist/runtime/heapEffect.js.map +1 -1
- package/dist/runtime/heapEffectIR.d.ts.map +1 -1
- package/dist/runtime/heapEffectIR.js +51 -11
- package/dist/runtime/heapEffectIR.js.map +1 -1
- package/dist/runtime/heapEligibility.d.ts.map +1 -1
- package/dist/runtime/heapEligibility.js +64 -12
- package/dist/runtime/heapEligibility.js.map +1 -1
- package/dist/runtime/heapIrBuilders.d.ts +8 -0
- package/dist/runtime/heapIrBuilders.d.ts.map +1 -1
- package/dist/runtime/heapIrBuilders.js +47 -2
- package/dist/runtime/heapIrBuilders.js.map +1 -1
- package/dist/runtime/heapScene/chunkedArena.d.ts +20 -28
- package/dist/runtime/heapScene/chunkedArena.d.ts.map +1 -1
- package/dist/runtime/heapScene/chunkedArena.js +28 -91
- package/dist/runtime/heapScene/chunkedArena.js.map +1 -1
- package/dist/runtime/heapScene/freelist.d.ts +1 -1
- package/dist/runtime/heapScene/freelist.js +3 -3
- package/dist/runtime/heapScene/freelist.js.map +1 -1
- package/dist/runtime/heapScene/pools.d.ts +103 -59
- package/dist/runtime/heapScene/pools.d.ts.map +1 -1
- package/dist/runtime/heapScene/pools.js +335 -194
- package/dist/runtime/heapScene/pools.js.map +1 -1
- package/dist/runtime/heapScene.d.ts +125 -1
- package/dist/runtime/heapScene.d.ts.map +1 -1
- package/dist/runtime/heapScene.js +671 -126
- package/dist/runtime/heapScene.js.map +1 -1
- package/dist/runtime/heapShaderFamily.d.ts.map +1 -1
- package/dist/runtime/heapShaderFamily.js +19 -0
- package/dist/runtime/heapShaderFamily.js.map +1 -1
- package/dist/runtime/hybridScene.d.ts +6 -0
- package/dist/runtime/hybridScene.d.ts.map +1 -1
- package/dist/runtime/hybridScene.js +36 -3
- package/dist/runtime/hybridScene.js.map +1 -1
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/renderTask.d.ts +6 -0
- package/dist/runtime/renderTask.d.ts.map +1 -1
- package/dist/runtime/renderTask.js +3 -0
- package/dist/runtime/renderTask.js.map +1 -1
- package/dist/runtime/runtime.d.ts +15 -0
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +3 -0
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/textureAtlas/atlasMipGutterKernel.d.ts +19 -2
- package/dist/runtime/textureAtlas/atlasMipGutterKernel.d.ts.map +1 -1
- package/dist/runtime/textureAtlas/atlasMipGutterKernel.js +37 -6
- package/dist/runtime/textureAtlas/atlasMipGutterKernel.js.map +1 -1
- package/dist/runtime/textureAtlas/atlasPool.d.ts +5 -0
- package/dist/runtime/textureAtlas/atlasPool.d.ts.map +1 -1
- package/dist/runtime/textureAtlas/atlasPool.js +25 -3
- package/dist/runtime/textureAtlas/atlasPool.js.map +1 -1
- package/dist/window/loop.d.ts.map +1 -1
- package/dist/window/loop.js +31 -4
- package/dist/window/loop.js.map +1 -1
- package/package.json +3 -3
- package/src/core/elementType.ts +10 -0
- package/src/core/provider.ts +56 -28
- package/src/core/renderObject.ts +18 -0
- package/src/resources/preparedRenderObject.ts +29 -6
- package/src/resources/uniformBuffer.ts +3 -1
- package/src/runtime/derivedModes/modeKeyCpu.ts +26 -11
- package/src/runtime/derivedModes/partitionDispatcher.ts +30 -0
- package/src/runtime/derivedUniforms/codegen.ts +34 -13
- package/src/runtime/derivedUniforms/dispatch.ts +6 -0
- package/src/runtime/derivedUniforms/records.ts +70 -15
- package/src/runtime/derivedUniforms/sceneIntegration.ts +19 -4
- package/src/runtime/flattenTree.ts +47 -2
- package/src/runtime/heapAdapter.ts +43 -5
- package/src/runtime/heapDecoder.ts +8 -3
- package/src/runtime/heapEffect.ts +44 -2
- package/src/runtime/heapEffectIR.ts +54 -11
- package/src/runtime/heapEligibility.ts +39 -12
- package/src/runtime/heapIrBuilders.ts +50 -2
- package/src/runtime/heapScene/chunkedArena.ts +29 -94
- package/src/runtime/heapScene/freelist.ts +3 -3
- package/src/runtime/heapScene/pools.ts +340 -218
- package/src/runtime/heapScene.ts +826 -142
- package/src/runtime/heapShaderFamily.ts +21 -0
- package/src/runtime/hybridScene.ts +43 -3
- package/src/runtime/index.ts +5 -0
- package/src/runtime/renderTask.ts +9 -0
- package/src/runtime/runtime.ts +18 -0
- package/src/runtime/textureAtlas/atlasMipGutterKernel.ts +52 -3
- package/src/runtime/textureAtlas/atlasPool.ts +28 -2
- package/src/window/loop.ts +32 -4
package/src/core/renderObject.ts
CHANGED
|
@@ -63,10 +63,28 @@ export interface RenderObject {
|
|
|
63
63
|
readonly samplers: HashMap<string, aval<ISampler>>;
|
|
64
64
|
/** name → storage buffer source. */
|
|
65
65
|
readonly storageBuffers?: HashMap<string, aval<IBuffer>>;
|
|
66
|
+
/**
|
|
67
|
+
* Scene-level INJECTED storage buffers (e.g. the OIT node pool the
|
|
68
|
+
* transparency task threads into every transparent leaf). Unlike
|
|
69
|
+
* `storageBuffers`, these do NOT disqualify the RO from the heap
|
|
70
|
+
* path — the heap binds them once per bucket (they are shared by
|
|
71
|
+
* construction). The classic path treats them exactly like
|
|
72
|
+
* `storageBuffers` (leaf-own entries win on name conflict).
|
|
73
|
+
*/
|
|
74
|
+
readonly injectedStorage?: HashMap<string, aval<IBuffer>>;
|
|
66
75
|
|
|
67
76
|
/** Index buffer for indexed draws. */
|
|
68
77
|
readonly indices?: BufferView;
|
|
69
78
|
readonly drawCall: aval<DrawCall>;
|
|
79
|
+
/**
|
|
80
|
+
* Optional picking id (u32 < 2^24). First-class instead of a
|
|
81
|
+
* `PickId` uniform: one unique `AVal.constant` per RO would drag a
|
|
82
|
+
* pool entry + per-RO provider identity + adaptive bookkeeping in —
|
|
83
|
+
* measured ~6.5 KB of JS heap PER RO at scale. The heap path writes
|
|
84
|
+
* this INLINE into the drawHeader (like `__layoutId`); the classic
|
|
85
|
+
* path synthesizes the `PickId` uniform from it.
|
|
86
|
+
*/
|
|
87
|
+
readonly pickId?: number;
|
|
70
88
|
/**
|
|
71
89
|
* Optional gate: when `active` ticks to `false`, the runtime must
|
|
72
90
|
* skip drawing this RO this frame **without** releasing pool
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// PreparedRenderObject so flipping a non-pipeline-affecting field
|
|
13
13
|
// (`stencilReference`, `blendConstant`) does NOT trigger a recompile.
|
|
14
14
|
|
|
15
|
-
import {
|
|
15
|
+
import { type IUniformProvider,
|
|
16
16
|
AdaptiveResource,
|
|
17
17
|
AttributeProvider,
|
|
18
18
|
BufferView,
|
|
@@ -340,7 +340,8 @@ function buildPipelineForSnap(
|
|
|
340
340
|
}
|
|
341
341
|
: undefined;
|
|
342
342
|
const desc: CompileRenderPipelineDescription = {
|
|
343
|
-
|
|
343
|
+
// default label = the effect identity — attributable GPU debugging
|
|
344
|
+
label: ctx.label ?? `classic/${ctx.effectId ?? "anon"}`,
|
|
344
345
|
...(ctx.effectId !== undefined ? { effectId: ctx.effectId } : {}),
|
|
345
346
|
vertexShaderSource: ctx.vsSource,
|
|
346
347
|
fragmentShaderSource: ctx.fsSource,
|
|
@@ -736,8 +737,16 @@ export function prepareRenderObject(
|
|
|
736
737
|
const perGroup: EntryDesc[][] = [];
|
|
737
738
|
for (let g = 0; g <= maxGroup; g++) perGroup.push([]);
|
|
738
739
|
|
|
740
|
+
// First-class `RenderObject.pickId` → synthesized `PickId` uniform on
|
|
741
|
+
// the classic path (the heap path writes it inline into the
|
|
742
|
+
// drawHeader instead — see INLINE_HEADER_UNIFORMS). Classic scenes
|
|
743
|
+
// are small, so a per-RO constant aval is fine here.
|
|
744
|
+
const uniformSource: IUniformProvider = obj.pickId !== undefined
|
|
745
|
+
? withPickId(obj.pickId, asUniformProvider(obj.uniforms))
|
|
746
|
+
: asUniformProvider(obj.uniforms);
|
|
747
|
+
|
|
739
748
|
for (const ub of iface.uniformBlocks) {
|
|
740
|
-
const merged = mergeUniformInputs(
|
|
749
|
+
const merged = mergeUniformInputs(uniformSource, effect.avalBindings, ub);
|
|
741
750
|
const block = ubAsBlock(ub);
|
|
742
751
|
const res = prepareUniformBuffer(device, block, merged, {
|
|
743
752
|
...(opts.label !== undefined ? { label: `${opts.label}.${ub.name}` } : {}),
|
|
@@ -768,7 +777,7 @@ export function prepareRenderObject(
|
|
|
768
777
|
}
|
|
769
778
|
|
|
770
779
|
for (const sb of iface.storageBuffers) {
|
|
771
|
-
const av = obj.storageBuffers?.tryFind(sb.name);
|
|
780
|
+
const av = obj.storageBuffers?.tryFind(sb.name) ?? obj.injectedStorage?.tryFind(sb.name);
|
|
772
781
|
if (av === undefined) throw new Error(`prepareRenderObject: missing storage buffer "${sb.name}"`);
|
|
773
782
|
const usage = BufferUsage.STORAGE
|
|
774
783
|
| (sb.access === "read_write" ? BufferUsage.COPY_DST : 0);
|
|
@@ -811,7 +820,9 @@ export function prepareRenderObject(
|
|
|
811
820
|
// the RO's provider (resolving nested derived-uniform rules too).
|
|
812
821
|
let modeUniformReader: ((name: string, token: AdaptiveToken) => unknown) | undefined;
|
|
813
822
|
if (obj.modeRules !== undefined) {
|
|
814
|
-
const uniProv =
|
|
823
|
+
const uniProv = obj.pickId !== undefined
|
|
824
|
+
? withPickId(obj.pickId, asUniformProvider(obj.uniforms))
|
|
825
|
+
: asUniformProvider(obj.uniforms);
|
|
815
826
|
modeUniformReader = (name: string, token: AdaptiveToken): unknown => {
|
|
816
827
|
const v = uniProv.tryGet(name);
|
|
817
828
|
if (v === undefined) return undefined;
|
|
@@ -844,6 +855,15 @@ export function prepareRenderObject(
|
|
|
844
855
|
const SENTINEL_PIPELINE = { __sentinel: "PreparedRenderObject.pipeline" } as unknown as GPURenderPipeline;
|
|
845
856
|
|
|
846
857
|
// ---------------------------------------------------------------------------
|
|
858
|
+
/** Overlay provider: `PickId` first (constant), then `base`. */
|
|
859
|
+
function withPickId(pickId: number, base: IUniformProvider): IUniformProvider {
|
|
860
|
+
const pick = AVal.constant<number>(pickId);
|
|
861
|
+
return {
|
|
862
|
+
tryGet(name: string) { return name === "PickId" ? (pick as aval<unknown>) : base.tryGet(name); },
|
|
863
|
+
*names() { yield "PickId"; yield* base.names(); },
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
|
|
847
867
|
// Uniform input merging — `obj.uniforms` wins; `avalBindings` is fallback.
|
|
848
868
|
// ---------------------------------------------------------------------------
|
|
849
869
|
|
|
@@ -867,7 +887,10 @@ function mergeUniformInputs(
|
|
|
867
887
|
// here just as the heap path derives it on the GPU, so the legacy path
|
|
868
888
|
// serves the same declared uniforms.
|
|
869
889
|
const recipe = STANDARD_DERIVED_RULES.get(f.name);
|
|
870
|
-
if (recipe !== undefined) merged = merged.add(f.name, ruleToAval(recipe, user, bindings));
|
|
890
|
+
if (recipe !== undefined) { merged = merged.add(f.name, ruleToAval(recipe, user, bindings)); continue; }
|
|
891
|
+
// An unresolved field stays ZERO in the uniform buffer — silent garbage
|
|
892
|
+
// transforms/values downstream. Say so once per (block, field).
|
|
893
|
+
console.warn(`prepareRenderObject: uniform block field "${f.name}" resolves to undefined (buffer stays zero)`);
|
|
871
894
|
}
|
|
872
895
|
return merged;
|
|
873
896
|
}
|
|
@@ -99,7 +99,9 @@ class UniformBufferResource extends AdaptiveResource<GPUBuffer> {
|
|
|
99
99
|
const desc: GPUBufferDescriptor = {
|
|
100
100
|
size: this._scratch.bytes.byteLength,
|
|
101
101
|
usage: BufferUsage.UNIFORM | BufferUsage.COPY_DST,
|
|
102
|
-
|
|
102
|
+
// default label: the block's field names — makes GPU debugging
|
|
103
|
+
// (writeBuffer interception, buffer inspectors) attributable.
|
|
104
|
+
label: this.label ?? `ubo{${this.layout.fields.map((f) => f.name).join(",")}}`,
|
|
103
105
|
};
|
|
104
106
|
this._gpu = this.device.createBuffer(desc);
|
|
105
107
|
}
|
|
@@ -165,18 +165,32 @@ function stripFormatFor(topology: GPUPrimitiveTopology): GPUIndexFormat | undefi
|
|
|
165
165
|
* — `addMarkingCallback` is a no-op for an aval that never marks, so
|
|
166
166
|
* no special-casing required.
|
|
167
167
|
*/
|
|
168
|
+
/** Canonical descriptor per modeKey. The modeKey is a COMPLETE encoding
|
|
169
|
+
* of the descriptor (that's what makes it the bucket key), so two
|
|
170
|
+
* trackers with equal keys can share one frozen snapshot object —
|
|
171
|
+
* at heap scale most draws share a handful of pipeline states, and a
|
|
172
|
+
* per-draw descriptor (+ its attachments array) was measurable ballast.
|
|
173
|
+
* The map is bounded by the number of DISTINCT pipeline states. */
|
|
174
|
+
const descriptorInterning = new Map<bigint, PipelineStateDescriptor>();
|
|
175
|
+
function internDescriptor(key: bigint, d: PipelineStateDescriptor): PipelineStateDescriptor {
|
|
176
|
+
const hit = descriptorInterning.get(key);
|
|
177
|
+
if (hit !== undefined) return hit;
|
|
178
|
+
descriptorInterning.set(key, d);
|
|
179
|
+
return d;
|
|
180
|
+
}
|
|
181
|
+
|
|
168
182
|
export class ModeKeyTracker implements IDisposable {
|
|
169
183
|
readonly ps: PipelineState | undefined;
|
|
170
184
|
readonly signature: FramebufferSignature;
|
|
171
185
|
private readonly onDirty: () => void;
|
|
172
|
-
private
|
|
186
|
+
private subs: IDisposable[] | undefined;
|
|
173
187
|
private cachedDescriptor: PipelineStateDescriptor;
|
|
174
188
|
private cachedModeKey: bigint;
|
|
175
189
|
private readonly modeRules: RoModeRules | undefined;
|
|
176
190
|
private readonly uniformAvals: ReadonlyMap<string, aval<unknown>> | undefined;
|
|
177
191
|
/** Avals discovered while evaluating rules; merged into the leaf
|
|
178
192
|
* set returned by `forEachLeaf` so the heap scene subscribes once. */
|
|
179
|
-
private
|
|
193
|
+
private ruleDeps: Set<aval<unknown>> | undefined;
|
|
180
194
|
|
|
181
195
|
constructor(
|
|
182
196
|
ps: PipelineState | undefined,
|
|
@@ -201,8 +215,9 @@ export class ModeKeyTracker implements IDisposable {
|
|
|
201
215
|
this.onDirty = onDirty;
|
|
202
216
|
if (options.modeRules !== undefined) this.modeRules = options.modeRules;
|
|
203
217
|
if (options.uniformAvals !== undefined) this.uniformAvals = options.uniformAvals;
|
|
204
|
-
|
|
205
|
-
this.cachedModeKey = encodeModeKey(
|
|
218
|
+
const d = this.snapshot();
|
|
219
|
+
this.cachedModeKey = encodeModeKey(d);
|
|
220
|
+
this.cachedDescriptor = internDescriptor(this.cachedModeKey, d);
|
|
206
221
|
if (options.skipSubscribe !== true) this.subscribeAll();
|
|
207
222
|
}
|
|
208
223
|
|
|
@@ -210,7 +225,7 @@ export class ModeKeyTracker implements IDisposable {
|
|
|
210
225
|
return snapshotDescriptor(this.ps, this.signature, {
|
|
211
226
|
...(this.modeRules !== undefined ? { modeRules: this.modeRules } : {}),
|
|
212
227
|
...(this.uniformAvals !== undefined ? { uniformAvals: this.uniformAvals } : {}),
|
|
213
|
-
recordDep: (av) => { this.ruleDeps.add(av); },
|
|
228
|
+
recordDep: (av) => { (this.ruleDeps ??= new Set()).add(av); },
|
|
214
229
|
});
|
|
215
230
|
}
|
|
216
231
|
|
|
@@ -257,7 +272,7 @@ export class ModeKeyTracker implements IDisposable {
|
|
|
257
272
|
}
|
|
258
273
|
}
|
|
259
274
|
}
|
|
260
|
-
for (const av of this.ruleDeps) visit(av);
|
|
275
|
+
if (this.ruleDeps !== undefined) for (const av of this.ruleDeps) visit(av);
|
|
261
276
|
}
|
|
262
277
|
|
|
263
278
|
get descriptor(): PipelineStateDescriptor { return this.cachedDescriptor; }
|
|
@@ -274,18 +289,18 @@ export class ModeKeyTracker implements IDisposable {
|
|
|
274
289
|
* subscription-dedupe layer picks them up.
|
|
275
290
|
*/
|
|
276
291
|
recompute(): boolean {
|
|
277
|
-
this.ruleDeps
|
|
292
|
+
this.ruleDeps?.clear();
|
|
278
293
|
const next = this.snapshot();
|
|
279
294
|
const nextKey = encodeModeKey(next);
|
|
280
295
|
if (nextKey === this.cachedModeKey) return false;
|
|
281
|
-
this.cachedDescriptor = next;
|
|
296
|
+
this.cachedDescriptor = internDescriptor(nextKey, next);
|
|
282
297
|
this.cachedModeKey = nextKey;
|
|
283
298
|
return true;
|
|
284
299
|
}
|
|
285
300
|
|
|
286
301
|
dispose(): void {
|
|
287
|
-
for (const s of this.subs) s.dispose();
|
|
288
|
-
this.subs
|
|
302
|
+
if (this.subs !== undefined) for (const s of this.subs) s.dispose();
|
|
303
|
+
this.subs = undefined;
|
|
289
304
|
}
|
|
290
305
|
|
|
291
306
|
private subscribeAll(): void {
|
|
@@ -318,7 +333,7 @@ export class ModeKeyTracker implements IDisposable {
|
|
|
318
333
|
}
|
|
319
334
|
|
|
320
335
|
private sub(a: aval<unknown>): void {
|
|
321
|
-
this.subs.push(addMarkingCallback(a, this.onDirty));
|
|
336
|
+
(this.subs ??= []).push(addMarkingCallback(a, this.onDirty));
|
|
322
337
|
}
|
|
323
338
|
|
|
324
339
|
private subBlendState(b: PsBlendState): void {
|
|
@@ -248,6 +248,13 @@ export class GpuPartitionScene {
|
|
|
248
248
|
this.masterShadow[recordIdx * this.recordU32 + 5] = comboId >>> 0;
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
+
/** Update an existing record's `indexStart` (field 2 = allocBase + slice).
|
|
252
|
+
* Used by index-buffer compaction; caller flags the bucket partitionDirty. */
|
|
253
|
+
setRecordIndexStart(recordIdx: number, indexStart: number): void {
|
|
254
|
+
if (recordIdx < 0 || recordIdx >= this.numRecords) return;
|
|
255
|
+
this.masterShadow[recordIdx * this.recordU32 + 2] = indexStart >>> 0;
|
|
256
|
+
}
|
|
257
|
+
|
|
251
258
|
/** Update an existing record's uniform ref by uniform index. */
|
|
252
259
|
setRecordUniformRef(recordIdx: number, uniformIdx: number, ref: number): void {
|
|
253
260
|
if (recordIdx < 0 || recordIdx >= this.numRecords) return;
|
|
@@ -255,6 +262,29 @@ export class GpuPartitionScene {
|
|
|
255
262
|
this.masterShadow[recordIdx * this.recordU32 + PARTITION_RECORD_PREFIX_U32 + uniformIdx] = ref >>> 0;
|
|
256
263
|
}
|
|
257
264
|
|
|
265
|
+
/**
|
|
266
|
+
* Re-seat every record's baked arena uniform refs after a main-heap
|
|
267
|
+
* compaction relocated their allocations. `remap` maps OLD→NEW arena byte
|
|
268
|
+
* offset (alloc header offset, same space the refs are stored in). Returns
|
|
269
|
+
* the number of ref words rewritten; the caller must flag the bucket
|
|
270
|
+
* `partitionDirty` so the master re-uploads. O(records × numUniforms).
|
|
271
|
+
*/
|
|
272
|
+
remapUniformRefs(remap: ReadonlyMap<number, number>): number {
|
|
273
|
+
if (remap.size === 0 || this._numUniforms === 0) return 0;
|
|
274
|
+
const ru32 = this.recordU32;
|
|
275
|
+
let changed = 0;
|
|
276
|
+
for (let i = 0; i < this.numRecords; i++) {
|
|
277
|
+
const o = i * ru32 + PARTITION_RECORD_PREFIX_U32;
|
|
278
|
+
for (let k = 0; k < this._numUniforms; k++) {
|
|
279
|
+
const nn = remap.get(this.masterShadow[o + k]!);
|
|
280
|
+
if (nn === undefined) continue;
|
|
281
|
+
this.masterShadow[o + k] = nn >>> 0;
|
|
282
|
+
changed++;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return changed;
|
|
286
|
+
}
|
|
287
|
+
|
|
258
288
|
removeRecord(recordIdx: number): number {
|
|
259
289
|
if (recordIdx < 0 || recordIdx >= this.numRecords) return -1;
|
|
260
290
|
const last = this.numRecords - 1;
|
|
@@ -88,28 +88,49 @@ function classify(entry: RuleEntry): Shape {
|
|
|
88
88
|
|
|
89
89
|
// ─── WGSL fragments ───────────────────────────────────────────────────
|
|
90
90
|
|
|
91
|
+
// Error-free transforms survive ONLY if the compiler cannot see their
|
|
92
|
+
// algebra: WebKit's WGSL→MSL path compiles with fast-math, which folds
|
|
93
|
+
// (a+b)-a → b (two_sum dies) and distributes Dekker's split products into
|
|
94
|
+
// exact cancellation (two_prod dies) — df32 silently degrades to f32, the
|
|
95
|
+
// planet-scale 0.25m wobble on iOS. Desktop drivers fold the naked forms
|
|
96
|
+
// too. Defense: the EFT-critical constants 1/-1/0 come from the `Count`
|
|
97
|
+
// uniform, whose contents no shader compiler can constant-fold; validated
|
|
98
|
+
// per-primitive on Apple M-series + NVIDIA/Vulkan (df32-probe, variant J).
|
|
91
99
|
const DF32_LIB = /* wgsl */ `
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
100
|
+
// launder: value-preserving, but an opaque integer op the optimizer
|
|
101
|
+
// cannot trace through (Count.zeroBits == 0 at runtime).
|
|
102
|
+
fn ln_(x: f32) -> f32 { return bitcast<f32>(bitcast<u32>(x) ^ Count.zeroBits); }
|
|
96
103
|
fn two_sum(a: f32, b: f32) -> vec2<f32> {
|
|
97
104
|
let s = a + b;
|
|
98
|
-
let bb = fma(
|
|
99
|
-
let t1 = fma(
|
|
100
|
-
let t2 = fma(
|
|
101
|
-
let t3 = fma(
|
|
105
|
+
let bb = fma(Count.one, s, -a);
|
|
106
|
+
let t1 = fma(Count.one, s, -bb);
|
|
107
|
+
let t2 = fma(Count.one, a, -t1);
|
|
108
|
+
let t3 = fma(Count.one, b, -bb);
|
|
102
109
|
return vec2<f32>(s, t2 + t3);
|
|
103
110
|
}
|
|
104
111
|
fn quick_two_sum(a: f32, b: f32) -> vec2<f32> {
|
|
105
112
|
let s = a + b;
|
|
106
|
-
let t = fma(
|
|
107
|
-
return vec2<f32>(s, fma(
|
|
113
|
+
let t = fma(Count.one, s, -a);
|
|
114
|
+
return vec2<f32>(s, fma(Count.one, b, -t));
|
|
115
|
+
}
|
|
116
|
+
// Veltkamp split with LAUNDERED halves. The laundering is load-bearing on
|
|
117
|
+
// two fronts: Apple's fast-math distributes Dekker's partial products into
|
|
118
|
+
// exact symbolic cancellation (err ≡ a*b - p ≡ 0) unless the halves are
|
|
119
|
+
// opaque; and an fma-based two_prod (err = fma(a,b,-p)) is NOT an option —
|
|
120
|
+
// Qualcomm Adreno and Intel Xe lower fma() to an unfused multiply-add,
|
|
121
|
+
// which makes that error term exactly zero. Laundered Dekker needs no fma
|
|
122
|
+
// at all, so it is exact on every backend probed (NVIDIA/Vulkan, Apple
|
|
123
|
+
// M-series/Metal, Adreno/Android, Intel Xe/D3D).
|
|
124
|
+
fn lsplit12(a: f32) -> vec2<f32> {
|
|
125
|
+
// 12/12 mantissa split (11 explicit + implicit high bits): every partial
|
|
126
|
+
// product below is <= 24 bits, i.e. exactly representable in f32.
|
|
127
|
+
let hi = bitcast<f32>(bitcast<u32>(a) & 0xFFFFF000u);
|
|
128
|
+
return vec2<f32>(ln_(hi), ln_(a - hi));
|
|
108
129
|
}
|
|
109
130
|
fn two_prod(a: f32, b: f32) -> vec2<f32> {
|
|
110
131
|
let p = a * b;
|
|
111
|
-
let A =
|
|
112
|
-
let B =
|
|
132
|
+
let A = lsplit12(a);
|
|
133
|
+
let B = lsplit12(b);
|
|
113
134
|
let err = ((A.x * B.x - p) + A.x * B.y + A.y * B.x) + A.y * B.y;
|
|
114
135
|
return vec2<f32>(p, err);
|
|
115
136
|
}
|
|
@@ -165,7 +186,7 @@ function bindings(strideU32: number): string {
|
|
|
165
186
|
@group(0) @binding(0) var<storage, read_write> Constituents: array<vec2<f32>>;
|
|
166
187
|
@group(0) @binding(1) var<storage, read_write> MainHeap: array<f32>;
|
|
167
188
|
@group(0) @binding(2) var<storage, read> RecordData: array<u32>;
|
|
168
|
-
struct CountUniform { count: u32 }
|
|
189
|
+
struct CountUniform { count: u32, zeroBits: u32, one: f32, negOne: f32 }
|
|
169
190
|
@group(0) @binding(3) var<uniform> Count: CountUniform;
|
|
170
191
|
|
|
171
192
|
const RECORD_STRIDE: u32 = ${strideU32 >>> 0}u;
|
|
@@ -85,6 +85,12 @@ class RecordsGpu {
|
|
|
85
85
|
size: 16, // uniform min binding size
|
|
86
86
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
87
87
|
});
|
|
88
|
+
// Fields 1..3 are DF32_LIB's fast-math guards (see codegen.ts): zeroBits,
|
|
89
|
+
// one, negOne — runtime values the shader compiler cannot constant-fold.
|
|
90
|
+
const init = new ArrayBuffer(16);
|
|
91
|
+
new Uint32Array(init, 0, 2).set([0, 0]); // count, zeroBits
|
|
92
|
+
new Float32Array(init, 8, 2).set([1.0, -1.0]); // one, negOne
|
|
93
|
+
device.queue.writeBuffer(this.countBuf, 0, init);
|
|
88
94
|
}
|
|
89
95
|
|
|
90
96
|
/** Re-upload the packed record data if it changed since last sync, and refresh `Count`. */
|
|
@@ -48,7 +48,42 @@ const MIN_STRIDE_U32 = 5; // rule_id, out_slot, in0, in1, in2 — covers the 13
|
|
|
48
48
|
export class RecordsBuffer {
|
|
49
49
|
private buf: Uint32Array;
|
|
50
50
|
private owners: (RecordOwner | undefined)[] = [];
|
|
51
|
-
|
|
51
|
+
/** Record indices per owner. A single number for the common one-
|
|
52
|
+
* record-per-owner case, promoted to a small array on the second —
|
|
53
|
+
* saves a whole `Set` per RO at heap scale. */
|
|
54
|
+
private readonly byOwner = new Map<RecordOwner, number | number[]>();
|
|
55
|
+
|
|
56
|
+
private ownerAdd(owner: RecordOwner, idx: number): void {
|
|
57
|
+
const v = this.byOwner.get(owner);
|
|
58
|
+
if (v === undefined) this.byOwner.set(owner, idx);
|
|
59
|
+
else if (typeof v === "number") this.byOwner.set(owner, [v, idx]);
|
|
60
|
+
else v.push(idx);
|
|
61
|
+
}
|
|
62
|
+
private ownerDelete(owner: RecordOwner, idx: number): void {
|
|
63
|
+
const v = this.byOwner.get(owner);
|
|
64
|
+
if (v === undefined) return;
|
|
65
|
+
if (typeof v === "number") {
|
|
66
|
+
if (v === idx) this.byOwner.delete(owner);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const i = v.indexOf(idx);
|
|
70
|
+
if (i >= 0) {
|
|
71
|
+
v[i] = v[v.length - 1]!;
|
|
72
|
+
v.pop();
|
|
73
|
+
if (v.length === 1) this.byOwner.set(owner, v[0]!);
|
|
74
|
+
else if (v.length === 0) this.byOwner.delete(owner);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
private ownerReplace(owner: RecordOwner, oldIdx: number, newIdx: number): void {
|
|
78
|
+
const v = this.byOwner.get(owner);
|
|
79
|
+
if (v === undefined) return;
|
|
80
|
+
if (typeof v === "number") {
|
|
81
|
+
if (v === oldIdx) this.byOwner.set(owner, newIdx);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const i = v.indexOf(oldIdx);
|
|
85
|
+
if (i >= 0) v[i] = newIdx;
|
|
86
|
+
}
|
|
52
87
|
private strideU32 = MIN_STRIDE_U32;
|
|
53
88
|
private count = 0;
|
|
54
89
|
/** Bumps whenever the byte layout changes shape (stride growth) so the dispatcher recompiles. */
|
|
@@ -86,44 +121,64 @@ export class RecordsBuffer {
|
|
|
86
121
|
for (let i = 0; i < inSlots.length; i++) this.buf[base + 2 + i] = inSlots[i]! >>> 0;
|
|
87
122
|
for (let i = base + 2 + inSlots.length; i < base + this.strideU32; i++) this.buf[i] = 0;
|
|
88
123
|
this.owners[idx] = owner;
|
|
89
|
-
|
|
90
|
-
if (set === undefined) {
|
|
91
|
-
set = new Set();
|
|
92
|
-
this.byOwner.set(owner, set);
|
|
93
|
-
}
|
|
94
|
-
set.add(idx);
|
|
124
|
+
this.ownerAdd(owner, idx);
|
|
95
125
|
this.generation++;
|
|
96
126
|
return idx;
|
|
97
127
|
}
|
|
98
128
|
|
|
99
129
|
/** Remove every record owned by `owner` (swap-remove against the tail). */
|
|
100
130
|
removeAllForOwner(owner: RecordOwner): void {
|
|
101
|
-
const
|
|
102
|
-
if (
|
|
103
|
-
this.byOwner.delete(owner);
|
|
131
|
+
const v = this.byOwner.get(owner);
|
|
132
|
+
if (v === undefined) {
|
|
104
133
|
return;
|
|
105
134
|
}
|
|
106
135
|
// Process highest indices first so a swap can never move a not-yet-removed
|
|
107
136
|
// record of this owner that we still hold a stale index for.
|
|
108
|
-
const indices = [...
|
|
137
|
+
const indices = typeof v === "number" ? [v] : [...v].sort((a, b) => b - a);
|
|
109
138
|
for (const idx of indices) this.removeAt(idx);
|
|
110
139
|
this.byOwner.delete(owner);
|
|
111
140
|
this.generation++;
|
|
112
141
|
}
|
|
113
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Re-seat `HostHeap` handles after a main-heap compaction relocated their
|
|
145
|
+
* backing allocations. `remap` maps an OLD handle payload (the data byte
|
|
146
|
+
* offset in the main heap) to its NEW one. `Constituent` handles point into
|
|
147
|
+
* the separate df32 slot buffer (never compacted) and are left untouched.
|
|
148
|
+
* Returns the number of handle words rewritten; bumps `generation` so the
|
|
149
|
+
* dispatcher re-uploads. O(records × stride).
|
|
150
|
+
*/
|
|
151
|
+
remapHostHeap(remap: ReadonlyMap<number, number>): number {
|
|
152
|
+
if (remap.size === 0 || this.count === 0) return 0;
|
|
153
|
+
const s = this.strideU32;
|
|
154
|
+
let changed = 0;
|
|
155
|
+
for (let i = 0; i < this.count; i++) {
|
|
156
|
+
const base = i * s;
|
|
157
|
+
// out_slot at base+1, in_slots at base+2 .. base+s-1.
|
|
158
|
+
for (let w = base + 1; w < base + s; w++) {
|
|
159
|
+
const h = this.buf[w]!;
|
|
160
|
+
if (handleTag(h) !== SlotTag.HostHeap) continue;
|
|
161
|
+
const nn = remap.get(handlePayload(h));
|
|
162
|
+
if (nn === undefined) continue;
|
|
163
|
+
this.buf[w] = makeHandle(SlotTag.HostHeap, nn);
|
|
164
|
+
changed++;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (changed > 0) this.generation++;
|
|
168
|
+
return changed;
|
|
169
|
+
}
|
|
170
|
+
|
|
114
171
|
private removeAt(idx: number): void {
|
|
115
172
|
const last = this.count - 1;
|
|
116
173
|
// The record currently at `idx` is the one being removed.
|
|
117
|
-
this.
|
|
174
|
+
this.ownerDelete(this.owners[idx]!, idx);
|
|
118
175
|
if (idx !== last) {
|
|
119
176
|
// Move the tail record's words into the hole, and re-home it.
|
|
120
177
|
const dst = idx * this.strideU32;
|
|
121
178
|
const src = last * this.strideU32;
|
|
122
179
|
this.buf.copyWithin(dst, src, src + this.strideU32);
|
|
123
180
|
const lastOwner = this.owners[last]!;
|
|
124
|
-
|
|
125
|
-
lset.delete(last);
|
|
126
|
-
lset.add(idx);
|
|
181
|
+
this.ownerReplace(lastOwner, last, idx);
|
|
127
182
|
this.owners[idx] = lastOwner;
|
|
128
183
|
}
|
|
129
184
|
this.owners[last] = undefined;
|
|
@@ -109,6 +109,16 @@ export class DerivedUniformsScene {
|
|
|
109
109
|
return r;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Re-seat the per-chunk derived-uniform records' `HostHeap` handles
|
|
114
|
+
* (derived outputs and host-uniform inputs) after the main heap compacted
|
|
115
|
+
* `chunkIdx`. `remap` maps OLD→NEW data byte offsets. Returns words changed.
|
|
116
|
+
*/
|
|
117
|
+
remapHostHeap(chunkIdx: number, remap: ReadonlyMap<number, number>): number {
|
|
118
|
+
const r = this.recordsByChunk.get(chunkIdx);
|
|
119
|
+
return r === undefined ? 0 : r.remapHostHeap(remap);
|
|
120
|
+
}
|
|
121
|
+
|
|
112
122
|
/** Total record count summed across every chunk's records buffer.
|
|
113
123
|
* Used by the scene's stats / diagnostics. */
|
|
114
124
|
get totalRecordCount(): number {
|
|
@@ -264,14 +274,19 @@ export class TrafoTree {
|
|
|
264
274
|
this.byKey.set(key, node);
|
|
265
275
|
const h = (s: number): number => makeHandle(SlotTag.Constituent, s);
|
|
266
276
|
const recs = this.scene.chainRecordsFor(level);
|
|
267
|
-
// node.world.fwd =
|
|
268
|
-
//
|
|
277
|
+
// node.world.fwd = parent.world.fwd · link.fwd (just link at the root end):
|
|
278
|
+
// deeper links sit on the RIGHT so they hit the column vector FIRST —
|
|
279
|
+
// p' = root · … · leaf · p, the scene-graph inner-first contract. (The
|
|
280
|
+
// previous `link · parent` order applied ancestors first, which put nested
|
|
281
|
+
// child trafos in WORLD space — invisible for the ubiquitous single-link
|
|
282
|
+
// chains, wrong for e.g. a translate under an adaptive billboard trafo.)
|
|
283
|
+
// node.world.inv = link.inv · parent.world.inv (reversed accordingly).
|
|
269
284
|
if (parent === undefined) {
|
|
270
285
|
recs.add(node, CHAIN_RULE_ID, h(modelPair.fwd), [1, h(link.fwd)]);
|
|
271
286
|
recs.add(node, CHAIN_RULE_ID, h(modelPair.inv), [1, h(link.inv)]);
|
|
272
287
|
} else {
|
|
273
|
-
recs.add(node, CHAIN_RULE_ID, h(modelPair.fwd), [2, h(
|
|
274
|
-
recs.add(node, CHAIN_RULE_ID, h(modelPair.inv), [2, h(
|
|
288
|
+
recs.add(node, CHAIN_RULE_ID, h(modelPair.fwd), [2, h(parent.modelPair.fwd), h(link.fwd)]);
|
|
289
|
+
recs.add(node, CHAIN_RULE_ID, h(modelPair.inv), [2, h(link.inv), h(parent.modelPair.inv)]);
|
|
275
290
|
}
|
|
276
291
|
return node;
|
|
277
292
|
}
|
|
@@ -19,6 +19,49 @@ import { AListBridges } from "@aardworx/wombat.adaptive";
|
|
|
19
19
|
import type { RenderObject } from "../core/renderObject.js";
|
|
20
20
|
import type { RenderTree } from "../core/renderTree.js";
|
|
21
21
|
|
|
22
|
+
/** True when `tree` contains no reactive nodes — its RenderObjects can
|
|
23
|
+
* be enumerated as a plain iterable (no inner aset / reader needed). */
|
|
24
|
+
function isStaticTree(tree: RenderTree): boolean {
|
|
25
|
+
switch (tree.kind) {
|
|
26
|
+
case "Empty":
|
|
27
|
+
case "Leaf":
|
|
28
|
+
return true;
|
|
29
|
+
case "Ordered":
|
|
30
|
+
case "Unordered":
|
|
31
|
+
return tree.children.every(isStaticTree);
|
|
32
|
+
default:
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Enumerate a static subtree's RenderObjects (pre: `isStaticTree`). */
|
|
38
|
+
function* staticObjects(tree: RenderTree): Iterable<RenderObject> {
|
|
39
|
+
switch (tree.kind) {
|
|
40
|
+
case "Empty":
|
|
41
|
+
return;
|
|
42
|
+
case "Leaf":
|
|
43
|
+
yield tree.object;
|
|
44
|
+
return;
|
|
45
|
+
case "Ordered":
|
|
46
|
+
case "Unordered":
|
|
47
|
+
for (const c of tree.children) yield* staticObjects(c);
|
|
48
|
+
return;
|
|
49
|
+
default:
|
|
50
|
+
throw new Error(`staticObjects: reactive node '${tree.kind}'`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Flatten a reactive container of subtrees. The static children go
|
|
55
|
+
* through `collectSeq` (plain iterables — ONE reader for the whole
|
|
56
|
+
* set, instead of a ConstantAset + reader + subscription PER LEAF,
|
|
57
|
+
* which at heap scale was megabytes of ballast); reactive children
|
|
58
|
+
* keep the full `collect`. */
|
|
59
|
+
function flattenChildSet(children: aset<RenderTree>): aset<RenderObject> {
|
|
60
|
+
const staticPart = children.filter(isStaticTree).collectSeq(staticObjects);
|
|
61
|
+
const reactivePart = children.filter(t => !isStaticTree(t)).collect(flattenRenderTree);
|
|
62
|
+
return ASet.union(staticPart, reactivePart);
|
|
63
|
+
}
|
|
64
|
+
|
|
22
65
|
export function flattenRenderTree(tree: RenderTree): aset<RenderObject> {
|
|
23
66
|
switch (tree.kind) {
|
|
24
67
|
case "Empty":
|
|
@@ -27,6 +70,8 @@ export function flattenRenderTree(tree: RenderTree): aset<RenderObject> {
|
|
|
27
70
|
return ASet.single(tree.object);
|
|
28
71
|
case "Ordered":
|
|
29
72
|
case "Unordered": {
|
|
73
|
+
// Fully-static subtree → one constant aset over a plain array.
|
|
74
|
+
if (isStaticTree(tree)) return ASet.ofArray([...staticObjects(tree)]);
|
|
30
75
|
// N-ary union via `unionMany(aset<aset<T>>)`. The naïve
|
|
31
76
|
// `children.reduce(ASet.union)` builds a 2N-deep nested-union
|
|
32
77
|
// tree; the reader's traversal recurses through it and blows
|
|
@@ -39,8 +84,8 @@ export function flattenRenderTree(tree: RenderTree): aset<RenderObject> {
|
|
|
39
84
|
case "Adaptive":
|
|
40
85
|
return ASet.bind(flattenRenderTree, tree.tree);
|
|
41
86
|
case "OrderedFromList":
|
|
42
|
-
return
|
|
87
|
+
return flattenChildSet(AListBridges.toASet(tree.children));
|
|
43
88
|
case "UnorderedFromSet":
|
|
44
|
-
return
|
|
89
|
+
return flattenChildSet(tree.children);
|
|
45
90
|
}
|
|
46
91
|
}
|