Skip to main content

softgpu_functional/
ir.rs

1//! SoftGPU Functional IR (`softgpu-sfir-v1`).
2//!
3//! Phase 7 extends the disclosed op set with waves/lanes, group memory,
4//! barriers, structured divergence, comparisons, and selected atomics.
5//! Schema id remains `softgpu-sfir-v1`; unknown ops still fail at parse time.
6
7use crate::error::{FunctionalError, Result};
8use serde::{Deserialize, Serialize};
9use std::path::Path;
10
11/// Schema id embedded in every program document.
12pub const SFIR_SCHEMA: &str = "softgpu-sfir-v1";
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(rename_all = "lowercase")]
16pub enum TypeId {
17    I32,
18    U32,
19    U64,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
23#[serde(rename_all = "snake_case")]
24pub enum AddrSpace {
25    Global,
26    Group,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
30#[serde(rename_all = "snake_case")]
31pub enum AtomicScope {
32    /// SoftGPU workgroup scope (not a hardware memory-order claim).
33    Workgroup,
34    /// SoftGPU device/global arena scope (functional atomic only).
35    Device,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
39#[serde(rename_all = "snake_case")]
40pub enum AtomicOrder {
41    /// SoftGPU sequential functional atomic; not a claim of GPU memory model.
42    Relaxed,
43    AcqRel,
44}
45
46#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
47#[serde(tag = "op", rename_all = "snake_case")]
48pub enum Op {
49    Const {
50        dst: String,
51        ty: TypeId,
52        value: i64,
53    },
54    GlobalId {
55        dst: String,
56        dim: u8,
57    },
58    LocalId {
59        dst: String,
60        dim: u8,
61    },
62    WorkgroupId {
63        dst: String,
64        dim: u8,
65    },
66    /// SoftGPU software lane id within the wave (`flat_local % wave_size`).
67    LaneId {
68        dst: String,
69    },
70    /// SoftGPU software wave id within the workgroup (`flat_local / wave_size`).
71    WaveId {
72        dst: String,
73    },
74    WaveSize {
75        dst: String,
76    },
77    Add {
78        dst: String,
79        lhs: String,
80        rhs: String,
81        ty: TypeId,
82    },
83    Sub {
84        dst: String,
85        lhs: String,
86        rhs: String,
87        ty: TypeId,
88    },
89    Mul {
90        dst: String,
91        lhs: String,
92        rhs: String,
93        ty: TypeId,
94    },
95    CmpEq {
96        dst: String,
97        lhs: String,
98        rhs: String,
99        ty: TypeId,
100    },
101    CmpNe {
102        dst: String,
103        lhs: String,
104        rhs: String,
105        ty: TypeId,
106    },
107    /// Bitwise and (SoftGPU scalar; used for lane predication masks).
108    And {
109        dst: String,
110        lhs: String,
111        rhs: String,
112        ty: TypeId,
113    },
114    KernargLoad {
115        dst: String,
116        offset: u32,
117        ty: TypeId,
118    },
119    LoadGlobal {
120        dst: String,
121        addr: String,
122        ty: TypeId,
123    },
124    StoreGlobal {
125        addr: String,
126        src: String,
127        ty: TypeId,
128    },
129    LoadGroup {
130        dst: String,
131        addr: String,
132        ty: TypeId,
133    },
134    StoreGroup {
135        addr: String,
136        src: String,
137        ty: TypeId,
138    },
139    /// Workgroup barrier (SoftGPU generation sync). Illegal inside `If`/`While`.
140    Barrier,
141    /// Structured divergence: active lanes with `cond != 0` run `then_body`,
142    /// others run `else_body`, then reconverge. SoftGPU SIMT, not gfx1201.
143    If {
144        cond: String,
145        then_body: Vec<Op>,
146        #[serde(default)]
147        else_body: Vec<Op>,
148    },
149    /// SoftGPU loop: while any active lane has `cond != 0`, those lanes run `body`.
150    While {
151        cond: String,
152        body: Vec<Op>,
153    },
154    /// `dst = atomic_add(addr, src)` returning the previous value (i32 only).
155    AtomicAdd {
156        dst: String,
157        addr: String,
158        src: String,
159        space: AddrSpace,
160        scope: AtomicScope,
161        order: AtomicOrder,
162    },
163    Ret,
164}
165
166#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
167pub struct KernargField {
168    pub name: String,
169    pub offset: u32,
170    pub size: u32,
171    pub kind: String,
172}
173
174#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
175pub struct Program {
176    pub schema: String,
177    pub fidelity: String,
178    pub note: String,
179    pub name: String,
180    pub source_provenance: String,
181    #[serde(default)]
182    pub kernarg_layout: Vec<KernargField>,
183    /// SoftGPU group/LDS bytes required for this program (software limit).
184    #[serde(default)]
185    pub group_bytes: u32,
186    pub body: Vec<Op>,
187}
188
189impl Program {
190    pub fn validate(&self) -> Result<()> {
191        if self.schema != SFIR_SCHEMA {
192            return Err(FunctionalError::Validation {
193                detail: format!("schema '{}' != '{SFIR_SCHEMA}'", self.schema),
194            });
195        }
196        if self.fidelity != "functional" {
197            return Err(FunctionalError::Validation {
198                detail: format!("fidelity must be 'functional', got '{}'", self.fidelity),
199            });
200        }
201        if self.note != "not_gfx1201_isa_emulation" {
202            return Err(FunctionalError::Validation {
203                detail: format!(
204                    "note must be 'not_gfx1201_isa_emulation', got '{}'",
205                    self.note
206                ),
207            });
208        }
209        if self.body.is_empty() {
210            return Err(FunctionalError::Validation {
211                detail: "empty body".into(),
212            });
213        }
214        if !matches!(self.body.last(), Some(Op::Ret)) {
215            return Err(FunctionalError::Validation {
216                detail: "body must end with ret".into(),
217            });
218        }
219        validate_ops(&self.body, /*allow_barrier*/ true)?;
220        Ok(())
221    }
222
223    pub fn has_barrier(&self) -> bool {
224        ops_have_barrier(&self.body)
225    }
226}
227
228fn ops_have_barrier(ops: &[Op]) -> bool {
229    for op in ops {
230        match op {
231            Op::Barrier => return true,
232            Op::If {
233                then_body,
234                else_body,
235                ..
236            } => {
237                if ops_have_barrier(then_body) || ops_have_barrier(else_body) {
238                    return true;
239                }
240            }
241            Op::While { body, .. } if ops_have_barrier(body) => return true,
242            _ => {}
243        }
244    }
245    false
246}
247
248fn validate_ops(ops: &[Op], allow_barrier: bool) -> Result<()> {
249    for op in ops {
250        match op {
251            Op::GlobalId { dim, .. } | Op::LocalId { dim, .. } | Op::WorkgroupId { dim, .. }
252                if *dim > 2 =>
253            {
254                return Err(FunctionalError::Validation {
255                    detail: format!("dim {dim} out of range 0..=2"),
256                });
257            }
258            Op::Barrier if !allow_barrier => {
259                return Err(FunctionalError::Validation {
260                    detail: "barrier is illegal inside if/while (divergent barrier unsupported)"
261                        .into(),
262                });
263            }
264            Op::If {
265                then_body,
266                else_body,
267                ..
268            } => {
269                validate_ops(then_body, false)?;
270                validate_ops(else_body, false)?;
271            }
272            Op::While { body, .. } => {
273                validate_ops(body, false)?;
274            }
275            Op::AtomicAdd { .. } => {}
276            Op::Ret => {}
277            _ => {}
278        }
279    }
280    Ok(())
281}
282
283/// Split a top-level body into barrier-separated segments (Ret stripped from last).
284pub fn barrier_segments(body: &[Op]) -> Result<Vec<Vec<Op>>> {
285    let mut segs = Vec::new();
286    let mut cur = Vec::new();
287    for op in body {
288        match op {
289            Op::Barrier => {
290                segs.push(std::mem::take(&mut cur));
291            }
292            Op::Ret => {
293                segs.push(std::mem::take(&mut cur));
294                break;
295            }
296            other => cur.push(other.clone()),
297        }
298    }
299    if segs.is_empty() {
300        return Err(FunctionalError::Validation {
301            detail: "empty barrier segments".into(),
302        });
303    }
304    Ok(segs)
305}
306
307pub fn load_program_str(s: &str) -> Result<Program> {
308    let p: Program = serde_json::from_str(s).map_err(|e| FunctionalError::Parse(e.to_string()))?;
309    p.validate()?;
310    Ok(p)
311}
312
313pub fn load_program_path(path: impl AsRef<Path>) -> Result<Program> {
314    let s =
315        std::fs::read_to_string(path.as_ref()).map_err(|e| FunctionalError::Io(e.to_string()))?;
316    load_program_str(&s)
317}