feat: implement virtual clock timing architecture (Phases 5-8)
Introduces a VirtualClock-based timing system replacing wall-clock Instant-based timing for hardware-accurate simulation. New modules: - virtual_clock.rs: Shared virtual time source with condvar sync - real_time_governor.rs: Thread that advances virtual time - component_timing.rs: Per-component timing constraints Key changes: - Component trait: added update_at_time(), timing(), set_virtual_clock_optional() - All Intel 400x components migrated to virtual-time-aware operation - ConfigurableSystem: start() returns Result, spawns governor thread - GUI: timing controls (speed slider, mode selector, step button) - Console: speed/mode/step/time commands - Backward compatible: legacy fallback for components without VirtualClock Reviewed-by: code review found 2 warnings (clock resume() dead code, reset() safety docs) — both fixed.
This commit is contained in:
parent
add7ace674
commit
08f53d68fe
28 changed files with 7186 additions and 270 deletions
174
plans/phase7_system_integration.md
Normal file
174
plans/phase7_system_integration.md
Normal file
|
|
@ -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<VirtualClock> 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<VirtualClock>,
|
||||
governor_handle: Arc<Mutex<Option<std::thread::JoinHandle<()>>>>,
|
||||
}
|
||||
```
|
||||
|
||||
#### Modified `new()` Constructor
|
||||
|
||||
- Derive tick_size from clock frequency in metadata
|
||||
- Create `Arc<VirtualClock>` 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<VirtualClock>` 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<dyn Component>` to each concrete type. The order of attempts:
|
||||
|
||||
1. `GenericClock` — `comp.as_any_mut().downcast_mut::<GenericClock>()`
|
||||
2. `TwoPhaseClock` — `comp.as_any_mut().downcast_mut::<TwoPhaseClock>()`
|
||||
3. `Intel4004` — `comp.as_any_mut().downcast_mut::<Intel4004>()`
|
||||
4. `Intel4001` — `comp.as_any_mut().downcast_mut::<Intel4001>()`
|
||||
5. `Intel4002` — `comp.as_any_mut().downcast_mut::<Intel4002>()`
|
||||
6. `Intel4003` — `comp.as_any_mut().downcast_mut::<Intel4003>()`
|
||||
|
||||
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue