@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,728 @@
1
+ // heapEffect — effect introspection + bucket-layout generation.
2
+ //
3
+ // Two stages:
4
+ //
5
+ // 1. `compileHeapEffect(effect)` — runs `effect.compile({target:"wgsl"})`
6
+ // and returns the raw stage WGSL (untouched) plus a `schema`
7
+ // describing the effect's surviving inputs / uniforms / outputs
8
+ // (post link + DCE). No name hardcoding; everything is read off
9
+ // `CompiledEffect.interface`.
10
+ //
11
+ // 2. `buildBucketLayout(schema, hasTextures, opts)` — turns a schema
12
+ // into a concrete per-bucket WGSL prelude + a JS-side layout
13
+ // object that knows where each named uniform / attribute lives
14
+ // (struct field offset for packing). The IR rewriter in
15
+ // `heapEffectIR.ts` consumes this layout to substitute uniform /
16
+ // attribute reads with heap loads at the IR level.
17
+
18
+ import type { Effect } from "@aardworx/wombat.shader";
19
+
20
+ // ─── Schema (extracted from CompiledEffect.interface) ───────────────
21
+
22
+ export interface HeapEffectInput {
23
+ readonly name: string;
24
+ readonly wgslType: string;
25
+ readonly byteSize: number; // tightly-packed (vec3<f32> = 12, mat4 = 64)
26
+ readonly location?: number; // present on vertex-stage attributes
27
+ }
28
+
29
+ /**
30
+ * One field of the VS-output / FS-input inter-stage struct ("VsOut"
31
+ * conventionally). Drives the per-effect VsOut struct generated into
32
+ * the prelude — no hardcoded clipPos/worldPos/etc. The runtime stays
33
+ * agnostic to varying names, just like uniforms.
34
+ */
35
+ export interface HeapVarying {
36
+ readonly name: string;
37
+ readonly wgslType: string;
38
+ readonly location?: number;
39
+ readonly builtin?: string;
40
+ readonly interpolation?: "smooth" | "flat" | "centroid" | "sample" | "no-perspective";
41
+ }
42
+
43
+ /** One texture binding the effect declares (post link + DCE). */
44
+ export interface HeapTextureBinding {
45
+ readonly name: string;
46
+ /** WGSL type expression, e.g. "texture_2d<f32>", "texture_cube<f32>". */
47
+ readonly wgslType: string;
48
+ }
49
+
50
+ /** One sampler binding the effect declares (post link + DCE). */
51
+ export interface HeapSamplerBinding {
52
+ readonly name: string;
53
+ /** WGSL type — "sampler" or "sampler_comparison". */
54
+ readonly wgslType: string;
55
+ }
56
+
57
+ export interface HeapEffectSchema {
58
+ readonly attributes: readonly HeapEffectInput[];
59
+ readonly uniforms: readonly HeapEffectInput[];
60
+ readonly varyings: readonly HeapVarying[];
61
+ readonly fragmentOutputs: readonly { readonly name: string; readonly location: number; readonly wgslType: string }[];
62
+ readonly textures: readonly HeapTextureBinding[];
63
+ readonly samplers: readonly HeapSamplerBinding[];
64
+ }
65
+
66
+ export interface CompiledHeapEffect {
67
+ readonly rawVs: string;
68
+ readonly rawFs: string;
69
+ readonly schema: HeapEffectSchema;
70
+ }
71
+
72
+ /**
73
+ * Maps fragment-output names to framebuffer locations. Outputs not
74
+ * in the map are pruned by `linkFragmentOutputs`, which cascades
75
+ * through `pruneCrossStage` / `reduceUniforms` — uniforms that only
76
+ * fed the dropped outputs vanish from the schema entirely.
77
+ */
78
+ export interface FragmentOutputLayout {
79
+ readonly locations: ReadonlyMap<string, number>;
80
+ }
81
+
82
+ export function compileHeapEffect(effect: Effect, fragmentOutputLayout?: FragmentOutputLayout): CompiledHeapEffect {
83
+ const compiled = effect.compile(
84
+ fragmentOutputLayout !== undefined
85
+ ? { target: "wgsl", fragmentOutputLayout }
86
+ : { target: "wgsl" },
87
+ );
88
+ const vsStage = compiled.stages.find(s => s.stage === "vertex");
89
+ const fsStage = compiled.stages.find(s => s.stage === "fragment");
90
+ return {
91
+ rawVs: vsStage?.source ?? "",
92
+ rawFs: fsStage?.source ?? "",
93
+ schema: buildSchema(compiled.interface),
94
+ };
95
+ }
96
+
97
+ // ─── IR-Type → WGSL-string + packed-size helpers ────────────────────
98
+
99
+ type ProgramInterface = ReturnType<Effect["compile"]>["interface"];
100
+ type IrType = ProgramInterface["uniformBlocks"][number]["fields"][number]["type"];
101
+
102
+ function buildSchema(iface: ProgramInterface): HeapEffectSchema {
103
+ const attributes: HeapEffectInput[] = iface.attributes.map(a => ({
104
+ name: a.name,
105
+ wgslType: irTypeToWgsl(a.type),
106
+ byteSize: a.byteSize,
107
+ location: a.location,
108
+ }));
109
+ // When `instanceUniforms` (or any other rewrite) converts a uniform
110
+ // read into a vertex-input fetch, the source module still carries the
111
+ // original Uniform declaration. The interface extractor surfaces it
112
+ // here, but the field is now satisfied by the per-instance attribute —
113
+ // collecting it as a uniform too would double-book the name in the
114
+ // drawHeader. Drop uniforms shadowed by an attribute of the same name.
115
+ const attributeNames = new Set(attributes.map(a => a.name));
116
+ const uniforms: HeapEffectInput[] = [];
117
+ for (const block of iface.uniformBlocks) {
118
+ for (const f of block.fields) {
119
+ if (attributeNames.has(f.name)) continue;
120
+ uniforms.push({
121
+ name: f.name,
122
+ wgslType: irTypeToWgsl(f.type),
123
+ byteSize: irTypePackedSize(f.type),
124
+ });
125
+ }
126
+ }
127
+ // GLSL-style "loose" uniforms (no enclosing buffer name). On WGSL these
128
+ // emit as individual `var<uniform>` decls; the heap IR rewrite substitutes
129
+ // each `ReadInput("Uniform", name, ...)` with a per-draw heap load before
130
+ // WGSL emit, so the loose decls never reach the final shader.
131
+ for (const u of iface.uniforms) {
132
+ if (attributeNames.has(u.name)) continue;
133
+ uniforms.push({
134
+ name: u.name,
135
+ wgslType: irTypeToWgsl(u.type),
136
+ byteSize: irTypePackedSize(u.type),
137
+ });
138
+ }
139
+ // Inter-stage varyings: VS outputs (post link + DCE). The FS inputs
140
+ // mirror these — `linkCrossStage` aligns them by semantic, so we
141
+ // can use VS outputs as the single source of truth for VsOut.
142
+ const vsStage = iface.stages.find(s => s.stage === "vertex");
143
+ const varyings: HeapVarying[] = vsStage === undefined
144
+ ? []
145
+ : vsStage.outputs.map(o => {
146
+ const v: {
147
+ name: string; wgslType: string;
148
+ location?: number; builtin?: string;
149
+ interpolation?: "smooth" | "flat" | "centroid" | "sample" | "no-perspective";
150
+ } = { name: o.name, wgslType: irTypeToWgsl(o.type) };
151
+ if (o.location !== undefined) v.location = o.location;
152
+ if (o.builtin !== undefined) v.builtin = o.builtin;
153
+ if (o.interpolation !== undefined) v.interpolation = o.interpolation;
154
+ return v as HeapVarying;
155
+ });
156
+ const fragmentOutputs = iface.fragmentOutputs.map(o => ({
157
+ name: o.name, location: o.location, wgslType: irTypeToWgsl(o.type),
158
+ }));
159
+ // Texture/sampler bindings post-DCE. The IR Type for textures is a
160
+ // structured kind we don't fully model here yet — for v1 every
161
+ // texture maps to "texture_2d<f32>" and every sampler to plain
162
+ // "sampler". Specialise as multi-kind support lands in the DSL.
163
+ const textures: HeapTextureBinding[] = iface.textures.map(t => ({
164
+ name: t.name, wgslType: "texture_2d<f32>",
165
+ }));
166
+ const samplers: HeapSamplerBinding[] = iface.samplers.map(s => ({
167
+ name: s.name, wgslType: "sampler",
168
+ }));
169
+ return { attributes, uniforms, varyings, fragmentOutputs, textures, samplers };
170
+ }
171
+
172
+ function irTypeToWgsl(t: IrType): string {
173
+ switch (t.kind) {
174
+ case "Float": return "f32";
175
+ case "Int": return t.signed ? "i32" : "u32";
176
+ case "Bool": return "bool";
177
+ case "Vector": return `vec${t.dim}<${irTypeToWgsl(t.element)}>`;
178
+ case "Matrix": return `mat${t.cols}x${t.rows}<${irTypeToWgsl(t.element)}>`;
179
+ case "Array": {
180
+ const inner = irTypeToWgsl(t.element);
181
+ return t.length === "runtime" ? `array<${inner}>` : `array<${inner}, ${t.length}>`;
182
+ }
183
+ default:
184
+ throw new Error(`compileHeapEffect: cannot map IR Type kind '${t.kind}' to WGSL`);
185
+ }
186
+ }
187
+
188
+ function irTypePackedSize(t: IrType): number {
189
+ switch (t.kind) {
190
+ case "Float": return 4;
191
+ case "Int": return 4;
192
+ case "Bool": return 4;
193
+ case "Vector": return t.dim * irTypePackedSize(t.element);
194
+ case "Matrix": return t.cols * t.rows * irTypePackedSize(t.element);
195
+ case "Array": {
196
+ if (t.length === "runtime") {
197
+ throw new Error("compileHeapEffect: runtime-sized arrays have no fixed packed size");
198
+ }
199
+ return t.length * irTypePackedSize(t.element);
200
+ }
201
+ default:
202
+ throw new Error(`compileHeapEffect: cannot size IR Type kind '${t.kind}'`);
203
+ }
204
+ }
205
+
206
+ // ─── Bucket layout ──────────────────────────────────────────────────
207
+
208
+ export interface DrawHeaderField {
209
+ readonly name: string; // schema name (e.g. "ModelTrafo")
210
+ readonly wgslName: string; // WGSL field name (e.g. "modelTrafoRef")
211
+ readonly wgslType: string; // "u32" for refs; "scalar"-packed for atlas texture-ref entries
212
+ readonly byteOffset: number;
213
+ readonly byteSize: number;
214
+ readonly kind: "uniform-ref" | "attribute-ref" | "texture-ref";
215
+ /**
216
+ * For `uniform-ref`: the uniform's underlying WGSL type. The
217
+ * rewriter uses this to pick the right inline read expression
218
+ * (mat4x4 → 4 vec4 reads, vec3 → 3 f32 reads, etc.).
219
+ */
220
+ readonly uniformWgslType?: string;
221
+ /**
222
+ * For `attribute-ref`: the attribute's declared WGSL type. Drives
223
+ * the inline `attributeLoadExpr` when generating preamble lets.
224
+ */
225
+ readonly attributeWgslType?: string;
226
+ /**
227
+ * For `texture-ref`: which sub-entry this is. Atlas-variant texture
228
+ * bindings expand into four contiguous drawHeader entries —
229
+ * `pageRef` (u32 slot index), `formatBits` (u32 packed sampler
230
+ * state + mip + format), `origin` (vec2<f32>, mip-0 top-left in
231
+ * normalized atlas coords), `size` (vec2<f32>, mip-0 size). The
232
+ * runtime packs these as inline values (not pool refs); the
233
+ * shader pass that consumes them lands in a follow-up PR.
234
+ */
235
+ readonly textureSub?: "pageRef" | "formatBits" | "origin" | "size";
236
+ /** For `texture-ref`: the schema's logical texture binding name (shared by all four sub-entries). */
237
+ readonly textureBindingName?: string;
238
+ }
239
+
240
+ export interface BucketLayout {
241
+ readonly drawHeaderFields: readonly DrawHeaderField[];
242
+ readonly drawHeaderBytes: number;
243
+ /** Generated WGSL prelude — bindings + per-effect VsOut struct. No helper fns, no DrawHeader struct. */
244
+ readonly preludeWgsl: string;
245
+ /** DrawHeader stride in u32 elements (drawHeaderBytes / 4). */
246
+ readonly strideU32: number;
247
+ /**
248
+ * Names (schema-side) of uniforms that vary per instance. Populated
249
+ * from `spec.instances.values` at addDraw time; the rewriter pulls
250
+ * these via `instanceLoadExpr` instead of `uniformLoadExpr`.
251
+ */
252
+ readonly perInstanceUniforms: ReadonlySet<string>;
253
+ /**
254
+ * Names (schema-side) of attributes that vary per instance. Populated
255
+ * from `spec.instanceAttributes` at addDraw time. The rewriter
256
+ * indexes per-instance attribute reads by `iidx` (= `instance_index`
257
+ * the megacall path) instead of `vid`.
258
+ */
259
+ readonly perInstanceAttributes: ReadonlySet<string>;
260
+ /**
261
+ * Texture bindings: name + WGSL type + bind-group binding number
262
+ * (allocated starting at TEX_BINDING_START, after the four heap-
263
+ * data bindings 0–3 and the three megacall bindings 4–6).
264
+ */
265
+ readonly textureBindings: readonly { readonly name: string; readonly wgslType: string; readonly binding: number }[];
266
+ /** Sampler bindings, allocated after textures. */
267
+ readonly samplerBindings: readonly { readonly name: string; readonly wgslType: string; readonly binding: number }[];
268
+ /**
269
+ * Atlas-routed texture binding names, exposed for the FS rewriter so
270
+ * it can substitute `textureSample(name, smp, uv)` with the heap
271
+ * `atlasSample(...)` helper for these names only.
272
+ */
273
+ readonly atlasTextureBindings: ReadonlySet<string>;
274
+ }
275
+
276
+ /**
277
+ * First texture binding number; samplers come after the textures.
278
+ * Bindings 0–3 are the four heap arena views, 4–6 are megacall
279
+ * (drawTable, indexStorage, firstDrawInTile).
280
+ */
281
+ export const HEAP_TEX_BINDING_START = 7;
282
+
283
+ /**
284
+ * Number of independent pages per format. Drives both the BGL slot
285
+ * count (N consecutive linear bindings + N consecutive srgb bindings
286
+ * + 1 sampler) and the shader's `switch pageRef` ladder. Must match
287
+ * `ATLAS_MAX_PAGES_PER_FORMAT` in `textureAtlas/atlasPool.ts`.
288
+ */
289
+ export const ATLAS_ARRAY_SIZE = 8;
290
+
291
+ /** First atlas binding (linear texture for page 0). */
292
+ export const ATLAS_LINEAR_BINDING_BASE = 11;
293
+ /** First srgb atlas binding (= linear base + N). */
294
+ export const ATLAS_SRGB_BINDING_BASE = ATLAS_LINEAR_BINDING_BASE + ATLAS_ARRAY_SIZE;
295
+ /** Atlas sampler binding (= linear base + 2N). */
296
+ export const ATLAS_SAMPLER_BINDING = ATLAS_LINEAR_BINDING_BASE + 2 * ATLAS_ARRAY_SIZE;
297
+
298
+ export function buildBucketLayout(
299
+ schema: HeapEffectSchema,
300
+ _hasTextures: boolean,
301
+ opts: {
302
+ perInstanceUniforms?: ReadonlySet<string>;
303
+ perInstanceAttributes?: ReadonlySet<string>;
304
+ /**
305
+ * Names of schema texture bindings routed via the atlas binding-array
306
+ * path. Each such binding is dropped from `textureBindings` /
307
+ * `samplerBindings` and replaced with four `texture-ref` drawHeader
308
+ * fields (pageRef / formatBits / origin / size). Pass an empty set
309
+ * (default) for the standalone texture path.
310
+ */
311
+ atlasTextureBindings?: ReadonlySet<string>;
312
+ } = {},
313
+ ): BucketLayout {
314
+ const perInstanceUniforms = opts.perInstanceUniforms ?? new Set<string>();
315
+ const perInstanceAttributes = opts.perInstanceAttributes ?? new Set<string>();
316
+ const atlasTextureBindings = opts.atlasTextureBindings ?? new Set<string>();
317
+ const drawHeaderFields: DrawHeaderField[] = [];
318
+
319
+ // Per-draw uniforms → u32 ref slots in the DrawHeader. The runtime
320
+ // (UniformPool) keeps one arena allocation per unique aval; sharing
321
+ // across draws → shared ref → shared upload.
322
+ let dhOff = 0;
323
+ for (const u of schema.uniforms) {
324
+ dhOff = roundUp(dhOff, 4);
325
+ drawHeaderFields.push({
326
+ name: u.name, wgslName: lowerFirst(u.name) + "Ref", wgslType: "u32",
327
+ byteOffset: dhOff, byteSize: 4, kind: "uniform-ref",
328
+ uniformWgslType: u.wgslType,
329
+ });
330
+ dhOff += 4;
331
+ }
332
+
333
+ // Attribute refs — same shape as uniform refs, also routed through the pool.
334
+ for (const a of schema.attributes) {
335
+ dhOff = roundUp(dhOff, 4);
336
+ drawHeaderFields.push({
337
+ name: a.name, wgslName: lowerFirst(a.name) + "Ref", wgslType: "u32",
338
+ byteOffset: dhOff, byteSize: 4, kind: "attribute-ref",
339
+ attributeWgslType: a.wgslType,
340
+ });
341
+ dhOff += 4;
342
+ }
343
+
344
+ // Atlas-routed texture bindings: 24 bytes / binding split into
345
+ // pageRef (u32) + formatBits (u32) + origin (vec2<f32>) + size
346
+ // (vec2<f32>). Inline values, not pool refs — the runtime packs
347
+ // them at addDraw time from the atlas acquisition + ISampler.
348
+ for (const t of schema.textures) {
349
+ if (!atlasTextureBindings.has(t.name)) continue;
350
+ const base = lowerFirst(t.name);
351
+ dhOff = roundUp(dhOff, 4);
352
+ drawHeaderFields.push({
353
+ name: `${t.name}.pageRef`, wgslName: `${base}PageRef`, wgslType: "u32",
354
+ byteOffset: dhOff, byteSize: 4, kind: "texture-ref",
355
+ textureSub: "pageRef", textureBindingName: t.name,
356
+ });
357
+ dhOff += 4;
358
+ drawHeaderFields.push({
359
+ name: `${t.name}.formatBits`, wgslName: `${base}FormatBits`, wgslType: "u32",
360
+ byteOffset: dhOff, byteSize: 4, kind: "texture-ref",
361
+ textureSub: "formatBits", textureBindingName: t.name,
362
+ });
363
+ dhOff += 4;
364
+ dhOff = roundUp(dhOff, 8);
365
+ drawHeaderFields.push({
366
+ name: `${t.name}.origin`, wgslName: `${base}Origin`, wgslType: "vec2<f32>",
367
+ byteOffset: dhOff, byteSize: 8, kind: "texture-ref",
368
+ textureSub: "origin", textureBindingName: t.name,
369
+ });
370
+ dhOff += 8;
371
+ drawHeaderFields.push({
372
+ name: `${t.name}.size`, wgslName: `${base}Size`, wgslType: "vec2<f32>",
373
+ byteOffset: dhOff, byteSize: 8, kind: "texture-ref",
374
+ textureSub: "size", textureBindingName: t.name,
375
+ });
376
+ dhOff += 8;
377
+ }
378
+
379
+ const drawHeaderBytes = roundUp(dhOff, 16);
380
+ const strideU32 = drawHeaderBytes / 4;
381
+
382
+ // Allocate texture + sampler bindings starting at HEAP_TEX_BINDING_START.
383
+ // Order: all textures, then all samplers — matches the prelude
384
+ // layout below and the runtime's BGL/bind-group construction.
385
+ // Atlas-routed bindings drop out — their data flows through the
386
+ // drawHeader instead of through individual texture/sampler slots.
387
+ let nextBinding = HEAP_TEX_BINDING_START;
388
+ const textureBindings = schema.textures
389
+ .filter(t => !atlasTextureBindings.has(t.name))
390
+ .map(t => ({ name: t.name, wgslType: t.wgslType, binding: nextBinding++ }));
391
+ // Samplers: drop the matching one when its texture is atlas-routed.
392
+ // Heuristic: assume position-aligned (nth sampler pairs with nth
393
+ // texture). Heap path's v1 surface only ever has 0..1 of each, so
394
+ // this collapses to "if the lone texture is atlas, drop the lone
395
+ // sampler too".
396
+ const atlasSamplerNames = new Set<string>();
397
+ for (let i = 0; i < schema.textures.length; i++) {
398
+ const t = schema.textures[i]!;
399
+ if (atlasTextureBindings.has(t.name)) {
400
+ const s = schema.samplers[i];
401
+ if (s !== undefined) atlasSamplerNames.add(s.name);
402
+ }
403
+ }
404
+ const samplerBindings = schema.samplers
405
+ .filter(s => !atlasSamplerNames.has(s.name))
406
+ .map(s => ({ name: s.name, wgslType: s.wgslType, binding: nextBinding++ }));
407
+
408
+ const isAtlasBucket = atlasTextureBindings.size > 0;
409
+ // Extend the schema's varyings with flat-interpolated atlas threading
410
+ // slots when atlas is in use — the prelude generator emits these into
411
+ // VsOut so user VS code can write them and FS can read them. The
412
+ // location numbers are picked after the existing varyings.
413
+ const schemaForPrelude = isAtlasBucket
414
+ ? extendSchemaWithAtlasVaryings(schema, atlasTextureBindings)
415
+ : schema;
416
+ const preludeWgsl = generatePrelude(
417
+ schemaForPrelude, textureBindings, samplerBindings,
418
+ isAtlasBucket,
419
+ );
420
+
421
+ return {
422
+ drawHeaderFields, drawHeaderBytes, preludeWgsl, strideU32,
423
+ perInstanceUniforms,
424
+ perInstanceAttributes,
425
+ textureBindings, samplerBindings,
426
+ atlasTextureBindings,
427
+ };
428
+ }
429
+
430
+ /**
431
+ * Append flat-interpolated atlas threading varyings to a schema.
432
+ * Locations follow the highest existing `@location(N)` so we don't
433
+ * collide with effect-declared varyings.
434
+ */
435
+ function extendSchemaWithAtlasVaryings(
436
+ schema: HeapEffectSchema,
437
+ atlasNames: ReadonlySet<string>,
438
+ ): HeapEffectSchema {
439
+ let nextLoc = 0;
440
+ for (const v of schema.varyings) {
441
+ if (v.location !== undefined) nextLoc = Math.max(nextLoc, v.location + 1);
442
+ }
443
+ const extras: HeapVarying[] = [];
444
+ for (const name of atlasNames) {
445
+ const v = atlasVaryingNames(name);
446
+ extras.push({ name: v.pageRef, wgslType: "u32", location: nextLoc++, interpolation: "flat" });
447
+ extras.push({ name: v.formatBits, wgslType: "u32", location: nextLoc++, interpolation: "flat" });
448
+ extras.push({ name: v.origin, wgslType: "vec2<f32>", location: nextLoc++, interpolation: "flat" });
449
+ extras.push({ name: v.size, wgslType: "vec2<f32>", location: nextLoc++, interpolation: "flat" });
450
+ }
451
+ return { ...schema, varyings: [...schema.varyings, ...extras] };
452
+ }
453
+
454
+ function lowerFirst(s: string): string {
455
+ return s.length === 0 ? s : s[0]!.toLowerCase() + s.slice(1);
456
+ }
457
+
458
+ function roundUp(value: number, mult: number): number {
459
+ return Math.ceil(value / mult) * mult;
460
+ }
461
+
462
+ // ─── Prelude generation ─────────────────────────────────────────────
463
+
464
+ function generatePrelude(
465
+ schema: HeapEffectSchema,
466
+ textureBindings: readonly { name: string; wgslType: string; binding: number }[],
467
+ samplerBindings: readonly { name: string; wgslType: string; binding: number }[],
468
+ isAtlasBucket: boolean,
469
+ ): string {
470
+ // VsOut is generated from the schema's surviving VS outputs (post
471
+ // link + DCE). No hardcoded fields — what the effect declared is
472
+ // what ends up here.
473
+ const vsOutBody = schema.varyings.map(v => ` ${vsOutFieldDecl(v)}: ${v.wgslType},`).join("\n");
474
+
475
+ // Textures + samplers — one binding per surviving entry, starting
476
+ // at HEAP_TEX_BINDING_START. The user's WGSL must reference them
477
+ // by their schema name; the rewriter drops any DSL-emitted decls
478
+ // that conflict.
479
+ const texDecls = textureBindings.map(t =>
480
+ `@group(0) @binding(${t.binding}) var ${t.name}: ${t.wgslType};`,
481
+ ).join("\n");
482
+ const smpDecls = samplerBindings.map(s =>
483
+ `@group(0) @binding(${s.binding}) var ${s.name}: ${s.wgslType};`,
484
+ ).join("\n");
485
+ const samplingBlock = (texDecls.length > 0 || smpDecls.length > 0)
486
+ ? `\n${texDecls}${texDecls && smpDecls ? "\n" : ""}${smpDecls}\n`
487
+ : "";
488
+
489
+ const megacallDecls = `\n@group(0) @binding(4) var<storage, read> drawTable: array<u32>;\n@group(0) @binding(5) var<storage, read> indexStorage: array<u32>;\n@group(0) @binding(6) var<storage, read> firstDrawInTile: array<u32>;`;
490
+
491
+ const atlasBlock = isAtlasBucket ? generateAtlasPrelude() : "";
492
+
493
+ return /* wgsl */`
494
+ @group(0) @binding(0) var<storage, read> heapU32: array<u32>;
495
+ @group(0) @binding(1) var<storage, read> headersU32: array<u32>;
496
+ @group(0) @binding(2) var<storage, read> heapF32: array<f32>;
497
+ @group(0) @binding(3) var<storage, read> heapV4f: array<vec4<f32>>;${megacallDecls}
498
+ ${samplingBlock}${atlasBlock}
499
+ struct VsOut {
500
+ ${vsOutBody}
501
+ };
502
+ `;
503
+ }
504
+
505
+ /**
506
+ * Atlas prelude: declares N independent `texture_2d<f32>` bindings
507
+ * per format (linear + srgb), plus a shared atlas sampler. The N-way
508
+ * `switch pageRef` ladder picks the matching texture pair for each
509
+ * draw. Each atlas page is its own `GPUTexture` — adding pages to a
510
+ * format is just a fresh allocation slotted into the next BGL entry,
511
+ * no GPU-side `copyTextureToTexture` required.
512
+ *
513
+ * `binding_array<texture_2d<f32>, N>` would collapse the switch into
514
+ * a runtime index but requires Chrome's experimental bindless
515
+ * feature; the N-consecutive-bindings + switch pattern is native
516
+ * WebGPU 1.0.
517
+ *
518
+ * Mip filtering is software (the GPU's mip walk would walk the
519
+ * texture's mip chain, not our embedded Iliffe pyramid). LOD is
520
+ * computed from screen-space derivatives of `uv * size_px` in mip-0
521
+ * texels-per-screen-pixel.
522
+ *
523
+ * Sample math (verified by `tests-browser/atlas-sampling-conformance.test.ts`
524
+ * to match native hardware bilinear within 1 LSB across clamp/repeat/
525
+ * mirror at any uv):
526
+ * - Clamp: atlas_p = origin_px + clamp(uv, 0, 1) * size_px
527
+ * - Mirror: atlas_p = origin_px + mirrorUv(uv) * size_px
528
+ * - Repeat: atlas_p = origin_px + fract(uv) * size_px, then shift ∓1
529
+ * if within 0.5 atlas-pixel of the sub-rect edge, so
530
+ * hardware bilinear straddles the outer wrap-gutter ring.
531
+ * `origin_px` / `size_px` are atlas-pixel coordinates of mip-0, NOT
532
+ * normalized. Mip-k rect is computed by walking the integer floor-
533
+ * halving sequence that matches the CPU packer's pyramid layout.
534
+ * Each sub-rect (incl. every mip slot) carries a 2-px gutter (inner
535
+ * ring = clamp-replicate edge, outer ring = wrap = opposite edge)
536
+ * filled by the upload kernel; without it bilinear at the edges
537
+ * bleeds across sub-rect boundaries.
538
+ */
539
+ export function generateAtlasPrelude(): string {
540
+ return /* wgsl */`
541
+ ${generateAtlasBindings()}
542
+
543
+ fn mirrorAtlasUv(u: f32) -> f32 {
544
+ let t = u - floor(u * 0.5) * 2.0;
545
+ return 1.0 - abs(t - 1.0);
546
+ }
547
+
548
+ // One axis: produce atlas-pixel coord from logical uv per wrap mode.
549
+ // mip_origin / mip_size are in atlas pixels for the target mip slot.
550
+ fn atlasAxisAt(uv: f32, mip_origin: f32, mip_size: f32, mode: u32) -> f32 {
551
+ if (mode == 0u) { // clamp
552
+ let c = clamp(uv, 0.0, 1.0);
553
+ return mip_origin + c * mip_size;
554
+ }
555
+ if (mode == 2u) { // mirror
556
+ let m = mirrorAtlasUv(uv);
557
+ return mip_origin + m * mip_size;
558
+ }
559
+ // repeat
560
+ let f = uv - floor(uv);
561
+ var p = mip_origin + f * mip_size;
562
+ let dL = p - mip_origin;
563
+ let dR = (mip_origin + mip_size) - p;
564
+ if (dL < 0.5) { p = p - 1.0; }
565
+ else if (dR < 0.5) { p = p + 1.0; }
566
+ return p;
567
+ }
568
+
569
+ // Mip-k rect within the Iliffe pyramid, in atlas pixels. Matches the
570
+ // CPU packer's integer floor-halving (so non-pow2 sources still hit
571
+ // the right texels). 2-px gutter between adjacent mip slots.
572
+ const ATLAS_MIP_GAP: f32 = 4.0;
573
+
574
+ fn atlasMipOriginPx(origin: vec2<f32>, size: vec2<f32>, k: u32) -> vec2<f32> {
575
+ if (k == 0u) { return origin; }
576
+ let x = origin.x + size.x + ATLAS_MIP_GAP;
577
+ var y = origin.y;
578
+ var j: u32 = 1u;
579
+ loop {
580
+ if (j >= k) { break; }
581
+ let mh = max(1.0, floor(size.y * exp2(-f32(j))));
582
+ y = y + mh + ATLAS_MIP_GAP;
583
+ j = j + 1u;
584
+ }
585
+ return vec2<f32>(x, y);
586
+ }
587
+
588
+ fn atlasMipSizePx(size: vec2<f32>, k: u32) -> vec2<f32> {
589
+ return vec2<f32>(
590
+ max(1.0, floor(size.x * exp2(-f32(k)))),
591
+ max(1.0, floor(size.y * exp2(-f32(k)))),
592
+ );
593
+ }
594
+
595
+ fn atlasSampleAtMip(
596
+ pageRef: u32, format: u32,
597
+ origin_px: vec2<f32>, size_px: vec2<f32>,
598
+ k: u32,
599
+ uv: vec2<f32>,
600
+ addrU: u32, addrV: u32,
601
+ ) -> vec4<f32> {
602
+ let mip_o = atlasMipOriginPx(origin_px, size_px, k);
603
+ let mip_s = atlasMipSizePx(size_px, k);
604
+ let px = atlasAxisAt(uv.x, mip_o.x, mip_s.x, addrU);
605
+ let py = atlasAxisAt(uv.y, mip_o.y, mip_s.y, addrV);
606
+ let atlasUv = vec2<f32>(px, py) / ${atlasPageSizeConst()}.0;
607
+ ${generateAtlasSwitch()}
608
+ return vec4<f32>(0.0);
609
+ }
610
+
611
+ fn atlasSample(
612
+ pageRef: u32, formatBits: u32,
613
+ origin_px: vec2<f32>, size_px: vec2<f32>,
614
+ uv: vec2<f32>,
615
+ ) -> vec4<f32> {
616
+ let format = formatBits & 0x1u;
617
+ let numMips = (formatBits >> 1u) & 0x7u;
618
+ let addrU = (formatBits >> 4u) & 0x3u;
619
+ let addrV = (formatBits >> 6u) & 0x3u;
620
+ // LOD on pre-wrap UV. dpdx/dpdy on the wrapped value spikes at the
621
+ // fract/mirror seam (discontinuity → bogus huge derivative → tiny
622
+ // mip selected → blurred band). Multiply by size_px to get
623
+ // texels-per-screen-pixel.
624
+ let dx = dpdx(uv) * size_px;
625
+ let dy = dpdy(uv) * size_px;
626
+ let rho = max(length(dx), length(dy));
627
+ let maxLod = f32(max(numMips, 1u) - 1u);
628
+ let lod = clamp(log2(max(rho, 1e-6)), 0.0, maxLod);
629
+ let lo = u32(floor(lod));
630
+ let hi = min(lo + 1u, max(numMips, 1u) - 1u);
631
+ let t = lod - f32(lo);
632
+ let a = atlasSampleAtMip(pageRef, format, origin_px, size_px, lo, uv, addrU, addrV);
633
+ if (numMips <= 1u) { return a; }
634
+ let b = atlasSampleAtMip(pageRef, format, origin_px, size_px, hi, uv, addrU, addrV);
635
+ return mix(a, b, t);
636
+ }
637
+ `;
638
+ }
639
+
640
+ function atlasPageSizeConst(): string {
641
+ // Embedded literal — divisor matches ATLAS_PAGE_SIZE in atlasPool.ts.
642
+ return "4096";
643
+ }
644
+
645
+ /**
646
+ * Emit `2 * ATLAS_ARRAY_SIZE + 1` binding declarations: N linear
647
+ * `texture_2d<f32>`, N srgb `texture_2d<f32>`, then the shared
648
+ * atlas sampler.
649
+ */
650
+ export function generateAtlasBindings(): string {
651
+ const lines: string[] = [];
652
+ for (let i = 0; i < ATLAS_ARRAY_SIZE; i++) {
653
+ lines.push(`@group(0) @binding(${ATLAS_LINEAR_BINDING_BASE + i}) var atlasLinear${i}: texture_2d<f32>;`);
654
+ }
655
+ for (let i = 0; i < ATLAS_ARRAY_SIZE; i++) {
656
+ lines.push(`@group(0) @binding(${ATLAS_SRGB_BINDING_BASE + i}) var atlasSrgb${i}: texture_2d<f32>;`);
657
+ }
658
+ lines.push(`@group(0) @binding(${ATLAS_SAMPLER_BINDING}) var atlasSampler: sampler;`);
659
+ return lines.join("\n");
660
+ }
661
+
662
+ /**
663
+ * Emit the `switch pageRef` ladder body for `atlasSampleAtMip`. Each
664
+ * case picks the matching `atlasLinear<i>` / `atlasSrgb<i>` decl and
665
+ * blends on `format == 1u`. The fallback (page out of range) is the
666
+ * function-level `vec4<f32>(0.0)` after the switch.
667
+ */
668
+ export function generateAtlasSwitch(): string {
669
+ const lines: string[] = [" switch pageRef {"];
670
+ for (let i = 0; i < ATLAS_ARRAY_SIZE; i++) {
671
+ lines.push(` case ${i}u: {`);
672
+ lines.push(` let lin = textureSampleLevel(atlasLinear${i}, atlasSampler, atlasUv, 0.0);`);
673
+ lines.push(` let sr = textureSampleLevel(atlasSrgb${i}, atlasSampler, atlasUv, 0.0);`);
674
+ lines.push(` return select(lin, sr, format == 1u);`);
675
+ lines.push(` }`);
676
+ }
677
+ lines.push(" default: {}");
678
+ lines.push(" }");
679
+ return lines.join("\n");
680
+ }
681
+
682
+ /** Render one varying as the WGSL field declaration: `[@builtin(...) | @location(N)] [@interpolate(...)] name`. */
683
+ function vsOutFieldDecl(v: HeapVarying): string {
684
+ if (v.builtin !== undefined) return `@builtin(${v.builtin}) ${v.name}`;
685
+ const interp = v.interpolation !== undefined ? ` @interpolate(${v.interpolation})` : "";
686
+ if (v.location === undefined) {
687
+ throw new Error(`heapEffect: varying '${v.name}' has neither @builtin nor @location`);
688
+ }
689
+ return `@location(${v.location})${interp} ${v.name}`;
690
+ }
691
+
692
+ /** Atlas-texture varying names used by the VS→FS threading + FS preamble. */
693
+ export function atlasVaryingNames(name: string): {
694
+ pageRef: string; formatBits: string; origin: string; size: string;
695
+ } {
696
+ return {
697
+ pageRef: `_h_${name}PageRef`,
698
+ formatBits: `_h_${name}FormatBits`,
699
+ origin: `_h_${name}Origin`,
700
+ size: `_h_${name}Size`,
701
+ };
702
+ }
703
+
704
+
705
+ /**
706
+ * Megacall VS prelude: binary-search `drawTable` (one record per slot,
707
+ * 5 u32s: firstEmit, drawIdx, indexStart, indexCount, instanceCount)
708
+ * for the slot owning the given `emitIdx`, then split the in-record
709
+ * offset into `(instId, vid)` so per-RO instancing collapses to one
710
+ * record + one drawIndirect. Defines:
711
+ * - `heap_drawIdx` — per-RO selector for header reads.
712
+ * - `instId` — instance index in [0, instanceCount-1].
713
+ * - `vid` — vertex index pulled from indexStorage.
714
+ *
715
+ * Layout: each record emits `indexCount * instanceCount` vertices.
716
+ * `local = emitIdx - firstEmit`, `instId = local / indexCount`,
717
+ * `vid = indices[indexStart + local % indexCount]`.
718
+ */
719
+ export function megacallSearchPrelude(): string {
720
+ // The shared megacall values (`heap_drawIdx`, `instId`, `vid`) are
721
+ // declared as `let` locals inside the @vertex fn body. Wombat.shader's
722
+ // composed-stage helper functions (extractFusedEntry) that reference
723
+ // these identifiers are post-processed in `applyMegacallToEmittedVs`
724
+ // to receive them as `u32` parameters — module-scope `var<private>`
725
+ // is rejected by some WGSL parsers (Safari/WebKit), so we thread the
726
+ // values through helper signatures instead.
727
+ return ` let _tileIdx = emitIdx >> 6u;\n var lo: u32 = firstDrawInTile[_tileIdx];\n var hi: u32 = firstDrawInTile[_tileIdx + 1u];\n loop {\n if (lo >= hi) { break; }\n let _mid = (lo + hi + 1u) >> 1u;\n if (drawTable[_mid * 5u] <= emitIdx) { lo = _mid; } else { hi = _mid - 1u; }\n }\n let _slot = lo;\n let _firstEmit = drawTable[_slot * 5u + 0u];\n let heap_drawIdx: u32 = drawTable[_slot * 5u + 1u];\n let _indexStart = drawTable[_slot * 5u + 2u];\n let _indexCount = drawTable[_slot * 5u + 3u];\n let _local = emitIdx - _firstEmit;\n let instId: u32 = _local / _indexCount;\n let vid: u32 = indexStorage[_indexStart + (_local % _indexCount)];\n`;
728
+ }