# 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