Skip to main content

oxicuda_levelzero/
spirv.rs

1//! SPIR-V compute kernel generators for the Level Zero backend.
2//!
3//! This module provides:
4//! - A lightweight [`SpvModule`] builder for emitting valid SPIR-V binaries.
5//! - Generator functions for **unary**, **binary**, **reduce**, and **GEMM**
6//!   compute kernels consumed by Level Zero's `zeModuleCreate`.
7//! - The original [`trivial_compute_shader`] placeholder used for validation.
8//!
9//! All generated kernels use the OpenCL SPIR-V execution model (`Kernel`)
10//! with `Physical64`/`OpenCL` memory model.  Buffer parameters are
11//! `CrossWorkgroup` pointers; scalar parameters are passed by value via
12//! `zeKernelSetArgumentValue`.
13//!
14//! The generated SPIR-V targets version 1.2 (widely supported by Level Zero).
15
16use oxicuda_backend::{BinaryOp, ReduceOp, UnaryOp};
17
18// ─── Constants ──────────────────────────────────────────────
19
20/// SPIR-V magic number.
21pub const SPIRV_MAGIC: u32 = 0x07230203;
22/// SPIR-V version 1.2.
23pub const SPIRV_VERSION_1_2: u32 = 0x0001_0200;
24/// Generator magic — OxiCUDA Level Zero backend.
25pub const SPIRV_GENERATOR: u32 = 0x000D_0002;
26
27// ─── SPIR-V opcodes ─────────────────────────────────────────
28
29pub(crate) const OP_EXTENSION: u32 = 10;
30pub(crate) const OP_EXT_INST_IMPORT: u32 = 11;
31pub(crate) const OP_EXT_INST: u32 = 12;
32pub(crate) const OP_MEMORY_MODEL: u32 = 14;
33pub(crate) const OP_ENTRY_POINT: u32 = 15;
34pub(crate) const OP_EXECUTION_MODE: u32 = 16;
35pub(crate) const OP_CAPABILITY: u32 = 17;
36pub(crate) const OP_TYPE_VOID: u32 = 19;
37pub(crate) const OP_TYPE_BOOL: u32 = 20;
38pub(crate) const OP_TYPE_INT: u32 = 21;
39pub(crate) const OP_TYPE_FLOAT: u32 = 22;
40pub(crate) const OP_TYPE_VECTOR: u32 = 23;
41pub(crate) const OP_TYPE_ARRAY: u32 = 28;
42pub(crate) const OP_TYPE_POINTER: u32 = 32;
43pub(crate) const OP_TYPE_FUNCTION: u32 = 33;
44pub(crate) const OP_CONSTANT: u32 = 43;
45pub(crate) const OP_FUNCTION: u32 = 54;
46pub(crate) const OP_FUNCTION_PARAMETER: u32 = 55;
47pub(crate) const OP_FUNCTION_END: u32 = 56;
48pub(crate) const OP_VARIABLE: u32 = 59;
49pub(crate) const OP_LOAD: u32 = 61;
50pub(crate) const OP_STORE: u32 = 62;
51pub(crate) const OP_IN_BOUNDS_PTR_ACCESS_CHAIN: u32 = 70;
52pub(crate) const OP_DECORATE: u32 = 71;
53pub(crate) const OP_COMPOSITE_EXTRACT: u32 = 81;
54pub(crate) const OP_CONVERT_U_TO_F: u32 = 112;
55pub(crate) const OP_F_NEGATE: u32 = 127;
56pub(crate) const OP_I_ADD: u32 = 128;
57pub(crate) const OP_F_ADD: u32 = 129;
58pub(crate) const OP_F_SUB: u32 = 131;
59pub(crate) const OP_I_MUL: u32 = 132;
60pub(crate) const OP_F_MUL: u32 = 133;
61pub(crate) const OP_U_DIV: u32 = 134;
62pub(crate) const OP_F_DIV: u32 = 136;
63pub(crate) const OP_U_MOD: u32 = 137;
64pub(crate) const OP_U_LESS_THAN: u32 = 176;
65pub(crate) const OP_LOOP_MERGE: u32 = 246;
66pub(crate) const OP_SELECTION_MERGE: u32 = 247;
67pub(crate) const OP_LABEL: u32 = 248;
68pub(crate) const OP_BRANCH: u32 = 249;
69pub(crate) const OP_BRANCH_CONDITIONAL: u32 = 250;
70pub(crate) const OP_CONTROL_BARRIER: u32 = 224;
71pub(crate) const OP_PHI: u32 = 245;
72pub(crate) const OP_RETURN: u32 = 253;
73
74// GroupNonUniform opcodes (SPIR-V 1.3+)
75pub(crate) const OP_GROUP_NON_UNIFORM_FADD: u32 = 350;
76pub(crate) const OP_GROUP_NON_UNIFORM_SHUFFLE: u32 = 345;
77
78// Capabilities
79const CAPABILITY_SHADER: u32 = 1;
80const CAPABILITY_ADDRESSES: u32 = 4;
81const CAPABILITY_KERNEL: u32 = 6;
82
83// Addressing / memory model
84const ADDRESSING_MODEL_LOGICAL: u32 = 0;
85const ADDRESSING_MODEL_PHYSICAL64: u32 = 2;
86const MEMORY_MODEL_GLSL450: u32 = 1;
87const MEMORY_MODEL_OPENCL: u32 = 2;
88
89// Execution model / mode
90const EXECUTION_MODEL_GLCOMPUTE: u32 = 5;
91pub(crate) const EXECUTION_MODEL_KERNEL: u32 = 6;
92const EXECUTION_MODE_LOCAL_SIZE: u32 = 17;
93
94// Function control
95pub(crate) const FUNCTION_CONTROL_NONE: u32 = 0;
96
97// Decorations
98const DECORATION_BUILTIN: u32 = 11;
99
100// BuiltIn values
101const BUILTIN_GLOBAL_INVOCATION_ID: u32 = 28;
102
103// Storage classes
104const STORAGE_CLASS_INPUT: u32 = 1;
105const STORAGE_CLASS_CROSS_WORKGROUP: u32 = 5;
106pub(crate) const STORAGE_CLASS_FUNCTION: u32 = 7;
107
108// Selection/loop control
109const SELECTION_CONTROL_NONE: u32 = 0;
110const LOOP_CONTROL_NONE: u32 = 0;
111
112// OpenCL.std extended instruction numbers
113pub(crate) const OPENCL_EXP: u32 = 19;
114const OPENCL_FABS: u32 = 23;
115pub(crate) const OPENCL_FMAX: u32 = 27;
116const OPENCL_FMIN: u32 = 28;
117const OPENCL_LOG: u32 = 37;
118const OPENCL_SQRT: u32 = 61;
119const OPENCL_TANH: u32 = 63;
120// Extended transcendental instruction numbers (OpenCL.std): atan=6, atan2=7,
121// cbrt=11, cos=14 per the OpenCL Extended Instruction Set spec (9=atanpi,
122// 16=cospi were wrong).
123const OPENCL_ATAN: u32 = 6;
124const OPENCL_ATAN2: u32 = 7;
125const OPENCL_CBRT: u32 = 11;
126const OPENCL_COS: u32 = 14;
127const OPENCL_ERFC: u32 = 17;
128const OPENCL_ERF: u32 = 18;
129const OPENCL_RSQRT: u32 = 56;
130const OPENCL_SIN: u32 = 57;
131
132/// Workgroup size for 1-D compute kernels.
133pub(crate) const WORKGROUP_SIZE: u32 = 256;
134
135// ─── Minimal SPIR-V builder ──────────────────────────────────
136
137/// Lightweight SPIR-V word-stream builder.
138///
139/// Emits valid SPIR-V instructions for simple compute shaders without
140/// pulling in a full compiler.
141pub struct SpvModule {
142    words: Vec<u32>,
143    /// Next available result ID.
144    id_bound: u32,
145}
146
147impl SpvModule {
148    /// Create a new module with a placeholder header (bound filled at finalise).
149    pub fn new() -> Self {
150        let words = vec![SPIRV_MAGIC, SPIRV_VERSION_1_2, SPIRV_GENERATOR, 0, 0];
151        Self { words, id_bound: 1 }
152    }
153
154    /// Allocate a fresh result ID.
155    pub fn alloc_id(&mut self) -> u32 {
156        let id = self.id_bound;
157        self.id_bound += 1;
158        id
159    }
160
161    /// Emit a SPIR-V instruction.
162    pub fn emit(&mut self, opcode: u32, operands: &[u32]) {
163        let word_count = (1 + operands.len()) as u32;
164        self.words.push((word_count << 16) | opcode);
165        self.words.extend_from_slice(operands);
166    }
167
168    /// Emit a string as null-terminated UTF-8 packed into 32-bit words.
169    pub fn string_words(s: &str) -> Vec<u32> {
170        let bytes = s.as_bytes();
171        let padded_len = (bytes.len() + 4) & !3;
172        let mut out = vec![0u32; padded_len / 4];
173        for (i, &b) in bytes.iter().enumerate() {
174            out[i / 4] |= (b as u32) << ((i % 4) * 8);
175        }
176        out
177    }
178
179    /// Finalise the module: patch the ID bound and return the word vector.
180    pub fn finalize(mut self) -> Vec<u32> {
181        self.words[3] = self.id_bound;
182        self.words
183    }
184
185    // ── Convenience emitters ─────────────────────────────────
186
187    pub(crate) fn emit_capability(&mut self, cap: u32) {
188        self.emit(OP_CAPABILITY, &[cap]);
189    }
190
191    pub(crate) fn emit_ext_inst_import(&mut self, id: u32, name: &str) {
192        let mut ops = vec![id];
193        ops.extend(Self::string_words(name));
194        self.emit(OP_EXT_INST_IMPORT, &ops);
195    }
196
197    /// Emit an `OpExtension` declaration (a SPIR-V extension string, no result ID).
198    pub(crate) fn emit_extension(&mut self, name: &str) {
199        let ops = Self::string_words(name);
200        self.emit(OP_EXTENSION, &ops);
201    }
202
203    pub(crate) fn emit_memory_model(&mut self, addressing: u32, memory: u32) {
204        self.emit(OP_MEMORY_MODEL, &[addressing, memory]);
205    }
206
207    pub(crate) fn emit_entry_point(
208        &mut self,
209        model: u32,
210        func_id: u32,
211        name: &str,
212        interfaces: &[u32],
213    ) {
214        let mut ops = vec![model, func_id];
215        ops.extend(Self::string_words(name));
216        ops.extend_from_slice(interfaces);
217        self.emit(OP_ENTRY_POINT, &ops);
218    }
219
220    pub(crate) fn emit_execution_mode_local_size(&mut self, func_id: u32, x: u32, y: u32, z: u32) {
221        self.emit(
222            OP_EXECUTION_MODE,
223            &[func_id, EXECUTION_MODE_LOCAL_SIZE, x, y, z],
224        );
225    }
226
227    pub(crate) fn emit_decorate(&mut self, target: u32, decoration: u32, operands: &[u32]) {
228        let mut ops = vec![target, decoration];
229        ops.extend_from_slice(operands);
230        self.emit(OP_DECORATE, &ops);
231    }
232
233    pub(crate) fn emit_type_void(&mut self, id: u32) {
234        self.emit(OP_TYPE_VOID, &[id]);
235    }
236
237    pub(crate) fn emit_type_bool(&mut self, id: u32) {
238        self.emit(OP_TYPE_BOOL, &[id]);
239    }
240
241    pub(crate) fn emit_type_int(&mut self, id: u32, width: u32, signedness: u32) {
242        self.emit(OP_TYPE_INT, &[id, width, signedness]);
243    }
244
245    pub(crate) fn emit_type_float(&mut self, id: u32, width: u32) {
246        self.emit(OP_TYPE_FLOAT, &[id, width]);
247    }
248
249    pub(crate) fn emit_type_vector(&mut self, id: u32, component: u32, count: u32) {
250        self.emit(OP_TYPE_VECTOR, &[id, component, count]);
251    }
252
253    pub(crate) fn emit_type_pointer(&mut self, id: u32, storage_class: u32, pointee: u32) {
254        self.emit(OP_TYPE_POINTER, &[id, storage_class, pointee]);
255    }
256
257    pub(crate) fn emit_type_function(&mut self, id: u32, return_type: u32, params: &[u32]) {
258        let mut ops = vec![id, return_type];
259        ops.extend_from_slice(params);
260        self.emit(OP_TYPE_FUNCTION, &ops);
261    }
262
263    pub(crate) fn emit_constant_u32(&mut self, ty: u32, id: u32, value: u32) {
264        self.emit(OP_CONSTANT, &[ty, id, value]);
265    }
266
267    pub(crate) fn emit_constant_f32(&mut self, ty: u32, id: u32, value: f32) {
268        self.emit(OP_CONSTANT, &[ty, id, value.to_bits()]);
269    }
270
271    pub(crate) fn emit_variable(&mut self, ty: u32, id: u32, storage_class: u32) {
272        self.emit(OP_VARIABLE, &[ty, id, storage_class]);
273    }
274
275    pub(crate) fn emit_load(&mut self, result_ty: u32, result: u32, pointer: u32) {
276        self.emit(OP_LOAD, &[result_ty, result, pointer]);
277    }
278
279    pub(crate) fn emit_store(&mut self, pointer: u32, value: u32) {
280        self.emit(OP_STORE, &[pointer, value]);
281    }
282
283    pub(crate) fn emit_in_bounds_ptr_access_chain(
284        &mut self,
285        result_ty: u32,
286        result: u32,
287        base: u32,
288        element: u32,
289    ) {
290        self.emit(
291            OP_IN_BOUNDS_PTR_ACCESS_CHAIN,
292            &[result_ty, result, base, element],
293        );
294    }
295
296    pub(crate) fn emit_function(&mut self, result_ty: u32, result: u32, control: u32, fn_ty: u32) {
297        self.emit(OP_FUNCTION, &[result_ty, result, control, fn_ty]);
298    }
299
300    pub(crate) fn emit_function_parameter(&mut self, result_ty: u32, result: u32) {
301        self.emit(OP_FUNCTION_PARAMETER, &[result_ty, result]);
302    }
303
304    pub(crate) fn emit_label(&mut self, id: u32) {
305        self.emit(OP_LABEL, &[id]);
306    }
307
308    pub(crate) fn emit_return(&mut self) {
309        self.emit(OP_RETURN, &[]);
310    }
311
312    pub(crate) fn emit_function_end(&mut self) {
313        self.emit(OP_FUNCTION_END, &[]);
314    }
315
316    pub(crate) fn emit_branch(&mut self, target: u32) {
317        self.emit(OP_BRANCH, &[target]);
318    }
319
320    pub(crate) fn emit_branch_conditional(&mut self, cond: u32, true_label: u32, false_label: u32) {
321        self.emit(OP_BRANCH_CONDITIONAL, &[cond, true_label, false_label]);
322    }
323
324    pub(crate) fn emit_selection_merge(&mut self, merge_label: u32) {
325        self.emit(OP_SELECTION_MERGE, &[merge_label, SELECTION_CONTROL_NONE]);
326    }
327
328    pub(crate) fn emit_loop_merge(&mut self, merge_label: u32, continue_label: u32) {
329        self.emit(
330            OP_LOOP_MERGE,
331            &[merge_label, continue_label, LOOP_CONTROL_NONE],
332        );
333    }
334
335    pub(crate) fn emit_opencl_ext(
336        &mut self,
337        ext_id: u32,
338        result_ty: u32,
339        result: u32,
340        inst: u32,
341        args: &[u32],
342    ) {
343        let mut ops = vec![result_ty, result, ext_id, inst];
344        ops.extend_from_slice(args);
345        self.emit(OP_EXT_INST, &ops);
346    }
347}
348
349impl Default for SpvModule {
350    fn default() -> Self {
351        Self::new()
352    }
353}
354
355// ─── Common preamble for OpenCL SPIR-V kernels ──────────────
356
357/// IDs shared by all compute kernels.
358pub(crate) struct BaseIds {
359    pub(crate) ty_void: u32,
360    pub(crate) ty_bool: u32,
361    pub(crate) ty_uint: u32,
362    pub(crate) ty_float: u32,
363    #[allow(dead_code)]
364    pub(crate) ty_v3uint: u32,
365    #[allow(dead_code)]
366    pub(crate) ty_fn_void: u32,
367    #[allow(dead_code)]
368    pub(crate) ty_ptr_input_v3uint: u32,
369    pub(crate) ty_ptr_cross_float: u32,
370    pub(crate) ty_ptr_func_float: u32,
371    pub(crate) ty_ptr_func_uint: u32,
372    pub(crate) c_uint_0: u32,
373    pub(crate) c_uint_1: u32,
374    pub(crate) c_float_0: u32,
375    pub(crate) c_float_1: u32,
376    pub(crate) var_gid: u32,
377    pub(crate) opencl_ext: u32,
378    /// The kernel entry-point function id (allocated + entry-point emitted by
379    /// `emit_preamble` so `OpEntryPoint`/`OpExecutionMode` land in the correct
380    /// SPIR-V logical-layout position — before decorations/types/constants).
381    pub(crate) main_fn: u32,
382}
383
384/// Emit the preamble shared by all OpenCL-style compute kernels.
385///
386/// This emits — in the exact SPIR-V logical-layout order (spec §2.4) —
387/// capabilities, the OpenCL ext-inst import, memory model, the `OpEntryPoint`
388/// and `OpExecutionMode` for `entry_name`, then decorations, types, constants,
389/// and the `GlobalInvocationId` Input variable. The caller emits only the
390/// function type and body afterward; the entry-point function id is returned in
391/// [`BaseIds::main_fn`].
392pub(crate) fn emit_preamble(m: &mut SpvModule, entry_name: &str) -> BaseIds {
393    let ty_void = m.alloc_id();
394    let ty_bool = m.alloc_id();
395    let ty_uint = m.alloc_id();
396    let ty_float = m.alloc_id();
397    let ty_v3uint = m.alloc_id();
398    let ty_fn_void = m.alloc_id();
399    let ty_ptr_input_v3uint = m.alloc_id();
400    let ty_ptr_cross_float = m.alloc_id();
401    let ty_ptr_func_float = m.alloc_id();
402    let ty_ptr_func_uint = m.alloc_id();
403    let c_uint_0 = m.alloc_id();
404    let c_uint_1 = m.alloc_id();
405    let c_float_0 = m.alloc_id();
406    let c_float_1 = m.alloc_id();
407    let var_gid = m.alloc_id();
408    let opencl_ext = m.alloc_id();
409    let main_fn = m.alloc_id();
410
411    // Capabilities
412    m.emit_capability(CAPABILITY_KERNEL);
413    m.emit_capability(CAPABILITY_ADDRESSES);
414
415    // Extension import
416    m.emit_ext_inst_import(opencl_ext, "OpenCL.std");
417
418    // Memory model: Physical64 + OpenCL
419    m.emit_memory_model(ADDRESSING_MODEL_PHYSICAL64, MEMORY_MODEL_OPENCL);
420
421    // Entry point (section 5) and execution mode (section 6) MUST precede
422    // annotations (section 8) and types/constants/globals (section 9).
423    m.emit_entry_point(EXECUTION_MODEL_KERNEL, main_fn, entry_name, &[var_gid]);
424    m.emit_execution_mode_local_size(main_fn, WORKGROUP_SIZE, 1, 1);
425
426    // Decoration: GlobalInvocationId on var_gid
427    m.emit_decorate(var_gid, DECORATION_BUILTIN, &[BUILTIN_GLOBAL_INVOCATION_ID]);
428
429    // Types
430    m.emit_type_void(ty_void);
431    m.emit_type_bool(ty_bool);
432    m.emit_type_int(ty_uint, 32, 0);
433    m.emit_type_float(ty_float, 32);
434    m.emit_type_vector(ty_v3uint, ty_uint, 3);
435    m.emit_type_function(ty_fn_void, ty_void, &[]);
436    m.emit_type_pointer(ty_ptr_input_v3uint, STORAGE_CLASS_INPUT, ty_v3uint);
437    m.emit_type_pointer(ty_ptr_cross_float, STORAGE_CLASS_CROSS_WORKGROUP, ty_float);
438    m.emit_type_pointer(ty_ptr_func_float, STORAGE_CLASS_FUNCTION, ty_float);
439    m.emit_type_pointer(ty_ptr_func_uint, STORAGE_CLASS_FUNCTION, ty_uint);
440
441    // Constants
442    m.emit_constant_u32(ty_uint, c_uint_0, 0);
443    m.emit_constant_u32(ty_uint, c_uint_1, 1);
444    m.emit_constant_f32(ty_float, c_float_0, 0.0);
445    m.emit_constant_f32(ty_float, c_float_1, 1.0);
446
447    // GlobalInvocationId input variable
448    m.emit_variable(ty_ptr_input_v3uint, var_gid, STORAGE_CLASS_INPUT);
449
450    BaseIds {
451        ty_void,
452        ty_bool,
453        ty_uint,
454        ty_float,
455        ty_v3uint,
456        ty_fn_void,
457        ty_ptr_input_v3uint,
458        ty_ptr_cross_float,
459        ty_ptr_func_float,
460        ty_ptr_func_uint,
461        c_uint_0,
462        c_uint_1,
463        c_float_0,
464        c_float_1,
465        var_gid,
466        opencl_ext,
467        main_fn,
468    }
469}
470
471/// Load `GlobalInvocationId.x` into a uint result.
472pub(crate) fn load_gid_x(m: &mut SpvModule, b: &BaseIds) -> u32 {
473    let gid_val = m.alloc_id();
474    m.emit_load(b.ty_v3uint, gid_val, b.var_gid);
475    let gid_x = m.alloc_id();
476    m.emit(OP_COMPOSITE_EXTRACT, &[b.ty_uint, gid_x, gid_val, 0]);
477    gid_x
478}
479
480// ─── Unary compute kernel ───────────────────────────────────
481
482/// Generate an OpenCL SPIR-V compute kernel for an element-wise unary operation.
483///
484/// Kernel parameters: `(CrossWorkgroup float* input, CrossWorkgroup float* output, uint count)`.
485pub fn unary_compute_shader(op: UnaryOp) -> Vec<u32> {
486    let mut m = SpvModule::new();
487    let b = emit_preamble(&mut m, "main");
488
489    let fn_ty = m.alloc_id();
490    let p_input = m.alloc_id();
491    let p_output = m.alloc_id();
492    let p_count = m.alloc_id();
493
494    // Function type: void(CrossWorkgroup float*, CrossWorkgroup float*, uint)
495    m.emit_type_function(
496        fn_ty,
497        b.ty_void,
498        &[b.ty_ptr_cross_float, b.ty_ptr_cross_float, b.ty_uint],
499    );
500
501    // Labels
502    let label_entry = m.alloc_id();
503    let label_body = m.alloc_id();
504    let label_merge = m.alloc_id();
505
506    // Function
507    m.emit_function(b.ty_void, b.main_fn, FUNCTION_CONTROL_NONE, fn_ty);
508    m.emit_function_parameter(b.ty_ptr_cross_float, p_input);
509    m.emit_function_parameter(b.ty_ptr_cross_float, p_output);
510    m.emit_function_parameter(b.ty_uint, p_count);
511    m.emit_label(label_entry);
512
513    let gid = load_gid_x(&mut m, &b);
514
515    // Bounds check: gid < count
516    let cond = m.alloc_id();
517    m.emit(OP_U_LESS_THAN, &[b.ty_bool, cond, gid, p_count]);
518    m.emit_selection_merge(label_merge);
519    m.emit_branch_conditional(cond, label_body, label_merge);
520
521    m.emit_label(label_body);
522
523    // input_ptr = &input[gid]
524    let inp_ptr = m.alloc_id();
525    m.emit_in_bounds_ptr_access_chain(b.ty_ptr_cross_float, inp_ptr, p_input, gid);
526    let inp_val = m.alloc_id();
527    m.emit_load(b.ty_float, inp_val, inp_ptr);
528
529    let result = emit_unary_op(&mut m, &b, op, inp_val);
530
531    // output_ptr = &output[gid]
532    let out_ptr = m.alloc_id();
533    m.emit_in_bounds_ptr_access_chain(b.ty_ptr_cross_float, out_ptr, p_output, gid);
534    m.emit_store(out_ptr, result);
535
536    m.emit_branch(label_merge);
537
538    m.emit_label(label_merge);
539    m.emit_return();
540    m.emit_function_end();
541
542    m.finalize()
543}
544
545/// Emit the SPIR-V instructions for a unary operation, returning the result ID.
546fn emit_unary_op(m: &mut SpvModule, b: &BaseIds, op: UnaryOp, x: u32) -> u32 {
547    let result = m.alloc_id();
548    match op {
549        UnaryOp::Relu => {
550            m.emit_opencl_ext(
551                b.opencl_ext,
552                b.ty_float,
553                result,
554                OPENCL_FMAX,
555                &[b.c_float_0, x],
556            );
557        }
558        UnaryOp::Sigmoid => {
559            let neg_x = m.alloc_id();
560            m.emit(OP_F_NEGATE, &[b.ty_float, neg_x, x]);
561            let exp_neg_x = m.alloc_id();
562            m.emit_opencl_ext(b.opencl_ext, b.ty_float, exp_neg_x, OPENCL_EXP, &[neg_x]);
563            let one_plus = m.alloc_id();
564            m.emit(OP_F_ADD, &[b.ty_float, one_plus, b.c_float_1, exp_neg_x]);
565            m.emit(OP_F_DIV, &[b.ty_float, result, b.c_float_1, one_plus]);
566        }
567        UnaryOp::Tanh => {
568            m.emit_opencl_ext(b.opencl_ext, b.ty_float, result, OPENCL_TANH, &[x]);
569        }
570        UnaryOp::Exp => {
571            m.emit_opencl_ext(b.opencl_ext, b.ty_float, result, OPENCL_EXP, &[x]);
572        }
573        UnaryOp::Log => {
574            m.emit_opencl_ext(b.opencl_ext, b.ty_float, result, OPENCL_LOG, &[x]);
575        }
576        UnaryOp::Sqrt => {
577            m.emit_opencl_ext(b.opencl_ext, b.ty_float, result, OPENCL_SQRT, &[x]);
578        }
579        UnaryOp::Abs => {
580            m.emit_opencl_ext(b.opencl_ext, b.ty_float, result, OPENCL_FABS, &[x]);
581        }
582        UnaryOp::Neg => {
583            m.emit(OP_F_NEGATE, &[b.ty_float, result, x]);
584        }
585    }
586    result
587}
588
589// ─── Binary compute kernel ──────────────────────────────────
590
591/// Generate an OpenCL SPIR-V compute kernel for an element-wise binary operation.
592///
593/// Kernel parameters: `(CrossWorkgroup float* a, CrossWorkgroup float* b,
594///                      CrossWorkgroup float* output, uint count)`.
595pub fn binary_compute_shader(op: BinaryOp) -> Vec<u32> {
596    let mut m = SpvModule::new();
597    let b = emit_preamble(&mut m, "main");
598
599    let fn_ty = m.alloc_id();
600    let p_a = m.alloc_id();
601    let p_b = m.alloc_id();
602    let p_out = m.alloc_id();
603    let p_count = m.alloc_id();
604
605    // Function type: void(float*, float*, float*, uint)
606    m.emit_type_function(
607        fn_ty,
608        b.ty_void,
609        &[
610            b.ty_ptr_cross_float,
611            b.ty_ptr_cross_float,
612            b.ty_ptr_cross_float,
613            b.ty_uint,
614        ],
615    );
616
617    let label_entry = m.alloc_id();
618    let label_body = m.alloc_id();
619    let label_merge = m.alloc_id();
620
621    m.emit_function(b.ty_void, b.main_fn, FUNCTION_CONTROL_NONE, fn_ty);
622    m.emit_function_parameter(b.ty_ptr_cross_float, p_a);
623    m.emit_function_parameter(b.ty_ptr_cross_float, p_b);
624    m.emit_function_parameter(b.ty_ptr_cross_float, p_out);
625    m.emit_function_parameter(b.ty_uint, p_count);
626    m.emit_label(label_entry);
627
628    let gid = load_gid_x(&mut m, &b);
629
630    let cond = m.alloc_id();
631    m.emit(OP_U_LESS_THAN, &[b.ty_bool, cond, gid, p_count]);
632    m.emit_selection_merge(label_merge);
633    m.emit_branch_conditional(cond, label_body, label_merge);
634
635    m.emit_label(label_body);
636
637    let a_ptr = m.alloc_id();
638    m.emit_in_bounds_ptr_access_chain(b.ty_ptr_cross_float, a_ptr, p_a, gid);
639    let a_val = m.alloc_id();
640    m.emit_load(b.ty_float, a_val, a_ptr);
641
642    let b_ptr = m.alloc_id();
643    m.emit_in_bounds_ptr_access_chain(b.ty_ptr_cross_float, b_ptr, p_b, gid);
644    let b_val = m.alloc_id();
645    m.emit_load(b.ty_float, b_val, b_ptr);
646
647    let result = emit_binary_op(&mut m, &b, op, a_val, b_val);
648
649    let out_ptr = m.alloc_id();
650    m.emit_in_bounds_ptr_access_chain(b.ty_ptr_cross_float, out_ptr, p_out, gid);
651    m.emit_store(out_ptr, result);
652
653    m.emit_branch(label_merge);
654
655    m.emit_label(label_merge);
656    m.emit_return();
657    m.emit_function_end();
658
659    m.finalize()
660}
661
662fn emit_binary_op(m: &mut SpvModule, b: &BaseIds, op: BinaryOp, lhs: u32, rhs: u32) -> u32 {
663    let result = m.alloc_id();
664    match op {
665        BinaryOp::Add => m.emit(OP_F_ADD, &[b.ty_float, result, lhs, rhs]),
666        BinaryOp::Sub => m.emit(OP_F_SUB, &[b.ty_float, result, lhs, rhs]),
667        BinaryOp::Mul => m.emit(OP_F_MUL, &[b.ty_float, result, lhs, rhs]),
668        BinaryOp::Div => m.emit(OP_F_DIV, &[b.ty_float, result, lhs, rhs]),
669        BinaryOp::Max => {
670            m.emit_opencl_ext(b.opencl_ext, b.ty_float, result, OPENCL_FMAX, &[lhs, rhs]);
671        }
672        BinaryOp::Min => {
673            m.emit_opencl_ext(b.opencl_ext, b.ty_float, result, OPENCL_FMIN, &[lhs, rhs]);
674        }
675    }
676    result
677}
678
679// ─── Reduce compute kernel ──────────────────────────────────
680
681/// Generate an OpenCL SPIR-V compute kernel for reduction along an axis.
682///
683/// Kernel parameters: `(CrossWorkgroup float* input, CrossWorkgroup float* output,
684///                      uint outer_size, uint reduce_size, uint inner_size)`.
685///
686/// Each thread computes one output element by iterating over the reduce dimension.
687pub fn reduce_compute_shader(op: ReduceOp) -> Vec<u32> {
688    let mut m = SpvModule::new();
689    let b = emit_preamble(&mut m, "main");
690
691    let fn_ty = m.alloc_id();
692    let p_input = m.alloc_id();
693    let p_output = m.alloc_id();
694    let p_outer = m.alloc_id();
695    let p_reduce = m.alloc_id();
696    let p_inner = m.alloc_id();
697
698    // Function type: void(float*, float*, uint, uint, uint)
699    m.emit_type_function(
700        fn_ty,
701        b.ty_void,
702        &[
703            b.ty_ptr_cross_float,
704            b.ty_ptr_cross_float,
705            b.ty_uint,
706            b.ty_uint,
707            b.ty_uint,
708        ],
709    );
710
711    // Accumulator initial value. For Max/Min this is a ±infinity constant that
712    // MUST live in the module-global constants section (before OpFunction), so
713    // allocate + emit it here rather than inside the function body.
714    let init_val = match op {
715        ReduceOp::Sum | ReduceOp::Mean => b.c_float_0,
716        ReduceOp::Max => {
717            let neg_inf = m.alloc_id();
718            m.emit_constant_f32(b.ty_float, neg_inf, f32::NEG_INFINITY);
719            neg_inf
720        }
721        ReduceOp::Min => {
722            let pos_inf = m.alloc_id();
723            m.emit_constant_f32(b.ty_float, pos_inf, f32::INFINITY);
724            pos_inf
725        }
726    };
727
728    // Function-storage variables must be the first instructions of the first
729    // block; allocate their ids now and emit the OpVariables in `label_entry`.
730    let var_i = m.alloc_id();
731    let var_acc = m.alloc_id();
732
733    let label_entry = m.alloc_id();
734    let label_bounds_body = m.alloc_id();
735    let label_bounds_merge = m.alloc_id();
736    let label_loop_header = m.alloc_id();
737    let label_loop_body = m.alloc_id();
738    let label_loop_continue = m.alloc_id();
739    let label_loop_merge = m.alloc_id();
740
741    m.emit_function(b.ty_void, b.main_fn, FUNCTION_CONTROL_NONE, fn_ty);
742    m.emit_function_parameter(b.ty_ptr_cross_float, p_input);
743    m.emit_function_parameter(b.ty_ptr_cross_float, p_output);
744    m.emit_function_parameter(b.ty_uint, p_outer);
745    m.emit_function_parameter(b.ty_uint, p_reduce);
746    m.emit_function_parameter(b.ty_uint, p_inner);
747    m.emit_label(label_entry);
748
749    // Function-storage OpVariables: first instructions of the first block.
750    m.emit_variable(b.ty_ptr_func_uint, var_i, STORAGE_CLASS_FUNCTION);
751    m.emit_variable(b.ty_ptr_func_float, var_acc, STORAGE_CLASS_FUNCTION);
752
753    let gid = load_gid_x(&mut m, &b);
754
755    // total_output = outer_size * inner_size
756    let total_output = m.alloc_id();
757    m.emit(OP_I_MUL, &[b.ty_uint, total_output, p_outer, p_inner]);
758
759    // Bounds check: gid < total_output
760    let cond_bounds = m.alloc_id();
761    m.emit(OP_U_LESS_THAN, &[b.ty_bool, cond_bounds, gid, total_output]);
762    m.emit_selection_merge(label_bounds_merge);
763    m.emit_branch_conditional(cond_bounds, label_bounds_body, label_bounds_merge);
764
765    m.emit_label(label_bounds_body);
766
767    // outer_idx = gid / inner_size, inner_idx = gid % inner_size
768    let outer_idx = m.alloc_id();
769    m.emit(OP_U_DIV, &[b.ty_uint, outer_idx, gid, p_inner]);
770    let inner_idx = m.alloc_id();
771    m.emit(OP_U_MOD, &[b.ty_uint, inner_idx, gid, p_inner]);
772
773    // base = outer_idx * reduce_size * inner_size + inner_idx
774    let t1 = m.alloc_id();
775    m.emit(OP_I_MUL, &[b.ty_uint, t1, outer_idx, p_reduce]);
776    let t2 = m.alloc_id();
777    m.emit(OP_I_MUL, &[b.ty_uint, t2, t1, p_inner]);
778    let base_idx = m.alloc_id();
779    m.emit(OP_I_ADD, &[b.ty_uint, base_idx, t2, inner_idx]);
780
781    // Initialize the loop counter and accumulator (variables were declared in
782    // the entry block; only the stores live here).
783    m.emit_store(var_i, b.c_uint_0);
784    m.emit_store(var_acc, init_val);
785
786    m.emit_branch(label_loop_header);
787
788    // ── Loop header ──
789    m.emit_label(label_loop_header);
790    let i_val = m.alloc_id();
791    m.emit_load(b.ty_uint, i_val, var_i);
792    let loop_cond = m.alloc_id();
793    m.emit(OP_U_LESS_THAN, &[b.ty_bool, loop_cond, i_val, p_reduce]);
794    m.emit_loop_merge(label_loop_merge, label_loop_continue);
795    m.emit_branch_conditional(loop_cond, label_loop_body, label_loop_merge);
796
797    // ── Loop body ──
798    m.emit_label(label_loop_body);
799
800    // input_idx = base_idx + i * inner_size
801    let i_times_inner = m.alloc_id();
802    m.emit(OP_I_MUL, &[b.ty_uint, i_times_inner, i_val, p_inner]);
803    let input_idx = m.alloc_id();
804    m.emit(OP_I_ADD, &[b.ty_uint, input_idx, base_idx, i_times_inner]);
805
806    let inp_ptr = m.alloc_id();
807    m.emit_in_bounds_ptr_access_chain(b.ty_ptr_cross_float, inp_ptr, p_input, input_idx);
808    let inp_val = m.alloc_id();
809    m.emit_load(b.ty_float, inp_val, inp_ptr);
810
811    let acc_val = m.alloc_id();
812    m.emit_load(b.ty_float, acc_val, var_acc);
813
814    let new_acc = m.alloc_id();
815    match op {
816        ReduceOp::Sum | ReduceOp::Mean => {
817            m.emit(OP_F_ADD, &[b.ty_float, new_acc, acc_val, inp_val]);
818        }
819        ReduceOp::Max => {
820            m.emit_opencl_ext(
821                b.opencl_ext,
822                b.ty_float,
823                new_acc,
824                OPENCL_FMAX,
825                &[acc_val, inp_val],
826            );
827        }
828        ReduceOp::Min => {
829            m.emit_opencl_ext(
830                b.opencl_ext,
831                b.ty_float,
832                new_acc,
833                OPENCL_FMIN,
834                &[acc_val, inp_val],
835            );
836        }
837    }
838    m.emit_store(var_acc, new_acc);
839
840    m.emit_branch(label_loop_continue);
841
842    // ── Loop continue ──
843    m.emit_label(label_loop_continue);
844    let i_inc = m.alloc_id();
845    m.emit(OP_I_ADD, &[b.ty_uint, i_inc, i_val, b.c_uint_1]);
846    m.emit_store(var_i, i_inc);
847    m.emit_branch(label_loop_header);
848
849    // ── Loop merge ──
850    m.emit_label(label_loop_merge);
851
852    let final_acc = m.alloc_id();
853    m.emit_load(b.ty_float, final_acc, var_acc);
854
855    let store_val = if op == ReduceOp::Mean {
856        let reduce_f = m.alloc_id();
857        m.emit(OP_CONVERT_U_TO_F, &[b.ty_float, reduce_f, p_reduce]);
858        let mean_val = m.alloc_id();
859        m.emit(OP_F_DIV, &[b.ty_float, mean_val, final_acc, reduce_f]);
860        mean_val
861    } else {
862        final_acc
863    };
864
865    let out_ptr = m.alloc_id();
866    m.emit_in_bounds_ptr_access_chain(b.ty_ptr_cross_float, out_ptr, p_output, gid);
867    m.emit_store(out_ptr, store_val);
868
869    m.emit_branch(label_bounds_merge);
870
871    m.emit_label(label_bounds_merge);
872    m.emit_return();
873    m.emit_function_end();
874
875    m.finalize()
876}
877
878// ─── GEMM compute kernel ────────────────────────────────────
879
880/// Generate an OpenCL SPIR-V compute kernel for GEMM: `C = alpha * A * B + beta * C`.
881///
882/// Naive reference (one thread per output element, row-major f32 layout).
883///
884/// Kernel parameters: `(CrossWorkgroup float* A, CrossWorkgroup float* B,
885///                      CrossWorkgroup float* C, uint m, uint n, uint k,
886///                      float alpha, float beta)`.
887pub fn gemm_compute_shader() -> Vec<u32> {
888    let mut m = SpvModule::new();
889    let b = emit_preamble(&mut m, "main");
890
891    let fn_ty = m.alloc_id();
892    let p_a = m.alloc_id();
893    let p_b = m.alloc_id();
894    let p_c = m.alloc_id();
895    let p_m = m.alloc_id();
896    let p_n = m.alloc_id();
897    let p_k = m.alloc_id();
898    let p_alpha = m.alloc_id();
899    let p_beta = m.alloc_id();
900
901    // Function type: void(float*, float*, float*, uint, uint, uint, float, float)
902    m.emit_type_function(
903        fn_ty,
904        b.ty_void,
905        &[
906            b.ty_ptr_cross_float,
907            b.ty_ptr_cross_float,
908            b.ty_ptr_cross_float,
909            b.ty_uint,
910            b.ty_uint,
911            b.ty_uint,
912            b.ty_float,
913            b.ty_float,
914        ],
915    );
916
917    // Function-storage variables must be the first instructions of the first
918    // block; allocate ids now and emit the OpVariables in `label_entry`.
919    let var_i = m.alloc_id();
920    let var_acc = m.alloc_id();
921
922    let label_entry = m.alloc_id();
923    let label_bounds_body = m.alloc_id();
924    let label_bounds_merge = m.alloc_id();
925    let label_loop_header = m.alloc_id();
926    let label_loop_body = m.alloc_id();
927    let label_loop_continue = m.alloc_id();
928    let label_loop_merge = m.alloc_id();
929
930    m.emit_function(b.ty_void, b.main_fn, FUNCTION_CONTROL_NONE, fn_ty);
931    m.emit_function_parameter(b.ty_ptr_cross_float, p_a);
932    m.emit_function_parameter(b.ty_ptr_cross_float, p_b);
933    m.emit_function_parameter(b.ty_ptr_cross_float, p_c);
934    m.emit_function_parameter(b.ty_uint, p_m);
935    m.emit_function_parameter(b.ty_uint, p_n);
936    m.emit_function_parameter(b.ty_uint, p_k);
937    m.emit_function_parameter(b.ty_float, p_alpha);
938    m.emit_function_parameter(b.ty_float, p_beta);
939    m.emit_label(label_entry);
940
941    // Function-storage OpVariables: first instructions of the first block.
942    m.emit_variable(b.ty_ptr_func_uint, var_i, STORAGE_CLASS_FUNCTION);
943    m.emit_variable(b.ty_ptr_func_float, var_acc, STORAGE_CLASS_FUNCTION);
944
945    let gid = load_gid_x(&mut m, &b);
946
947    // total = m * n
948    let total = m.alloc_id();
949    m.emit(OP_I_MUL, &[b.ty_uint, total, p_m, p_n]);
950
951    // Bounds check
952    let cond = m.alloc_id();
953    m.emit(OP_U_LESS_THAN, &[b.ty_bool, cond, gid, total]);
954    m.emit_selection_merge(label_bounds_merge);
955    m.emit_branch_conditional(cond, label_bounds_body, label_bounds_merge);
956
957    m.emit_label(label_bounds_body);
958
959    // row = gid / n, col = gid % n
960    let row = m.alloc_id();
961    m.emit(OP_U_DIV, &[b.ty_uint, row, gid, p_n]);
962    let col = m.alloc_id();
963    m.emit(OP_U_MOD, &[b.ty_uint, col, gid, p_n]);
964
965    // Initialize loop counter + accumulator (declared in the entry block).
966    m.emit_store(var_i, b.c_uint_0);
967    m.emit_store(var_acc, b.c_float_0);
968
969    m.emit_branch(label_loop_header);
970
971    // ── Loop header ──
972    m.emit_label(label_loop_header);
973    let i_val = m.alloc_id();
974    m.emit_load(b.ty_uint, i_val, var_i);
975    let loop_cond = m.alloc_id();
976    m.emit(OP_U_LESS_THAN, &[b.ty_bool, loop_cond, i_val, p_k]);
977    m.emit_loop_merge(label_loop_merge, label_loop_continue);
978    m.emit_branch_conditional(loop_cond, label_loop_body, label_loop_merge);
979
980    // ── Loop body ──
981    m.emit_label(label_loop_body);
982
983    // a_idx = row * k + i
984    let row_k = m.alloc_id();
985    m.emit(OP_I_MUL, &[b.ty_uint, row_k, row, p_k]);
986    let a_idx = m.alloc_id();
987    m.emit(OP_I_ADD, &[b.ty_uint, a_idx, row_k, i_val]);
988
989    // b_idx = i * n + col
990    let i_n = m.alloc_id();
991    m.emit(OP_I_MUL, &[b.ty_uint, i_n, i_val, p_n]);
992    let b_idx = m.alloc_id();
993    m.emit(OP_I_ADD, &[b.ty_uint, b_idx, i_n, col]);
994
995    let a_ptr = m.alloc_id();
996    m.emit_in_bounds_ptr_access_chain(b.ty_ptr_cross_float, a_ptr, p_a, a_idx);
997    let a_val = m.alloc_id();
998    m.emit_load(b.ty_float, a_val, a_ptr);
999
1000    let b_ptr = m.alloc_id();
1001    m.emit_in_bounds_ptr_access_chain(b.ty_ptr_cross_float, b_ptr, p_b, b_idx);
1002    let b_val = m.alloc_id();
1003    m.emit_load(b.ty_float, b_val, b_ptr);
1004
1005    let prod = m.alloc_id();
1006    m.emit(OP_F_MUL, &[b.ty_float, prod, a_val, b_val]);
1007    let old_acc = m.alloc_id();
1008    m.emit_load(b.ty_float, old_acc, var_acc);
1009    let new_acc = m.alloc_id();
1010    m.emit(OP_F_ADD, &[b.ty_float, new_acc, old_acc, prod]);
1011    m.emit_store(var_acc, new_acc);
1012
1013    m.emit_branch(label_loop_continue);
1014
1015    // ── Loop continue ──
1016    m.emit_label(label_loop_continue);
1017    let i_inc = m.alloc_id();
1018    m.emit(OP_I_ADD, &[b.ty_uint, i_inc, i_val, b.c_uint_1]);
1019    m.emit_store(var_i, i_inc);
1020    m.emit_branch(label_loop_header);
1021
1022    // ── Loop merge ──
1023    m.emit_label(label_loop_merge);
1024
1025    // result = alpha * acc + beta * C[gid]
1026    let final_acc = m.alloc_id();
1027    m.emit_load(b.ty_float, final_acc, var_acc);
1028    let alpha_acc = m.alloc_id();
1029    m.emit(OP_F_MUL, &[b.ty_float, alpha_acc, p_alpha, final_acc]);
1030
1031    let c_ptr = m.alloc_id();
1032    m.emit_in_bounds_ptr_access_chain(b.ty_ptr_cross_float, c_ptr, p_c, gid);
1033    let c_old = m.alloc_id();
1034    m.emit_load(b.ty_float, c_old, c_ptr);
1035    let beta_c = m.alloc_id();
1036    m.emit(OP_F_MUL, &[b.ty_float, beta_c, p_beta, c_old]);
1037    let c_new = m.alloc_id();
1038    m.emit(OP_F_ADD, &[b.ty_float, c_new, alpha_acc, beta_c]);
1039    m.emit_store(c_ptr, c_new);
1040
1041    m.emit_branch(label_bounds_merge);
1042
1043    m.emit_label(label_bounds_merge);
1044    m.emit_return();
1045    m.emit_function_end();
1046
1047    m.finalize()
1048}
1049
1050// ─── Batched GEMM compute kernel ─────────────────────────────
1051
1052/// Load `GlobalInvocationId.z` into a uint result.
1053fn load_gid_z(m: &mut SpvModule, b: &BaseIds) -> u32 {
1054    let gid_val = m.alloc_id();
1055    m.emit_load(b.ty_v3uint, gid_val, b.var_gid);
1056    let gid_z = m.alloc_id();
1057    m.emit(OP_COMPOSITE_EXTRACT, &[b.ty_uint, gid_z, gid_val, 2]);
1058    gid_z
1059}
1060
1061/// Generate an OpenCL SPIR-V compute kernel for batched GEMM.
1062///
1063/// For each batch `b` in `0..batch_count`:
1064///   `C_b = alpha * A_b * B_b + beta * C_b`
1065/// where `A_b` starts at offset `b * stride_a`, etc.
1066///
1067/// Uses 3D global work size `(ceil(m*n / WG), 1, batch_count)`:
1068/// - `get_global_id(0)` = element index within a single m×n output
1069/// - `get_global_id(2)` = batch index
1070///
1071/// Kernel parameters:
1072/// `(CrossWorkgroup float* A, CrossWorkgroup float* B, CrossWorkgroup float* C,
1073///   uint m, uint n, uint k, float alpha, float beta,
1074///   uint batch_count, uint stride_a, uint stride_b, uint stride_c)`.
1075pub fn batched_gemm_compute_shader() -> Vec<u32> {
1076    let mut m = SpvModule::new();
1077    let b = emit_preamble(&mut m, "main");
1078
1079    let fn_ty = m.alloc_id();
1080    let p_a = m.alloc_id();
1081    let p_b = m.alloc_id();
1082    let p_c = m.alloc_id();
1083    let p_m = m.alloc_id();
1084    let p_n = m.alloc_id();
1085    let p_k = m.alloc_id();
1086    let p_alpha = m.alloc_id();
1087    let p_beta = m.alloc_id();
1088    let p_batch_count = m.alloc_id();
1089    let p_stride_a = m.alloc_id();
1090    let p_stride_b = m.alloc_id();
1091    let p_stride_c = m.alloc_id();
1092
1093    // Function type: void(float*, float*, float*, uint, uint, uint, float, float,
1094    //                      uint, uint, uint, uint)
1095    m.emit_type_function(
1096        fn_ty,
1097        b.ty_void,
1098        &[
1099            b.ty_ptr_cross_float,
1100            b.ty_ptr_cross_float,
1101            b.ty_ptr_cross_float,
1102            b.ty_uint,
1103            b.ty_uint,
1104            b.ty_uint,
1105            b.ty_float,
1106            b.ty_float,
1107            b.ty_uint,
1108            b.ty_uint,
1109            b.ty_uint,
1110            b.ty_uint,
1111        ],
1112    );
1113
1114    // Function-storage variables must be the first instructions of the first
1115    // block; allocate ids now and emit the OpVariables in `label_entry`.
1116    let var_i = m.alloc_id();
1117    let var_acc = m.alloc_id();
1118
1119    let label_entry = m.alloc_id();
1120    let label_bounds_body = m.alloc_id();
1121    let label_bounds_merge = m.alloc_id();
1122    let label_loop_header = m.alloc_id();
1123    let label_loop_body = m.alloc_id();
1124    let label_loop_continue = m.alloc_id();
1125    let label_loop_merge = m.alloc_id();
1126
1127    m.emit_function(b.ty_void, b.main_fn, FUNCTION_CONTROL_NONE, fn_ty);
1128    m.emit_function_parameter(b.ty_ptr_cross_float, p_a);
1129    m.emit_function_parameter(b.ty_ptr_cross_float, p_b);
1130    m.emit_function_parameter(b.ty_ptr_cross_float, p_c);
1131    m.emit_function_parameter(b.ty_uint, p_m);
1132    m.emit_function_parameter(b.ty_uint, p_n);
1133    m.emit_function_parameter(b.ty_uint, p_k);
1134    m.emit_function_parameter(b.ty_float, p_alpha);
1135    m.emit_function_parameter(b.ty_float, p_beta);
1136    m.emit_function_parameter(b.ty_uint, p_batch_count);
1137    m.emit_function_parameter(b.ty_uint, p_stride_a);
1138    m.emit_function_parameter(b.ty_uint, p_stride_b);
1139    m.emit_function_parameter(b.ty_uint, p_stride_c);
1140    m.emit_label(label_entry);
1141
1142    // Function-storage OpVariables: first instructions of the first block.
1143    m.emit_variable(b.ty_ptr_func_uint, var_i, STORAGE_CLASS_FUNCTION);
1144    m.emit_variable(b.ty_ptr_func_float, var_acc, STORAGE_CLASS_FUNCTION);
1145
1146    // gid_x = element index within single GEMM output
1147    let gid = load_gid_x(&mut m, &b);
1148    // gid_z = batch index
1149    let batch_idx = load_gid_z(&mut m, &b);
1150
1151    // total = m * n
1152    let total = m.alloc_id();
1153    m.emit(OP_I_MUL, &[b.ty_uint, total, p_m, p_n]);
1154
1155    // Bounds check: gid < total && batch_idx < batch_count
1156    let cond1 = m.alloc_id();
1157    m.emit(OP_U_LESS_THAN, &[b.ty_bool, cond1, gid, total]);
1158    let cond2 = m.alloc_id();
1159    m.emit(
1160        OP_U_LESS_THAN,
1161        &[b.ty_bool, cond2, batch_idx, p_batch_count],
1162    );
1163    // Combined condition via OpLogicalAnd
1164    let cond = m.alloc_id();
1165    // OpLogicalAnd = 166
1166    m.emit(166, &[b.ty_bool, cond, cond1, cond2]);
1167    m.emit_selection_merge(label_bounds_merge);
1168    m.emit_branch_conditional(cond, label_bounds_body, label_bounds_merge);
1169
1170    m.emit_label(label_bounds_body);
1171
1172    // Compute batch offsets: a_base = batch_idx * stride_a, etc.
1173    let a_offset = m.alloc_id();
1174    m.emit(OP_I_MUL, &[b.ty_uint, a_offset, batch_idx, p_stride_a]);
1175    let b_offset = m.alloc_id();
1176    m.emit(OP_I_MUL, &[b.ty_uint, b_offset, batch_idx, p_stride_b]);
1177    let c_offset = m.alloc_id();
1178    m.emit(OP_I_MUL, &[b.ty_uint, c_offset, batch_idx, p_stride_c]);
1179
1180    // Offset the base pointers
1181    let a_batch = m.alloc_id();
1182    m.emit_in_bounds_ptr_access_chain(b.ty_ptr_cross_float, a_batch, p_a, a_offset);
1183    let b_batch = m.alloc_id();
1184    m.emit_in_bounds_ptr_access_chain(b.ty_ptr_cross_float, b_batch, p_b, b_offset);
1185    let c_batch = m.alloc_id();
1186    m.emit_in_bounds_ptr_access_chain(b.ty_ptr_cross_float, c_batch, p_c, c_offset);
1187
1188    // row = gid / n, col = gid % n
1189    let row = m.alloc_id();
1190    m.emit(OP_U_DIV, &[b.ty_uint, row, gid, p_n]);
1191    let col = m.alloc_id();
1192    m.emit(OP_U_MOD, &[b.ty_uint, col, gid, p_n]);
1193
1194    // Initialize loop counter + accumulator (declared in the entry block).
1195    m.emit_store(var_i, b.c_uint_0);
1196    m.emit_store(var_acc, b.c_float_0);
1197
1198    m.emit_branch(label_loop_header);
1199
1200    // ── Loop header ──
1201    m.emit_label(label_loop_header);
1202    let i_val = m.alloc_id();
1203    m.emit_load(b.ty_uint, i_val, var_i);
1204    let loop_cond = m.alloc_id();
1205    m.emit(OP_U_LESS_THAN, &[b.ty_bool, loop_cond, i_val, p_k]);
1206    m.emit_loop_merge(label_loop_merge, label_loop_continue);
1207    m.emit_branch_conditional(loop_cond, label_loop_body, label_loop_merge);
1208
1209    // ── Loop body ──
1210    m.emit_label(label_loop_body);
1211
1212    // a_idx = row * k + i
1213    let row_k = m.alloc_id();
1214    m.emit(OP_I_MUL, &[b.ty_uint, row_k, row, p_k]);
1215    let a_idx = m.alloc_id();
1216    m.emit(OP_I_ADD, &[b.ty_uint, a_idx, row_k, i_val]);
1217
1218    // b_idx = i * n + col
1219    let i_n = m.alloc_id();
1220    m.emit(OP_I_MUL, &[b.ty_uint, i_n, i_val, p_n]);
1221    let b_idx = m.alloc_id();
1222    m.emit(OP_I_ADD, &[b.ty_uint, b_idx, i_n, col]);
1223
1224    let a_ptr = m.alloc_id();
1225    m.emit_in_bounds_ptr_access_chain(b.ty_ptr_cross_float, a_ptr, a_batch, a_idx);
1226    let a_val = m.alloc_id();
1227    m.emit_load(b.ty_float, a_val, a_ptr);
1228
1229    let b_ptr = m.alloc_id();
1230    m.emit_in_bounds_ptr_access_chain(b.ty_ptr_cross_float, b_ptr, b_batch, b_idx);
1231    let b_val = m.alloc_id();
1232    m.emit_load(b.ty_float, b_val, b_ptr);
1233
1234    let prod = m.alloc_id();
1235    m.emit(OP_F_MUL, &[b.ty_float, prod, a_val, b_val]);
1236    let old_acc = m.alloc_id();
1237    m.emit_load(b.ty_float, old_acc, var_acc);
1238    let new_acc = m.alloc_id();
1239    m.emit(OP_F_ADD, &[b.ty_float, new_acc, old_acc, prod]);
1240    m.emit_store(var_acc, new_acc);
1241
1242    m.emit_branch(label_loop_continue);
1243
1244    // ── Loop continue ──
1245    m.emit_label(label_loop_continue);
1246    let i_inc = m.alloc_id();
1247    m.emit(OP_I_ADD, &[b.ty_uint, i_inc, i_val, b.c_uint_1]);
1248    m.emit_store(var_i, i_inc);
1249    m.emit_branch(label_loop_header);
1250
1251    // ── Loop merge ──
1252    m.emit_label(label_loop_merge);
1253
1254    // result = alpha * acc + beta * C_batch[gid]
1255    let final_acc = m.alloc_id();
1256    m.emit_load(b.ty_float, final_acc, var_acc);
1257    let alpha_acc = m.alloc_id();
1258    m.emit(OP_F_MUL, &[b.ty_float, alpha_acc, p_alpha, final_acc]);
1259
1260    let c_ptr = m.alloc_id();
1261    m.emit_in_bounds_ptr_access_chain(b.ty_ptr_cross_float, c_ptr, c_batch, gid);
1262    let c_old = m.alloc_id();
1263    m.emit_load(b.ty_float, c_old, c_ptr);
1264    let beta_c = m.alloc_id();
1265    m.emit(OP_F_MUL, &[b.ty_float, beta_c, p_beta, c_old]);
1266    let c_new = m.alloc_id();
1267    m.emit(OP_F_ADD, &[b.ty_float, c_new, alpha_acc, beta_c]);
1268    m.emit_store(c_ptr, c_new);
1269
1270    m.emit_branch(label_bounds_merge);
1271
1272    m.emit_label(label_bounds_merge);
1273    m.emit_return();
1274    m.emit_function_end();
1275
1276    m.finalize()
1277}
1278
1279// ─── Trivial placeholder ────────────────────────────────────
1280
1281/// Build a minimal valid Shader-style compute shader: `void main() {}`.
1282///
1283/// Uses `GLCompute` / Shader capability for basic Level Zero module validation.
1284pub fn trivial_compute_shader() -> Vec<u32> {
1285    let mut m = SpvModule::new();
1286
1287    let id_main_fn = m.alloc_id();
1288    let id_void = m.alloc_id();
1289    let id_void_fn = m.alloc_id();
1290    let id_label = m.alloc_id();
1291
1292    m.emit_capability(CAPABILITY_SHADER);
1293    m.emit_memory_model(ADDRESSING_MODEL_LOGICAL, MEMORY_MODEL_GLSL450);
1294
1295    let mut entry_words = vec![EXECUTION_MODEL_GLCOMPUTE, id_main_fn];
1296    entry_words.extend(SpvModule::string_words("main"));
1297    m.emit(OP_ENTRY_POINT, &entry_words);
1298
1299    m.emit_execution_mode_local_size(id_main_fn, 1, 1, 1);
1300
1301    m.emit_type_void(id_void);
1302    m.emit_type_function(id_void_fn, id_void, &[]);
1303
1304    m.emit_function(id_void, id_main_fn, FUNCTION_CONTROL_NONE, id_void_fn);
1305    m.emit_label(id_label);
1306    m.emit_return();
1307    m.emit_function_end();
1308
1309    m.finalize()
1310}
1311
1312/// Return the trivial compute shader as a byte slice suitable for
1313/// passing to Level Zero module creation.
1314pub fn trivial_compute_shader_bytes() -> Vec<u8> {
1315    trivial_compute_shader()
1316        .iter()
1317        .flat_map(|w| w.to_ne_bytes())
1318        .collect()
1319}
1320
1321// ─── Extended transcendental-math kernels ───────────────────
1322
1323/// A transcendental function from the `OpenCL.std` extended instruction set
1324/// not already covered by [`UnaryOp`].
1325///
1326/// These extend OpenCL extended-instruction-set coverage beyond the
1327/// relu/gelu/exp/log set with the elementary functions needed for activation
1328/// fusions, error-function-based GELU, and angle math.
1329#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1330pub enum ExtMathFn {
1331    /// `erf(x)` — Gauss error function (exact-GELU building block).
1332    Erf,
1333    /// `erfc(x)` — complementary error function.
1334    Erfc,
1335    /// `sin(x)`.
1336    Sin,
1337    /// `cos(x)`.
1338    Cos,
1339    /// `atan(x)`.
1340    Atan,
1341    /// `tanh(x)` (also reachable via `UnaryOp::Tanh`).
1342    Tanh,
1343    /// `cbrt(x)` — cube root.
1344    Cbrt,
1345    /// `rsqrt(x)` — reciprocal square root.
1346    Rsqrt,
1347}
1348
1349impl ExtMathFn {
1350    /// The `OpenCL.std` extended-instruction number for a single-argument call.
1351    #[must_use]
1352    pub fn opencl_inst(self) -> u32 {
1353        match self {
1354            ExtMathFn::Erf => OPENCL_ERF,
1355            ExtMathFn::Erfc => OPENCL_ERFC,
1356            ExtMathFn::Sin => OPENCL_SIN,
1357            ExtMathFn::Cos => OPENCL_COS,
1358            ExtMathFn::Atan => OPENCL_ATAN,
1359            ExtMathFn::Tanh => OPENCL_TANH,
1360            ExtMathFn::Cbrt => OPENCL_CBRT,
1361            ExtMathFn::Rsqrt => OPENCL_RSQRT,
1362        }
1363    }
1364
1365    /// The lowercase function name (used as the kernel entry-point suffix).
1366    #[must_use]
1367    pub fn name(self) -> &'static str {
1368        match self {
1369            ExtMathFn::Erf => "erf",
1370            ExtMathFn::Erfc => "erfc",
1371            ExtMathFn::Sin => "sin",
1372            ExtMathFn::Cos => "cos",
1373            ExtMathFn::Atan => "atan",
1374            ExtMathFn::Tanh => "tanh",
1375            ExtMathFn::Cbrt => "cbrt",
1376            ExtMathFn::Rsqrt => "rsqrt",
1377        }
1378    }
1379}
1380
1381/// Generate an OpenCL SPIR-V kernel applying a single-argument transcendental
1382/// [`ExtMathFn`] element-wise.
1383///
1384/// Kernel parameters: `(CrossWorkgroup float* input, CrossWorkgroup float* output, uint count)`.
1385/// Entry-point name: `"main"`.
1386pub fn ext_math_compute_shader(func: ExtMathFn) -> Vec<u32> {
1387    let mut m = SpvModule::new();
1388    let b = emit_preamble(&mut m, "main");
1389
1390    let fn_ty = m.alloc_id();
1391    let p_input = m.alloc_id();
1392    let p_output = m.alloc_id();
1393    let p_count = m.alloc_id();
1394
1395    m.emit_type_function(
1396        fn_ty,
1397        b.ty_void,
1398        &[b.ty_ptr_cross_float, b.ty_ptr_cross_float, b.ty_uint],
1399    );
1400
1401    let label_entry = m.alloc_id();
1402    let label_body = m.alloc_id();
1403    let label_merge = m.alloc_id();
1404
1405    m.emit_function(b.ty_void, b.main_fn, FUNCTION_CONTROL_NONE, fn_ty);
1406    m.emit_function_parameter(b.ty_ptr_cross_float, p_input);
1407    m.emit_function_parameter(b.ty_ptr_cross_float, p_output);
1408    m.emit_function_parameter(b.ty_uint, p_count);
1409    m.emit_label(label_entry);
1410
1411    let gid = load_gid_x(&mut m, &b);
1412    let cond = m.alloc_id();
1413    m.emit(OP_U_LESS_THAN, &[b.ty_bool, cond, gid, p_count]);
1414    m.emit_selection_merge(label_merge);
1415    m.emit_branch_conditional(cond, label_body, label_merge);
1416
1417    m.emit_label(label_body);
1418
1419    let inp_ptr = m.alloc_id();
1420    m.emit_in_bounds_ptr_access_chain(b.ty_ptr_cross_float, inp_ptr, p_input, gid);
1421    let inp_val = m.alloc_id();
1422    m.emit_load(b.ty_float, inp_val, inp_ptr);
1423
1424    let result = m.alloc_id();
1425    m.emit_opencl_ext(
1426        b.opencl_ext,
1427        b.ty_float,
1428        result,
1429        func.opencl_inst(),
1430        &[inp_val],
1431    );
1432
1433    let out_ptr = m.alloc_id();
1434    m.emit_in_bounds_ptr_access_chain(b.ty_ptr_cross_float, out_ptr, p_output, gid);
1435    m.emit_store(out_ptr, result);
1436
1437    m.emit_branch(label_merge);
1438    m.emit_label(label_merge);
1439    m.emit_return();
1440    m.emit_function_end();
1441
1442    m.finalize()
1443}
1444
1445/// Generate an OpenCL SPIR-V kernel computing `atan2(y, x)` element-wise.
1446///
1447/// `atan2` is the canonical two-argument extended instruction; this exercises
1448/// the `OpExtInst` path with multiple arguments.
1449///
1450/// Kernel parameters: `(CrossWorkgroup float* y, CrossWorkgroup float* x,
1451///                      CrossWorkgroup float* output, uint count)`.
1452/// Entry-point name: `"atan2"`.
1453pub fn atan2_compute_shader() -> Vec<u32> {
1454    let mut m = SpvModule::new();
1455    let b = emit_preamble(&mut m, "atan2");
1456
1457    let fn_ty = m.alloc_id();
1458    let p_y = m.alloc_id();
1459    let p_x = m.alloc_id();
1460    let p_out = m.alloc_id();
1461    let p_count = m.alloc_id();
1462
1463    m.emit_type_function(
1464        fn_ty,
1465        b.ty_void,
1466        &[
1467            b.ty_ptr_cross_float,
1468            b.ty_ptr_cross_float,
1469            b.ty_ptr_cross_float,
1470            b.ty_uint,
1471        ],
1472    );
1473
1474    let label_entry = m.alloc_id();
1475    let label_body = m.alloc_id();
1476    let label_merge = m.alloc_id();
1477
1478    m.emit_function(b.ty_void, b.main_fn, FUNCTION_CONTROL_NONE, fn_ty);
1479    m.emit_function_parameter(b.ty_ptr_cross_float, p_y);
1480    m.emit_function_parameter(b.ty_ptr_cross_float, p_x);
1481    m.emit_function_parameter(b.ty_ptr_cross_float, p_out);
1482    m.emit_function_parameter(b.ty_uint, p_count);
1483    m.emit_label(label_entry);
1484
1485    let gid = load_gid_x(&mut m, &b);
1486    let cond = m.alloc_id();
1487    m.emit(OP_U_LESS_THAN, &[b.ty_bool, cond, gid, p_count]);
1488    m.emit_selection_merge(label_merge);
1489    m.emit_branch_conditional(cond, label_body, label_merge);
1490
1491    m.emit_label(label_body);
1492
1493    let y_ptr = m.alloc_id();
1494    m.emit_in_bounds_ptr_access_chain(b.ty_ptr_cross_float, y_ptr, p_y, gid);
1495    let y_val = m.alloc_id();
1496    m.emit_load(b.ty_float, y_val, y_ptr);
1497
1498    let x_ptr = m.alloc_id();
1499    m.emit_in_bounds_ptr_access_chain(b.ty_ptr_cross_float, x_ptr, p_x, gid);
1500    let x_val = m.alloc_id();
1501    m.emit_load(b.ty_float, x_val, x_ptr);
1502
1503    let result = m.alloc_id();
1504    m.emit_opencl_ext(
1505        b.opencl_ext,
1506        b.ty_float,
1507        result,
1508        OPENCL_ATAN2,
1509        &[y_val, x_val],
1510    );
1511
1512    let out_ptr = m.alloc_id();
1513    m.emit_in_bounds_ptr_access_chain(b.ty_ptr_cross_float, out_ptr, p_out, gid);
1514    m.emit_store(out_ptr, result);
1515
1516    m.emit_branch(label_merge);
1517    m.emit_label(label_merge);
1518    m.emit_return();
1519    m.emit_function_end();
1520
1521    m.finalize()
1522}
1523
1524// ─── Tests ──────────────────────────────────────────────────
1525
1526#[cfg(test)]
1527mod tests {
1528    use super::*;
1529
1530    fn check_valid_spirv(words: &[u32]) {
1531        assert!(words.len() >= 5, "too short for SPIR-V header");
1532        assert_eq!(words[0], SPIRV_MAGIC, "bad magic");
1533        assert!(words[3] > 0, "ID bound must be > 0");
1534        assert_eq!(words[4], 0, "schema must be 0");
1535    }
1536
1537    #[test]
1538    fn placeholder_spv_valid_magic() {
1539        let words = trivial_compute_shader();
1540        check_valid_spirv(&words);
1541    }
1542
1543    #[test]
1544    fn placeholder_spv_word_aligned() {
1545        let bytes = trivial_compute_shader_bytes();
1546        assert_eq!(bytes.len() % 4, 0);
1547    }
1548
1549    #[test]
1550    fn placeholder_spv_version_and_schema() {
1551        let words = trivial_compute_shader();
1552        assert!(words[1] >= 0x0001_0000);
1553        assert_eq!(words[4], 0);
1554    }
1555
1556    #[test]
1557    fn placeholder_spv_nonzero_bound() {
1558        let words = trivial_compute_shader();
1559        assert!(words[3] > 0);
1560    }
1561
1562    #[test]
1563    fn spv_module_id_allocation_is_monotonic() {
1564        let mut m = SpvModule::new();
1565        let id1 = m.alloc_id();
1566        let id2 = m.alloc_id();
1567        assert!(id2 > id1);
1568    }
1569
1570    #[test]
1571    fn string_words_null_terminated() {
1572        let words = SpvModule::string_words("abc");
1573        assert!(!words.is_empty());
1574        let bytes: Vec<u8> = words.iter().flat_map(|w| w.to_le_bytes()).collect();
1575        assert_eq!(bytes[0], b'a');
1576        assert_eq!(bytes[1], b'b');
1577        assert_eq!(bytes[2], b'c');
1578        assert_eq!(bytes[3], 0);
1579    }
1580
1581    #[test]
1582    fn string_words_empty_string() {
1583        let words = SpvModule::string_words("");
1584        assert!(!words.is_empty());
1585        let bytes: Vec<u8> = words.iter().flat_map(|w| w.to_le_bytes()).collect();
1586        assert_eq!(bytes[0], 0);
1587    }
1588
1589    #[test]
1590    fn generator_magic_is_level_zero() {
1591        assert_eq!(SPIRV_GENERATOR, 0x000D_0002);
1592        assert_ne!(SPIRV_GENERATOR, 0x000D_0001);
1593    }
1594
1595    // ── Compute kernel generation ────────────────────────────
1596
1597    #[test]
1598    fn unary_shader_all_ops() {
1599        let ops = [
1600            UnaryOp::Relu,
1601            UnaryOp::Sigmoid,
1602            UnaryOp::Tanh,
1603            UnaryOp::Exp,
1604            UnaryOp::Log,
1605            UnaryOp::Sqrt,
1606            UnaryOp::Abs,
1607            UnaryOp::Neg,
1608        ];
1609        for op in ops {
1610            let words = unary_compute_shader(op);
1611            check_valid_spirv(&words);
1612        }
1613    }
1614
1615    #[test]
1616    fn binary_shader_all_ops() {
1617        let ops = [
1618            BinaryOp::Add,
1619            BinaryOp::Sub,
1620            BinaryOp::Mul,
1621            BinaryOp::Div,
1622            BinaryOp::Max,
1623            BinaryOp::Min,
1624        ];
1625        for op in ops {
1626            let words = binary_compute_shader(op);
1627            check_valid_spirv(&words);
1628        }
1629    }
1630
1631    #[test]
1632    fn reduce_shader_all_ops() {
1633        let ops = [ReduceOp::Sum, ReduceOp::Max, ReduceOp::Min, ReduceOp::Mean];
1634        for op in ops {
1635            let words = reduce_compute_shader(op);
1636            check_valid_spirv(&words);
1637        }
1638    }
1639
1640    #[test]
1641    fn gemm_shader_valid() {
1642        let words = gemm_compute_shader();
1643        check_valid_spirv(&words);
1644    }
1645
1646    #[test]
1647    fn batched_gemm_shader_valid() {
1648        let words = batched_gemm_compute_shader();
1649        check_valid_spirv(&words);
1650    }
1651
1652    #[test]
1653    fn batched_gemm_shader_word_aligned() {
1654        let words = batched_gemm_compute_shader();
1655        let bytes: Vec<u8> = words.iter().flat_map(|w| w.to_ne_bytes()).collect();
1656        assert_eq!(bytes.len() % 4, 0);
1657    }
1658
1659    #[test]
1660    fn batched_gemm_shader_uses_kernel_capability() {
1661        let words = batched_gemm_compute_shader();
1662        let cap_header = (2u32 << 16) | OP_CAPABILITY;
1663        assert_eq!(words[5], cap_header);
1664        assert_eq!(words[6], 6); // CAPABILITY_KERNEL
1665    }
1666
1667    #[test]
1668    fn all_kernel_shaders_word_aligned() {
1669        fn to_bytes(words: &[u32]) -> Vec<u8> {
1670            words.iter().flat_map(|w| w.to_ne_bytes()).collect()
1671        }
1672        assert_eq!(to_bytes(&unary_compute_shader(UnaryOp::Relu)).len() % 4, 0);
1673        assert_eq!(to_bytes(&binary_compute_shader(BinaryOp::Add)).len() % 4, 0);
1674        assert_eq!(to_bytes(&reduce_compute_shader(ReduceOp::Sum)).len() % 4, 0);
1675        assert_eq!(to_bytes(&gemm_compute_shader()).len() % 4, 0);
1676        assert_eq!(to_bytes(&batched_gemm_compute_shader()).len() % 4, 0);
1677    }
1678
1679    #[test]
1680    fn kernel_shaders_use_opencl_memory_model() {
1681        // Check that kernel shaders use Physical64 + OpenCL memory model,
1682        // while trivial shader uses Logical + GLSL450.
1683        let trivial = trivial_compute_shader();
1684        let unary = unary_compute_shader(UnaryOp::Relu);
1685
1686        // The trivial shader should contain the Shader capability (1)
1687        // The unary shader should contain the Kernel capability (6)
1688        // These appear in OpCapability instructions after the header.
1689
1690        // After header (5 words), first instruction is OpCapability
1691        // Format: (2 << 16) | 17 = 0x00020011
1692        let cap_header = (2u32 << 16) | OP_CAPABILITY;
1693        assert_eq!(trivial[5], cap_header);
1694        assert_eq!(trivial[6], CAPABILITY_SHADER);
1695        assert_eq!(unary[5], cap_header);
1696        assert_eq!(unary[6], CAPABILITY_KERNEL);
1697    }
1698
1699    // ── Extended transcendental kernels ──────────────────────
1700
1701    /// Decode the instruction stream after the header into `(opcode, operands)`.
1702    fn decode_insts(words: &[u32]) -> Vec<(u32, Vec<u32>)> {
1703        let mut out = Vec::new();
1704        let mut i = 5;
1705        while i < words.len() {
1706            let wc = (words[i] >> 16) as usize;
1707            let op = words[i] & 0xffff;
1708            if wc == 0 || i + wc > words.len() {
1709                break;
1710            }
1711            out.push((op, words[i + 1..i + wc].to_vec()));
1712            i += wc;
1713        }
1714        out
1715    }
1716
1717    #[test]
1718    fn ext_math_fn_metadata() {
1719        assert_eq!(ExtMathFn::Erf.opencl_inst(), 18);
1720        assert_eq!(ExtMathFn::Erfc.opencl_inst(), 17);
1721        assert_eq!(ExtMathFn::Atan.opencl_inst(), 6);
1722        assert_eq!(ExtMathFn::Cos.opencl_inst(), 14);
1723        assert_eq!(ExtMathFn::Cbrt.opencl_inst(), 11);
1724        assert_eq!(ExtMathFn::Erf.name(), "erf");
1725        assert_eq!(ExtMathFn::Rsqrt.name(), "rsqrt");
1726    }
1727
1728    #[test]
1729    fn ext_math_all_fns_valid() {
1730        let fns = [
1731            ExtMathFn::Erf,
1732            ExtMathFn::Erfc,
1733            ExtMathFn::Sin,
1734            ExtMathFn::Cos,
1735            ExtMathFn::Atan,
1736            ExtMathFn::Tanh,
1737            ExtMathFn::Cbrt,
1738            ExtMathFn::Rsqrt,
1739        ];
1740        for f in fns {
1741            let words = ext_math_compute_shader(f);
1742            check_valid_spirv(&words);
1743            let bytes: Vec<u8> = words.iter().flat_map(|w| w.to_ne_bytes()).collect();
1744            assert_eq!(bytes.len() % 4, 0);
1745            // The OpenCL ext instruction number for `f` must appear in an OpExtInst.
1746            let insts = decode_insts(&words);
1747            let has_inst = insts
1748                .iter()
1749                .any(|(op, ops)| *op == OP_EXT_INST && ops.get(3) == Some(&f.opencl_inst()));
1750            assert!(has_inst, "missing OpExtInst for {}", f.name());
1751        }
1752    }
1753
1754    #[test]
1755    fn atan2_shader_emits_two_arg_ext_inst() {
1756        let words = atan2_compute_shader();
1757        check_valid_spirv(&words);
1758        let insts = decode_insts(&words);
1759        // OpExtInst with inst number = atan2 (7) and two value args.
1760        let atan2_inst = insts
1761            .iter()
1762            .find(|(op, ops)| *op == OP_EXT_INST && ops.get(3) == Some(&7));
1763        let (_, ops) = atan2_inst.expect("atan2 OpExtInst present");
1764        // result_ty, result, ext_set, inst, arg0, arg1 → 6 operands.
1765        assert_eq!(ops.len(), 6, "atan2 must pass two arguments");
1766    }
1767}