1use crate::ast::projection::AstProjection;
7use crate::dag::symbol::{OpKind, SymbolKind, SymbolRegistry};
8use bytemuck;
9
10pub fn compile_to_wgsl(
24 ast: &AstProjection,
25 var_registry: &SymbolRegistry,
26 fn_registry: &SymbolRegistry,
27 var_order: &[&str],
28) -> Result<String, String> {
29 if ast.is_empty() {
30 return Err("Cannot compile empty AST projection to WGSL".to_string());
31 }
32
33 let expr_str = format_node(ast, 0, var_registry, fn_registry, var_order)?;
35
36 use std::fmt::Write; let mut bindings = String::new();
40 for (i, _) in var_order.iter().enumerate() {
41 writeln!(
42 &mut bindings,
43 "@group(0) @binding({i}) var<storage, read> var_{i}: array<f32>;"
44 )
45 .unwrap(); }
47 let out_binding = var_order.len();
49 writeln!(
50 &mut bindings,
51 "@group(0) @binding({out_binding}) var<storage, read_write> out_col: array<f32>;"
52 )
53 .unwrap();
54
55 let wgsl_code = format!(
56 "{bindings}
57@compute @workgroup_size(256)
58fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {{
59 let idx = global_id.x;
60 if (idx >= arrayLength(&out_col)) {{
61 return;
62 }}
63
64 let val = {expr_str};
65 out_col[idx] = val;
66}}
67"
68 );
69
70 Ok(wgsl_code)
71}
72
73fn format_node(
75 projection: &AstProjection,
76 node_idx: usize,
77 var_registry: &SymbolRegistry,
78 fn_registry: &SymbolRegistry,
79 var_order: &[&str],
80) -> Result<String, String> {
81 let node = projection
82 .nodes
83 .get(node_idx)
84 .ok_or_else(|| "Invalid node index in AST projection".to_string())?;
85
86 match node.kind {
87 SymbolKind::Constant(v) => {
88 if v.is_infinite() || v.is_nan() {
90 return Err("GPU JIT does not support infinity or NaN constants".to_string());
91 }
92 if v.fract() == 0.0 {
94 Ok(format!("{v:.1}f"))
95 } else {
96 Ok(format!("{v}f"))
97 }
98 }
99 SymbolKind::Variable(sym_id) => {
100 let name = var_registry
101 .name(sym_id)
102 .ok_or_else(|| "Unknown variable symbol ID".to_string())?;
103 let var_idx = var_order
104 .iter()
105 .position(|&s| s == name)
106 .ok_or_else(|| format!("Variable '{name}' not found in var_order"))?;
107 Ok(format!("var_{var_idx}[idx]"))
108 }
109 SymbolKind::Operator(op) => {
110 let children = node.children.as_slice_with_pool(&projection.children_pool);
111 match op {
112 OpKind::Neg => {
113 if children.len() != 1 {
114 return Err("Neg operator must have exactly 1 child".to_string());
115 }
116 let child_idx = children[0]
117 .resolve(node_idx)
118 .ok_or_else(|| "Unresolved relative child pointer".to_string())?;
119 let inner =
120 format_node(projection, child_idx, var_registry, fn_registry, var_order)?;
121 Ok(format!("(-{inner})"))
122 }
123 OpKind::Add | OpKind::Sub | OpKind::Mul | OpKind::Div | OpKind::Mod => {
124 if children.len() != 2 {
125 return Err(format!("{op:?} operator must have exactly 2 children"));
126 }
127 let lhs_idx = children[0]
128 .resolve(node_idx)
129 .ok_or_else(|| "Unresolved left child pointer".to_string())?;
130 let rhs_idx = children[1]
131 .resolve(node_idx)
132 .ok_or_else(|| "Unresolved right child pointer".to_string())?;
133 let lhs =
134 format_node(projection, lhs_idx, var_registry, fn_registry, var_order)?;
135 let rhs =
136 format_node(projection, rhs_idx, var_registry, fn_registry, var_order)?;
137 let op_str = match op {
138 OpKind::Add => "+",
139 OpKind::Sub => "-",
140 OpKind::Mul => "*",
141 OpKind::Div => "/",
142 OpKind::Mod => "%",
143 _ => unreachable!(),
144 };
145 Ok(format!("({lhs} {op_str} {rhs})"))
146 }
147 OpKind::Pow => {
148 if children.len() != 2 {
149 return Err("Pow operator must have exactly 2 children".to_string());
150 }
151 let lhs_idx = children[0]
152 .resolve(node_idx)
153 .ok_or_else(|| "Unresolved left child pointer".to_string())?;
154 let rhs_idx = children[1]
155 .resolve(node_idx)
156 .ok_or_else(|| "Unresolved right child pointer".to_string())?;
157 let lhs =
158 format_node(projection, lhs_idx, var_registry, fn_registry, var_order)?;
159 let rhs =
160 format_node(projection, rhs_idx, var_registry, fn_registry, var_order)?;
161 Ok(format!("pow({lhs}, {rhs})"))
162 }
163 }
164 }
165 SymbolKind::Function(fn_id) => {
166 let children = node.children.as_slice_with_pool(&projection.children_pool);
167 let fn_name = fn_registry
168 .name(crate::dag::symbol::SymbolId(fn_id.0))
169 .ok_or_else(|| "Unknown function symbol ID".to_string())?;
170
171 let wgsl_fn = match fn_name {
173 "sin" | "cos" | "tan" | "asin" | "acos" | "atan" | "sinh" | "cosh" | "tanh"
174 | "exp" | "log" | "log2" | "sqrt" | "abs" | "floor" | "ceil" | "round" | "sign" => {
175 fn_name
176 }
177 _ => return Err(format!("Unsupported function on GPU: '{fn_name}'")),
178 };
179
180 let mut args = Vec::new();
181 for ptr in children {
182 let child_idx = ptr
183 .resolve(node_idx)
184 .ok_or_else(|| "Unresolved relative child pointer".to_string())?;
185 args.push(format_node(
186 projection,
187 child_idx,
188 var_registry,
189 fn_registry,
190 var_order,
191 )?);
192 }
193 Ok(format!("{}({})", wgsl_fn, args.join(", ")))
194 }
195 SymbolKind::ControlFlow(_) => {
196 Err("Control flow is not supported on the GPU backend".to_string())
197 }
198 }
199}
200
201pub struct CachedPipeline {
203 pipeline: wgpu::ComputePipeline,
204 bind_group_layout: wgpu::BindGroupLayout,
205}
206
207pub struct GpuExecutor {
209 device: wgpu::Device,
210 queue: wgpu::Queue,
211 pipeline_cache: std::collections::HashMap<u64, CachedPipeline, rapidhash::fast::GlobalState>,
212 input_buffers: Vec<wgpu::Buffer>,
213 output_buffer: Option<wgpu::Buffer>,
214 staging_buffer: Option<wgpu::Buffer>,
215 conversion_scratchpad: Vec<f32>,
217 current_capacity: usize,
218}
219
220fn hash_shader_src(src: &str) -> u64 {
222 use std::hash::Hasher;
223 let mut hasher = rapidhash::fast::RapidHasher::default();
224 hasher.write(src.as_bytes());
225 hasher.finish()
226}
227
228impl GpuExecutor {
229 #[must_use]
233 pub fn new() -> Option<Self> {
234 let instance = wgpu::Instance::default();
235 let Ok(adapter) =
236 pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions::default()))
237 else {
238 return None;
239 };
240 let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
241 label: Some("RSSN JIT GPU Device"),
242 required_features: wgpu::Features::empty(),
243 required_limits: wgpu::Limits::default(),
244 memory_hints: wgpu::MemoryHints::default(),
245 experimental_features: wgpu::ExperimentalFeatures::default(),
246 trace: wgpu::Trace::Off,
247 }))
248 .ok()?;
249
250 Some(Self {
251 device,
252 queue,
253 pipeline_cache: std::collections::HashMap::default(),
254 input_buffers: Vec::new(),
255 output_buffer: None,
256 staging_buffer: None,
257 conversion_scratchpad: Vec::new(),
258 current_capacity: 0,
259 })
260 }
261
262 pub fn execute_batch(
283 &mut self,
284 shader_src: &str,
285 n_rows: usize,
286 vars_cols: &[&[f64]],
287 out: &mut [f64],
288 ) -> Result<(), String> {
289 if n_rows == 0 {
290 return Ok(());
291 }
292
293 let hash = hash_shader_src(shader_src);
295 if !self.pipeline_cache.contains_key(&hash) {
296 let shader_module = self
297 .device
298 .create_shader_module(wgpu::ShaderModuleDescriptor {
299 label: Some("RSSN JIT Compute Shader"),
300 source: wgpu::ShaderSource::Wgsl(shader_src.into()),
301 });
302
303 let compute_pipeline =
304 self.device
305 .create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
306 label: Some("RSSN JIT GPU Compute Pipeline"),
307 layout: None,
308 module: &shader_module,
309 entry_point: Some("main"),
310 compilation_options: wgpu::PipelineCompilationOptions::default(),
311 cache: None,
312 });
313
314 let bind_group_layout = compute_pipeline.get_bind_group_layout(0);
316
317 self.pipeline_cache.insert(
318 hash,
319 CachedPipeline {
320 pipeline: compute_pipeline,
321 bind_group_layout,
322 },
323 );
324 }
325 let cached = self
326 .pipeline_cache
327 .get(&hash)
328 .ok_or("Failed to fetch compute pipeline")?;
329
330 let needs_realloc = self.output_buffer.is_none() || self.current_capacity != n_rows;
332 if needs_realloc {
333 self.current_capacity = n_rows;
334 let output_size_bytes = (n_rows * std::mem::size_of::<f32>()) as u64;
335
336 self.output_buffer = Some(self.device.create_buffer(&wgpu::BufferDescriptor {
337 label: Some("GPU Output Storage Buffer"),
338 size: output_size_bytes,
339 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
340 mapped_at_creation: false,
341 }));
342
343 self.staging_buffer = Some(self.device.create_buffer(&wgpu::BufferDescriptor {
344 label: Some("GPU Staging Buffer"),
345 size: output_size_bytes,
346 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
347 mapped_at_creation: false,
348 }));
349
350 self.input_buffers.clear();
352 }
353
354 if self.input_buffers.len() != vars_cols.len() {
356 self.input_buffers.clear();
357 let input_size_bytes = (n_rows * std::mem::size_of::<f32>()) as u64;
358 for i in 0..vars_cols.len() {
359 let buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
360 label: Some(&format!("GPU Input Storage Buffer {i}")),
361 size: input_size_bytes,
362 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
363 mapped_at_creation: false,
364 });
365 self.input_buffers.push(buffer);
366 }
367 }
368
369 self.conversion_scratchpad.resize(n_rows, 0.0f32);
372 for (i, col) in vars_cols.iter().enumerate() {
373 for (dst, &src) in self.conversion_scratchpad.iter_mut().zip(col.iter()) {
375 *dst = src as f32;
376 }
377 self.queue.write_buffer(
378 &self.input_buffers[i],
379 0,
380 bytemuck::cast_slice(&self.conversion_scratchpad),
381 );
382 }
383
384 let mut bind_group_entries = Vec::with_capacity(vars_cols.len() + 1);
386 for (i, buffer) in self.input_buffers.iter().enumerate() {
387 bind_group_entries.push(wgpu::BindGroupEntry {
388 binding: i as u32,
389 resource: buffer.as_entire_binding(),
390 });
391 }
392
393 let output_buffer = self
394 .output_buffer
395 .as_ref()
396 .ok_or("Uninitialized output buffer")?;
397 let staging_buffer = self
398 .staging_buffer
399 .as_ref()
400 .ok_or("Uninitialized staging buffer")?;
401
402 let out_binding = vars_cols.len() as u32;
404 bind_group_entries.push(wgpu::BindGroupEntry {
405 binding: out_binding,
406 resource: output_buffer.as_entire_binding(),
407 });
408
409 let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
410 label: Some("RSSN JIT GPU Bind Group"),
411 layout: &cached.bind_group_layout,
412 entries: &bind_group_entries,
413 });
414
415 let mut encoder = self
417 .device
418 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
419 label: Some("RSSN JIT GPU Compute Encoder"),
420 });
421
422 {
423 let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
424 label: Some("RSSN JIT Compute Pass"),
425 timestamp_writes: None,
426 });
427 compute_pass.set_pipeline(&cached.pipeline);
428 compute_pass.set_bind_group(0, &bind_group, &[]);
429
430 let workgroup_count = n_rows.div_ceil(256);
432 compute_pass.dispatch_workgroups(workgroup_count as u32, 1, 1);
433 }
434
435 let output_size_bytes = (n_rows * std::mem::size_of::<f32>()) as u64;
436 encoder.copy_buffer_to_buffer(output_buffer, 0, staging_buffer, 0, output_size_bytes);
437 self.queue.submit(Some(encoder.finish()));
438
439 let buffer_slice = staging_buffer.slice(0..output_size_bytes);
441 let (sender, receiver) = std::sync::mpsc::channel();
442 buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
443 let _ = sender.send(result);
444 });
445
446 self.device
448 .poll(wgpu::PollType::Wait {
449 submission_index: None,
450 timeout: None,
451 })
452 .unwrap();
453 receiver
454 .recv()
455 .map_err(|e| format!("GPU channel disconnect error: {e}"))?
456 .map_err(|e| format!("Staging buffer mapping failed: {e:?}"))?;
457
458 {
459 let data = buffer_slice.get_mapped_range();
460 let f32_out: &[f32] = bytemuck::cast_slice(&data);
461
462 for (dest, &src) in out.iter_mut().zip(f32_out.iter()) {
464 *dest = f64::from(src);
465 }
466 }
467
468 staging_buffer.unmap();
469 Ok(())
470 }
471}
472
473#[cfg(test)]
474mod tests {
475 use super::*;
476 use crate::ast::convert::dag_to_ast;
477 use crate::dag::builder::DagBuilder;
478 use crate::parser::expr::parse_expression;
479
480 #[test]
481 fn test_compile_simple_expression_to_wgsl() {
482 let mut builder = DagBuilder::new();
483 let root = parse_expression("x^2 + 2*x + 1.5", &mut builder).unwrap();
484 let ast = dag_to_ast(builder.arena(), root);
485 let var_order = vec!["x"];
486
487 let shader =
488 compile_to_wgsl(&ast, builder.registry(), builder.fn_registry(), &var_order).unwrap();
489
490 assert!(shader.contains("@group(0) @binding(0) var<storage, read> var_0: array<f32>;"));
491 assert!(
492 shader.contains("@group(0) @binding(1) var<storage, read_write> out_col: array<f32>;")
493 );
494 assert!(shader.contains("let val = "));
495 }
496
497 #[test]
498 fn test_execute_batch_gpu() {
499 let mut executor = match GpuExecutor::new() {
500 Some(e) => e,
501 None => return, };
503
504 let mut builder = DagBuilder::new();
505 let root = parse_expression("x + y + 10.0", &mut builder).unwrap();
506 let ast = dag_to_ast(builder.arena(), root);
507 let var_order = vec!["x", "y"];
508
509 let shader =
510 compile_to_wgsl(&ast, builder.registry(), builder.fn_registry(), &var_order).unwrap();
511
512 let n = 1000;
513 let x_data = vec![1.0; n];
514 let y_data = vec![2.0; n];
515 let mut out = vec![0.0; n];
516
517 for _ in 0..3 {
519 executor
520 .execute_batch(&shader, n, &[&x_data, &y_data], &mut out)
521 .unwrap();
522 for i in 0..n {
523 assert_eq!(out[i], 13.0); }
525 }
526 }
527}