Skip to content

Commit 9599839

Browse files
committed
feat(pipeline): per-type stall counters, branch accuracy, speculative Gantt, FU stall fix
Four improvements to the pipeline simulator: 1. Per-type stall breakdown — stall_by_type[5] tracks RAW, Load-Use, Branch, FU, and MemLatency stall cycles separately. The Execution panel now shows a second stats line: "Stalls — RAW:N Load-Use:N Branch:N FU:N Mem:N". 2. Branch prediction accuracy — branches_executed counter added; the stats panel now shows "branches:N mispred:N (X%)" so misprediction rate is immediately visible. 3. Speculative Gantt cells — instructions fetched while a branch is unresolved now show in orange in the timing diagram (GanttCell::Speculative) instead of the same color as committed instructions. Flushed speculative instructions still show ◀FL. 4. FU stall not counted — FunctionalUnits mode stalls were never incremented in stall_count; fixed by adding the increment to the FU early-return path. Also cleaned up the outdated FMA "WORKAROUND" comments in stage_ex.
1 parent 07f28a6 commit 9599839

4 files changed

Lines changed: 92 additions & 35 deletions

File tree

src/ui/pipeline/mod.rs

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,9 @@ pub enum HazardType {
348348
}
349349

350350
impl HazardType {
351+
/// Number of stall-causing hazard types (WAW/WAR are informational, not counted).
352+
pub const STALL_TYPE_COUNT: usize = 5;
353+
351354
pub fn label(self) -> &'static str {
352355
match self {
353356
Self::Raw => "RAW",
@@ -359,6 +362,19 @@ impl HazardType {
359362
Self::War => "WAR",
360363
}
361364
}
365+
366+
/// Index into the `stall_by_type` array. Returns `None` for WAW/WAR which
367+
/// are informational only and do not cause pipeline stalls in an in-order pipeline.
368+
pub fn as_stall_index(self) -> Option<usize> {
369+
match self {
370+
Self::Raw => Some(0),
371+
Self::LoadUse => Some(1),
372+
Self::BranchFlush => Some(2),
373+
Self::FuBusy => Some(3),
374+
Self::MemLatency => Some(4),
375+
Self::Waw | Self::War => None,
376+
}
377+
}
362378
pub fn color(self) -> Color {
363379
match self {
364380
Self::Raw | Self::LoadUse => Color::Rgb(225, 180, 80),
@@ -540,11 +556,12 @@ pub const MAX_GANTT_COLS: usize = 20;
540556

541557
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
542558
pub enum GanttCell {
543-
Empty, // instruction not in pipeline yet / already done
544-
InStage(Stage), // instruction is in this stage
545-
Stall, // stalled in current stage
546-
Bubble, // NOP bubble occupies this slot
547-
Flush, // instruction was flushed
559+
Empty, // instruction not in pipeline yet / already done
560+
InStage(Stage), // instruction is in this stage
561+
Speculative(Stage), // instruction is in this stage but was fetched speculatively
562+
Stall, // stalled in current stage
563+
Bubble, // NOP bubble occupies this slot
564+
Flush, // instruction was flushed (branch misprediction)
548565
}
549566

550567
#[derive(Clone)]
@@ -746,7 +763,12 @@ pub struct PipelineSimState {
746763
pub cycle_count: u64,
747764
pub instr_committed: u64,
748765
pub stall_count: u64,
766+
/// Stall cycles broken down by hazard type (indexed by HazardType::as_stall_index).
767+
/// Indices: 0=RAW, 1=LoadUse, 2=BranchFlush, 3=FuBusy, 4=MemLatency.
768+
pub stall_by_type: [u64; HazardType::STALL_TYPE_COUNT],
749769
pub flush_count: u64,
770+
/// Total branch and jump instructions committed.
771+
pub branches_executed: u64,
750772
pub class_counts: [u64; InstrClass::COUNT],
751773

752774
// ── Gantt history ──
@@ -802,7 +824,9 @@ impl PipelineSimState {
802824
cycle_count: 0,
803825
instr_committed: 0,
804826
stall_count: 0,
827+
stall_by_type: [0; HazardType::STALL_TYPE_COUNT],
805828
flush_count: 0,
829+
branches_executed: 0,
806830
class_counts: [0; InstrClass::COUNT],
807831
gantt: VecDeque::new(),
808832
speed: PipelineSpeed::Normal,

src/ui/pipeline/sim.rs

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ pub fn pipeline_tick(
4747
if let Some((stall_stage, hazard)) = stall {
4848
insert_stall(state, stall_stage, hazard, cpu, mem, console);
4949
state.stall_count += 1;
50+
if let Some(idx) = hazard.as_stall_index() {
51+
state.stall_by_type[idx] += 1;
52+
}
5053
} else {
5154
advance_stages(state, cpu, mem, cpi, console);
5255
}
@@ -434,17 +437,12 @@ fn stage_ex(slot: &mut PipeSlot) {
434437
_ => 0x000,
435438
};
436439
}
440+
// Fused multiply-add family: rs3 is read at ID and stored in slot.mem_addr
441+
// (the FP ALU instructions never use mem_addr for actual addressing).
442+
// Forwarding for rs3 is handled in apply_forwarding_to_id (same field).
437443
Instruction::FmaddS { .. } => {
438444
let a = f32::from_bits(slot.rs1_val);
439445
let b = f32::from_bits(slot.rs2_val);
440-
// rs3 is not in our slot — we only have rs1/rs2. For fused ops, we stored rs3's
441-
// value... actually we don't have an rs3_val field. We'll need the instruction's rs3.
442-
// For now, use the decoded instruction to get rs3 index and read from the slot's
443-
// existing values. But we need a workaround here.
444-
// Actually: fmadd uses rs1, rs2, rs3 — but we only read rs1, rs2 at ID.
445-
// For rs3, we need to handle it specially at ID stage.
446-
// WORKAROUND: store rs3 value in mem_addr field (reusing unused field for FP arith)
447-
// This is set in stage_id via special handling.
448446
let c = f32::from_bits(slot.mem_addr.unwrap_or(0));
449447
slot.alu_result = (a * b + c).to_bits();
450448
}
@@ -1481,6 +1479,9 @@ fn commit_wb(
14811479

14821480
state.instr_committed += 1;
14831481
state.class_counts[slot.class.as_usize()] += 1;
1482+
if matches!(slot.class, InstrClass::Branch | InstrClass::Jump) {
1483+
state.branches_executed += 1;
1484+
}
14841485

14851486
Some(CommitInfo {
14861487
pc: slot.pc,
@@ -2087,6 +2088,8 @@ fn advance_stages(
20872088
s.hazard = Some(HazardType::MemLatency);
20882089
}
20892090
}
2091+
state.stall_count += 1;
2092+
state.stall_by_type[HazardType::FuBusy.as_stall_index().unwrap()] += 1;
20902093
return;
20912094
}
20922095
}
@@ -2145,6 +2148,7 @@ fn advance_stages(
21452148
s.hazard = Some(HazardType::MemLatency);
21462149
}
21472150
state.stall_count += 1;
2151+
state.stall_by_type[HazardType::MemLatency.as_stall_index().unwrap()] += 1;
21482152
}
21492153

21502154
let ex_producer = state.stages[Stage::EX as usize].clone();
@@ -2303,6 +2307,7 @@ fn update_gantt(state: &mut PipelineSimState) {
23032307
None => continue,
23042308
Some(s) if s.is_bubble && s.hazard == Some(HazardType::BranchFlush) => GanttCell::Flush,
23052309
Some(s) if s.is_bubble => GanttCell::Bubble,
2310+
Some(s) if s.is_speculative => GanttCell::Speculative(stage),
23062311
Some(_) => GanttCell::InStage(stage),
23072312
};
23082313
if let Some(slot) = maybe_slot {
@@ -2317,15 +2322,20 @@ fn update_gantt(state: &mut PipelineSimState) {
23172322
};
23182323
let row = state.gantt.iter_mut().find(|r| r.pc == pc && !r.done);
23192324
if let Some(row) = row {
2320-
let emit_cell = if let GanttCell::InStage(s) = cell {
2321-
if row.last_stage == Some(s) {
2322-
GanttCell::Stall
2323-
} else {
2325+
let emit_cell = match cell {
2326+
GanttCell::InStage(s) => {
2327+
if row.last_stage == Some(s) {
2328+
GanttCell::Stall
2329+
} else {
2330+
row.last_stage = Some(s);
2331+
GanttCell::InStage(s)
2332+
}
2333+
}
2334+
GanttCell::Speculative(s) => {
23242335
row.last_stage = Some(s);
2325-
GanttCell::InStage(s)
2336+
GanttCell::Speculative(s)
23262337
}
2327-
} else {
2328-
cell
2338+
_ => cell,
23292339
};
23302340
let expected_len = (cycle - row.first_cycle) as usize;
23312341
while row.cells.len() < expected_len {
@@ -2340,10 +2350,9 @@ fn update_gantt(state: &mut PipelineSimState) {
23402350
row.cells.pop_front();
23412351
}
23422352
} else {
2343-
let initial_stage = if let GanttCell::InStage(s) = cell {
2344-
Some(s)
2345-
} else {
2346-
None
2353+
let initial_stage = match cell {
2354+
GanttCell::InStage(s) | GanttCell::Speculative(s) => Some(s),
2355+
_ => None,
23472356
};
23482357
let mut new_row = GanttRow {
23492358
pc,

src/ui/view/pipeline/main_view.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -941,13 +941,21 @@ fn render_gantt(f: &mut Frame, area: Rect, app: &App) {
941941
}
942942

943943
fn cell_to_span(cell: GanttCell) -> (&'static str, Style) {
944+
// Speculative stages: same label as InStage but in orange — shows instruction
945+
// was fetched/decoded speculatively while a branch was unresolved.
946+
let spec_style = Style::default().fg(Color::Rgb(220, 140, 40));
944947
match cell {
945948
GanttCell::Empty => ("·", Style::default().fg(theme::BORDER)),
946949
GanttCell::InStage(Stage::IF) => ("IF", Style::default().fg(theme::ACCENT)),
947950
GanttCell::InStage(Stage::ID) => ("ID", Style::default().fg(theme::LABEL_Y)),
948951
GanttCell::InStage(Stage::EX) => ("EX", Style::default().fg(theme::RUNNING)),
949952
GanttCell::InStage(Stage::MEM) => ("MM", Style::default().fg(theme::LABEL_Y)),
950953
GanttCell::InStage(Stage::WB) => ("WB", Style::default().fg(theme::ACCENT)),
954+
GanttCell::Speculative(Stage::IF) => ("IF", spec_style),
955+
GanttCell::Speculative(Stage::ID) => ("ID", spec_style),
956+
GanttCell::Speculative(Stage::EX) => ("EX", spec_style),
957+
GanttCell::Speculative(Stage::MEM) => ("MM", spec_style),
958+
GanttCell::Speculative(Stage::WB) => ("WB", spec_style),
951959
GanttCell::Stall => ("──", Style::default().fg(theme::PAUSED)),
952960
GanttCell::Bubble => (
953961
"NOP",

src/ui/view/pipeline/mod.rs

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -167,20 +167,36 @@ fn render_exec_controls(f: &mut Frame, area: Rect, app: &App) {
167167
));
168168
let line1 = Line::from(spans);
169169

170-
let cpi_str = if p.instr_committed > 0 {
171-
format!(
172-
" Cycle:{} CPI:{:.2} instrs:{} stalls:{} flushes:{}",
173-
p.cycle_count,
174-
p.cycle_count as f64 / p.instr_committed as f64,
175-
p.instr_committed,
176-
p.stall_count,
177-
p.flush_count,
178-
)
170+
let (cpi_str, stall_str) = if p.instr_committed > 0 {
171+
let cpi = p.cycle_count as f64 / p.instr_committed as f64;
172+
let branch_str = if p.branches_executed > 0 {
173+
let mispredict_pct =
174+
p.flush_count as f64 / p.branches_executed as f64 * 100.0;
175+
format!(
176+
" branches:{} mispred:{} ({:.0}%)",
177+
p.branches_executed, p.flush_count, mispredict_pct
178+
)
179+
} else {
180+
String::new()
181+
};
182+
let main = format!(
183+
" Cycle:{} CPI:{cpi:.2} instrs:{} stalls:{}{}",
184+
p.cycle_count, p.instr_committed, p.stall_count, branch_str,
185+
);
186+
let [raw, lu, br, fu, mem] = p.stall_by_type;
187+
let detail = format!(
188+
" Stalls — RAW:{raw} Load-Use:{lu} Branch:{br} FU:{fu} Mem:{mem}"
189+
);
190+
(main, detail)
179191
} else {
180-
format!(" Cycle:{} (nenhuma instrução committed)", p.cycle_count)
192+
(
193+
format!(" Cycle:{} (no instructions committed)", p.cycle_count),
194+
String::new(),
195+
)
181196
};
182197

183198
let line2 = Line::from(Span::styled(cpi_str, Style::default().fg(theme::LABEL)));
199+
let line3 = Line::from(Span::styled(stall_str, Style::default().fg(theme::LABEL)));
184200

185201
let block = Block::default()
186202
.borders(Borders::ALL)
@@ -189,7 +205,7 @@ fn render_exec_controls(f: &mut Frame, area: Rect, app: &App) {
189205
.title(Span::styled("Execução", Style::default().fg(theme::LABEL)));
190206
let inner = block.inner(area);
191207
f.render_widget(block, area);
192-
f.render_widget(Paragraph::new(vec![line1, line2]), inner);
208+
f.render_widget(Paragraph::new(vec![line1, line2, line3]), inner);
193209

194210
// Record button geometry for mouse
195211
// speed <label> state <label> reset

0 commit comments

Comments
 (0)