Skip to main content

oxicuda_levelzero/
spirv.rs

1//! SPIR-V utilities for the Level Zero compute backend.
2//!
3//! This module provides a minimal SPIR-V module builder and a pre-encoded
4//! placeholder compute shader.  The placeholder is a valid "do nothing"
5//! `void main()` with `LocalSize(1,1,1)` that satisfies Level Zero
6//! requirements for module creation, even though all compute
7//! operations in this backend currently return `Unsupported`.
8//!
9//! # SPIR-V binary format
10//!
11//! SPIR-V is a sequence of 32-bit words (little-endian on all current
12//! platforms).  Each instruction is encoded as:
13//!
14//! ```text
15//! word[0] = (word_count << 16) | opcode
16//! word[1..n] = operands
17//! ```
18//!
19//! The module header is always five words:
20//!
21//! ```text
22//! 0x07230203  magic
23//! version     e.g. 0x00010500 = 1.5
24//! generator   arbitrary; we use 0x000D_0002
25//! bound       highest ID used + 1
26//! schema      reserved (0)
27//! ```
28
29/// SPIR-V magic number (identifies a binary as SPIR-V).
30pub const SPIRV_MAGIC: u32 = 0x07230203;
31/// SPIR-V version 1.2 (widely supported, no ray-tracing extras needed).
32pub const SPIRV_VERSION_1_2: u32 = 0x0001_0200;
33/// Generator magic — OxiCUDA Level Zero backend.
34pub const SPIRV_GENERATOR: u32 = 0x000D_0002;
35
36// ─── Minimal SPIR-V builder ──────────────────────────────────
37
38/// Lightweight SPIR-V word-stream builder.
39///
40/// Emits valid SPIR-V instructions for simple compute shaders without
41/// pulling in a full compiler.
42pub struct SpvModule {
43    words: Vec<u32>,
44    /// Next available result ID.
45    id_bound: u32,
46}
47
48impl SpvModule {
49    /// Create a new module with a placeholder header (bound filled at finalise).
50    pub fn new() -> Self {
51        // Five header words; bound (word[3]) is filled by `finalize`.
52        let words = vec![
53            SPIRV_MAGIC,
54            SPIRV_VERSION_1_2,
55            SPIRV_GENERATOR,
56            0, // bound — filled in finalize()
57            0, // schema
58        ];
59        Self { words, id_bound: 1 }
60    }
61
62    /// Allocate a fresh result ID.
63    pub fn alloc_id(&mut self) -> u32 {
64        let id = self.id_bound;
65        self.id_bound += 1;
66        id
67    }
68
69    /// Emit a SPIR-V instruction.
70    ///
71    /// `opcode` is the raw opcode value; `operands` are the additional words.
72    pub fn emit(&mut self, opcode: u32, operands: &[u32]) {
73        let word_count = (1 + operands.len()) as u32;
74        self.words.push((word_count << 16) | opcode);
75        self.words.extend_from_slice(operands);
76    }
77
78    /// Emit a string as null-terminated UTF-8 packed into 32-bit words.
79    pub fn string_words(s: &str) -> Vec<u32> {
80        let bytes = s.as_bytes();
81        // Pad to a multiple of 4, with at least one null terminator.
82        let padded_len = (bytes.len() + 4) & !3;
83        let mut out = vec![0u32; padded_len / 4];
84        for (i, &b) in bytes.iter().enumerate() {
85            let word_idx = i / 4;
86            let byte_idx = i % 4;
87            out[word_idx] |= (b as u32) << (byte_idx * 8);
88        }
89        out
90    }
91
92    /// Finalise the module: patch the ID bound and return the word vector.
93    pub fn finalize(mut self) -> Vec<u32> {
94        self.words[3] = self.id_bound;
95        self.words
96    }
97}
98
99impl Default for SpvModule {
100    fn default() -> Self {
101        Self::new()
102    }
103}
104
105// ─── SPIR-V opcode constants ─────────────────────────────────
106
107const OP_CAPABILITY: u32 = 17;
108const OP_MEMORY_MODEL: u32 = 14;
109const OP_ENTRY_POINT: u32 = 15;
110const OP_EXECUTION_MODE: u32 = 16;
111const OP_TYPE_VOID: u32 = 19;
112const OP_TYPE_FUNCTION: u32 = 33;
113const OP_FUNCTION: u32 = 54;
114const OP_LABEL: u32 = 248;
115const OP_RETURN: u32 = 253;
116const OP_FUNCTION_END: u32 = 56;
117
118// Capability
119const CAPABILITY_SHADER: u32 = 1;
120// Addressing / memory model
121const ADDRESSING_MODEL_LOGICAL: u32 = 0;
122const MEMORY_MODEL_GLSL450: u32 = 1;
123// Execution model
124const EXECUTION_MODEL_GLCOMPUTE: u32 = 5;
125// Execution mode
126const EXECUTION_MODE_LOCAL_SIZE: u32 = 17;
127// Function control
128const FUNCTION_CONTROL_NONE: u32 = 0;
129
130/// Build a minimal valid compute shader: `void main() {}` with `LocalSize(1,1,1)`.
131///
132/// The resulting SPIR-V module is suitable for Level Zero module creation and
133/// serves as a placeholder while real kernel SPIR-V is not yet available.
134pub fn trivial_compute_shader() -> Vec<u32> {
135    let mut m = SpvModule::new();
136
137    // IDs
138    let id_main_fn = m.alloc_id(); // 1 — the entry-point function
139    let id_void = m.alloc_id(); // 2 — OpTypeVoid
140    let id_void_fn = m.alloc_id(); // 3 — OpTypeFunction %void
141    let id_label = m.alloc_id(); // 4 — OpLabel inside main
142
143    // ── Global section ──────────────────────────────────────
144
145    // OpCapability Shader
146    m.emit(OP_CAPABILITY, &[CAPABILITY_SHADER]);
147
148    // OpMemoryModel Logical GLSL450
149    m.emit(
150        OP_MEMORY_MODEL,
151        &[ADDRESSING_MODEL_LOGICAL, MEMORY_MODEL_GLSL450],
152    );
153
154    // OpEntryPoint GLCompute %main "main"
155    // Format: execution_model result_id name_words...
156    let mut entry_words = vec![EXECUTION_MODEL_GLCOMPUTE, id_main_fn];
157    entry_words.extend(SpvModule::string_words("main"));
158    m.emit(OP_ENTRY_POINT, &entry_words);
159
160    // OpExecutionMode %main LocalSize 1 1 1
161    m.emit(
162        OP_EXECUTION_MODE,
163        &[id_main_fn, EXECUTION_MODE_LOCAL_SIZE, 1, 1, 1],
164    );
165
166    // ── Type declarations ────────────────────────────────────
167
168    // OpTypeVoid %void
169    m.emit(OP_TYPE_VOID, &[id_void]);
170
171    // OpTypeFunction %void_fn %void
172    m.emit(OP_TYPE_FUNCTION, &[id_void_fn, id_void]);
173
174    // ── Function body ────────────────────────────────────────
175
176    // OpFunction %void %main None %void_fn
177    m.emit(
178        OP_FUNCTION,
179        &[id_void, id_main_fn, FUNCTION_CONTROL_NONE, id_void_fn],
180    );
181
182    // OpLabel %entry
183    m.emit(OP_LABEL, &[id_label]);
184
185    // OpReturn
186    m.emit(OP_RETURN, &[]);
187
188    // OpFunctionEnd
189    m.emit(OP_FUNCTION_END, &[]);
190
191    m.finalize()
192}
193
194/// Return the trivial compute shader as a byte slice suitable for
195/// passing to Level Zero module creation.
196///
197/// The bytes are the native-endian representation of the SPIR-V words,
198/// which is correct for the current platform.
199pub fn trivial_compute_shader_bytes() -> Vec<u8> {
200    trivial_compute_shader()
201        .iter()
202        .flat_map(|w| w.to_ne_bytes())
203        .collect()
204}
205
206// ─── Tests ──────────────────────────────────────────────────
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211
212    #[test]
213    fn placeholder_spv_valid_magic() {
214        let words = trivial_compute_shader();
215        assert!(!words.is_empty(), "SPIR-V module must not be empty");
216        assert_eq!(words[0], SPIRV_MAGIC, "first word must be SPIR-V magic");
217    }
218
219    #[test]
220    fn placeholder_spv_word_aligned() {
221        let bytes = trivial_compute_shader_bytes();
222        assert_eq!(bytes.len() % 4, 0, "SPIR-V must be 4-byte aligned");
223    }
224
225    #[test]
226    fn placeholder_spv_version_and_schema() {
227        let words = trivial_compute_shader();
228        assert!(words.len() >= 5, "header must have 5 words");
229        // version >= 1.0 (0x00010000)
230        assert!(words[1] >= 0x0001_0000, "SPIR-V version must be >= 1.0");
231        assert_eq!(words[4], 0, "schema word must be 0");
232    }
233
234    #[test]
235    fn placeholder_spv_nonzero_bound() {
236        let words = trivial_compute_shader();
237        assert!(words[3] > 0, "ID bound must be > 0 when IDs are allocated");
238    }
239
240    #[test]
241    fn spv_module_id_allocation_is_monotonic() {
242        let mut m = SpvModule::new();
243        let id1 = m.alloc_id();
244        let id2 = m.alloc_id();
245        assert!(id2 > id1);
246    }
247
248    #[test]
249    fn string_words_null_terminated() {
250        let words = SpvModule::string_words("abc");
251        // "abc\0" packed into one 32-bit word
252        assert!(!words.is_empty());
253        // Reconstruct bytes
254        let bytes: Vec<u8> = words.iter().flat_map(|w| w.to_le_bytes()).collect();
255        // Must contain 'a', 'b', 'c' followed by a null byte
256        assert_eq!(bytes[0], b'a');
257        assert_eq!(bytes[1], b'b');
258        assert_eq!(bytes[2], b'c');
259        assert_eq!(bytes[3], 0);
260    }
261
262    #[test]
263    fn string_words_empty_string() {
264        let words = SpvModule::string_words("");
265        // Even the empty string must produce at least one word (null terminator).
266        assert!(!words.is_empty());
267        // First byte must be null.
268        let bytes: Vec<u8> = words.iter().flat_map(|w| w.to_le_bytes()).collect();
269        assert_eq!(bytes[0], 0);
270    }
271
272    #[test]
273    fn generator_magic_is_level_zero() {
274        // Verify Level Zero generator ID differs from the Vulkan one (0x000D_0001).
275        assert_eq!(SPIRV_GENERATOR, 0x000D_0002);
276        assert_ne!(SPIRV_GENERATOR, 0x000D_0001);
277    }
278}