qualia_core_db/wgsl_forge/emit/
spirv.rs1use super::{emit_wgsl, GeneratedShader};
20use crate::wgsl_forge::{ForgeError, KernelSpec, Schedule};
21
22const OP_EXECUTION_MODE: u32 = 16;
27const OP_EXECUTION_MODE_LOCAL_SIZE_WORD_COUNT: u32 = 6;
28const EXEC_MODE_LOCAL_SIZE: u32 = 17; pub fn patch_spirv_workgroup_size(
41 words: &mut [u32],
42 workgroup_size: u32,
43) -> Result<(), ForgeError> {
44 let target = (OP_EXECUTION_MODE_LOCAL_SIZE_WORD_COUNT << 16) | OP_EXECUTION_MODE;
48 for i in 0..words
49 .len()
50 .saturating_sub(OP_EXECUTION_MODE_LOCAL_SIZE_WORD_COUNT as usize)
51 {
52 if words[i] == target && words[i + 2] == EXEC_MODE_LOCAL_SIZE {
53 words[i + 3] = workgroup_size;
54 words[i + 4] = 1;
55 words[i + 5] = 1;
56 return Ok(());
57 }
58 }
59 Err(ForgeError::Emission(
60 "OpExecutionMode LocalSize not found in SPIR-V binary".to_string(),
61 ))
62}
63
64fn encode_spirv_words(words: &[u32]) -> String {
66 let mut source = String::with_capacity(words.len() * 6);
67 for (index, word) in words.iter().enumerate() {
68 if index > 0 {
69 source.push(SPIRV_WORD_SEPARATOR);
70 }
71 source.push_str(&word.to_string());
72 }
73 source
74}
75
76pub const SPIRV_WORD_SEPARATOR: char = ';';
78
79pub fn emit_spirv(kernel: &KernelSpec, schedule: Schedule) -> Result<GeneratedShader, ForgeError> {
86 let wgsl = emit_wgsl(kernel, schedule)?;
88
89 let module = naga::front::wgsl::parse_str(&wgsl.source)
91 .map_err(|error| ForgeError::WgslParse(error.emit_to_string(&wgsl.source)))?;
92
93 let mut validator = naga::valid::Validator::new(
96 naga::valid::ValidationFlags::all(),
97 naga::valid::Capabilities::RAY_QUERY
98 | naga::valid::Capabilities::COOPERATIVE_MATRIX
99 | naga::valid::Capabilities::SHADER_FLOAT16,
100 );
101 let info = validator
102 .validate(&module)
103 .map_err(|error| ForgeError::WgslValidation(format!("{error:?}")))?;
104
105 let words =
108 naga::back::spv::write_vec(&module, &info, &naga::back::spv::Options::default(), None)
109 .map_err(|error| ForgeError::Emission(format!("SPIR-V backend: {error}")))?;
110
111 if words.is_empty() {
112 return Err(ForgeError::Emission(
113 "SPIR-V backend produced an empty module".to_string(),
114 ));
115 }
116
117 let source = encode_spirv_words(&words);
119 let source_hash = blake3::hash(source.as_bytes()).to_hex().to_string();
120
121 Ok(GeneratedShader {
122 kernel_id: wgsl.kernel_id,
123 semantic_hash: wgsl.semantic_hash,
124 source_hash,
125 schedule: wgsl.schedule,
126 source,
127 })
128}
129
130pub fn decode_spirv_words(source: &str) -> Result<Vec<u32>, ForgeError> {
135 source
136 .split(SPIRV_WORD_SEPARATOR)
137 .map(|token| {
138 token.parse::<u32>().map_err(|error| {
139 ForgeError::Emission(format!("invalid SPIR-V word {token:?}: {error}"))
140 })
141 })
142 .collect()
143}
144
145pub fn emit_spirv_patched(
151 kernel: &KernelSpec,
152 schedule: Schedule,
153) -> Result<GeneratedShader, ForgeError> {
154 let mut words = {
155 let base = emit_spirv(kernel, schedule)?;
156 decode_spirv_words(&base.source)?
157 };
158 patch_spirv_workgroup_size(&mut words, schedule.workgroup_size)?;
159 let source = encode_spirv_words(&words);
160 let source_hash = blake3::hash(source.as_bytes()).to_hex().to_string();
161 Ok(GeneratedShader {
162 kernel_id: kernel.id.clone(),
163 semantic_hash: kernel.semantic_hash()?,
164 source_hash,
165 schedule,
166 source,
167 })
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173 use crate::wgsl_forge::{BuiltinKernel, Schedule};
174
175 #[test]
176 fn affine_emits_non_empty_spirv_words() {
177 let kernel = BuiltinKernel::AffineF32.spec();
178 let generated = emit_spirv(&kernel, Schedule::default()).expect("spirv emission");
179 let words = decode_spirv_words(&generated.source).expect("decode words");
180 assert!(!words.is_empty(), "SPIR-V module must contain words");
181 assert_eq!(words[0], 0x0723_0203, "SPIR-V magic number header");
183 assert_eq!(generated.kernel_id, kernel.id);
184 }
185
186 #[test]
187 fn patch_workgroup_size_modifies_op_local_size() {
188 let kernel = BuiltinKernel::AffineF32.spec();
189 let base = emit_spirv(&kernel, Schedule::default()).expect("spirv emission");
190 let mut words = decode_spirv_words(&base.source).expect("decode words");
191
192 let target = (OP_EXECUTION_MODE_LOCAL_SIZE_WORD_COUNT << 16) | OP_EXECUTION_MODE;
194 let mut orig_x = 0;
195 for i in 0..words.len().saturating_sub(6) {
196 if words[i] == target && words[i + 2] == EXEC_MODE_LOCAL_SIZE {
197 orig_x = words[i + 3];
198 break;
199 }
200 }
201 assert!(
202 orig_x > 0,
203 "OpExecutionMode LocalSize should exist in emitted SPIR-V"
204 );
205
206 patch_spirv_workgroup_size(&mut words, 128).expect("patch");
208
209 for i in 0..words.len().saturating_sub(6) {
211 if words[i] == target && words[i + 2] == EXEC_MODE_LOCAL_SIZE {
212 assert_eq!(words[i + 3], 128, "workgroup x patched to 128");
213 assert_eq!(words[i + 4], 1, "workgroup y = 1");
214 assert_eq!(words[i + 5], 1, "workgroup z = 1");
215 return;
216 }
217 }
218 panic!("OpExecutionMode LocalSize disappeared after patch");
219 }
220
221 #[test]
222 fn emit_spirv_patched_produces_valid_variant() {
223 let kernel = BuiltinKernel::AffineF32.spec();
224 let schedule = Schedule {
225 workgroup_size: 128,
226 ..Default::default()
227 };
228 let generated = emit_spirv_patched(&kernel, schedule).expect("patched spirv");
229 let words = decode_spirv_words(&generated.source).expect("decode");
230 assert!(!words.is_empty());
231 assert_eq!(words[0], 0x0723_0203, "magic number preserved");
232
233 let target = (OP_EXECUTION_MODE_LOCAL_SIZE_WORD_COUNT << 16) | OP_EXECUTION_MODE;
235 for i in 0..words.len().saturating_sub(6) {
236 if words[i] == target && words[i + 2] == EXEC_MODE_LOCAL_SIZE {
237 assert_eq!(words[i + 3], 128);
238 return;
239 }
240 }
241 panic!("OpExecutionMode LocalSize not found in patched SPIR-V");
242 }
243}