pub const SPIRV_MAGIC: u32 = 0x07230203;
pub const SPIRV_VERSION_1_2: u32 = 0x0001_0200;
pub const SPIRV_GENERATOR: u32 = 0x000D_0002;
pub struct SpvModule {
words: Vec<u32>,
id_bound: u32,
}
impl SpvModule {
pub fn new() -> Self {
let words = vec![
SPIRV_MAGIC,
SPIRV_VERSION_1_2,
SPIRV_GENERATOR,
0, 0, ];
Self { words, id_bound: 1 }
}
pub fn alloc_id(&mut self) -> u32 {
let id = self.id_bound;
self.id_bound += 1;
id
}
pub fn emit(&mut self, opcode: u32, operands: &[u32]) {
let word_count = (1 + operands.len()) as u32;
self.words.push((word_count << 16) | opcode);
self.words.extend_from_slice(operands);
}
pub fn string_words(s: &str) -> Vec<u32> {
let bytes = s.as_bytes();
let padded_len = (bytes.len() + 4) & !3;
let mut out = vec![0u32; padded_len / 4];
for (i, &b) in bytes.iter().enumerate() {
let word_idx = i / 4;
let byte_idx = i % 4;
out[word_idx] |= (b as u32) << (byte_idx * 8);
}
out
}
pub fn finalize(mut self) -> Vec<u32> {
self.words[3] = self.id_bound;
self.words
}
}
impl Default for SpvModule {
fn default() -> Self {
Self::new()
}
}
const OP_CAPABILITY: u32 = 17;
const OP_MEMORY_MODEL: u32 = 14;
const OP_ENTRY_POINT: u32 = 15;
const OP_EXECUTION_MODE: u32 = 16;
const OP_TYPE_VOID: u32 = 19;
const OP_TYPE_FUNCTION: u32 = 33;
const OP_FUNCTION: u32 = 54;
const OP_LABEL: u32 = 248;
const OP_RETURN: u32 = 253;
const OP_FUNCTION_END: u32 = 56;
const CAPABILITY_SHADER: u32 = 1;
const ADDRESSING_MODEL_LOGICAL: u32 = 0;
const MEMORY_MODEL_GLSL450: u32 = 1;
const EXECUTION_MODEL_GLCOMPUTE: u32 = 5;
const EXECUTION_MODE_LOCAL_SIZE: u32 = 17;
const FUNCTION_CONTROL_NONE: u32 = 0;
pub fn trivial_compute_shader() -> Vec<u32> {
let mut m = SpvModule::new();
let id_main_fn = m.alloc_id(); let id_void = m.alloc_id(); let id_void_fn = m.alloc_id(); let id_label = m.alloc_id();
m.emit(OP_CAPABILITY, &[CAPABILITY_SHADER]);
m.emit(
OP_MEMORY_MODEL,
&[ADDRESSING_MODEL_LOGICAL, MEMORY_MODEL_GLSL450],
);
let mut entry_words = vec![EXECUTION_MODEL_GLCOMPUTE, id_main_fn];
entry_words.extend(SpvModule::string_words("main"));
m.emit(OP_ENTRY_POINT, &entry_words);
m.emit(
OP_EXECUTION_MODE,
&[id_main_fn, EXECUTION_MODE_LOCAL_SIZE, 1, 1, 1],
);
m.emit(OP_TYPE_VOID, &[id_void]);
m.emit(OP_TYPE_FUNCTION, &[id_void_fn, id_void]);
m.emit(
OP_FUNCTION,
&[id_void, id_main_fn, FUNCTION_CONTROL_NONE, id_void_fn],
);
m.emit(OP_LABEL, &[id_label]);
m.emit(OP_RETURN, &[]);
m.emit(OP_FUNCTION_END, &[]);
m.finalize()
}
pub fn trivial_compute_shader_bytes() -> Vec<u8> {
trivial_compute_shader()
.iter()
.flat_map(|w| w.to_ne_bytes())
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn placeholder_spv_valid_magic() {
let words = trivial_compute_shader();
assert!(!words.is_empty(), "SPIR-V module must not be empty");
assert_eq!(words[0], SPIRV_MAGIC, "first word must be SPIR-V magic");
}
#[test]
fn placeholder_spv_word_aligned() {
let bytes = trivial_compute_shader_bytes();
assert_eq!(bytes.len() % 4, 0, "SPIR-V must be 4-byte aligned");
}
#[test]
fn placeholder_spv_version_and_schema() {
let words = trivial_compute_shader();
assert!(words.len() >= 5, "header must have 5 words");
assert!(words[1] >= 0x0001_0000, "SPIR-V version must be >= 1.0");
assert_eq!(words[4], 0, "schema word must be 0");
}
#[test]
fn placeholder_spv_nonzero_bound() {
let words = trivial_compute_shader();
assert!(words[3] > 0, "ID bound must be > 0 when IDs are allocated");
}
#[test]
fn spv_module_id_allocation_is_monotonic() {
let mut m = SpvModule::new();
let id1 = m.alloc_id();
let id2 = m.alloc_id();
assert!(id2 > id1);
}
#[test]
fn string_words_null_terminated() {
let words = SpvModule::string_words("abc");
assert!(!words.is_empty());
let bytes: Vec<u8> = words.iter().flat_map(|w| w.to_le_bytes()).collect();
assert_eq!(bytes[0], b'a');
assert_eq!(bytes[1], b'b');
assert_eq!(bytes[2], b'c');
assert_eq!(bytes[3], 0);
}
#[test]
fn string_words_empty_string() {
let words = SpvModule::string_words("");
assert!(!words.is_empty());
let bytes: Vec<u8> = words.iter().flat_map(|w| w.to_le_bytes()).collect();
assert_eq!(bytes[0], 0);
}
#[test]
fn generator_magic_is_level_zero() {
assert_eq!(SPIRV_GENERATOR, 0x000D_0002);
assert_ne!(SPIRV_GENERATOR, 0x000D_0001);
}
}