1use crate::error::{FunctionalError, Result};
14use crate::exec::{run_with_config_sanitized, ExecConfig, LaunchConfig, RunReport, SchedulePolicy};
15use crate::ir::{AddrSpace, Op, Program};
16use crate::memory::GlobalArena;
17use crate::sanitize::{
18 Finding, FindingKind, ReplayBundle, SanitizeMode, SanitizeReport, WorkItemId,
19};
20use serde::{Deserialize, Serialize};
21use std::collections::BTreeMap;
22
23pub const DEBUG_TRACE_SCHEMA: &str = "softgpu-debug-trace-v1";
24pub const DEFAULT_TRACE_EVENT_BUDGET: usize = 64 * 1024;
25pub const MAX_TRACE_BYTES: usize = 8 * 1024 * 1024;
26pub const MAX_TRACE_LINE_BYTES: usize = 64 * 1024;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(rename_all = "snake_case")]
30pub enum DebugAction {
31 Continue,
32 Break,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
36#[serde(rename_all = "snake_case")]
37pub enum Breakpoint {
38 AfterStep { step: u64 },
40 OnMemoryAccess,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
45#[serde(rename_all = "snake_case")]
46pub enum TraceEvent {
47 Header {
48 schema: String,
49 program_name: String,
50 source_provenance: String,
51 grid: [u32; 3],
52 workgroup: [u32; 3],
53 wave_size: u32,
54 schedule: SchedulePolicy,
55 schedule_seed: u64,
56 note: String,
57 },
58 Step {
59 step: u64,
60 barrier_gen: u64,
61 actor: WorkItemId,
62 op: String,
63 },
64 MemoryAccess {
65 step: u64,
66 space: AddrSpace,
67 addr: u64,
68 is_write: bool,
69 is_atomic: bool,
70 actor: WorkItemId,
71 },
72 Barrier {
73 barrier_gen: u64,
74 },
75 Break {
76 step: u64,
77 reason: String,
78 },
79 SanitizeFinding {
80 finding: Finding,
81 },
82 Done {
83 steps: u64,
84 workitems: u64,
85 },
86}
87
88#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
89pub struct StateSnapshot {
90 pub step: u64,
91 pub barrier_gen: u64,
92 pub actor: WorkItemId,
93 pub op: String,
94 pub registers: BTreeMap<String, i64>,
95 pub source_provenance: String,
97 pub source_mapping: Option<()>,
98}
99
100impl StateSnapshot {
101 pub fn from_lane(
102 step: u64,
103 barrier_gen: u64,
104 actor: WorkItemId,
105 op: &str,
106 regs: &BTreeMap<String, i64>,
107 program: &Program,
108 ) -> Self {
109 Self {
110 step,
111 barrier_gen,
112 actor,
113 op: op.to_string(),
114 registers: regs.clone(),
115 source_provenance: program.source_provenance.clone(),
116 source_mapping: None,
118 }
119 }
120}
121
122#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
123pub struct DebugStop {
124 pub reason: String,
125 pub snapshot: StateSnapshot,
126}
127
128#[derive(Debug, Default)]
129pub struct TraceLog {
130 events: Vec<TraceEvent>,
131 budget: usize,
132 exhausted: bool,
133}
134
135impl TraceLog {
136 pub fn new(budget: usize) -> Self {
137 Self {
138 events: Vec::new(),
139 budget: budget.max(1),
140 exhausted: false,
141 }
142 }
143
144 pub fn push(&mut self, ev: TraceEvent) -> Result<()> {
145 if self.events.len() >= self.budget {
146 self.exhausted = true;
147 return Err(FunctionalError::Validation {
148 detail: format!(
149 "debug trace event budget exhausted (budget={})",
150 self.budget
151 ),
152 });
153 }
154 self.events.push(ev);
155 Ok(())
156 }
157
158 pub fn events(&self) -> &[TraceEvent] {
159 &self.events
160 }
161
162 pub fn exhausted(&self) -> bool {
163 self.exhausted
164 }
165
166 pub fn to_jsonl(&self) -> Result<String> {
167 let mut out = String::new();
168 for ev in &self.events {
169 let line =
170 serde_json::to_string(ev).map_err(|e| FunctionalError::Internal(e.to_string()))?;
171 out.push_str(&line);
172 out.push('\n');
173 }
174 Ok(out)
175 }
176}
177
178pub fn parse_trace_jsonl(input: &str) -> Result<Vec<TraceEvent>> {
180 if input.len() > MAX_TRACE_BYTES {
181 return Err(FunctionalError::Validation {
182 detail: format!(
183 "trace input {} bytes exceeds SoftGPU max {MAX_TRACE_BYTES}",
184 input.len()
185 ),
186 });
187 }
188 let mut events = Vec::new();
189 for (i, line) in input.lines().enumerate() {
190 let line = line.trim();
191 if line.is_empty() {
192 continue;
193 }
194 if line.len() > MAX_TRACE_LINE_BYTES {
195 return Err(FunctionalError::Validation {
196 detail: format!(
197 "trace line {} length {} exceeds SoftGPU max {MAX_TRACE_LINE_BYTES}",
198 i + 1,
199 line.len()
200 ),
201 });
202 }
203 let ev: TraceEvent = serde_json::from_str(line)
204 .map_err(|e| FunctionalError::Parse(format!("trace line {}: {e}", i + 1)))?;
205 if events.len() >= DEFAULT_TRACE_EVENT_BUDGET {
206 return Err(FunctionalError::Validation {
207 detail: format!(
208 "trace has more than {DEFAULT_TRACE_EVENT_BUDGET} events (SoftGPU limit)"
209 ),
210 });
211 }
212 events.push(ev);
213 }
214 if let Some(TraceEvent::Header { schema, .. }) = events.first() {
215 if schema != DEBUG_TRACE_SCHEMA {
216 return Err(FunctionalError::Validation {
217 detail: format!("trace schema '{schema}' != '{DEBUG_TRACE_SCHEMA}'"),
218 });
219 }
220 } else if !events.is_empty() {
221 return Err(FunctionalError::Validation {
222 detail: "debug trace must begin with a header event".into(),
223 });
224 }
225 Ok(events)
226}
227
228fn op_label(op: &Op) -> String {
229 match op {
230 Op::Const { .. } => "const".into(),
231 Op::GlobalId { .. } => "global_id".into(),
232 Op::LocalId { .. } => "local_id".into(),
233 Op::WorkgroupId { .. } => "workgroup_id".into(),
234 Op::LaneId { .. } => "lane_id".into(),
235 Op::WaveId { .. } => "wave_id".into(),
236 Op::WaveSize { .. } => "wave_size".into(),
237 Op::Add { .. } => "add".into(),
238 Op::Sub { .. } => "sub".into(),
239 Op::Mul { .. } => "mul".into(),
240 Op::CmpEq { .. } => "cmp_eq".into(),
241 Op::CmpNe { .. } => "cmp_ne".into(),
242 Op::And { .. } => "and".into(),
243 Op::KernargLoad { .. } => "kernarg_load".into(),
244 Op::LoadGlobal { .. } => "load_global".into(),
245 Op::StoreGlobal { .. } => "store_global".into(),
246 Op::LoadGroup { .. } => "load_group".into(),
247 Op::StoreGroup { .. } => "store_group".into(),
248 Op::AtomicAdd { .. } => "atomic_add".into(),
249 Op::Barrier => "barrier".into(),
250 Op::If { .. } => "if".into(),
251 Op::While { .. } => "while".into(),
252 Op::Ret => "ret".into(),
253 }
254}
255
256fn is_memory_op(op: &Op) -> bool {
257 matches!(
258 op,
259 Op::LoadGlobal { .. }
260 | Op::StoreGlobal { .. }
261 | Op::LoadGroup { .. }
262 | Op::StoreGroup { .. }
263 | Op::AtomicAdd { .. }
264 )
265}
266
267pub trait ExecObserver {
269 fn on_lane_step(
270 &mut self,
271 step: u64,
272 barrier_gen: u64,
273 actor: WorkItemId,
274 op: &Op,
275 regs: &BTreeMap<String, i64>,
276 ) -> Result<DebugAction>;
277
278 fn on_barrier(&mut self, barrier_gen: u64) -> Result<()> {
279 let _ = barrier_gen;
280 Ok(())
281 }
282}
283
284#[derive(Debug)]
285pub struct NoopObserver;
286
287impl ExecObserver for NoopObserver {
288 fn on_lane_step(
289 &mut self,
290 _step: u64,
291 _barrier_gen: u64,
292 _actor: WorkItemId,
293 _op: &Op,
294 _regs: &BTreeMap<String, i64>,
295 ) -> Result<DebugAction> {
296 Ok(DebugAction::Continue)
297 }
298}
299
300pub struct DebugSession<'a> {
301 program: &'a Program,
302 breakpoints: Vec<Breakpoint>,
303 pub trace: TraceLog,
304 stop: Option<DebugStop>,
305 single_step: bool,
306}
307
308impl<'a> DebugSession<'a> {
309 pub fn new(program: &'a Program, breakpoints: Vec<Breakpoint>, trace_budget: usize) -> Self {
310 Self {
311 program,
312 breakpoints,
313 trace: TraceLog::new(trace_budget),
314 stop: None,
315 single_step: false,
316 }
317 }
318
319 pub fn with_single_step(mut self) -> Self {
320 self.single_step = true;
321 self
322 }
323
324 pub fn take_stop(&mut self) -> Option<DebugStop> {
325 self.stop.take()
326 }
327
328 fn should_break(&self, step: u64, op: &Op) -> Option<&'static str> {
329 if self.single_step {
330 return Some("single_step");
331 }
332 for bp in &self.breakpoints {
333 match bp {
334 Breakpoint::AfterStep { step: s } if *s == step => return Some("after_step"),
335 Breakpoint::OnMemoryAccess if is_memory_op(op) => return Some("memory_access"),
336 _ => {}
337 }
338 }
339 None
340 }
341}
342
343impl ExecObserver for DebugSession<'_> {
344 fn on_lane_step(
345 &mut self,
346 step: u64,
347 barrier_gen: u64,
348 actor: WorkItemId,
349 op: &Op,
350 regs: &BTreeMap<String, i64>,
351 ) -> Result<DebugAction> {
352 let label = op_label(op);
353 self.trace.push(TraceEvent::Step {
354 step,
355 barrier_gen,
356 actor,
357 op: label.clone(),
358 })?;
359 if is_memory_op(op) {
360 let (space, is_write, is_atomic) = match op {
361 Op::LoadGlobal { .. } => (AddrSpace::Global, false, false),
362 Op::StoreGlobal { .. } => (AddrSpace::Global, true, false),
363 Op::LoadGroup { .. } => (AddrSpace::Group, false, false),
364 Op::StoreGroup { .. } => (AddrSpace::Group, true, false),
365 Op::AtomicAdd { space, .. } => (*space, true, true),
366 _ => unreachable!(),
367 };
368 self.trace.push(TraceEvent::MemoryAccess {
370 step,
371 space,
372 addr: 0,
373 is_write,
374 is_atomic,
375 actor,
376 })?;
377 }
378 if let Some(reason) = self.should_break(step, op) {
379 let snap =
380 StateSnapshot::from_lane(step, barrier_gen, actor, &label, regs, self.program);
381 self.trace.push(TraceEvent::Break {
382 step,
383 reason: reason.into(),
384 })?;
385 self.stop = Some(DebugStop {
386 reason: reason.into(),
387 snapshot: snap,
388 });
389 return Ok(DebugAction::Break);
390 }
391 Ok(DebugAction::Continue)
392 }
393
394 fn on_barrier(&mut self, barrier_gen: u64) -> Result<()> {
395 self.trace.push(TraceEvent::Barrier { barrier_gen })
396 }
397}
398
399#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
400pub struct DebugRunReport {
401 pub fidelity: &'static str,
402 pub mode: &'static str,
403 pub note: &'static str,
404 pub run: Option<RunReport>,
405 pub sanitize: SanitizeReport,
406 pub stop: Option<DebugStop>,
407 pub trace_events: usize,
408}
409
410pub fn run_debug(
412 program: &Program,
413 cfg: ExecConfig,
414 arena: &mut GlobalArena,
415 kernarg: &[u8],
416 breakpoints: Vec<Breakpoint>,
417 single_step: bool,
418 trace_budget: usize,
419) -> Result<(DebugRunReport, TraceLog)> {
420 let mut session = DebugSession::new(program, breakpoints, trace_budget);
421 if single_step {
422 session = session.with_single_step();
423 }
424 session.trace.push(TraceEvent::Header {
425 schema: DEBUG_TRACE_SCHEMA.into(),
426 program_name: program.name.clone(),
427 source_provenance: program.source_provenance.clone(),
428 grid: cfg.launch.grid,
429 workgroup: cfg.launch.workgroup,
430 wave_size: cfg.wave_size,
431 schedule: cfg.schedule,
432 schedule_seed: cfg.schedule_seed,
433 note: "not_gfx1201_isa_emulation".into(),
434 })?;
435
436 let outcome = crate::exec::run_with_observer(program, cfg, arena, kernarg, &mut session);
437 let stop = session.take_stop();
438 match outcome {
439 Ok((run, sanitize)) => {
440 for f in &sanitize.findings {
441 let _ = session
442 .trace
443 .push(TraceEvent::SanitizeFinding { finding: f.clone() });
444 }
445 session.trace.push(TraceEvent::Done {
446 steps: run.steps,
447 workitems: run.workitems_executed,
448 })?;
449 let n = session.trace.events().len();
450 Ok((
451 DebugRunReport {
452 fidelity: "functional",
453 mode: "softgpu_functional_debug_v1",
454 note: "not_gfx1201_isa_emulation",
455 run: Some(run),
456 sanitize,
457 stop,
458 trace_events: n,
459 },
460 session.trace,
461 ))
462 }
463 Err(FunctionalError::DebugBreak) => {
464 let n = session.trace.events().len();
465 Ok((
466 DebugRunReport {
467 fidelity: "functional",
468 mode: "softgpu_functional_debug_v1",
469 note: "not_gfx1201_isa_emulation",
470 run: None,
471 sanitize: SanitizeReport::clean(),
472 stop,
473 trace_events: n,
474 },
475 session.trace,
476 ))
477 }
478 Err(FunctionalError::Sanitize(finding)) => {
479 let _ = session.trace.push(TraceEvent::SanitizeFinding {
480 finding: finding.clone(),
481 });
482 let snap = StateSnapshot {
483 step: finding.step,
484 barrier_gen: finding.barrier_gen,
485 actor: finding.actor,
486 op: format!("{:?}", finding.kind),
487 registers: BTreeMap::new(),
488 source_provenance: program.source_provenance.clone(),
489 source_mapping: None,
490 };
491 let _ = session.trace.push(TraceEvent::Break {
492 step: finding.step,
493 reason: "sanitize_finding".into(),
494 });
495 let n = session.trace.events().len();
496 Ok((
497 DebugRunReport {
498 fidelity: "sanitized",
499 mode: "softgpu_functional_debug_v1",
500 note: "not_gfx1201_isa_emulation",
501 run: None,
502 sanitize: SanitizeReport {
503 fidelity: "sanitized",
504 mode: "softgpu_functional_sanitizer_v1",
505 note: "not_gfx1201_isa_emulation",
506 findings: vec![finding],
507 },
508 stop: Some(DebugStop {
509 reason: "sanitize_finding".into(),
510 snapshot: snap,
511 }),
512 trace_events: n,
513 },
514 session.trace,
515 ))
516 }
517 Err(e) => Err(e),
518 }
519}
520
521pub fn inspect_sanitizer_finding(
523 program: &Program,
524 mut cfg: ExecConfig,
525 arena: &mut GlobalArena,
526 kernarg: &[u8],
527 finding: &Finding,
528) -> Result<(DebugRunReport, TraceLog, ReplayBundle)> {
529 cfg.sanitize = SanitizeMode::FailFast;
530 let bps = vec![Breakpoint::AfterStep {
531 step: finding.step.max(1),
532 }];
533 let (report, trace) = run_debug(
534 program,
535 cfg,
536 arena,
537 kernarg,
538 bps,
539 false,
540 DEFAULT_TRACE_EVENT_BUDGET,
541 )?;
542 let bundle = ReplayBundle::from_finding(program, &cfg, finding.clone());
543 Ok((report, trace, bundle))
544}
545
546#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
547pub struct ExploreTrial {
548 pub wave_size: u32,
549 pub schedule_seed: u64,
550 pub workgroup_x: u32,
551 pub found: bool,
552 pub kind: Option<FindingKind>,
553 pub step: Option<u64>,
554}
555
556#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
557pub struct ExploreReport {
558 pub fidelity: &'static str,
559 pub mode: &'static str,
560 pub note: &'static str,
561 pub seed: u64,
562 pub trials: Vec<ExploreTrial>,
563 pub minimized: Option<ExploreTrial>,
564}
565
566pub fn explore_and_minimize_race(
568 program: &Program,
569 base: ExecConfig,
570 kernarg: &[u8],
571 arena_len: usize,
572 seed: u64,
573 max_trials: u32,
574) -> Result<ExploreReport> {
575 let mut trials = Vec::new();
576 let mut hit: Option<ExploreTrial> = None;
577 let wave_candidates = [
578 base.wave_size,
579 ((seed % 31) as u32 + 1).min(base.launch.workgroup[0].max(1)),
580 1,
581 2,
582 4,
583 8,
584 base.launch.workgroup[0].max(1),
585 ];
586 let mut attempt = 0u32;
587 for &ws in &wave_candidates {
588 if attempt >= max_trials {
589 break;
590 }
591 for bit in 0..2u64 {
592 if attempt >= max_trials {
593 break;
594 }
595 attempt += 1;
596 let mut cfg = base;
597 cfg.wave_size = ws.max(1);
598 cfg.schedule_seed = seed ^ (bit << 1) ^ u64::from(ws);
599 cfg.sanitize = SanitizeMode::FailFast;
600 let mut arena = GlobalArena::new(arena_len);
601 let found = match run_with_config_sanitized(program, cfg, &mut arena, kernarg) {
602 Err(FunctionalError::Sanitize(f))
603 if matches!(
604 f.kind,
605 FindingKind::Race
606 | FindingKind::MissingBarrier
607 | FindingKind::CrossWorkgroupRace
608 ) =>
609 {
610 Some(ExploreTrial {
611 wave_size: cfg.wave_size,
612 schedule_seed: cfg.schedule_seed,
613 workgroup_x: cfg.launch.workgroup[0],
614 found: true,
615 kind: Some(f.kind),
616 step: Some(f.step),
617 })
618 }
619 Ok(_) | Err(FunctionalError::Sanitize(_)) => Some(ExploreTrial {
620 wave_size: cfg.wave_size,
621 schedule_seed: cfg.schedule_seed,
622 workgroup_x: cfg.launch.workgroup[0],
623 found: false,
624 kind: None,
625 step: None,
626 }),
627 Err(e) => return Err(e),
628 };
629 if let Some(t) = found {
630 if t.found && hit.is_none() {
631 hit = Some(t.clone());
632 }
633 trials.push(t);
634 }
635 }
636 }
637
638 let minimized = if let Some(h) = hit.clone() {
639 let mut best = h.clone();
640 let mut wg = h.workgroup_x;
641 while wg > 1 {
642 let next = wg / 2;
643 let mut cfg = base;
644 cfg.wave_size = best.wave_size.min(next).max(1);
645 cfg.schedule_seed = best.schedule_seed;
646 cfg.sanitize = SanitizeMode::FailFast;
647 cfg.launch = LaunchConfig {
648 grid: [next, 1, 1],
649 workgroup: [next, 1, 1],
650 };
651 let mut arena = GlobalArena::new(arena_len);
652 match run_with_config_sanitized(program, cfg, &mut arena, kernarg) {
653 Err(FunctionalError::Sanitize(f))
654 if matches!(
655 f.kind,
656 FindingKind::Race
657 | FindingKind::MissingBarrier
658 | FindingKind::CrossWorkgroupRace
659 ) =>
660 {
661 best = ExploreTrial {
662 wave_size: cfg.wave_size,
663 schedule_seed: cfg.schedule_seed,
664 workgroup_x: next,
665 found: true,
666 kind: Some(f.kind),
667 step: Some(f.step),
668 };
669 wg = next;
670 }
671 _ => break,
672 }
673 }
674 if let Some(step) = best.step {
676 let mut lo = 1u64;
677 let mut hi = step;
678 let mut earliest = step;
679 while lo < hi {
680 let mid = (lo + hi) / 2;
681 let mut cfg = base;
682 cfg.wave_size = best.wave_size;
683 cfg.schedule_seed = best.schedule_seed;
684 cfg.sanitize = SanitizeMode::FailFast;
685 cfg.launch = LaunchConfig {
686 grid: [best.workgroup_x, 1, 1],
687 workgroup: [best.workgroup_x, 1, 1],
688 };
689 cfg.step_budget = mid;
690 let mut arena = GlobalArena::new(arena_len);
691 match run_with_config_sanitized(program, cfg, &mut arena, kernarg) {
692 Err(FunctionalError::Sanitize(f)) => {
693 earliest = f.step.min(earliest);
694 hi = mid;
695 }
696 Err(FunctionalError::StepBudgetExceeded { .. }) => {
697 lo = mid + 1;
698 }
699 _ => {
700 lo = mid + 1;
701 }
702 }
703 }
704 best.step = Some(earliest);
705 }
706 Some(best)
707 } else {
708 None
709 };
710
711 Ok(ExploreReport {
712 fidelity: "functional",
713 mode: "softgpu_schedule_explore_v1",
714 note: "not_gfx1201_isa_emulation",
715 seed,
716 trials,
717 minimized,
718 })
719}
720
721#[cfg(test)]
722mod tests {
723 use super::*;
724
725 #[test]
726 fn schema_constant_stable() {
727 assert_eq!(DEBUG_TRACE_SCHEMA, "softgpu-debug-trace-v1");
728 }
729
730 #[test]
731 fn snapshot_never_invents_source_mapping() {
732 let p = Program {
733 schema: crate::ir::SFIR_SCHEMA.into(),
734 fidelity: "functional".into(),
735 note: "not_gfx1201_isa_emulation".into(),
736 name: "t".into(),
737 source_provenance: "SoftGPU-authored".into(),
738 kernarg_layout: vec![],
739 group_bytes: 0,
740 body: vec![Op::Ret],
741 };
742 let snap = StateSnapshot::from_lane(
743 1,
744 0,
745 WorkItemId {
746 workgroup: [0, 0, 0],
747 wave: 0,
748 lane: 0,
749 flat_local: 0,
750 },
751 "const",
752 &BTreeMap::new(),
753 &p,
754 );
755 assert!(snap.source_mapping.is_none());
756 assert_eq!(snap.source_provenance, "SoftGPU-authored");
757 }
758}