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.
603 lines
21 KiB
Markdown
603 lines
21 KiB
Markdown
# 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<VirtualClock>,
|
||
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<VirtualClock>` 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
|