Skip to main content

qualia_core_db/wgsl_forge/emit/
wgsl.rs

1use std::fmt::Write;
2
3use super::GeneratedShader;
4use crate::wgsl_forge::{ForgeError, KernelSpec, Op, Schedule, FORGE_SCHEMA_VERSION};
5
6pub fn emit_wgsl(kernel: &KernelSpec, schedule: Schedule) -> Result<GeneratedShader, ForgeError> {
7    kernel.validate()?;
8    let semantic_hash = kernel.semantic_hash()?;
9    let mut source = String::with_capacity(2_048);
10    writeln!(
11        source,
12        "// Generated by Qualia WGSL Forge schema {FORGE_SCHEMA_VERSION}."
13    )
14    .map_err(|error| ForgeError::Emission(error.to_string()))?;
15    writeln!(
16        source,
17        "// Kernel: {}@{}",
18        kernel.id, kernel.semantic_version
19    )
20    .map_err(|error| ForgeError::Emission(error.to_string()))?;
21    writeln!(source, "// Semantic hash: {semantic_hash}")
22        .map_err(|error| ForgeError::Emission(error.to_string()))?;
23    writeln!(
24        source,
25        "// Schedule: workgroup={}, items={}, vector={}",
26        schedule.workgroup_size, schedule.items_per_invocation, schedule.vector_width
27    )
28    .map_err(|error| ForgeError::Emission(error.to_string()))?;
29
30    emit_kernel_body(&mut source, kernel, schedule)?;
31
32    let source_hash = blake3::hash(source.as_bytes()).to_hex().to_string();
33    Ok(GeneratedShader {
34        kernel_id: kernel.id.clone(),
35        semantic_hash,
36        source_hash,
37        schedule,
38        source,
39    })
40}
41
42fn emit_kernel_body(
43    source: &mut String,
44    kernel: &KernelSpec,
45    schedule: Schedule,
46) -> Result<(), ForgeError> {
47    if kernel.id == "topk" {
48        return emit_topk_wgsl(source, kernel, schedule);
49    }
50
51    if kernel.id == "fused-ffn" {
52        return emit_ffn_wgsl(source, kernel, schedule);
53    }
54
55    if kernel.id == "p64-project" {
56        return emit_p64_wgsl(source, kernel, schedule);
57    }
58
59    if kernel.id == "ternary-gemv" {
60        return emit_ternary_gemv_wgsl(source, kernel, schedule);
61    }
62
63    // gemm / gemv / fft are lowered through the compute-graph IR: the KernelSpec becomes a
64    // one-node ComputeGraph (`to_graph`) and `lower_graph` walks it, dispatching the node
65    // to the WgslDelegateLowerer. In Phase 1 the leaf emission DELEGATES to the proven
66    // emit_*_wgsl functions, so the generated bytes are byte-identical by construction (the
67    // certify-cache source_hash is unchanged). This is the seam that later phases use to
68    // replace the per-kernel.id string emitters with native graph templates and to lower
69    // the same graph to other backends. See docs/plans/dag-ir-forge.md.
70    if matches!(kernel.id.as_str(), "gemm" | "gemv" | "fft") {
71        let graph = kernel.to_graph()?;
72        let mut lowerer = WgslDelegateLowerer {
73            source,
74            kernel,
75            schedule,
76        };
77        return crate::wgsl_forge::ir::graph::lower_graph(&graph, &mut lowerer);
78    }
79
80    if kernel.id == "ray-probe" {
81        // The ray-query enable extension must precede any other item.
82        writeln!(source, "enable wgpu_ray_query;")
83            .map_err(|error| ForgeError::Emission(error.to_string()))?;
84    }
85
86    if kernel.id == "affine-f32" {
87        writeln!(
88            source,
89            r#"
90struct AffineParams {{
91    length: u32,
92    scale: f32,
93    bias: f32,
94    _pad: u32,
95}}"#
96        )
97        .map_err(|error| ForgeError::Emission(error.to_string()))?;
98    }
99
100    writeln!(source, "").map_err(|error| ForgeError::Emission(error.to_string()))?;
101    for buffer in &kernel.buffers {
102        // Acceleration structures bind without an address space.
103        if buffer.element == crate::wgsl_forge::ir::BufferElement::AccelerationStructure {
104            writeln!(
105                source,
106                "@group({}) @binding({}) var {}: acceleration_structure;",
107                buffer.group, buffer.binding, buffer.name
108            )
109            .map_err(|error| ForgeError::Emission(error.to_string()))?;
110            continue;
111        }
112        let access = match buffer.access {
113            crate::wgsl_forge::ir::BufferAccess::StorageRead => "storage, read",
114            crate::wgsl_forge::ir::BufferAccess::StorageReadWrite => "storage, read_write",
115            crate::wgsl_forge::ir::BufferAccess::Uniform => "uniform",
116        };
117        let type_decl = match buffer.element {
118            crate::wgsl_forge::ir::BufferElement::Scalar(
119                crate::wgsl_forge::ir::ScalarType::F32,
120            ) => "array<f32>",
121            crate::wgsl_forge::ir::BufferElement::Scalar(
122                crate::wgsl_forge::ir::ScalarType::U32,
123            ) => "array<u32>",
124            crate::wgsl_forge::ir::BufferElement::Scalar(
125                crate::wgsl_forge::ir::ScalarType::I32,
126            ) => "array<i32>",
127            crate::wgsl_forge::ir::BufferElement::Scalar(
128                crate::wgsl_forge::ir::ScalarType::U64Words,
129            ) => "array<vec2<u32>>",
130            crate::wgsl_forge::ir::BufferElement::AffineParams => "AffineParams",
131            crate::wgsl_forge::ir::BufferElement::P64Words64 => "array<P64Words64>",
132            crate::wgsl_forge::ir::BufferElement::AccelerationStructure => {
133                unreachable!("handled above")
134            }
135        };
136        writeln!(
137            source,
138            "@group({}) @binding({}) var<{}> {}: {};",
139            buffer.group, buffer.binding, access, buffer.name, type_decl
140        )
141        .map_err(|error| ForgeError::Emission(error.to_string()))?;
142    }
143
144    writeln!(
145        source,
146        r#"
147const ITEMS_PER_INVOCATION: u32 = {}u;
148const VECTOR_WIDTH: u32 = {}u;
149
150@compute @workgroup_size({})
151fn {}(@builtin(global_invocation_id) gid: vec3<u32>) {{"#,
152        schedule.items_per_invocation,
153        schedule.vector_width,
154        schedule.workgroup_size,
155        kernel.entry_point
156    )
157    .map_err(|error| ForgeError::Emission(error.to_string()))?;
158
159    if kernel.id == "ray-probe" {
160        // One ray per invocation: load the 8-float ray, intersect, store the hit.
161        writeln!(source, "    let i = gid.x;")
162            .map_err(|error| ForgeError::Emission(error.to_string()))?;
163        writeln!(source, "    if (i >= arrayLength(&hits)) {{ return; }}")
164            .map_err(|error| ForgeError::Emission(error.to_string()))?;
165        writeln!(source, "    let base = i * 8u;")
166            .map_err(|error| ForgeError::Emission(error.to_string()))?;
167        writeln!(
168            source,
169            "    let origin = vec3<f32>(rays[base], rays[base + 1u], rays[base + 2u]);"
170        )
171        .map_err(|error| ForgeError::Emission(error.to_string()))?;
172        writeln!(
173            source,
174            "    let direction = vec3<f32>(rays[base + 3u], rays[base + 4u], rays[base + 5u]);"
175        )
176        .map_err(|error| ForgeError::Emission(error.to_string()))?;
177        writeln!(source, "    let t_min = rays[base + 6u];")
178            .map_err(|error| ForgeError::Emission(error.to_string()))?;
179        writeln!(source, "    let t_max = rays[base + 7u];")
180            .map_err(|error| ForgeError::Emission(error.to_string()))?;
181        emit_ops(source, &kernel.ops, "    ")?;
182        writeln!(source, "    hits[i] = hit_t;")
183            .map_err(|error| ForgeError::Emission(error.to_string()))?;
184        writeln!(source, "}}").map_err(|error| ForgeError::Emission(error.to_string()))?;
185    } else if kernel.id == "affine-f32" {
186        // Keeping the optimized vectorized wrapper for affine-f32 specifically
187        writeln!(
188            source,
189            "    for (var item: u32 = 0u; item < ITEMS_PER_INVOCATION; item = item + 1u) {{"
190        )
191        .map_err(|error| ForgeError::Emission(error.to_string()))?;
192        writeln!(
193            source,
194            "        let global_id = (gid.x * ITEMS_PER_INVOCATION + item) * VECTOR_WIDTH;"
195        )
196        .map_err(|error| ForgeError::Emission(error.to_string()))?;
197
198        if schedule.vector_width == 1 {
199            writeln!(source, "        if (global_id < params.length) {{")
200                .map_err(|error| ForgeError::Emission(error.to_string()))?;
201            emit_ops(source, &kernel.ops, "            ")?;
202            writeln!(source, "        }}")
203                .map_err(|error| ForgeError::Emission(error.to_string()))?;
204        } else {
205            writeln!(
206                source,
207                "        if (global_id + {}u < params.length) {{",
208                schedule.vector_width - 1
209            )
210            .map_err(|error| ForgeError::Emission(error.to_string()))?;
211            let constructor = (0..schedule.vector_width)
212                .map(|index| format!("input[global_id + {index}u]"))
213                .collect::<Vec<_>>()
214                .join(", ");
215            writeln!(
216                source,
217                "            let value = vec{}<f32>({});",
218                schedule.vector_width, constructor
219            )
220            .map_err(|error| ForgeError::Emission(error.to_string()))?;
221            writeln!(source, "            let transformed = value * vec{}<f32>(params.scale) + vec{}<f32>(params.bias);", schedule.vector_width, schedule.vector_width).map_err(|error| ForgeError::Emission(error.to_string()))?;
222            for index in 0..schedule.vector_width {
223                let components = ["x", "y", "z", "w"];
224                writeln!(
225                    source,
226                    "            output[global_id + {index}u] = transformed.{};",
227                    components[index as usize]
228                )
229                .map_err(|error| ForgeError::Emission(error.to_string()))?;
230            }
231            writeln!(source, "        }} else {{")
232                .map_err(|error| ForgeError::Emission(error.to_string()))?;
233            writeln!(source, "            for (var component: u32 = 0u; component < VECTOR_WIDTH; component = component + 1u) {{").map_err(|error| ForgeError::Emission(error.to_string()))?;
234            writeln!(
235                source,
236                "                let global_id = global_id + component;"
237            )
238            .map_err(|error| ForgeError::Emission(error.to_string()))?;
239            writeln!(source, "                if (global_id < params.length) {{")
240                .map_err(|error| ForgeError::Emission(error.to_string()))?;
241            emit_ops(source, &kernel.ops, "                    ")?;
242            writeln!(source, "                }}")
243                .map_err(|error| ForgeError::Emission(error.to_string()))?;
244            writeln!(source, "            }}")
245                .map_err(|error| ForgeError::Emission(error.to_string()))?;
246            writeln!(source, "        }}")
247                .map_err(|error| ForgeError::Emission(error.to_string()))?;
248        }
249        writeln!(source, "    }}\n}}").map_err(|error| ForgeError::Emission(error.to_string()))?;
250    } else {
251        // Generic loop for fused-ffn and other kernels
252        writeln!(
253            source,
254            "    for (var item: u32 = 0u; item < ITEMS_PER_INVOCATION; item = item + 1u) {{"
255        )
256        .map_err(|error| ForgeError::Emission(error.to_string()))?;
257        writeln!(
258            source,
259            "        let global_id = gid.x * ITEMS_PER_INVOCATION + item;"
260        )
261        .map_err(|error| ForgeError::Emission(error.to_string()))?;
262        emit_ops(source, &kernel.ops, "        ")?;
263        writeln!(source, "    }}\n}}").map_err(|error| ForgeError::Emission(error.to_string()))?;
264    }
265
266    Ok(())
267}
268
269/// Emits the top-k reduction kernel: one workgroup per block, the `k` largest
270/// values of each block written to `output` in descending order.
271///
272/// This is the first kernel to exercise workgroup-shared memory and barrier
273/// synchronisation. The shared arrays come from the IR (`kernel.shared_memory`)
274/// and `Op::Barrier` lowers to `workgroupBarrier()`; the reduction control flow
275/// is specialised here because it is not expressible in the scalar op set.
276fn emit_topk_wgsl(
277    source: &mut String,
278    kernel: &KernelSpec,
279    schedule: Schedule,
280) -> Result<(), ForgeError> {
281    let wg = schedule.workgroup_size;
282
283    writeln!(
284        source,
285        r#"
286struct TopKParams {{
287    length: u32,
288    k: u32,
289    block_size: u32,
290    _pad: u32,
291}}
292"#
293    )
294    .map_err(|error| ForgeError::Emission(error.to_string()))?;
295
296    for buffer in &kernel.buffers {
297        let access = match buffer.access {
298            crate::wgsl_forge::ir::BufferAccess::StorageRead => "storage, read",
299            crate::wgsl_forge::ir::BufferAccess::StorageReadWrite => "storage, read_write",
300            crate::wgsl_forge::ir::BufferAccess::Uniform => "uniform",
301        };
302        let type_decl = match buffer.access {
303            crate::wgsl_forge::ir::BufferAccess::Uniform => "TopKParams",
304            _ => "array<f32>",
305        };
306        writeln!(
307            source,
308            "@group({}) @binding({}) var<{}> {}: {};",
309            buffer.group, buffer.binding, access, buffer.name, type_decl
310        )
311        .map_err(|error| ForgeError::Emission(error.to_string()))?;
312    }
313
314    writeln!(source, "").map_err(|error| ForgeError::Emission(error.to_string()))?;
315    for shared in &kernel.shared_memory {
316        writeln!(
317            source,
318            "var<workgroup> {}: array<{}, {}>;",
319            shared.name,
320            shared.element.wgsl_name(),
321            shared.length.resolve(wg)
322        )
323        .map_err(|error| ForgeError::Emission(error.to_string()))?;
324    }
325
326    writeln!(
327        source,
328        r#"
329@compute @workgroup_size({wg})
330fn {entry}(
331    @builtin(local_invocation_id) lid: vec3<u32>,
332    @builtin(workgroup_id) wid: vec3<u32>,
333) {{
334    let tid = lid.x;
335    let block = wid.x;
336    let base = block * {wg}u;
337    let gidx = base + tid;
338    // Sentinel = f32::MIN (0xff7fffff); threads past the input load it so they
339    // never win, and selected slots are reset to it between extractions.
340    let sentinel = bitcast<f32>(0xff7fffffu);
341    var v = sentinel;
342    if (gidx < params.length) {{
343        v = input[gidx];
344    }}
345    s_val[tid] = v;
346    s_idx[tid] = tid;
347    workgroupBarrier();
348
349    for (var i: u32 = 0u; i < params.k; i = i + 1u) {{
350        r_val[tid] = s_val[tid];
351        r_idx[tid] = s_idx[tid];
352        workgroupBarrier();
353        // Tree arg-max reduction over the working copy.
354        for (var stride: u32 = {wg}u / 2u; stride > 0u; stride = stride / 2u) {{
355            if (tid < stride) {{
356                if (r_val[tid + stride] > r_val[tid]) {{
357                    r_val[tid] = r_val[tid + stride];
358                    r_idx[tid] = r_idx[tid + stride];
359                }}
360            }}
361            workgroupBarrier();
362        }}
363        if (tid == 0u) {{
364            output[block * params.k + i] = r_val[0];
365            s_val[r_idx[0]] = sentinel;
366        }}
367        workgroupBarrier();
368    }}
369}}"#,
370        wg = wg,
371        entry = kernel.entry_point
372    )
373    .map_err(|error| ForgeError::Emission(error.to_string()))?;
374
375    Ok(())
376}
377
378/// Fused feed-forward network: one invocation per output element computes
379/// `out[o] = sum_h w2[o,h] * gelu(sum_i w1[h,i] * input[i])`. Self-contained
380/// (no shared memory); dimensions come from the uniform params block.
381fn emit_ffn_wgsl(
382    source: &mut String,
383    kernel: &KernelSpec,
384    schedule: Schedule,
385) -> Result<(), ForgeError> {
386    let wg = schedule.workgroup_size;
387    writeln!(
388        source,
389        r#"
390struct FfnParams {{
391    input_size: u32,
392    hidden_size: u32,
393    output_size: u32,
394    _pad: u32,
395}}
396
397@group(0) @binding(0) var<storage, read> input: array<f32>;
398@group(0) @binding(1) var<storage, read> w1: array<f32>;
399@group(0) @binding(2) var<storage, read> w2: array<f32>;
400@group(0) @binding(3) var<storage, read_write> output: array<f32>;
401@group(0) @binding(4) var<uniform> params: FfnParams;
402
403@compute @workgroup_size({wg})
404fn {entry}(@builtin(global_invocation_id) gid: vec3<u32>) {{
405    let o = gid.x;
406    if (o >= params.output_size) {{ return; }}
407    var acc = 0.0;
408    for (var h: u32 = 0u; h < params.hidden_size; h = h + 1u) {{
409        var hv = 0.0;
410        let w1_row = h * params.input_size;
411        for (var i: u32 = 0u; i < params.input_size; i = i + 1u) {{
412            hv = hv + w1[w1_row + i] * input[i];
413        }}
414        // GELU(hv)
415        let g = 0.5 * hv * (1.0 + tanh(0.7978845608 * (hv + 0.044715 * hv * hv * hv)));
416        acc = acc + w2[o * params.hidden_size + h] * g;
417    }}
418    output[o] = acc;
419}}"#,
420        wg = wg,
421        entry = kernel.entry_point
422    )
423    .map_err(|error| ForgeError::Emission(error.to_string()))?;
424    Ok(())
425}
426
427/// P64 descriptor projection: one invocation per record projects its 16 packed
428/// u32 words onto the 16-element weight vector.
429fn emit_p64_wgsl(
430    source: &mut String,
431    kernel: &KernelSpec,
432    schedule: Schedule,
433) -> Result<(), ForgeError> {
434    let wg = schedule.workgroup_size;
435    writeln!(
436        source,
437        r#"
438struct P64Words64 {{
439    lanes: array<vec4<u32>, 4>,
440}}
441
442@group(0) @binding(0) var<storage, read> input: array<P64Words64>;
443@group(0) @binding(1) var<storage, read> weights: array<f32>;
444@group(0) @binding(2) var<storage, read_write> output: array<f32>;
445
446@compute @workgroup_size({wg})
447fn {entry}(@builtin(global_invocation_id) gid: vec3<u32>) {{
448    let r = gid.x;
449    if (r >= arrayLength(&output)) {{ return; }}
450    let rec = input[r];
451    var acc = 0.0;
452    for (var w: u32 = 0u; w < 16u; w = w + 1u) {{
453        let word = rec.lanes[w / 4u][w % 4u];
454        acc = acc + weights[w] * f32(word);
455    }}
456    output[r] = acc;
457}}"#,
458        wg = wg,
459        entry = kernel.entry_point
460    )
461    .map_err(|error| ForgeError::Emission(error.to_string()))?;
462    Ok(())
463}
464
465/// BitNet-style ternary GEMV with on-the-fly dequant: one invocation per output
466/// row `o` computes `out[o] = scale[o] * sum_i ternary(w[o,i]) * x[i]`. Weights
467/// are 2-bit-packed ternary codes (16 codes per u32, low-to-high lanes; code
468/// `0->0.0, 1->+1.0, 2->-1.0, 3->0.0`), `ceil(K/16)` words per row laid out
469/// contiguously. Self-contained (no shared memory); dimensions come from the
470/// uniform params block (m, k, k_words, _pad).
471fn emit_ternary_gemv_wgsl(
472    source: &mut String,
473    kernel: &KernelSpec,
474    schedule: Schedule,
475) -> Result<(), ForgeError> {
476    let wg = schedule.workgroup_size;
477    writeln!(
478        source,
479        r#"
480struct TernaryGemvParams {{
481    m: u32,
482    k: u32,
483    k_words: u32,
484    _pad: u32,
485}}
486
487@group(0) @binding(0) var<storage, read> x: array<f32>;
488@group(0) @binding(1) var<storage, read> w_packed: array<u32>;
489@group(0) @binding(2) var<storage, read> scale: array<f32>;
490@group(0) @binding(3) var<storage, read_write> output: array<f32>;
491@group(0) @binding(4) var<uniform> params: TernaryGemvParams;
492
493@compute @workgroup_size({wg})
494fn {entry}(@builtin(global_invocation_id) gid: vec3<u32>) {{
495    let o = gid.x;
496    if (o >= params.m) {{ return; }}
497    var acc = 0.0;
498    let row_base = o * params.k_words;
499    for (var word_idx: u32 = 0u; word_idx < params.k_words; word_idx = word_idx + 1u) {{
500        let word = w_packed[row_base + word_idx];
501        let lane_base = word_idx * 16u;
502        for (var lane: u32 = 0u; lane < 16u; lane = lane + 1u) {{
503            let i = lane_base + lane;
504            if (i >= params.k) {{ break; }}
505            // Extract the 2-bit ternary code for this lane (low-to-high).
506            let code = (word >> (lane * 2u)) & 3u;
507            // Map code -> value: 0->0.0, 1->+1.0, 2->-1.0, 3->0.0 (unused).
508            var tern = 0.0;
509            if (code == 1u) {{
510                tern = 1.0;
511            }} else if (code == 2u) {{
512                tern = -1.0;
513            }}
514            acc = acc + tern * x[i];
515        }}
516    }}
517    output[o] = scale[o] * acc;
518}}"#,
519        wg = wg,
520        entry = kernel.entry_point
521    )
522    .map_err(|error| ForgeError::Emission(error.to_string()))?;
523    Ok(())
524}
525
526/// General dense row-major GEMM, all f32: one invocation per output element
527/// `o = i*N + j` computes `C[i][j] = sum_k A[i*K+k] * B[k*N+j]`. Self-contained
528/// (no shared memory); dimensions come from the uniform params block (m, n, k, _pad).
529/// The K dimension is `params.k`; the inner accumulation index is `kk` so it never
530/// shadows the dimension name.
531/// Phase-1 WGSL lowerer for the compute-graph IR. Its `matmul`/`gemv`/`fft` methods
532/// **delegate** to the existing, certified `emit_*_wgsl` functions, so routing a
533/// gemm/gemv/fft `KernelSpec` through `to_graph` → `lower_graph` produces byte-identical
534/// WGSL. Native graph-template nodes (Reduce/Broadcast, …) use [`WgslGraphLowerer`]
535/// instead; unimplemented op-classes inherit the trait's default `Err` (never a silent
536/// no-op).
537struct WgslDelegateLowerer<'a> {
538    source: &'a mut String,
539    kernel: &'a KernelSpec,
540    schedule: Schedule,
541}
542
543impl crate::wgsl_forge::ir::graph::Lowerer for WgslDelegateLowerer<'_> {
544    fn matmul(
545        &mut self,
546        _node: &crate::wgsl_forge::ir::graph::GraphNode,
547    ) -> Result<(), ForgeError> {
548        emit_gemm_wgsl(self.source, self.kernel, self.schedule)
549    }
550    fn gemv(&mut self, _node: &crate::wgsl_forge::ir::graph::GraphNode) -> Result<(), ForgeError> {
551        emit_gemv_wgsl(self.source, self.kernel, self.schedule)
552    }
553    fn fft(&mut self, _node: &crate::wgsl_forge::ir::graph::GraphNode) -> Result<(), ForgeError> {
554        emit_fft_wgsl(self.source, self.kernel, self.schedule)
555    }
556}
557
558/// Phase-2 WGSL lowerer for **native** compute-graph nodes — those with no backing
559/// `KernelSpec` (there is no legacy standalone reduce/broadcast kernel to delegate to).
560/// Each method emits the node's WGSL template directly from its op payload + per-node
561/// schedule. Op-classes not yet built inherit the trait default (`Err`).
562struct WgslGraphLowerer<'a> {
563    source: &'a mut String,
564}
565
566impl crate::wgsl_forge::ir::graph::Lowerer for WgslGraphLowerer<'_> {
567    fn reduce(&mut self, node: &crate::wgsl_forge::ir::graph::GraphNode) -> Result<(), ForgeError> {
568        use crate::wgsl_forge::ir::graph::OpNode;
569        if let OpNode::Reduce { op, .. } = node.op {
570            self.source
571                .push_str(&crate::wgsl_forge::graph_ops::reduce::reduce_wgsl(
572                    op,
573                    node.sched.workgroup_size,
574                ));
575            Ok(())
576        } else {
577            Err(ForgeError::Emission(
578                "WgslGraphLowerer::reduce received a non-Reduce node".to_string(),
579            ))
580        }
581    }
582
583    fn broadcast(
584        &mut self,
585        node: &crate::wgsl_forge::ir::graph::GraphNode,
586    ) -> Result<(), ForgeError> {
587        self.source
588            .push_str(&crate::wgsl_forge::graph_ops::broadcast::broadcast_wgsl(
589                node.sched.workgroup_size,
590            ));
591        Ok(())
592    }
593
594    fn elementwise(
595        &mut self,
596        node: &crate::wgsl_forge::ir::graph::GraphNode,
597    ) -> Result<(), ForgeError> {
598        use crate::wgsl_forge::ir::graph::OpNode;
599        if let OpNode::Elementwise { f } = node.op {
600            let src = crate::wgsl_forge::graph_ops::elementwise::elementwise_wgsl(
601                f,
602                node.sched.workgroup_size,
603            )?;
604            self.source.push_str(&src);
605            Ok(())
606        } else {
607            Err(ForgeError::Emission(
608                "WgslGraphLowerer::elementwise received a non-Elementwise node".to_string(),
609            ))
610        }
611    }
612}
613
614/// Emit a complete WGSL module for a **pure compute-graph** (no backing `KernelSpec`) —
615/// the native-node path (Reduce/Broadcast in Phase 2). A single-node graph produces one
616/// kernel module; multi-node graph emission (per-node functions + a driver) is Phase 4.
617pub fn emit_graph_wgsl(
618    graph: &crate::wgsl_forge::ir::graph::ComputeGraph,
619    schedule: Schedule,
620) -> Result<GeneratedShader, ForgeError> {
621    let mut source = String::with_capacity(1_024);
622    writeln!(
623        source,
624        "// Generated by Qualia WGSL Forge schema {FORGE_SCHEMA_VERSION} (compute-graph)."
625    )
626    .map_err(|error| ForgeError::Emission(error.to_string()))?;
627    let mut lowerer = WgslGraphLowerer {
628        source: &mut source,
629    };
630    crate::wgsl_forge::ir::graph::lower_graph(graph, &mut lowerer)?;
631    let source_hash = blake3::hash(source.as_bytes()).to_hex().to_string();
632    Ok(GeneratedShader {
633        kernel_id: "graph".to_string(),
634        semantic_hash: source_hash.clone(),
635        source_hash,
636        schedule,
637        source,
638    })
639}
640
641fn emit_gemm_wgsl(
642    source: &mut String,
643    kernel: &KernelSpec,
644    schedule: Schedule,
645) -> Result<(), ForgeError> {
646    let wg = schedule.workgroup_size;
647    writeln!(
648        source,
649        r#"
650struct GemmParams {{
651    m: u32,
652    n: u32,
653    k: u32,
654    _pad: u32,
655}}
656
657@group(0) @binding(0) var<storage, read> a: array<f32>;
658@group(0) @binding(1) var<storage, read> b: array<f32>;
659@group(0) @binding(2) var<storage, read_write> c: array<f32>;
660@group(0) @binding(3) var<uniform> params: GemmParams;
661
662@compute @workgroup_size({wg})
663fn {entry}(@builtin(global_invocation_id) gid: vec3<u32>) {{
664    let o = gid.x;
665    if (o >= params.m * params.n) {{ return; }}
666    let row = o / params.n;
667    let col = o % params.n;
668    var acc = 0.0;
669    let a_row = row * params.k;
670    for (var kk: u32 = 0u; kk < params.k; kk = kk + 1u) {{
671        acc = acc + a[a_row + kk] * b[kk * params.n + col];
672    }}
673    c[o] = acc;
674}}"#,
675        wg = wg,
676        entry = kernel.entry_point
677    )
678    .map_err(|error| ForgeError::Emission(error.to_string()))?;
679    Ok(())
680}
681
682/// Dense row-major matrix-vector product, all f32: one invocation per output ROW
683/// `i` computes `y[i] = sum_j A[i*N+j] * x[j]`. Self-contained (no shared memory);
684/// dimensions come from the uniform params block (m, n, _pad0, _pad1). The inner
685/// accumulation index is `j`.
686fn emit_gemv_wgsl(
687    source: &mut String,
688    kernel: &KernelSpec,
689    schedule: Schedule,
690) -> Result<(), ForgeError> {
691    let wg = schedule.workgroup_size;
692    writeln!(
693        source,
694        r#"
695struct GemvParams {{
696    m: u32,
697    n: u32,
698    _pad0: u32,
699    _pad1: u32,
700}}
701
702@group(0) @binding(0) var<storage, read> a: array<f32>;
703@group(0) @binding(1) var<storage, read> x: array<f32>;
704@group(0) @binding(2) var<storage, read_write> y: array<f32>;
705@group(0) @binding(3) var<uniform> params: GemvParams;
706
707@compute @workgroup_size({wg})
708fn {entry}(@builtin(global_invocation_id) gid: vec3<u32>) {{
709    let i = gid.x;
710    if (i >= params.m) {{ return; }}
711    var acc = 0.0;
712    let a_row = i * params.n;
713    for (var j: u32 = 0u; j < params.n; j = j + 1u) {{
714        acc = acc + a[a_row + j] * x[j];
715    }}
716    y[i] = acc;
717}}"#,
718        wg = wg,
719        entry = kernel.entry_point
720    )
721    .map_err(|error| ForgeError::Emission(error.to_string()))?;
722    Ok(())
723}
724
725/// Forward DFT via iterative radix-2 Decimation-In-Time over ONE workgroup of
726/// `N = workgroup_size` threads (one complex element per thread; `N` a power of
727/// two). Complex data is interleaved f32: element `j` is
728/// `(input[2*j], input[2*j+1]) = (real, imag)`, so the input/output buffers hold
729/// `2*N` f32. Stage 1 is a bit-reversal load into the `s_re`/`s_im` shared arrays;
730/// then `log2(N)` butterfly stages run, each with a `workgroupBarrier()`. The same
731/// forward sign convention `exp(-2*pi*i*k/m)` as the [`crate::wgsl_forge::oracle::dft_cpu`]
732/// reference is used so GPU and CPU compute the identical transform. The shared
733/// arrays come from the IR (`kernel.shared_memory`, sized to the workgroup size)
734/// and `Op::Barrier` is the reusable IR primitive; the butterfly control flow is
735/// specialised here because it is not expressible in the scalar op set.
736fn emit_fft_wgsl(
737    source: &mut String,
738    kernel: &KernelSpec,
739    schedule: Schedule,
740) -> Result<(), ForgeError> {
741    let wg = schedule.workgroup_size;
742
743    writeln!(
744        source,
745        r#"
746struct FftParams {{
747    n: u32,
748    log2n: u32,
749    _pad0: u32,
750    _pad1: u32,
751}}
752"#
753    )
754    .map_err(|error| ForgeError::Emission(error.to_string()))?;
755
756    for buffer in &kernel.buffers {
757        let access = match buffer.access {
758            crate::wgsl_forge::ir::BufferAccess::StorageRead => "storage, read",
759            crate::wgsl_forge::ir::BufferAccess::StorageReadWrite => "storage, read_write",
760            crate::wgsl_forge::ir::BufferAccess::Uniform => "uniform",
761        };
762        let type_decl = match buffer.access {
763            crate::wgsl_forge::ir::BufferAccess::Uniform => "FftParams",
764            _ => "array<f32>",
765        };
766        writeln!(
767            source,
768            "@group({}) @binding({}) var<{}> {}: {};",
769            buffer.group, buffer.binding, access, buffer.name, type_decl
770        )
771        .map_err(|error| ForgeError::Emission(error.to_string()))?;
772    }
773
774    writeln!(source, "").map_err(|error| ForgeError::Emission(error.to_string()))?;
775    for shared in &kernel.shared_memory {
776        writeln!(
777            source,
778            "var<workgroup> {}: array<{}, {}>;",
779            shared.name,
780            shared.element.wgsl_name(),
781            shared.length.resolve(wg)
782        )
783        .map_err(|error| ForgeError::Emission(error.to_string()))?;
784    }
785
786    // `-2*pi` as a valid WGSL f32 literal (the `f` suffix keeps it f32). The
787    // butterfly indexing and the forward twiddle `exp(-2*pi*i*k/m)` follow the
788    // certified algorithm exactly.
789    writeln!(
790        source,
791        r#"
792@compute @workgroup_size({wg})
793fn {entry}(
794    @builtin(local_invocation_id) local_id: vec3<u32>,
795    @builtin(global_invocation_id) global_id: vec3<u32>,
796) {{
797    let t = local_id.x;                 // thread index 0..N-1
798    let n = params.n;
799    let logn = params.log2n;
800    // 1. Bit-reversal load: element t goes to reverse(t).
801    let rev = reverseBits(t) >> (32u - logn);
802    s_re[rev] = input[2u * t];
803    s_im[rev] = input[2u * t + 1u];
804    workgroupBarrier();
805    // 2. log2(N) butterfly stages. Stage s: span = 1<<s, m = 2*span.
806    //    N/2 butterflies; threads 0..N/2-1 active.
807    for (var s: u32 = 0u; s < logn; s = s + 1u) {{
808        let span = 1u << s;
809        let m = span << 1u;
810        if (t < (n >> 1u)) {{
811            let k = t & (span - 1u);                  // position within the butterfly group
812            let j = ((t >> s) << (s + 1u)) + k;       // lower index = (t/span)*m + k
813            let jp = j + span;                        // upper index
814            // twiddle w = exp(-2*pi*i*k/m)
815            let ang = -6.28318548f * f32(k) / f32(m);
816            let wr = cos(ang);
817            let wi = sin(ang);
818            let ur = s_re[j];
819            let ui = s_im[j];
820            let vr = s_re[jp];
821            let vi = s_im[jp];
822            // v' = v * w
823            let tr = vr * wr - vi * wi;
824            let ti = vr * wi + vi * wr;
825            s_re[j] = ur + tr;
826            s_im[j] = ui + ti;
827            s_re[jp] = ur - tr;
828            s_im[jp] = ui - ti;
829        }}
830        workgroupBarrier();
831    }}
832    // 3. Store.
833    output[2u * t] = s_re[t];
834    output[2u * t + 1u] = s_im[t];
835}}"#,
836        wg = wg,
837        entry = kernel.entry_point
838    )
839    .map_err(|error| ForgeError::Emission(error.to_string()))?;
840
841    Ok(())
842}
843
844fn emit_ops(source: &mut String, ops: &[Op], indent: &str) -> Result<(), ForgeError> {
845    for op in ops {
846        match op {
847            Op::StructLoad {
848                buffer,
849                field,
850                destination,
851            } => {
852                writeln!(source, "{indent}let {destination} = {buffer}.{field};")
853                    .map_err(|error| ForgeError::Emission(error.to_string()))?;
854            }
855            Op::Load {
856                buffer,
857                index,
858                destination,
859            } => {
860                writeln!(source, "{indent}let {destination} = {buffer}[{index}];")
861                    .map_err(|error| ForgeError::Emission(error.to_string()))?;
862            }
863            Op::Store {
864                buffer,
865                index,
866                value,
867            } => {
868                writeln!(source, "{indent}{buffer}[{index}] = {value};")
869                    .map_err(|error| ForgeError::Emission(error.to_string()))?;
870            }
871            Op::Fma {
872                a,
873                b,
874                c,
875                destination,
876            } => {
877                writeln!(source, "{indent}let {destination} = {a} * {b} + {c};")
878                    .map_err(|error| ForgeError::Emission(error.to_string()))?;
879            }
880            Op::Mul {
881                left,
882                right,
883                destination,
884            } => {
885                writeln!(source, "{indent}let {destination} = {left} * {right};")
886                    .map_err(|error| ForgeError::Emission(error.to_string()))?;
887            }
888            Op::Add {
889                left,
890                right,
891                destination,
892            } => {
893                writeln!(source, "{indent}let {destination} = {left} + {right};")
894                    .map_err(|error| ForgeError::Emission(error.to_string()))?;
895            }
896            Op::DotProduct {
897                left_buffer,
898                left_base,
899                right_buffer,
900                right_base,
901                len,
902                destination,
903            } => {
904                writeln!(source, "{indent}var {destination} = 0.0;")
905                    .map_err(|error| ForgeError::Emission(error.to_string()))?;
906                writeln!(
907                    source,
908                    "{indent}for (var i: u32 = 0u; i < {len}; i = i + 1u) {{"
909                )
910                .map_err(|error| ForgeError::Emission(error.to_string()))?;
911                writeln!(source, "{indent}    {destination} = {destination} + {left_buffer}[{left_base} + i] * {right_buffer}[{right_base} + i];").map_err(|error| ForgeError::Emission(error.to_string()))?;
912                writeln!(source, "{indent}}}")
913                    .map_err(|error| ForgeError::Emission(error.to_string()))?;
914            }
915            Op::Loop {
916                induction_var,
917                start,
918                end,
919                step,
920                body,
921            } => {
922                writeln!(source, "{indent}for (var {induction_var}: u32 = {start}; {induction_var} < {end}; {induction_var} = {induction_var} + {step}) {{").map_err(|error| ForgeError::Emission(error.to_string()))?;
923                emit_ops(source, body, &format!("{indent}    "))?;
924                writeln!(source, "{indent}}}")
925                    .map_err(|error| ForgeError::Emission(error.to_string()))?;
926            }
927            Op::Relu {
928                operand,
929                destination,
930            } => {
931                writeln!(source, "{indent}let {destination} = max(0.0, {operand});")
932                    .map_err(|error| ForgeError::Emission(error.to_string()))?;
933            }
934            Op::Gelu {
935                operand,
936                destination,
937            } => {
938                // GELU approximation: 0.5 * x * (1.0 + tanh(sqrt(2/pi) * (x + 0.044715 * x^3)))
939                writeln!(source, "{indent}let {destination} = 0.5 * {operand} * (1.0 + tanh(0.7978845608 * ({operand} + 0.044715 * {operand} * {operand} * {operand})));").map_err(|error| ForgeError::Emission(error.to_string()))?;
940            }
941            Op::MatrixMultiply { .. } => {
942                // No scalar WGSL lowering exists for a dense GEMM op; fail loudly
943                // rather than silently emit nothing. Tensor-core GEMM is delivered
944                // via the cooperative-matrix tile or the CUDA WMMA path.
945                return Err(ForgeError::Emission(
946                    "Op::MatrixMultiply has no scalar WGSL lowering; use coopmat::matmul_tc_wgsl (cooperative-matrix) or the CUDA WMMA path".to_string(),
947                ));
948            }
949            Op::Barrier => {
950                writeln!(source, "{indent}workgroupBarrier();")
951                    .map_err(|error| ForgeError::Emission(error.to_string()))?;
952            }
953            Op::Intrinsic(crate::wgsl_forge::ir::Intrinsic::RayQuery {
954                acceleration_structure,
955                origin,
956                direction,
957                t_min,
958                t_max,
959                destination,
960            }) => {
961                // Hardware ray-query: initialise, proceed to completion, read the
962                // committed hit. Traversal must loop until `rayQueryProceed` returns
963                // false — a single call can leave a multi-node BVH partially walked.
964                writeln!(source, "{indent}var rq: ray_query;")
965                    .map_err(|error| ForgeError::Emission(error.to_string()))?;
966                writeln!(source, "{indent}rayQueryInitialize(&rq, {acceleration_structure}, RayDesc(0u, 0xFFu, {t_min}, {t_max}, {origin}, {direction}));").map_err(|error| ForgeError::Emission(error.to_string()))?;
967                writeln!(
968                    source,
969                    "{indent}loop {{ if (!rayQueryProceed(&rq)) {{ break; }} }}"
970                )
971                .map_err(|error| ForgeError::Emission(error.to_string()))?;
972                writeln!(
973                    source,
974                    "{indent}let committed = rayQueryGetCommittedIntersection(&rq);"
975                )
976                .map_err(|error| ForgeError::Emission(error.to_string()))?;
977                writeln!(source, "{indent}var {destination} = -1.0;")
978                    .map_err(|error| ForgeError::Emission(error.to_string()))?;
979                writeln!(source, "{indent}if (committed.kind != RAY_QUERY_INTERSECTION_NONE) {{ {destination} = committed.t; }}").map_err(|error| ForgeError::Emission(error.to_string()))?;
980            }
981            Op::Intrinsic(_) => {
982                return Err(ForgeError::Emission(
983                    "Intrinsics not implemented for WGSL yet".to_string(),
984                ));
985            }
986        }
987    }
988    Ok(())
989}
990
991#[cfg(test)]
992mod tests {
993    use super::*;
994    use crate::wgsl_forge::{BuiltinKernel, Schedule};
995
996    #[test]
997    fn emission_is_byte_deterministic() {
998        let kernel = BuiltinKernel::AffineF32.spec();
999        let first = emit_wgsl(&kernel, Schedule::default()).unwrap();
1000        let second = emit_wgsl(&kernel, Schedule::default()).unwrap();
1001        assert_eq!(first.source, second.source);
1002    }
1003
1004    /// Phase-1 DAG-IR seam (docs/plans/dag-ir-forge.md §8.2): routing gemm/gemv/fft through
1005    /// `KernelSpec::to_graph` → `lower_graph` (serviced by `WgslDelegateLowerer`) must emit
1006    /// the EXACT body the legacy leaf emitter produces — byte-identical, since the certify
1007    /// cache key folds in the source_hash. This also guards the bridge: if `to_graph`
1008    /// produced the wrong op-class, the routed body would not match the right leaf.
1009    #[test]
1010    fn graph_routing_is_byte_equal_for_gemm_gemv_fft() {
1011        let sched = Schedule::default();
1012        let cases: [(&str, KernelSpec); 3] = [
1013            ("gemm", BuiltinKernel::Gemm.spec()),
1014            ("gemv", BuiltinKernel::Gemv.spec()),
1015            ("fft", BuiltinKernel::Fft.spec()),
1016        ];
1017        for (id, kernel) in cases {
1018            let routed = emit_wgsl(&kernel, sched).unwrap();
1019            let mut legacy_body = String::new();
1020            match id {
1021                "gemm" => emit_gemm_wgsl(&mut legacy_body, &kernel, sched).unwrap(),
1022                "gemv" => emit_gemv_wgsl(&mut legacy_body, &kernel, sched).unwrap(),
1023                "fft" => emit_fft_wgsl(&mut legacy_body, &kernel, sched).unwrap(),
1024                _ => unreachable!(),
1025            }
1026            assert!(
1027                routed.source.ends_with(&legacy_body),
1028                "{id}: graph-routed WGSL body must be byte-identical to the legacy leaf body"
1029            );
1030        }
1031    }
1032
1033    /// Phase-2: a native `Reduce` graph node lowers through `emit_graph_wgsl` →
1034    /// `WgslGraphLowerer` to the `reduce_wgsl` template (no `KernelSpec` involved). This
1035    /// proves the native-template lowering path, distinct from Phase 1's delegation.
1036    #[test]
1037    fn graph_native_reduce_lowers_to_template() {
1038        use crate::wgsl_forge::graph_ops::reduce::{reduce_wgsl, REDUCE_ENTRY};
1039        use crate::wgsl_forge::ir::graph::{
1040            Axis, ComputeGraph, DType, OpNode, RedKind, Shape, TensorRef,
1041        };
1042
1043        let sched = Schedule {
1044            workgroup_size: 256,
1045            ..Default::default()
1046        };
1047        let mut g = ComputeGraph::new();
1048        let inp = TensorRef::external(Shape::new(&[1024]), DType::F32);
1049        g.push(
1050            OpNode::Reduce {
1051                op: RedKind::Sum,
1052                axis: Axis::Last,
1053            },
1054            &[inp],
1055            Shape::new(&[1]),
1056            DType::F32,
1057            sched,
1058        )
1059        .unwrap();
1060        let shader = emit_graph_wgsl(&g, sched).unwrap();
1061        assert!(shader.source.contains(REDUCE_ENTRY));
1062        assert!(
1063            shader.source.ends_with(&reduce_wgsl(RedKind::Sum, 256)),
1064            "native graph emission must equal the reduce template"
1065        );
1066    }
1067
1068    #[test]
1069    fn p64_kernel_lays_64bit_fields_as_paired_u32_words() {
1070        // The p64 kernel carries 64-bit P64 fields. WGSL has no portable native u64,
1071        // so the generated module must lay them out as packed `u32` words. Assert the
1072        // exact packed layout the emitter produces (plan §1 / §10): a `P64Words64`
1073        // struct of `array<vec4<u32>, 4>` lanes, bound as `array<P64Words64>`, and the
1074        // per-word `f32(...)` projection over those `u32` lanes.
1075        let kernel = BuiltinKernel::P64Project.spec();
1076        let generated = emit_wgsl(&kernel, Schedule::default()).unwrap();
1077        let source = &generated.source;
1078
1079        assert!(
1080            source.contains("lanes: array<vec4<u32>, 4>"),
1081            "expected paired/packed vec4<u32> word layout, got:\n{source}"
1082        );
1083        assert!(
1084            source.contains("var<storage, read> input: array<P64Words64>"),
1085            "expected the P64 record buffer bound as packed u32 words, got:\n{source}"
1086        );
1087        // The 64-bit data is read as u32 words and projected via f32(word) — i.e. no
1088        // native 64-bit scalar appears in the kernel body.
1089        assert!(
1090            source.contains("f32(word)"),
1091            "expected per-u32-word projection, got:\n{source}"
1092        );
1093        assert!(
1094            !source.contains("u64") && !source.contains("f64"),
1095            "the portable p64 kernel must not emit any native 64-bit scalar, got:\n{source}"
1096        );
1097    }
1098}