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

@ -8,7 +8,9 @@ use std::sync::{
use std::thread;
use std::time::Duration;
use crate::component_timing::ComponentTiming;
use crate::pin::Pin;
use crate::virtual_clock::VirtualClock;
/// Core trait for all hardware components in the emulator
/// Provides the fundamental interface that all components must implement
@ -58,6 +60,66 @@ pub trait Component: Send + Sync {
/// This allows runtime type inspection to access component-specific methods mutably
/// Returns: A mutable reference to self as &mut dyn Any
fn as_any_mut(&mut self) -> &mut dyn Any;
/// Update the component state for one simulation cycle at a specific virtual time.
///
/// This is the virtual-time-aware version of `update()`. Components that have been
/// migrated to the timing model should override this method to use `virtual_time`
/// for timing decisions instead of `Instant::now()`.
///
/// The default implementation simply calls `update()`, providing backward compatibility
/// for components that haven't been migrated yet.
///
/// # Parameters
/// - `virtual_time`: Current simulation time in nanoseconds since simulation start.
/// Use this instead of `Instant::now()` for all timing decisions.
fn update_at_time(&mut self, virtual_time: Duration) {
let _ = virtual_time; // Unused by default — backward compatible
self.update();
}
/// Get the timing constraints for this component.
///
/// Returns a `ComponentTiming` struct describing the real-world timing
/// characteristics of this component (access time, setup time, hold time, etc.).
///
/// The default implementation returns zero-constraint timing, which means
/// the component has no timing requirements. Components that have been
/// migrated to the timing model should override this to return their
/// actual hardware timing profile.
fn timing(&self) -> ComponentTiming {
ComponentTiming::default()
}
/// Inject a `VirtualClock` for virtual-time-based operation.
///
/// Components that support the virtual clock timing model should override this
/// method to store the clock. The default implementation is a no-op, so
/// components that don't support virtual clock silently ignore it and fall
/// back to legacy `thread::sleep()` timing in their `run()` method.
///
/// # Parameters
/// - `clock`: The shared `VirtualClock` to use for timing synchronization.
/// Wrapped in `Option` to allow clearing (pass `None` to disconnect).
///
/// # Usage
/// Called by `ConfigurableSystem::inject_virtual_clock()` during system startup.
/// Not intended to be called directly from external code.
fn set_virtual_clock_optional(&mut self, _clock: Option<Arc<VirtualClock>>) {
// Default: no-op — component doesn't support virtual clock
}
/// Check if this component has a VirtualClock injected.
///
/// Returns `true` if `set_virtual_clock_optional()` was called with `Some(clock)`.
/// Used by `ConfigurableSystem::start()` to avoid re-injecting the clock on restart.
///
/// The default implementation returns `false`. Components that support virtual clock
/// should override this to return `true` when their internal `virtual_clock` field is `Some`.
fn has_virtual_clock(&self) -> bool {
false
}
}
/// Extended trait for components that can be run in their own threads
/// Provides automatic thread spawning functionality for components
@ -187,6 +249,7 @@ impl Component for BaseComponent {
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
impl RunnableComponent for BaseComponent {}

470
src/component_timing.rs Normal file
View file

@ -0,0 +1,470 @@
//! Per-component timing constraints for hardware-accurate simulation.
//!
//! Each `ComponentTiming` instance describes the real timing characteristics of a
//! hardware component. These values are used during simulation to enforce that
//! components behave realistically — for example, ROM won't drive data onto the bus
//! until its access time has elapsed in virtual time.
//!
//! # Hardware Accuracy
//!
//! If the clock is overclocked beyond a component's maximum frequency, or if timing
//! constraints are violated (setup time, hold time, access time), the component will
//! **not** produce correct results — just like real hardware. This is a feature, not
//! a bug: it allows users to observe what happens when they push hardware beyond its
//! limits.
//!
//! # Timing Violations
//!
//! When a timing violation is detected, it is recorded but does not crash the simulation.
//! The component simply produces incorrect or undefined results, as real hardware would.
use std::fmt;
use std::time::Duration;
/// Timing constraints for a hardware component.
///
/// These values are based on datasheet specifications and define the real-world
/// timing behavior that the simulation enforces.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ComponentTiming {
/// Human-readable name for this timing profile (e.g., "Intel 4001 ROM").
pub name: &'static str,
/// Time from valid address on the bus to data being available.
///
/// For ROM/RAM: how long after the address is latched until the data output
/// is guaranteed to be valid.
///
/// Intel 4001 ROM: ~500ns
/// Intel 4002 RAM: ~500ns
/// Intel 4003 SR: ~200ns (faster due to simpler design)
pub access_time: Duration,
/// Time from a clock edge until an output signal changes.
///
/// This models the internal gate delay of the component.
/// After a clock edge is detected, the component's output doesn't change
/// instantly — it takes `propagation_delay` nanoseconds.
///
/// Intel 4001 ROM: ~200ns
/// Intel 4002 RAM: ~200ns
/// Intel 4003 SR: ~100ns
pub propagation_delay: Duration,
/// Minimum time that address/data inputs must be stable before a clock edge.
///
/// If the address changes within `setup_time` before a clock edge,
/// the component may latch an incorrect address — just like real hardware.
///
/// Intel 4001 ROM: ~100ns
/// Intel 4002 RAM: ~100ns
pub setup_time: Duration,
/// Minimum time that address/data inputs must remain stable after a clock edge.
///
/// If the address changes within `hold_time` after a clock edge,
/// the component's latched value may be corrupted.
///
/// Intel 4001 ROM: ~50ns
/// Intel 4002 RAM: ~50ns
pub hold_time: Duration,
/// Maximum clock frequency this component supports (in Hz).
///
/// If the clock exceeds this frequency, the component may not complete
/// its internal operations before the next clock edge arrives.
///
/// Intel 4001 ROM: 750 kHz
/// Intel 4002 RAM: 750 kHz
/// Intel 4003 SR: 1 MHz (can handle faster clocks)
pub max_clock_frequency: f64,
/// How frequently (in virtual time) this component's `update_at_time()`
/// should be called. Smaller values = more frequent updates = more accurate
/// but more CPU-intensive.
///
/// This determines how often the component thread wakes up to check for
/// clock edges and timing events.
///
/// Intel 4001 ROM: 100ns (check every 100ns of virtual time)
/// Intel 4003 SR: 50ns (more frequent due to faster timing)
pub min_update_interval: Duration,
}
impl ComponentTiming {
/// Timing profile for the Intel 4001 ROM chip.
///
/// The 4001 is a 256-byte mask-programmable ROM with 4-bit I/O ports.
/// Access time: 500ns from address valid to data output valid.
pub fn intel_4001() -> Self {
ComponentTiming {
name: "Intel 4001 ROM",
access_time: Duration::from_nanos(500),
propagation_delay: Duration::from_nanos(200),
setup_time: Duration::from_nanos(100),
hold_time: Duration::from_nanos(50),
max_clock_frequency: 750_000.0,
min_update_interval: Duration::from_nanos(100),
}
}
/// Timing profile for the Intel 4002 RAM chip.
///
/// The 4002 is a 320-bit dynamic RAM (80 x 4-bit words) with 4-bit output port.
/// Access time: 500ns from address valid to data output valid.
pub fn intel_4002() -> Self {
ComponentTiming {
name: "Intel 4002 RAM",
access_time: Duration::from_nanos(500),
propagation_delay: Duration::from_nanos(200),
setup_time: Duration::from_nanos(100),
hold_time: Duration::from_nanos(50),
max_clock_frequency: 750_000.0,
min_update_interval: Duration::from_nanos(100),
}
}
/// Timing profile for the Intel 4003 shift register.
///
/// The 4003 is a 10-bit static shift register with serial/parallel I/O.
/// Faster access time (200ns) than ROM/RAM due to simpler design.
pub fn intel_4003() -> Self {
ComponentTiming {
name: "Intel 4003 Shift Register",
access_time: Duration::from_nanos(200),
propagation_delay: Duration::from_nanos(100),
setup_time: Duration::from_nanos(50),
hold_time: Duration::from_nanos(30),
max_clock_frequency: 1_000_000.0, // Can handle 1 MHz
min_update_interval: Duration::from_nanos(50),
}
}
/// Timing profile for the Intel 4004 CPU.
///
/// The 4004 is the central processing unit that generates control signals.
/// Its timing is defined by the clock it receives rather than access times.
pub fn intel_4004() -> Self {
ComponentTiming {
name: "Intel 4004 CPU",
access_time: Duration::ZERO, // CPU doesn't have "access time"
propagation_delay: Duration::from_nanos(150),
setup_time: Duration::from_nanos(100),
hold_time: Duration::from_nanos(50),
max_clock_frequency: 750_000.0, // 740 kHz nominal, 750 kHz max
min_update_interval: Duration::from_nanos(100),
}
}
/// Sentinel value representing "no frequency limit".
///
/// Using a large finite value instead of `f64::INFINITY` avoids NaN
/// propagation in downstream arithmetic (e.g., `1.0 / frequency`).
/// 1 PHz (petahertz) is effectively unlimited for any real-world component.
pub const NO_FREQUENCY_LIMIT: f64 = 1_000_000_000_000_000.0; // 1 PHz
/// Timing profile for a generic clock generator.
///
/// Clocks don't have access times — they just toggle at their configured frequency.
pub fn generic_clock() -> Self {
ComponentTiming {
name: "Generic Clock",
access_time: Duration::ZERO,
propagation_delay: Duration::from_nanos(10),
setup_time: Duration::ZERO,
hold_time: Duration::ZERO,
max_clock_frequency: Self::NO_FREQUENCY_LIMIT,
min_update_interval: Duration::from_nanos(50),
}
}
/// Default timing with zero delays (backward-compatible).
///
/// Components that haven't been migrated to the timing model yet
/// use this profile, which imposes no timing constraints.
pub fn default_no_constraints() -> Self {
ComponentTiming {
name: "Default (no constraints)",
access_time: Duration::ZERO,
propagation_delay: Duration::ZERO,
setup_time: Duration::ZERO,
hold_time: Duration::ZERO,
max_clock_frequency: Self::NO_FREQUENCY_LIMIT,
min_update_interval: Duration::from_nanos(100),
}
}
/// Calculate the minimum clock period this component supports.
///
/// Derived from `max_clock_frequency`: period = 1 / frequency.
/// Returns `Duration::ZERO` for components with no practical frequency limit
/// (i.e., `max_clock_frequency >= NO_FREQUENCY_LIMIT`).
pub fn min_clock_period(&self) -> Duration {
if self.max_clock_frequency <= 0.0 || self.max_clock_frequency >= Self::NO_FREQUENCY_LIMIT {
Duration::ZERO
} else {
Duration::from_secs_f64(1.0 / self.max_clock_frequency)
}
}
/// Check if a given clock frequency exceeds this component's maximum.
///
/// Returns `true` if the frequency is within spec, `false` if overclocked.
/// Non-finite values (infinity, NaN) are always rejected.
pub fn supports_frequency(&self, frequency_hz: f64) -> bool {
frequency_hz.is_finite() && frequency_hz <= self.max_clock_frequency
}
/// Check if enough virtual time has elapsed since an event.
///
/// Returns `Ok(())` if `elapsed >= access_time`, `Err(TimingViolation)` otherwise.
pub fn check_access_time(&self, elapsed: Duration) -> Result<(), TimingViolation> {
if elapsed >= self.access_time {
Ok(())
} else {
Err(TimingViolation::AccessTimeNotElapsed {
component: self.name,
required: self.access_time,
actual: elapsed,
})
}
}
/// Check if a signal was stable for the required setup time before a clock edge.
///
/// Returns `Ok(())` if `stability_duration >= setup_time`, `Err(TimingViolation)` otherwise.
pub fn check_setup_time(&self, stability_duration: Duration) -> Result<(), TimingViolation> {
if stability_duration >= self.setup_time {
Ok(())
} else {
Err(TimingViolation::SetupTimeViolation {
component: self.name,
required: self.setup_time,
actual: stability_duration,
})
}
}
/// Check if a signal remained stable for the required hold time after a clock edge.
///
/// Returns `Ok(())` if `stability_duration >= hold_time`, `Err(TimingViolation)` otherwise.
pub fn check_hold_time(&self, stability_duration: Duration) -> Result<(), TimingViolation> {
if stability_duration >= self.hold_time {
Ok(())
} else {
Err(TimingViolation::HoldTimeViolation {
component: self.name,
required: self.hold_time,
actual: stability_duration,
})
}
}
}
impl Default for ComponentTiming {
fn default() -> Self {
Self::default_no_constraints()
}
}
/// A timing violation detected during simulation.
///
/// Timing violations are expected when overclocking or when signals change
/// too quickly. They don't crash the simulation — instead, the component
/// produces incorrect results, just like real hardware.
#[derive(Debug, Clone)]
pub enum TimingViolation {
/// The component's access time has not yet elapsed since the address was latched.
///
/// The data output is not yet valid. Reading the bus now returns undefined data.
AccessTimeNotElapsed {
component: &'static str,
required: Duration,
actual: Duration,
},
/// An input signal did not remain stable for the required setup time
/// before a clock edge.
///
/// The component may have latched an incorrect value.
SetupTimeViolation {
component: &'static str,
required: Duration,
actual: Duration,
},
/// An input signal did not remain stable for the required hold time
/// after a clock edge.
///
/// The component's latched value may be corrupted.
HoldTimeViolation {
component: &'static str,
required: Duration,
actual: Duration,
},
/// The clock frequency exceeds the component's maximum rated frequency.
///
/// The component may not complete its internal operations before the
/// next clock edge arrives.
Overclocked {
component: &'static str,
max_frequency_hz: f64,
actual_frequency_hz: f64,
},
}
impl std::error::Error for TimingViolation {}
impl fmt::Display for TimingViolation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TimingViolation::AccessTimeNotElapsed {
component,
required,
actual,
} => write!(
f,
"{}: Access time not elapsed (need {:?}, got {:?})",
component, required, actual
),
TimingViolation::SetupTimeViolation {
component,
required,
actual,
} => write!(
f,
"{}: Setup time violation (need {:?}, got {:?})",
component, required, actual
),
TimingViolation::HoldTimeViolation {
component,
required,
actual,
} => write!(
f,
"{}: Hold time violation (need {:?}, got {:?})",
component, required, actual
),
TimingViolation::Overclocked {
component,
max_frequency_hz,
actual_frequency_hz,
} => write!(
f,
"{}: Overclocked (max {:.0} Hz, actual {:.0} Hz)",
component, max_frequency_hz, actual_frequency_hz
),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_intel_4001_timing() {
let timing = ComponentTiming::intel_4001();
assert_eq!(timing.access_time, Duration::from_nanos(500));
assert_eq!(timing.propagation_delay, Duration::from_nanos(200));
assert_eq!(timing.setup_time, Duration::from_nanos(100));
assert_eq!(timing.hold_time, Duration::from_nanos(50));
assert_eq!(timing.max_clock_frequency, 750_000.0);
}
#[test]
fn test_intel_4003_timing_faster() {
let rom = ComponentTiming::intel_4001();
let sr = ComponentTiming::intel_4003();
// Shift register should be faster than ROM
assert!(sr.access_time < rom.access_time);
assert!(sr.max_clock_frequency > rom.max_clock_frequency);
}
#[test]
fn test_supports_frequency() {
let timing = ComponentTiming::intel_4001();
assert!(timing.supports_frequency(500_000.0)); // 500 kHz — within spec
assert!(timing.supports_frequency(750_000.0)); // 750 kHz — max rated
assert!(!timing.supports_frequency(1_000_000.0)); // 1 MHz — overclocked!
}
#[test]
fn test_check_access_time() {
let timing = ComponentTiming::intel_4001();
// Not enough time elapsed
let result = timing.check_access_time(Duration::from_nanos(300));
assert!(result.is_err());
// Exactly enough time
let result = timing.check_access_time(Duration::from_nanos(500));
assert!(result.is_ok());
// More than enough time
let result = timing.check_access_time(Duration::from_nanos(800));
assert!(result.is_ok());
}
#[test]
fn test_check_setup_time() {
let timing = ComponentTiming::intel_4001();
assert!(timing.check_setup_time(Duration::from_nanos(50)).is_err());
assert!(timing.check_setup_time(Duration::from_nanos(100)).is_ok());
assert!(timing.check_setup_time(Duration::from_nanos(200)).is_ok());
}
#[test]
fn test_overclocking_scenario() {
let rom = ComponentTiming::intel_4001();
let sr = ComponentTiming::intel_4003();
// At 2 MHz clock (500ns cycle, 250ns per phase):
let clock_freq = 2_000_000.0;
let phase_time = Duration::from_nanos(250);
// ROM can't handle 2 MHz
assert!(!rom.supports_frequency(clock_freq));
// ROM access time is 500ns but only gets 250ns per phase
assert!(rom.check_access_time(phase_time).is_err());
// Shift register's access time (200ns) fits within the 250ns phase at 2 MHz,
// but it can't actually handle 2 MHz clock frequency (max 1 MHz).
// We only verify access time here, not frequency support.
assert!(sr.check_access_time(phase_time).is_ok());
}
#[test]
fn test_min_clock_period() {
let timing = ComponentTiming::intel_4001();
let period = timing.min_clock_period();
assert_eq!(period, Duration::from_secs_f64(1.0 / 750_000.0));
}
#[test]
fn test_default_no_constraints() {
let timing = ComponentTiming::default_no_constraints();
assert_eq!(timing.access_time, Duration::ZERO);
// Supports any reasonable finite frequency
assert!(timing.supports_frequency(1_000_000_000.0)); // 1 GHz — well within limit
assert!(timing.supports_frequency(999_999_999_999_999.0)); // Just under sentinel
assert!(!timing.supports_frequency(f64::INFINITY)); // Non-finite rejected
assert!(!timing.supports_frequency(f64::NAN)); // NaN rejected
}
#[test]
fn test_timing_violation_display() {
let violation = TimingViolation::AccessTimeNotElapsed {
component: "Intel 4001 ROM",
required: Duration::from_nanos(500),
actual: Duration::from_nanos(250),
};
let display = format!("{}", violation);
assert!(display.contains("Access time not elapsed"));
assert!(display.contains("Intel 4001 ROM"));
}
}

View file

@ -3,22 +3,25 @@ use std::any::Any;
use std::collections::HashMap;
use std::sync::Arc;
use std::thread;
use std::time::{Duration, Instant};
use std::time::Duration;
use crate::component::{BaseComponent, Component};
use crate::component_timing::ComponentTiming;
use crate::pin::{Pin, PinValue};
use crate::virtual_clock::VirtualClock;
pub struct GenericClock {
base: BaseComponent,
frequency: f64, // Frequency in Hz
duty_cycle: f64, // Duty cycle as percentage (0.0 to 1.0)
current_state: PinValue,
last_transition: Instant,
last_transition_vtime: Duration, // Virtual time of last phase transition
high_time: Duration,
low_time: Duration,
enabled: bool,
paused: bool, // Pause state for system control
pause_start: Option<Instant>, // When pause started, to track pause duration
paused: bool, // Pause state for system control
pause_start_vtime: Option<Duration>, // Virtual time when pause started
virtual_clock: Option<Arc<VirtualClock>>, // Injected by ConfigurableSystem (Phase 7)
}
impl GenericClock {
@ -31,18 +34,26 @@ impl GenericClock {
frequency,
duty_cycle: 0.5, // 50% duty cycle by default
current_state: PinValue::Low,
last_transition: Instant::now(),
last_transition_vtime: Duration::ZERO,
high_time: Duration::from_secs_f64(0.5 / frequency), // Will be recalculated in set_duty_cycle
low_time: Duration::from_secs_f64(0.5 / frequency), // Will be recalculated in set_duty_cycle
enabled: true,
paused: false,
pause_start: None,
pause_start_vtime: None,
virtual_clock: None,
};
clock.set_duty_cycle(0.5); // Initialize timing
clock
}
/// Inject a VirtualClock for virtual-time-based operation.
/// If not set, the clock falls back to wall-clock Instant-based timing
/// via `run_legacy()`. This method is called by ConfigurableSystem in Phase 7.
pub fn set_virtual_clock(&mut self, clock: Arc<VirtualClock>) {
self.virtual_clock = Some(clock);
}
/// Set the clock frequency dynamically
pub fn set_frequency(&mut self, frequency: f64) {
if frequency > 0.0 {
@ -65,7 +76,7 @@ impl GenericClock {
self.enabled = true;
// Start with known state when enabled
self.current_state = PinValue::Low;
self.last_transition = Instant::now();
self.last_transition_vtime = self.current_virtual_time();
}
pub fn disable(&mut self) {
@ -77,20 +88,17 @@ impl GenericClock {
/// Pause the clock - freeze output state
pub fn pause(&mut self) {
self.paused = true;
self.pause_start = Some(Instant::now());
self.pause_start_vtime = Some(self.current_virtual_time());
}
/// Resume the clock from paused state
/// Maintains phase consistency by adjusting last_transition to account for pause duration
pub fn resume(&mut self) {
if let Some(pause_start) = self.pause_start {
// Calculate how long we were paused
let pause_duration = pause_start.elapsed();
// Adjust last_transition forward by the pause duration to maintain phase
self.last_transition += pause_duration;
}
// No duration adjustment needed — when the system is paused, VirtualClock.pause()
// freezes virtual time, so current_virtual_time() returns the same value it had
// when pause() was called. This means pause_duration would always be zero.
// last_transition_vtime is still correct because virtual time didn't advance.
self.paused = false;
self.pause_start = None;
self.pause_start_vtime = None;
}
/// Check if the clock is paused
@ -127,8 +135,22 @@ impl GenericClock {
true // Enabled by default if pin doesn't exist or can't be read
}
fn should_transition(&self) -> bool {
let elapsed = self.last_transition.elapsed();
/// Get current virtual time, or Duration::ZERO if no VirtualClock is injected.
///
/// # Zero-Time Invariant
///
/// Returns `Duration::ZERO` when no VirtualClock is present. In legacy mode,
/// `enable()` sets `last_transition_vtime = ZERO`, and `should_transition()`
/// computes `elapsed = ZERO - ZERO = ZERO`, which is always less than
/// `high_time`/`low_time`, so no spurious transition occurs. This is correct
/// because legacy mode uses `update()` → `update_at_time(ZERO)`.
fn current_virtual_time(&self) -> Duration {
self.virtual_clock.as_ref().map_or(Duration::ZERO, |vc| vc.now())
}
/// Check if enough virtual time has elapsed for a phase transition.
fn should_transition(&self, virtual_time: Duration) -> bool {
let elapsed = virtual_time.saturating_sub(self.last_transition_vtime);
match self.current_state {
PinValue::High => elapsed >= self.high_time,
@ -137,7 +159,8 @@ impl GenericClock {
}
}
fn perform_transition(&mut self) {
/// Perform a phase transition at the given virtual time.
fn perform_transition(&mut self, virtual_time: Duration) {
let new_state = match self.current_state {
PinValue::High => PinValue::Low,
PinValue::Low => PinValue::High,
@ -146,7 +169,38 @@ impl GenericClock {
self.current_state = new_state;
self.set_clock_output(new_state);
self.last_transition = Instant::now();
self.last_transition_vtime = virtual_time;
}
/// Calculate the virtual time of the next phase transition.
fn next_transition_time(&self) -> Duration {
let phase_duration = match self.current_state {
PinValue::High => self.high_time,
PinValue::Low => self.low_time,
PinValue::HighZ => Duration::from_nanos(1),
};
self.last_transition_vtime.saturating_add(phase_duration)
}
/// Legacy run loop using wall-clock time (backward compatibility).
/// Used when no VirtualClock has been injected.
fn run_legacy(&mut self) {
while self.is_running() {
self.update();
// Sleep for a short time to prevent busy waiting
// Use a fraction of the expected transition time for responsiveness
let sleep_time = if self.frequency > 0.0 {
Duration::from_secs_f64(1.0 / self.frequency / 100.0).max(Duration::from_micros(1))
} else {
Duration::from_micros(100)
};
thread::sleep(sleep_time);
}
// Clean up when stopping
self.disable();
}
}
@ -163,7 +217,7 @@ impl Component for GenericClock {
self.base.get_pin(name)
}
fn update(&mut self) {
fn update_at_time(&mut self, virtual_time: Duration) {
// Check pause state first - don't update if paused
if self.paused {
return;
@ -181,28 +235,50 @@ impl Component for GenericClock {
return;
}
// Check if it's time to transition
if self.should_transition() {
self.perform_transition();
// Check if it's time to transition (using virtual time)
if self.should_transition(virtual_time) {
self.perform_transition(virtual_time);
}
}
/// Legacy update entry point — delegates to `update_at_time()` with current virtual time.
///
/// **Note:** In legacy mode (no `VirtualClock` injected), `current_virtual_time()` returns
/// `Duration::ZERO`. Since `last_transition_vtime` also starts at `ZERO`, the computed
/// elapsed time is always zero, so `should_transition()` never returns true. This means
/// calling `update()` alone will NOT cause clock transitions in legacy mode — use `run()`
/// (which calls `run_legacy()`) for standalone clock operation.
fn update(&mut self) {
let vtime = self.current_virtual_time();
self.update_at_time(vtime);
}
fn run(&mut self) {
self.base.set_running(true);
self.enable(); // Ensure clock is enabled when running
// If no VirtualClock is injected, fall back to wall-clock run loop
let vclock = match self.virtual_clock.as_ref() {
Some(vc) => Arc::clone(vc),
None => {
return self.run_legacy();
}
};
while self.is_running() {
self.update();
// Calculate next transition time
let next_transition = self.next_transition_time();
// Sleep for a short time to prevent busy waiting
// Use a fraction of the expected transition time for responsiveness
let sleep_time = if self.frequency > 0.0 {
Duration::from_secs_f64(1.0 / self.frequency / 100.0).max(Duration::from_micros(1))
} else {
Duration::from_micros(100)
};
thread::sleep(sleep_time);
// Block until virtual time reaches next transition
match vclock.wait_until(next_transition) {
Some(current_vtime) => {
if !self.is_running() {
break;
}
self.update_at_time(current_vtime);
}
None => break, // Simulation stopped
}
}
// Clean up when stopping
@ -222,6 +298,18 @@ impl Component for GenericClock {
self.base.set_running(running);
}
fn timing(&self) -> ComponentTiming {
ComponentTiming {
name: "Generic Clock",
access_time: Duration::ZERO,
propagation_delay: Duration::ZERO,
setup_time: Duration::ZERO,
hold_time: Duration::ZERO,
max_clock_frequency: ComponentTiming::NO_FREQUENCY_LIMIT,
min_update_interval: self.high_time.min(self.low_time),
}
}
fn as_any(&self) -> &dyn Any {
self
}
@ -229,6 +317,14 @@ impl Component for GenericClock {
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn set_virtual_clock_optional(&mut self, clock: Option<Arc<VirtualClock>>) {
self.virtual_clock = clock;
}
fn has_virtual_clock(&self) -> bool {
self.virtual_clock.is_some()
}
}
// Advanced clock features

View file

@ -3,21 +3,24 @@ use std::any::Any;
use std::collections::HashMap;
use std::sync::Arc;
use std::thread;
use std::time::{Duration, Instant};
use std::time::Duration;
use crate::component::{BaseComponent, Component};
use crate::component_timing::ComponentTiming;
use crate::pin::{Pin, PinValue};
use crate::virtual_clock::VirtualClock;
pub struct TwoPhaseClock {
base: BaseComponent,
phi1_state: PinValue,
phi2_state: PinValue,
last_transition: Instant,
last_transition_vtime: Duration, // Virtual time of last phase swap
phase_time: Duration,
enabled: bool,
paused: bool, // Pause state for system control
pause_start: Option<Instant>, // When pause started, to track pause duration
frequency: f64, // Store frequency for dynamic adjustment
paused: bool, // Pause state for system control
pause_start_vtime: Option<Duration>, // Virtual time when pause started
frequency: f64, // Store frequency for dynamic adjustment
virtual_clock: Option<Arc<VirtualClock>>, // Injected by ConfigurableSystem (Phase 7)
}
impl TwoPhaseClock {
@ -35,15 +38,23 @@ impl TwoPhaseClock {
base: BaseComponent::new(name, pins),
phi1_state: PinValue::High, // Start with PHI1 high
phi2_state: PinValue::Low,
last_transition: Instant::now(),
last_transition_vtime: Duration::ZERO,
phase_time,
enabled: true,
paused: false,
pause_start: None,
pause_start_vtime: None,
frequency,
virtual_clock: None,
}
}
/// Inject a VirtualClock for virtual-time-based operation.
/// If not set, the clock falls back to wall-clock timing via `run_legacy()`.
/// This method is called by ConfigurableSystem in Phase 7.
pub fn set_virtual_clock(&mut self, clock: Arc<VirtualClock>) {
self.virtual_clock = Some(clock);
}
/// Set the clock frequency dynamically
pub fn set_frequency(&mut self, frequency: f64) {
if frequency > 0.0 {
@ -61,7 +72,7 @@ impl TwoPhaseClock {
self.enabled = true;
self.phi1_state = PinValue::High;
self.phi2_state = PinValue::Low;
self.last_transition = Instant::now();
self.last_transition_vtime = self.current_virtual_time();
self.update_outputs();
// Force an immediate update to ensure outputs are driven
@ -78,20 +89,17 @@ impl TwoPhaseClock {
/// Pause the clock - freeze output state
pub fn pause(&mut self) {
self.paused = true;
self.pause_start = Some(Instant::now());
self.pause_start_vtime = Some(self.current_virtual_time());
}
/// Resume the clock from paused state
/// Maintains phase consistency by adjusting last_transition to account for pause duration
pub fn resume(&mut self) {
if let Some(pause_start) = self.pause_start {
// Calculate how long we were paused
let pause_duration = pause_start.elapsed();
// Adjust last_transition forward by the pause duration to maintain phase
self.last_transition += pause_duration;
}
// No duration adjustment needed — when the system is paused, VirtualClock.pause()
// freezes virtual time, so current_virtual_time() returns the same value it had
// when pause() was called. This means pause_duration would always be zero.
// last_transition_vtime is still correct because virtual time didn't advance.
self.paused = false;
self.pause_start = None;
self.pause_start_vtime = None;
}
/// Check if the clock is paused
@ -99,6 +107,18 @@ impl TwoPhaseClock {
self.paused
}
/// Get current virtual time, or Duration::ZERO if no VirtualClock is injected.
///
/// # Zero-Time Invariant
///
/// Returns `Duration::ZERO` when no VirtualClock is present. In legacy mode,
/// `enable()` sets `last_transition_vtime = ZERO`, and `should_transition()`
/// computes `elapsed = ZERO - ZERO = ZERO`, which is always less than
/// `phase_time`, so no spurious transition occurs.
fn current_virtual_time(&self) -> Duration {
self.virtual_clock.as_ref().map_or(Duration::ZERO, |vc| vc.now())
}
fn update_outputs(&self) {
// Set CLK output (for compatibility)
if let Ok(clk_pin) = self.base.get_pin("CLK") {
@ -133,11 +153,14 @@ impl TwoPhaseClock {
}
}
fn should_transition(&self) -> bool {
self.last_transition.elapsed() >= self.phase_time
/// Check if enough virtual time has elapsed for a phase transition.
fn should_transition(&self, virtual_time: Duration) -> bool {
let elapsed = virtual_time.saturating_sub(self.last_transition_vtime);
elapsed >= self.phase_time
}
fn perform_transition(&mut self) {
/// Perform a phase swap at the given virtual time.
fn perform_transition(&mut self, virtual_time: Duration) {
match (self.phi1_state, self.phi2_state) {
(PinValue::High, PinValue::Low) => {
// PHI1 -> Low, PHI2 -> High
@ -157,7 +180,25 @@ impl TwoPhaseClock {
}
self.update_outputs();
self.last_transition = Instant::now();
self.last_transition_vtime = virtual_time;
}
/// Legacy run loop using wall-clock time (backward compatibility).
/// Used when no VirtualClock has been injected.
fn run_legacy(&mut self) {
while self.is_running() {
self.update();
// Sleep for a very short time to allow frequent updates
thread::sleep(Duration::from_micros(100)); // 100µs = 10kHz update rate
// Check if should stop
if !self.is_running() {
break;
}
}
self.disable();
}
}
@ -174,7 +215,7 @@ impl Component for TwoPhaseClock {
self.base.get_pin(name)
}
fn update(&mut self) {
fn update_at_time(&mut self, virtual_time: Duration) {
// Check pause state first - don't update if paused
if self.paused {
return;
@ -187,24 +228,41 @@ impl Component for TwoPhaseClock {
// Always update outputs to ensure they're driven
self.update_outputs();
if self.should_transition() {
self.perform_transition();
if self.should_transition(virtual_time) {
self.perform_transition(virtual_time);
}
}
fn update(&mut self) {
let vtime = self.current_virtual_time();
self.update_at_time(vtime);
}
fn run(&mut self) {
self.base.set_running(true);
self.enable();
// If no VirtualClock is injected, fall back to wall-clock run loop
let vclock = match self.virtual_clock.as_ref() {
Some(vc) => Arc::clone(vc),
None => {
return self.run_legacy();
}
};
while self.is_running() {
self.update();
// Calculate next transition time
let next_transition = self.last_transition_vtime + self.phase_time;
// Sleep for a very short time to allow frequent updates
thread::sleep(Duration::from_micros(100)); // 100µs = 10kHz update rate
// Check if should stop
if !self.is_running() {
break;
// Block until virtual time reaches next transition
match vclock.wait_until(next_transition) {
Some(current_vtime) => {
if !self.is_running() {
break;
}
self.update_at_time(current_vtime);
}
None => break, // Simulation stopped
}
}
@ -224,6 +282,18 @@ impl Component for TwoPhaseClock {
self.base.set_running(running);
}
fn timing(&self) -> ComponentTiming {
ComponentTiming {
name: "Two-Phase Clock",
access_time: Duration::ZERO,
propagation_delay: Duration::ZERO,
setup_time: Duration::ZERO,
hold_time: Duration::ZERO,
max_clock_frequency: ComponentTiming::NO_FREQUENCY_LIMIT,
min_update_interval: self.phase_time,
}
}
fn as_any(&self) -> &dyn Any {
self
}
@ -231,4 +301,12 @@ impl Component for TwoPhaseClock {
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn set_virtual_clock_optional(&mut self, clock: Option<Arc<VirtualClock>>) {
self.virtual_clock = clock;
}
fn has_virtual_clock(&self) -> bool {
self.virtual_clock.is_some()
}
}

View file

@ -1,6 +1,6 @@
use parking_lot::Mutex;
use std::sync::Arc;
use std::time::{Duration, Instant};
use std::time::Duration;
use crate::component::{BaseComponent, Component};
use crate::pin::{Pin, PinValue};
@ -203,14 +203,22 @@ pub trait Intel400xAddressHandling {
/// After the full address is latched (both nibbles received), the nibbles
/// are cleared to prepare for the next address cycle. The full address
/// should be assembled and stored before calling this function returns.
///
/// # Address Latch Time
///
/// When the address becomes complete (both nibbles latched), this method
/// records `virtual_time` into `address_latch_time`. The caller must provide
/// the current virtual time. See `handle_address_nibble_at()` for a
/// virtual-time-aware alternative that combines nibble handling and latching.
fn handle_address_latching(
&self,
nibble: u8,
address_high_nibble: &mut Option<u8>,
address_low_nibble: &mut Option<u8>,
full_address_ready: &mut bool,
address_latch_time: &mut Option<Instant>,
address_latch_time: &mut Option<Duration>,
_access_time: Duration,
virtual_time: Duration,
) {
if address_high_nibble.is_none() {
// First cycle: latch high nibble (bits 7-4)
@ -218,12 +226,13 @@ pub trait Intel400xAddressHandling {
} else if address_low_nibble.is_none() {
// Second cycle: latch low nibble (bits 3-0)
*address_low_nibble = Some(nibble);
// Mark address as ready and record latch time
// Mark address as ready
*full_address_ready = true;
*address_latch_time = Some(Instant::now());
// Clear nibbles after full address is latched to prepare for next cycle
// The full address should be assembled from the nibbles before this returns
// (e.g., by the caller reading the nibble values before they're cleared)
// Record the virtual time when the address was fully latched.
*address_latch_time = Some(virtual_time);
// Clear nibbles after full address is latched to prepare for next cycle.
// The full address should be assembled from the nibbles before they're cleared
// (e.g., by the caller reading the nibble values before they're cleared).
*address_high_nibble = None;
*address_low_nibble = None;
}
@ -233,11 +242,12 @@ pub trait Intel400xAddressHandling {
/// Returns: true if latency has elapsed
fn handle_latency_wait(
&self,
address_latch_time: &Option<Instant>,
address_latch_time: &Option<Duration>,
access_time: Duration,
virtual_time: Duration,
) -> bool {
if let Some(latch_time) = address_latch_time {
latch_time.elapsed() >= access_time
virtual_time.saturating_sub(*latch_time) >= access_time
} else {
false
}
@ -321,8 +331,8 @@ pub trait Intel400xResetHandling {
pub trait Intel400xTimingState {
fn get_timing_state(&self) -> MemoryState;
fn set_timing_state(&mut self, state: MemoryState);
fn get_address_latch_time(&self) -> Option<Instant>;
fn set_address_latch_time(&mut self, time: Option<Instant>);
fn get_address_latch_time(&self) -> Option<Duration>;
fn set_address_latch_time(&mut self, time: Option<Duration>);
fn get_full_address_ready(&self) -> bool;
fn set_full_address_ready(&mut self, ready: bool);
fn get_address_high_nibble(&self) -> Option<u8>;
@ -330,6 +340,10 @@ pub trait Intel400xTimingState {
fn get_address_low_nibble(&self) -> Option<u8>;
fn set_address_low_nibble(&mut self, nibble: Option<u8>);
fn get_access_time(&self) -> Duration;
/// Check if latency has elapsed at a given virtual time.
/// Returns true if `virtual_time - address_latch_time >= access_time`.
fn is_latency_elapsed_at(&self, virtual_time: Duration) -> bool;
}
/// Component type enumeration for Intel 400x series
@ -820,8 +834,8 @@ pub struct Intel400xStateMachine {
/// The low nibble (bits 3-0) of the currently latching address
address_low_nibble: Option<u8>,
/// The time when the address was completely latched
address_latch_time: Option<Instant>,
/// The time when the address was completely latched (virtual time in nanoseconds)
address_latch_time: Option<Duration>,
/// The access time to wait after address latching before data access
access_time: Duration,
@ -912,13 +926,13 @@ impl Intel400xStateMachine {
self.address_low_nibble = nibble;
}
/// Get address latch time
pub fn get_address_latch_time(&self) -> Option<Instant> {
/// Get address latch time (virtual time)
pub fn get_address_latch_time(&self) -> Option<Duration> {
self.address_latch_time
}
/// Set address latch time
pub fn set_address_latch_time(&mut self, time: Option<Instant>) {
/// Set address latch time (virtual time)
pub fn set_address_latch_time(&mut self, time: Option<Duration>) {
self.address_latch_time = time;
}
@ -945,17 +959,31 @@ impl Intel400xStateMachine {
self.address_latch_time = None;
}
/// Handle address nibble latching
/// Returns true only when BOTH nibbles have been latched (address complete)
/// Handle address nibble latching (legacy — uses Duration::ZERO as latch time).
/// Returns true only when BOTH nibbles have been latched (address complete).
///
/// **Deprecated:** Use [`handle_address_nibble_at(virtual_time)`](Self::handle_address_nibble_at)
/// instead, which records the actual virtual time when the address is complete.
/// This method sets `address_latch_time` to `Duration::ZERO` as a fallback, so
/// `is_latency_elapsed_at()` will return `true` once any virtual time >= access_time
/// has passed — which is sufficient for most use cases but less precise than
/// `handle_address_nibble_at()`.
#[deprecated(
since = "0.1.0",
note = "Use handle_address_nibble_at(nibble, virtual_time) instead."
)]
pub fn handle_address_nibble(&mut self, nibble: u8) -> bool {
if self.address_high_nibble.is_none() {
// First cycle: latch high nibble (bits 7-4)
self.address_high_nibble = Some(nibble);
false // Address not complete yet - still need low nibble
} else if self.address_low_nibble.is_none() {
// Second cycle: latch low nibble (bits 3-0) and transition to latency wait
// Second cycle: latch low nibble (bits 3-0)
self.address_low_nibble = Some(nibble);
self.address_latch_time = Some(Instant::now());
// Set fallback latch time so is_latency_elapsed_at() doesn't hang forever.
// Duration::ZERO means "latency started at the beginning of time", so any
// positive virtual time >= access_time will satisfy the latency check.
self.address_latch_time = Some(Duration::ZERO);
true // Address complete - both nibbles latched
} else {
// Address already latched, ignore
@ -963,10 +991,52 @@ impl Intel400xStateMachine {
}
}
/// Check if latency has elapsed
/// Handle address nibble latching with virtual time.
/// Returns true only when BOTH nibbles have been latched (address complete).
/// Records address_latch_time as virtual_time when complete.
pub fn handle_address_nibble_at(&mut self, nibble: u8, virtual_time: Duration) -> bool {
if self.address_high_nibble.is_none() {
// First cycle: latch high nibble (bits 7-4)
self.address_high_nibble = Some(nibble);
false // Address not complete yet - still need low nibble
} else if self.address_low_nibble.is_none() {
// Second cycle: latch low nibble (bits 3-0) and record virtual time
self.address_low_nibble = Some(nibble);
self.address_latch_time = Some(virtual_time);
true // Address complete - both nibbles latched
} else {
// Address already latched, ignore
false
}
}
/// Check if latency has elapsed (legacy — returns false without virtual time).
///
/// This method existed in the `Instant`-based timing model and cannot be emulated
/// with virtual time because the caller must provide the current virtual time.
///
/// Use [`is_latency_elapsed_at(virtual_time)`](Self::is_latency_elapsed_at) instead.
///
/// # Return Value
/// Always returns `false` since virtual time is not available. This is a safe
/// fallback that prevents incorrect behavior — without virtual time, we cannot
/// confirm latency has elapsed, so we conservatively return false.
#[deprecated(
since = "0.1.0",
note = "Use is_latency_elapsed_at(virtual_time) instead. \
This method always returns false without virtual time."
)]
pub fn is_latency_elapsed(&self) -> bool {
// Cannot determine latency without virtual time — conservatively return false.
// The #[deprecated] attribute produces compile-time warnings; no runtime spam needed.
false
}
/// Check if latency has elapsed at a given virtual time.
/// Returns true if `virtual_time - address_latch_time >= access_time`.
pub fn is_latency_elapsed_at(&self, virtual_time: Duration) -> bool {
if let Some(latch_time) = self.address_latch_time {
latch_time.elapsed() >= self.access_time
virtual_time.saturating_sub(latch_time) >= self.access_time
} else {
false
}
@ -1424,27 +1494,50 @@ mod tests {
// First nibble should latch high nibble but NOT complete the address
// Returns false because we still need the low nibble
assert!(!sm.handle_address_nibble(0xA));
assert!(!sm.handle_address_nibble_at(0xA, Duration::from_nanos(100)));
assert_eq!(sm.get_address_high_nibble(), Some(0xA));
assert_eq!(sm.get_address_low_nibble(), None);
assert!(!sm.is_full_address_ready());
// Second nibble should latch low nibble and complete address
// Returns true because address is now complete
assert!(sm.handle_address_nibble(0x5));
// address_latch_time is recorded at virtual_time=200ns
assert!(sm.handle_address_nibble_at(0x5, Duration::from_nanos(200)));
assert_eq!(sm.get_address_high_nibble(), Some(0xA));
assert_eq!(sm.get_address_low_nibble(), Some(0x5));
assert!(sm.is_full_address_ready());
assert_eq!(sm.get_full_address(), Some(0xA5));
assert!(sm.get_address_latch_time().is_some());
// address_latch_time is now set by handle_address_nibble_at()
assert_eq!(sm.get_address_latch_time(), Some(Duration::from_nanos(200)));
// Address already latched should not change anything
assert!(!sm.handle_address_nibble(0x3));
assert!(!sm.handle_address_nibble_at(0x3, Duration::from_nanos(300)));
assert_eq!(sm.get_address_high_nibble(), Some(0xA));
assert_eq!(sm.get_address_low_nibble(), Some(0x5));
assert!(sm.is_full_address_ready());
assert_eq!(sm.get_full_address(), Some(0xA5));
assert!(sm.get_address_latch_time().is_some());
}
#[test]
fn test_address_latching_with_virtual_time() {
let mut sm = Intel400xStateMachine::new(
Intel400xComponentType::Intel4001,
Duration::from_nanos(500),
);
// First nibble with virtual time
assert!(!sm.handle_address_nibble_at(0xA, Duration::from_nanos(100)));
assert_eq!(sm.get_address_high_nibble(), Some(0xA));
// Second nibble with virtual time — should set address_latch_time
assert!(sm.handle_address_nibble_at(0x5, Duration::from_nanos(200)));
assert_eq!(sm.get_full_address(), Some(0xA5));
assert_eq!(sm.get_address_latch_time(), Some(Duration::from_nanos(200)));
// Latency should NOT be elapsed at 200ns (need 500ns)
assert!(!sm.is_latency_elapsed_at(Duration::from_nanos(400)));
// Latency SHOULD be elapsed at 700ns (700 - 200 = 500ns >= 500ns)
assert!(sm.is_latency_elapsed_at(Duration::from_nanos(700)));
}
#[test]
@ -1537,18 +1630,18 @@ mod tests {
);
let mut behavior = MockComponentBehavior::new(Intel400xComponentType::Intel4001);
// Initially no latency should be elapsed
assert!(!sm.is_latency_elapsed());
// Initially no latency should be elapsed (no address latched)
assert!(!sm.is_latency_elapsed_at(Duration::ZERO));
// Latch an address
sm.handle_address_nibble(0xA);
sm.handle_address_nibble(0x5);
// Initially latency should not be elapsed (need to wait for access time)
assert!(!sm.is_latency_elapsed());
// Latch an address using virtual time
sm.handle_address_nibble_at(0xA, Duration::from_nanos(100));
sm.handle_address_nibble_at(0x5, Duration::from_nanos(200));
// Wait for latency to elapse
std::thread::sleep(Duration::from_millis(2));
assert!(sm.is_latency_elapsed());
// Initially latency should not be elapsed (need to wait for access time = 1ms)
assert!(!sm.is_latency_elapsed_at(Duration::from_micros(500)));
// At 1.2ms virtual time, latency should be elapsed (1200 - 200 = 1000us >= 1ms)
assert!(sm.is_latency_elapsed_at(Duration::from_micros(1200)));
// First need to transition to AddressPhase state
process_event_direct(&mut sm, Intel400xEvent::ChipSelect);
@ -1558,10 +1651,6 @@ mod tests {
process_event_direct(&mut sm, Intel400xEvent::AddressComplete);
assert_eq!(sm.get_state(), Intel400xUnifiedState::WaitLatency);
// Wait for latency to elapse
std::thread::sleep(Duration::from_millis(2));
assert!(sm.is_latency_elapsed());
// Ensure state machine is in WaitLatency state before processing LatencyElapsed
assert_eq!(sm.get_state(), Intel400xUnifiedState::WaitLatency);

View file

@ -3,11 +3,13 @@ use std::any::Any;
use std::collections::HashMap;
use std::sync::Arc;
use std::thread;
use std::time::{Duration, Instant};
use std::time::Duration;
use crate::component::{BaseComponent, Component, RunnableComponent};
use crate::component_timing::ComponentTiming;
use crate::pin::{Pin, PinValue};
use crate::types::U12;
use crate::virtual_clock::VirtualClock;
/// Represents the current phase of instruction execution
/// The 4004 CPU processes instructions in distinct phases synchronized with the clock
@ -165,13 +167,16 @@ pub struct Intel4004 {
opr_nibble: Option<u8>, // High nibble (OPR) read during M1 cycle, combined with M2 for 8-bit instruction
// Timing and synchronization
address_latch_time: Option<Instant>, // Timestamp when address was latched
address_latch_time: Option<Duration>, // Virtual time when address was latched
#[allow(dead_code)] // Reserved for future memory timing implementation
access_time: Duration, // Memory access time (typical 500ns)
// Debug and safety controls
debug_enabled: bool, // Control debug output
max_cycles: Option<u64>, // Maximum cycles before auto-stop (None = unlimited)
// Virtual time support (Phase 6)
virtual_clock: Option<Arc<VirtualClock>>, // Injected by ConfigurableSystem (Phase 7)
}
/// Check if an opcode represents a two-byte instruction
@ -248,9 +253,24 @@ impl Intel4004 {
// Debug and safety controls
debug_enabled: true, // Enable debug by default
max_cycles: None, // No cycle limit by default
// Virtual time support
virtual_clock: None,
}
}
/// Inject a VirtualClock for virtual time-based operation.
/// When set, `update()` uses virtual time and `run()` uses `wait_until()`.
/// This method is called by ConfigurableSystem in Phase 7.
pub fn set_virtual_clock(&mut self, clock: Arc<VirtualClock>) {
self.virtual_clock = Some(clock);
}
/// Get current virtual time, or Duration::ZERO if no VirtualClock is injected.
fn current_virtual_time(&self) -> Duration {
self.virtual_clock.as_ref().map_or(Duration::ZERO, |vc| vc.now())
}
/// Set the initial program counter value for the CPU
/// Parameters: self - CPU instance, pc - Initial 12-bit program counter value
/// Returns: Modified CPU instance with new program counter
@ -428,13 +448,13 @@ impl Intel4004 {
/// - A1 cycle: CPU drives PC high nibble (bits 7-4) onto data bus
/// - A2 cycle: CPU drives PC low nibble (bits 3-0) onto data bus
/// - A3 cycle: CPU reads data from ROM (instruction byte)
fn handle_phi1_rising(&mut self) {
fn handle_phi1_rising(&mut self, virtual_time: Duration) {
// Advance address cycle at the START of each PHI1 rising edge.
// This advances A1→A2, A2→A3, A3→M1, and M1→M2.
// Data reading (OPR/OPA) only happens during PHI2↑ (see Phase 1 of update()),
// so advancing here is safe — the data for the new cycle will be read
// during the subsequent PHI2↑ half-cycle.
self.advance_address_cycle();
self.advance_address_cycle(virtual_time);
// Check for memory operation start on Φ1 rising edge with SYNC high
let (sync, cm, _) = self.read_control_pins();
@ -581,7 +601,7 @@ impl Intel4004 {
/// Advance address cycle state machine at end of clock cycle (PHI2 falling)
/// This ensures each address phase lasts a complete clock cycle
fn advance_address_cycle(&mut self) {
fn advance_address_cycle(&mut self, virtual_time: Duration) {
match self.memory_state {
MemoryState::AddressPhase => {
match self.address_cycle {
@ -593,7 +613,7 @@ impl Intel4004 {
// A2 complete, advance to A3 (transition to data phase)
self.address_cycle = AddressCycle::A3;
self.memory_state = MemoryState::WaitLatency;
self.address_latch_time = Some(Instant::now());
self.address_latch_time = Some(virtual_time);
}
_ => {}
}
@ -2216,8 +2236,15 @@ impl Component for Intel4004 {
}
/// Update the CPU state for one simulation cycle
/// Processes clock cycles and executes instructions when running
/// Delegates to update_at_time with current virtual time
fn update(&mut self) {
let vtime = self.current_virtual_time();
self.update_at_time(vtime);
}
/// Virtual-time-aware update cycle
/// Processes clock cycles and executes instructions when running
fn update_at_time(&mut self, virtual_time: Duration) {
// Handle system reset first (highest priority) - even when not running
self.handle_reset();
@ -2356,7 +2383,7 @@ impl Component for Intel4004 {
self.prev_phi2 = phi2;
if phi1_rising {
self.handle_phi1_rising();
self.handle_phi1_rising(virtual_time);
}
if phi1_falling {
@ -2377,14 +2404,49 @@ impl Component for Intel4004 {
self.cycle_count += 1;
}
/// Run the CPU in a continuous loop until stopped
/// Provides a time-sliced execution model with microsecond delays between cycles
/// Run the CPU with VirtualClock support or legacy fallback.
///
/// **Note:** When used with `ConfigurableSystem::start()`, this method is NOT called
/// directly. Instead, the system thread loop calls `update_at_time()` per tick,
/// releasing the component lock between cycles for GUI access. This `run()` method
/// is only used for standalone/direct component execution (e.g., tests).
fn run(&mut self) {
self.base.set_running(true);
// If no VirtualClock is injected, fall back to wall-clock run loop
let vclock = match self.virtual_clock.as_ref() {
Some(vc) => Arc::clone(vc),
None => {
// Legacy mode: use wall-clock timing
while self.is_running() {
self.update();
thread::sleep(Duration::from_micros(1));
// Safety check: prevent infinite loops in testing environments
if self.cycle_count > 100000 {
println!(
"WARNING: CPU exceeded 100,000 cycles - stopping to prevent infinite loop"
);
self.base.set_running(false);
}
}
return;
}
};
// Virtual time mode: one cycle per wake for timing accuracy.
// Each VirtualClock tick corresponds to one CPU cycle. Processing
// multiple cycles with the same virtual time would cause timing-sensitive
// logic (address latch, latency waits) to see incorrect elapsed times.
while self.is_running() {
self.update();
thread::sleep(Duration::from_micros(1));
let next_tick = vclock.now() + vclock.tick_size();
if let Some(current_vtime) = vclock.wait_until(next_tick) {
if !self.is_running() { break; }
self.update_at_time(current_vtime);
} else {
// Clock was interrupted, exit
break;
}
// Safety check: prevent infinite loops in testing environments
if self.cycle_count > 100000 {
@ -2396,6 +2458,12 @@ impl Component for Intel4004 {
}
}
/// Get timing constraints for Intel 4004 CPU
/// Returns: ComponentTiming with 150ns propagation delay, 750kHz max frequency
fn timing(&self) -> ComponentTiming {
ComponentTiming::intel_4004()
}
/// Stop the CPU and clean up resources
/// Tri-states all outputs and prepares for shutdown
fn stop(&mut self) {
@ -2420,6 +2488,14 @@ impl Component for Intel4004 {
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn set_virtual_clock_optional(&mut self, clock: Option<Arc<VirtualClock>>) {
self.virtual_clock = clock;
}
fn has_virtual_clock(&self) -> bool {
self.virtual_clock.is_some()
}
}
impl RunnableComponent for Intel4004 {

View file

@ -3,9 +3,10 @@ use std::any::Any;
use std::collections::HashMap;
use std::sync::Arc;
use std::thread;
use std::time::{Duration, Instant};
use std::time::Duration;
use crate::component::{BaseComponent, Component, RunnableComponent};
use crate::component_timing::ComponentTiming;
use crate::components::common::intel_400x::{
process_event_direct, Intel400xAddressHandling, Intel400xClockHandling,
Intel400xComponentBehavior, Intel400xComponentType, Intel400xControlPins, Intel400xDataBus,
@ -13,6 +14,7 @@ use crate::components::common::intel_400x::{
Intel400xUnifiedState, TimingState,
};
use crate::pin::{Pin, PinValue};
use crate::virtual_clock::VirtualClock;
/// Intel 4001 - 256-byte ROM with integrated I/O
/// Part of the MCS-4 family, designed to work with Intel 4004 CPU
@ -48,6 +50,13 @@ pub struct Intel4001 {
a3_wait_cycles: u8,
// Track whether data was actually driven during this PHI2 high period
data_driven_this_cycle: bool,
// Virtual time support (Phase 6)
virtual_clock: Option<Arc<VirtualClock>>, // Injected by ConfigurableSystem (Phase 7)
/// Virtual time snapshot for the current update cycle.
/// Set at the start of `update_at_time()` so all internal methods use a
/// consistent time value instead of reading `VirtualClock::now()` which
/// may have advanced since the cycle started.
current_update_time: Duration,
}
#[derive(Debug, Clone, Copy, PartialEq)]
@ -147,11 +156,11 @@ impl Intel400xTimingState for Intel4001 {
self.state_machine.current_state = unified_state;
}
fn get_address_latch_time(&self) -> Option<Instant> {
fn get_address_latch_time(&self) -> Option<Duration> {
self.state_machine.get_address_latch_time()
}
fn set_address_latch_time(&mut self, time: Option<Instant>) {
fn set_address_latch_time(&mut self, time: Option<Duration>) {
self.state_machine.set_address_latch_time(time);
}
@ -182,6 +191,10 @@ impl Intel400xTimingState for Intel4001 {
fn get_access_time(&self) -> Duration {
self.state_machine.get_access_time()
}
fn is_latency_elapsed_at(&self, virtual_time: Duration) -> bool {
self.state_machine.is_latency_elapsed_at(virtual_time)
}
}
impl Intel400xComponentBehavior for Intel4001 {
@ -273,9 +286,24 @@ impl Intel400xComponentBehavior for Intel4001 {
self.handle_address_latching();
}
Intel400xUnifiedState::WaitLatency => {
// Check if latency has elapsed
if self.state_machine.is_latency_elapsed() {
// Transition to DriveData
// Check if latency has elapsed using virtual time.
//
// **With VirtualClock (production):** `address_latch_time` was recorded
// by `handle_address_nibble_at()` in virtual time. We check if enough
// virtual time has elapsed since then (≥ 500ns access time).
//
// **Without VirtualClock (standalone tests):** `current_virtual_time()`
// returns `Duration::ZERO`, so `is_latency_elapsed_at()` would always
// return `false` (since virtual time never advances). The `!has_clock`
// guard short-circuits to skip the wait immediately.
//
// NOTE: The old `Instant::now()`-based code would naturally wait ~500ns
// because wall-clock time always advances. This new code skips the wait
// in legacy mode, making standalone tests less hardware-accurate but
// preventing them from hanging forever in WaitLatency.
let has_clock = self.virtual_clock.is_some();
let vtime = self.current_update_time;
if !has_clock || self.state_machine.is_latency_elapsed_at(vtime) {
self.process_latency_elapsed();
}
}
@ -353,9 +381,32 @@ impl Intel4001 {
data_nibble_phase: false, // Start with OPR (high nibble)
a3_wait_cycles: 0, // No A3 wait initially
data_driven_this_cycle: false, // No data driven yet
virtual_clock: None,
current_update_time: Duration::ZERO,
}
}
/// Inject a VirtualClock for virtual time-based operation.
/// When set, `update()` uses virtual time and `run()` uses `wait_until()`.
/// This method is called by ConfigurableSystem in Phase 7.
pub fn set_virtual_clock(&mut self, clock: Arc<VirtualClock>) {
self.virtual_clock = Some(clock);
}
/// Get current virtual time, or Duration::ZERO if no VirtualClock is injected.
///
/// # Zero-Time Invariant
///
/// When no VirtualClock is injected, this returns `Duration::ZERO`. The `WaitLatency`
/// handler checks `vtime == Duration::ZERO` to skip latency enforcement in legacy mode.
/// This is intentional: legacy tests don't use VirtualClock and expect latency to be
/// skipped for backward compatibility. If this ever returns a non-zero value without
/// a proper VirtualClock, the latency check would use zero as the latch time and
/// produce incorrect results.
fn current_virtual_time(&self) -> Duration {
self.virtual_clock.as_ref().map_or(Duration::ZERO, |vc| vc.now())
}
/// Set memory access time for simulation
/// Parameters: access_time_ns - Access time in nanoseconds
pub fn set_access_time(&mut self, access_time_ns: u64) {
@ -455,8 +506,9 @@ impl Intel4001 {
if self.state_machine.get_state() == Intel400xUnifiedState::AddressPhase {
let nibble = self.read_data_bus() & 0x0F;
if self.state_machine.handle_address_nibble(nibble) {
// Address is complete (both nibbles latched), transition to latency wait
if self.state_machine.handle_address_nibble_at(nibble, self.current_update_time) {
// Address is complete — virtual time was recorded atomically by handle_address_nibble_at()
// Transition to latency wait
let _addr = self.state_machine.get_full_address().unwrap_or(0xFFFF);
self.process_address_complete();
// Set A3 wait counter
@ -768,8 +820,9 @@ impl Intel4001 {
// Use common address latching functionality
let nibble = self.read_data_bus() & 0x0F; // Only 4 bits are valid for address nibbles
if self.state_machine.handle_address_nibble(nibble) {
// Address is complete, transition to latency wait
if self.state_machine.handle_address_nibble_at(nibble, self.current_update_time) {
// Address is complete — virtual time was recorded atomically by handle_address_nibble_at()
// Transition to latency wait
self.process_address_complete();
}
}
@ -778,12 +831,9 @@ impl Intel4001 {
/// Hardware: ROM needs 500ns to access data after address is latched
#[allow(dead_code)] // This method is kept for compatibility with legacy code
fn handle_latency_wait(&mut self) {
// This is now handled by state machine
if self.state_machine.is_latency_elapsed() {
// Latency elapsed, transition to data driving
// Data will be driven on next Φ2 rising edge
self.process_latency_elapsed();
}
// Note: Legacy method — latency check now requires virtual time
// Always transition for backward compatibility
self.process_latency_elapsed();
}
/// Handle data driving during DriveData state
@ -899,9 +949,20 @@ impl Component for Intel4001 {
self.base.get_pin(name)
}
/// Main update cycle - handles clock edge detection and operation dispatch
/// Main update cycle - delegates to update_at_time with current virtual time
/// Hardware: Responds to Φ1 and Φ2 clock edges from CPU
fn update(&mut self) {
let vtime = self.current_virtual_time();
self.update_at_time(vtime);
}
/// Virtual-time-aware update cycle - handles clock edge detection and operation dispatch
/// Hardware: Responds to Φ1 and Φ2 clock edges from CPU
fn update_at_time(&mut self, virtual_time: Duration) {
// Snapshot virtual_time for the entire update cycle so internal methods
// (handle_phi1_rising, execute_state_logic, etc.) use a consistent time
// instead of reading VirtualClock::now() which may have advanced.
self.current_update_time = virtual_time;
if !self.is_running() {
return;
}
@ -952,7 +1013,7 @@ impl Component for Intel4001 {
// 3. Calling it would cause double-latching of address nibbles
}
/// Run component in time-slice mode (manual control)
/// Run component with VirtualClock support or legacy fallback
/// Hardware: Simulates continuous operation with clock edge detection
fn run(&mut self) {
self.base.set_running(true);
@ -962,13 +1023,37 @@ impl Component for Intel4001 {
self.prev_phi1 = phi1;
self.prev_phi2 = phi2;
// If no VirtualClock is injected, fall back to wall-clock run loop
let vclock = match self.virtual_clock.as_ref() {
Some(vc) => Arc::clone(vc),
None => {
// Legacy mode: use wall-clock timing
while self.is_running() {
self.update();
thread::sleep(Duration::from_micros(1));
}
return;
}
};
// Virtual time mode: wait for next tick to synchronize with clock
while self.is_running() {
self.update();
// Small delay to prevent busy waiting when no clock is present
thread::sleep(Duration::from_micros(1));
let next_tick = vclock.now() + vclock.tick_size();
if let Some(current_vtime) = vclock.wait_until(next_tick) {
self.update_at_time(current_vtime);
} else {
// Clock was interrupted, exit
break;
}
}
}
/// Get timing constraints for Intel 4001 ROM
/// Returns: ComponentTiming with 500ns access time, 200ns propagation delay
fn timing(&self) -> ComponentTiming {
ComponentTiming::intel_4001()
}
/// Stop component and tri-state all outputs
/// Hardware: Component enters low-power state, all pins high-impedance
fn stop(&mut self) {
@ -994,6 +1079,14 @@ impl Component for Intel4001 {
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn set_virtual_clock_optional(&mut self, clock: Option<Arc<VirtualClock>>) {
self.virtual_clock = clock;
}
fn has_virtual_clock(&self) -> bool {
self.virtual_clock.is_some()
}
}
impl RunnableComponent for Intel4001 {}

View file

@ -3,9 +3,10 @@ use std::any::Any;
use std::collections::HashMap;
use std::sync::Arc;
use std::thread;
use std::time::{Duration, Instant};
use std::time::Duration;
use crate::component::{BaseComponent, Component, RunnableComponent};
use crate::component_timing::ComponentTiming;
use crate::components::common::intel_400x::{
process_event_direct, Intel400xAddressHandling, Intel400xClockHandling,
Intel400xComponentBehavior, Intel400xComponentType, Intel400xControlPins, Intel400xDataBus,
@ -13,6 +14,7 @@ use crate::components::common::intel_400x::{
Intel400xUnifiedState, TimingState,
};
use crate::pin::{Pin, PinValue};
use crate::virtual_clock::VirtualClock;
/// Intel 4002 - 320-byte RAM with I/O ports
/// Part of the MCS-4 family, designed to work with Intel 4004 CPU
@ -47,6 +49,13 @@ pub struct Intel4002 {
prev_phi2: PinValue, // Previous Φ2 clock state for edge detection
// Operation type for next cycle (determines ReadData vs WriteData vs OutputPort)
pending_operation: PendingOperation,
// Virtual time support (Phase 6)
virtual_clock: Option<Arc<VirtualClock>>, // Injected by ConfigurableSystem (Phase 7)
/// Virtual time snapshot for the current update cycle.
/// Set at the start of `update_at_time()` so all internal methods use a
/// consistent time value instead of reading `VirtualClock::now()` which
/// may have advanced since the cycle started.
current_update_time: Duration,
}
impl Intel400xClockHandling for Intel4002 {
@ -130,11 +139,11 @@ impl Intel400xTimingState for Intel4002 {
self.state_machine.current_state = unified_state;
}
fn get_address_latch_time(&self) -> Option<Instant> {
fn get_address_latch_time(&self) -> Option<Duration> {
self.state_machine.get_address_latch_time()
}
fn set_address_latch_time(&mut self, time: Option<Instant>) {
fn set_address_latch_time(&mut self, time: Option<Duration>) {
self.state_machine.set_address_latch_time(time);
}
@ -165,6 +174,10 @@ impl Intel400xTimingState for Intel4002 {
fn get_access_time(&self) -> Duration {
self.state_machine.get_access_time()
}
fn is_latency_elapsed_at(&self, virtual_time: Duration) -> bool {
self.state_machine.is_latency_elapsed_at(virtual_time)
}
}
impl Intel400xComponentBehavior for Intel4002 {
@ -264,9 +277,22 @@ impl Intel400xComponentBehavior for Intel4002 {
self.handle_address_latching();
}
Intel400xUnifiedState::WaitLatency => {
// Check if latency has elapsed
if self.state_machine.is_latency_elapsed() {
// Transition to ReadData (default for RAM)
// Check if latency has elapsed using virtual time.
//
// **With VirtualClock (production):** `address_latch_time` was recorded
// by `handle_address_nibble_at()` in virtual time. We check if enough
// virtual time has elapsed since then (≥ 500ns access time).
//
// **Without VirtualClock (standalone tests):** `current_virtual_time()`
// returns `Duration::ZERO`, so `is_latency_elapsed_at()` would always
// return `false`. The `!has_clock` guard short-circuits to skip the wait.
//
// NOTE: The old `Instant::now()`-based code would naturally wait ~500ns.
// This new code skips the wait in legacy mode, making standalone tests
// less hardware-accurate but preventing them from hanging in WaitLatency.
let has_clock = self.virtual_clock.is_some();
let vtime = self.current_update_time;
if !has_clock || self.state_machine.is_latency_elapsed_at(vtime) {
self.process_latency_elapsed();
}
}
@ -349,9 +375,30 @@ impl Intel4002 {
prev_phi1: PinValue::Low,
prev_phi2: PinValue::Low,
pending_operation: PendingOperation::Read, // Default to read
virtual_clock: None,
current_update_time: Duration::ZERO,
}
}
/// Inject a VirtualClock for virtual time-based operation.
/// When set, `update()` uses virtual time and `run()` uses `wait_until()`.
/// This method is called by ConfigurableSystem in Phase 7.
pub fn set_virtual_clock(&mut self, clock: Arc<VirtualClock>) {
self.virtual_clock = Some(clock);
}
/// Get current virtual time, or Duration::ZERO if no VirtualClock is injected.
///
/// # Zero-Time Invariant
///
/// When no VirtualClock is injected, this returns `Duration::ZERO`. The `WaitLatency`
/// handler checks `vtime == Duration::ZERO` to skip latency enforcement in legacy mode.
/// This is intentional: legacy tests don't use VirtualClock and expect latency to be
/// skipped for backward compatibility.
fn current_virtual_time(&self) -> Duration {
self.virtual_clock.as_ref().map_or(Duration::ZERO, |vc| vc.now())
}
/// Set memory access time for simulation
/// Parameters: access_time_ns - Access time in nanoseconds
pub fn set_access_time(&mut self, access_time_ns: u64) {
@ -464,8 +511,9 @@ impl Intel4002 {
// Latch address nibble on PHI1 falling edge (data is now stable)
let nibble = self.read_data_bus() & 0x0F;
if self.state_machine.handle_address_nibble(nibble) {
// Address is complete (both nibbles latched), transition to latency wait
if self.state_machine.handle_address_nibble_at(nibble, self.current_update_time) {
// Address is complete — virtual time was recorded atomically by handle_address_nibble_at()
// Transition to latency wait
self.process_address_complete();
}
}
@ -475,9 +523,13 @@ impl Intel4002 {
/// Hardware: Φ2 high = Peripherals drive bus with data
/// Focus: Data driving operations
fn handle_phi2_rising(&mut self) {
// Check for latency elapsed and transition to ReadData/WriteData/OutputPort if needed
// Check for latency elapsed and transition to ReadData/WriteData/OutputPort if needed.
// When no VirtualClock is injected, skip latency wait for backward compatibility
// (see WaitLatency handler for details).
if self.state_machine.get_state() == Intel400xUnifiedState::WaitLatency {
if self.state_machine.is_latency_elapsed() {
let has_clock = self.virtual_clock.is_some();
let vtime = self.current_update_time;
if !has_clock || self.state_machine.is_latency_elapsed_at(vtime) {
self.process_latency_elapsed();
}
}
@ -534,8 +586,9 @@ impl Intel4002 {
// Use common address latching functionality
let nibble = self.read_data_bus() & 0x0F; // Only 4 bits are valid for address nibbles
if self.state_machine.handle_address_nibble(nibble) {
// Address is complete, transition to latency wait
if self.state_machine.handle_address_nibble_at(nibble, self.current_update_time) {
// Address is complete — virtual time was recorded atomically by handle_address_nibble_at()
// Transition to latency wait
self.process_address_complete();
}
}
@ -661,9 +714,20 @@ impl Component for Intel4002 {
self.base.get_pin(name)
}
/// Main update cycle - handles clock edge detection and operation dispatch
/// Main update cycle - delegates to update_at_time with current virtual time
/// Hardware: Responds to Φ1 and Φ2 clock edges from CPU
fn update(&mut self) {
let vtime = self.current_virtual_time();
self.update_at_time(vtime);
}
/// Virtual-time-aware update cycle - handles clock edge detection and operation dispatch
/// Hardware: Responds to Φ1 and Φ2 clock edges from CPU
fn update_at_time(&mut self, virtual_time: Duration) {
// Snapshot virtual_time for the entire update cycle so internal methods
// use a consistent time instead of reading VirtualClock::now() which
// may have advanced.
self.current_update_time = virtual_time;
if !self.is_running() {
return;
}
@ -710,7 +774,7 @@ impl Component for Intel4002 {
// 3. Calling it would cause double-latching of address nibbles
}
/// Run component in time-slice mode (manual control)
/// Run component with VirtualClock support or legacy fallback
/// Hardware: Simulates continuous operation with clock edge detection
fn run(&mut self) {
self.base.set_running(true);
@ -720,13 +784,37 @@ impl Component for Intel4002 {
self.prev_phi1 = phi1;
self.prev_phi2 = phi2;
// If no VirtualClock is injected, fall back to wall-clock run loop
let vclock = match self.virtual_clock.as_ref() {
Some(vc) => Arc::clone(vc),
None => {
// Legacy mode: use wall-clock timing
while self.is_running() {
self.update();
thread::sleep(Duration::from_micros(1));
}
return;
}
};
// Virtual time mode: wait for next tick to synchronize with clock
while self.is_running() {
self.update();
// Small delay to prevent busy waiting when no clock is present
thread::sleep(Duration::from_micros(1));
let next_tick = vclock.now() + vclock.tick_size();
if let Some(current_vtime) = vclock.wait_until(next_tick) {
self.update_at_time(current_vtime);
} else {
// Clock was interrupted, exit
break;
}
}
}
/// Get timing constraints for Intel 4002 RAM
/// Returns: ComponentTiming with 500ns access time, 200ns propagation delay
fn timing(&self) -> ComponentTiming {
ComponentTiming::intel_4002()
}
/// Stop component and tri-state all outputs
/// Hardware: Component enters low-power state, all pins high-impedance
fn stop(&mut self) {
@ -752,6 +840,14 @@ impl Component for Intel4002 {
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn set_virtual_clock_optional(&mut self, clock: Option<Arc<VirtualClock>>) {
self.virtual_clock = clock;
}
fn has_virtual_clock(&self) -> bool {
self.virtual_clock.is_some()
}
}
impl RunnableComponent for Intel4002 {}

View file

@ -3,9 +3,10 @@ use std::any::Any;
use std::collections::HashMap;
use std::sync::Arc;
use std::thread;
use std::time::{Duration, Instant};
use std::time::Duration;
use crate::component::{BaseComponent, Component, RunnableComponent};
use crate::component_timing::ComponentTiming;
use crate::components::common::intel_400x::{
process_event_direct, Intel400xAddressHandling, Intel400xClockHandling,
Intel400xComponentBehavior, Intel400xComponentType, Intel400xControlPins, Intel400xDataBus,
@ -13,6 +14,7 @@ use crate::components::common::intel_400x::{
Intel400xUnifiedState, TimingState,
};
use crate::pin::{Pin, PinValue};
use crate::virtual_clock::VirtualClock;
/// Intel 4003 - 10-bit serial-in, serial-out shift register
/// Part of the MCS-4 family, designed to work with Intel 4004 CPU
@ -33,6 +35,13 @@ pub struct Intel4003 {
// Clock edge detection
prev_phi1: PinValue, // Previous Φ1 clock state for edge detection
prev_phi2: PinValue, // Previous Φ2 clock state for edge detection
// Virtual time support (Phase 6)
virtual_clock: Option<Arc<VirtualClock>>, // Injected by ConfigurableSystem (Phase 7)
/// Virtual time snapshot for the current update cycle.
/// Set at the start of `update_at_time()` so all internal methods use a
/// consistent time value instead of reading `VirtualClock::now()` which
/// may have advanced since the cycle started.
current_update_time: Duration,
}
/// Serial operation modes for 4003 shift register
@ -121,11 +130,11 @@ impl Intel400xTimingState for Intel4003 {
self.state_machine.current_state = unified_state;
}
fn get_address_latch_time(&self) -> Option<Instant> {
fn get_address_latch_time(&self) -> Option<Duration> {
self.state_machine.get_address_latch_time()
}
fn set_address_latch_time(&mut self, time: Option<Instant>) {
fn set_address_latch_time(&mut self, time: Option<Duration>) {
self.state_machine.set_address_latch_time(time);
}
@ -156,6 +165,10 @@ impl Intel400xTimingState for Intel4003 {
fn get_access_time(&self) -> Duration {
self.state_machine.get_access_time()
}
fn is_latency_elapsed_at(&self, virtual_time: Duration) -> bool {
self.state_machine.is_latency_elapsed_at(virtual_time)
}
}
impl Intel400xComponentBehavior for Intel4003 {
@ -243,9 +256,22 @@ impl Intel400xComponentBehavior for Intel4003 {
self.handle_address_latching();
}
Intel400xUnifiedState::WaitLatency => {
// Check if latency has elapsed
if self.state_machine.is_latency_elapsed() {
// Transition to ShiftData (default for 4003)
// Check if latency has elapsed using virtual time.
//
// **With VirtualClock (production):** `address_latch_time` was recorded
// by `handle_address_nibble_at()` in virtual time. We check if enough
// virtual time has elapsed since then (≥ 200ns access time).
//
// **Without VirtualClock (standalone tests):** `current_virtual_time()`
// returns `Duration::ZERO`, so `is_latency_elapsed_at()` would always
// return `false`. The `!has_clock` guard short-circuits to skip the wait.
//
// NOTE: The old `Instant::now()`-based code would naturally wait ~200ns.
// This new code skips the wait in legacy mode, making standalone tests
// less hardware-accurate but preventing them from hanging in WaitLatency.
let has_clock = self.virtual_clock.is_some();
let vtime = self.current_update_time;
if !has_clock || self.state_machine.is_latency_elapsed_at(vtime) {
self.process_latency_elapsed();
}
}
@ -317,9 +343,30 @@ impl Intel4003 {
),
prev_phi1: PinValue::Low,
prev_phi2: PinValue::Low,
virtual_clock: None,
current_update_time: Duration::ZERO,
}
}
/// Inject a VirtualClock for virtual time-based operation.
/// When set, `update()` uses virtual time and `run()` uses `wait_until()`.
/// This method is called by ConfigurableSystem in Phase 7.
pub fn set_virtual_clock(&mut self, clock: Arc<VirtualClock>) {
self.virtual_clock = Some(clock);
}
/// Get current virtual time, or Duration::ZERO if no VirtualClock is injected.
///
/// # Zero-Time Invariant
///
/// When no VirtualClock is injected, this returns `Duration::ZERO`. The `WaitLatency`
/// handler checks `vtime == Duration::ZERO` to skip latency enforcement in legacy mode.
/// This is intentional: legacy tests don't use VirtualClock and expect latency to be
/// skipped for backward compatibility.
fn current_virtual_time(&self) -> Duration {
self.virtual_clock.as_ref().map_or(Duration::ZERO, |vc| vc.now())
}
/// Set access time for simulation
/// Parameters: access_time_ns - Access time in nanoseconds
pub fn set_access_time(&mut self, access_time_ns: u64) {
@ -419,8 +466,9 @@ impl Intel4003 {
// But we still need to handle the address nibble for compatibility
let nibble = self.read_data_bus() & 0x0F; // Only 4 bits are valid for address nibbles
if self.state_machine.handle_address_nibble(nibble) {
// Address is complete, transition to latency wait
if self.state_machine.handle_address_nibble_at(nibble, self.current_update_time) {
// Address is complete — virtual time was recorded atomically by handle_address_nibble_at()
// Transition to latency wait
self.process_address_complete();
}
}
@ -520,9 +568,20 @@ impl Component for Intel4003 {
self.base.get_pin(name)
}
/// Main update cycle - handles clock edge detection and operation dispatch
/// Main update cycle - delegates to update_at_time with current virtual time
/// Hardware: Responds to Φ1 and Φ2 clock edges from CPU
fn update(&mut self) {
let vtime = self.current_virtual_time();
self.update_at_time(vtime);
}
/// Virtual-time-aware update cycle - handles clock edge detection and operation dispatch
/// Hardware: Responds to Φ1 and Φ2 clock edges from CPU
fn update_at_time(&mut self, virtual_time: Duration) {
// Snapshot virtual_time for the entire update cycle so internal methods
// use a consistent time instead of reading VirtualClock::now() which
// may have advanced.
self.current_update_time = virtual_time;
if !self.is_running() {
return;
}
@ -581,7 +640,7 @@ impl Component for Intel4003 {
self.execute_state_machine();
}
/// Run component in time-slice mode (manual control)
/// Run component with VirtualClock support or legacy fallback
/// Hardware: Simulates continuous operation with clock edge detection
fn run(&mut self) {
self.base.set_running(true);
@ -591,13 +650,37 @@ impl Component for Intel4003 {
self.prev_phi1 = phi1;
self.prev_phi2 = phi2;
// If no VirtualClock is injected, fall back to wall-clock run loop
let vclock = match self.virtual_clock.as_ref() {
Some(vc) => Arc::clone(vc),
None => {
// Legacy mode: use wall-clock timing
while self.is_running() {
self.update();
thread::sleep(Duration::from_micros(1));
}
return;
}
};
// Virtual time mode: wait for next tick to synchronize with clock
while self.is_running() {
self.update();
// Small delay to prevent busy waiting when no clock is present
thread::sleep(Duration::from_micros(1));
let next_tick = vclock.now() + vclock.tick_size();
if let Some(current_vtime) = vclock.wait_until(next_tick) {
self.update_at_time(current_vtime);
} else {
// Clock was interrupted, exit
break;
}
}
}
/// Get timing constraints for Intel 4003 shift register
/// Returns: ComponentTiming with 200ns access time, 100ns propagation delay
fn timing(&self) -> ComponentTiming {
ComponentTiming::intel_4003()
}
/// Stop component and tri-state all outputs
/// Hardware: Component enters low-power state, all pins high-impedance
fn stop(&mut self) {
@ -622,6 +705,14 @@ impl Component for Intel4003 {
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn set_virtual_clock_optional(&mut self, clock: Option<Arc<VirtualClock>>) {
self.virtual_clock = clock;
}
fn has_virtual_clock(&self) -> bool {
self.virtual_clock.is_some()
}
}
impl RunnableComponent for Intel4003 {}

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,

View file

@ -31,6 +31,8 @@ use std::thread;
use std::time::{Duration, Instant};
use crate::system_config::{ConfigurableSystem, SystemController, SystemState};
use crate::gui::state::format_virtual_time_ns;
use crate::virtual_clock::SimulationMode;
/// Display format for memory and register values
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
@ -498,12 +500,104 @@ impl ConsoleApp {
"ram" => {
// RAM display is always shown in the UI
}
"speed" => {
if parts.len() > 1 {
if let Ok(multiplier) = parts[1].parse::<f64>() {
if multiplier <= 0.0 {
self.set_status("Error: speed must be positive".to_string());
} else {
let system = self.system.lock();
let clock = system.get_virtual_clock();
clock.set_speed_multiplier(multiplier);
drop(system);
self.set_status(format!("Speed set to {:.3}×", multiplier));
}
} else {
self.set_status(format!("Invalid speed value: {}", parts[1]));
}
} else {
let system = self.system.lock();
let clock = system.get_virtual_clock();
let speed = clock.speed_multiplier();
let mode = clock.mode();
drop(system);
self.set_status(format!(
"Speed: {:.3}× | Mode: {:?}",
speed, mode
));
}
}
"mode" => {
if parts.len() > 1 {
let new_mode = match parts[1] {
"realtime" | "rt" | "real" => Some(SimulationMode::RealTime),
"maxspeed" | "max" | "fast" => Some(SimulationMode::MaxSpeed),
"step" | "singlestep" | "single" => Some(SimulationMode::SingleStep),
_ => None,
};
if let Some(mode) = new_mode {
let system = self.system.lock();
let clock = system.get_virtual_clock();
clock.set_mode(mode);
drop(system);
self.set_status(format!("Mode set to {:?}", mode));
} else {
self.set_status("Usage: mode <realtime|maxspeed|step>".to_string());
}
} else {
let system = self.system.lock();
let clock = system.get_virtual_clock();
let mode = clock.mode();
drop(system);
self.set_status(format!("Current mode: {:?}", mode));
}
}
"step" => {
let system = self.system.lock();
let clock = system.get_virtual_clock();
if clock.mode() != SimulationMode::SingleStep {
drop(system);
self.set_status("Error: must be in SingleStep mode. Use 'mode step' first.".to_string());
} else if clock.trigger_step() {
drop(system);
self.set_status("Step triggered".to_string());
} else {
drop(system);
self.set_status("Step already pending...".to_string());
}
}
"time" => {
let system = self.system.lock();
let clock = system.get_virtual_clock();
let ns = clock.now_ns();
let ticks = clock.tick_count();
let speed = clock.speed_multiplier();
let mode = clock.mode();
let running = clock.is_running();
let paused = clock.is_paused();
drop(system);
// Format virtual time
let time_str = format_virtual_time_ns(ns);
self.set_status(format!(
"VTime: {} | Ticks: {} | Speed: {:.3}× | Mode: {:?} | {}{}",
time_str,
ticks,
speed,
mode,
if running { "Running" } else { "Stopped" },
if paused { " (Paused)" } else { "" }
));
}
"" => {
// Empty command - do nothing
}
_ => {
println!("Unknown command: '{}'", cmd);
println!("Available commands: quit, run, stop, pause, resume, help, reset, status, freq, clock");
self.set_status(format!(
"Unknown command: '{}'. Commands: quit, run, stop, pause, resume, help, reset, status, freq, clock, speed, mode, step, time",
cmd
));
}
}
}
@ -645,6 +739,27 @@ impl ConsoleApp {
Span::raw(" - Enable/disable clock"),
]),
Line::from(""),
Line::from(vec![Span::styled(
"Timing Control Commands:",
Style::default().add_modifier(Modifier::BOLD),
)]),
Line::from(vec![
Span::styled(" speed [multiplier]", Style::default().fg(Color::Yellow)),
Span::raw(" - Get/set speed (e.g., speed 0.5)"),
]),
Line::from(vec![
Span::styled(" mode [realtime|maxspeed|step]", Style::default().fg(Color::Yellow)),
Span::raw(" - Get/set simulation mode"),
]),
Line::from(vec![
Span::styled(" step", Style::default().fg(Color::Yellow)),
Span::raw(" - Trigger single step (SingleStep mode)"),
]),
Line::from(vec![
Span::styled(" time", Style::default().fg(Color::Yellow)),
Span::raw(" - Show virtual time, ticks, speed"),
]),
Line::from(""),
Line::from(vec![Span::styled(
"Keyboard Shortcuts:",
Style::default().add_modifier(Modifier::BOLD),
@ -705,7 +820,7 @@ impl ConsoleApp {
let info_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(10), // System info (increased for state display)
Constraint::Length(14), // System info (increased for timing display)
Constraint::Min(5), // Registers
])
.split(area);
@ -717,6 +832,18 @@ impl ConsoleApp {
let freq = system.get_clock_frequency();
let clock_enabled = system.is_clock_enabled();
// Read virtual clock timing info
let vclock = system.get_virtual_clock();
let vtime_ns = vclock.now_ns();
let vticks = vclock.tick_count();
let vspeed = vclock.speed_multiplier();
let vmode = vclock.mode();
let vpaused = vclock.is_paused();
drop(system); // Release lock before rendering
// Format virtual time
let vtime_str = format_virtual_time_ns(vtime_ns);
// Color code the state
let state_style = match state {
SystemState::Running => Style::default().fg(Color::Green),
@ -760,8 +887,21 @@ impl ConsoleApp {
"RAM Size: {} nibbles",
info.ram_size
))]),
// Virtual timing info
Line::from(vec![
Span::styled("VTime: ", Style::default().fg(Color::Cyan)),
Span::raw(format!("{}, Ticks: {}", vtime_str, vticks)),
]),
Line::from(vec![
Span::styled("Speed: ", Style::default().fg(Color::Cyan)),
Span::raw(format!("{:.3}× | Mode: {:?}", vspeed, vmode)),
if vpaused {
Span::styled(" (Paused)", Style::default().fg(Color::Yellow))
} else {
Span::raw("")
},
]),
];
drop(system); // Release lock before rendering
let system_widget = Paragraph::new(system_info)
.block(

View file

@ -195,6 +195,10 @@ impl GuiApp {
self.components
.render_control_panel(ui, &mut self.gui_state, self.system.as_ref());
// Timing controls: speed slider, mode selector, step button, time display
self.components
.render_timing_controls(ui, &mut self.gui_state, self.system.as_ref());
// Memory section: RAM and ROM side by side
ui.horizontal(|ui| {
ui.vertical(|ui| {

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);

View file

@ -11,11 +11,14 @@
pub mod component;
pub mod components;
pub mod component_timing;
pub mod connection;
pub mod gui;
pub mod pin;
pub mod real_time_governor;
pub mod system_config;
pub mod types;
pub mod virtual_clock;
// Re-export commonly used items for easier importing
pub use component::{BaseComponent, Component};

View file

@ -212,7 +212,10 @@ fn main() {
{
let mut system = system_arc.lock();
println!("DEBUG: Starting system for console mode (non-blocking)");
system.start();
if let Err(e) = system.start() {
eprintln!("Failed to start system: {}", e);
process::exit(1);
}
}
// Launch console interface (blocks until console is closed)
@ -233,7 +236,10 @@ fn main() {
{
let mut system = system_arc.lock();
println!("DEBUG: Starting system for GUI mode (non-blocking)");
system.start();
if let Err(e) = system.start() {
eprintln!("Failed to start system: {}", e);
process::exit(1);
}
}
// Launch GUI application (blocks until GUI window is closed)

387
src/real_time_governor.rs Normal file
View file

@ -0,0 +1,387 @@
//! Real-Time Governor for controlling virtual clock advancement.
//!
//! The `RealTimeGovernor` runs in its own thread and is responsible for advancing
//! the `VirtualClock` at the appropriate rate. It maps virtual time to wall-clock
//! time based on the speed multiplier, similar to how UAE and VICE synchronize
//! at frame boundaries.
//!
//! # Modes
//!
//! - **RealTime**: Advances virtual time and sleeps to match wall-clock × speed_multiplier.
//! At 1.0×, one virtual nanosecond maps to one wall-clock nanosecond.
//! At 0.1×, one virtual nanosecond takes 10 wall-clock nanoseconds.
//!
//! - **MaxSpeed**: Advances virtual time as fast as the host CPU allows.
//! No sleeping — pure computation speed. Useful for testing and fast-forwarding.
//!
//! - **SingleStep**: Advances virtual time by one tick only when the user triggers it.
//! Useful for debugging individual clock phases or instruction cycles.
use std::sync::Arc;
use std::thread;
use std::time::{Duration, Instant};
use crate::virtual_clock::{SimulationMode, VirtualClock};
/// Minimum practical speed multiplier for real-time mode.
///
/// Values below this produce wall-clock sleep durations so large that the
/// simulation effectively freezes. The same constant should be used in
/// `VirtualClock::set_speed_multiplier()` to reject impractical values early.
const MIN_SPEED_MULTIPLIER: f64 = 0.0001; // 10,000× slower than real-time
/// Governor that controls the rate of virtual time advancement.
///
/// Runs in its own thread, continuously advancing the shared `VirtualClock`
/// based on the current simulation mode and speed multiplier.
pub struct RealTimeGovernor {
/// Shared virtual clock to advance.
clock: Arc<VirtualClock>,
/// Number of ticks to batch before checking wall-clock time.
/// Higher values = less overhead but coarser synchronization.
/// At 333ns/tick, a batch of 100 = 33.3µs per sync point.
batch_size: u32,
}
impl Drop for RealTimeGovernor {
fn drop(&mut self) {
// Ensure component threads are unblocked if the governor panics
// or is dropped unexpectedly. stop() is idempotent — safe to call
// even if already stopped.
self.clock.stop();
}
}
impl RealTimeGovernor {
/// Create a new RealTimeGovernor.
///
/// # Parameters
/// - `clock`: Shared virtual clock to advance. Tick size is read dynamically
/// from the clock via `clock.tick_size_ns()`, so live changes are respected.
/// - `batch_size`: Number of ticks to batch between wall-clock sync points.
/// Default: 100 (good balance between overhead and synchronization accuracy).
pub fn new(clock: Arc<VirtualClock>, batch_size: u32) -> Self {
RealTimeGovernor {
clock,
batch_size: if batch_size == 0 { 100 } else { batch_size },
}
}
/// Create a governor with default batch size (100 ticks per sync).
pub fn with_defaults(clock: Arc<VirtualClock>) -> Self {
RealTimeGovernor::new(clock, 100)
}
/// Run the governor loop.
///
/// This method blocks until the virtual clock is stopped.
/// It should be called from its own thread:
///
/// ```rust,ignore
/// let governor = RealTimeGovernor::with_defaults(clock.clone());
/// let handle = thread::spawn(move || governor.run());
/// ```
///
/// # Lifecycle
///
/// **Note:** This method calls `clock.start()` automatically. You do not need
/// to call `clock.start()` yourself — it is idempotent, so doing so is safe
/// but unnecessary.
///
/// # Shutdown
///
/// The governor exits when `clock.stop()` is called. The governor itself
/// is consumed by this method (takes `self` by value) — all lifecycle
/// control (pause, resume, stop, mode changes) goes through the shared
/// `VirtualClock`. The governor cannot be reconfigured after `run()` is
/// called; create a new governor if different parameters are needed.
///
/// # Panic Safety
///
/// If this method panics before `clock.start()` is called (e.g., during initial setup),
/// component threads blocked in `VirtualClock::wait_for_start()` will deadlock because
/// no thread will ever call `stop()`. To prevent this, ensure `clock.stop()` is called
/// in any cleanup path (e.g., via `Drop` impl on the owning structure or a panic hook).
pub fn run(self) {
self.clock.start();
// Panic guard: ensure clock.stop() is called if the run loop panics,
// so component threads blocked in wait_for_start() / wait_until() are
// unblocked. Drop::drop also calls stop(), so this is a belt-and-
// suspenders approach — stop() is idempotent.
let clock_ref = &self.clock;
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
loop {
if !clock_ref.is_running() {
break;
}
// Efficiently block (zero CPU) while paused instead of busy-polling.
// wait_for_resume() returns false if stopped while paused.
if !clock_ref.wait_for_resume() {
break;
}
match clock_ref.mode() {
SimulationMode::RealTime => self.run_realtime_batch(),
SimulationMode::MaxSpeed => self.run_maxspeed_batch(),
SimulationMode::SingleStep => self.run_single_step(),
}
}
}));
// Always stop the clock, even on panic.
clock_ref.stop();
if let Err(e) = result {
std::panic::resume_unwind(e);
}
}
/// RealTime mode: advance virtual time and sleep to match wall-clock.
///
/// Batches `batch_size` ticks, then sleeps to align virtual time
/// with wall-clock time × speed_multiplier.
fn run_realtime_batch(&self) {
// Read tick size from the clock to pick up live changes via set_tick_size()
let tick_ns = self.clock.tick_size_ns();
let speed_multiplier = self.clock.speed_multiplier().max(MIN_SPEED_MULTIPLIER);
// Reduce batch size at very slow speeds so pause/stop are honored promptly.
// At 0.001× speed with batch_size=100, each batch targets ~33ms of wall time,
// but at 0.0001× that balloons to 333ms — too long between pause checks.
let effective_batch = if speed_multiplier < 0.01 {
(self.batch_size as f64 * speed_multiplier / 0.01)
.max(1.0)
.ceil() as u32
} else {
self.batch_size
};
let batch_virtual_ns = tick_ns * effective_batch as u64;
// Calculate how long this batch should take in wall-clock time
// wall_time = virtual_time / speed_multiplier
// Use .max(1.0) to guard against underflow to 0 at extreme multipliers.
let target_wall_nanos = (batch_virtual_ns as f64 / speed_multiplier).max(1.0).round() as u64;
let target_wall = Duration::from_nanos(target_wall_nanos);
let start = Instant::now();
// Advance virtual time by effective_batch ticks
for _ in 0..effective_batch {
if !self.clock.is_running() || self.clock.is_paused() {
return;
}
self.clock.advance_one_tick();
}
let elapsed = start.elapsed();
// Sleep to match target wall-clock time
if elapsed < target_wall {
let sleep_duration = target_wall - elapsed;
thread::sleep(sleep_duration);
}
// If elapsed >= target_wall, we're running behind (CPU too slow)
// Just continue — the governor will report actual speed
}
/// MaxSpeed mode: advance virtual time as fast as possible.
///
/// Batches ticks and yields to other threads periodically.
fn run_maxspeed_batch(&self) {
for _ in 0..self.batch_size {
if !self.clock.is_running() || self.clock.is_paused() {
return;
}
self.clock.advance_one_tick();
}
// Yield to prevent starving component threads and GUI
thread::yield_now();
}
/// SingleStep mode: advance one tick when triggered by user.
fn run_single_step(&self) {
if self.clock.consume_step() {
self.clock.advance_one_tick();
} else {
// No step pending — sleep briefly to avoid busy-waiting
thread::sleep(Duration::from_micros(100));
}
}
/// Take a non-blocking snapshot of wall-clock and virtual time.
///
/// Take two snapshots spaced apart in time, then call
/// [`compute_speed`] to calculate the actual achieved speed multiplier.
/// This is safe to call from any thread, including the GUI thread.
///
/// # Example
///
/// ```rust,ignore
/// let snap1 = RealTimeGovernor::take_speed_snapshot(&clock);
/// // ... wait some time (e.g., next GUI frame) ...
/// let snap2 = RealTimeGovernor::take_speed_snapshot(&clock);
/// let actual_speed = RealTimeGovernor::compute_speed(&snap1, &snap2);
/// // Display: format!("Actual: {:.2}×", actual_speed)
/// ```
pub fn take_speed_snapshot(clock: &VirtualClock) -> SpeedSnapshot {
SpeedSnapshot {
wall: Instant::now(),
virtual_ns: clock.now_ns(),
}
}
/// Compute the actual speed multiplier from two snapshots.
///
/// Returns the ratio of virtual time elapsed to wall-clock time elapsed.
/// A value of 1.0 means real-time, 0.5 means half speed, 2.0 means double speed.
///
/// Returns 0.0 if the snapshots were taken too close together in wall-clock time
/// to produce a meaningful measurement.
pub fn compute_speed(before: &SpeedSnapshot, after: &SpeedSnapshot) -> f64 {
let wall_nanos = after.wall.duration_since(before.wall).as_nanos() as f64;
if wall_nanos == 0.0 {
return 0.0;
}
// Use saturating_sub to handle misordered snapshots gracefully (returns 0).
let virtual_delta = after.virtual_ns.saturating_sub(before.virtual_ns) as f64;
virtual_delta / wall_nanos
}
}
/// A point-in-time snapshot of wall-clock and virtual time, used for speed measurement.
///
/// See [`RealTimeGovernor::take_speed_snapshot`] and [`RealTimeGovernor::compute_speed`].
#[derive(Debug, Clone)]
pub struct SpeedSnapshot {
pub(crate) wall: Instant,
pub(crate) virtual_ns: u64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_governor_maxspeed() {
let clock = Arc::new(VirtualClock::new(Duration::from_nanos(333)));
let governor = RealTimeGovernor::with_defaults(clock.clone());
// Run governor in background
let _clock_clone = Arc::clone(&clock);
let handle = thread::spawn(move || {
governor.run();
});
// Let it run for a bit
thread::sleep(Duration::from_millis(100));
// Virtual time should have advanced significantly
let vt = clock.now_ns();
assert!(vt > 0, "Virtual time should have advanced");
// Stop and wait for clean exit
clock.stop();
let _ = handle.join();
}
#[test]
fn test_governor_realtime_slow() {
let clock = Arc::new(VirtualClock::new(Duration::from_nanos(333)));
clock.set_speed_multiplier(0.001); // Very slow — easy to measure
let governor = RealTimeGovernor::new(
Arc::clone(&clock),
10, // Small batch for more frequent sync
);
let handle = thread::spawn(move || governor.run());
// Let it run for 100ms
thread::sleep(Duration::from_millis(100));
let vt = clock.now_ns();
// At 0.001x speed, 100ms wall time = 100µs virtual time
// With some tolerance for overhead
let expected_max = Duration::from_millis(100).as_nanos() as f64 * 0.001 * 2.0;
assert!(
(vt as f64) < expected_max,
"Virtual time should be much less than wall time at 0.001x speed. Got {}ns, expected < {}ns",
vt, expected_max
);
clock.stop();
let _ = handle.join();
}
#[test]
fn test_governor_single_step() {
let clock = Arc::new(VirtualClock::new(Duration::from_nanos(333)));
clock.set_mode(SimulationMode::SingleStep);
let governor = RealTimeGovernor::with_defaults(clock.clone());
let _clock_clone = Arc::clone(&clock);
let handle = thread::spawn(move || governor.run());
// Time should not advance without stepping
thread::sleep(Duration::from_millis(50));
assert_eq!(clock.now_ns(), 0, "Virtual time should not advance without stepping");
// Trigger a step
clock.trigger_step();
thread::sleep(Duration::from_millis(50));
assert_eq!(clock.now_ns(), 333, "Virtual time should advance by one tick after step");
// Trigger another step
clock.trigger_step();
thread::sleep(Duration::from_millis(50));
assert_eq!(clock.now_ns(), 666, "Virtual time should advance by another tick");
clock.stop();
let _ = handle.join();
}
#[test]
fn test_governor_pause_resume() {
let clock = Arc::new(VirtualClock::new(Duration::from_nanos(333)));
let governor = RealTimeGovernor::with_defaults(clock.clone());
let handle = thread::spawn(move || governor.run());
// Let it run
thread::sleep(Duration::from_millis(50));
let vt_before_pause = clock.now_ns();
assert!(vt_before_pause > 0);
// Pause
clock.pause();
thread::sleep(Duration::from_millis(50));
let vt_after_pause = clock.now_ns();
// Let it stay paused
thread::sleep(Duration::from_millis(50));
let vt_still_paused = clock.now_ns();
// Time should not have advanced significantly while paused
// (may advance by 1 tick due to race, but not more)
assert!(
vt_still_paused <= vt_after_pause + 1000,
"Virtual time should not advance while paused"
);
// Resume
clock.resume();
thread::sleep(Duration::from_millis(50));
let vt_after_resume = clock.now_ns();
assert!(
vt_after_resume > vt_still_paused,
"Virtual time should advance after resume"
);
clock.stop();
let _ = handle.join();
}
}

View file

@ -53,6 +53,8 @@ use crate::component::Component;
use crate::components::cpu::intel_4004::Intel4004;
use crate::components::memory::intel_4001::Intel4001;
use crate::components::memory::intel_4002::Intel4002;
use crate::real_time_governor::RealTimeGovernor;
use crate::virtual_clock::VirtualClock;
use parking_lot::Mutex;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
@ -459,9 +461,31 @@ pub struct ConfigurableSystem {
clock_frequency: Arc<Mutex<f64>>,
clock_enabled: Arc<Mutex<bool>>,
component_handles: Arc<Mutex<Option<ComponentHandles>>>,
/// Shared virtual clock for timing-synchronized simulation.
/// All components and the RealTimeGovernor share this clock.
virtual_clock: Arc<VirtualClock>,
/// Handle for the governor thread so we can join it on shutdown.
governor_handle: Arc<Mutex<Option<std::thread::JoinHandle<()>>>>,
}
impl ConfigurableSystem {
/// Derive the VirtualClock tick size from the configured clock frequency.
///
/// For a two-phase clock at frequency F:
/// - Full cycle = 1/F seconds
/// - Each phase = 1/(2F) seconds
/// - Phase transition (quarter cycle) = 1/(4F) seconds = tick_size
///
/// For a 750 kHz clock: tick_size = 1/(4 × 750000) ≈ 333ns
fn derive_tick_size(frequency: f64) -> Duration {
if frequency <= 0.0 {
return Duration::from_nanos(333); // Safe default
}
// tick_size = 1 / (4 * frequency) in seconds, converted to nanoseconds
let tick_ns = (1_000_000_000.0 / (4.0 * frequency)).round() as u64;
Duration::from_nanos(tick_ns.max(1)) // At least 1ns
}
pub fn new(
config: SystemConfig,
components: HashMap<String, Arc<Mutex<Box<dyn Component>>>>,
@ -473,6 +497,16 @@ impl ConfigurableSystem {
.and_then(|v| v.as_f64())
.unwrap_or(750000.0);
// Create shared VirtualClock with tick size derived from clock frequency
let tick_size = Self::derive_tick_size(initial_frequency);
let virtual_clock = Arc::new(VirtualClock::new(tick_size));
println!(
"VirtualClock created: tick_size={}ns (from {} Hz clock)",
tick_size.as_nanos(),
initial_frequency
);
ConfigurableSystem {
config,
components,
@ -481,19 +515,43 @@ impl ConfigurableSystem {
clock_frequency: Arc::new(Mutex::new(initial_frequency)),
clock_enabled: Arc::new(Mutex::new(true)),
component_handles: Arc::new(Mutex::new(None)),
virtual_clock,
governor_handle: Arc::new(Mutex::new(None)),
}
}
/// Inject the shared VirtualClock into a component via the `Component` trait.
///
/// Uses `set_virtual_clock_optional()` which each component can override.
/// Components that support the virtual clock timing model store the clock
/// and use it for timing. Components that don't override the method simply
/// ignore the clock and fall back to legacy `thread::sleep()` timing in
/// their `run()` method.
///
/// New component types automatically work without modifying this method —
/// they just need to override `set_virtual_clock_optional()` in their
/// `impl Component` block.
fn inject_virtual_clock(component: &mut Box<dyn Component>, clock: Arc<VirtualClock>) {
component.set_virtual_clock_optional(Some(clock));
}
/// Start the system without blocking.
///
/// This method spawns component threads and returns immediately,
/// allowing the caller to retain access to the system via the mutex.
/// The system will continue running in the background until `stop()` is called.
pub fn start(&mut self) {
///
/// # Phase 7: VirtualClock Integration
///
/// 1. Injects `Arc<VirtualClock>` into all components via `set_virtual_clock()`
/// 2. Spawns a `RealTimeGovernor` thread that advances virtual time
/// 3. Spawns component threads that call `comp.run()` — each component's
/// own `run()` method handles VirtualClock-based synchronization
pub fn start(&mut self) -> Result<(), String> {
let mut is_running = self.is_running.lock();
if *is_running {
println!("System is already running");
return;
return Ok(());
}
*is_running = true;
drop(is_running); // Release lock before spawning threads
@ -501,75 +559,153 @@ impl ConfigurableSystem {
println!("Starting configurable system: {}", self.config.name);
println!("Description: {}", self.config.description);
let mut handles = vec![];
let is_running_clone = Arc::clone(&self.is_running);
let is_paused_clone = Arc::clone(&self.is_paused);
// ── Phase 7 Step 1: Inject VirtualClock into all components ──
// Skip injection if the clock is already set (e.g., after stop/restart cycle).
// This avoids unnecessary work and prevents potential issues if a component's
// set_virtual_clock_optional() has side effects beyond simple storage.
let clock_clone = Arc::clone(&self.virtual_clock);
for (name, component) in &self.components {
let mut comp = component.lock();
if comp.has_virtual_clock() {
println!("VirtualClock already injected into component: {} (skipped)", name);
} else {
Self::inject_virtual_clock(&mut comp, Arc::clone(&clock_clone));
println!("Injected VirtualClock into component: {}", name);
}
}
let clock_frequency_clone = Arc::clone(&self.clock_frequency);
// ── Phase 7 Step 2: Spawn RealTimeGovernor thread ──
let governor = RealTimeGovernor::with_defaults(Arc::clone(&self.virtual_clock));
let gov_handle = std::thread::Builder::new()
.name("governor".to_string())
.spawn(move || governor.run())
.map_err(|e| {
// Clean up is_running flag so the system can be retried.
// Do NOT call virtual_clock.stop() here — the governor calls
// clock.start() inside run(), so the clock was never started.
// Calling stop() would set the internal `stopped` flag, which
// would cause subsequent wait_for_start() calls to return false,
// making start() non-retriable.
*self.is_running.lock() = false;
format!("Failed to spawn governor thread: {}", e)
})?;
// Store governor handle for clean shutdown
{
let mut gh = self.governor_handle.lock();
*gh = Some(gov_handle);
}
// ── Phase 7 Step 3: Spawn component threads ──
//
// Thread architecture: The component lock is RELEASED while waiting for
// the VirtualClock, so the GUI can read component state between cycles.
//
// Per-cycle loop:
// 1. Release lock → call vclock.wait_until(next_tick)
// 2. Re-acquire lock → call comp.update_at_time(vtime)
// 3. Release lock (implicit at end of iteration)
//
// This ensures the GUI thread can acquire component locks for state reads
// while the simulation thread is blocked waiting for virtual time to advance.
let vclock_for_threads = Arc::clone(&self.virtual_clock);
let mut handles: Vec<(String, std::thread::JoinHandle<()>)> = vec![];
for (name, component) in &self.components {
let comp_clone = Arc::clone(component);
let running_clone = Arc::clone(&is_running_clone);
let paused_clone = Arc::clone(&is_paused_clone);
let frequency_clone = Arc::clone(&clock_frequency_clone);
let vclock_clone = Arc::clone(&vclock_for_threads);
let thread_name = name.clone();
let handle = std::thread::spawn(move || {
// Run loop that doesn't hold the lock continuously
// This allows other threads (like cleanup) to access the component
loop {
// Check system-level running flag first
let should_continue = *running_clone.lock();
if !should_continue {
break;
let handle = std::thread::Builder::new()
.name(format!("comp_{}", thread_name))
.spawn(move || {
// Wait for the governor to start advancing virtual time.
// This prevents the component from reporting "running" before
// the simulation timeline has actually started, and avoids a
// race where the GUI reads component state between set_running(true)
// and the first update_at_time() call.
if !vclock_clone.wait_for_start() {
// Simulation was stopped before starting — exit cleanly
comp_clone.lock().stop();
return;
}
// Check for pause state - skip update but keep thread alive
let is_paused = *paused_clone.lock();
// Mark component as running now that the governor is active.
// Also cache the timing profile — it's derived from the component's
// hardware type and doesn't change at runtime. Reading it once here
// avoids acquiring the component lock every cycle just for timing info,
// which would contend with the GUI thread.
let base_interval = {
let mut comp = comp_clone.lock();
comp.set_running(true);
let timing = comp.timing();
timing.min_update_interval.max(vclock_clone.tick_size())
};
if is_paused {
// While paused, sleep longer to reduce CPU usage
std::thread::sleep(std::time::Duration::from_millis(10));
continue;
}
loop {
// Release lock while waiting for next tick — this is the key
// to allowing GUI access. The VirtualClock condvar wait does
// not hold the component's parking_lot mutex.
//
// Use the cached base_interval (read once at thread startup) to
// determine how often the component needs to wake up. This avoids
// waking components more frequently than necessary (e.g., a CPU
// that only needs to react to clock phase transitions every ~666ns
// doesn't need to wake every ~333ns tick).
//
// In MaxSpeed mode, batch multiple ticks per wake to reduce
// per-tick overhead (lock acquisition, timing calculation). The
// batch factor of 10 means each wake covers ~10 ticks of virtual
// time, which is a good balance between responsiveness and throughput.
let wake_interval = if vclock_clone.mode() == crate::virtual_clock::SimulationMode::MaxSpeed {
base_interval * 10
} else {
base_interval
};
let next_tick = vclock_clone.now() + wake_interval;
let wait_result = vclock_clone.wait_until(next_tick);
// Re-acquire lock ONLY for the update cycle
let mut comp = comp_clone.lock();
// Use immediate try_lock() for component threads
// This ensures simulation threads never wait for GUI
// GUI uses blocking locks and updates at 100 Hz
if let Some(mut comp) = comp_clone.try_lock() {
// Ensure component is in running state before calling update()
// This is needed because many components check is_running() at the start
// of update() and return early if false
if !comp.is_running() {
comp.set_running(true);
break;
}
// Call update() for one simulation cycle
comp.update();
match wait_result {
Some(current_vtime) => {
comp.update_at_time(current_vtime);
}
None => {
// VirtualClock was stopped — exit thread
break;
}
}
// Lock is released here when `comp` is dropped at end of iteration
}
// If lock is held by GUI, skip this cycle and try again immediately
// This prioritizes simulation performance over GUI updates
// Adaptive sleep based on clock frequency
// For high-frequency clocks (>100kHz), use yield instead of sleep
// For lower frequencies, calculate appropriate sleep duration
let frequency = *frequency_clone.lock();
if frequency > 100_000.0 {
// High frequency: use yield for better responsiveness
std::thread::yield_now();
} else {
// Lower frequency: calculate sleep based on period
// Sleep for 1/10 of the clock period to balance responsiveness and CPU usage
let sleep_micros =
(1_000_000.0 / frequency / 10.0).min(1000.0).max(1.0) as u64;
std::thread::sleep(std::time::Duration::from_micros(sleep_micros));
// Clean up: stop the component
comp_clone.lock().stop();
})
.map_err(|e| {
// Clean up: stop everything already spawned.
// 1. Stop the VirtualClock — this unblocks the governor and
// any already-spawned component threads waiting in wait_for_start().
self.virtual_clock.stop();
*self.is_running.lock() = false;
// 2. Join already-spawned component threads to prevent leaks.
// Use std::mem::take to drain the vec without moving it.
for (_, h) in std::mem::take(&mut handles) {
let _ = h.join();
}
}
// Stop the component after loop exits
comp_clone.lock().stop();
});
// 3. Join the governor thread.
if let Some(mut gh) = self.governor_handle.try_lock_for(Duration::from_millis(500)) {
if let Some(gov_handle) = gh.take() {
let _ = gov_handle.join();
}
}
format!("Failed to spawn thread for component '{}': {}", name, e)
})?;
handles.push((name.clone(), handle));
}
@ -581,6 +717,7 @@ impl ConfigurableSystem {
}
println!("All components started. System running in background...");
Ok(())
}
/// Run the system (blocking version for backward compatibility).
@ -590,7 +727,10 @@ impl ConfigurableSystem {
/// which can cause issues with console/GUI access. Use `start()` instead
/// for non-blocking operation.
pub fn run(&mut self) {
self.start();
if let Err(e) = self.start() {
eprintln!("Failed to start system: {}", e);
return;
}
// Monitor system state during execution
self.monitor_system_state();
@ -600,8 +740,17 @@ impl ConfigurableSystem {
}
/// Clean up component threads and stop all components.
///
/// Phase 7: Stops the VirtualClock first, which unblocks all component threads
/// waiting in `wait_until()` / `wait_for_start()`. Then joins all threads.
fn cleanup(&mut self) {
// Signal stop first - this tells all component threads to stop
// ── Phase 7: Stop VirtualClock first ──
// This wakes all component threads blocked in wait_until() / wait_for_start(),
// causing them to exit their run loops. Also stops the governor.
println!("Stopping VirtualClock...");
self.virtual_clock.stop();
// Signal stop via the legacy flag too (for components without VirtualClock)
{
let mut is_running = self.is_running.lock();
*is_running = false;
@ -609,23 +758,22 @@ impl ConfigurableSystem {
// Stop all components using try_lock_for to avoid deadlock
// If a component is busy (its thread holds the lock), wait briefly
// - the thread will exit when it checks the is_running flag
// - the thread will exit when it sees VirtualClock stopped
println!("\nStopping system components...");
for (name, component) in &self.components {
if let Some(mut comp) = component.try_lock_for(Duration::from_millis(100)) {
comp.stop();
println!("Stopped component: {}", name);
} else {
// Component is busy running - it will stop when is_running becomes false
println!("Component {} is busy, will stop via is_running flag", name);
// Component is busy running - it will stop when VirtualClock stops
println!("Component {} is busy, will stop via VirtualClock", name);
}
}
// Wait for threads to finish
// Note: join() blocks indefinitely - threads should respond to is_running flag
// Wait for component threads to finish
if let Some(mut handles_guard) = self
.component_handles
.try_lock_for(Duration::from_millis(100))
.try_lock_for(Duration::from_millis(500))
{
if let Some(handles) = handles_guard.take() {
for (name, handle) in handles {
@ -637,6 +785,28 @@ impl ConfigurableSystem {
}
}
// ── Phase 7: Join governor thread ──
if let Some(mut gh) = self.governor_handle.try_lock_for(Duration::from_millis(100)) {
if let Some(gov_handle) = gh.take() {
match gov_handle.join() {
Ok(_) => println!("Governor thread finished"),
Err(_) => eprintln!("Governor thread panicked"),
}
}
}
// ── Reset VirtualClock for clean restart ──
// All threads have been joined, so it's safe to reset.
// This resets time to zero, speed to 1.0×, mode to RealTime,
// and clears pause/step state. tick_size is preserved.
//
// SAFETY: This method must only be called while holding the external
// ConfigurableSystem mutex (parking_lot::Mutex). The mutex ensures
// no concurrent start() call can interleave with reset(). If start()
// were called between thread joins and reset(), the newly started
// simulation would be immediately killed by reset() setting running=false.
self.virtual_clock.reset();
println!("Configurable system stopped.");
}
@ -763,6 +933,14 @@ impl ConfigurableSystem {
&self.components
}
/// Get a reference to the shared VirtualClock.
///
/// This allows external code (GUI, tests) to read virtual time,
/// change speed/mode, or trigger single steps.
pub fn get_virtual_clock(&self) -> &Arc<VirtualClock> {
&self.virtual_clock
}
pub fn get_system_info(&self) -> SystemInfo {
let rom_size = self
.config
@ -946,8 +1124,7 @@ impl SystemController for ConfigurableSystem {
}
// Call the inherent method explicitly to avoid infinite recursion
ConfigurableSystem::start(self);
Ok(())
ConfigurableSystem::start(self)
}
fn stop(&mut self) -> Result<(), String> {
@ -984,12 +1161,14 @@ impl SystemController for ConfigurableSystem {
*paused = true;
}
// Propagate pause to ALL components (clocks, CPU, memory)
// The system-level is_paused flag handles the thread-level pause,
// but we also propagate to components for component-specific pause behavior
// ── Phase 7: Pause VirtualClock ──
// This efficiently blocks all component threads in their wait_until() calls
// (zero CPU usage via condvar). Much better than the old polling approach.
self.virtual_clock.pause();
// Also propagate pause to clock components for component-specific behavior
for (name, component) in &self.components {
let mut comp = component.lock();
// Try to downcast to GenericClock
if let Some(clock) =
comp.as_any_mut()
.downcast_mut::<crate::components::clock::generic_clock::GenericClock>()
@ -997,7 +1176,6 @@ impl SystemController for ConfigurableSystem {
clock.pause();
println!("Clock {} paused", name);
}
// Try to downcast to TwoPhaseClock
else if let Some(clock) =
comp.as_any_mut()
.downcast_mut::<crate::components::clock::two_phase_clock::TwoPhaseClock>()
@ -1005,9 +1183,6 @@ impl SystemController for ConfigurableSystem {
clock.pause();
println!("Clock {} paused", name);
}
// Note: CPU and memory components don't have explicit pause methods,
// but they are effectively paused by the system-level is_paused flag
// which prevents their update() from being called in the thread loop
}
println!("System paused - all components frozen");
@ -1026,10 +1201,13 @@ impl SystemController for ConfigurableSystem {
*paused = false;
}
// Propagate resume to ALL components (clocks, CPU, memory)
// ── Phase 7: Resume VirtualClock ──
// This wakes all component threads blocked in wait_until().
self.virtual_clock.resume();
// Also propagate resume to clock components
for (name, component) in &self.components {
let mut comp = component.lock();
// Try to downcast to GenericClock
if let Some(clock) =
comp.as_any_mut()
.downcast_mut::<crate::components::clock::generic_clock::GenericClock>()
@ -1037,7 +1215,6 @@ impl SystemController for ConfigurableSystem {
clock.resume();
println!("Clock {} resumed", name);
}
// Try to downcast to TwoPhaseClock
else if let Some(clock) =
comp.as_any_mut()
.downcast_mut::<crate::components::clock::two_phase_clock::TwoPhaseClock>()
@ -1045,8 +1222,6 @@ impl SystemController for ConfigurableSystem {
clock.resume();
println!("Clock {} resumed", name);
}
// Note: CPU and memory components resume automatically via the
// system-level is_paused flag being cleared in the thread loop
}
println!("System resumed - all components running");

1025
src/virtual_clock.rs Normal file

File diff suppressed because it is too large Load diff