Skip to main content

softgpu_functional/
exec.rs

1//! Deterministic SoftGPU Functional IR interpreter (Phase 6–7).
2//!
3//! Phase 7 adds SoftGPU software waves/lanes, group memory, barrier segments,
4//! structured divergence, and selected atomics. This is **not** gfx1201 ISA.
5
6use crate::error::{FunctionalError, Result};
7use crate::ir::{barrier_segments, AddrSpace, Op, Program, TypeId};
8use crate::memory::{kernarg_load, GlobalArena};
9use crate::sanitize::{SanitizeMode, SanitizeReport, Sanitizer, WorkItemId};
10use serde::{Deserialize, Serialize};
11use std::collections::BTreeMap;
12
13/// Marker required on all functional-mode outputs.
14pub const FUNCTIONAL_MODE_MARKER: &str = "softgpu_functional_cpu_not_gfx1201_isa";
15
16/// SoftGPU software launch limits (not hardware).
17pub const MAX_FLAT_WORKITEMS: u64 = 1_048_576;
18pub const MAX_WORKGROUP_FLAT: u32 = 1_024;
19pub const DEFAULT_STEP_BUDGET: u64 = 50_000_000;
20pub const DEFAULT_WAVE_SIZE: u32 = 32;
21pub const MAX_WAVE_SIZE: u32 = 128;
22pub const MAX_GROUP_BYTES: u32 = 64 * 1024;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(rename_all = "snake_case")]
26pub enum SchedulePolicy {
27    /// Phase 6 compatible: each workitem runs to completion in lex order.
28    LexWorkitem,
29    /// Waves in order; barrier-separated segments sync the workgroup.
30    WaveBarrier,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub struct LaunchConfig {
35    pub grid: [u32; 3],
36    pub workgroup: [u32; 3],
37}
38
39impl LaunchConfig {
40    pub fn validate(&self) -> Result<()> {
41        for d in 0..3 {
42            if self.workgroup[d] == 0 {
43                return Err(FunctionalError::Validation {
44                    detail: format!("workgroup[{d}] must be > 0"),
45                });
46            }
47            if self.grid[d] == 0 {
48                return Err(FunctionalError::Validation {
49                    detail: format!("grid[{d}] must be > 0"),
50                });
51            }
52            if self.grid[d] % self.workgroup[d] != 0 {
53                return Err(FunctionalError::Validation {
54                    detail: format!(
55                        "grid[{d}]={} not divisible by workgroup[{d}]={}",
56                        self.grid[d], self.workgroup[d]
57                    ),
58                });
59            }
60        }
61        let wg_flat = u64::from(self.workgroup[0])
62            * u64::from(self.workgroup[1])
63            * u64::from(self.workgroup[2]);
64        if wg_flat > u64::from(MAX_WORKGROUP_FLAT) {
65            return Err(FunctionalError::Validation {
66                detail: format!("workgroup flat {wg_flat} > {MAX_WORKGROUP_FLAT}"),
67            });
68        }
69        let flat = u64::from(self.grid[0]) * u64::from(self.grid[1]) * u64::from(self.grid[2]);
70        if flat > MAX_FLAT_WORKITEMS {
71            return Err(FunctionalError::Validation {
72                detail: format!("flat workitems {flat} > {MAX_FLAT_WORKITEMS}"),
73            });
74        }
75        Ok(())
76    }
77
78    pub fn num_workgroups(&self) -> [u32; 3] {
79        [
80            self.grid[0] / self.workgroup[0],
81            self.grid[1] / self.workgroup[1],
82            self.grid[2] / self.workgroup[2],
83        ]
84    }
85
86    pub fn workgroup_flat(&self) -> u32 {
87        self.workgroup[0] * self.workgroup[1] * self.workgroup[2]
88    }
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub struct ExecConfig {
93    pub launch: LaunchConfig,
94    pub wave_size: u32,
95    pub group_bytes: u32,
96    pub schedule: SchedulePolicy,
97    pub step_budget: u64,
98    pub sanitize: SanitizeMode,
99}
100
101impl ExecConfig {
102    pub fn from_launch(launch: LaunchConfig) -> Self {
103        Self {
104            launch,
105            wave_size: DEFAULT_WAVE_SIZE,
106            group_bytes: 0,
107            schedule: SchedulePolicy::LexWorkitem,
108            step_budget: DEFAULT_STEP_BUDGET,
109            sanitize: SanitizeMode::Off,
110        }
111    }
112
113    pub fn validate(&self, program: &Program) -> Result<()> {
114        self.launch.validate()?;
115        if self.wave_size == 0 || self.wave_size > MAX_WAVE_SIZE {
116            return Err(FunctionalError::Validation {
117                detail: format!(
118                    "wave_size {} out of range 1..={MAX_WAVE_SIZE}",
119                    self.wave_size
120                ),
121            });
122        }
123        let need = self.group_bytes.max(program.group_bytes);
124        if need > MAX_GROUP_BYTES {
125            return Err(FunctionalError::Validation {
126                detail: format!("group_bytes {need} > SoftGPU max {MAX_GROUP_BYTES}"),
127            });
128        }
129        if program.has_barrier() && self.schedule != SchedulePolicy::WaveBarrier {
130            return Err(FunctionalError::Validation {
131                detail: "programs with barrier require schedule=wave_barrier".into(),
132            });
133        }
134        Ok(())
135    }
136}
137
138#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
139pub struct RunReport {
140    pub fidelity: &'static str,
141    pub mode: &'static str,
142    pub note: &'static str,
143    pub kernel: String,
144    pub workitems_executed: u64,
145    pub steps: u64,
146    pub grid: [u32; 3],
147    pub workgroup: [u32; 3],
148    pub wave_size: u32,
149    pub schedule: SchedulePolicy,
150    pub barrier_generations: u64,
151    pub atomics_executed: u64,
152}
153
154/// Execute `program` under lex workitem schedule (Phase 6 API).
155pub fn run(
156    program: &Program,
157    launch: LaunchConfig,
158    arena: &mut GlobalArena,
159    kernarg: &[u8],
160) -> Result<RunReport> {
161    let mut cfg = ExecConfig::from_launch(launch);
162    cfg.group_bytes = program.group_bytes;
163    if program.has_barrier() {
164        cfg.schedule = SchedulePolicy::WaveBarrier;
165    }
166    Ok(run_with_config_sanitized(program, cfg, arena, kernarg)?.0)
167}
168
169pub fn run_with_budget(
170    program: &Program,
171    launch: LaunchConfig,
172    arena: &mut GlobalArena,
173    kernarg: &[u8],
174    step_budget: u64,
175) -> Result<RunReport> {
176    let mut cfg = ExecConfig::from_launch(launch);
177    cfg.group_bytes = program.group_bytes;
178    cfg.step_budget = step_budget;
179    if program.has_barrier() {
180        cfg.schedule = SchedulePolicy::WaveBarrier;
181    }
182    Ok(run_with_config_sanitized(program, cfg, arena, kernarg)?.0)
183}
184
185pub fn run_with_config(
186    program: &Program,
187    cfg: ExecConfig,
188    arena: &mut GlobalArena,
189    kernarg: &[u8],
190) -> Result<RunReport> {
191    Ok(run_with_config_sanitized(program, cfg, arena, kernarg)?.0)
192}
193
194/// Execute with optional SoftGPU Phase 8 sanitizer instrumentation.
195pub fn run_with_config_sanitized(
196    program: &Program,
197    cfg: ExecConfig,
198    arena: &mut GlobalArena,
199    kernarg: &[u8],
200) -> Result<(RunReport, SanitizeReport)> {
201    let group_len = cfg.group_bytes.max(program.group_bytes) as usize;
202    let sanitizer = Sanitizer::new(cfg.sanitize, arena.len(), group_len)?;
203    run_with_sanitizer(program, cfg, arena, kernarg, sanitizer)
204}
205
206/// Execute with a caller-owned sanitizer (for shadow setup such as SoftGPU free/uninit).
207pub fn run_with_sanitizer(
208    program: &Program,
209    cfg: ExecConfig,
210    arena: &mut GlobalArena,
211    kernarg: &[u8],
212    mut sanitizer: Sanitizer,
213) -> Result<(RunReport, SanitizeReport)> {
214    program.validate()?;
215    cfg.validate(program)?;
216
217    let mut steps = 0u64;
218    let mut workitems = 0u64;
219    let mut barrier_generations = 0u64;
220    let mut atomics_executed = 0u64;
221    let nwg = cfg.launch.num_workgroups();
222
223    for gz in 0..nwg[2] {
224        for gy in 0..nwg[1] {
225            for gx in 0..nwg[0] {
226                let group_len = cfg.group_bytes.max(program.group_bytes) as usize;
227                let mut group = GlobalArena::new(group_len);
228                sanitizer.reset_group();
229                sanitizer.barrier_gen = 0;
230                let (s, wi, bg, at) = match cfg.schedule {
231                    SchedulePolicy::LexWorkitem => run_workgroup_lex(
232                        program,
233                        &cfg,
234                        arena,
235                        &mut group,
236                        kernarg,
237                        [gx, gy, gz],
238                        cfg.step_budget.saturating_sub(steps),
239                        &mut sanitizer,
240                    )?,
241                    SchedulePolicy::WaveBarrier => run_workgroup_wave_barrier(
242                        program,
243                        &cfg,
244                        arena,
245                        &mut group,
246                        kernarg,
247                        [gx, gy, gz],
248                        cfg.step_budget.saturating_sub(steps),
249                        &mut sanitizer,
250                    )?,
251                };
252                steps = steps.saturating_add(s);
253                if steps > cfg.step_budget {
254                    return Err(FunctionalError::StepBudgetExceeded { steps });
255                }
256                workitems += wi;
257                barrier_generations = barrier_generations.saturating_add(bg);
258                atomics_executed = atomics_executed.saturating_add(at);
259            }
260        }
261    }
262
263    let report = RunReport {
264        fidelity: "functional",
265        mode: FUNCTIONAL_MODE_MARKER,
266        note: "not_gfx1201_isa_emulation",
267        kernel: program.name.clone(),
268        workitems_executed: workitems,
269        steps,
270        grid: cfg.launch.grid,
271        workgroup: cfg.launch.workgroup,
272        wave_size: cfg.wave_size,
273        schedule: cfg.schedule,
274        barrier_generations,
275        atomics_executed,
276    };
277    Ok((report, sanitizer.into_report()))
278}
279
280#[allow(clippy::too_many_arguments)]
281fn run_workgroup_lex(
282    program: &Program,
283    cfg: &ExecConfig,
284    global: &mut GlobalArena,
285    group: &mut GlobalArena,
286    kernarg: &[u8],
287    wg: [u32; 3],
288    budget: u64,
289    sanitizer: &mut Sanitizer,
290) -> Result<(u64, u64, u64, u64)> {
291    let mut steps = 0u64;
292    let mut atomics = 0u64;
293    let mut workitems = 0u64;
294    let launch = cfg.launch;
295    for lz in 0..launch.workgroup[2] {
296        for ly in 0..launch.workgroup[1] {
297            for lx in 0..launch.workgroup[0] {
298                let local = [lx, ly, lz];
299                let flat = flat_local(local, launch.workgroup);
300                let global_id = [
301                    wg[0] * launch.workgroup[0] + lx,
302                    wg[1] * launch.workgroup[1] + ly,
303                    wg[2] * launch.workgroup[2] + lz,
304                ];
305                let mut lane = Lane::new(
306                    global_id,
307                    local,
308                    wg,
309                    flat / cfg.wave_size,
310                    flat % cfg.wave_size,
311                    cfg.wave_size,
312                );
313                let (used, at) = exec_ops(
314                    &program.body,
315                    std::slice::from_mut(&mut lane),
316                    &[true],
317                    global,
318                    group,
319                    kernarg,
320                    budget.saturating_sub(steps),
321                    true,
322                    launch.workgroup,
323                    sanitizer,
324                )?;
325                steps = steps.saturating_add(used);
326                atomics = atomics.saturating_add(at);
327                if steps > budget {
328                    return Err(FunctionalError::StepBudgetExceeded { steps });
329                }
330                workitems += 1;
331            }
332        }
333    }
334    Ok((steps, workitems, 0, atomics))
335}
336
337#[allow(clippy::too_many_arguments)]
338fn run_workgroup_wave_barrier(
339    program: &Program,
340    cfg: &ExecConfig,
341    global: &mut GlobalArena,
342    group: &mut GlobalArena,
343    kernarg: &[u8],
344    wg: [u32; 3],
345    budget: u64,
346    sanitizer: &mut Sanitizer,
347) -> Result<(u64, u64, u64, u64)> {
348    let launch = cfg.launch;
349    let flat_n = launch.workgroup_flat();
350    let mut lanes: Vec<Lane> = Vec::with_capacity(flat_n as usize);
351    for flat in 0..flat_n {
352        let local = unflat_local(flat, launch.workgroup);
353        let global_id = [
354            wg[0] * launch.workgroup[0] + local[0],
355            wg[1] * launch.workgroup[1] + local[1],
356            wg[2] * launch.workgroup[2] + local[2],
357        ];
358        lanes.push(Lane::new(
359            global_id,
360            local,
361            wg,
362            flat / cfg.wave_size,
363            flat % cfg.wave_size,
364            cfg.wave_size,
365        ));
366    }
367
368    let segments = barrier_segments(&program.body)?;
369    let barrier_gens = segments.len().saturating_sub(1) as u64;
370    let mut steps = 0u64;
371    let mut atomics = 0u64;
372    let n_waves = flat_n.div_ceil(cfg.wave_size);
373
374    for (si, seg) in segments.iter().enumerate() {
375        for wave in 0..n_waves {
376            let lo = (wave * cfg.wave_size) as usize;
377            let hi = ((wave * cfg.wave_size + cfg.wave_size).min(flat_n)) as usize;
378            let mask: Vec<bool> = (lo..hi).map(|_| true).collect();
379            let (used, at) = exec_ops(
380                seg,
381                &mut lanes[lo..hi],
382                &mask,
383                global,
384                group,
385                kernarg,
386                budget.saturating_sub(steps),
387                false,
388                launch.workgroup,
389                sanitizer,
390            )?;
391            steps = steps.saturating_add(used);
392            atomics = atomics.saturating_add(at);
393            if steps > budget {
394                return Err(FunctionalError::StepBudgetExceeded { steps });
395            }
396        }
397        if si + 1 < segments.len() {
398            sanitizer.note_barrier();
399        }
400    }
401
402    Ok((steps, u64::from(flat_n), barrier_gens, atomics))
403}
404
405fn flat_local(local: [u32; 3], wg: [u32; 3]) -> u32 {
406    local[0] + local[1] * wg[0] + local[2] * wg[0] * wg[1]
407}
408
409fn unflat_local(flat: u32, wg: [u32; 3]) -> [u32; 3] {
410    let x = flat % wg[0];
411    let t = flat / wg[0];
412    let y = t % wg[1];
413    let z = t / wg[1];
414    [x, y, z]
415}
416
417#[derive(Debug, Clone)]
418struct Lane {
419    global: [u32; 3],
420    local: [u32; 3],
421    wg: [u32; 3],
422    wave_id: u32,
423    lane_id: u32,
424    wave_size: u32,
425    regs: BTreeMap<String, i64>,
426}
427
428impl Lane {
429    fn new(
430        global: [u32; 3],
431        local: [u32; 3],
432        wg: [u32; 3],
433        wave_id: u32,
434        lane_id: u32,
435        wave_size: u32,
436    ) -> Self {
437        Self {
438            global,
439            local,
440            wg,
441            wave_id,
442            lane_id,
443            wave_size,
444            regs: BTreeMap::new(),
445        }
446    }
447}
448
449#[allow(clippy::too_many_arguments)]
450fn exec_ops(
451    ops: &[Op],
452    lanes: &mut [Lane],
453    mask: &[bool],
454    global: &mut GlobalArena,
455    group: &mut GlobalArena,
456    kernarg: &[u8],
457    budget: u64,
458    stop_at_ret: bool,
459    workgroup: [u32; 3],
460    sanitizer: &mut Sanitizer,
461) -> Result<(u64, u64)> {
462    let mut steps = 0u64;
463    let mut atomics = 0u64;
464
465    for op in ops {
466        if matches!(op, Op::Ret) {
467            if stop_at_ret {
468                return Ok((steps, atomics));
469            }
470            continue;
471        }
472        if matches!(op, Op::Barrier) {
473            return Err(FunctionalError::Internal(
474                "barrier must be handled by segment splitter".into(),
475            ));
476        }
477
478        match op {
479            Op::If {
480                cond,
481                then_body,
482                else_body,
483            } => {
484                let mut then_mask = vec![false; lanes.len()];
485                let mut else_mask = vec![false; lanes.len()];
486                for (i, lane) in lanes.iter().enumerate() {
487                    if !mask[i] {
488                        continue;
489                    }
490                    let c = get_reg(&lane.regs, cond)?;
491                    if c != 0 {
492                        then_mask[i] = true;
493                    } else {
494                        else_mask[i] = true;
495                    }
496                }
497                let (s1, a1) = exec_ops(
498                    then_body,
499                    lanes,
500                    &then_mask,
501                    global,
502                    group,
503                    kernarg,
504                    budget.saturating_sub(steps),
505                    false,
506                    workgroup,
507                    sanitizer,
508                )?;
509                steps = steps.saturating_add(s1);
510                atomics = atomics.saturating_add(a1);
511                let (s2, a2) = exec_ops(
512                    else_body,
513                    lanes,
514                    &else_mask,
515                    global,
516                    group,
517                    kernarg,
518                    budget.saturating_sub(steps),
519                    false,
520                    workgroup,
521                    sanitizer,
522                )?;
523                steps = steps.saturating_add(s2);
524                atomics = atomics.saturating_add(a2);
525            }
526            Op::While { cond, body } => loop {
527                let mut iter_mask = vec![false; lanes.len()];
528                let mut any = false;
529                for (i, lane) in lanes.iter().enumerate() {
530                    if !mask[i] {
531                        continue;
532                    }
533                    let c = get_reg(&lane.regs, cond)?;
534                    if c != 0 {
535                        iter_mask[i] = true;
536                        any = true;
537                    }
538                }
539                if !any {
540                    break;
541                }
542                let (s, a) = exec_ops(
543                    body,
544                    lanes,
545                    &iter_mask,
546                    global,
547                    group,
548                    kernarg,
549                    budget.saturating_sub(steps),
550                    false,
551                    workgroup,
552                    sanitizer,
553                )?;
554                steps = steps.saturating_add(s);
555                atomics = atomics.saturating_add(a);
556                if steps > budget {
557                    return Err(FunctionalError::StepBudgetExceeded { steps });
558                }
559            },
560            other => {
561                for (i, lane) in lanes.iter_mut().enumerate() {
562                    if !mask[i] {
563                        continue;
564                    }
565                    steps += 1;
566                    if steps > budget {
567                        return Err(FunctionalError::StepBudgetExceeded { steps });
568                    }
569                    sanitizer.step = sanitizer.step.saturating_add(1);
570                    let at =
571                        exec_lane_op(other, lane, global, group, kernarg, workgroup, sanitizer)?;
572                    atomics = atomics.saturating_add(at);
573                }
574            }
575        }
576        if steps > budget {
577            return Err(FunctionalError::StepBudgetExceeded { steps });
578        }
579    }
580    Ok((steps, atomics))
581}
582
583fn actor_of(lane: &Lane, workgroup: [u32; 3]) -> WorkItemId {
584    WorkItemId {
585        workgroup: lane.wg,
586        wave: lane.wave_id,
587        lane: lane.lane_id,
588        flat_local: flat_local(lane.local, workgroup),
589    }
590}
591
592#[allow(clippy::too_many_arguments)]
593fn exec_lane_op(
594    op: &Op,
595    lane: &mut Lane,
596    global: &mut GlobalArena,
597    group: &mut GlobalArena,
598    kernarg: &[u8],
599    workgroup: [u32; 3],
600    sanitizer: &mut Sanitizer,
601) -> Result<u64> {
602    let actor = actor_of(lane, workgroup);
603    match op {
604        Op::Const { dst, ty, value } => {
605            lane.regs.insert(dst.clone(), narrow(*value, *ty)?);
606            Ok(0)
607        }
608        Op::GlobalId { dst, dim } => {
609            lane.regs
610                .insert(dst.clone(), i64::from(lane.global[*dim as usize]));
611            Ok(0)
612        }
613        Op::LocalId { dst, dim } => {
614            lane.regs
615                .insert(dst.clone(), i64::from(lane.local[*dim as usize]));
616            Ok(0)
617        }
618        Op::WorkgroupId { dst, dim } => {
619            lane.regs
620                .insert(dst.clone(), i64::from(lane.wg[*dim as usize]));
621            Ok(0)
622        }
623        Op::LaneId { dst } => {
624            lane.regs.insert(dst.clone(), i64::from(lane.lane_id));
625            Ok(0)
626        }
627        Op::WaveId { dst } => {
628            lane.regs.insert(dst.clone(), i64::from(lane.wave_id));
629            Ok(0)
630        }
631        Op::WaveSize { dst } => {
632            lane.regs.insert(dst.clone(), i64::from(lane.wave_size));
633            Ok(0)
634        }
635        Op::Add { dst, lhs, rhs, ty } => {
636            let a = get_reg(&lane.regs, lhs)?;
637            let b = get_reg(&lane.regs, rhs)?;
638            lane.regs
639                .insert(dst.clone(), narrow(a.wrapping_add(b), *ty)?);
640            Ok(0)
641        }
642        Op::Sub { dst, lhs, rhs, ty } => {
643            let a = get_reg(&lane.regs, lhs)?;
644            let b = get_reg(&lane.regs, rhs)?;
645            lane.regs
646                .insert(dst.clone(), narrow(a.wrapping_sub(b), *ty)?);
647            Ok(0)
648        }
649        Op::Mul { dst, lhs, rhs, ty } => {
650            let a = get_reg(&lane.regs, lhs)?;
651            let b = get_reg(&lane.regs, rhs)?;
652            lane.regs
653                .insert(dst.clone(), narrow(a.wrapping_mul(b), *ty)?);
654            Ok(0)
655        }
656        Op::CmpEq { dst, lhs, rhs, ty } => {
657            let a = narrow(get_reg(&lane.regs, lhs)?, *ty)?;
658            let b = narrow(get_reg(&lane.regs, rhs)?, *ty)?;
659            lane.regs.insert(dst.clone(), i64::from(a == b));
660            Ok(0)
661        }
662        Op::CmpNe { dst, lhs, rhs, ty } => {
663            let a = narrow(get_reg(&lane.regs, lhs)?, *ty)?;
664            let b = narrow(get_reg(&lane.regs, rhs)?, *ty)?;
665            lane.regs.insert(dst.clone(), i64::from(a != b));
666            Ok(0)
667        }
668        Op::And { dst, lhs, rhs, ty } => {
669            let a = get_reg(&lane.regs, lhs)?;
670            let b = get_reg(&lane.regs, rhs)?;
671            let v = match ty {
672                TypeId::I32 => i64::from((a as i32) & (b as i32)),
673                TypeId::U32 => i64::from((a as u32) & (b as u32)),
674                TypeId::U64 => (a as u64 & b as u64) as i64,
675            };
676            lane.regs.insert(dst.clone(), v);
677            Ok(0)
678        }
679        Op::KernargLoad { dst, offset, ty } => {
680            lane.regs
681                .insert(dst.clone(), kernarg_load(kernarg, *offset, *ty)?);
682            Ok(0)
683        }
684        Op::LoadGlobal { dst, addr, ty } => {
685            let a = get_reg(&lane.regs, addr)? as u64;
686            sanitizer.on_access(AddrSpace::Global, a, *ty, false, false, actor)?;
687            lane.regs.insert(dst.clone(), global.load(a, *ty)?);
688            Ok(0)
689        }
690        Op::StoreGlobal { addr, src, ty } => {
691            let a = get_reg(&lane.regs, addr)? as u64;
692            let v = get_reg(&lane.regs, src)?;
693            sanitizer.on_access(AddrSpace::Global, a, *ty, true, false, actor)?;
694            global.store(a, *ty, v)?;
695            Ok(0)
696        }
697        Op::LoadGroup { dst, addr, ty } => {
698            let a = get_reg(&lane.regs, addr)? as u64;
699            sanitizer.on_access(AddrSpace::Group, a, *ty, false, false, actor)?;
700            lane.regs.insert(dst.clone(), group.load(a, *ty)?);
701            Ok(0)
702        }
703        Op::StoreGroup { addr, src, ty } => {
704            let a = get_reg(&lane.regs, addr)? as u64;
705            let v = get_reg(&lane.regs, src)?;
706            sanitizer.on_access(AddrSpace::Group, a, *ty, true, false, actor)?;
707            group.store(a, *ty, v)?;
708            Ok(0)
709        }
710        Op::AtomicAdd {
711            dst,
712            addr,
713            src,
714            space,
715            scope: _,
716            order: _,
717        } => {
718            let a = get_reg(&lane.regs, addr)? as u64;
719            let v = get_reg(&lane.regs, src)?;
720            sanitizer.on_access(*space, a, TypeId::I32, true, true, actor)?;
721            let arena = match space {
722                AddrSpace::Global => &mut *global,
723                AddrSpace::Group => &mut *group,
724            };
725            let old = arena.load(a, TypeId::I32)?;
726            let newv = i64::from((old as i32).wrapping_add(v as i32));
727            arena.store(a, TypeId::I32, newv)?;
728            lane.regs.insert(dst.clone(), old);
729            Ok(1)
730        }
731        Op::Barrier | Op::If { .. } | Op::While { .. } | Op::Ret => Err(FunctionalError::Internal(
732            format!("unexpected op in exec_lane_op: {op:?}"),
733        )),
734    }
735}
736
737fn get_reg(regs: &BTreeMap<String, i64>, name: &str) -> Result<i64> {
738    regs.get(name)
739        .copied()
740        .ok_or_else(|| FunctionalError::UndefinedReg {
741            name: name.to_string(),
742        })
743}
744
745fn narrow(v: i64, ty: TypeId) -> Result<i64> {
746    Ok(match ty {
747        TypeId::I32 => v as i32 as i64,
748        TypeId::U32 => (v as u32) as i64,
749        TypeId::U64 => v,
750    })
751}