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