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

@ -49,6 +49,7 @@
use super::state::GuiState;
use crate::system_config::{ConfigurableSystem, SystemController};
use crate::virtual_clock::SimulationMode;
use eframe::egui;
use parking_lot::Mutex;
use std::sync::Arc;
@ -75,6 +76,8 @@ use std::sync::Arc;
pub struct GuiComponents {
/// System control and management interface
control_panel: ControlPanel,
/// Timing/speed/mode/step controls
timing_controls: TimingControls,
/// RAM content inspection and visualization
memory_viewer: MemoryViewer,
/// ROM content inspection and visualization
@ -107,6 +110,7 @@ impl GuiComponents {
pub fn new() -> Self {
Self {
control_panel: ControlPanel::new(),
timing_controls: TimingControls::new(),
memory_viewer: MemoryViewer::new(),
rom_viewer: RomViewer::new(),
register_viewer: RegisterViewer::new(),
@ -133,6 +137,23 @@ impl GuiComponents {
self.control_panel.render(ui, state, system);
}
/// Render the timing controls component
///
/// Displays simulation speed, mode, step controls, and virtual time.
///
/// # Arguments
/// * `ui` - egui UI context for rendering
/// * `state` - Mutable reference to GUI state for display and interaction
/// * `system` - Optional reference to the emulator system for clock control
pub fn render_timing_controls(
&mut self,
ui: &mut egui::Ui,
state: &mut GuiState,
system: Option<&Arc<Mutex<ConfigurableSystem>>>,
) {
self.timing_controls.render(ui, state, system);
}
/// Render the memory viewer component
///
/// Displays RAM contents in a tabular format with bank selection
@ -398,6 +419,191 @@ impl ControlPanel {
}
}
/// Timing controls component for simulation speed, mode, and step management.
///
/// This component provides controls for the virtual clock system:
/// - **Speed slider**: Logarithmic scale from 0.001× to 100×
/// - **Mode selector**: RealTime / MaxSpeed / SingleStep radio buttons
/// - **Step button**: Advance one tick in SingleStep mode
/// - **Virtual time display**: Current time, tick count, actual vs target speed
///
/// ## Logarithmic Slider Mapping
///
/// The slider position `t` (0.0 to 1.0) maps to speed via:
/// `speed = 10^(t * 5 - 3)` giving range [0.001, 100.0]
pub struct TimingControls {
/// Current slider position (0.0 to 1.0), mapped logarithmically to speed
speed_slider_pos: f32,
}
impl TimingControls {
/// Create a new timing controls component
pub fn new() -> Self {
// Default: 1.0x speed → log10(1.0) = 0, so t = (0 + 3) / 5 = 0.6
Self {
speed_slider_pos: 0.6,
}
}
/// Convert slider position (0..1) to speed multiplier using logarithmic scale.
///
/// Range: 0.001× (position 0.0) to 100.0× (position 1.0)
/// Formula: speed = 10^(pos * 5 - 3)
fn pos_to_speed(pos: f32) -> f64 {
let pos = pos.clamp(0.0, 1.0) as f64;
10f64.powf(pos * 5.0 - 3.0)
}
/// Convert speed multiplier to slider position (0..1) using logarithmic scale.
///
/// Formula: pos = (log10(speed) + 3) / 5
fn speed_to_pos(speed: f64) -> f32 {
if speed <= 0.0 {
return 0.0;
}
let pos = (speed.log10() + 3.0) / 5.0;
pos.clamp(0.0, 1.0) as f32
}
/// Render the timing controls interface.
///
/// # Layout
/// ```text
/// ┌──────────────────────────────────────────────────────────────────┐
/// │ Timing Controls │
/// │ Speed: [========●===========] 1.00× (actual: 0.98×) │
/// │ Mode: (●) RealTime ( ) MaxSpeed ( ) SingleStep [Step] │
/// │ Virtual Time: 1.23 ms | Ticks: 3,690 | Paused: No │
/// └──────────────────────────────────────────────────────────────────┘
/// ```
pub fn render(
&mut self,
ui: &mut egui::Ui,
state: &mut GuiState,
system: Option<&Arc<Mutex<ConfigurableSystem>>>,
) {
// Sync slider position from actual clock speed so external changes
// (e.g., console "speed 0.1" command) are reflected in the GUI.
self.speed_slider_pos = Self::speed_to_pos(state.timing_state.target_speed);
ui.vertical(|ui| {
ui.heading("Timing Controls");
// Speed slider row
ui.horizontal(|ui| {
ui.label("Speed:");
let slider = egui::Slider::new(&mut self.speed_slider_pos, 0.0..=1.0)
.show_value(false)
.step_by(0.001);
let response = ui.add(slider);
if response.changed() {
let new_speed = Self::pos_to_speed(self.speed_slider_pos);
if let Some(sys) = system {
let sys_guard = sys.lock();
let clock = sys_guard.get_virtual_clock();
clock.set_speed_multiplier(new_speed);
// If in MaxSpeed mode, switch back to RealTime when slider moves
if clock.mode() == SimulationMode::MaxSpeed {
clock.set_mode(SimulationMode::RealTime);
}
}
}
// Show current target speed
let speed_text = state.timing_state.format_speed();
ui.label(format!("Target: {}", speed_text));
// Show actual speed
let actual_text = state.timing_state.format_actual_speed();
ui.label(format!("Actual: {}", actual_text));
});
// Mode selector and step button row
ui.horizontal(|ui| {
ui.label("Mode:");
let current_mode = state.timing_state.mode.as_str();
let is_realtime = current_mode == "RealTime";
let is_maxspeed = current_mode == "MaxSpeed";
let is_singlestep = current_mode == "SingleStep";
// RealTime radio
if ui.radio(is_realtime, "RealTime").clicked() {
self.apply_mode(system, SimulationMode::RealTime);
}
// MaxSpeed radio
if ui.radio(is_maxspeed, "MaxSpeed").clicked() {
self.apply_mode(system, SimulationMode::MaxSpeed);
}
// SingleStep radio
if ui.radio(is_singlestep, "SingleStep").clicked() {
self.apply_mode(system, SimulationMode::SingleStep);
}
ui.separator();
// Step button (enabled only in SingleStep mode)
let step_pending = state.timing_state.step_pending;
let step_button = ui.add_enabled(
is_singlestep && !step_pending,
egui::Button::new("Step"),
);
if step_button.clicked() {
if let Some(sys) = system {
let sys_guard = sys.lock();
let clock = sys_guard.get_virtual_clock();
clock.trigger_step();
}
}
if is_singlestep && step_pending {
ui.colored_label(egui::Color32::YELLOW, "⏳ Step pending...");
}
});
// Virtual time display row
ui.horizontal(|ui| {
ui.label("Virtual Time:");
ui.strong(state.timing_state.format_virtual_time());
ui.separator();
ui.label("Ticks:");
ui.strong(format!("{}", state.timing_state.tick_count));
ui.separator();
// Paused indicator
if state.timing_state.paused {
ui.colored_label(egui::Color32::YELLOW, "⏸ Paused");
} else if state.system_running {
ui.colored_label(egui::Color32::GREEN, "▶ Running");
} else {
ui.colored_label(egui::Color32::RED, "⏹ Stopped");
}
});
});
ui.separator();
}
/// Apply a simulation mode change to the virtual clock.
fn apply_mode(
&mut self,
system: Option<&Arc<Mutex<ConfigurableSystem>>>,
mode: SimulationMode,
) {
if let Some(sys) = system {
let sys_guard = sys.lock();
let clock = sys_guard.get_virtual_clock();
clock.set_mode(mode);
}
}
}
/// Memory viewer component for displaying RAM contents and state
///
/// The memory viewer provides comprehensive RAM inspection capabilities,