rusty_emu/plans/phase7_system_integration.md
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

174 lines
6.6 KiB
Markdown

# 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