//! SPIR-V binary emitter for nox formulas
//!
//! Vulkan compute shaders with native 64-bit integers (Int64 capability).
//! Clean Goldilocks arithmetic โ no u32 pair emulation needed.
//! Works on desktop Vulkan GPUs (NVIDIA, AMD, Intel).
use nox::noun::{Order, NounId};
use super::{CompileError, formula_parts, body_pair, body_triple, atom_u64, axis_to_param,
detect_loop_setup, detect_back_edge};
const P: u64 = 0xFFFF_FFFF_0000_0001;
pub fn compile_to_spirv<const N: usize>(
order: &Order<N>,
formula: NounId,
num_params: u32,
) -> Result<Vec<u8>, CompileError> {
let mut b = SpvBuilder::new(num_params);
b.emit_formula(order, formula)?;
let result = b.pop_reg();
Ok(b.finish(result, num_params))
}
// SPIR-V opcodes
const OP_CAPABILITY: u16 = 17;
const OP_EXT_INST_IMPORT: u16 = 11;
const OP_MEMORY_MODEL: u16 = 14;
const OP_ENTRY_POINT: u16 = 15;
const OP_EXECUTION_MODE: u16 = 16;
const OP_DECORATE: u16 = 71;
const OP_MEMBER_DECORATE: u16 = 72;
const OP_TYPE_VOID: u16 = 19;
const OP_TYPE_INT: u16 = 21;
const OP_TYPE_STRUCT: u16 = 30;
const OP_TYPE_POINTER: u16 = 32;
const OP_TYPE_FUNCTION: u16 = 33;
const OP_TYPE_RUNTIME_ARRAY: u16 = 29;
const OP_CONSTANT: u16 = 43;
const OP_VARIABLE: u16 = 59;
const OP_FUNCTION: u16 = 54;
const OP_FUNCTION_END: u16 = 56;
const OP_LABEL: u16 = 248;
const OP_RETURN: u16 = 253;
const OP_ACCESS_CHAIN: u16 = 65;
const OP_LOAD: u16 = 61;
const OP_STORE: u16 = 62;
const OP_IADD: u16 = 128;
const OP_ISUB: u16 = 130;
const OP_IMUL: u16 = 132;
const OP_UMOD: u16 = 137;
const OP_BITWISE_AND: u16 = 199;
const OP_BITWISE_OR: u16 = 200;
const OP_BITWISE_XOR: u16 = 198;
const OP_SHIFT_LEFT: u16 = 196;
const OP_SHIFT_RIGHT_LOGICAL: u16 = 197;
const OP_IEQUAL: u16 = 170;
const OP_INOTEQUAL: u16 = 171;
const OP_ULESS_THAN: u16 = 176;
const OP_UGREATER_THAN_EQUAL: u16 = 178;
const OP_SELECT: u16 = 169;
const OP_BRANCH: u16 = 249;
const OP_BRANCH_CONDITIONAL: u16 = 250;
const OP_LOOP_MERGE: u16 = 246;
const OP_SELECTION_MERGE: u16 = 247;
const OP_TYPE_BOOL: u16 = 20;
struct SpvBuilder {
/// All SPIR-V words
preamble: Vec<u32>, // capabilities, types, decorations, variables
code: Vec<u32>, // function body instructions
next_id: u32,
reg_stack: Vec<u32>, // SSA result IDs
subject: Vec<u32>, // SSA IDs for subject positions
// Pre-allocated type/const IDs
type_void: u32,
type_bool: u32,
type_u64: u32,
type_u32: u32,
type_fn_void: u32,
type_ptr_storage_u64: u32,
type_runtime_array_u64: u32,
type_struct_buf: u32,
type_ptr_storage_struct: u32,
const_0: u32,
const_1: u32,
const_p: u32,
const_mask32: u32,
entry_point_id: u32,
global_invocation_id: u32,
loop_state: Option<SpvLoopState>,
}
#[derive(Clone)]
struct SpvLoopState {
carried: Vec<u32>, // variable IDs for carried state
header_label: u32,
merge_label: u32,
continue_label: u32,
}
impl SpvBuilder {
fn new(num_params: u32) -> Self {
let mut b = Self {
preamble: Vec::with_capacity(512),
code: Vec::with_capacity(512),
next_id: 1,
reg_stack: Vec::new(),
subject: Vec::new(),
type_void: 0, type_bool: 0, type_u64: 0, type_u32: 0,
type_fn_void: 0, type_ptr_storage_u64: 0,
type_runtime_array_u64: 0, type_struct_buf: 0,
type_ptr_storage_struct: 0,
const_0: 0, const_1: 0, const_p: 0, const_mask32: 0,
entry_point_id: 0, global_invocation_id: 0,
loop_state: None,
};
b.emit_preamble(num_params);
b
}
fn alloc_id(&mut self) -> u32 {
let id = self.next_id;
self.next_id += 1;
id
}
fn push_reg(&mut self) -> u32 {
let id = self.alloc_id();
self.reg_stack.push(id);
id
}
fn pop_reg(&mut self) -> u32 {
self.reg_stack.pop().unwrap_or(1)
}
fn emit_insn(buf: &mut Vec<u32>, opcode: u16, operands: &[u32]) {
let word_count = (1 + operands.len()) as u16;
buf.push(((word_count as u32) << 16) | (opcode as u32));
buf.extend_from_slice(operands);
}
fn emit_preamble(&mut self, num_params: u32) {
// Allocate type IDs
self.type_void = self.alloc_id();
self.type_bool = self.alloc_id();
self.type_u64 = self.alloc_id();
self.type_u32 = self.alloc_id();
self.type_fn_void = self.alloc_id();
self.type_runtime_array_u64 = self.alloc_id();
self.type_struct_buf = self.alloc_id();
self.type_ptr_storage_u64 = self.alloc_id();
self.type_ptr_storage_struct = self.alloc_id();
self.entry_point_id = self.alloc_id();
// Constants
self.const_0 = self.alloc_id();
self.const_1 = self.alloc_id();
self.const_p = self.alloc_id();
self.const_mask32 = self.alloc_id();
// Capabilities
Self::emit_insn(&mut self.preamble, OP_CAPABILITY, &[5]);
Self::emit_insn(&mut self.preamble, OP_CAPABILITY, &[11]);
Self::emit_insn(&mut self.preamble, OP_MEMORY_MODEL, &[0, 1]);
// Types
Self::emit_insn(&mut self.preamble, OP_TYPE_VOID, &[self.type_void]);
Self::emit_insn(&mut self.preamble, OP_TYPE_BOOL, &[self.type_bool]);
Self::emit_insn(&mut self.preamble, OP_TYPE_INT, &[self.type_u64, 64, 0]);
Self::emit_insn(&mut self.preamble, OP_TYPE_INT, &[self.type_u32, 32, 0]);
Self::emit_insn(&mut self.preamble, OP_TYPE_FUNCTION, &[self.type_fn_void, self.type_void]);
Self::emit_insn(&mut self.preamble, OP_TYPE_RUNTIME_ARRAY, &[self.type_runtime_array_u64, self.type_u64]);
Self::emit_insn(&mut self.preamble, OP_TYPE_STRUCT, &[self.type_struct_buf, self.type_runtime_array_u64]);
Self::emit_insn(&mut self.preamble, OP_TYPE_POINTER, &[self.type_ptr_storage_struct, 12, self.type_struct_buf]);
Self::emit_insn(&mut self.preamble, OP_TYPE_POINTER, &[self.type_ptr_storage_u64, 12, self.type_u64]);
Self::emit_insn(&mut self.preamble, OP_DECORATE, &[self.type_runtime_array_u64, 6, 8]);
Self::emit_insn(&mut self.preamble, OP_MEMBER_DECORATE, &[self.type_struct_buf, 0, 35, 0]);
Self::emit_insn(&mut self.preamble, OP_DECORATE, &[self.type_struct_buf, 2]);
// Constants
Self::emit_insn(&mut self.preamble, OP_CONSTANT, &[self.type_u64, self.const_0, 0, 0]);
Self::emit_insn(&mut self.preamble, OP_CONSTANT, &[self.type_u64, self.const_1, 1, 0]);
Self::emit_insn(&mut self.preamble, OP_CONSTANT, &[self.type_u64, self.const_p, P as u32, (P >> 32) as u32]);
Self::emit_insn(&mut self.preamble, OP_CONSTANT, &[self.type_u64, self.const_mask32, 0xFFFFFFFF, 0]);
// const u32 0 for access chain
let const_u32_0 = self.alloc_id();
Self::emit_insn(&mut self.preamble, OP_CONSTANT, &[self.type_u32, const_u32_0, 0]);
// Variables: one buffer per param + output buffer
let mut buf_vars = Vec::new();
for i in 0..=num_params {
let var_id = self.alloc_id();
Self::emit_insn(&mut self.preamble, OP_VARIABLE, &[self.type_ptr_storage_struct, var_id, 12]);
Self::emit_insn(&mut self.preamble, OP_DECORATE, &[var_id, 33, i]);
Self::emit_insn(&mut self.preamble, OP_DECORATE, &[var_id, 34, 0]);
buf_vars.push(var_id);
}
for i in (0..num_params).rev() {
self.subject.push(buf_vars[i as usize]);
}
}
fn emit_formula<const N: usize>(&mut self, order: &Order<N>, formula: NounId) -> Result<(), CompileError> {
let (tag, body) = formula_parts(order, formula)?;
match tag {
0 => self.emit_axis(order, body),
1 => self.emit_quote(order, body),
2 => self.emit_compose(order, body),
4 => self.emit_branch(order, body),
5 => { self.emit_binop(order, body, OP_IADD) }
6 => { self.emit_binop(order, body, OP_ISUB) }
7 => { self.emit_binop(order, body, OP_IMUL) }
9 => self.emit_eq(order, body),
10 => self.emit_lt(order, body),
11 => { self.emit_binop(order, body, OP_BITWISE_XOR) }
12 => { self.emit_binop(order, body, OP_BITWISE_AND) }
13 => self.emit_not(order, body),
14 => { self.emit_binop(order, body, OP_SHIFT_LEFT) }
_ => Err(CompileError::UnsupportedPattern(tag)),
}
}
fn emit_axis<const N: usize>(&mut self, order: &Order<N>, body: NounId) -> Result<(), CompileError> {
let addr = atom_u64(order, body)?;
let depth = axis_to_param(addr)?;
if (depth as usize) >= self.subject.len() { return Err(CompileError::NoParams); }
let src = self.subject[depth as usize];
let dst = self.push_reg();
// In SPIR-V SSA, just alias the ID
// Actually we need a proper load or copy. For now, use OpLoad if it's a variable,
// or just push the existing ID.
self.reg_stack.pop(); // undo push_reg
self.reg_stack.push(src);
let _ = dst; // unused
Ok(())
}
fn emit_quote<const N: usize>(&mut self, order: &Order<N>, body: NounId) -> Result<(), CompileError> {
let val = atom_u64(order, body)?;
if val == 0 {
self.reg_stack.push(self.const_0);
} else if val == 1 {
self.reg_stack.push(self.const_1);
} else if val == P {
self.reg_stack.push(self.const_p);
} else {
let id = self.alloc_id();
Self::emit_insn(&mut self.preamble, OP_CONSTANT,
&[self.type_u64, id, val as u32, (val >> 32) as u32]);
self.reg_stack.push(id);
}
Ok(())
}
fn emit_compose<const N: usize>(&mut self, order: &Order<N>, body: NounId) -> Result<(), CompileError> {
if let Some((_loop_body, _inits)) = detect_loop_setup(order, body) {
return Err(CompileError::UnsupportedPattern(2)); // TODO: SPIR-V loops
}
if let Some((_new_subj, _)) = detect_back_edge(order, body) {
return Err(CompileError::UnsupportedPattern(2));
}
// Let-binding
let (a_formula, b_formula) = body_pair(order, body)?;
let (a_tag, a_body) = formula_parts(order, a_formula)?;
if a_tag != 3 { return Err(CompileError::UnsupportedPattern(2)); }
let (value_formula, identity) = body_pair(order, a_body)?;
let (id_tag, id_body) = formula_parts(order, identity)?;
if id_tag != 0 || atom_u64(order, id_body)? != 1 {
return Err(CompileError::UnsupportedPattern(2));
}
let (b_tag, body_formula) = formula_parts(order, b_formula)?;
if b_tag != 1 { return Err(CompileError::UnsupportedPattern(2)); }
self.emit_formula(order, value_formula)?;
let val = self.pop_reg();
self.subject.insert(0, val);
let result = self.emit_formula(order, body_formula);
self.subject.remove(0);
result
}
fn emit_binop<const N: usize>(&mut self, order: &Order<N>, body: NounId, op: u16) -> Result<(), CompileError> {
let (a, b) = body_pair(order, body)?;
self.emit_formula(order, a)?;
let ra = self.pop_reg();
self.emit_formula(order, b)?;
let rb = self.pop_reg();
let dst = self.push_reg();
Self::emit_insn(&mut self.code, op, &[self.type_u64, dst, ra, rb]);
Ok(())
}
fn emit_eq<const N: usize>(&mut self, order: &Order<N>, body: NounId) -> Result<(), CompileError> {
let (a, b) = body_pair(order, body)?;
self.emit_formula(order, a)?;
let ra = self.pop_reg();
self.emit_formula(order, b)?;
let rb = self.pop_reg();
let cmp = self.alloc_id();
let dst = self.push_reg();
// nox eq: 0 if equal, 1 if not
Self::emit_insn(&mut self.code, OP_INOTEQUAL, &[self.type_bool, cmp, ra, rb]);
Self::emit_insn(&mut self.code, OP_SELECT, &[self.type_u64, dst, cmp, self.const_1, self.const_0]);
Ok(())
}
fn emit_lt<const N: usize>(&mut self, order: &Order<N>, body: NounId) -> Result<(), CompileError> {
let (a, b) = body_pair(order, body)?;
self.emit_formula(order, a)?;
let ra = self.pop_reg();
self.emit_formula(order, b)?;
let rb = self.pop_reg();
let cmp = self.alloc_id();
let dst = self.push_reg();
// nox lt: 0 if a<b, 1 if a>=b
Self::emit_insn(&mut self.code, OP_UGREATER_THAN_EQUAL, &[self.type_bool, cmp, ra, rb]);
Self::emit_insn(&mut self.code, OP_SELECT, &[self.type_u64, dst, cmp, self.const_1, self.const_0]);
Ok(())
}
fn emit_not<const N: usize>(&mut self, order: &Order<N>, body: NounId) -> Result<(), CompileError> {
self.emit_formula(order, body)?;
let ra = self.pop_reg();
let tmp = self.alloc_id();
let dst = self.push_reg();
// NOT then mask to 32 bits
Self::emit_insn(&mut self.code, OP_BITWISE_XOR, &[self.type_u64, tmp, ra, self.const_mask32]);
// Actually NOT = XOR with all-1s, but for 32-bit: XOR with 0xFFFFFFFF
Self::emit_insn(&mut self.code, OP_BITWISE_AND, &[self.type_u64, dst, tmp, self.const_mask32]);
Ok(())
}
fn emit_branch<const N: usize>(&mut self, order: &Order<N>, body: NounId) -> Result<(), CompileError> {
let (test, yes, no) = body_triple(order, body)?;
self.emit_formula(order, test)?;
let test_r = self.pop_reg();
// Compare test == 0
let cmp = self.alloc_id();
Self::emit_insn(&mut self.code, OP_IEQUAL, &[self.type_bool, cmp, test_r, self.const_0]);
// For now, use OpSelect instead of structured control flow
// This only works for simple values, not for side-effecting branches.
// Compile both branches unconditionally.
self.emit_formula(order, yes)?;
let yr = self.pop_reg();
self.emit_formula(order, no)?;
let nr = self.pop_reg();
let dst = self.push_reg();
// Select: if cmp (test==0) then yes else no
Self::emit_insn(&mut self.code, OP_SELECT, &[self.type_u64, dst, cmp, yr, nr]);
Ok(())
}
fn finish(mut self, result: u32, num_params: u32) -> Vec<u8> {
// Entry point references
let entry_label = self.alloc_id();
// Build entry point (needs to list all interface variables)
let ep = &mut self.preamble;
// OpEntryPoint GLCompute entry_id "main" (interface vars...)
let mut ep_words = vec![5u32, self.entry_point_id]; // GLCompute, entry_id
// Name "main" as 32-bit words
ep_words.push(0x6E69616D); // "main"
ep_words.push(0); // null terminator padded
let word_count = (1 + ep_words.len()) as u16;
let header = ((word_count as u32) << 16) | (OP_ENTRY_POINT as u32);
// Insert at beginning after capabilities
// Actually, just build the full binary at the end.
// OpExecutionMode
Self::emit_insn(ep, OP_EXECUTION_MODE, &[self.entry_point_id, 17, 256, 1, 1]); // LocalSize 256,1,1
// Function
Self::emit_insn(&mut self.code, OP_FUNCTION,
&[self.type_void, self.entry_point_id, 0, self.type_fn_void]);
let body_label = self.alloc_id();
Self::emit_insn(&mut self.code, OP_LABEL, &[body_label]);
// (formula code is already in self.code from emit_formula calls)
// Just need to end properly
Self::emit_insn(&mut self.code, OP_RETURN, &[]);
Self::emit_insn(&mut self.code, OP_FUNCTION_END, &[]);
// Assemble binary
let bound = self.next_id;
let mut binary = Vec::with_capacity((5 + self.preamble.len() + self.code.len()) * 4);
// SPIR-V header
binary.extend_from_slice(&0x07230203u32.to_le_bytes()); // magic
binary.extend_from_slice(&0x00010500u32.to_le_bytes()); // version 1.5
binary.extend_from_slice(&0u32.to_le_bytes()); // generator
binary.extend_from_slice(&bound.to_le_bytes()); // bound
binary.extend_from_slice(&0u32.to_le_bytes()); // schema
for &w in &self.preamble { binary.extend_from_slice(&w.to_le_bytes()); }
for &w in &self.code { binary.extend_from_slice(&w.to_le_bytes()); }
binary
}
}
trident/src/compile/spirv.rs
ฯ 0.0%
//! SPIR-V binary emitter for nox formulas
//!
//! Vulkan compute shaders with native 64-bit integers (Int64 capability).
//! Clean Goldilocks arithmetic โ no u32 pair emulation needed.
//! Works on desktop Vulkan GPUs (NVIDIA, AMD, Intel).
use ;
use ;
const P: u64 = 0xFFFF_FFFF_0000_0001;
// SPIR-V opcodes
const OP_CAPABILITY: u16 = 17;
const OP_EXT_INST_IMPORT: u16 = 11;
const OP_MEMORY_MODEL: u16 = 14;
const OP_ENTRY_POINT: u16 = 15;
const OP_EXECUTION_MODE: u16 = 16;
const OP_DECORATE: u16 = 71;
const OP_MEMBER_DECORATE: u16 = 72;
const OP_TYPE_VOID: u16 = 19;
const OP_TYPE_INT: u16 = 21;
const OP_TYPE_STRUCT: u16 = 30;
const OP_TYPE_POINTER: u16 = 32;
const OP_TYPE_FUNCTION: u16 = 33;
const OP_TYPE_RUNTIME_ARRAY: u16 = 29;
const OP_CONSTANT: u16 = 43;
const OP_VARIABLE: u16 = 59;
const OP_FUNCTION: u16 = 54;
const OP_FUNCTION_END: u16 = 56;
const OP_LABEL: u16 = 248;
const OP_RETURN: u16 = 253;
const OP_ACCESS_CHAIN: u16 = 65;
const OP_LOAD: u16 = 61;
const OP_STORE: u16 = 62;
const OP_IADD: u16 = 128;
const OP_ISUB: u16 = 130;
const OP_IMUL: u16 = 132;
const OP_UMOD: u16 = 137;
const OP_BITWISE_AND: u16 = 199;
const OP_BITWISE_OR: u16 = 200;
const OP_BITWISE_XOR: u16 = 198;
const OP_SHIFT_LEFT: u16 = 196;
const OP_SHIFT_RIGHT_LOGICAL: u16 = 197;
const OP_IEQUAL: u16 = 170;
const OP_INOTEQUAL: u16 = 171;
const OP_ULESS_THAN: u16 = 176;
const OP_UGREATER_THAN_EQUAL: u16 = 178;
const OP_SELECT: u16 = 169;
const OP_BRANCH: u16 = 249;
const OP_BRANCH_CONDITIONAL: u16 = 250;
const OP_LOOP_MERGE: u16 = 246;
const OP_SELECTION_MERGE: u16 = 247;
const OP_TYPE_BOOL: u16 = 20;