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.
6.6 KiB
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
nanosleephas ~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
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:
- Inject
Arc<VirtualClock>into all components viaset_virtual_clock()(downcast to each concrete type) - Spawn
RealTimeGovernorin its own thread —governor.run()callsclock.start()internally - For each component, spawn a thread that calls
comp.run()— the component's ownrun()method handles the VirtualClock wait loop (already implemented in Phases 5-6) - Store the governor thread handle
Modified cleanup() Method
- Call
clock.stop()to unblock all component threads waiting inwait_until()/wait_for_start() - Set
is_running = false - Wait for component threads to finish (join)
- 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
Nonefromwait_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:
GenericClock—comp.as_any_mut().downcast_mut::<GenericClock>()TwoPhaseClock—comp.as_any_mut().downcast_mut::<TwoPhaseClock>()Intel4004—comp.as_any_mut().downcast_mut::<Intel4004>()Intel4001—comp.as_any_mut().downcast_mut::<Intel4001>()Intel4002—comp.as_any_mut().downcast_mut::<Intel4002>()Intel4003—comp.as_any_mut().downcast_mut::<Intel4003>()
This is the same pattern already used in pause(), resume(), set_clock_frequency(), etc.
Implementation Steps
- Add imports for
VirtualClock,RealTimeGovernor, and concrete component types - Add
virtual_clockandgovernor_handlefields toConfigurableSystem - Update
ConfigurableSystem::new()to createVirtualClockfrom frequency - Add
inject_virtual_clock()helper method - Update
ConfigurableSystem::start()(inherent) to:- Inject VirtualClock into all components
- Spawn governor thread
- Spawn component threads that call
comp.run()
- Update
ConfigurableSystem::cleanup()to stop clock and join governor - Update
SystemController::pause()/resume()to use clock.pause()/resume() - 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)
- Mitigated by the fact that
- Low Risk: Downcast-based injection — same pattern already used extensively
- Low Risk: Governor thread — already tested independently
Testing Strategy
cargo build— must compilecargo test— all existing tests must pass- Manual test:
cargo run -- --system basic— system should start, run, and stop cleanly - Manual test:
cargo run -- --gui --system basic— GUI should work with VirtualClock