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

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 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

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