rusty_emu/src/component_timing.rs
Jan-Peter von Hunnius 08f53d68fe feat: implement virtual clock timing architecture (Phases 5-8)
Introduces a VirtualClock-based timing system replacing wall-clock
Instant-based timing for hardware-accurate simulation.

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

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

Reviewed-by: code review found 2 warnings (clock resume() dead code,
reset() safety docs) — both fixed.
2026-05-14 21:00:22 +02:00

470 lines
17 KiB
Rust

//! 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"));
}
}