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

@ -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 {}