From 08f53d68fee860d5f143295f2ccc70ed78a8440b Mon Sep 17 00:00:00 2001 From: Jan-Peter von Hunnius Date: Thu, 14 May 2026 21:00:22 +0200 Subject: [PATCH] feat: implement virtual clock timing architecture (Phases 5-8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- plans/phase5_clock_migration.md | 898 ++++++++++++++++++++ plans/phase6_timing_enforcement.md | 870 +++++++++++++++++++ plans/phase7_system_integration.md | 174 ++++ plans/phase8_gui_controls.md | 117 +++ plans/phase9_tests.md | 47 ++ plans/timing_architecture.md | 603 +++++++++++++ src/component.rs | 63 ++ src/component_timing.rs | 470 +++++++++++ src/components/clock/generic_clock.rs | 164 +++- src/components/clock/two_phase_clock.rs | 140 +++- src/components/common/intel_400x.rs | 173 +++- src/components/cpu/intel_4004.rs | 100 ++- src/components/memory/intel_4001.rs | 135 ++- src/components/memory/intel_4002.rs | 130 ++- src/components/memory/intel_4003.rs | 117 ++- src/gui/components.rs | 206 +++++ src/gui/console.rs | 148 +++- src/gui/mod.rs | 4 + src/gui/state.rs | 156 ++++ src/lib.rs | 3 + src/main.rs | 10 +- src/real_time_governor.rs | 387 +++++++++ src/system_config.rs | 333 ++++++-- src/virtual_clock.rs | 1025 +++++++++++++++++++++++ tests/integration_tests.rs | 4 +- tests/intel_400x_tests.rs | 39 +- tests/lib.rs | 52 ++ tests/virtual_time_tests.rs | 888 ++++++++++++++++++++ 28 files changed, 7186 insertions(+), 270 deletions(-) create mode 100644 plans/phase5_clock_migration.md create mode 100644 plans/phase6_timing_enforcement.md create mode 100644 plans/phase7_system_integration.md create mode 100644 plans/phase8_gui_controls.md create mode 100644 plans/phase9_tests.md create mode 100644 plans/timing_architecture.md create mode 100644 src/component_timing.rs create mode 100644 src/real_time_governor.rs create mode 100644 src/virtual_clock.rs create mode 100644 tests/virtual_time_tests.rs diff --git a/plans/phase5_clock_migration.md b/plans/phase5_clock_migration.md new file mode 100644 index 0000000..93aaf2a --- /dev/null +++ b/plans/phase5_clock_migration.md @@ -0,0 +1,898 @@ +# Phase 5: Migrate Clock Components to Virtual Time + +## Overview + +Phase 5 replaces `Instant::now()` and `thread::sleep()` in both clock components with virtual time from `Arc`. This is the first phase that modifies existing components rather than adding new infrastructure. + +**Files modified:** +- [`generic_clock.rs`](src/components/clock/generic_clock.rs) — GenericClock +- [`two_phase_clock.rs`](src/components/clock/two_phase_clock.rs) — TwoPhaseClock +- [`system_config.rs`](src/system_config.rs) — Clock factory closures (minor) + +**Files NOT modified** (Phase 7 responsibility): +- [`virtual_clock.rs`](src/virtual_clock.rs) — Already complete +- [`real_time_governor.rs`](src/real_time_governor.rs) — Already complete +- [`component.rs`](src/component.rs) — Already has `update_at_time()` default + +--- + +## Implementation Order + +**Migrate `GenericClock` first, then `TwoPhaseClock`.** + +Rationale: +1. `GenericClock` is simpler — single-phase, single CLK output +2. `TwoPhaseClock` has dual outputs (PHI1/PHI2) and inter-phase dependencies +3. Both follow the same pattern, so GenericClock establishes the template +4. `GenericClock` has existing unit tests to validate the migration + +--- + +## Part A: GenericClock Migration + +### A1. Current Timing Mechanism + +[`GenericClock`](src/components/clock/generic_clock.rs:11) uses wall-clock time in 5 locations: + +| Location | Line | Current Code | Purpose | +|----------|------|-------------|---------| +| [`last_transition`](src/components/clock/generic_clock.rs:16) field | 16 | `last_transition: Instant` | Track when last phase change occurred | +| [`pause_start`](src/components/clock/generic_clock.rs:21) field | 21 | `pause_start: Option` | Track when pause began | +| [`new()`](src/components/clock/generic_clock.rs:34) constructor | 34 | `last_transition: Instant::now()` | Initialize transition timer | +| [`enable()`](src/components/clock/generic_clock.rs:68) | 68 | `self.last_transition = Instant::now()` | Reset timer on enable | +| [`pause()`](src/components/clock/generic_clock.rs:80) | 80 | `self.pause_start = Some(Instant::now())` | Record pause start | +| [`resume()`](src/components/clock/generic_clock.rs:88) | 88 | `pause_start.elapsed()` | Calculate pause duration | +| [`should_transition()`](src/components/clock/generic_clock.rs:131) | 131 | `self.last_transition.elapsed()` | Check if phase duration elapsed | +| [`perform_transition()`](src/components/clock/generic_clock.rs:149) | 149 | `self.last_transition = Instant::now()` | Record transition time | +| [`run()`](src/components/clock/generic_clock.rs:205) | 205 | `thread::sleep(sleep_time)` | Busy-wait loop with sleep | + +### A2. Required Changes + +#### A2.1 — Add `VirtualClock` field (optional, gradual integration) + +Replace `Instant`-based fields with virtual-time equivalents. Add `Option>` for gradual integration — clocks work without a VirtualClock (falling back to wall-clock), but use virtual time when one is injected. + +```rust +// BEFORE (line 11-22): +pub struct GenericClock { + base: BaseComponent, + frequency: f64, + duty_cycle: f64, + current_state: PinValue, + last_transition: Instant, + high_time: Duration, + low_time: Duration, + enabled: bool, + paused: bool, + pause_start: Option, +} + +// AFTER: +pub struct GenericClock { + base: BaseComponent, + frequency: f64, + duty_cycle: f64, + current_state: PinValue, + last_transition_vtime: Duration, // Virtual time of last transition + high_time: Duration, + low_time: Duration, + enabled: bool, + paused: bool, + pause_start_vtime: Option, // Virtual time when pause started + virtual_clock: Option>, // Injected in Phase 7 +} +``` + +**Key change:** `last_transition: Instant` → `last_transition_vtime: Duration` (virtual nanoseconds since sim start). `pause_start: Option` → `pause_start_vtime: Option`. + +#### A2.2 — Update constructor + +```rust +// BEFORE (line 29-43): +let mut clock = GenericClock { + base: BaseComponent::new(name, pins), + frequency, + duty_cycle: 0.5, + current_state: PinValue::Low, + last_transition: Instant::now(), + high_time: Duration::from_secs_f64(0.5 / frequency), + low_time: Duration::from_secs_f64(0.5 / frequency), + enabled: true, + paused: false, + pause_start: None, +}; + +// AFTER: +let mut clock = GenericClock { + base: BaseComponent::new(name, pins), + frequency, + duty_cycle: 0.5, + current_state: PinValue::Low, + last_transition_vtime: Duration::ZERO, // Virtual time 0 + high_time: Duration::from_secs_f64(0.5 / frequency), + low_time: Duration::from_secs_f64(0.5 / frequency), + enabled: true, + paused: false, + pause_start_vtime: None, + virtual_clock: None, // Set later via set_virtual_clock() +}; +``` + +#### A2.3 — Add `set_virtual_clock()` method + +New method for Phase 7 injection: + +```rust +/// Inject a VirtualClock for virtual-time-based operation. +/// If not set, the clock falls back to wall-clock Instant-based timing. +/// This method is called by ConfigurableSystem in Phase 7. +pub fn set_virtual_clock(&mut self, clock: Arc) { + self.virtual_clock = Some(clock); +} +``` + +#### A2.4 — Update `enable()` method (line 64-69) + +```rust +// BEFORE: +pub fn enable(&mut self) { + self.enabled = true; + self.current_state = PinValue::Low; + self.last_transition = Instant::now(); +} + +// AFTER: +pub fn enable(&mut self) { + self.enabled = true; + self.current_state = PinValue::Low; + self.last_transition_vtime = self.current_virtual_time(); +} +``` + +Where `current_virtual_time()` is a new helper: + +```rust +/// Get current virtual time, or fall back to Duration::ZERO if no VirtualClock. +fn current_virtual_time(&self) -> Duration { + self.virtual_clock.as_ref().map_or(Duration::ZERO, |vc| vc.now()) +} +``` + +#### A2.5 — Update `pause()` and `resume()` (lines 78-94) + +```rust +// BEFORE: +pub fn pause(&mut self) { + self.paused = true; + self.pause_start = Some(Instant::now()); +} + +pub fn resume(&mut self) { + if let Some(pause_start) = self.pause_start { + let pause_duration = pause_start.elapsed(); + self.last_transition += pause_duration; + } + self.paused = false; + self.pause_start = None; +} + +// AFTER: +pub fn pause(&mut self) { + self.paused = true; + self.pause_start_vtime = Some(self.current_virtual_time()); +} + +pub fn resume(&mut self) { + if let Some(pause_start_vtime) = self.pause_start_vtime { + let pause_duration = self.current_virtual_time().saturating_sub(pause_start_vtime); + self.last_transition_vtime = self.last_transition_vtime.saturating_add(pause_duration); + } + self.paused = false; + self.pause_start_vtime = None; +} +``` + +#### A2.6 — Update `should_transition()` (line 130-138) + +```rust +// BEFORE: +fn should_transition(&self) -> bool { + let elapsed = self.last_transition.elapsed(); + match self.current_state { + PinValue::High => elapsed >= self.high_time, + PinValue::Low => elapsed >= self.low_time, + PinValue::HighZ => true, + } +} + +// AFTER: +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, + PinValue::Low => elapsed >= self.low_time, + PinValue::HighZ => true, + } +} +``` + +**Note:** `should_transition()` now takes `virtual_time: Duration` as a parameter instead of computing elapsed from `Instant`. + +#### A2.7 — Update `perform_transition()` (line 140-150) + +```rust +// BEFORE: +fn perform_transition(&mut self) { + let new_state = match self.current_state { + PinValue::High => PinValue::Low, + PinValue::Low => PinValue::High, + PinValue::HighZ => PinValue::Low, + }; + self.current_state = new_state; + self.set_clock_output(new_state); + self.last_transition = Instant::now(); +} + +// AFTER: +fn perform_transition(&mut self, virtual_time: Duration) { + let new_state = match self.current_state { + PinValue::High => PinValue::Low, + PinValue::Low => PinValue::High, + PinValue::HighZ => PinValue::Low, + }; + self.current_state = new_state; + self.set_clock_output(new_state); + self.last_transition_vtime = virtual_time; +} +``` + +#### A2.8 — Implement `update_at_time()` (new, replaces `update()` for virtual-time path) + +Add the virtual-time-aware update method. This overrides the default from the `Component` trait: + +```rust +impl Component for GenericClock { + // ... existing methods unchanged ... + + fn update_at_time(&mut self, virtual_time: Duration) { + // Check pause state first - don't update if paused + if self.paused { + return; + } + + // Read external enable control + let external_enable = self.read_enable_pin(); + let should_be_enabled = self.enabled && external_enable; + + if !should_be_enabled { + if self.current_state != PinValue::Low { + self.set_clock_output(PinValue::Low); + self.current_state = PinValue::Low; + } + return; + } + + // Check if it's time to transition (using virtual time) + if self.should_transition(virtual_time) { + self.perform_transition(virtual_time); + } + } + + // Keep update() for backward compatibility — delegates to update_at_time + // using current virtual time (or Duration::ZERO if no VirtualClock) + fn update(&mut self) { + let vtime = self.current_virtual_time(); + self.update_at_time(vtime); + } +} +``` + +#### A2.9 — Rewrite `run()` to use `wait_until()` (line 190-210) + +This is the most significant change. Replace the `thread::sleep()` busy-loop with `virtual_clock.wait_until()`: + +```rust +// BEFORE: +fn run(&mut self) { + self.base.set_running(true); + self.enable(); + + while self.is_running() { + self.update(); + 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); + } + self.disable(); +} + +// AFTER: +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 => { + // Legacy path: use old Instant-based timing + return self.run_legacy(); + } + }; + + while self.is_running() { + // Calculate next transition time + let next_transition = self.next_transition_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 + } + } + self.disable(); +} +``` + +Add helper methods: + +```rust +impl GenericClock { + /// 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). + fn run_legacy(&mut self) { + while self.is_running() { + self.update(); + 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); + } + self.disable(); + } +} +``` + +#### A2.10 — Add `timing()` override + +```rust +impl Component for GenericClock { + // ... existing methods ... + + 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: f64::INFINITY, + min_update_interval: self.high_time.min(self.low_time), + } + } +} +``` + +#### A2.11 — Update imports + +```rust +// BEFORE (line 1-9): +use parking_lot::Mutex; +use std::any::Any; +use std::collections::HashMap; +use std::sync::Arc; +use std::thread; +use std::time::{Duration, Instant}; +use crate::component::{BaseComponent, Component}; +use crate::pin::{Pin, PinValue}; + +// AFTER: +use parking_lot::Mutex; +use std::any::Any; +use std::collections::HashMap; +use std::sync::Arc; +use std::thread; +use std::time::Duration; +use crate::component::{BaseComponent, Component}; +use crate::component_timing::ComponentTiming; +use crate::pin::{Pin, PinValue}; +use crate::virtual_clock::VirtualClock; +``` + +**Key change:** Remove `Instant` from imports, add `VirtualClock` and `ComponentTiming`. + +### A3. GenericClock Test Impact + +The existing tests in [`generic_clock.rs:238-258`](src/components/clock/generic_clock.rs:238) are: + +| Test | What it checks | Impact | +|------|---------------|--------| +| [`test_clock_creation`](src/components/clock/generic_clock.rs:243) | Name and running state | **No impact** — doesn't use `Instant` | +| [`test_clock_timing_calculation`](src/components/clock/generic_clock.rs:250) | `frequency_to_duration()` | **No impact** — static calculation | + +These tests will pass unchanged. No new tests needed for the migration itself — the existing tests validate construction and static calculations. Virtual-time behavior tests should be added in Phase 9. + +--- + +## Part B: TwoPhaseClock Migration + +### B1. Current Timing Mechanism + +[`TwoPhaseClock`](src/components/clock/two_phase_clock.rs:11) uses wall-clock time in 5 locations: + +| Location | Line | Current Code | Purpose | +|----------|------|-------------|---------| +| [`last_transition`](src/components/clock/two_phase_clock.rs:15) field | 15 | `last_transition: Instant` | Track when last phase swap occurred | +| [`pause_start`](src/components/clock/two_phase_clock.rs:19) field | 19 | `pause_start: Option` | Track when pause began | +| [`new()`](src/components/clock/two_phase_clock.rs:38) constructor | 38 | `last_transition: Instant::now()` | Initialize transition timer | +| [`enable()`](src/components/clock/two_phase_clock.rs:64) | 64 | `self.last_transition = Instant::now()` | Reset timer on enable | +| [`pause()`](src/components/clock/two_phase_clock.rs:80) | 80 | `self.pause_start = Some(Instant::now())` | Record pause start | +| [`resume()`](src/components/clock/two_phase_clock.rs:88) | 88 | `pause_start.elapsed()` | Calculate pause duration | +| [`should_transition()`](src/components/clock/two_phase_clock.rs:137) | 137 | `self.last_transition.elapsed()` | Check if phase_time elapsed | +| [`perform_transition()`](src/components/clock/two_phase_clock.rs:160) | 160 | `self.last_transition = Instant::now()` | Record transition time | +| [`run()`](src/components/clock/two_phase_clock.rs:203) | 203 | `thread::sleep(Duration::from_micros(100))` | Fixed 100µs busy-wait | + +### B2. Required Changes + +#### B2.1 — Add `VirtualClock` field (optional, gradual integration) + +```rust +// BEFORE (line 11-21): +pub struct TwoPhaseClock { + base: BaseComponent, + phi1_state: PinValue, + phi2_state: PinValue, + last_transition: Instant, + phase_time: Duration, + enabled: bool, + paused: bool, + pause_start: Option, + frequency: f64, +} + +// AFTER: +pub struct TwoPhaseClock { + base: BaseComponent, + phi1_state: PinValue, + phi2_state: PinValue, + last_transition_vtime: Duration, // Virtual time of last phase swap + phase_time: Duration, + enabled: bool, + paused: bool, + pause_start_vtime: Option, // Virtual time when pause started + frequency: f64, + virtual_clock: Option>, // Injected in Phase 7 +} +``` + +#### B2.2 — Update constructor (line 24-45) + +```rust +// BEFORE: +TwoPhaseClock { + base: BaseComponent::new(name, pins), + phi1_state: PinValue::High, + phi2_state: PinValue::Low, + last_transition: Instant::now(), + phase_time, + enabled: true, + paused: false, + pause_start: None, + frequency, +} + +// AFTER: +TwoPhaseClock { + base: BaseComponent::new(name, pins), + phi1_state: PinValue::High, + phi2_state: PinValue::Low, + last_transition_vtime: Duration::ZERO, + phase_time, + enabled: true, + paused: false, + pause_start_vtime: None, + frequency, + virtual_clock: None, +} +``` + +#### B2.3 — Add `set_virtual_clock()` and `current_virtual_time()` helpers + +```rust +impl TwoPhaseClock { + /// Inject a VirtualClock for virtual-time-based operation. + pub fn set_virtual_clock(&mut self, clock: Arc) { + self.virtual_clock = Some(clock); + } + + /// Get current virtual time, or Duration::ZERO if no VirtualClock. + fn current_virtual_time(&self) -> Duration { + self.virtual_clock.as_ref().map_or(Duration::ZERO, |vc| vc.now()) + } +} +``` + +#### B2.4 — Update `enable()` (line 60-69) + +```rust +// BEFORE: +pub fn enable(&mut self) { + self.enabled = true; + self.phi1_state = PinValue::High; + self.phi2_state = PinValue::Low; + self.last_transition = Instant::now(); + self.update_outputs(); + self.update_outputs(); +} + +// AFTER: +pub fn enable(&mut self) { + self.enabled = true; + self.phi1_state = PinValue::High; + self.phi2_state = PinValue::Low; + self.last_transition_vtime = self.current_virtual_time(); + self.update_outputs(); + self.update_outputs(); +} +``` + +#### B2.5 — Update `pause()` and `resume()` (lines 79-95) + +Same pattern as GenericClock: + +```rust +pub fn pause(&mut self) { + self.paused = true; + self.pause_start_vtime = Some(self.current_virtual_time()); +} + +pub fn resume(&mut self) { + if let Some(pause_start_vtime) = self.pause_start_vtime { + let pause_duration = self.current_virtual_time().saturating_sub(pause_start_vtime); + self.last_transition_vtime = self.last_transition_vtime.saturating_add(pause_duration); + } + self.paused = false; + self.pause_start_vtime = None; +} +``` + +#### B2.6 — Update `should_transition()` (line 136-138) + +```rust +// BEFORE: +fn should_transition(&self) -> bool { + self.last_transition.elapsed() >= self.phase_time +} + +// AFTER: +fn should_transition(&self, virtual_time: Duration) -> bool { + let elapsed = virtual_time.saturating_sub(self.last_transition_vtime); + elapsed >= self.phase_time +} +``` + +#### B2.7 — Update `perform_transition()` (line 140-161) + +```rust +// BEFORE: +fn perform_transition(&mut self) { + match (self.phi1_state, self.phi2_state) { + (PinValue::High, PinValue::Low) => { + self.phi1_state = PinValue::Low; + self.phi2_state = PinValue::High; + } + (PinValue::Low, PinValue::High) => { + self.phi1_state = PinValue::High; + self.phi2_state = PinValue::Low; + } + _ => { + self.phi1_state = PinValue::High; + self.phi2_state = PinValue::Low; + } + } + self.update_outputs(); + self.last_transition = Instant::now(); +} + +// AFTER: +fn perform_transition(&mut self, virtual_time: Duration) { + match (self.phi1_state, self.phi2_state) { + (PinValue::High, PinValue::Low) => { + self.phi1_state = PinValue::Low; + self.phi2_state = PinValue::High; + } + (PinValue::Low, PinValue::High) => { + self.phi1_state = PinValue::High; + self.phi2_state = PinValue::Low; + } + _ => { + self.phi1_state = PinValue::High; + self.phi2_state = PinValue::Low; + } + } + self.update_outputs(); + self.last_transition_vtime = virtual_time; +} +``` + +#### B2.8 — Implement `update_at_time()` and update `update()` + +```rust +impl Component for TwoPhaseClock { + // ... existing name/pins/get_pin unchanged ... + + fn update_at_time(&mut self, virtual_time: Duration) { + if self.paused { + return; + } + if !self.enabled { + return; + } + self.update_outputs(); + 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); + } + + // ... rest unchanged ... +} +``` + +#### B2.9 — Rewrite `run()` to use `wait_until()` (line 195-212) + +```rust +// BEFORE: +fn run(&mut self) { + self.base.set_running(true); + self.enable(); + while self.is_running() { + self.update(); + thread::sleep(Duration::from_micros(100)); + if !self.is_running() { + break; + } + } + self.disable(); +} + +// AFTER: +fn run(&mut self) { + self.base.set_running(true); + self.enable(); + + let vclock = match self.virtual_clock.as_ref() { + Some(vc) => Arc::clone(vc), + None => return self.run_legacy(), + }; + + while self.is_running() { + let next_transition = self.last_transition_vtime + self.phase_time; + match vclock.wait_until(next_transition) { + Some(current_vtime) => { + if !self.is_running() { + break; + } + self.update_at_time(current_vtime); + } + None => break, + } + } + self.disable(); +} +``` + +Add legacy helper: + +```rust +impl TwoPhaseClock { + /// Legacy run loop using wall-clock time (backward compatibility). + fn run_legacy(&mut self) { + while self.is_running() { + self.update(); + thread::sleep(Duration::from_micros(100)); + if !self.is_running() { + break; + } + } + self.disable(); + } +} +``` + +#### B2.10 — Add `timing()` override + +```rust +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: f64::INFINITY, + min_update_interval: self.phase_time, + } +} +``` + +#### B2.11 — Update imports + +```rust +// BEFORE (line 1-9): +use parking_lot::Mutex; +use std::any::Any; +use std::collections::HashMap; +use std::sync::Arc; +use std::thread; +use std::time::{Duration, Instant}; +use crate::component::{BaseComponent, Component}; +use crate::pin::{Pin, PinValue}; + +// AFTER: +use parking_lot::Mutex; +use std::any::Any; +use std::collections::HashMap; +use std::sync::Arc; +use std::thread; +use std::time::Duration; +use crate::component::{BaseComponent, Component}; +use crate::component_timing::ComponentTiming; +use crate::pin::{Pin, PinValue}; +use crate::virtual_clock::VirtualClock; +``` + +### B3. TwoPhaseClock Test Impact + +No tests exist for `TwoPhaseClock` (no `#[cfg(test)]` module in the file). No test breakage risk. + +--- + +## Part C: System Config Changes + +### C1. Factory Closure Updates + +In [`system_config.rs`](src/system_config.rs:241-277), the factory closures create clocks without a `VirtualClock`. In Phase 5, these remain unchanged — the `virtual_clock` field is `None` by default, and clocks use the legacy `run_legacy()` path. + +In **Phase 7**, `ConfigurableSystem` will: +1. Create `Arc` +2. After constructing each clock, call `set_virtual_clock(Arc::clone(&virtual_clock))` +3. This requires changing the factory closure signature or adding a post-construction hook + +For Phase 5, no changes to [`system_config.rs`](src/system_config.rs) are needed. + +--- + +## Part D: Backward Compatibility Analysis + +### D1. What stays the same + +| Aspect | Compatibility | +|--------|--------------| +| `new(name, frequency)` constructor signature | **Unchanged** — no new required parameters | +| `Component` trait impl | **Unchanged** — `update()` still works via delegation | +| `enable()` / `disable()` / `pause()` / `resume()` | **Unchanged** — public API identical | +| `set_frequency()` / `get_frequency()` | **Unchanged** | +| Pin names (CLK, PHI1, PHI2, ENABLE) | **Unchanged** | +| `run()` without VirtualClock | **Works** — falls back to `run_legacy()` with `thread::sleep()` | +| Existing tests | **Pass** — no Instant dependency in test assertions | + +### D2. What changes (internal only) + +| Change | Visible externally? | +|--------|-------------------| +| `last_transition: Instant` → `last_transition_vtime: Duration` | No — private field | +| `pause_start: Option` → `pause_start_vtime: Option` | No — private field | +| `should_transition()` takes `Duration` param | No — private method | +| `perform_transition()` takes `Duration` param | No — private method | +| New `virtual_clock: Option>` field | No — private, `None` by default | +| New `set_virtual_clock()` method | Yes — but opt-in, not required | + +### D3. Risk assessment + +**Risk: LOW.** All changes are internal field replacements. The public API is unchanged. The `Option>` pattern ensures clocks work identically without a VirtualClock injected. + +--- + +## Part E: Execution Checklist + +These are the implementation steps in order: + +- [ ] **E1.** Update `GenericClock` imports — remove `Instant`, add `VirtualClock` + `ComponentTiming` +- [ ] **E2.** Replace `GenericClock` struct fields: `last_transition: Instant` → `last_transition_vtime: Duration`, `pause_start: Option` → `pause_start_vtime: Option`, add `virtual_clock: Option>` +- [ ] **E3.** Update `GenericClock::new()` — initialize `last_transition_vtime: Duration::ZERO`, `virtual_clock: None` +- [ ] **E4.** Add `GenericClock::set_virtual_clock()` and `GenericClock::current_virtual_time()` helper methods +- [ ] **E5.** Update `GenericClock::enable()` — use `current_virtual_time()` instead of `Instant::now()` +- [ ] **E6.** Update `GenericClock::pause()` — use `current_virtual_time()` instead of `Instant::now()` +- [ ] **E7.** Update `GenericClock::resume()` — compute pause duration from virtual time deltas +- [ ] **E8.** Update `GenericClock::should_transition()` — accept `virtual_time: Duration` parameter +- [ ] **E9.** Update `GenericClock::perform_transition()` — accept `virtual_time: Duration`, store in `last_transition_vtime` +- [ ] **E10.** Implement `GenericClock::update_at_time()` in `Component` impl +- [ ] **E11.** Update `GenericClock::update()` to delegate to `update_at_time(current_virtual_time())` +- [ ] **E12.** Add `GenericClock::next_transition_time()` and `GenericClock::run_legacy()` helper methods +- [ ] **E13.** Rewrite `GenericClock::run()` — use `wait_until()` when VirtualClock present, else `run_legacy()` +- [ ] **E14.** Add `GenericClock::timing()` override in `Component` impl +- [ ] **E15.** Verify `GenericClock` existing tests still pass +- [ ] **E16.** Update `TwoPhaseClock` imports — remove `Instant`, add `VirtualClock` + `ComponentTiming` +- [ ] **E17.** Replace `TwoPhaseClock` struct fields: same pattern as GenericClock +- [ ] **E18.** Update `TwoPhaseClock::new()` — initialize virtual time fields +- [ ] **E19.** Add `TwoPhaseClock::set_virtual_clock()` and `TwoPhaseClock::current_virtual_time()` helpers +- [ ] **E20.** Update `TwoPhaseClock::enable()` — use `current_virtual_time()` +- [ ] **E21.** Update `TwoPhaseClock::pause()` and `resume()` — virtual time deltas +- [ ] **E22.** Update `TwoPhaseClock::should_transition()` — accept `virtual_time: Duration` +- [ ] **E23.** Update `TwoPhaseClock::perform_transition()` — accept `virtual_time: Duration` +- [ ] **E24.** Implement `TwoPhaseClock::update_at_time()` in `Component` impl +- [ ] **E25.** Update `TwoPhaseClock::update()` to delegate to `update_at_time()` +- [ ] **E26.** Add `TwoPhaseClock::run_legacy()` helper +- [ ] **E27.** Rewrite `TwoPhaseClock::run()` — use `wait_until()` when VirtualClock present +- [ ] **E28.** Add `TwoPhaseClock::timing()` override in `Component` impl +- [ ] **E29.** Run `cargo build` to verify compilation +- [ ] **E30.** Run `cargo test` to verify all existing tests pass + +--- + +## Flow Diagram: Clock Thread with Virtual Time + +```mermaid +flowchart TD + A[run called] --> B{virtual_clock set?} + B -->|No| C[run_legacy - thread::sleep loop] + B -->|Yes| D[enable - reset phase state] + D --> E[Calculate next_transition_time] + E --> F[wait_until next_transition] + F --> G{wait_until returned} + G -->|None - stopped| H[disable and exit] + G -->|Some vtime| I{is_running?} + I -->|No| H + I -->|Yes| J[update_at_time with vtime] + J --> K[Transition phase if needed] + K --> E +``` + +## Flow Diagram: Phase Transition Logic in Virtual Time + +```mermaid +flowchart LR + subgraph GenericClock + G1[last_transition_vtime = 0ns] --> G2{vtime >= high_time?} + G2 -->|Yes| G3[High to Low transition] + G3 --> G4[last_transition_vtime = vtime] + G4 --> G5{vtime >= low_time?} + G5 -->|Yes| G6[Low to High transition] + G6 --> G4 + end + + subgraph TwoPhaseClock + T1[last_transition_vtime = 0ns] --> T2{elapsed >= phase_time?} + T2 -->|Yes| T3[PHI1 High+PHI2 Low to PHI1 Low+PHI2 High] + T3 --> T4[last_transition_vtime = vtime] + T4 --> T5{elapsed >= phase_time?} + T5 -->|Yes| T6[PHI1 Low+PHI2 High to PHI1 High+PHI2 Low] + T6 --> T4 + end +``` diff --git a/plans/phase6_timing_enforcement.md b/plans/phase6_timing_enforcement.md new file mode 100644 index 0000000..3d6b6f8 --- /dev/null +++ b/plans/phase6_timing_enforcement.md @@ -0,0 +1,870 @@ +# Phase 6: Add Timing Enforcement to Memory/CPU Components + +## Overview + +Phase 6 adds virtual-time-based timing enforcement to the four Intel 400x series components: ROM (4001), RAM (4002), Shift Register (4003), and CPU (4004). These components currently use `Instant::now()` for address latch timestamps and check latency via `Instant::elapsed()`. This phase replaces those with virtual time from `Arc`, adds `ComponentTiming` enforcement checks, and records timing violations. + +**Files modified:** +- [`intel_400x.rs`](src/components/common/intel_400x.rs) — Shared state machine timing (highest priority) +- [`intel_4001.rs`](src/components/memory/intel_4001.rs) — Intel 4001 ROM +- [`intel_4002.rs`](src/components/memory/intel_4002.rs) — Intel 4002 RAM +- [`intel_4003.rs`](src/components/memory/intel_4003.rs) — Intel 4003 Shift Register +- [`intel_4004.rs`](src/components/cpu/intel_4004.rs) — Intel 4004 CPU + +**Files NOT modified** (Phase 7 responsibility): +- [`system_config.rs`](src/system_config.rs) — Will inject `Arc` into components +- [`virtual_clock.rs`](src/virtual_clock.rs) — Already complete +- [`component_timing.rs`](src/component_timing.rs) — Already complete + +--- + +## Implementation Order + +```mermaid +graph LR + A[Step 0: intel_400x.rs shared code] --> B[Step 1: Intel 4003 SR] + B --> C[Step 2: Intel 4001 ROM] + C --> D[Step 3: Intel 4002 RAM] + D --> E[Step 4: Intel 4004 CPU] +``` + +**Rationale:** +1. **`intel_400x.rs` first** — The shared state machine contains `address_latch_time: Option` and `is_latency_elapsed()` used by all three memory components. Changing this first unblocks all component migrations. +2. **Intel 4003 second** — Simplest component; no address latching in the traditional sense, uses shift/load operations. Good validation of the shared code changes. +3. **Intel 4001 third** — ROM is simpler than RAM (read-only, no write operations). Validates the full address→wait→drive flow. +4. **Intel 4002 fourth** — RAM adds write operations and I/O port states on top of the ROM pattern. +5. **Intel 4004 last** — CPU is the most complex (98K chars, own MemoryState enum, own `address_latch_time` field). Benefits from all patterns established by the memory components. + +--- + +## Step 0: Migrate Shared State Machine (`intel_400x.rs`) + +### Current Timing Mechanism + +The shared [`Intel400xStateMachine`](src/components/common/intel_400x.rs:830) uses `Instant` in two critical locations: + +| Location | Line | Current Code | Purpose | +|----------|------|-------------|---------| +| [`address_latch_time`](src/components/common/intel_400x.rs:824) field | 824 | `address_latch_time: Option` | Timestamp when address was fully latched | +| [`handle_address_nibble()`](src/components/common/intel_400x.rs:958) | 958 | `self.address_latch_time = Some(Instant::now())` | Record latch time when both nibbles received | +| [`is_latency_elapsed()`](src/components/common/intel_400x.rs:967) | 969 | `latch_time.elapsed() >= self.access_time` | Check if access time has passed | + +Additionally, the [`Intel400xTimingState`](src/components/common/intel_400x.rs:320) trait exposes `Instant` in its interface: + +| Location | Line | Signature | +|----------|------|-----------| +| [`get_address_latch_time()`](src/components/common/intel_400x.rs:324) | 324 | `fn get_address_latch_time(&self) -> Option` | +| [`set_address_latch_time()`](src/components/common/intel_400x.rs:325) | 325 | `fn set_address_latch_time(&mut self, time: Option)` | + +And the [`Intel400xAddressHandling`](src/components/common/intel_400x.rs:196) trait's [`handle_address_latching()`](src/components/common/intel_400x.rs:206) method takes `&mut Option`: + +| Location | Line | Current Code | +|----------|------|-------------| +| [`handle_address_latching()`](src/components/common/intel_400x.rs:212) | 212 | `address_latch_time: &mut Option` parameter | +| | 223 | `*address_latch_time = Some(Instant::now())` | +| [`handle_latency_wait()`](src/components/common/intel_400x.rs:236) | 236 | `address_latch_time: &Option` parameter | +| | 240 | `latch_time.elapsed() >= access_time` | + +### Required Changes + +#### 0.1 — Change `address_latch_time` from `Instant` to `Duration` + +Replace `Instant`-based timing with virtual time (`Duration` = nanoseconds since simulation start). + +```rust +// BEFORE (line 824): +address_latch_time: Option, + +// AFTER: +address_latch_time: Option, // Virtual time when address was latched +``` + +#### 0.2 — Add `is_latency_elapsed_at()` method (new, virtual-time-aware) + +Add a new method that takes the current virtual time as a parameter, replacing the `Instant::elapsed()`-based check: + +```rust +// NEW METHOD — add after is_latency_elapsed() at line ~973: +/// 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 { + virtual_time.saturating_sub(latch_time) >= self.access_time + } else { + false + } +} +``` + +**Keep the existing `is_latency_elapsed()` method** for backward compatibility — it will continue to work with `Instant::now()` for legacy code paths. Mark it with a comment that it's deprecated for virtual-time usage. + +#### 0.3 — Add `set_address_latch_vtime()` method (new) + +```rust +/// Set address latch time using virtual time directly. +/// Called from update_at_time() when both address nibbles are latched. +pub fn set_address_latch_vtime(&mut self, vtime: Duration) { + self.address_latch_time = Some(vtime); +} +``` + +#### 0.4 — Add `handle_address_nibble_at()` method (new, virtual-time-aware) + +```rust +/// Handle address nibble latching with virtual time. +/// Returns true only when BOTH nibbles have been latched. +/// 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() { + self.address_high_nibble = Some(nibble); + false + } else if self.address_low_nibble.is_none() { + self.address_low_nibble = Some(nibble); + self.address_latch_time = Some(virtual_time); // Virtual time, not Instant::now() + true + } else { + false + } +} +``` + +#### 0.5 — Update `Intel400xTimingState` trait (additive only) + +Add new virtual-time-aware methods to the trait alongside existing ones: + +```rust +pub trait Intel400xTimingState { + // ... existing methods unchanged ... + + /// Get address latch time as virtual time (Duration) + fn get_address_latch_vtime(&self) -> Option; + + /// Set address latch time using virtual time + fn set_address_latch_vtime(&mut self, time: Option); + + /// Check if latency has elapsed at a given virtual time + fn is_latency_elapsed_at(&self, virtual_time: Duration) -> bool; +} +``` + +#### 0.6 — Update `Intel400xAddressHandling` trait (additive only) + +Add virtual-time-aware versions alongside existing methods: + +```rust +pub trait Intel400xAddressHandling { + // ... existing methods unchanged ... + + /// Handle latency wait check using virtual time + fn handle_latency_wait_at( + &self, + address_latch_time: &Option, + access_time: Duration, + virtual_time: Duration, + ) -> bool { + if let Some(latch_time) = address_latch_time { + virtual_time.saturating_sub(*latch_time) >= access_time + } else { + false + } + } +} +``` + +#### 0.7 — Update getter/setter for `address_latch_time` + +```rust +// Change signatures (lines 916-923): +pub fn get_address_latch_time(&self) -> Option { // was Option + self.address_latch_time +} + +pub fn set_address_latch_time(&mut self, time: Option) { // was Option + self.address_latch_time = time; +} +``` + +**Impact:** This changes the return type of `get_address_latch_time()` from `Option` to `Option`. All callers (the `Intel400xTimingState` impls in each component) must be updated to match. + +--- + +## Step 1: Intel 4003 Shift Register + +### Current Timing Mechanism + +[`Intel4003`](src/components/memory/intel_4003.rs:24) is the simplest component: +- Uses [`Intel400xStateMachine`](src/components/memory/intel_4003.rs:32) for state management +- Implements [`Intel400xTimingState`](src/components/memory/intel_4003.rs:94) trait +- Has no direct `Instant::now()` calls — all timing goes through the shared state machine +- Has `thread::sleep(Duration::from_micros(1))` in [`run()`](src/components/memory/intel_4003.rs:586) (line 597) +- Uses `prev_phi1`/`prev_phi2` for edge detection (same as ROM/RAM) + +### Timing Enforcement Points + +The 4003 has simpler timing than ROM/RAM: +- **No address latching** — `responds_to_address() = false` +- **Shift operations** triggered by clock edges +- **Access time: 200ns** — faster than ROM/RAM + +### Required Changes + +#### 1.1 — Add `VirtualClock` field and helper + +```rust +// Add to struct Intel4003 (after state_machine field): +virtual_clock: Option>, + +// Add helper method: +fn current_virtual_time(&self) -> Duration { + self.virtual_clock.as_ref().map_or(Duration::ZERO, |vc| vc.now()) +} +``` + +#### 1.2 — Add `set_virtual_clock()` method + +```rust +pub fn set_virtual_clock(&mut self, clock: Arc) { + self.virtual_clock = Some(clock); +} +``` + +#### 1.3 — Update `Intel400xTimingState` impl + +Change `get_address_latch_time` / `set_address_latch_time` to use `Duration`: + +```rust +// BEFORE (line 124): +fn get_address_latch_time(&self) -> Option { + self.state_machine.get_address_latch_time() +} + +fn set_address_latch_time(&mut self, time: Option) { + self.state_machine.set_address_latch_time(time); +} + +// AFTER: +fn get_address_latch_time(&self) -> Option { + self.state_machine.get_address_latch_time() +} + +fn set_address_latch_time(&mut self, time: Option) { + self.state_machine.set_address_latch_time(time); +} + +// New virtual-time methods: +fn get_address_latch_vtime(&self) -> Option { + self.state_machine.get_address_latch_time() +} + +fn set_address_latch_vtime(&mut self, time: Option) { + self.state_machine.set_address_latch_time(time); +} + +fn is_latency_elapsed_at(&self, virtual_time: Duration) -> bool { + self.state_machine.is_latency_elapsed_at(virtual_time) +} +``` + +#### 1.4 — Implement `update_at_time()` + +```rust +fn update_at_time(&mut self, virtual_time: Duration) { + if !self.is_running() { + return; + } + + let phi1_rising = self.is_phi1_rising_edge(self.prev_phi1); + let phi1_falling = self.is_phi1_falling_edge(self.prev_phi1); + let phi2_rising = self.is_phi2_rising_edge(self.prev_phi2); + let phi2_falling = self.is_phi2_falling_edge(self.prev_phi2); + + let (phi1, phi2) = self.read_clock_pins(); + self.prev_phi1 = phi1; + self.prev_phi2 = phi2; + + let reset_active = self.read_reset_pin(); + if reset_active { + self.perform_reset(); + return; + } + + // Timing enforcement: check if clock frequency is within spec + // (frequency check happens at clock edge detection time) + + if phi2_falling { + self.handle_phi2_falling(); + } + if phi1_falling { + self.handle_phi1_falling(); + } + if phi1_rising { + self.handle_phi1_rising(); + } + if phi2_rising { + self.handle_phi2_rising(); + } +} +``` + +#### 1.5 — Update `update()` to delegate to `update_at_time()` + +```rust +fn update(&mut self) { + let vtime = self.current_virtual_time(); + self.update_at_time(vtime); +} +``` + +#### 1.6 — Update `run()` with VirtualClock wait pattern + +```rust +fn run(&mut self) { + self.base.set_running(true); + + // Initialize clock states for edge detection + let (phi1, phi2) = self.read_clock_pins(); + self.prev_phi1 = phi1; + self.prev_phi2 = phi2; + + // If no VirtualClock, fall back to legacy busy-wait loop + let vclock = match self.virtual_clock.as_ref() { + Some(vc) => Arc::clone(vc), + None => { + while self.is_running() { + self.update(); + thread::sleep(Duration::from_micros(1)); + } + return; + } + }; + + // Virtual-time run loop + let timing = ComponentTiming::intel_4003(); + while self.is_running() { + let next_update = vclock.now() + timing.min_update_interval; + match vclock.wait_until(next_update) { + Some(current_vtime) => { + if !self.is_running() { break; } + self.update_at_time(current_vtime); + } + None => break, + } + } +} +``` + +#### 1.7 — Implement `timing()` trait method + +```rust +fn timing(&self) -> ComponentTiming { + ComponentTiming::intel_4003() +} +``` + +#### 1.8 — Update constructor + +```rust +// Add to new_with_access_time(): +virtual_clock: None, +``` + +#### 1.9 — Add import + +```rust +use crate::virtual_clock::VirtualClock; +use crate::component_timing::ComponentTiming; +``` + +--- + +## Step 2: Intel 4001 ROM + +### Current Timing Mechanism + +[`Intel4001`](src/components/memory/intel_4001.rs:25) uses timing in several places: + +| Location | Line | Current Code | Purpose | +|----------|------|-------------|---------| +| [`access_time`](src/components/memory/intel_4001.rs:29) field | 29 | `access_time: Duration` | ROM access latency (500ns) | +| [`state_machine.is_latency_elapsed()`](src/components/memory/intel_4001.rs:277) | 277 | In `execute_state_logic()` WaitLatency | Check if ROM access time elapsed | +| [`state_machine.is_latency_elapsed()`](src/components/memory/intel_4001.rs:782) | 782 | In legacy `handle_latency_wait()` | Same check in legacy path | +| [`thread::sleep()`](src/components/memory/intel_4001.rs:968) | 968 | In `run()` | Busy-wait loop | + +The ROM state machine flow: +``` +Idle → AddressPhase → WaitLatency → DriveData → Idle +``` + +### Timing Enforcement Points + +```mermaid +graph TD + A[PHI1 Rising: Address Latching] -->|address_latch_time = virtual_time| B[WaitLatency State] + B -->|Check: virtual_time - latch_time >= access_time| C{Access Time Elapsed?} + C -->|Yes| D[PHI2 Rising: Drive Data onto bus] + C -->|No - Timing Violation| E[Record violation - data undefined] + D --> F[PHI2 Falling: Tri-state bus, return to Idle] +``` + +**Key enforcement point:** In `WaitLatency` state, when checking if latency has elapsed, use virtual time comparison. If the clock is overclocked (phase time < access_time), the ROM will NOT produce valid data — just like real hardware. + +### Required Changes + +#### 2.1 — Add `VirtualClock` field and helper + +```rust +// Add to struct Intel4001: +virtual_clock: Option>, + +// Add helper: +fn current_virtual_time(&self) -> Duration { + self.virtual_clock.as_ref().map_or(Duration::ZERO, |vc| vc.now()) +} +``` + +#### 2.2 — Add `set_virtual_clock()` method + +```rust +pub fn set_virtual_clock(&mut self, clock: Arc) { + self.virtual_clock = Some(clock); +} +``` + +#### 2.3 — Update `Intel400xTimingState` impl + +Same pattern as 4003 — change `Instant` to `Duration` in `get/set_address_latch_time()`, add new virtual-time trait methods. + +#### 2.4 — Implement `update_at_time()` + +```rust +fn update_at_time(&mut self, virtual_time: Duration) { + if !self.is_running() { + return; + } + + let phi1_rising = self.is_phi1_rising_edge(self.prev_phi1); + let phi1_falling = self.is_phi1_falling_edge(self.prev_phi1); + let phi2_rising = self.is_phi2_rising_edge(self.prev_phi2); + let phi2_falling = self.is_phi2_falling_edge(self.prev_phi2); + + let (phi1, phi2) = self.read_clock_pins(); + self.prev_phi1 = phi1; + self.prev_phi2 = phi2; + + let reset_active = self.read_reset_pin(); + if reset_active { + self.perform_reset(); + return; + } + + if phi2_falling { self.handle_phi2_falling(); } + if phi1_falling { self.handle_phi1_falling(); } + if phi1_rising { self.handle_phi1_rising(); } + if phi2_rising { self.handle_phi2_rising(); } +} +``` + +#### 2.5 — Update `update()` to delegate + +```rust +fn update(&mut self) { + let vtime = self.current_virtual_time(); + self.update_at_time(vtime); +} +``` + +#### 2.6 — Update `run()` with VirtualClock wait pattern + +Same pattern as 4003 — check for `virtual_clock`, fall back to legacy busy-wait if absent. + +#### 2.7 — Implement `timing()` trait method + +```rust +fn timing(&self) -> ComponentTiming { + ComponentTiming::intel_4001() +} +``` + +#### 2.8 — Update `handle_phi2_rising()` for timing enforcement + +The key timing enforcement happens in [`handle_phi2_rising()`](src/components/memory/intel_4001.rs:484) where `WaitLatency` state checks `is_latency_elapsed()`: + +```rust +// BEFORE (line 487-493): +if self.state_machine.get_state() == Intel400xUnifiedState::WaitLatency { + if self.a3_wait_cycles > 0 { + self.a3_wait_cycles -= 1; + } else { + // A3 wait complete, transition to DriveData. + self.process_latency_elapsed(); + } +} + +// AFTER — add timing check: +if self.state_machine.get_state() == Intel400xUnifiedState::WaitLatency { + if self.a3_wait_cycles > 0 { + self.a3_wait_cycles -= 1; + } else { + // Check access time using virtual time + let virtual_time = self.current_virtual_time(); + if self.state_machine.is_latency_elapsed_at(virtual_time) { + self.process_latency_elapsed(); + } else { + // TIMING VIOLATION: Access time not yet elapsed + // Component produces undefined results (just like real hardware) + // The ROM will NOT drive data — the bus remains undefined + // Violation is recorded but simulation continues + } + } +} +``` + +#### 2.9 — Update `execute_state_logic()` WaitLatency branch + +Similarly update the [`execute_state_logic()`](src/components/memory/intel_4001.rs:275) WaitLatency branch: + +```rust +// BEFORE (line 275-280): +Intel400xUnifiedState::WaitLatency => { + if self.state_machine.is_latency_elapsed() { + self.process_latency_elapsed(); + } +} + +// AFTER: +Intel400xUnifiedState::WaitLatency => { + let virtual_time = self.current_virtual_time(); + if self.state_machine.is_latency_elapsed_at(virtual_time) { + self.process_latency_elapsed(); + } +} +``` + +#### 2.10 — Update constructor and imports + +```rust +// Add to new_with_access_time(): +virtual_clock: None, + +// Add imports: +use crate::virtual_clock::VirtualClock; +use crate::component_timing::ComponentTiming; +``` + +--- + +## Step 3: Intel 4002 RAM + +### Current Timing Mechanism + +[`Intel4002`](src/components/memory/intel_4002.rs) follows the same pattern as ROM but adds: +- **Read/Write/OutputPort** states (vs. just DriveData) +- [`is_latency_elapsed()`](src/components/memory/intel_4002.rs:268) check in `execute_state_logic()` WaitLatency +- [`is_latency_elapsed()`](src/components/memory/intel_4002.rs:480) check in `handle_phi2_rising()` +- [`thread::sleep()`](src/components/memory/intel_4002.rs:726) in `run()` (line 726) + +State machine flow: +``` +Idle → AddressPhase → WaitLatency → ReadData/WriteData/OutputPort → Idle +``` + +### Timing Enforcement Points + +Same as ROM — the critical check is in `WaitLatency` state when transitioning to data operations. + +### Required Changes + +Identical pattern to ROM (Step 2): + +#### 3.1 — Add `virtual_clock: Option>` field + helper +#### 3.2 — Add `set_virtual_clock()` method +#### 3.3 — Update `Intel400xTimingState` impl (Instant → Duration) +#### 3.4 — Implement `update_at_time()` (same structure as ROM) +#### 3.5 — Update `update()` to delegate to `update_at_time()` +#### 3.6 — Update `run()` with VirtualClock wait pattern +#### 3.7 — Implement `timing()` returning `ComponentTiming::intel_4002()` +#### 3.8 — Update `handle_phi2_rising()` WaitLatency check (line ~479-481): + +```rust +// BEFORE: +if self.state_machine.is_latency_elapsed() { + self.process_latency_elapsed(); +} + +// AFTER: +let virtual_time = self.current_virtual_time(); +if self.state_machine.is_latency_elapsed_at(virtual_time) { + self.process_latency_elapsed(); +} +``` + +#### 3.9 — Update `execute_state_logic()` WaitLatency branch (line ~266-269) +#### 3.10 — Update constructor and imports + +--- + +## Step 4: Intel 4004 CPU + +### Current Timing Mechanism + +[`Intel4004`](src/components/cpu/intel_4004.rs:124) has its **own** `MemoryState` enum and timing fields, separate from the shared `Intel400xStateMachine`: + +| Location | Line | Current Code | Purpose | +|----------|------|-------------|---------| +| [`address_latch_time`](src/components/cpu/intel_4004.rs:168) field | 168 | `address_latch_time: Option` | Timestamp when address was latched | +| [`access_time`](src/components/cpu/intel_4004.rs:170) field | 170 | `access_time: Duration` | Memory access time (500ns) | +| [`advance_address_cycle()`](src/components/cpu/intel_4004.rs:596) | 596 | `self.address_latch_time = Some(Instant::now())` | Record latch time at A2→A3 transition | +| [`thread::sleep()`](src/components/cpu/intel_4004.rs:2387) | 2387 | In `run()` | Busy-wait loop | + +**Key difference:** The CPU does NOT use the shared `Intel400xStateMachine`. It has its own `MemoryState` enum and `AddressCycle` enum. The `address_latch_time` field is directly on the CPU struct. + +### Timing Enforcement Points + +The CPU's timing enforcement is different from memory components: +- **Propagation delay** (150ns): After the CPU drives address/data onto the bus, its outputs don't change instantly +- **Setup/hold time checks**: The CPU should verify that data from ROM/RAM was stable for the required setup time before the clock edge +- **Frequency check**: The CPU should verify the clock frequency is within spec (750 kHz max) + +```mermaid +graph TD + A[A1: Drive addr high nibble] --> B[A2: Drive addr low nibble] + B -->|address_latch_time = virtual_time| C[A3: Wait for ROM/RAM] + C -->|Wait access_time| D[M1: Read OPR from bus] + D --> E[M2: Read OPA from bus] + E --> F[Execute instruction] +``` + +### Required Changes + +#### 4.1 — Change `address_latch_time` from `Instant` to `Duration` + +```rust +// BEFORE (line 168): +address_latch_time: Option, + +// AFTER: +address_latch_time: Option, // Virtual time when address was latched +``` + +#### 4.2 — Add `VirtualClock` field and helper + +```rust +// Add to struct Intel4004: +virtual_clock: Option>, + +fn current_virtual_time(&self) -> Duration { + self.virtual_clock.as_ref().map_or(Duration::ZERO, |vc| vc.now()) +} +``` + +#### 4.3 — Add `set_virtual_clock()` method + +```rust +pub fn set_virtual_clock(&mut self, clock: Arc) { + self.virtual_clock = Some(clock); +} +``` + +#### 4.4 — Update `advance_address_cycle()` (line 596) + +```rust +// BEFORE: +self.address_latch_time = Some(Instant::now()); + +// AFTER: +self.address_latch_time = Some(self.current_virtual_time()); +``` + +#### 4.5 — Implement `update_at_time()` + +```rust +fn update_at_time(&mut self, virtual_time: Duration) { + if !self.is_running() { + return; + } + + // ... same clock edge detection logic as current update() ... + // The key difference: address_latch_time is now Duration-based, + // so any elapsed time checks use virtual_time - address_latch_time +} +``` + +#### 4.6 — Update `update()` to delegate + +```rust +fn update(&mut self) { + let vtime = self.current_virtual_time(); + self.update_at_time(vtime); +} +``` + +#### 4.7 — Update `run()` with VirtualClock wait pattern + +```rust +fn run(&mut self) { + self.base.set_running(true); + + let vclock = match self.virtual_clock.as_ref() { + Some(vc) => Arc::clone(vc), + None => { + // Legacy busy-wait loop + while self.is_running() { + self.update(); + thread::sleep(Duration::from_micros(1)); + if self.cycle_count > 100000 { + // ... safety check ... + } + } + return; + } + }; + + let timing = ComponentTiming::intel_4004(); + while self.is_running() { + let next_update = vclock.now() + timing.min_update_interval; + match vclock.wait_until(next_update) { + Some(current_vtime) => { + if !self.is_running() { break; } + self.update_at_time(current_vtime); + if self.cycle_count > 100000 { + // ... safety check ... + } + } + None => break, + } + } +} +``` + +#### 4.8 — Implement `timing()` trait method + +```rust +fn timing(&self) -> ComponentTiming { + ComponentTiming::intel_4004() +} +``` + +#### 4.9 — Update constructor and reset methods + +```rust +// In new() constructor: +address_latch_time: None, // Already None, just type change +virtual_clock: None, + +// In reset methods (lines 278, 497, 705): +self.address_latch_time = None; // No change needed — already None +``` + +#### 4.10 — Add imports + +```rust +use crate::virtual_clock::VirtualClock; +use crate::component_timing::ComponentTiming; +``` + +--- + +## Timing Violation Handling Strategy + +Timing violations are **recorded, not fatal**. When a violation is detected: + +1. **Access time not elapsed** (ROM/RAM): The component stays in `WaitLatency` and does NOT drive data onto the bus. The bus remains in its previous state (possibly undefined). The CPU reads garbage — just like real hardware. + +2. **Overclocked frequency**: Detected when the clock period is shorter than `min_clock_period()`. The component continues operating but may produce incorrect results. Violation is logged. + +3. **Setup/hold time violation**: The latched value may be incorrect. The component continues with the corrupted value — just like real hardware. + +### Violation Recording (Future Enhancement) + +For Phase 6, violations are detected at the enforcement points but **not yet recorded to a global log**. The component simply behaves incorrectly (doesn't drive data, latches wrong values, etc.). A `TimingViolationLog` can be added in a future phase to collect violations for display in the GUI. + +The `ComponentTiming` check methods already return `Result<(), TimingViolation>`: +- [`check_access_time(elapsed)`](src/component_timing.rs:221) — Returns `Err(TimingViolation::AccessTimeNotElapsed)` if not enough time +- [`check_setup_time(stability_duration)`](src/component_timing.rs:236) — Returns `Err(TimingViolation::SetupTimeViolation)` +- [`check_hold_time(stability_duration)`](src/component_timing.rs:251) — Returns `Err(TimingViolation::HoldTimeViolation)` +- [`supports_frequency(freq_hz)`](src/component_timing.rs:214) — Returns `bool` + +These can be used at enforcement points to generate violations for logging when that infrastructure is added. + +--- + +## Summary of Changes by File + +### [`intel_400x.rs`](src/components/common/intel_400x.rs) (Shared) +| Change | Type | +|--------|------| +| `address_latch_time: Option` → `Option` | Field type change | +| `get/set_address_latch_time()` return/param type | Signature change | +| Add `is_latency_elapsed_at(virtual_time: Duration)` | New method | +| Add `handle_address_nibble_at(nibble, virtual_time)` | New method | +| Add `set_address_latch_vtime(time: Duration)` | New method | +| Add virtual-time methods to `Intel400xTimingState` trait | Trait extension | +| Add `handle_latency_wait_at()` to `Intel400xAddressHandling` trait | Trait extension | +| Keep existing `is_latency_elapsed()` for backward compat | No change | + +### [`intel_4001.rs`](src/components/memory/intel_4001.rs) (ROM) +| Change | Type | +|--------|------| +| Add `virtual_clock: Option>` field | New field | +| Add `set_virtual_clock()` method | New method | +| Add `current_virtual_time()` helper | New method | +| Implement `update_at_time(virtual_time: Duration)` | New method | +| Update `update()` to delegate to `update_at_time()` | Modify | +| Update `run()` with VirtualClock wait pattern | Modify | +| Implement `timing()` returning `ComponentTiming::intel_4001()` | New method | +| Update `Intel400xTimingState` impl (Instant → Duration) | Modify | +| Update `handle_phi2_rising()` WaitLatency check | Modify | +| Update `execute_state_logic()` WaitLatency check | Modify | +| Update constructor (`virtual_clock: None`) | Modify | +| Add imports for `VirtualClock`, `ComponentTiming` | Modify | + +### [`intel_4002.rs`](src/components/memory/intel_4002.rs) (RAM) +| Change | Type | +|--------|------| +| Same changes as ROM, but with `ComponentTiming::intel_4002()` | Same pattern | +| Update `handle_phi2_rising()` WaitLatency check | Modify | +| Update `execute_state_logic()` WaitLatency check | Modify | + +### [`intel_4003.rs`](src/components/memory/intel_4003.rs) (Shift Register) +| Change | Type | +|--------|------| +| Same pattern as ROM, but with `ComponentTiming::intel_4003()` | Same pattern | +| No `handle_phi2_rising()` WaitLatency changes needed (4003 doesn't wait for access time the same way) | N/A | + +### [`intel_4004.rs`](src/components/cpu/intel_4004.rs) (CPU) +| Change | Type | +|--------|------| +| `address_latch_time: Option` → `Option` | Field type change | +| Add `virtual_clock: Option>` field | New field | +| Add `set_virtual_clock()` method | New method | +| Add `current_virtual_time()` helper | New method | +| Implement `update_at_time(virtual_time: Duration)` | New method | +| Update `update()` to delegate to `update_at_time()` | Modify | +| Update `run()` with VirtualClock wait pattern | Modify | +| Implement `timing()` returning `ComponentTiming::intel_4004()` | New method | +| Update `advance_address_cycle()` latch time assignment | Modify | +| Update constructor (`virtual_clock: None`) | Modify | +| Add imports for `VirtualClock`, `ComponentTiming` | Modify | + +--- + +## Backward Compatibility + +All changes follow the Phase 5 pattern: +- `Option>` — components work without a VirtualClock +- `run()` falls back to legacy `thread::sleep()` busy-wait when no VirtualClock is injected +- `update()` delegates to `update_at_time()` using `current_virtual_time()` (returns `Duration::ZERO` when no clock) +- `update_at_time()` is a new override; existing `update()` callers continue to work +- Existing `is_latency_elapsed()` method kept for backward compat (uses `Instant::elapsed()` internally) + +Phase 7 (`ConfigurableSystem`) will create the `Arc` and inject it via `set_virtual_clock()` on each component. + +--- + +## Testing Strategy + +After each component migration, verify: +1. **Legacy path still works** — Run existing tests without VirtualClock injected +2. **Virtual time path works** — Inject a VirtualClock, advance time, verify correct behavior +3. **Timing violation behavior** — Overclock the clock and verify ROM/RAM produce incorrect results while the faster shift register still works +4. **State machine transitions** — Verify Idle → AddressPhase → WaitLatency → DriveData cycle completes correctly in virtual time diff --git a/plans/phase7_system_integration.md b/plans/phase7_system_integration.md new file mode 100644 index 0000000..746c1f2 --- /dev/null +++ b/plans/phase7_system_integration.md @@ -0,0 +1,174 @@ +# Phase 7: System Integration — ConfigurableSystem + VirtualClock + +## Overview + +Phase 7 wires the `VirtualClock` and `RealTimeGovernor` into `ConfigurableSystem`, completing the transition from `thread::sleep()`-based timing to virtual-time-synchronized per-component threads. + +## Current Architecture (Pre-Phase 7) + +``` +ConfigurableSystem::start() + → for each component: + thread::spawn(|| { + loop { + if !running → break + if paused → sleep(10ms), continue + comp.try_lock() → comp.update() + adaptive sleep (yield or µs-sleep based on frequency) + } + comp.stop() + }) +``` + +Problems: +- No timing accuracy — OS `nanosleep` has ~100µs granularity +- No synchronization between components +- No speed control +- Components don't use virtual time + +## Target Architecture (Post-Phase 7) + +``` +ConfigurableSystem::start() + 1. Create Arc with tick_size derived from clock frequency + 2. Inject VirtualClock into all components via set_virtual_clock() + 3. Spawn RealTimeGovernor thread → governor.run() calls clock.start() + 4. for each component: + thread::spawn(|| { + comp.run() // Component's own run() handles VirtualClock wait + }) + 5. Store governor handle for clean shutdown +``` + +## Changes Required + +### 1. `src/system_config.rs` — `ConfigurableSystem` + +#### New Fields + +```rust +pub struct ConfigurableSystem { + // ... existing fields ... + virtual_clock: Arc, + governor_handle: Arc>>>, +} +``` + +#### Modified `new()` Constructor + +- Derive tick_size from clock frequency in metadata +- Create `Arc` with that tick_size +- Store it in the struct + +#### Modified `start()` Method (Inherent) + +The current `start()` spawns threads with a manual loop that calls `comp.update()`. +The new `start()` will: + +1. Inject `Arc` into all components via `set_virtual_clock()` (downcast to each concrete type) +2. Spawn `RealTimeGovernor` in its own thread — `governor.run()` calls `clock.start()` internally +3. For each component, spawn a thread that calls `comp.run()` — the component's own `run()` method handles the VirtualClock wait loop (already implemented in Phases 5-6) +4. Store the governor thread handle + +#### Modified `cleanup()` Method + +1. Call `clock.stop()` to unblock all component threads waiting in `wait_until()` / `wait_for_start()` +2. Set `is_running = false` +3. Wait for component threads to finish (join) +4. Wait for governor thread to finish (join) + +#### Modified `SystemController::stop()` (Trait Impl) + +- Call `clock.stop()` before existing cleanup +- This wakes all component threads, which see `None` from `wait_until()` and exit + +#### Modified `SystemController::pause()` / `resume()` (Trait Impl) + +- Delegate to `clock.pause()` / `clock.resume()` instead of (or in addition to) component-level pause +- This is more efficient — all threads block on the condvar instead of polling + +### 2. `src/main.rs` — No Changes Required + +The `main.rs` calls `ConfigurableSystem::start()` or `system.run()`, both of which will now use VirtualClock internally. No changes needed. + +### 3. Tick Size Derivation + +For a 750 kHz two-phase clock: +- Full cycle = 1/750000 ≈ 1333ns +- Each phase = 1333ns / 2 ≈ 666ns +- Phase transition = 666ns / 2 = 333ns (quarter cycle) +- tick_size = 333ns + +General formula: `tick_size = 1 / (4 * frequency)` seconds + +For non-two-phase clocks (generic_clock): +- tick_size = 1 / (2 * frequency) seconds (half cycle) + +We use the conservative (smaller) tick_size: `1 / (4 * frequency)`. + +### 4. Backward Compatibility + +If `set_virtual_clock()` is not called on a component (e.g., a component type that doesn't have the method), the component's `run()` method falls back to `run_legacy()` which uses `thread::sleep()`. This is already implemented in each component's `run()` method from Phases 5-6. + +The `Component` trait's `run()` method has a default implementation that uses `thread::sleep()`, so any component that hasn't been migrated will still work. + +### 5. Thread Lifecycle + +``` +Component Thread: + 1. comp.run() is called + 2. If virtual_clock is Some: + a. Loop: wait_until(next_time) → update_at_time(vtime) → schedule next + b. Exit when wait_until returns None (clock stopped) + 3. If virtual_clock is None: + a. run_legacy() — original thread::sleep loop + +Governor Thread: + 1. governor.run() is called + 2. Calls clock.start() — wakes all component threads in wait_for_start() + 3. Loop: advance virtual time based on mode/speed + 4. Exit when clock.is_running() == false + 5. Drop guard calls clock.stop() +``` + +### 6. Downcasting Strategy + +Since `set_virtual_clock()` is a method on each concrete type (not on the `Component` trait), we need to downcast from `Box` to each concrete type. The order of attempts: + +1. `GenericClock` — `comp.as_any_mut().downcast_mut::()` +2. `TwoPhaseClock` — `comp.as_any_mut().downcast_mut::()` +3. `Intel4004` — `comp.as_any_mut().downcast_mut::()` +4. `Intel4001` — `comp.as_any_mut().downcast_mut::()` +5. `Intel4002` — `comp.as_any_mut().downcast_mut::()` +6. `Intel4003` — `comp.as_any_mut().downcast_mut::()` + +This is the same pattern already used in `pause()`, `resume()`, `set_clock_frequency()`, etc. + +## Implementation Steps + +1. Add imports for `VirtualClock`, `RealTimeGovernor`, and concrete component types +2. Add `virtual_clock` and `governor_handle` fields to `ConfigurableSystem` +3. Update `ConfigurableSystem::new()` to create `VirtualClock` from frequency +4. Add `inject_virtual_clock()` helper method +5. Update `ConfigurableSystem::start()` (inherent) to: + - Inject VirtualClock into all components + - Spawn governor thread + - Spawn component threads that call `comp.run()` +6. Update `ConfigurableSystem::cleanup()` to stop clock and join governor +7. Update `SystemController::pause()`/`resume()` to use clock.pause()/resume() +8. Build and test + +## Risk Assessment + +- **Low Risk**: Adding new fields and methods to `ConfigurableSystem` +- **Medium Risk**: Changing the thread spawn model from external loop to `comp.run()` + - Mitigated by the fact that `comp.run()` already works (tested in Phases 5-6) +- **Low Risk**: Downcast-based injection — same pattern already used extensively +- **Low Risk**: Governor thread — already tested independently + +## Testing Strategy + +1. `cargo build` — must compile +2. `cargo test` — all existing tests must pass +3. Manual test: `cargo run -- --system basic` — system should start, run, and stop cleanly +4. Manual test: `cargo run -- --gui --system basic` — GUI should work with VirtualClock diff --git a/plans/phase8_gui_controls.md b/plans/phase8_gui_controls.md new file mode 100644 index 0000000..3bad835 --- /dev/null +++ b/plans/phase8_gui_controls.md @@ -0,0 +1,117 @@ +# Phase 8: Speed/Step Controls for GUI and Console + +## Overview + +Add simulation speed, mode, and step controls to both the egui-based GUI and the ratatui-based console interface. These controls leverage the existing `VirtualClock` and `RealTimeGovernor` APIs. + +## Files to Modify + +1. **`src/gui/state.rs`** — Add `TimingState` struct to `GuiState` for virtual time display data +2. **`src/gui/components.rs`** — Add `TimingControls` component (speed slider, mode selector, step button, time display) +3. **`src/gui/mod.rs`** — Integrate `TimingControls` into the main render loop +4. **`src/gui/console.rs`** — Add console commands: `speed`, `mode`, `step`, `time` + +## Detailed Design + +### 1. GUI State (`src/gui/state.rs`) + +Add a `TimingState` struct to `GuiState`: + +```rust +pub struct TimingState { + /// Current virtual time in nanoseconds + pub virtual_time_ns: u64, + /// Total ticks elapsed + pub tick_count: u64, + /// Current speed multiplier (from VirtualClock) + pub target_speed: f64, + /// Actual measured speed (from RealTimeGovernor snapshots) + pub actual_speed: f64, + /// Current simulation mode: "RealTime", "MaxSpeed", "SingleStep" + pub mode: String, + /// Whether a step is pending (SingleStep mode) + pub step_pending: bool, +} +``` + +The `update_from_system()` method will be extended to: +- Access `ConfigurableSystem::get_virtual_clock()` → `Arc` +- Read `clock.now_ns()`, `clock.tick_count()`, `clock.speed_multiplier()`, `clock.mode()`, `clock.is_step_pending()` +- Compute actual speed using `RealTimeGovernor::take_speed_snapshot()` / `compute_speed()` across frames + +To support speed snapshot computation across frames, `GuiState` will hold two `SpeedSnapshot` instances: +```rust +pub speed_snap_prev: Option, +pub speed_snap_curr: Option, +``` + +### 2. TimingControls Component (`src/gui/components.rs`) + +New component `TimingControls` with: + +#### Speed Slider (Logarithmic Scale) +- Slider range: logarithmic from 0.001× to 100.0× +- Uses `egui::Slider` with custom logarithmic mapping +- Position 0.0 → 0.001×, position 1.0 → 100.0× +- Special "MaxSpeed" position at the far right +- Calls `clock.set_speed_multiplier()` on change +- Display: "Speed: 1.00× (actual: 0.98×)" + +#### Mode Selector +- Three radio buttons: RealTime / MaxSpeed / SingleStep +- Calls `clock.set_mode()` on selection change + +#### Step Controls +- "Step" button, enabled only in SingleStep mode +- Calls `clock.trigger_step()` on click +- Shows "Step pending..." indicator when step is pending + +#### Virtual Time Display +- Human-readable format: auto-selects µs, ms, or s +- Shows tick count +- Shows target vs actual speed + +### 3. Integration (`src/gui/mod.rs`) + +Add `timing_controls: TimingControls` to `GuiComponents` and call `render_timing_controls()` between the control panel and memory sections. + +### 4. Console Commands (`src/gui/console.rs`) + +New commands in `execute_command()`: + +| Command | Description | +|---------|-------------| +| `speed ` | Set speed multiplier (e.g., `speed 0.5`) | +| `speed` | Show current speed | +| `mode ` | Set simulation mode | +| `mode` | Show current mode | +| `step` | Trigger a single step (SingleStep mode) | +| `time` | Show virtual time and tick count | + +Also add keyboard shortcuts: +- `+`/`-` already exist for frequency; add new shortcuts for speed: + - Keep existing `+`/`-` for frequency control (unchanged) + +### Logarithmic Slider Mapping + +The slider position `t` (0.0 to 1.0) maps to speed multiplier via: +``` +t=0.00 → 0.001× +t=0.25 → 0.01× +t=0.50 → 0.1× +t=0.75 → 1.0× +t=0.90 → 10.0× +t=1.00 → 100.0× +``` + +Formula: `speed = 10^(t * 5 - 3)` gives range [0.001, 100.0] + +For MaxSpeed mode, we'll use a separate toggle rather than a slider position. + +## Implementation Order + +1. Add `TimingState` to `src/gui/state.rs` +2. Add `TimingControls` component to `src/gui/components.rs` +3. Integrate into `src/gui/mod.rs` +4. Add console commands to `src/gui/console.rs` +5. Build and test diff --git a/plans/phase9_tests.md b/plans/phase9_tests.md new file mode 100644 index 0000000..c5c73b0 --- /dev/null +++ b/plans/phase9_tests.md @@ -0,0 +1,47 @@ +# Phase 9: Update Tests for Virtual Time + +## Current State + +All 226 existing tests pass (142 unit tests, 16 fibonacci tests, 62 integration tests, 5 simple tests, 15 doc-tests). No tests are broken by the virtual time migration — the existing tests use `update()` (without virtual time) and direct component manipulation, which still works. + +## Plan + +### 1. Update `tests/lib.rs` — Add Virtual Time Test Utilities + +Add helper functions to the `test_utils` module: +- `create_maxspeed_clock()` — Creates a VirtualClock in MaxSpeed mode with a 333ns tick size +- `create_singlestep_clock()` — Creates a VirtualClock in SingleStep mode +- `spawn_governor()` — Spawns a RealTimeGovernor thread for a given clock +- `stop_and_wait()` — Stops the clock and waits for governor thread to finish + +### 2. New Test File: `tests/virtual_time_tests.rs` + +#### MaxSpeed Mode Tests +- `test_maxspeed_clock_advances` — Verify VirtualClock advances in MaxSpeed mode with governor +- `test_maxspeed_component_updates` — Verify Intel4001 updates correctly at virtual time +- `test_maxspeed_rom_access_timing` — Verify ROM data is valid after access_time elapsed in virtual time + +#### SingleStep Mode Tests +- `test_singlestep_advances_one_tick` — Verify each trigger_step() advances exactly one tick +- `test_singlestep_rom_state_at_specific_time` — Verify ROM state at specific virtual times: + - Before access_time: data not ready + - After access_time: data valid + +#### Overclocking Tests +- `test_overclocking_rom_fails_at_2mhz` — Set 2 MHz clock (250ns phase), verify ROM (500ns access) fails +- `test_overclocking_ram_fails_at_2mhz` — Same for RAM +- `test_shift_register_works_at_higher_freq` — Intel4003 (200ns access) works at 2 MHz (250ns phase >= 200ns) +- `test_component_timing_check_access_time` — Direct ComponentTiming API tests + +#### Virtual Clock Integration Tests +- `test_configurable_system_creates_virtual_clock` — Verify ConfigurableSystem has a VirtualClock +- `test_configurable_system_maxspeed_execution` — Run system in MaxSpeed mode briefly, verify it works +- `test_virtual_clock_stop_exits_threads` — Verify clock.stop() causes governor to exit +- `test_pause_resume_behavior` — Verify pause stops time advancement, resume continues + +### 3. Implementation Notes + +- All new tests go in a separate file to avoid modifying existing passing tests +- Tests use `Arc` and `RealTimeGovernor` directly (not through ConfigurableSystem) for precise control +- Overclocking tests use `ComponentTiming::check_access_time()` directly since component-internal timing checks happen via `update_at_time()` which requires the full component thread infrastructure +- Tests that spawn governor threads must ensure proper cleanup (stop + join) diff --git a/plans/timing_architecture.md b/plans/timing_architecture.md new file mode 100644 index 0000000..df1ded7 --- /dev/null +++ b/plans/timing_architecture.md @@ -0,0 +1,603 @@ +# Timing-Accurate Simulation Architecture + +## Problem Statement + +The current emulator spawns each component in its own OS thread with independent `thread::sleep()` calls. This means: + +1. **No timing accuracy** — `nanosleep` has ~100µs granularity but components need nanosecond-level timing +2. **No synchronization** — Components update independently with no guaranteed ordering +3. **No speed control** — No way to slow down, speed up, or single-step +4. **No hardware timing enforcement** — Timing constants exist but aren't checked against simulation time + +## How Other Emulators/Simulators Handle Timing + +### UAE (Ultimate Amiga Emulator) + +UAE uses a **line-based timing model**: +- The Amiga's custom chips are synchronized to the video beam position (horizontal line) +- Each emulated line takes a fixed number of CPU cycles (e.g., 228 cycles for PAL) +- The CPU runs for one line's worth of cycles, then the custom chips update +- A **host synchronization point** occurs once per frame (50/60 Hz) — UAE sleeps to match real-time at the frame boundary, not per-cycle +- Sound output drives real-time sync: the audio buffer must be filled at the correct rate +- Key insight: **UAE batches many cycles of work, then syncs to wall-clock at coarse granularity (frame rate)** + +### VICE (Versatile Commodore Emulator) + +VICE uses a **cycle-accurate but batched** model: +- The CPU and CIA/VIC chips each track their own cycle count +- The main loop runs the CPU for one cycle, then lets the VIC/CIA advance one cycle +- **No per-cycle sleeping** — VICE runs cycles as fast as possible +- Real-time sync happens at the **frame boundary** (50/60 Hz = every ~20ms) +- VICE uses the host's audio subsystem or `usleep()` at frame boundaries to maintain real-time +- The SID (sound chip) and VIC (video) generate their output in batches, then the host presents it at the right time +- Key insight: **Cycle-accurate internally, wall-clock synced at frame boundaries** + +### Verilog/VHDL Simulators (Icarus Verilog, ModelSim, Verilator) + +- **Event-driven scheduling** at virtual time points +- Components register callbacks: "wake me at virtual time T" +- No wall-clock sleeping at all — pure computation +- The simulator maintains an **event queue** sorted by virtual time +- Key insight: **Virtual time is just a number in a priority queue** + +### QEMU + +- Uses **instruction counting** for timing +- Each emulated instruction advances a virtual clock counter +- For real-time workloads (audio, video), QEMU sleeps at coarse boundaries +- Key insight: **Count instructions, sync at coarse intervals** + +### SPICE (Analog Simulator) + +- **Adaptive timestep** integration +- Time advances in variable-size steps based on signal activity +- Smaller steps when signals change rapidly, larger steps when stable +- Key insight: **Variable granularity based on activity** + +### Common Pattern + +All these emulators share the same core pattern: + +```mermaid +graph LR + A[Run N cycles of virtual time] --> B[Batch: process all events for those cycles] + B --> C[Sync: sleep to match wall-clock if needed] + C --> A +``` + +The **sync point granularity** varies: +| Emulator | Sync granularity | Virtual time per sync | +|----------|-----------------|----------------------| +| UAE | Per video frame | ~20ms | +| VICE | Per video frame | ~20ms | +| QEMU | Per instruction batch | ~1-10ms | +| Verilog | Never (pure computation) | N/A | +| SPICE | Never (pure computation) | N/A | + +**The key insight for our emulator:** We should use **virtual time internally** (like all of them) and **sync to wall-clock at coarse boundaries** (like UAE/VICE), not try to `nanosleep` per cycle. + +--- + +## Proposed Architecture: Virtual-Time-Synchronized Per-Component Threads + +### Core Idea + +Keep each component in its own thread (the project's signature feature), but replace `thread::sleep()`-based timing with a **shared virtual clock** that all components synchronize against. Components sleep until the virtual clock reaches their next scheduled update time. + +### Architecture Diagram + +```mermaid +graph TB + subgraph "Global Synchronization" + VC[VirtualClock - shared Arc atomic u64 nanoseconds] + RTG[RealTimeGovernor - advances virtual clock] + SC[SpeedControl - multiplier + mode] + end + + subgraph "Component Threads - each runs independently" + CT1[Clock Thread] + CT2[CPU Thread] + CT3[ROM Thread] + CT4[RAM Thread] + CT5[ShiftReg Thread] + end + + subgraph "Per-Component Timing" + T1[ComponentTiming - access_time, setup_time, etc.] + T2[TimingChecker - validates constraints against virtual time] + end + + RTG --> VC + SC --> RTG + VC --> CT1 + VC --> CT2 + VC --> CT3 + VC --> CT4 + VC --> CT5 + T1 --> CT2 + T1 --> CT3 + T2 --> CT3 +``` + +### How It Works — Virtual Time Synchronization + +```mermaid +sequenceDiagram + participant Gov as Governor Thread + participant VC as VirtualClock + participant CLK as Clock Thread + participant CPU as CPU Thread + participant ROM as ROM Thread + + Note over Gov: Governor advances virtual clock in steps + Gov->>VC: advance to 333ns + VC->>VC: notify_all - wake sleeping components + + CLK->>VC: read virtual_time = 333ns + CLK->>CLK: 333ns >= 666ns? No - sleep until 666ns + + CPU->>VC: read virtual_time = 333ns + CPU->>CPU: Check for clock edges - no change - wait + + Note over Gov: Governor advances + Gov->>VC: advance to 666ns + VC->>VC: notify_all + + CLK->>VC: read virtual_time = 666ns + CLK->>CLK: 666ns >= 666ns? Yes! Toggle PHI1 LOW, PHI2 HIGH + CLK->>CLK: Schedule next transition at 1333ns + CLK->>VC: wait_until 1333ns + + CPU->>VC: read virtual_time = 666ns + CPU->>CPU: Detect PHI2 rising - process data phase + + ROM->>VC: read virtual_time = 666ns + ROM->>ROM: See address on bus. Record latch_time = 666ns + ROM->>ROM: Need 500ns access time. Data ready at 1166ns + ROM->>VC: wait_until 1166ns + + Note over Gov: Governor advances + Gov->>VC: advance to 1166ns + + ROM->>VC: read virtual_time = 1166ns + ROM->>ROM: 1166 - 666 = 500ns >= 500ns access time. Drive data! +``` + +--- + +## Key Components + +### 1. VirtualClock — Shared Virtual Time Source + +```rust +pub struct VirtualClock { + /// Current virtual time in nanoseconds + time_ns: AtomicU64, + /// Target time for next step (components wait for this) + target_ns: AtomicU64, + /// Condvar for notifying components when time advances + time_advanced: Condvar, + /// Mutex for condvar + state_mutex: Mutex<()>, + /// Speed multiplier: 1.0 = real-time, 0.1 = 10x slower + speed_multiplier: AtomicU64, // Stored as fixed-point + /// Simulation mode + mode: AtomicU8, // 0=RealTime, 1=MaxSpeed, 2=SingleStep + /// Single-step trigger + step_pending: AtomicBool, +} + +impl VirtualClock { + /// Get current virtual time + pub fn now(&self) -> Duration { + Duration::from_nanos(self.time_ns.load(Ordering::SeqCst)) + } + + /// Block until virtual time reaches at least target + /// Returns the actual virtual time when woken + pub fn wait_until(&self, target: Duration) -> Duration { + let target_ns = target.as_nanos() as u64; + self.target_ns.store(target_ns, Ordering::SeqCst); + + loop { + let current = self.time_ns.load(Ordering::SeqCst); + if current >= target_ns { + return Duration::from_nanos(current); + } + // Wait for governor to advance time + let guard = self.state_mutex.lock(); + self.time_advanced.wait(guard); + } + } + + /// Non-blocking check: has virtual time reached target? + pub fn has_reached(&self, target: Duration) -> bool { + self.time_ns.load(Ordering::SeqCst) >= target.as_nanos() as u64 + } +} +``` + +### 2. RealTimeGovernor — Advances Virtual Clock + +Runs in its own thread. Advances the shared virtual clock based on speed multiplier: + +```rust +pub struct RealTimeGovernor { + clock: Arc, + tick_size_ns: u64, // Virtual nanoseconds per tick (e.g., 333) + speed_multiplier: f64, // 1.0 = real-time +} + +impl RealTimeGovernor { + fn run(&mut self) { + let mut last_wall_time = Instant::now(); + + loop { + match self.clock.mode() { + SimulationMode::RealTime => { + // Calculate how long to sleep to match speed_multiplier + let virtual_advance = Duration::from_nanos(self.tick_size_ns); + let target_wall = virtual_advance.div_f64(self.speed_multiplier); + + let elapsed = last_wall_time.elapsed(); + if elapsed < target_wall { + thread::sleep(target_wall - elapsed); + } + + // Advance virtual time + self.clock.advance_by(self.tick_size_ns); + last_wall_time = Instant::now(); + } + + SimulationMode::MaxSpeed => { + // Advance as fast as possible + self.clock.advance_by(self.tick_size_ns); + // Yield occasionally to prevent starving other threads + if self.clock.now().as_nanos() % 1000 == 0 { + thread::yield_now(); + } + } + + SimulationMode::SingleStep => { + // Wait for user to trigger a step + if self.clock.consume_step_trigger() { + self.clock.advance_by(self.tick_size_ns); + } else { + thread::sleep(Duration::from_millis(1)); + } + } + } + } + } +} +``` + +### 3. Component Thread Pattern + +Each component thread follows this pattern: + +```rust +// In ConfigurableSystem::start() — for each component: +let clock_clone = Arc::clone(&virtual_clock); +let comp_clone = Arc::clone(&component); + +thread::spawn(move || { + let mut next_update_time = Duration::ZERO; + let timing = ComponentTiming::for_component(&*comp_clone.lock()); + + loop { + // Wait for virtual time to reach our next scheduled update + let current_vtime = clock_clone.wait_until(next_update_time); + + if !clock_clone.is_running() { + break; + } + + // Lock component and update + if let Some(mut comp) = comp_clone.try_lock() { + comp.update_at_time(current_vtime); + } + + // Schedule next update based on component's timing needs + next_update_time = current_vtime + timing.minimum_update_interval(); + } +}); +``` + +### 4. Per-Component Timing with Virtual Time + +Components use virtual time instead of `Instant::now()`: + +```rust +// In Intel4001 (ROM): +impl Component for Intel4001 { + fn update_at_time(&mut self, virtual_time: Duration) { + let (phi1, phi2) = self.read_clock_pins(); + + let phi1_rising = phi1 == PinValue::High && self.prev_phi1 == PinValue::Low; + + if phi1_rising { + // Record when address was latched (in virtual time!) + self.address_latch_time = Some(virtual_time); + self.handle_phi1_rising(); + } + + if phi2_rising { + // Check if access time has elapsed IN VIRTUAL TIME + if let Some(latch_time) = self.address_latch_time { + let elapsed_virtual = virtual_time.saturating_sub(latch_time); + if elapsed_virtual >= self.timing.access_time { + // Data is valid — drive it + self.handle_phi2_rising(); + } else { + // Access time NOT yet elapsed — data not ready! + // This happens when the clock is overclocked + self.handle_data_not_ready(); + } + } + } + + self.prev_phi1 = phi1; + self.prev_phi2 = phi2; + } +} +``` + +### 5. Clock Component with Virtual Time + +```rust +// In TwoPhaseClock: +impl Component for TwoPhaseClock { + fn update_at_time(&mut self, virtual_time: Duration) { + // Check if it's time to transition (using virtual time) + let elapsed = virtual_time.saturating_sub(self.last_transition_vtime); + + match self.current_phase { + Phase::Phi1High => { + if elapsed >= self.phase_time { + self.phi1_state = PinValue::Low; + self.phi2_state = PinValue::High; + self.last_transition_vtime = virtual_time; + self.update_outputs(); + } + } + Phase::Phi2High => { + if elapsed >= self.phase_time { + self.phi2_state = PinValue::Low; + self.phi1_state = PinValue::High; + self.last_transition_vtime = virtual_time; + self.update_outputs(); + } + } + } + } +} +``` + +--- + +## Thread Architecture + +```mermaid +graph LR + subgraph "Governor Thread" + G[RealTimeGovernor] + G -->|advance_by tick_size| VC[VirtualClock] + end + + subgraph "Component Thread 1" + C1[TwoPhaseClock] + C1 -->|wait_until next_transition| VC + end + + subgraph "Component Thread 2" + C2[Intel4004] + C2 -->|wait_until next_cycle| VC + end + + subgraph "Component Thread 3" + C3[Intel4001] + C3 -->|wait_until access_time_elapsed| VC + end + + subgraph "Component Thread N" + CN[...] + CN -->|wait_until scheduled_time| VC + end + + VC -->|notify_all on advance| C1 + VC -->|notify_all on advance| C2 + VC -->|notify_all on advance| C3 + VC -->|notify_all on advance| CN +``` + +Each component thread: +1. Calls `virtual_clock.wait_until(target_time)` — blocks until the governor advances virtual time past the target +2. Wakes up, reads pins, processes its logic +3. Schedules its next wake-up time based on its timing constraints +4. Goes back to sleep + +The governor thread: +1. Advances virtual time by `tick_size` (e.g., 333ns) +2. Notifies all waiting component threads +3. Sleeps to match wall-clock time × speed_multiplier (if in RealTime mode) + +--- + +## Component Timing Model + +```rust +pub struct ComponentTiming { + pub access_time: Duration, // Time from address valid to data ready + pub propagation_delay: Duration, // Output delay after input change + pub setup_time: Duration, // Input stability before clock edge + pub hold_time: Duration, // Input stability after clock edge + pub max_clock_frequency: f64, // Maximum supported clock frequency + pub min_update_interval: Duration, // How often update_at_time should be called +} + +// Per-component defaults from datasheets: +impl Intel4001 { // ROM - 500ns access + fn timing() -> ComponentTiming { + ComponentTiming { + 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), // Check every 100ns + } + } +} + +impl Intel4002 { // RAM - 500ns access + fn timing() -> ComponentTiming { + ComponentTiming { + 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), + } + } +} + +impl Intel4003 { // Shift Register - faster 200ns + fn timing() -> ComponentTiming { + ComponentTiming { + 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, + min_update_interval: Duration::from_nanos(50), + } + } +} +``` + +--- + +## Speed Control + +| Mode | Multiplier | Governor Behavior | Use Case | +|------|-----------|-------------------|----------| +| RealTime | 0.001× | Sleep 333µs per 333ns tick | Observe individual clock edges | +| RealTime | 0.01× | Sleep 33.3µs per 333ns tick | Slow-motion debugging | +| RealTime | 0.1× | Sleep 3.33µs per 333ns tick | Slower than real-time | +| RealTime | 1.0× | Sleep 333ns per tick (≈ max speed) | Attempt real-time | +| MaxSpeed | ∞ | No sleep, yield occasionally | Fastest possible simulation | +| SingleStep | — | Advance one tick per trigger | Debug one phase at a time | + +**At 1.0× and above**, the OS cannot keep up (333ns is below scheduling granularity). The governor will run as fast as it can and report the **actual achieved speed** (e.g., "Target: 1.0×, Achieved: 0.47×"). + +--- + +## Overclocking Behavior + +When the clock frequency exceeds a component's `max_clock_frequency`: + +```mermaid +graph TD + OC[Clock set to 2 MHz - 500ns cycle] + ROM1[ROM access_time = 500ns] + RAM1[RAM access_time = 500ns] + SR1[ShiftReg access_time = 200ns] + + OC -->|Phase = 250ns| ROM1 + OC -->|Phase = 250ns| RAM1 + OC -->|Phase = 250ns| SR1 + + ROM1 -->|250ns < 500ns| FAIL1[ROM data NOT ready - CPU reads garbage] + RAM1 -->|250ns < 500ns| FAIL2[RAM data NOT ready - CPU reads garbage] + SR1 -->|250ns >= 200ns| OK[ShiftReg responds correctly - its faster] +``` + +This is **exactly what happens in real hardware** — the shift register (200ns access) keeps working while ROM/RAM (500ns access) fails. + +--- + +## Migration Strategy + +### Phase 1: Create VirtualClock (new file, non-breaking) + +- New file: `src/virtual_clock.rs` +- Contains `VirtualClock` struct with atomic virtual time, condvar, and speed control +- No changes to existing code + +### Phase 2: Create RealTimeGovernor (new file, non-breaking) + +- New file: `src/real_time_governor.rs` +- Governor thread that advances `VirtualClock` based on speed multiplier +- No changes to existing code + +### Phase 3: Create ComponentTiming (new file, non-breaking) + +- New file: `src/component_timing.rs` +- `ComponentTiming` struct and per-component defaults +- No changes to existing code + +### Phase 4: Extend Component trait with virtual time support + +- Add `update_at_time(&mut self, virtual_time: Duration)` with default that calls `update()` +- Add `timing(&self) -> ComponentTiming` with default that returns zero-delay timing +- Backward-compatible — existing `update()` still works + +### Phase 5: Migrate clock components to virtual time + +- `GenericClock` and `TwoPhaseClock`: Replace `Instant::now()` with virtual time parameter +- Clock transitions based on virtual time comparisons +- Clocks call `virtual_clock.wait_until(next_transition)` + +### Phase 6: Add timing enforcement to memory/CPU components + +- Intel4001, Intel4002, Intel4003: Check access times against virtual time +- Intel4004: Check setup/hold times +- Record timing violations for reporting + +### Phase 7: Update ConfigurableSystem to use VirtualClock + +- Create `Arc` shared by all components +- Governor thread manages virtual time advancement +- Component threads use `virtual_clock.wait_until()` instead of `thread::sleep()` +- Keep the per-component thread model — just change how they wait + +### Phase 8: Add speed/step controls to GUI and console + +- Speed slider (logarithmic: 0.001× to MaxSpeed) +- Step buttons: Step Phase, Step Cycle, Step Instruction +- Display: actual speed, virtual time, timing violations + +### Phase 9: Update tests + +- Tests use MaxSpeed mode for fast deterministic execution +- Tests use SingleStep mode for precise state verification +- Add overclocking tests: verify ROM fails at 2 MHz but shift register still works + +--- + +## Key Differences from Single-Scheduler Approach + +| Aspect | Single Scheduler | Per-Component Threads + VirtualClock | +|--------|-----------------|--------------------------------------| +| Threading model | 1 scheduler thread | N component threads + 1 governor thread | +| Component autonomy | Components are passive — called by scheduler | Components are active — they wait for their own timing | +| Realism | Less realistic — all components update in strict order | More realistic — components race independently, just like real hardware | +| Timing enforcement | Centralized in scheduler | Distributed — each component enforces its own timing | +| Race conditions | None — deterministic order | Possible — but that's **realistic**! Real hardware has races too | +| Speed control | Scheduler controls tick rate | Governor controls virtual clock rate | +| Complexity | Simpler | More complex but more faithful to hardware | + +--- + +## Benefits + +1. **Per-component threads preserved** — The project's signature feature remains +2. **Hardware-accurate timing** — Each component enforces its real timing constraints +3. **Overclocking realism** — Components fail when clocked too fast, just like real hardware +4. **Virtual time** — No dependency on OS scheduling granularity +5. **Speed control** — Logarithmic multiplier from 0.001× to MaxSpeed +6. **Single-step** — Step by phase, cycle, or instruction +7. **Realistic races** — Components compete for the bus independently, modeling real hardware behavior +8. **Backward compatible** — Phases 1-3 are additive; existing code continues to work during migration diff --git a/src/component.rs b/src/component.rs index f725485..7a2d49c 100644 --- a/src/component.rs +++ b/src/component.rs @@ -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>) { + // 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 {} diff --git a/src/component_timing.rs b/src/component_timing.rs new file mode 100644 index 0000000..2573205 --- /dev/null +++ b/src/component_timing.rs @@ -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")); + } +} diff --git a/src/components/clock/generic_clock.rs b/src/components/clock/generic_clock.rs index e8a3252..d770a83 100644 --- a/src/components/clock/generic_clock.rs +++ b/src/components/clock/generic_clock.rs @@ -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, // When pause started, to track pause duration + paused: bool, // Pause state for system control + pause_start_vtime: Option, // Virtual time when pause started + virtual_clock: Option>, // 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) { + 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>) { + self.virtual_clock = clock; + } + + fn has_virtual_clock(&self) -> bool { + self.virtual_clock.is_some() + } } // Advanced clock features diff --git a/src/components/clock/two_phase_clock.rs b/src/components/clock/two_phase_clock.rs index dc99650..2d6b032 100644 --- a/src/components/clock/two_phase_clock.rs +++ b/src/components/clock/two_phase_clock.rs @@ -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, // 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, // Virtual time when pause started + frequency: f64, // Store frequency for dynamic adjustment + virtual_clock: Option>, // 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) { + 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>) { + self.virtual_clock = clock; + } + + fn has_virtual_clock(&self) -> bool { + self.virtual_clock.is_some() + } } diff --git a/src/components/common/intel_400x.rs b/src/components/common/intel_400x.rs index 4cfae88..f112378 100644 --- a/src/components/common/intel_400x.rs +++ b/src/components/common/intel_400x.rs @@ -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, address_low_nibble: &mut Option, full_address_ready: &mut bool, - address_latch_time: &mut Option, + address_latch_time: &mut Option, _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, + address_latch_time: &Option, 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; - fn set_address_latch_time(&mut self, time: Option); + fn get_address_latch_time(&self) -> Option; + fn set_address_latch_time(&mut self, time: Option); fn get_full_address_ready(&self) -> bool; fn set_full_address_ready(&mut self, ready: bool); fn get_address_high_nibble(&self) -> Option; @@ -330,6 +340,10 @@ pub trait Intel400xTimingState { fn get_address_low_nibble(&self) -> Option; fn set_address_low_nibble(&mut self, nibble: Option); 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, - /// The time when the address was completely latched - address_latch_time: Option, + /// The time when the address was completely latched (virtual time in nanoseconds) + address_latch_time: Option, /// 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 { + /// Get address latch time (virtual time) + pub fn get_address_latch_time(&self) -> Option { self.address_latch_time } - /// Set address latch time - pub fn set_address_latch_time(&mut self, time: Option) { + /// Set address latch time (virtual time) + pub fn set_address_latch_time(&mut self, time: Option) { 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); diff --git a/src/components/cpu/intel_4004.rs b/src/components/cpu/intel_4004.rs index 8651302..5b089ea 100644 --- a/src/components/cpu/intel_4004.rs +++ b/src/components/cpu/intel_4004.rs @@ -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, // High nibble (OPR) read during M1 cycle, combined with M2 for 8-bit instruction // Timing and synchronization - address_latch_time: Option, // Timestamp when address was latched + address_latch_time: Option, // 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, // Maximum cycles before auto-stop (None = unlimited) + + // Virtual time support (Phase 6) + virtual_clock: Option>, // 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) { + 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>) { + self.virtual_clock = clock; + } + + fn has_virtual_clock(&self) -> bool { + self.virtual_clock.is_some() + } } impl RunnableComponent for Intel4004 { diff --git a/src/components/memory/intel_4001.rs b/src/components/memory/intel_4001.rs index 4b18c36..b6d49aa 100644 --- a/src/components/memory/intel_4001.rs +++ b/src/components/memory/intel_4001.rs @@ -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>, // 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 { + fn get_address_latch_time(&self) -> Option { self.state_machine.get_address_latch_time() } - fn set_address_latch_time(&mut self, time: Option) { + fn set_address_latch_time(&mut self, time: Option) { 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) { + 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>) { + self.virtual_clock = clock; + } + + fn has_virtual_clock(&self) -> bool { + self.virtual_clock.is_some() + } } impl RunnableComponent for Intel4001 {} diff --git a/src/components/memory/intel_4002.rs b/src/components/memory/intel_4002.rs index f39f1f6..ad82b45 100644 --- a/src/components/memory/intel_4002.rs +++ b/src/components/memory/intel_4002.rs @@ -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>, // 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 { + fn get_address_latch_time(&self) -> Option { self.state_machine.get_address_latch_time() } - fn set_address_latch_time(&mut self, time: Option) { + fn set_address_latch_time(&mut self, time: Option) { 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) { + 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>) { + self.virtual_clock = clock; + } + + fn has_virtual_clock(&self) -> bool { + self.virtual_clock.is_some() + } } impl RunnableComponent for Intel4002 {} diff --git a/src/components/memory/intel_4003.rs b/src/components/memory/intel_4003.rs index d29af71..3271d09 100644 --- a/src/components/memory/intel_4003.rs +++ b/src/components/memory/intel_4003.rs @@ -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>, // 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 { + fn get_address_latch_time(&self) -> Option { self.state_machine.get_address_latch_time() } - fn set_address_latch_time(&mut self, time: Option) { + fn set_address_latch_time(&mut self, time: Option) { 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) { + 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>) { + self.virtual_clock = clock; + } + + fn has_virtual_clock(&self) -> bool { + self.virtual_clock.is_some() + } } impl RunnableComponent for Intel4003 {} diff --git a/src/gui/components.rs b/src/gui/components.rs index fa5481f..0dbaf0b 100644 --- a/src/gui/components.rs +++ b/src/gui/components.rs @@ -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>>, + ) { + 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>>, + ) { + // 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>>, + 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, diff --git a/src/gui/console.rs b/src/gui/console.rs index 2b24325..1a05a02 100644 --- a/src/gui/console.rs +++ b/src/gui/console.rs @@ -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::() { + 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 ".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( diff --git a/src/gui/mod.rs b/src/gui/mod.rs index 47320b3..ac69058 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -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| { diff --git a/src/gui/state.rs b/src/gui/state.rs index 00723ad..4b7a19f 100644 --- a/src/gui/state.rs +++ b/src/gui/state.rs @@ -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, + /// 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, +} + +/// 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); diff --git a/src/lib.rs b/src/lib.rs index 4baa93d..b1a2300 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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}; diff --git a/src/main.rs b/src/main.rs index ae61ffd..b03d300 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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) diff --git a/src/real_time_governor.rs b/src/real_time_governor.rs new file mode 100644 index 0000000..82a95ee --- /dev/null +++ b/src/real_time_governor.rs @@ -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, + + /// 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, 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) -> 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(); + } +} diff --git a/src/system_config.rs b/src/system_config.rs index a958aa4..8e78f3d 100644 --- a/src/system_config.rs +++ b/src/system_config.rs @@ -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>, clock_enabled: Arc>, component_handles: Arc>>, + /// Shared virtual clock for timing-synchronized simulation. + /// All components and the RealTimeGovernor share this clock. + virtual_clock: Arc, + /// Handle for the governor thread so we can join it on shutdown. + governor_handle: Arc>>>, } 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>>>, @@ -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, clock: Arc) { + 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` 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 { + &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::() @@ -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::() @@ -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::() @@ -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::() @@ -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"); diff --git a/src/virtual_clock.rs b/src/virtual_clock.rs new file mode 100644 index 0000000..fabdfe5 --- /dev/null +++ b/src/virtual_clock.rs @@ -0,0 +1,1025 @@ +//! Virtual Clock for simulation timing synchronization. +//! +//! The `VirtualClock` provides a shared virtual time source that all component threads +//! synchronize against. Instead of using wall-clock time (`Instant::now()`) for timing +//! decisions, components use virtual time — a monotonically increasing nanosecond counter +//! that is independent of OS scheduling granularity. +//! +//! # Architecture +//! +//! ```text +//! Governor Thread Component Thread 1 Component Thread N +//! | | | +//! advance_by(tick_size) wait_until(target) wait_until(target) +//! | | | +//! v v v +//! [VirtualClock] <-- notify_all wakes all waiting threads +//! time_ns: AtomicU64 +//! ``` +//! +//! # How It Works +//! +//! 1. The `RealTimeGovernor` advances virtual time by `tick_size` nanoseconds +//! 2. All component threads that called `wait_until()` are woken via `condvar.notify_all()` +//! 3. Each component checks if virtual time has reached its target +//! 4. If yes: process the update, schedule next wake-up +//! 5. If no: go back to waiting +//! +//! This models real hardware where each chip operates independently but shares +//! a common timeline. Components can race each other, just like real chips. + +use std::sync::atomic::{AtomicBool, AtomicU64, AtomicU8, AtomicUsize, Ordering}; +use std::sync::{Condvar, Mutex}; +use std::time::{Duration, Instant}; + +/// Convert a Duration to u64 nanoseconds, panicking if it exceeds u64::MAX (~584 years). +/// +/// This is a safety check to prevent silent truncation when converting from +/// `Duration::as_nanos()` (u128) to u64. In practice, durations exceeding +/// u64::MAX nanoseconds are impossible in simulation, so a panic indicates a bug. +fn duration_to_nanos(d: Duration) -> u64 { + u64::try_from(d.as_nanos()).expect("Duration exceeds u64::MAX nanoseconds") +} + +/// Simulation mode controlling how virtual time advances. +#[derive(Debug, Clone, Copy, PartialEq)] +#[repr(u8)] +pub enum SimulationMode { + /// Virtual time advances at a rate determined by `speed_multiplier`. + /// At 1.0x, one virtual nanosecond maps to one wall-clock nanosecond. + /// At 0.1x, one virtual nanosecond takes 10 wall-clock nanoseconds. + RealTime = 0, + + /// Virtual time advances as fast as the host CPU allows. + /// No wall-clock sleeping — pure computation speed. + MaxSpeed = 1, + + /// Virtual time advances only when the user triggers a step. + /// Each step advances by `tick_size` nanoseconds. + SingleStep = 2, +} + +impl SimulationMode { + fn from_u8(value: u8) -> Self { + match value { + 0 => SimulationMode::RealTime, + 1 => SimulationMode::MaxSpeed, + 2 => SimulationMode::SingleStep, + _ => { + debug_assert!(false, "Invalid SimulationMode value: {}", value); + SimulationMode::RealTime + } + } + } +} + +/// Granularity for single-step mode. +/// +/// TODO: Integrate into `trigger_step()` / `consume_step()` to allow stepping +/// by clock phase, cycle, or instruction granularity. +#[derive(Debug, Clone, Copy, PartialEq)] +#[allow(dead_code)] // Planned for future use in single-step control +pub enum StepGranularity { + /// Advance by one clock phase (e.g., PHI1 high → PHI1 low). + /// For a 750 kHz clock, this is ~666 ns of virtual time. + ClockPhase, + + /// Advance by one full clock cycle (PHI1 high → PHI1 low → PHI2 high → PHI2 low). + /// For a 750 kHz clock, this is ~1333 ns of virtual time. + ClockCycle, + + /// Advance by one complete CPU instruction (8 or 16 clock cycles). + /// For a 750 kHz clock, this is ~10.6–21.3 µs of virtual time. + Instruction, +} + +/// Internal state protected by the condvar mutex. +struct ClockState { + /// Flag indicating the simulation is paused. + paused: bool, + /// Flag indicating the simulation has been stopped (cancelled). + /// Used to wake threads in `wait_for_start()` that are blocking + /// before the simulation was ever started. + stopped: bool, +} + +/// Shared virtual clock for simulation timing synchronization. +/// +/// This struct is wrapped in `Arc` and shared between: +/// - The `RealTimeGovernor` thread (advances time) +/// - All component threads (wait for time to reach their scheduled update) +/// +/// # Thread Safety +/// +/// All time values are stored as `AtomicU64` for lock-free reads. +/// The `Condvar` is used to efficiently wake waiting threads when time advances. +/// +/// # Lock Ordering +/// +/// The `VirtualClock` has an internal `state_mutex` used for condvar synchronization. +/// When integrating with external mutexes (e.g., `parking_lot::Mutex`), +/// the following lock ordering must be observed to prevent deadlocks: +/// +/// **Order: External mutex → VirtualClock.state_mutex** +/// +/// ```text +/// ConfigurableSystem mutex (parking_lot) +/// └→ VirtualClock.state_mutex (std::sync) +/// └→ No further locks acquired +/// ``` +/// +/// This ordering is safe because: +/// - The GUI thread acquires `ConfigurableSystem` first, then reads VirtualClock atomics +/// - Component threads never acquire `ConfigurableSystem`; they only access VirtualClock +/// - The governor thread only accesses VirtualClock +/// +/// **Do NOT** acquire `ConfigurableSystem` mutex while holding `VirtualClock.state_mutex`. +/// +/// # Example +/// +/// ```rust,ignore +/// use std::sync::Arc; +/// use std::time::Duration; +/// use rusty_emu::virtual_clock::VirtualClock; +/// +/// let clock = Arc::new(VirtualClock::new(Duration::from_nanos(333))); +/// +/// // In governor thread: +/// clock.advance_by(Duration::from_nanos(333)); +/// +/// // In component thread: +/// let current_time = clock.wait_until(Duration::from_nanos(666)); +/// // Component can now process its update at virtual time 666ns +/// ``` +pub struct VirtualClock { + /// Current virtual time in nanoseconds (monotonically increasing). + time_ns: AtomicU64, + + /// Virtual time advance per tick in nanoseconds. + tick_size_ns: AtomicU64, + + /// Speed multiplier stored as fixed-point: actual_value = raw / 1_000_000_000. + /// This allows atomic updates without floating-point. + /// Examples: + /// 1.0x = 1_000_000_000 + /// 0.1x = 100_000_000 + /// 0.001x = 1_000_000 + /// 10.0x = 10_000_000_000 + speed_multiplier_fp: AtomicU64, + + /// Current simulation mode (RealTime, MaxSpeed, SingleStep). + mode: AtomicU8, + + /// Whether the simulation is running (used to signal component threads to exit). + running: AtomicBool, + + /// Single-step trigger: set to true by user, consumed by governor. + step_pending: AtomicBool, + + /// Counter for batching notify_all calls. Only every NOTIFY_INTERVAL-th + /// call to advance_by() actually notifies waiting threads. This reduces + /// mutex contention in MaxSpeed mode where advance_by() is called in a + /// tight loop. + notify_counter: AtomicU64, + + /// Debug-only: tracks which thread last called advance_by() to detect + /// concurrent callers. 0 means no owner yet. Each thread gets a unique + /// ID via a global counter (see advance_by). + #[cfg(debug_assertions)] + advance_owner: AtomicUsize, + + /// Condvar for notifying component threads when virtual time advances. + time_advanced: Condvar, + + /// Mutex for condvar wait. + state_mutex: Mutex, + +} + +impl VirtualClock { + /// Create a new VirtualClock with the specified tick size. + /// + /// # Parameters + /// - `tick_size`: Virtual time advance per tick. For a 750 kHz two-phase clock, + /// use `Duration::from_nanos(333)` (1/4 of the 1333ns cycle). + /// + /// # Returns + /// A new VirtualClock initialized at virtual time 0, speed 1.0x, RealTime mode. + pub fn new(tick_size: Duration) -> Self { + VirtualClock { + time_ns: AtomicU64::new(0), + tick_size_ns: AtomicU64::new(duration_to_nanos(tick_size)), + speed_multiplier_fp: AtomicU64::new(1_000_000_000), // 1.0x + mode: AtomicU8::new(SimulationMode::RealTime as u8), + running: AtomicBool::new(false), + step_pending: AtomicBool::new(false), + notify_counter: AtomicU64::new(0), + #[cfg(debug_assertions)] + advance_owner: AtomicUsize::new(0), + time_advanced: Condvar::new(), + state_mutex: Mutex::new(ClockState { paused: false, stopped: false }), + } + } + + // ========================================================================= + // Time Read Operations (lock-free) + // ========================================================================= + + /// Get the current virtual time. + /// + /// This is lock-free and can be called from any thread at any time. + #[inline] + pub fn now(&self) -> Duration { + Duration::from_nanos(self.time_ns.load(Ordering::Acquire)) + } + + /// Get the current virtual time as raw nanoseconds. + /// + /// Useful for high-performance comparisons where `Duration` overhead matters. + #[inline] + pub fn now_ns(&self) -> u64 { + self.time_ns.load(Ordering::Acquire) + } + + /// Get the current tick size. + pub fn tick_size(&self) -> Duration { + Duration::from_nanos(self.tick_size_ns.load(Ordering::Acquire)) + } + + /// Get the current tick size as raw nanoseconds. + /// + /// Useful for high-performance calculations where `Duration` overhead matters. + #[inline] + pub fn tick_size_ns(&self) -> u64 { + self.tick_size_ns.load(Ordering::Acquire) + } + + /// Get the total number of ticks elapsed since start. + /// + /// Computed from `time_ns / tick_size_ns` for consistency — avoids + /// maintaining a separate counter that could diverge from the actual time. + /// + /// **Note:** This is an approximate value. The two atomic loads (`time_ns` + /// and `tick_size_ns`) are not performed atomically. If `set_tick_size()` + /// is called concurrently from another thread, the result may be briefly + /// inconsistent. For precise timing decisions, use `wait_until()` instead. + pub fn tick_count(&self) -> u64 { + let time = self.time_ns.load(Ordering::Acquire); + let tick = self.tick_size_ns.load(Ordering::Acquire); + if tick == 0 { return 0; } + time / tick + } + + // ========================================================================= + // Time Wait Operations (blocking) + // ========================================================================= + + /// Block the calling thread until virtual time reaches at least `target`. + /// + /// This is the primary synchronization mechanism for component threads. + /// The thread will sleep efficiently (via condvar) until the governor + /// advances virtual time past the target. + /// + /// # Parameters + /// - `target`: The minimum virtual time to wait for. + /// + /// # Returns + /// The actual virtual time when the thread was woken (>= target). + /// + /// # Cancellation + /// Returns `None` if the simulation is stopped while waiting. + /// + /// # Pause Behavior + /// When the simulation is paused, this method blocks indefinitely (the condvar + /// wait will not return until `resume()` or `stop()` is called). If `resume()` + /// is called, the thread re-enters the wait loop. If `stop()` is called, the + /// thread returns `None`. This means paused components do not consume CPU — they + /// efficiently sleep until the simulation is resumed or stopped. + pub fn wait_until(&self, target: Duration) -> Option { + // NOTE: All component threads are woken on every tick via notify_all(), + // even if their individual target hasn't been reached. For large numbers + // of components, this causes O(N) spurious wakeups per tick. With + // NOTIFY_INTERVAL=10 batching at 750 kHz, this means ~300K wakeups/sec. + // Acceptable for current component counts (~10), but if this becomes a + // bottleneck, consider per-target notification (e.g., min-heap of targets) + // or timed wait batches. + let target_ns = duration_to_nanos(target); + + // Fast path: check if time has already reached target (lock-free) + let current = self.time_ns.load(Ordering::Acquire); + if current >= target_ns { + // SeqCst to match stop()'s SeqCst store — ensures we honor the + // "Returns None if the simulation is stopped" contract even on + // the fast path. + if !self.running.load(Ordering::SeqCst) { + return None; + } + return Some(Duration::from_nanos(current)); + } + + // Slow path: acquire mutex to safely wait on condvar. + // All checks are done under the mutex to prevent missed-wakeup races + // with stop() — if stop() calls notify_all() while we hold the lock, + // we'll see the running=false on the next iteration. + let mut guard = self.state_mutex.lock().unwrap_or_else(|e| e.into_inner()); + + loop { + // Fast path: check if time has reached target (common case). + // Acquire is sufficient for time_ns: we need to see the latest value + // written by advance_by()'s Release store. The condvar wait already + // provides the necessary synchronization guarantee (the mutex ensures + // we can't miss a notify_all from advance_by). + let current = self.time_ns.load(Ordering::Acquire); + if current >= target_ns { + return Some(Duration::from_nanos(current)); + } + // SeqCst to match stop()'s SeqCst store — ensures we see the + // stopped state even if notify_all races with our wait(). + if !self.running.load(Ordering::SeqCst) { + return None; + } + // If paused, stay sleeping efficiently (no CPU waste). + // When resume() notifies, we'll re-check conditions. + if guard.paused { + guard = self.time_advanced.wait(guard).unwrap_or_else(|e| e.into_inner()); + continue; + } + guard = self.time_advanced.wait(guard).unwrap_or_else(|e| e.into_inner()); + } + } + + /// Non-blocking check: has virtual time reached the target? + /// + /// Use this for polling-based component loops that prefer to do useful work + /// rather than block. + #[inline] + pub fn has_reached(&self, target: Duration) -> bool { + self.time_ns.load(Ordering::Acquire) >= duration_to_nanos(target) + } + + /// Wait until virtual time reaches target, with a timeout. + /// + /// Useful for component threads that need to periodically check for + /// external events (e.g., GUI updates) even if virtual time hasn't advanced. + /// + /// # Returns + /// - `Ok(Duration)`: Virtual time reached the target + /// - `Err(())`: Timed out or simulation stopped + pub fn wait_until_with_timeout(&self, target: Duration, timeout: Duration) -> Result { + let target_ns = duration_to_nanos(target); + let deadline = Instant::now() + timeout; + + let mut guard = self.state_mutex.lock().unwrap_or_else(|e| e.into_inner()); + + loop { + // If paused, stay sleeping (timeout still applies). + // When resume() notifies, we'll re-check conditions. + if guard.paused { + // SeqCst to match stop()'s SeqCst store + if !self.running.load(Ordering::SeqCst) { + return Err(()); + } + let remaining = deadline.saturating_duration_since(Instant::now()); + if remaining.is_zero() { + return Err(()); + } + let result = self.time_advanced.wait_timeout(guard, remaining); + match result { + Ok((new_guard, timeout_result)) => { + guard = new_guard; + if timeout_result.timed_out() { + return Err(()); + } + } + Err(poisoned) => { + (guard, _) = poisoned.into_inner(); + } + } + continue; + } + let current = self.time_ns.load(Ordering::Acquire); + if current >= target_ns { + return Ok(Duration::from_nanos(current)); + } + // SeqCst to match stop()'s SeqCst store + if !self.running.load(Ordering::SeqCst) { + return Err(()); + } + + let remaining = deadline.saturating_duration_since(Instant::now()); + if remaining.is_zero() { + return Err(()); + } + let result = self.time_advanced.wait_timeout(guard, remaining); + match result { + Ok((new_guard, timeout_result)) => { + guard = new_guard; + if timeout_result.timed_out() { + return Err(()); + } + } + Err(poisoned) => { + // Mutex was poisoned — recover the guard and re-check conditions + (guard, _) = poisoned.into_inner(); + } + } + } + } + + // ========================================================================= + // Time Advance Operations (called by governor only) + // ========================================================================= + + /// Advance virtual time by the specified duration. + /// + /// This is called by the `RealTimeGovernor` to move simulation time forward. + /// All component threads waiting in `wait_until()` will be woken. + /// + /// # Parameters + /// - `delta`: Amount of virtual time to advance. + /// + /// # Visibility + /// This method is `pub(crate)` because it must only be called by the + /// governor thread. Calling it from component threads would corrupt + /// the simulation timeline. + pub(crate) fn advance_by(&self, delta: Duration) { + let delta_ns = duration_to_nanos(delta); + if delta_ns == 0 { + return; + } + + // Debug-only guard: advance_by must only be called from a single thread + // (the governor). Concurrent calls would produce correct fetch_add results + // but could wake components at intermediate time values. + #[cfg(debug_assertions)] + { + use std::cell::Cell; + use std::sync::atomic::AtomicUsize; + // Global counter for assigning unique per-thread IDs. + static NEXT_THREAD_ID: AtomicUsize = AtomicUsize::new(1); + thread_local! { + static THREAD_ID: Cell = const { Cell::new(0) }; + } + let tid = THREAD_ID.with(|t| { + let id = t.get(); + if id == 0 { + let assigned = NEXT_THREAD_ID.fetch_add(1, Ordering::Relaxed); + t.set(assigned); + assigned + } else { + id + } + }); + // Per-instance owner tracking: different VirtualClock instances + // (e.g., in different tests) can have different owner threads. + let prev = self.advance_owner.load(Ordering::SeqCst); + debug_assert!( + prev == 0 || prev == tid, + "advance_by() called from multiple threads — only the governor should advance time" + ); + self.advance_owner.store(tid, Ordering::SeqCst); + } + + // SeqCst ensures that woken component threads always see the latest time_ns value, + // even on weakly-ordered architectures (ARM, RISC-V). The Release/Acquire pair that + // was here previously is sufficient on x86-64 (TSO) but could allow stale reads on + // weaker memory models when combined with the NOTIFY_INTERVAL batching — a thread + // woken by a later notify_all() might not see the cumulative effect of intermediate + // Release stores. SeqCst provides total ordering at negligible cost on x86-64. + self.time_ns.fetch_add(delta_ns, Ordering::SeqCst); + + // Batch notifications: only acquire mutex and call notify_all() every + // NOTIFY_INTERVAL ticks. This reduces syscall overhead from ~3M/s to ~300K/s + // at 750 kHz simulation speed, while keeping wakeup latency under ~3.3µs. + let count = self.notify_counter.fetch_add(1, Ordering::Relaxed); + if count % Self::NOTIFY_INTERVAL == 0 { + // Hold the mutex while notifying to prevent a missed-wakeup race: + // Without this, a waiter could load time_ns (see old value), then we + // update time_ns AND call notify_all(), then the waiter enters wait() + // — missing the notification entirely. + let _guard = self.state_mutex.lock().unwrap_or_else(|e| e.into_inner()); + // We use notify_all (not notify_one) because multiple components + // may be waiting for different target times, and any of them might + // now have their target satisfied. + self.time_advanced.notify_all(); + } + } + + /// Number of advance_by() calls between notify_all() calls. + /// Reduces mutex/syscall overhead in MaxSpeed mode where ticks happen + /// in a tight loop. Components may observe slightly stale time values + /// (up to NOTIFY_INTERVAL ticks behind), but this is acceptable because: + /// - Components that need precise timing use wait_until() which always + /// wakes on the next notification + /// - The maximum staleness is NOTIFY_INTERVAL × tick_size (e.g., 10 × 333ns = 3.33µs) + const NOTIFY_INTERVAL: u64 = 10; + + /// Advance virtual time by one tick size. + /// + /// Convenience method equivalent to `self.advance_by(self.tick_size())`. + /// + /// # Visibility + /// This method is `pub(crate)` because it must only be called by the + /// governor thread. Calling it from component threads would corrupt + /// the simulation timeline. + pub(crate) fn advance_one_tick(&self) { + let tick_ns = self.tick_size_ns.load(Ordering::Acquire); + self.advance_by(Duration::from_nanos(tick_ns)); + } + + // ========================================================================= + // Speed Control + // ========================================================================= + + /// Get the current speed multiplier. + /// + /// Returns the multiplier as a floating-point value: + /// - 1.0 = real-time + /// - 0.1 = 10× slower than real-time + /// - 10.0 = 10× faster than real-time (limited by host CPU) + pub fn speed_multiplier(&self) -> f64 { + self.speed_multiplier_fp.load(Ordering::Acquire) as f64 / 1_000_000_000.0 + } + + /// Set the speed multiplier. + /// + /// # Parameters + /// - `multiplier`: Speed multiplier. Must be > 0.0. + /// - 0.001 = 1000× slower than real-time + /// - 1.0 = real-time + /// - 100.0 = 100× faster than real-time (limited by host CPU) + /// + /// # Panics + /// Panics if `multiplier` is not positive (<= 0.0). + pub fn set_speed_multiplier(&self, multiplier: f64) { + assert!(multiplier > 0.0, "Speed multiplier must be positive, got {}", multiplier); + let fp_value = (multiplier * 1_000_000_000.0).round().clamp(1.0, u64::MAX as f64) as u64; + // Verify round-trip to detect precision loss from f64 → u64 fixed-point conversion. + // This catches values that cannot be represented in our fixed-point format. + let reconstructed = fp_value as f64 / 1_000_000_000.0; + assert!( + (reconstructed - multiplier).abs() <= 0.000_000_001, + "Speed multiplier {} cannot be represented with sufficient precision (stored as {}). \ + Use a value with at most ~9 significant decimal digits.", + multiplier, reconstructed + ); + self.speed_multiplier_fp.store(fp_value, Ordering::Release); + } + + /// Get the simulation mode. + pub fn mode(&self) -> SimulationMode { + SimulationMode::from_u8(self.mode.load(Ordering::Acquire)) + } + + /// Set the simulation mode. + pub fn set_mode(&self, mode: SimulationMode) { + self.mode.store(mode as u8, Ordering::Release); + // Hold mutex during notify to prevent missed-wakeup race with wait_until() + { + let _guard = self.state_mutex.lock().unwrap_or_else(|e| e.into_inner()); + self.time_advanced.notify_all(); + } + } + + /// Set the tick size. + /// + /// This changes the virtual time advance per tick. + /// Can be called while the simulation is running. + /// + /// # Panics + /// Panics if `tick_size` is zero. A zero tick size would cause division-by-zero + /// in `tick_count()` and produce no time advancement in the governor loop. + pub fn set_tick_size(&self, tick_size: Duration) { + let ns = duration_to_nanos(tick_size); + assert!(ns > 0, "tick_size must be non-zero"); + self.tick_size_ns.store(ns, Ordering::Release); + } + + // ========================================================================= + // Single-Step Control + // ========================================================================= + + /// Trigger a single step in SingleStep mode. + /// + /// Returns `false` if a step is already pending (not yet consumed by governor). + pub fn trigger_step(&self) -> bool { + // Only allow stepping in SingleStep mode + if self.mode() != SimulationMode::SingleStep { + return false; + } + // Try to set step_pending from false to true + self.step_pending + .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) + .is_ok() + } + + /// Consume a pending step trigger (called by governor). + /// + /// Returns `true` if a step was pending and has been consumed. + pub fn consume_step(&self) -> bool { + self.step_pending + .compare_exchange(true, false, Ordering::SeqCst, Ordering::SeqCst) + .is_ok() + } + + /// Check if a step is pending without consuming it. + pub fn is_step_pending(&self) -> bool { + self.step_pending.load(Ordering::Acquire) + } + + // ========================================================================= + // Lifecycle Control + // ========================================================================= + + /// Start the simulation. + /// + /// Sets the running flag and wakes all waiting threads. + /// This method is idempotent — calling it when already running has no effect + /// beyond waking currently waiting threads. + pub fn start(&self) { + let was_stopped = self.running.compare_exchange( + false, true, Ordering::SeqCst, Ordering::SeqCst + ).is_ok(); + if was_stopped { + // Hold mutex during notify to prevent missed-wakeup race with wait_until() + let _guard = self.state_mutex.lock().unwrap_or_else(|e| e.into_inner()); + self.time_advanced.notify_all(); + } + } + + /// Stop the simulation. + /// + /// Clears the running flag and wakes all waiting threads so they can exit. + /// Also sets the internal `stopped` flag so that threads blocked in + /// `wait_for_start()` can detect cancellation and return `false`. + pub fn stop(&self) { + self.running.store(false, Ordering::SeqCst); + // Hold mutex during notify to prevent missed-wakeup race with wait_until() + { + let mut guard = self.state_mutex.lock().unwrap_or_else(|e| e.into_inner()); + guard.stopped = true; + self.time_advanced.notify_all(); + } + } + + /// Check if the simulation is running. + #[inline] + pub fn is_running(&self) -> bool { + self.running.load(Ordering::SeqCst) + } + + /// Pause the simulation (virtual time stops advancing). + /// + /// The governor will stop advancing time but component threads remain alive. + /// Waiting threads are notified so they can re-check the pause state. + pub fn pause(&self) { + { + let mut guard = self.state_mutex.lock().unwrap_or_else(|e| e.into_inner()); + guard.paused = true; + // Hold mutex during notify to prevent missed-wakeup race with wait_until() + self.time_advanced.notify_all(); + } + } + + /// Resume the simulation from paused state. + pub fn resume(&self) { + { + let mut guard = self.state_mutex.lock().unwrap_or_else(|e| e.into_inner()); + guard.paused = false; + // Hold mutex during notify to prevent missed-wakeup race with wait_until() + self.time_advanced.notify_all(); + } + } + + /// Check if the simulation is paused. + pub fn is_paused(&self) -> bool { + self.state_mutex + .lock() + .map(|g| g.paused) + .unwrap_or(false) + } + + /// Block the calling thread until the simulation is unpaused. + /// + /// This is used by the governor to efficiently wait (zero CPU) while paused, + /// instead of busy-polling with `thread::sleep`. Returns immediately if not + /// currently paused. + /// + /// # Returns + /// - `true`: Simulation was resumed (or was never paused). + /// - `false`: Simulation was stopped while paused. + pub fn wait_for_resume(&self) -> bool { + let mut guard = self.state_mutex.lock().unwrap_or_else(|e| e.into_inner()); + while guard.paused { + // SeqCst to match stop()'s SeqCst store + if !self.running.load(Ordering::SeqCst) { + return false; + } + guard = self.time_advanced.wait(guard).unwrap_or_else(|e| e.into_inner()); + } + true + } + + /// Block the calling thread until the simulation has been started. + /// + /// This is used by component threads to safely wait for the governor to call + /// `start()` before entering their main loop. Without this, components that call + /// `wait_until()` before the governor starts will see `running == false` and + /// immediately return `None`. + /// + /// # Returns + /// - `true`: Simulation was started. + /// - `false`: Simulation was stopped before starting (should not normally happen). + /// + /// # Deadlock Prevention + /// If `stop()` is called before `start()`, this method returns `false` instead + /// of blocking forever. This prevents deadlock when the simulation is torn down + /// during initialization. + /// + /// # Panic Safety + /// If the governor thread panics before calling `start()` and no other thread + /// calls `stop()`, component threads will block forever in this method. To + /// prevent this, ensure `stop()` is called in the governor's cleanup path + /// (e.g., via `Drop` or a panic hook), or use `wait_until_with_timeout()` + /// instead for time-bounded waiting. + pub fn wait_for_start(&self) -> bool { + let mut guard = self.state_mutex.lock().unwrap_or_else(|e| e.into_inner()); + loop { + // SeqCst to match start()'s SeqCst compare_exchange + if self.running.load(Ordering::SeqCst) { + return true; + } + if guard.stopped { + return false; + } + guard = self.time_advanced.wait(guard).unwrap_or_else(|e| e.into_inner()); + } + } + + /// Reset the virtual clock to time zero and clear all state. + /// + /// This is equivalent to calling `stop()` followed by a full state reset. + /// After `reset()`, the clock is in the same state as a freshly created one + /// (except `tick_size_ns` is preserved as a config parameter). + /// + /// Safe to call whether the clock is running or stopped — if running, it will + /// be stopped first (setting `running = false` under the mutex to prevent + /// TOCTOU races with `start()`). + pub fn reset(&self) { + // Hold the state mutex while stopping AND performing the stores. + // This prevents a TOCTOU race where another thread calls start() between + // dropping the mutex and the subsequent atomic stores. + let mut guard = self.state_mutex.lock().unwrap_or_else(|e| e.into_inner()); + // Stop the clock under the mutex so start() cannot interleave + self.running.store(false, Ordering::SeqCst); + guard.paused = false; + guard.stopped = false; + + // Perform stores while still holding the mutex so that start() + // (which also acquires the mutex) cannot interleave. + self.time_ns.store(0, Ordering::Release); + self.step_pending.store(false, Ordering::Release); + // Reset to initial state — callers expect a clean slate after reset(). + self.speed_multiplier_fp.store(1_000_000_000, Ordering::Release); // 1.0× + self.mode.store(SimulationMode::RealTime as u8, Ordering::Release); + // Reset notification counter to ensure the first advance_by() after + // restart triggers an immediate notify_all(). Without this, the counter + // retains its pre-reset value, shifting the batching phase unpredictably. + self.notify_counter.store(0, Ordering::Release); + // tick_size_ns is intentionally NOT reset — it's a config parameter + // that should persist across resets (e.g., set during system construction). + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::Arc; + use std::thread; + use std::time::Duration; + + #[test] + fn test_virtual_clock_creation() { + let clock = VirtualClock::new(Duration::from_nanos(333)); + assert_eq!(clock.now(), Duration::ZERO); + assert_eq!(clock.tick_size(), Duration::from_nanos(333)); + assert!((clock.speed_multiplier() - 1.0).abs() < 0.001); + assert_eq!(clock.mode(), SimulationMode::RealTime); + assert!(!clock.is_running()); + } + + #[test] + fn test_advance_by() { + let clock = VirtualClock::new(Duration::from_nanos(333)); + + clock.advance_by(Duration::from_nanos(100)); + assert_eq!(clock.now(), Duration::from_nanos(100)); + + clock.advance_by(Duration::from_nanos(200)); + assert_eq!(clock.now(), Duration::from_nanos(300)); + } + + #[test] + fn test_advance_one_tick() { + let clock = VirtualClock::new(Duration::from_nanos(333)); + + clock.advance_one_tick(); + assert_eq!(clock.now(), Duration::from_nanos(333)); + + clock.advance_one_tick(); + assert_eq!(clock.now(), Duration::from_nanos(666)); + } + + #[test] + fn test_has_reached() { + let clock = VirtualClock::new(Duration::from_nanos(333)); + + assert!(!clock.has_reached(Duration::from_nanos(100))); + clock.advance_by(Duration::from_nanos(100)); + assert!(clock.has_reached(Duration::from_nanos(100))); + assert!(!clock.has_reached(Duration::from_nanos(200))); + } + + #[test] + fn test_speed_multiplier() { + let clock = VirtualClock::new(Duration::from_nanos(333)); + + clock.set_speed_multiplier(0.5); + assert!((clock.speed_multiplier() - 0.5).abs() < 0.001); + + clock.set_speed_multiplier(10.0); + assert!((clock.speed_multiplier() - 10.0).abs() < 0.01); + + clock.set_speed_multiplier(0.001); + assert!((clock.speed_multiplier() - 0.001).abs() < 0.0001); + } + + #[test] + fn test_simulation_mode() { + let clock = VirtualClock::new(Duration::from_nanos(333)); + + clock.set_mode(SimulationMode::MaxSpeed); + assert_eq!(clock.mode(), SimulationMode::MaxSpeed); + + clock.set_mode(SimulationMode::SingleStep); + assert_eq!(clock.mode(), SimulationMode::SingleStep); + + clock.set_mode(SimulationMode::RealTime); + assert_eq!(clock.mode(), SimulationMode::RealTime); + } + + #[test] + fn test_single_step() { + let clock = VirtualClock::new(Duration::from_nanos(333)); + clock.set_mode(SimulationMode::SingleStep); + + // Can't step in non-SingleStep mode + clock.set_mode(SimulationMode::RealTime); + assert!(!clock.trigger_step()); + + clock.set_mode(SimulationMode::SingleStep); + + // Trigger a step + assert!(clock.trigger_step()); + assert!(clock.is_step_pending()); + + // Can't trigger again while pending + assert!(!clock.trigger_step()); + + // Consume the step + assert!(clock.consume_step()); + assert!(!clock.is_step_pending()); + + // Can't consume when none pending + assert!(!clock.consume_step()); + } + + #[test] + fn test_lifecycle() { + let clock = VirtualClock::new(Duration::from_nanos(333)); + + assert!(!clock.is_running()); + + clock.start(); + assert!(clock.is_running()); + + clock.pause(); + assert!(clock.is_paused()); + + clock.resume(); + assert!(!clock.is_paused()); + + clock.stop(); + assert!(!clock.is_running()); + } + + #[test] + fn test_reset() { + let clock = VirtualClock::new(Duration::from_nanos(333)); + + clock.advance_by(Duration::from_nanos(1000)); + assert_eq!(clock.now(), Duration::from_nanos(1000)); + + clock.reset(); + assert_eq!(clock.now(), Duration::ZERO); + } + + #[test] + fn test_wait_until_immediate() { + let clock = Arc::new(VirtualClock::new(Duration::from_nanos(333))); + clock.start(); + + // If time has already passed, wait_until should return immediately + clock.advance_by(Duration::from_nanos(1000)); + + let result = clock.wait_until(Duration::from_nanos(500)); + assert!(result.is_some()); + assert!(result.unwrap() >= Duration::from_nanos(500)); + } + + #[test] + fn test_wait_until_blocks_then_returns() { + let clock = Arc::new(VirtualClock::new(Duration::from_nanos(333))); + clock.start(); + + let clock_clone = Arc::clone(&clock); + let handle = thread::spawn(move || { + // This should block until time reaches 1000ns + clock_clone.wait_until(Duration::from_nanos(1000)) + }); + + // Give the thread time to start waiting + thread::sleep(Duration::from_millis(50)); + + // Advance time past the target + clock.advance_by(Duration::from_nanos(1500)); + + let result = handle.join().expect("Thread panicked"); + assert!(result.is_some()); + assert!(result.unwrap() >= Duration::from_nanos(1000)); + } + + #[test] + fn test_wait_until_returns_none_on_stop() { + let clock = Arc::new(VirtualClock::new(Duration::from_nanos(333))); + clock.start(); + + let clock_clone = Arc::clone(&clock); + let handle = thread::spawn(move || { + // This should block until stopped + clock_clone.wait_until(Duration::from_nanos(1_000_000)) + }); + + // Give the thread time to start waiting + thread::sleep(Duration::from_millis(50)); + + // Stop the simulation + clock.stop(); + + let result = handle.join().expect("Thread panicked"); + assert!(result.is_none()); + } + + #[test] + fn test_multiple_waiters() { + let clock = Arc::new(VirtualClock::new(Duration::from_nanos(100))); + clock.start(); + + let clock1 = Arc::clone(&clock); + let clock2 = Arc::clone(&clock); + let clock3 = Arc::clone(&clock); + + let h1 = thread::spawn(move || clock1.wait_until(Duration::from_nanos(300))); + let h2 = thread::spawn(move || clock2.wait_until(Duration::from_nanos(200))); + let h3 = thread::spawn(move || clock3.wait_until(Duration::from_nanos(500))); + + // Advance past all targets + thread::sleep(Duration::from_millis(10)); + clock.advance_by(Duration::from_nanos(600)); + + assert!(h1.join().unwrap().is_some()); + assert!(h2.join().unwrap().is_some()); + assert!(h3.join().unwrap().is_some()); + } + + #[test] + fn test_tick_count() { + let clock = VirtualClock::new(Duration::from_nanos(333)); + + assert_eq!(clock.tick_count(), 0); + + clock.advance_one_tick(); + assert_eq!(clock.tick_count(), 1); + + clock.advance_one_tick(); + assert_eq!(clock.tick_count(), 2); + + // advance_by(100ns) with tick_size=333ns → total 766ns / 333 = 2 (integer division) + clock.advance_by(Duration::from_nanos(100)); + assert_eq!(clock.tick_count(), 2); // 766 / 333 = 2 + + // One more full tick → total 1099ns / 333 = 3 + clock.advance_one_tick(); + assert_eq!(clock.tick_count(), 3); // 1099 / 333 = 3 + } +} diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 4d858b3..361f99b 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -81,8 +81,8 @@ mod intel_4001_integration_tests { // Test access time configuration assert_eq!(rom.get_access_time(), 1); - // Test address latch time handling - let test_time = std::time::Instant::now(); + // Test address latch time handling (virtual time: Duration-based) + let test_time = std::time::Duration::from_nanos(500); rom.set_address_latch_time(Some(test_time)); assert_eq!(rom.get_address_latch_time(), Some(test_time)); } diff --git a/tests/intel_400x_tests.rs b/tests/intel_400x_tests.rs index a0cde16..2a4cadc 100644 --- a/tests/intel_400x_tests.rs +++ b/tests/intel_400x_tests.rs @@ -6,7 +6,6 @@ use rusty_emu::pin::{Pin, PinValue}; use std::any::Any; use std::collections::HashMap; use std::sync::Arc; -use std::time::Instant; // Mock component for testing trait implementations #[derive(Debug)] @@ -247,7 +246,7 @@ mod tests { let mut high_nibble: Option = None; let mut low_nibble: Option = None; let mut full_address_ready = false; - let mut address_latch_time: Option = None; + let mut address_latch_time: Option = None; let access_time = Duration::from_nanos(100); // Mock address handler for testing the algorithm @@ -275,6 +274,7 @@ mod tests { &mut full_address_ready, &mut address_latch_time, access_time, + Duration::from_nanos(100), // virtual_time ); assert_eq!(high_nibble, Some(0x12)); @@ -282,7 +282,7 @@ mod tests { assert_eq!(full_address_ready, false); assert_eq!(address_latch_time, None); - // Second cycle: low nibble + // Second cycle: low nibble — address_latch_time is now set internally handler.handle_address_latching( 0x34, &mut high_nibble, @@ -290,12 +290,14 @@ mod tests { &mut full_address_ready, &mut address_latch_time, access_time, + Duration::from_nanos(200), // virtual_time when address is fully latched ); assert_eq!(high_nibble, None); // Should be cleared assert_eq!(low_nibble, None); // Should be cleared assert_eq!(full_address_ready, true); - assert!(address_latch_time.is_some()); + // address_latch_time is now set by handle_address_latching() internally + assert_eq!(address_latch_time, Some(Duration::from_nanos(200))); // Verify the assembled address let assembled = handler.assemble_full_address(Some(0x02), Some(0x04)); @@ -311,26 +313,37 @@ mod tests { Intel400xAddressHandling::handle_latency_wait( &MockComponent::new(), &None, - access_time + access_time, + Duration::from_nanos(200), ), false ); - // Test with recent latch time - need to ensure enough time has elapsed - // Create the latch time, then sleep to guarantee elapsed >= access_time - let recent_time = Some(Instant::now()); - thread::sleep(Duration::from_micros(1)); + // Test with latch time and sufficient virtual time elapsed + let latch_time = Some(Duration::from_nanos(50)); + let virtual_time = Duration::from_nanos(200); assert_eq!( Intel400xAddressHandling::handle_latency_wait( &MockComponent::new(), - &recent_time, - access_time + &latch_time, + access_time, + virtual_time, ), true ); - // Note: Testing with elapsed time would require sleeping or mocking time - // This demonstrates the logic structure is correct + // Test with latch time but insufficient virtual time elapsed + let latch_time = Some(Duration::from_nanos(150)); + let virtual_time = Duration::from_nanos(200); + assert_eq!( + Intel400xAddressHandling::handle_latency_wait( + &MockComponent::new(), + &latch_time, + access_time, + virtual_time, + ), + false + ); } #[test] diff --git a/tests/lib.rs b/tests/lib.rs index 90c9ab1..7519ee9 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -17,6 +17,7 @@ mod intel_400x_tests; // Common test utilities and helpers pub mod test_utils { + use std::sync::Arc; use std::time::{Duration, Instant}; /// Create a deterministic time source for testing @@ -34,6 +35,57 @@ pub mod test_utils { std::thread::sleep(duration); } + /// Create a VirtualClock in MaxSpeed mode for fast deterministic tests. + /// + /// The clock starts in stopped state. Call `clock.start()` after spawning + /// the governor thread. + /// + /// # Parameters + /// - `tick_size_ns`: Virtual time advance per tick (default: 333ns for 750 kHz) + pub fn create_maxspeed_clock(tick_size_ns: u64) -> Arc { + let clock = Arc::new(rusty_emu::virtual_clock::VirtualClock::new( + Duration::from_nanos(tick_size_ns), + )); + clock.set_mode(rusty_emu::virtual_clock::SimulationMode::MaxSpeed); + clock + } + + /// Create a VirtualClock in SingleStep mode for precise state verification. + /// + /// Each call to `clock.trigger_step()` advances virtual time by one tick. + pub fn create_singlestep_clock(tick_size_ns: u64) -> Arc { + let clock = Arc::new(rusty_emu::virtual_clock::VirtualClock::new( + Duration::from_nanos(tick_size_ns), + )); + clock.set_mode(rusty_emu::virtual_clock::SimulationMode::SingleStep); + clock + } + + /// Spawn a RealTimeGovernor thread for the given clock. + /// + /// Returns a JoinHandle that can be used to wait for the governor to exit. + /// The governor will exit when `clock.stop()` is called. + pub fn spawn_governor( + clock: Arc, + ) -> std::thread::JoinHandle<()> { + std::thread::spawn(move || { + let governor = rusty_emu::real_time_governor::RealTimeGovernor::with_defaults(clock); + governor.run(); + }) + } + + /// Stop the virtual clock and wait for the governor thread to finish. + /// + /// This ensures clean shutdown of the governor thread before the test ends. + pub fn stop_governor( + clock: &Arc, + handle: std::thread::JoinHandle<()>, + ) { + clock.stop(); + // Give the governor thread a moment to exit cleanly + let _ = handle.join(); + } + /// Verify that a component implements all required traits pub fn verify_intel400x_traits(component: &C) where diff --git a/tests/virtual_time_tests.rs b/tests/virtual_time_tests.rs new file mode 100644 index 0000000..62fe831 --- /dev/null +++ b/tests/virtual_time_tests.rs @@ -0,0 +1,888 @@ +//! Virtual Time Integration Tests +//! +//! Tests for the virtual time architecture (Phase 9): +//! - MaxSpeed mode for fast deterministic execution +//! - SingleStep mode for precise state verification +//! - Overclocking tests verifying component timing violations +//! - Virtual clock integration with ConfigurableSystem + +use std::sync::Arc; +use std::time::Duration; + +use rusty_emu::component::Component; +use rusty_emu::component_timing::ComponentTiming; +use rusty_emu::components::common::intel_400x::*; +use rusty_emu::components::memory::intel_4001::Intel4001; +use rusty_emu::components::memory::intel_4002::Intel4002; +use rusty_emu::components::memory::intel_4003::Intel4003; +use rusty_emu::real_time_governor::RealTimeGovernor; +use rusty_emu::system_config::SystemFactory; +use rusty_emu::virtual_clock::{SimulationMode, VirtualClock}; + +// ============================================================================= +// MaxSpeed Mode Tests +// ============================================================================= + +#[cfg(test)] +mod maxspeed_tests { + use super::*; + + /// Verify that a VirtualClock in MaxSpeed mode advances time + /// when driven by a RealTimeGovernor. + #[test] + fn test_maxspeed_clock_advances() { + let clock = Arc::new(VirtualClock::new(Duration::from_nanos(333))); + let governor = RealTimeGovernor::with_defaults(clock.clone()); + + let handle = std::thread::spawn(move || { + governor.run(); + }); + + // Let the governor run for a bit (it calls clock.start() internally) + std::thread::sleep(Duration::from_millis(50)); + + // Virtual time should have advanced significantly + let vt = clock.now_ns(); + assert!(vt > 0, "Virtual time should have advanced past 0"); + + // Verify we can read the time + assert!(clock.now() > Duration::ZERO, "Virtual time should be > 0"); + + clock.stop(); + let _ = handle.join(); + } + + /// Verify that multiple time targets are reached in MaxSpeed mode. + #[test] + fn test_maxspeed_multiple_targets() { + let clock = Arc::new(VirtualClock::new(Duration::from_nanos(100))); + let governor = RealTimeGovernor::with_defaults(clock.clone()); + + let handle = std::thread::spawn(move || { + governor.run(); + }); + + // Let the governor advance time + std::thread::sleep(Duration::from_millis(50)); + + // Virtual time should have advanced well past multiple targets + let vt = clock.now_ns(); + assert!(vt >= 1000, "Should have passed 1000ns, got {}", vt); + assert!(vt >= 5000, "Should have passed 5000ns, got {}", vt); + assert!(vt >= 10000, "Should have passed 10000ns, got {}", vt); + + clock.stop(); + let _ = handle.join(); + } + + /// Verify that component update_at_time works with virtual time in MaxSpeed mode. + /// Tests that a ROM component can be updated with specific virtual times. + #[test] + fn test_maxspeed_component_update_at_time() { + let mut rom = Intel4001::new_with_access_time("TestROM".to_string(), 500); + + // Load test data + rom.load_rom_data(vec![0xAB, 0xCD], 0).unwrap(); + + // Simulate address latching at virtual time T=0 + rom.set_timing_state(TimingState::Idle); + rom.set_address_high_nibble(Some(0x0)); + rom.set_address_low_nibble(Some(0x0)); + rom.set_full_address_ready(true); + rom.set_address_latch_time(Some(Duration::ZERO)); + + // At T=0 (before access_time), data should not be ready + let timing = ComponentTiming::intel_4001(); + let elapsed = Duration::ZERO; + assert!( + timing.check_access_time(elapsed).is_err(), + "Access time should not have elapsed at T=0" + ); + + // At T=500ns (exactly access_time), data should be ready + let elapsed_500 = Duration::from_nanos(500); + assert!( + timing.check_access_time(elapsed_500).is_ok(), + "Access time should have elapsed at T=500ns" + ); + + // At T=600ns (after access_time), data should definitely be ready + let elapsed_600 = Duration::from_nanos(600); + assert!( + timing.check_access_time(elapsed_600).is_ok(), + "Access time should have elapsed at T=600ns" + ); + } + + /// Verify that a ROM with 1ns access time works correctly through + /// the full state machine in MaxSpeed mode. + #[test] + fn test_maxspeed_rom_fast_access() { + // Create ROM with 1ns access time (fast for testing) + let mut rom = Intel4001::new_with_access_time("FastROM".to_string(), 1); + rom.load_rom_data(vec![0x42], 0).unwrap(); + + // Drive through state machine with virtual time + // T=0: Idle state + assert_eq!(rom.get_timing_state(), TimingState::Idle); + + // T=100ns: Set address (simulating PHI1 rising edge) + let t = Duration::from_nanos(100); + rom.set_address_high_nibble(Some(0x0)); + rom.set_address_low_nibble(Some(0x0)); + rom.set_full_address_ready(true); + rom.set_address_latch_time(Some(t)); + rom.set_timing_state(TimingState::AddressPhase); + assert_eq!(rom.get_timing_state(), TimingState::AddressPhase); + + // T=200ns: Wait for access time (1ns already elapsed) + rom.set_timing_state(TimingState::WaitLatency); + + // T=300ns: Drive data + rom.set_timing_state(TimingState::DriveData); + assert_eq!(rom.get_timing_state(), TimingState::DriveData); + + // Verify data is accessible + assert_eq!(rom.read_rom(0x00).unwrap(), 0x42); + } +} + +// ============================================================================= +// SingleStep Mode Tests +// ============================================================================= + +#[cfg(test)] +mod singlestep_tests { + use super::*; + + /// Verify that each trigger_step() advances virtual time by exactly one tick. + #[test] + fn test_singlestep_advances_one_tick() { + let clock = Arc::new(VirtualClock::new(Duration::from_nanos(333))); + clock.set_mode(SimulationMode::SingleStep); + + let governor = RealTimeGovernor::with_defaults(clock.clone()); + let handle = std::thread::spawn(move || { + governor.run(); + }); + + // Wait for governor to start + std::thread::sleep(Duration::from_millis(50)); + + // Initial time should be 0 + assert_eq!(clock.now_ns(), 0, "Initial time should be 0"); + + // Trigger one step + clock.trigger_step(); + std::thread::sleep(Duration::from_millis(50)); + assert_eq!(clock.now_ns(), 333, "After 1 step, time should be 333ns"); + + // Trigger another step + clock.trigger_step(); + std::thread::sleep(Duration::from_millis(50)); + assert_eq!(clock.now_ns(), 666, "After 2 steps, time should be 666ns"); + + // Trigger a third step + clock.trigger_step(); + std::thread::sleep(Duration::from_millis(50)); + assert_eq!(clock.now_ns(), 999, "After 3 steps, time should be 999ns"); + + clock.stop(); + let _ = handle.join(); + } + + /// Verify that ROM state can be inspected at specific virtual times + /// using SingleStep mode. + #[test] + fn test_singlestep_rom_state_at_specific_time() { + let clock = Arc::new(VirtualClock::new(Duration::from_nanos(100))); + clock.set_mode(SimulationMode::SingleStep); + + let governor = RealTimeGovernor::with_defaults(clock.clone()); + let handle = std::thread::spawn(move || { + governor.run(); + }); + + // Wait for governor to start + std::thread::sleep(Duration::from_millis(50)); + + // Create ROM with 500ns access time (standard Intel 4001) + let mut rom = Intel4001::new_with_access_time("StepROM".to_string(), 500); + rom.load_rom_data(vec![0xAB], 0).unwrap(); + + // Trigger 1 step → T=100ns — latch address + clock.trigger_step(); + std::thread::sleep(Duration::from_millis(20)); + let t_latch = clock.now(); + assert!(t_latch >= Duration::from_nanos(100)); + + rom.set_address_latch_time(Some(t_latch)); + rom.set_address_high_nibble(Some(0x0)); + rom.set_address_low_nibble(Some(0x0)); + rom.set_full_address_ready(true); + + // At T=100ns, 0ns have elapsed since latch — access time NOT met + let timing = ComponentTiming::intel_4001(); + assert!( + timing.check_access_time(Duration::ZERO).is_err(), + "At T=100ns (0ns since latch), access time (500ns) should NOT be met" + ); + + // Trigger 4 more steps → T=500ns (4 × 100ns = 400ns more) + for _ in 0..4 { + clock.trigger_step(); + std::thread::sleep(Duration::from_millis(20)); + } + + // At T=500ns, 400ns have elapsed since latch at T=100ns — still NOT enough + let elapsed_400 = Duration::from_nanos(400); + assert!( + timing.check_access_time(elapsed_400).is_err(), + "At T=500ns (400ns since latch at T=100ns), access time (500ns) should NOT be met" + ); + + // Trigger 1 more step → T=600ns + clock.trigger_step(); + std::thread::sleep(Duration::from_millis(20)); + + // At T=600ns, 500ns have elapsed since latch at T=100ns — exactly access time + let elapsed_500 = Duration::from_nanos(500); + assert!( + timing.check_access_time(elapsed_500).is_ok(), + "At T=600ns (500ns since latch at T=100ns), access time (500ns) SHOULD be met" + ); + + clock.stop(); + let _ = handle.join(); + } + + /// Verify that trigger_step returns false when not in SingleStep mode. + #[test] + fn test_trigger_step_only_works_in_singlestep() { + let clock = Arc::new(VirtualClock::new(Duration::from_nanos(333))); + + // Default mode is RealTime — trigger_step should fail + assert!( + !clock.trigger_step(), + "trigger_step should return false in RealTime mode" + ); + + // Switch to MaxSpeed — trigger_step should still fail + clock.set_mode(SimulationMode::MaxSpeed); + assert!( + !clock.trigger_step(), + "trigger_step should return false in MaxSpeed mode" + ); + + // Switch to SingleStep — now it should work + clock.set_mode(SimulationMode::SingleStep); + assert!( + clock.trigger_step(), + "trigger_step should return true in SingleStep mode" + ); + } + + /// Verify precise tick count tracking in SingleStep mode. + #[test] + fn test_singlestep_tick_count() { + let clock = Arc::new(VirtualClock::new(Duration::from_nanos(100))); + clock.set_mode(SimulationMode::SingleStep); + + let governor = RealTimeGovernor::with_defaults(clock.clone()); + let handle = std::thread::spawn(move || { + governor.run(); + }); + + // Wait for governor to start + std::thread::sleep(Duration::from_millis(50)); + + assert_eq!(clock.tick_count(), 0, "Initial tick count should be 0"); + + // Trigger 5 steps + for _ in 0..5 { + clock.trigger_step(); + std::thread::sleep(Duration::from_millis(20)); + } + + // After 5 ticks of 100ns each + assert_eq!(clock.now_ns(), 500, "After 5 steps, time should be 500ns"); + assert_eq!(clock.tick_count(), 5, "After 5 steps, tick_count should be 5"); + + clock.stop(); + let _ = handle.join(); + } +} + +// ============================================================================= +// Overclocking Tests +// ============================================================================= + +#[cfg(test)] +mod overclocking_tests { + use super::*; + + /// Verify that ROM (500ns access time) fails timing check at 2 MHz. + /// + /// At 2 MHz, the clock phase is 250ns. The ROM needs 500ns from address + /// valid to data ready, but only gets 250ns — a timing violation. + #[test] + fn test_overclocking_rom_fails_at_2mhz() { + let rom_timing = ComponentTiming::intel_4001(); + + // 2 MHz clock → 500ns cycle → 250ns per phase + let phase_time = Duration::from_nanos(250); + + // ROM has 500ns access time, but phase is only 250ns + assert!( + rom_timing.check_access_time(phase_time).is_err(), + "ROM (500ns access) should FAIL at 2 MHz (250ns phase)" + ); + + // Also verify: ROM does NOT support 2 MHz + assert!( + !rom_timing.supports_frequency(2_000_000.0), + "ROM should NOT support 2 MHz (max is 750 kHz)" + ); + } + + /// Verify that RAM (500ns access time) fails timing check at 2 MHz. + #[test] + fn test_overclocking_ram_fails_at_2mhz() { + let ram_timing = ComponentTiming::intel_4002(); + + // 2 MHz clock → 250ns per phase + let phase_time = Duration::from_nanos(250); + + // RAM has 500ns access time, but phase is only 250ns + assert!( + ram_timing.check_access_time(phase_time).is_err(), + "RAM (500ns access) should FAIL at 2 MHz (250ns phase)" + ); + + assert!( + !ram_timing.supports_frequency(2_000_000.0), + "RAM should NOT support 2 MHz (max is 750 kHz)" + ); + } + + /// Verify that the shift register (200ns access time) STILL WORKS at 2 MHz. + /// + /// The Intel 4003 has a faster 200ns access time, so at 2 MHz (250ns phase), + /// there's still enough time for data to become valid. This models real + /// hardware behavior where faster chips continue working when slower ones fail. + #[test] + fn test_shift_register_works_at_higher_frequency() { + let sr_timing = ComponentTiming::intel_4003(); + + // 2 MHz clock → 250ns per phase + let phase_time = Duration::from_nanos(250); + + // Shift register has 200ns access time, phase is 250ns — PLENTY of time + assert!( + sr_timing.check_access_time(phase_time).is_ok(), + "Shift register (200ns access) should WORK at 2 MHz (250ns phase)" + ); + + // Shift register supports 1 MHz + assert!( + sr_timing.supports_frequency(1_000_000.0), + "Shift register should support 1 MHz" + ); + + // But NOT 2 MHz (max is 1 MHz) + assert!( + !sr_timing.supports_frequency(2_000_000.0), + "Shift register should NOT support 2 MHz (max is 1 MHz)" + ); + + // However, the access time check passes because 250ns > 200ns. + // This is the key insight: access_time and max_clock_frequency are + // independent constraints. The access time is met (250ns >= 200ns) + // even though the frequency exceeds the spec. + } + + /// Verify the overclocking scenario from the timing architecture plan: + /// At 2 MHz, ROM and RAM fail while the shift register keeps working. + #[test] + fn test_overclocking_scenario_comprehensive() { + let rom_timing = ComponentTiming::intel_4001(); + let ram_timing = ComponentTiming::intel_4002(); + let sr_timing = ComponentTiming::intel_4003(); + let cpu_timing = ComponentTiming::intel_4004(); + + // 2 MHz clock: phase = 250ns + let phase_time_2mhz = Duration::from_nanos(250); + + // ROM: 500ns access, 250ns available → FAIL + assert!(rom_timing.check_access_time(phase_time_2mhz).is_err()); + + // RAM: 500ns access, 250ns available → FAIL + assert!(ram_timing.check_access_time(phase_time_2mhz).is_err()); + + // Shift register: 200ns access, 250ns available → OK + assert!(sr_timing.check_access_time(phase_time_2mhz).is_ok()); + + // CPU: 0ns access time → always OK (CPU doesn't have "access time") + assert!(cpu_timing.check_access_time(phase_time_2mhz).is_ok()); + + // Now at standard 750 kHz: phase = 666ns + let phase_time_750khz = Duration::from_nanos(666); + + // All components should work at 750 kHz + assert!(rom_timing.check_access_time(phase_time_750khz).is_ok()); + assert!(ram_timing.check_access_time(phase_time_750khz).is_ok()); + assert!(sr_timing.check_access_time(phase_time_750khz).is_ok()); + assert!(cpu_timing.check_access_time(phase_time_750khz).is_ok()); + } + + /// Verify that ComponentTiming::min_clock_period() returns correct values. + #[test] + fn test_min_clock_period_per_component() { + let rom_timing = ComponentTiming::intel_4001(); + let ram_timing = ComponentTiming::intel_4002(); + let sr_timing = ComponentTiming::intel_4003(); + + // ROM: max 750 kHz → min period ~1333ns + let rom_period = rom_timing.min_clock_period(); + assert!( + rom_period > Duration::from_nanos(1300) && rom_period < Duration::from_nanos(1400), + "ROM min clock period should be ~1333ns, got {:?}", + rom_period + ); + + // RAM: max 750 kHz → min period ~1333ns + let ram_period = ram_timing.min_clock_period(); + assert!( + ram_period > Duration::from_nanos(1300) && ram_period < Duration::from_nanos(1400), + "RAM min clock period should be ~1333ns, got {:?}", + ram_period + ); + + // Shift register: max 1 MHz → min period 1000ns + let sr_period = sr_timing.min_clock_period(); + assert!( + sr_period > Duration::from_nanos(900) && sr_period < Duration::from_nanos(1100), + "Shift register min clock period should be ~1000ns, got {:?}", + sr_period + ); + } + + /// Verify setup time and hold time checks work correctly. + #[test] + fn test_setup_and_hold_time_checks() { + let rom_timing = ComponentTiming::intel_4001(); + + // Setup time: 100ns + assert!(rom_timing.check_setup_time(Duration::from_nanos(150)).is_ok()); + assert!(rom_timing.check_setup_time(Duration::from_nanos(100)).is_ok()); + assert!(rom_timing.check_setup_time(Duration::from_nanos(50)).is_err()); + + // Hold time: 50ns + assert!(rom_timing.check_hold_time(Duration::from_nanos(100)).is_ok()); + assert!(rom_timing.check_hold_time(Duration::from_nanos(50)).is_ok()); + assert!(rom_timing.check_hold_time(Duration::from_nanos(25)).is_err()); + } + + /// Verify that timing violations contain meaningful error information. + #[test] + fn test_timing_violation_error_details() { + let rom_timing = ComponentTiming::intel_4001(); + + let err = rom_timing.check_access_time(Duration::from_nanos(100)); + assert!(err.is_err()); + let err_msg = format!("{:?}", err.unwrap_err()); + assert!( + err_msg.contains("Intel 4001 ROM") || err_msg.contains("4001"), + "Violation should mention component name: {}", + err_msg + ); + } +} + +// ============================================================================= +// Virtual Clock Integration Tests +// ============================================================================= + +#[cfg(test)] +mod virtual_clock_integration_tests { + use super::*; + + /// Verify that ConfigurableSystem creates and exposes a VirtualClock. + #[test] + fn test_configurable_system_creates_virtual_clock() { + let factory = SystemFactory::new(); + let system = factory.create_from_json("configs/mcs4_basic.json").unwrap(); + + // The system should have a virtual clock + let clock = system.get_virtual_clock(); + assert!(Arc::strong_count(clock) >= 1, "VirtualClock Arc should exist"); + + // Clock should have correct tick size for 750 kHz (333ns per quarter-cycle) + let tick_size = clock.tick_size(); + assert!( + tick_size > Duration::ZERO, + "Tick size should be non-zero" + ); + // For 750 kHz, cycle time is ~1333ns, quarter-cycle is ~333ns + assert_eq!( + tick_size, + Duration::from_nanos(333), + "Tick size should be 333ns for 750 kHz" + ); + } + + /// Verify that ConfigurableSystem's VirtualClock can be put in MaxSpeed mode. + #[test] + fn test_configurable_system_maxspeed_mode() { + let factory = SystemFactory::new(); + let system = factory.create_from_json("configs/mcs4_basic.json").unwrap(); + + let clock = system.get_virtual_clock(); + clock.set_mode(SimulationMode::MaxSpeed); + + assert_eq!(clock.mode(), SimulationMode::MaxSpeed); + } + + /// Verify that clock.stop() causes the governor thread to exit cleanly. + #[test] + fn test_virtual_clock_stop_exits_governor() { + let clock = Arc::new(VirtualClock::new(Duration::from_nanos(333))); + let governor = RealTimeGovernor::with_defaults(clock.clone()); + + let handle = std::thread::spawn(move || { + governor.run(); + }); + + // Let the governor start and advance time + std::thread::sleep(Duration::from_millis(50)); + assert!(clock.now_ns() > 0, "Clock should have advanced"); + + // Stop the clock + clock.stop(); + + // Governor thread should exit cleanly + let join_result = handle.join(); + assert!( + join_result.is_ok(), + "Governor thread should exit cleanly after stop" + ); + } + + /// Verify that clock.stop() causes wait_until to return None. + #[test] + fn test_stop_unblocks_wait_until() { + let clock = Arc::new(VirtualClock::new(Duration::from_nanos(333))); + let governor = RealTimeGovernor::with_defaults(clock.clone()); + + let clock_clone = Arc::clone(&clock); + let handle = std::thread::spawn(move || { + governor.run(); + }); + + // Let the governor start + std::thread::sleep(Duration::from_millis(50)); + + // Now stop the clock + clock.stop(); + + // wait_until for a far-future time should return None immediately + let result = clock_clone.wait_until(Duration::from_secs(100)); + assert!( + result.is_none(), + "wait_until should return None after clock is stopped" + ); + + let _ = handle.join(); + } + + /// Verify pause/resume behavior: time stops advancing when paused. + #[test] + fn test_pause_resume_behavior() { + let clock = Arc::new(VirtualClock::new(Duration::from_nanos(333))); + let governor = RealTimeGovernor::with_defaults(clock.clone()); + + let handle = std::thread::spawn(move || { + governor.run(); + }); + + // Let it run + std::thread::sleep(Duration::from_millis(50)); + let time_before_pause = clock.now_ns(); + assert!(time_before_pause > 0); + + // Pause the clock + clock.pause(); + assert!(clock.is_paused()); + + // Wait a short wall-clock time, then check virtual time hasn't advanced + std::thread::sleep(Duration::from_millis(50)); + let time_after_pause = clock.now_ns(); + + // Virtual time should not have advanced significantly while paused + // (may have advanced by 1-2 ticks due to race, but not more) + let diff = time_after_pause.saturating_sub(time_before_pause); + assert!( + diff < 1000, + "Virtual time should not advance significantly while paused. \ + Before: {}, After: {}, Diff: {}", + time_before_pause, time_after_pause, diff + ); + + // Resume the clock + clock.resume(); + assert!(!clock.is_paused()); + + // Wait for time to advance again + std::thread::sleep(Duration::from_millis(50)); + let time_after_resume = clock.now_ns(); + assert!( + time_after_resume > time_after_pause, + "Virtual time should advance after resume" + ); + + clock.stop(); + let _ = handle.join(); + } + + /// Verify that the VirtualClock reset works correctly. + #[test] + fn test_virtual_clock_reset() { + let clock = Arc::new(VirtualClock::new(Duration::from_nanos(333))); + let governor = RealTimeGovernor::with_defaults(clock.clone()); + + let handle = std::thread::spawn(move || { + governor.run(); + }); + + // Advance time + std::thread::sleep(Duration::from_millis(50)); + assert!(clock.now_ns() > 0); + + // Stop and reset + clock.stop(); + let _ = handle.join(); + + // Reset time to zero + clock.reset(); + + // Verify time is back to zero + assert_eq!(clock.now(), Duration::ZERO); + assert_eq!(clock.tick_count(), 0); + } + + /// Verify speed multiplier can be changed dynamically. + #[test] + fn test_speed_multiplier_change() { + let clock = Arc::new(VirtualClock::new(Duration::from_nanos(333))); + + // Default speed is 1.0x + assert!((clock.speed_multiplier() - 1.0).abs() < 0.001); + + // Set to 0.5x + clock.set_speed_multiplier(0.5); + assert!((clock.speed_multiplier() - 0.5).abs() < 0.001); + + // Set to 10.0x + clock.set_speed_multiplier(10.0); + assert!((clock.speed_multiplier() - 10.0).abs() < 0.01); + + // Set to 0.001x (very slow) + clock.set_speed_multiplier(0.001); + assert!((clock.speed_multiplier() - 0.001).abs() < 0.0001); + } + + /// Verify that has_reached works correctly for non-blocking time checks. + #[test] + fn test_has_reached_nonblocking() { + let clock = Arc::new(VirtualClock::new(Duration::from_nanos(100))); + let governor = RealTimeGovernor::with_defaults(clock.clone()); + + let handle = std::thread::spawn(move || { + governor.run(); + }); + + // Initially, time is 0 + assert!(clock.has_reached(Duration::ZERO)); + assert!(!clock.has_reached(Duration::from_nanos(1))); + + // Wait for time to advance + std::thread::sleep(Duration::from_millis(50)); + + // Now it should have reached various targets + assert!(clock.has_reached(Duration::from_nanos(100))); + assert!(clock.has_reached(Duration::from_nanos(1000))); + assert!(clock.has_reached(Duration::from_micros(5))); + + clock.stop(); + let _ = handle.join(); + } + + /// Verify tick_size can be changed dynamically. + #[test] + fn test_tick_size_change() { + let clock = Arc::new(VirtualClock::new(Duration::from_nanos(333))); + + assert_eq!(clock.tick_size(), Duration::from_nanos(333)); + + clock.set_tick_size(Duration::from_nanos(100)); + assert_eq!(clock.tick_size(), Duration::from_nanos(100)); + + clock.set_tick_size(Duration::from_nanos(500)); + assert_eq!(clock.tick_size(), Duration::from_nanos(500)); + } +} + +// ============================================================================= +// Component Timing with Virtual Time Tests +// ============================================================================= + +#[cfg(test)] +mod component_virtual_time_tests { + use super::*; + + /// Verify that Intel4001 implements the timing() method correctly. + #[test] + fn test_intel4001_timing_profile() { + let rom = Intel4001::new("TestROM".to_string()); + let timing = rom.timing(); + + assert_eq!(timing.name, "Intel 4001 ROM"); + 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!((timing.max_clock_frequency - 750_000.0).abs() < 1.0); + } + + /// Verify that Intel4002 implements the timing() method correctly. + #[test] + fn test_intel4002_timing_profile() { + let ram = Intel4002::new("TestRAM".to_string()); + let timing = ram.timing(); + + assert_eq!(timing.name, "Intel 4002 RAM"); + assert_eq!(timing.access_time, Duration::from_nanos(500)); + assert_eq!(timing.propagation_delay, Duration::from_nanos(200)); + } + + /// Verify that Intel4003 implements the timing() method correctly. + #[test] + fn test_intel4003_timing_profile() { + let sr = Intel4003::new("TestSR".to_string()); + let timing = sr.timing(); + + assert_eq!(timing.name, "Intel 4003 Shift Register"); + assert_eq!(timing.access_time, Duration::from_nanos(200)); + assert_eq!(timing.propagation_delay, Duration::from_nanos(100)); + assert!((timing.max_clock_frequency - 1_000_000.0).abs() < 1.0); + } + + /// Verify that update_at_time() works correctly for ROM component. + /// This tests the virtual-time-aware update path. + #[test] + fn test_rom_update_at_time() { + let mut rom = Intel4001::new_with_access_time("TestROM".to_string(), 1); + rom.load_rom_data(vec![0x42], 0).unwrap(); + rom.set_running(true); + + // Call update_at_time at various virtual times + // This should work without panicking + rom.update_at_time(Duration::from_nanos(0)); + rom.update_at_time(Duration::from_nanos(333)); + rom.update_at_time(Duration::from_nanos(666)); + rom.update_at_time(Duration::from_nanos(999)); + + // Component should still function correctly + assert_eq!(rom.read_rom(0x00).unwrap(), 0x42); + } + + /// Verify that update_at_time() works correctly for RAM component. + #[test] + fn test_ram_update_at_time() { + let mut ram = Intel4002::new("TestRAM".to_string()); + ram.set_running(true); + + // Call update_at_time at various virtual times + rom_update_at_time_helper(&mut ram); + } + + fn rom_update_at_time_helper(ram: &mut Intel4002) { + ram.update_at_time(Duration::from_nanos(0)); + ram.update_at_time(Duration::from_nanos(333)); + ram.update_at_time(Duration::from_nanos(666)); + } + + /// Verify that update_at_time() works correctly for shift register component. + #[test] + fn test_sr_update_at_time() { + let mut sr = Intel4003::new("TestSR".to_string()); + sr.set_running(true); + + // Call update_at_time at various virtual times + sr.update_at_time(Duration::from_nanos(0)); + sr.update_at_time(Duration::from_nanos(333)); + sr.update_at_time(Duration::from_nanos(666)); + + // Should not panic + } + + /// Test that address latch time tracking works with virtual time. + #[test] + fn test_address_latch_time_virtual_time() { + let mut rom = Intel4001::new_with_access_time("TestROM".to_string(), 500); + + // Set address latch time to a specific virtual time + let latch_time = Duration::from_nanos(1000); + rom.set_address_latch_time(Some(latch_time)); + + assert_eq!(rom.get_address_latch_time(), Some(latch_time)); + + // Clear it + rom.set_address_latch_time(None); + assert_eq!(rom.get_address_latch_time(), None); + } + + /// Verify that the timing state machine works correctly with virtual time. + #[test] + fn test_timing_state_machine_with_virtual_time() { + let mut rom = Intel4001::new_with_access_time("TestROM".to_string(), 500); + + // Start in Idle + assert_eq!(rom.get_timing_state(), TimingState::Idle); + + // Simulate a complete memory access cycle with virtual time + let t0 = Duration::ZERO; // Start + let _t1 = Duration::from_nanos(333); // Address phase + let _t2 = Duration::from_nanos(666); // Wait latency + let t3 = Duration::from_nanos(999); // Drive data + + // T0: Idle + rom.update_at_time(t0); + assert_eq!(rom.get_timing_state(), TimingState::Idle); + + // T1: Address latched + let t1 = Duration::from_nanos(333); + rom.set_address_latch_time(Some(t1)); + rom.set_timing_state(TimingState::AddressPhase); + assert_eq!(rom.get_timing_state(), TimingState::AddressPhase); + + // T2: Waiting for access time + rom.set_timing_state(TimingState::WaitLatency); + assert_eq!(rom.get_timing_state(), TimingState::WaitLatency); + + // T3: Access time (666ns) > 500ns → data ready + let elapsed = t3.saturating_sub(t1); + let timing = ComponentTiming::intel_4001(); + assert!( + timing.check_access_time(elapsed).is_ok(), + "After 666ns, access time (500ns) should be met" + ); + + rom.set_timing_state(TimingState::DriveData); + assert_eq!(rom.get_timing_state(), TimingState::DriveData); + + // Return to idle + rom.set_timing_state(TimingState::Idle); + assert_eq!(rom.get_timing_state(), TimingState::Idle); + } +}