feat: implement virtual clock timing architecture (Phases 5-8)

Introduces a VirtualClock-based timing system replacing wall-clock
Instant-based timing for hardware-accurate simulation.

New modules:
- virtual_clock.rs: Shared virtual time source with condvar sync
- real_time_governor.rs: Thread that advances virtual time
- component_timing.rs: Per-component timing constraints

Key changes:
- Component trait: added update_at_time(), timing(), set_virtual_clock_optional()
- All Intel 400x components migrated to virtual-time-aware operation
- ConfigurableSystem: start() returns Result, spawns governor thread
- GUI: timing controls (speed slider, mode selector, step button)
- Console: speed/mode/step/time commands
- Backward compatible: legacy fallback for components without VirtualClock

Reviewed-by: code review found 2 warnings (clock resume() dead code,
reset() safety docs) — both fixed.
This commit is contained in:
Jan-Peter von Hunnius 2026-05-14 21:00:22 +02:00
parent add7ace674
commit 08f53d68fe
28 changed files with 7186 additions and 270 deletions

View file

@ -5,8 +5,11 @@
//! to emulator state for real-time GUI updates.
use crate::system_config::ConfigurableSystem;
use crate::real_time_governor::RealTimeGovernor;
use crate::virtual_clock::SimulationMode;
use parking_lot::Mutex;
use std::sync::Arc;
use std::time::Duration;
/// GUI state structure containing all UI-relevant data
#[derive(Debug, Clone)]
@ -29,6 +32,104 @@ pub struct GuiState {
pub register_state: RegisterState,
/// Last error message
pub last_error: Option<String>,
/// Timing/speed control state
pub timing_state: SimulationTimingState,
/// Speed snapshot for measuring actual simulation speed.
/// Uses RealTimeGovernor::SpeedSnapshot which wraps (Instant, u64) properly.
speed_snap_prev: Option<crate::real_time_governor::SpeedSnapshot>,
}
/// Timing and speed control state for the simulation.
///
/// Named `SimulationTimingState` to avoid confusion with the hardware
/// [`TimingState`](crate::components::common::intel_400x::TimingState) enum
/// that represents component-level timing states (e.g., WaitLatency, DriveData).
#[derive(Debug, Clone)]
pub struct SimulationTimingState {
/// Current virtual time in nanoseconds
pub virtual_time_ns: u64,
/// Total ticks elapsed
pub tick_count: u64,
/// Current target speed multiplier (from VirtualClock)
pub target_speed: f64,
/// Actual measured speed ratio (from RealTimeGovernor snapshots)
/// 1.0 = real-time, 0.5 = half speed, 2.0 = double speed
pub actual_speed: f64,
/// Current simulation mode as string: "RealTime", "MaxSpeed", "SingleStep"
pub mode: String,
/// Whether a step is pending (SingleStep mode)
pub step_pending: bool,
/// Whether the simulation is paused
pub paused: bool,
}
impl Default for SimulationTimingState {
fn default() -> Self {
Self {
virtual_time_ns: 0,
tick_count: 0,
target_speed: 1.0,
actual_speed: 0.0,
mode: "RealTime".to_string(),
step_pending: false,
paused: false,
}
}
}
/// Format a virtual time value in nanoseconds as a human-readable string.
///
/// Automatically selects the most appropriate unit (ns, µs, ms, s).
/// This is the single source of truth for virtual time formatting,
/// used by both the GUI and console interfaces.
pub fn format_virtual_time_ns(ns: u64) -> String {
if ns == 0 {
return "0 ns".to_string();
}
if ns < 1_000 {
format!("{} ns", ns)
} else if ns < 1_000_000 {
format!("{:.2} µs", ns as f64 / 1_000.0)
} else if ns < 1_000_000_000 {
format!("{:.2} ms", ns as f64 / 1_000_000.0)
} else {
format!("{:.3} s", ns as f64 / 1_000_000_000.0)
}
}
impl SimulationTimingState {
/// Format virtual time as a human-readable string.
///
/// Automatically selects the most appropriate unit (ns, µs, ms, s).
/// Delegates to [`format_virtual_time_ns`] for the actual formatting.
pub fn format_virtual_time(&self) -> String {
format_virtual_time_ns(self.virtual_time_ns)
}
/// Format the speed multiplier for display.
pub fn format_speed(&self) -> String {
if self.target_speed >= 100.0 {
format!("{:.0}×", self.target_speed)
} else if self.target_speed >= 1.0 {
format!("{:.1}×", self.target_speed)
} else {
format!("{:.3}×", self.target_speed)
}
}
/// Format the actual measured speed for display.
pub fn format_actual_speed(&self) -> String {
let speed = self.actual_speed;
if speed <= 0.0 {
"—".to_string()
} else if speed >= 100.0 {
format!("{:.0}×", speed)
} else if speed >= 1.0 {
format!("{:.1}×", speed)
} else {
format!("{:.3}×", speed)
}
}
}
/// System information for display
@ -118,6 +219,8 @@ impl GuiState {
selected_register: 0,
},
last_error: None,
timing_state: SimulationTimingState::default(),
speed_snap_prev: None,
}
}
@ -144,6 +247,9 @@ impl GuiState {
// Update ROM state from real data
self.update_rom_state(&system_guard);
// Update timing state from virtual clock
self.update_timing_state(&system_guard);
// Update cycle count (simulate for now)
if self.system_running {
self.cycle_count += 1;
@ -218,6 +324,56 @@ impl GuiState {
}
}
/// Update timing state from the virtual clock
fn update_timing_state(&mut self, system: &ConfigurableSystem) {
let clock = system.get_virtual_clock();
// Read current values from the virtual clock
self.timing_state.virtual_time_ns = clock.now_ns();
self.timing_state.tick_count = clock.tick_count();
self.timing_state.target_speed = clock.speed_multiplier();
self.timing_state.step_pending = clock.is_step_pending();
self.timing_state.paused = clock.is_paused();
let mode = clock.mode();
self.timing_state.mode = match mode {
SimulationMode::RealTime => "RealTime".to_string(),
SimulationMode::MaxSpeed => "MaxSpeed".to_string(),
SimulationMode::SingleStep => "SingleStep".to_string(),
};
// Compute actual speed using RealTimeGovernor's snapshot infrastructure.
// Shift current → previous, take new measurement, then compute delta.
// This avoids the precision loss from accumulating nanos in a single u128
// and delegates the calculation to the well-tested compute_speed() method.
//
// We require at least 100ms of wall-clock time between snapshots to produce
// a stable measurement. At 60 FPS, consecutive frames are ~16ms apart, which
// is too short for accurate speed calculation — especially at low simulation
// speeds where virtual time barely advances between frames.
const MIN_SPEED_SAMPLE_INTERVAL: Duration = Duration::from_millis(100);
let new_snap = RealTimeGovernor::take_speed_snapshot(clock);
let should_compute = self.speed_snap_prev.as_ref().map_or(false, |prev| {
new_snap.wall.duration_since(prev.wall) >= MIN_SPEED_SAMPLE_INTERVAL
});
if should_compute {
if let Some(prev) = &self.speed_snap_prev {
let speed = RealTimeGovernor::compute_speed(prev, &new_snap);
if speed > 0.0 {
self.timing_state.actual_speed = speed;
}
}
// Only shift snapshots when we actually computed a new speed value,
// so the next computation uses a baseline at least 100ms old.
self.speed_snap_prev = Some(new_snap);
} else if self.speed_snap_prev.is_none() {
// First frame — initialize the baseline snapshot
self.speed_snap_prev = Some(new_snap);
}
}
/// Set an error message
pub fn set_error(&mut self, error: String) {
self.last_error = Some(error);