@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,159 @@
1
+ // §7 derived-uniform recipe table — leaf-only flavour.
2
+ //
3
+ // Every recipe is a self-contained leaf: reads constituents directly,
4
+ // computes inline (one or two df32 muls + collapse), writes the final
5
+ // f32 mat4/mat3 to the main heap drawHeader. No intermediates, no
6
+ // layering, no dependents graph.
7
+ //
8
+ // Two consumers of the same df32 product (e.g. ModelViewTrafo and
9
+ // ModelViewProjTrafo for the same RO) recompute Model·View twice —
10
+ // trivial vs the encode-time savings of one flat dispatch.
11
+ //
12
+ // All df32 math runs through the verified primitives in
13
+ // uberKernel.wgsl.ts; the final consumer-facing values are f32.
14
+
15
+ /** Recipe IDs. Numeric values are stable — the kernel switches on them. */
16
+ export const enum RecipeId {
17
+ /** Model.fwd → f32 mat4. */
18
+ ModelTrafo = 0,
19
+ /** Model.bwd → f32 mat4. */
20
+ ModelTrafoInv = 1,
21
+ /** (Model.bwd)ᵀ upper-3×3 → f32 mat3. */
22
+ NormalMatrix = 2,
23
+
24
+ /** View.fwd · Model.fwd → f32 mat4. */
25
+ ModelViewTrafo = 3,
26
+ /** Model.bwd · View.bwd → f32 mat4. */
27
+ ModelViewTrafoInv = 4,
28
+ /** Proj.fwd · View.fwd · Model.fwd → f32 mat4. */
29
+ ModelViewProjTrafo = 5,
30
+ /** Model.bwd · View.bwd · Proj.bwd → f32 mat4. */
31
+ ModelViewProjTrafoInv = 6,
32
+
33
+ /** View.fwd → f32 mat4. */
34
+ ViewTrafo = 7,
35
+ /** View.bwd → f32 mat4. */
36
+ ViewTrafoInv = 8,
37
+ /** Proj.fwd · View.fwd → f32 mat4. */
38
+ ViewProjTrafo = 9,
39
+ /** View.bwd · Proj.bwd → f32 mat4. */
40
+ ViewProjTrafoInv = 10,
41
+ /** Proj.fwd → f32 mat4. */
42
+ ProjTrafo = 11,
43
+ /** Proj.bwd → f32 mat4. */
44
+ ProjTrafoInv = 12,
45
+ }
46
+
47
+ /** Number of constituent inputs each recipe consumes (1, 2, or 3). */
48
+ const INPUT_COUNT: Record<RecipeId, 1 | 2 | 3> = {
49
+ [RecipeId.ModelTrafo]: 1,
50
+ [RecipeId.ModelTrafoInv]: 1,
51
+ [RecipeId.NormalMatrix]: 1,
52
+ [RecipeId.ModelViewTrafo]: 2,
53
+ [RecipeId.ModelViewTrafoInv]: 2,
54
+ [RecipeId.ModelViewProjTrafo]: 3,
55
+ [RecipeId.ModelViewProjTrafoInv]: 3,
56
+ [RecipeId.ViewTrafo]: 1,
57
+ [RecipeId.ViewTrafoInv]: 1,
58
+ [RecipeId.ViewProjTrafo]: 2,
59
+ [RecipeId.ViewProjTrafoInv]: 2,
60
+ [RecipeId.ProjTrafo]: 1,
61
+ [RecipeId.ProjTrafoInv]: 1,
62
+ };
63
+
64
+ export function recipeInputCount(id: RecipeId): 1 | 2 | 3 {
65
+ return INPUT_COUNT[id];
66
+ }
67
+
68
+ /** Output shape — drives drawHeader byte allocation. */
69
+ export type RecipeOutput = "mat4" | "mat3";
70
+
71
+ const OUTPUT: Record<RecipeId, RecipeOutput> = {
72
+ [RecipeId.ModelTrafo]: "mat4",
73
+ [RecipeId.ModelTrafoInv]: "mat4",
74
+ [RecipeId.NormalMatrix]: "mat3",
75
+ [RecipeId.ModelViewTrafo]: "mat4",
76
+ [RecipeId.ModelViewTrafoInv]: "mat4",
77
+ [RecipeId.ModelViewProjTrafo]: "mat4",
78
+ [RecipeId.ModelViewProjTrafoInv]: "mat4",
79
+ [RecipeId.ViewTrafo]: "mat4",
80
+ [RecipeId.ViewTrafoInv]: "mat4",
81
+ [RecipeId.ViewProjTrafo]: "mat4",
82
+ [RecipeId.ViewProjTrafoInv]: "mat4",
83
+ [RecipeId.ProjTrafo]: "mat4",
84
+ [RecipeId.ProjTrafoInv]: "mat4",
85
+ };
86
+
87
+ export function recipeOutput(id: RecipeId): RecipeOutput {
88
+ return OUTPUT[id];
89
+ }
90
+
91
+ /** Which side (forward/backward) of which Trafo3d each input slot draws from. */
92
+ export type ConstituentRef =
93
+ | "Model.fwd" | "Model.bwd"
94
+ | "View.fwd" | "View.bwd"
95
+ | "Proj.fwd" | "Proj.bwd";
96
+
97
+ const INPUTS: Record<RecipeId, readonly ConstituentRef[]> = {
98
+ [RecipeId.ModelTrafo]: ["Model.fwd"],
99
+ [RecipeId.ModelTrafoInv]: ["Model.bwd"],
100
+ [RecipeId.NormalMatrix]: ["Model.bwd"],
101
+ // df_mul args are ordered (outer, inner) so the math product
102
+ // matches Aardvark's `View.mul(Model).forward = View.fwd · Model.fwd`.
103
+ [RecipeId.ModelViewTrafo]: ["View.fwd", "Model.fwd"],
104
+ [RecipeId.ModelViewTrafoInv]: ["Model.bwd", "View.bwd"],
105
+ [RecipeId.ModelViewProjTrafo]: ["Proj.fwd", "View.fwd", "Model.fwd"],
106
+ [RecipeId.ModelViewProjTrafoInv]: ["Model.bwd", "View.bwd", "Proj.bwd"],
107
+ [RecipeId.ViewTrafo]: ["View.fwd"],
108
+ [RecipeId.ViewTrafoInv]: ["View.bwd"],
109
+ [RecipeId.ViewProjTrafo]: ["Proj.fwd", "View.fwd"],
110
+ [RecipeId.ViewProjTrafoInv]: ["View.bwd", "Proj.bwd"],
111
+ [RecipeId.ProjTrafo]: ["Proj.fwd"],
112
+ [RecipeId.ProjTrafoInv]: ["Proj.bwd"],
113
+ };
114
+
115
+ export function recipeInputs(id: RecipeId): readonly ConstituentRef[] {
116
+ return INPUTS[id];
117
+ }
118
+
119
+ /** Public uniform name (one per recipe). */
120
+ const NAME: Record<RecipeId, string> = {
121
+ [RecipeId.ModelTrafo]: "ModelTrafo",
122
+ [RecipeId.ModelTrafoInv]: "ModelTrafoInv",
123
+ [RecipeId.NormalMatrix]: "NormalMatrix",
124
+ [RecipeId.ModelViewTrafo]: "ModelViewTrafo",
125
+ [RecipeId.ModelViewTrafoInv]: "ModelViewTrafoInv",
126
+ [RecipeId.ModelViewProjTrafo]: "ModelViewProjTrafo",
127
+ [RecipeId.ModelViewProjTrafoInv]: "ModelViewProjTrafoInv",
128
+ [RecipeId.ViewTrafo]: "ViewTrafo",
129
+ [RecipeId.ViewTrafoInv]: "ViewTrafoInv",
130
+ [RecipeId.ViewProjTrafo]: "ViewProjTrafo",
131
+ [RecipeId.ViewProjTrafoInv]: "ViewProjTrafoInv",
132
+ [RecipeId.ProjTrafo]: "ProjTrafo",
133
+ [RecipeId.ProjTrafoInv]: "ProjTrafoInv",
134
+ };
135
+
136
+ /** All recipe IDs in switch order. */
137
+ export const ALL_RECIPES: readonly RecipeId[] = [
138
+ RecipeId.ModelTrafo, RecipeId.ModelTrafoInv, RecipeId.NormalMatrix,
139
+ RecipeId.ModelViewTrafo, RecipeId.ModelViewTrafoInv,
140
+ RecipeId.ModelViewProjTrafo, RecipeId.ModelViewProjTrafoInv,
141
+ RecipeId.ViewTrafo, RecipeId.ViewTrafoInv,
142
+ RecipeId.ViewProjTrafo, RecipeId.ViewProjTrafoInv,
143
+ RecipeId.ProjTrafo, RecipeId.ProjTrafoInv,
144
+ ];
145
+
146
+ const BY_NAME: ReadonlyMap<string, RecipeId> = new Map(
147
+ ALL_RECIPES.map(id => [NAME[id], id] as const),
148
+ );
149
+
150
+ export const DERIVED_UNIFORM_NAMES: ReadonlySet<string> =
151
+ new Set(ALL_RECIPES.map(id => NAME[id]));
152
+
153
+ export function recipeIdByName(name: string): RecipeId | undefined {
154
+ return BY_NAME.get(name);
155
+ }
156
+
157
+ export function recipeName(id: RecipeId): string {
158
+ return NAME[id];
159
+ }
@@ -0,0 +1,239 @@
1
+ // §7 → heap scene integration helper.
2
+ //
3
+ // Subscription model. Standard adaptive idiom: calling `av.getValue(t)`
4
+ // inside an `evaluateAlways(t, ...)` scope (re-)adds the evaluating
5
+ // object to `av.outputs`. We don't manage subscription manually.
6
+ //
7
+ // Per frame, the heap scene's `update()` runs `evaluateAlways` once on
8
+ // the parent scene-object (as it already does). Inside that scope:
9
+ // 1. `scene.constituents.pullDirty(token)` drains the dirty queue.
10
+ // For each dirty aval, it calls `getValue(token)` — which both
11
+ // gives us the new value AND re-establishes our subscription on
12
+ // that specific aval. O(changed). Avals that didn't change keep
13
+ // us in their outputs from earlier evaluations.
14
+ // 2. `scene.uploadDirty(dirtySlots)` — O(changed) value uploads +
15
+ // O(changed) bitmask flips.
16
+ // 3. Per bucket: `bucket.dispatcher.encode(enc)`. The kernel
17
+ // early-returns on records whose inputs are all clean.
18
+ //
19
+ // On clean frames pullDirty/uploadDirty are no-ops. The records
20
+ // buffer is static. CPU steady-state cost: zero.
21
+
22
+ import type { aval, IAdaptiveObject } from "@aardworx/wombat.adaptive";
23
+ import type { Trafo3d } from "@aardworx/wombat.base";
24
+ import {
25
+ ConstituentSlots,
26
+ DF32_MAT4_BYTES,
27
+ type DerivationRecord, type PairedSlots, type SlotIndex,
28
+ type SubscribeFn,
29
+ } from "./slots.js";
30
+ import {
31
+ RecipeId, recipeIdByName, recipeInputs, recipeInputCount,
32
+ DERIVED_UNIFORM_NAMES,
33
+ type ConstituentRef,
34
+ } from "./recipes.js";
35
+ import {
36
+ DerivedUniformsDispatcher, DerivedUniformsPipeline,
37
+ uploadConstituentsRange,
38
+ } from "./dispatch.js";
39
+
40
+ /** Scene-wide §7 state. One per heap scene. Owns the single records
41
+ * buffer + dispatcher — all buckets share constituents, dirty mask,
42
+ * AND main heap (arena.attrs.buffer), so per-bucket records would
43
+ * just multiply dispatch overhead for no reason. */
44
+ export class DerivedUniformsScene {
45
+ readonly device: GPUDevice;
46
+ readonly constituents: ConstituentSlots;
47
+ readonly pipeline: DerivedUniformsPipeline;
48
+ readonly dispatcher: DerivedUniformsDispatcher;
49
+ /** Constituents storage GPU buffer (df32 mat4 array). */
50
+ constituentsBuf: GPUBuffer;
51
+ /** Caller-owned main heap (arena.attrs.buffer). Updated via
52
+ * `rebindMainHeap` on arena resize. */
53
+ private mainHeapRef: { current: GPUBuffer };
54
+ /** Bumped each time a shared GPU buffer is replaced. */
55
+ bufferEpoch = 0;
56
+
57
+ constructor(
58
+ device: GPUDevice,
59
+ mainHeapBuf: GPUBuffer,
60
+ opts?: { initialConstituentSlots?: number; initialRecordCapacity?: number },
61
+ ) {
62
+ this.device = device;
63
+ const initial = opts?.initialConstituentSlots ?? 64;
64
+ const subscribe: SubscribeFn = () => {};
65
+ this.constituents = new ConstituentSlots(subscribe, initial);
66
+ this.pipeline = new DerivedUniformsPipeline(device);
67
+ this.constituentsBuf = device.createBuffer({
68
+ label: "derivedUniforms.constituents",
69
+ size: initial * DF32_MAT4_BYTES,
70
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
71
+ });
72
+ this.mainHeapRef = { current: mainHeapBuf };
73
+ this.dispatcher = new DerivedUniformsDispatcher(
74
+ device,
75
+ this.pipeline,
76
+ {
77
+ constituentsBuf: () => this.constituentsBuf,
78
+ mainHeapBuf: () => this.mainHeapRef.current,
79
+ },
80
+ opts?.initialRecordCapacity ?? 64,
81
+ );
82
+ }
83
+
84
+ rebindMainHeap(mainHeapBuf: GPUBuffer): void {
85
+ this.mainHeapRef.current = mainHeapBuf;
86
+ }
87
+
88
+ ensureConstituentsCapacity(requiredBytes: number): void {
89
+ if (requiredBytes <= this.constituentsBuf.size) return;
90
+ let cap = this.constituentsBuf.size;
91
+ while (cap < requiredBytes) cap *= 2;
92
+ this.constituentsBuf.destroy();
93
+ this.constituentsBuf = this.device.createBuffer({
94
+ label: "derivedUniforms.constituents",
95
+ size: cap,
96
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
97
+ });
98
+ this.bufferEpoch++;
99
+ // The fresh buffer is zero-initialized; the previous contents are
100
+ // gone. Re-upload everything we've ever packed (slot 0..slotCount).
101
+ // Without this, slots that were last written before the grow read
102
+ // zero on the GPU — every df_mul on them produces NaN.
103
+ const slotCount = this.constituents.slotCount;
104
+ if (slotCount > 0) {
105
+ const mirror = this.constituents.cpuMirror;
106
+ this.device.queue.writeBuffer(
107
+ this.constituentsBuf, 0,
108
+ mirror.buffer, mirror.byteOffset,
109
+ slotCount * DF32_MAT4_BYTES,
110
+ );
111
+ }
112
+ }
113
+
114
+ /** Routed from heap scene's `inputChanged(t, o)`. Returns true iff
115
+ * `o` matched a known constituent aval. */
116
+ routeInputChanged(o: IAdaptiveObject): boolean {
117
+ if (!this.constituents.has(o)) return false;
118
+ this.constituents.markDirty(o);
119
+ return true;
120
+ }
121
+
122
+ /** Apply the dirty set: upload changed constituent values. O(changed). */
123
+ uploadDirty(dirty: ReadonlySet<SlotIndex>): void {
124
+ if (dirty.size === 0) return;
125
+ this.ensureConstituentsCapacity(this.constituents.slotCount * DF32_MAT4_BYTES);
126
+ uploadConstituentsRange(
127
+ this.device, this.constituentsBuf, this.constituents.cpuMirror, dirty,
128
+ );
129
+ }
130
+
131
+ dispose(): void {
132
+ this.dispatcher.dispose();
133
+ this.constituentsBuf.destroy();
134
+ }
135
+ }
136
+
137
+ // ─── Per-RO registration ──────────────────────────────────────────────
138
+
139
+ export interface RoTrafoInputs {
140
+ readonly modelTrafo?: aval<Trafo3d> | undefined;
141
+ readonly viewTrafo?: aval<Trafo3d> | undefined;
142
+ readonly projTrafo?: aval<Trafo3d> | undefined;
143
+ }
144
+
145
+ export interface RoDerivedRequest {
146
+ readonly trafos: RoTrafoInputs;
147
+ readonly requiredNames: readonly string[];
148
+ readonly byteOffsetByName: ReadonlyMap<string, number>;
149
+ readonly drawHeaderBaseByte: number;
150
+ }
151
+
152
+ export interface RoRegistration {
153
+ /** Per-record handles (RecordsBuffer ids) — passed back to deregister. */
154
+ readonly recordIds: readonly number[];
155
+ readonly constituentAvals: readonly aval<Trafo3d>[];
156
+ }
157
+
158
+ /** Register all derived-uniform records for one RO. */
159
+ export function registerRoDerivations(
160
+ scene: DerivedUniformsScene,
161
+ req: RoDerivedRequest,
162
+ ): RoRegistration {
163
+ if (req.requiredNames.length === 0) {
164
+ return { recordIds: [], constituentAvals: [] };
165
+ }
166
+
167
+ const acquiredAvals: aval<Trafo3d>[] = [];
168
+ const pairFor = new Map<"Model" | "View" | "Proj", PairedSlots>();
169
+ const TRAFO_FIELD: Record<"Model" | "View" | "Proj", keyof RoTrafoInputs> = {
170
+ Model: "modelTrafo", View: "viewTrafo", Proj: "projTrafo",
171
+ };
172
+ const acquirePair = (key: "Model" | "View" | "Proj"): PairedSlots => {
173
+ let p = pairFor.get(key);
174
+ if (p !== undefined) return p;
175
+ const av = req.trafos[TRAFO_FIELD[key]];
176
+ if (av === undefined) {
177
+ throw new Error(
178
+ `derivedUniforms: RO requires '${key}' trafo but trafos.${TRAFO_FIELD[key]} is undefined`,
179
+ );
180
+ }
181
+ p = scene.constituents.acquire(av);
182
+ pairFor.set(key, p);
183
+ acquiredAvals.push(av);
184
+ return p;
185
+ };
186
+
187
+ const resolveConstituent = (ref: ConstituentRef): SlotIndex => {
188
+ switch (ref) {
189
+ case "Model.fwd": return acquirePair("Model").fwd;
190
+ case "Model.bwd": return acquirePair("Model").inv;
191
+ case "View.fwd": return acquirePair("View" ).fwd;
192
+ case "View.bwd": return acquirePair("View" ).inv;
193
+ case "Proj.fwd": return acquirePair("Proj" ).fwd;
194
+ case "Proj.bwd": return acquirePair("Proj" ).inv;
195
+ }
196
+ };
197
+
198
+ const recordIds: number[] = [];
199
+ for (const name of req.requiredNames) {
200
+ const id = recipeIdByName(name);
201
+ if (id === undefined) {
202
+ throw new Error(`derivedUniforms: unknown derived name '${name}'`);
203
+ }
204
+ const off = req.byteOffsetByName.get(name);
205
+ if (off === undefined) {
206
+ throw new Error(
207
+ `derivedUniforms: '${name}' has no byte offset in drawHeader for this RO`,
208
+ );
209
+ }
210
+ const refs = recipeInputs(id);
211
+ const slots = refs.map(resolveConstituent);
212
+ const inCount = recipeInputCount(id);
213
+ const rec: DerivationRecord = {
214
+ recipe: id as number,
215
+ in0: slots[0]!,
216
+ in1: inCount >= 2 ? slots[1]! : (0 as SlotIndex),
217
+ in2: inCount >= 3 ? slots[2]! : (0 as SlotIndex),
218
+ outByte: req.drawHeaderBaseByte + off,
219
+ };
220
+ recordIds.push(scene.dispatcher.records.add(rec));
221
+ }
222
+
223
+ return { recordIds, constituentAvals: acquiredAvals };
224
+ }
225
+
226
+ export function deregisterRoDerivations(
227
+ scene: DerivedUniformsScene,
228
+ reg: RoRegistration,
229
+ ): void {
230
+ for (const id of reg.recordIds) scene.dispatcher.records.remove(id);
231
+ for (const av of reg.constituentAvals) scene.constituents.release(av);
232
+ }
233
+
234
+ export function isDerivedUniformName(name: string): boolean {
235
+ return DERIVED_UNIFORM_NAMES.has(name);
236
+ }
237
+
238
+ // Re-export RecipeId for callers (e.g. heapScene's drawHeader walk).
239
+ export { RecipeId };
@@ -0,0 +1,35 @@
1
+ // Slot-ID encoding for the §7 uber kernel.
2
+ //
3
+ // One u32 namespace covers both the Constituents heap (df32 trafos
4
+ // uploaded from CPU) and the Intermediates heap (df32 outputs of
5
+ // layer-1 recipes). The high bit selects the heap; the low 31 bits are
6
+ // the linear index within that heap. Each slot holds one mat4 in df32
7
+ // = 16 vec2<f32> = 128 bytes.
8
+ //
9
+ // Uber-kernel side: a single helper reads from either heap based on
10
+ // the flag — see `read_mat4_df` in the WGSL source.
11
+
12
+ /** Branded u32 — top bit selects heap, low 31 bits are slot index. */
13
+ export type SlotId = number & { readonly __slotId: unique symbol };
14
+
15
+ const FLAG_INTERMEDIATE = 0x80000000;
16
+
17
+ export const SlotId = {
18
+ /** Encode a Constituents-heap slot. */
19
+ constituent(idx: number): SlotId {
20
+ return (idx & 0x7FFFFFFF) as SlotId;
21
+ },
22
+ /** Encode an Intermediates-heap slot. */
23
+ intermediate(idx: number): SlotId {
24
+ return ((idx & 0x7FFFFFFF) | FLAG_INTERMEDIATE) >>> 0 as SlotId;
25
+ },
26
+ isIntermediate(s: SlotId): boolean {
27
+ return (s & FLAG_INTERMEDIATE) !== 0;
28
+ },
29
+ index(s: SlotId): number {
30
+ return s & 0x7FFFFFFF;
31
+ },
32
+ };
33
+
34
+ /** Bytes per df32 mat4 slot: 16 entries × 2 floats × 4 bytes. */
35
+ export const DF32_MAT4_BYTES = 128;
@@ -0,0 +1,155 @@
1
+ // §7 constituent slot allocator + derivation record store.
2
+ //
3
+ // One refcounted heap of df32 mat4 slots, keyed by `aval<Trafo3d>`.
4
+ // Each acquired aval gets an adjacent (fwd, inv) slot pair on the
5
+ // constituents heap. The pull phase reads each tracked aval through
6
+ // `getValue(token)` and re-packs the slot pair only if the returned
7
+ // Trafo3d differs from the previously cached value (reference identity
8
+ // — Trafo3d is immutable).
9
+ //
10
+ // Lifetime is RO-driven: addDraw → acquire(...), removeDraw → release.
11
+
12
+ import type { aval, AdaptiveToken, IAdaptiveObject } from "@aardworx/wombat.adaptive";
13
+ import type { M44d, Trafo3d } from "@aardworx/wombat.base";
14
+
15
+ /** Slot index — pure linear index into the constituents heap. */
16
+ export type SlotIndex = number & { readonly __slotIndex: unique symbol };
17
+
18
+ /** Bytes per df32 mat4 slot: 16 entries × 2 floats × 4 bytes. */
19
+ export const DF32_MAT4_BYTES = 128;
20
+
21
+ /** Forward + inverse slot indices for one Trafo3d aval. */
22
+ export interface PairedSlots {
23
+ readonly fwd: SlotIndex;
24
+ readonly inv: SlotIndex;
25
+ }
26
+
27
+ /** Caller-provided hook: register the parent scene-object as a
28
+ * downstream output of `av`. Without this, the framework never
29
+ * delivers `inputChanged` and our dirty queue stays empty. */
30
+ export type SubscribeFn = (av: aval<unknown>) => void;
31
+
32
+ /** A single derivation request: "run this recipe over these constituent
33
+ * slots, write its output to `out_byte` in main heap." Identity-tracked
34
+ * so add/remove can locate the row in the records buffer. */
35
+ export interface DerivationRecord {
36
+ readonly recipe: number; // RecipeId
37
+ readonly in0: SlotIndex;
38
+ readonly in1: SlotIndex; // 0 if unused
39
+ readonly in2: SlotIndex; // 0 if unused
40
+ readonly outByte: number;
41
+ }
42
+
43
+ class IndexPool {
44
+ private next = 0;
45
+ private free: number[] = [];
46
+ alloc(): number { return this.free.pop() ?? this.next++; }
47
+ release(idx: number): void { this.free.push(idx); }
48
+ get highWaterMark(): number { return this.next; }
49
+ }
50
+
51
+ interface ConstituentEntry {
52
+ readonly slots: PairedSlots;
53
+ refs: number;
54
+ }
55
+
56
+ function packEntry(mirror: Float32Array, off: number, v: number): void {
57
+ const hi = Math.fround(v);
58
+ mirror[off] = hi;
59
+ mirror[off + 1] = Math.fround(v - hi);
60
+ }
61
+
62
+ function packMat4ToSlot(mirror: Float32Array, slotIdx: number, m: M44d): void {
63
+ const base = slotIdx * 32;
64
+ packEntry(mirror, base + 0, m.M00); packEntry(mirror, base + 2, m.M01);
65
+ packEntry(mirror, base + 4, m.M02); packEntry(mirror, base + 6, m.M03);
66
+ packEntry(mirror, base + 8, m.M10); packEntry(mirror, base + 10, m.M11);
67
+ packEntry(mirror, base + 12, m.M12); packEntry(mirror, base + 14, m.M13);
68
+ packEntry(mirror, base + 16, m.M20); packEntry(mirror, base + 18, m.M21);
69
+ packEntry(mirror, base + 20, m.M22); packEntry(mirror, base + 22, m.M23);
70
+ packEntry(mirror, base + 24, m.M30); packEntry(mirror, base + 26, m.M31);
71
+ packEntry(mirror, base + 28, m.M32); packEntry(mirror, base + 30, m.M33);
72
+ }
73
+
74
+ export class ConstituentSlots {
75
+ private readonly pool = new IndexPool();
76
+ private readonly byAval = new Map<aval<Trafo3d>, ConstituentEntry>();
77
+ private readonly dirtyAvals = new Set<aval<Trafo3d>>();
78
+ private mirror: Float32Array;
79
+ private readonly subscribe: SubscribeFn;
80
+
81
+ constructor(subscribe: SubscribeFn, initialCapacity = 64) {
82
+ this.subscribe = subscribe;
83
+ this.mirror = new Float32Array(initialCapacity * 32);
84
+ }
85
+
86
+ acquire(av: aval<Trafo3d>): PairedSlots {
87
+ let entry = this.byAval.get(av);
88
+ if (entry === undefined) {
89
+ const fwd = this.pool.alloc() as SlotIndex;
90
+ const inv = this.pool.alloc() as SlotIndex;
91
+ entry = { slots: { fwd, inv }, refs: 0 };
92
+ this.byAval.set(av, entry);
93
+ this.ensureCapacity(this.pool.highWaterMark);
94
+ this.subscribe(av);
95
+ this.dirtyAvals.add(av);
96
+ }
97
+ entry.refs++;
98
+ return entry.slots;
99
+ }
100
+
101
+ release(av: aval<Trafo3d>): void {
102
+ const entry = this.byAval.get(av);
103
+ if (entry === undefined) {
104
+ throw new Error("ConstituentSlots.release: aval was never acquired");
105
+ }
106
+ entry.refs--;
107
+ if (entry.refs === 0) {
108
+ this.pool.release(entry.slots.fwd);
109
+ this.pool.release(entry.slots.inv);
110
+ this.byAval.delete(av);
111
+ this.dirtyAvals.delete(av);
112
+ }
113
+ }
114
+
115
+ has(o: IAdaptiveObject): boolean {
116
+ return this.byAval.has(o as unknown as aval<Trafo3d>);
117
+ }
118
+ markDirty(o: IAdaptiveObject): void {
119
+ const av = o as unknown as aval<Trafo3d>;
120
+ if (this.byAval.has(av)) this.dirtyAvals.add(av);
121
+ }
122
+
123
+ /** Drain the dirty queue: pack each marked aval's value into the
124
+ * mirror, return the affected slot indices. O(changed). The
125
+ * returned Set is OWNED by ConstituentSlots and reused frame to
126
+ * frame — callers must not retain it past the current frame. */
127
+ pullDirty(token: AdaptiveToken): Set<SlotIndex> {
128
+ this.dirtyOut.clear();
129
+ for (const av of this.dirtyAvals) {
130
+ const entry = this.byAval.get(av);
131
+ if (entry === undefined) continue;
132
+ const value = av.getValue(token);
133
+ packMat4ToSlot(this.mirror, entry.slots.fwd, value.forward);
134
+ packMat4ToSlot(this.mirror, entry.slots.inv, value.backward);
135
+ this.dirtyOut.add(entry.slots.fwd);
136
+ this.dirtyOut.add(entry.slots.inv);
137
+ }
138
+ this.dirtyAvals.clear();
139
+ return this.dirtyOut;
140
+ }
141
+ private readonly dirtyOut = new Set<SlotIndex>();
142
+
143
+ get cpuMirror(): Float32Array { return this.mirror; }
144
+ get slotCount(): number { return this.pool.highWaterMark; }
145
+
146
+ private ensureCapacity(slots: number): void {
147
+ const need = slots * 32;
148
+ if (need <= this.mirror.length) return;
149
+ let cap = this.mirror.length;
150
+ while (cap < need) cap *= 2;
151
+ const grown = new Float32Array(cap);
152
+ grown.set(this.mirror);
153
+ this.mirror = grown;
154
+ }
155
+ }