diff --git a/plans/phase5_clock_migration.md b/plans/phase5_clock_migration.md deleted file mode 100644 index 93aaf2a..0000000 --- a/plans/phase5_clock_migration.md +++ /dev/null @@ -1,898 +0,0 @@ -# Phase 5: Migrate Clock Components to Virtual Time - -## Overview - -Phase 5 replaces `Instant::now()` and `thread::sleep()` in both clock components with virtual time from `Arc`. This is the first phase that modifies existing components rather than adding new infrastructure. - -**Files modified:** -- [`generic_clock.rs`](src/components/clock/generic_clock.rs) — GenericClock -- [`two_phase_clock.rs`](src/components/clock/two_phase_clock.rs) — TwoPhaseClock -- [`system_config.rs`](src/system_config.rs) — Clock factory closures (minor) - -**Files NOT modified** (Phase 7 responsibility): -- [`virtual_clock.rs`](src/virtual_clock.rs) — Already complete -- [`real_time_governor.rs`](src/real_time_governor.rs) — Already complete -- [`component.rs`](src/component.rs) — Already has `update_at_time()` default - ---- - -## Implementation Order - -**Migrate `GenericClock` first, then `TwoPhaseClock`.** - -Rationale: -1. `GenericClock` is simpler — single-phase, single CLK output -2. `TwoPhaseClock` has dual outputs (PHI1/PHI2) and inter-phase dependencies -3. Both follow the same pattern, so GenericClock establishes the template -4. `GenericClock` has existing unit tests to validate the migration - ---- - -## Part A: GenericClock Migration - -### A1. Current Timing Mechanism - -[`GenericClock`](src/components/clock/generic_clock.rs:11) uses wall-clock time in 5 locations: - -| Location | Line | Current Code | Purpose | -|----------|------|-------------|---------| -| [`last_transition`](src/components/clock/generic_clock.rs:16) field | 16 | `last_transition: Instant` | Track when last phase change occurred | -| [`pause_start`](src/components/clock/generic_clock.rs:21) field | 21 | `pause_start: Option` | Track when pause began | -| [`new()`](src/components/clock/generic_clock.rs:34) constructor | 34 | `last_transition: Instant::now()` | Initialize transition timer | -| [`enable()`](src/components/clock/generic_clock.rs:68) | 68 | `self.last_transition = Instant::now()` | Reset timer on enable | -| [`pause()`](src/components/clock/generic_clock.rs:80) | 80 | `self.pause_start = Some(Instant::now())` | Record pause start | -| [`resume()`](src/components/clock/generic_clock.rs:88) | 88 | `pause_start.elapsed()` | Calculate pause duration | -| [`should_transition()`](src/components/clock/generic_clock.rs:131) | 131 | `self.last_transition.elapsed()` | Check if phase duration elapsed | -| [`perform_transition()`](src/components/clock/generic_clock.rs:149) | 149 | `self.last_transition = Instant::now()` | Record transition time | -| [`run()`](src/components/clock/generic_clock.rs:205) | 205 | `thread::sleep(sleep_time)` | Busy-wait loop with sleep | - -### A2. Required Changes - -#### A2.1 — Add `VirtualClock` field (optional, gradual integration) - -Replace `Instant`-based fields with virtual-time equivalents. Add `Option>` for gradual integration — clocks work without a VirtualClock (falling back to wall-clock), but use virtual time when one is injected. - -```rust -// BEFORE (line 11-22): -pub struct GenericClock { - base: BaseComponent, - frequency: f64, - duty_cycle: f64, - current_state: PinValue, - last_transition: Instant, - high_time: Duration, - low_time: Duration, - enabled: bool, - paused: bool, - pause_start: Option, -} - -// AFTER: -pub struct GenericClock { - base: BaseComponent, - frequency: f64, - duty_cycle: f64, - current_state: PinValue, - last_transition_vtime: Duration, // Virtual time of last transition - high_time: Duration, - low_time: Duration, - enabled: bool, - paused: bool, - pause_start_vtime: Option, // Virtual time when pause started - virtual_clock: Option>, // Injected in Phase 7 -} -``` - -**Key change:** `last_transition: Instant` → `last_transition_vtime: Duration` (virtual nanoseconds since sim start). `pause_start: Option` → `pause_start_vtime: Option`. - -#### A2.2 — Update constructor - -```rust -// BEFORE (line 29-43): -let mut clock = GenericClock { - base: BaseComponent::new(name, pins), - frequency, - duty_cycle: 0.5, - current_state: PinValue::Low, - last_transition: Instant::now(), - high_time: Duration::from_secs_f64(0.5 / frequency), - low_time: Duration::from_secs_f64(0.5 / frequency), - enabled: true, - paused: false, - pause_start: None, -}; - -// AFTER: -let mut clock = GenericClock { - base: BaseComponent::new(name, pins), - frequency, - duty_cycle: 0.5, - current_state: PinValue::Low, - last_transition_vtime: Duration::ZERO, // Virtual time 0 - high_time: Duration::from_secs_f64(0.5 / frequency), - low_time: Duration::from_secs_f64(0.5 / frequency), - enabled: true, - paused: false, - pause_start_vtime: None, - virtual_clock: None, // Set later via set_virtual_clock() -}; -``` - -#### A2.3 — Add `set_virtual_clock()` method - -New method for Phase 7 injection: - -```rust -/// Inject a VirtualClock for virtual-time-based operation. -/// If not set, the clock falls back to wall-clock Instant-based timing. -/// This method is called by ConfigurableSystem in Phase 7. -pub fn set_virtual_clock(&mut self, clock: Arc) { - self.virtual_clock = Some(clock); -} -``` - -#### A2.4 — Update `enable()` method (line 64-69) - -```rust -// BEFORE: -pub fn enable(&mut self) { - self.enabled = true; - self.current_state = PinValue::Low; - self.last_transition = Instant::now(); -} - -// AFTER: -pub fn enable(&mut self) { - self.enabled = true; - self.current_state = PinValue::Low; - self.last_transition_vtime = self.current_virtual_time(); -} -``` - -Where `current_virtual_time()` is a new helper: - -```rust -/// Get current virtual time, or fall back to Duration::ZERO if no VirtualClock. -fn current_virtual_time(&self) -> Duration { - self.virtual_clock.as_ref().map_or(Duration::ZERO, |vc| vc.now()) -} -``` - -#### A2.5 — Update `pause()` and `resume()` (lines 78-94) - -```rust -// BEFORE: -pub fn pause(&mut self) { - self.paused = true; - self.pause_start = Some(Instant::now()); -} - -pub fn resume(&mut self) { - if let Some(pause_start) = self.pause_start { - let pause_duration = pause_start.elapsed(); - self.last_transition += pause_duration; - } - self.paused = false; - self.pause_start = None; -} - -// AFTER: -pub fn pause(&mut self) { - self.paused = true; - self.pause_start_vtime = Some(self.current_virtual_time()); -} - -pub fn resume(&mut self) { - if let Some(pause_start_vtime) = self.pause_start_vtime { - let pause_duration = self.current_virtual_time().saturating_sub(pause_start_vtime); - self.last_transition_vtime = self.last_transition_vtime.saturating_add(pause_duration); - } - self.paused = false; - self.pause_start_vtime = None; -} -``` - -#### A2.6 — Update `should_transition()` (line 130-138) - -```rust -// BEFORE: -fn should_transition(&self) -> bool { - let elapsed = self.last_transition.elapsed(); - match self.current_state { - PinValue::High => elapsed >= self.high_time, - PinValue::Low => elapsed >= self.low_time, - PinValue::HighZ => true, - } -} - -// AFTER: -fn should_transition(&self, virtual_time: Duration) -> bool { - let elapsed = virtual_time.saturating_sub(self.last_transition_vtime); - match self.current_state { - PinValue::High => elapsed >= self.high_time, - PinValue::Low => elapsed >= self.low_time, - PinValue::HighZ => true, - } -} -``` - -**Note:** `should_transition()` now takes `virtual_time: Duration` as a parameter instead of computing elapsed from `Instant`. - -#### A2.7 — Update `perform_transition()` (line 140-150) - -```rust -// BEFORE: -fn perform_transition(&mut self) { - let new_state = match self.current_state { - PinValue::High => PinValue::Low, - PinValue::Low => PinValue::High, - PinValue::HighZ => PinValue::Low, - }; - self.current_state = new_state; - self.set_clock_output(new_state); - self.last_transition = Instant::now(); -} - -// AFTER: -fn perform_transition(&mut self, virtual_time: Duration) { - let new_state = match self.current_state { - PinValue::High => PinValue::Low, - PinValue::Low => PinValue::High, - PinValue::HighZ => PinValue::Low, - }; - self.current_state = new_state; - self.set_clock_output(new_state); - self.last_transition_vtime = virtual_time; -} -``` - -#### A2.8 — Implement `update_at_time()` (new, replaces `update()` for virtual-time path) - -Add the virtual-time-aware update method. This overrides the default from the `Component` trait: - -```rust -impl Component for GenericClock { - // ... existing methods unchanged ... - - fn update_at_time(&mut self, virtual_time: Duration) { - // Check pause state first - don't update if paused - if self.paused { - return; - } - - // Read external enable control - let external_enable = self.read_enable_pin(); - let should_be_enabled = self.enabled && external_enable; - - if !should_be_enabled { - if self.current_state != PinValue::Low { - self.set_clock_output(PinValue::Low); - self.current_state = PinValue::Low; - } - return; - } - - // Check if it's time to transition (using virtual time) - if self.should_transition(virtual_time) { - self.perform_transition(virtual_time); - } - } - - // Keep update() for backward compatibility — delegates to update_at_time - // using current virtual time (or Duration::ZERO if no VirtualClock) - fn update(&mut self) { - let vtime = self.current_virtual_time(); - self.update_at_time(vtime); - } -} -``` - -#### A2.9 — Rewrite `run()` to use `wait_until()` (line 190-210) - -This is the most significant change. Replace the `thread::sleep()` busy-loop with `virtual_clock.wait_until()`: - -```rust -// BEFORE: -fn run(&mut self) { - self.base.set_running(true); - self.enable(); - - while self.is_running() { - self.update(); - let sleep_time = if self.frequency > 0.0 { - Duration::from_secs_f64(1.0 / self.frequency / 100.0) - .max(Duration::from_micros(1)) - } else { - Duration::from_micros(100) - }; - thread::sleep(sleep_time); - } - self.disable(); -} - -// AFTER: -fn run(&mut self) { - self.base.set_running(true); - self.enable(); - - // If no VirtualClock is injected, fall back to wall-clock run loop - let vclock = match self.virtual_clock.as_ref() { - Some(vc) => Arc::clone(vc), - None => { - // Legacy path: use old Instant-based timing - return self.run_legacy(); - } - }; - - while self.is_running() { - // Calculate next transition time - let next_transition = self.next_transition_time(); - - // Block until virtual time reaches next transition - match vclock.wait_until(next_transition) { - Some(current_vtime) => { - if !self.is_running() { - break; - } - self.update_at_time(current_vtime); - } - None => break, // Simulation stopped - } - } - self.disable(); -} -``` - -Add helper methods: - -```rust -impl GenericClock { - /// Calculate the virtual time of the next phase transition. - fn next_transition_time(&self) -> Duration { - let phase_duration = match self.current_state { - PinValue::High => self.high_time, - PinValue::Low => self.low_time, - PinValue::HighZ => Duration::from_nanos(1), - }; - self.last_transition_vtime.saturating_add(phase_duration) - } - - /// Legacy run loop using wall-clock time (backward compatibility). - fn run_legacy(&mut self) { - while self.is_running() { - self.update(); - let sleep_time = if self.frequency > 0.0 { - Duration::from_secs_f64(1.0 / self.frequency / 100.0) - .max(Duration::from_micros(1)) - } else { - Duration::from_micros(100) - }; - thread::sleep(sleep_time); - } - self.disable(); - } -} -``` - -#### A2.10 — Add `timing()` override - -```rust -impl Component for GenericClock { - // ... existing methods ... - - fn timing(&self) -> ComponentTiming { - ComponentTiming { - name: "Generic Clock", - access_time: Duration::ZERO, - propagation_delay: Duration::ZERO, - setup_time: Duration::ZERO, - hold_time: Duration::ZERO, - max_clock_frequency: f64::INFINITY, - min_update_interval: self.high_time.min(self.low_time), - } - } -} -``` - -#### A2.11 — Update imports - -```rust -// BEFORE (line 1-9): -use parking_lot::Mutex; -use std::any::Any; -use std::collections::HashMap; -use std::sync::Arc; -use std::thread; -use std::time::{Duration, Instant}; -use crate::component::{BaseComponent, Component}; -use crate::pin::{Pin, PinValue}; - -// AFTER: -use parking_lot::Mutex; -use std::any::Any; -use std::collections::HashMap; -use std::sync::Arc; -use std::thread; -use std::time::Duration; -use crate::component::{BaseComponent, Component}; -use crate::component_timing::ComponentTiming; -use crate::pin::{Pin, PinValue}; -use crate::virtual_clock::VirtualClock; -``` - -**Key change:** Remove `Instant` from imports, add `VirtualClock` and `ComponentTiming`. - -### A3. GenericClock Test Impact - -The existing tests in [`generic_clock.rs:238-258`](src/components/clock/generic_clock.rs:238) are: - -| Test | What it checks | Impact | -|------|---------------|--------| -| [`test_clock_creation`](src/components/clock/generic_clock.rs:243) | Name and running state | **No impact** — doesn't use `Instant` | -| [`test_clock_timing_calculation`](src/components/clock/generic_clock.rs:250) | `frequency_to_duration()` | **No impact** — static calculation | - -These tests will pass unchanged. No new tests needed for the migration itself — the existing tests validate construction and static calculations. Virtual-time behavior tests should be added in Phase 9. - ---- - -## Part B: TwoPhaseClock Migration - -### B1. Current Timing Mechanism - -[`TwoPhaseClock`](src/components/clock/two_phase_clock.rs:11) uses wall-clock time in 5 locations: - -| Location | Line | Current Code | Purpose | -|----------|------|-------------|---------| -| [`last_transition`](src/components/clock/two_phase_clock.rs:15) field | 15 | `last_transition: Instant` | Track when last phase swap occurred | -| [`pause_start`](src/components/clock/two_phase_clock.rs:19) field | 19 | `pause_start: Option` | Track when pause began | -| [`new()`](src/components/clock/two_phase_clock.rs:38) constructor | 38 | `last_transition: Instant::now()` | Initialize transition timer | -| [`enable()`](src/components/clock/two_phase_clock.rs:64) | 64 | `self.last_transition = Instant::now()` | Reset timer on enable | -| [`pause()`](src/components/clock/two_phase_clock.rs:80) | 80 | `self.pause_start = Some(Instant::now())` | Record pause start | -| [`resume()`](src/components/clock/two_phase_clock.rs:88) | 88 | `pause_start.elapsed()` | Calculate pause duration | -| [`should_transition()`](src/components/clock/two_phase_clock.rs:137) | 137 | `self.last_transition.elapsed()` | Check if phase_time elapsed | -| [`perform_transition()`](src/components/clock/two_phase_clock.rs:160) | 160 | `self.last_transition = Instant::now()` | Record transition time | -| [`run()`](src/components/clock/two_phase_clock.rs:203) | 203 | `thread::sleep(Duration::from_micros(100))` | Fixed 100µs busy-wait | - -### B2. Required Changes - -#### B2.1 — Add `VirtualClock` field (optional, gradual integration) - -```rust -// BEFORE (line 11-21): -pub struct TwoPhaseClock { - base: BaseComponent, - phi1_state: PinValue, - phi2_state: PinValue, - last_transition: Instant, - phase_time: Duration, - enabled: bool, - paused: bool, - pause_start: Option, - frequency: f64, -} - -// AFTER: -pub struct TwoPhaseClock { - base: BaseComponent, - phi1_state: PinValue, - phi2_state: PinValue, - last_transition_vtime: Duration, // Virtual time of last phase swap - phase_time: Duration, - enabled: bool, - paused: bool, - pause_start_vtime: Option, // Virtual time when pause started - frequency: f64, - virtual_clock: Option>, // Injected in Phase 7 -} -``` - -#### B2.2 — Update constructor (line 24-45) - -```rust -// BEFORE: -TwoPhaseClock { - base: BaseComponent::new(name, pins), - phi1_state: PinValue::High, - phi2_state: PinValue::Low, - last_transition: Instant::now(), - phase_time, - enabled: true, - paused: false, - pause_start: None, - frequency, -} - -// AFTER: -TwoPhaseClock { - base: BaseComponent::new(name, pins), - phi1_state: PinValue::High, - phi2_state: PinValue::Low, - last_transition_vtime: Duration::ZERO, - phase_time, - enabled: true, - paused: false, - pause_start_vtime: None, - frequency, - virtual_clock: None, -} -``` - -#### B2.3 — Add `set_virtual_clock()` and `current_virtual_time()` helpers - -```rust -impl TwoPhaseClock { - /// Inject a VirtualClock for virtual-time-based operation. - pub fn set_virtual_clock(&mut self, clock: Arc) { - self.virtual_clock = Some(clock); - } - - /// Get current virtual time, or Duration::ZERO if no VirtualClock. - fn current_virtual_time(&self) -> Duration { - self.virtual_clock.as_ref().map_or(Duration::ZERO, |vc| vc.now()) - } -} -``` - -#### B2.4 — Update `enable()` (line 60-69) - -```rust -// BEFORE: -pub fn enable(&mut self) { - self.enabled = true; - self.phi1_state = PinValue::High; - self.phi2_state = PinValue::Low; - self.last_transition = Instant::now(); - self.update_outputs(); - self.update_outputs(); -} - -// AFTER: -pub fn enable(&mut self) { - self.enabled = true; - self.phi1_state = PinValue::High; - self.phi2_state = PinValue::Low; - self.last_transition_vtime = self.current_virtual_time(); - self.update_outputs(); - self.update_outputs(); -} -``` - -#### B2.5 — Update `pause()` and `resume()` (lines 79-95) - -Same pattern as GenericClock: - -```rust -pub fn pause(&mut self) { - self.paused = true; - self.pause_start_vtime = Some(self.current_virtual_time()); -} - -pub fn resume(&mut self) { - if let Some(pause_start_vtime) = self.pause_start_vtime { - let pause_duration = self.current_virtual_time().saturating_sub(pause_start_vtime); - self.last_transition_vtime = self.last_transition_vtime.saturating_add(pause_duration); - } - self.paused = false; - self.pause_start_vtime = None; -} -``` - -#### B2.6 — Update `should_transition()` (line 136-138) - -```rust -// BEFORE: -fn should_transition(&self) -> bool { - self.last_transition.elapsed() >= self.phase_time -} - -// AFTER: -fn should_transition(&self, virtual_time: Duration) -> bool { - let elapsed = virtual_time.saturating_sub(self.last_transition_vtime); - elapsed >= self.phase_time -} -``` - -#### B2.7 — Update `perform_transition()` (line 140-161) - -```rust -// BEFORE: -fn perform_transition(&mut self) { - match (self.phi1_state, self.phi2_state) { - (PinValue::High, PinValue::Low) => { - self.phi1_state = PinValue::Low; - self.phi2_state = PinValue::High; - } - (PinValue::Low, PinValue::High) => { - self.phi1_state = PinValue::High; - self.phi2_state = PinValue::Low; - } - _ => { - self.phi1_state = PinValue::High; - self.phi2_state = PinValue::Low; - } - } - self.update_outputs(); - self.last_transition = Instant::now(); -} - -// AFTER: -fn perform_transition(&mut self, virtual_time: Duration) { - match (self.phi1_state, self.phi2_state) { - (PinValue::High, PinValue::Low) => { - self.phi1_state = PinValue::Low; - self.phi2_state = PinValue::High; - } - (PinValue::Low, PinValue::High) => { - self.phi1_state = PinValue::High; - self.phi2_state = PinValue::Low; - } - _ => { - self.phi1_state = PinValue::High; - self.phi2_state = PinValue::Low; - } - } - self.update_outputs(); - self.last_transition_vtime = virtual_time; -} -``` - -#### B2.8 — Implement `update_at_time()` and update `update()` - -```rust -impl Component for TwoPhaseClock { - // ... existing name/pins/get_pin unchanged ... - - fn update_at_time(&mut self, virtual_time: Duration) { - if self.paused { - return; - } - if !self.enabled { - return; - } - self.update_outputs(); - if self.should_transition(virtual_time) { - self.perform_transition(virtual_time); - } - } - - fn update(&mut self) { - let vtime = self.current_virtual_time(); - self.update_at_time(vtime); - } - - // ... rest unchanged ... -} -``` - -#### B2.9 — Rewrite `run()` to use `wait_until()` (line 195-212) - -```rust -// BEFORE: -fn run(&mut self) { - self.base.set_running(true); - self.enable(); - while self.is_running() { - self.update(); - thread::sleep(Duration::from_micros(100)); - if !self.is_running() { - break; - } - } - self.disable(); -} - -// AFTER: -fn run(&mut self) { - self.base.set_running(true); - self.enable(); - - let vclock = match self.virtual_clock.as_ref() { - Some(vc) => Arc::clone(vc), - None => return self.run_legacy(), - }; - - while self.is_running() { - let next_transition = self.last_transition_vtime + self.phase_time; - match vclock.wait_until(next_transition) { - Some(current_vtime) => { - if !self.is_running() { - break; - } - self.update_at_time(current_vtime); - } - None => break, - } - } - self.disable(); -} -``` - -Add legacy helper: - -```rust -impl TwoPhaseClock { - /// Legacy run loop using wall-clock time (backward compatibility). - fn run_legacy(&mut self) { - while self.is_running() { - self.update(); - thread::sleep(Duration::from_micros(100)); - if !self.is_running() { - break; - } - } - self.disable(); - } -} -``` - -#### B2.10 — Add `timing()` override - -```rust -fn timing(&self) -> ComponentTiming { - ComponentTiming { - name: "Two-Phase Clock", - access_time: Duration::ZERO, - propagation_delay: Duration::ZERO, - setup_time: Duration::ZERO, - hold_time: Duration::ZERO, - max_clock_frequency: f64::INFINITY, - min_update_interval: self.phase_time, - } -} -``` - -#### B2.11 — Update imports - -```rust -// BEFORE (line 1-9): -use parking_lot::Mutex; -use std::any::Any; -use std::collections::HashMap; -use std::sync::Arc; -use std::thread; -use std::time::{Duration, Instant}; -use crate::component::{BaseComponent, Component}; -use crate::pin::{Pin, PinValue}; - -// AFTER: -use parking_lot::Mutex; -use std::any::Any; -use std::collections::HashMap; -use std::sync::Arc; -use std::thread; -use std::time::Duration; -use crate::component::{BaseComponent, Component}; -use crate::component_timing::ComponentTiming; -use crate::pin::{Pin, PinValue}; -use crate::virtual_clock::VirtualClock; -``` - -### B3. TwoPhaseClock Test Impact - -No tests exist for `TwoPhaseClock` (no `#[cfg(test)]` module in the file). No test breakage risk. - ---- - -## Part C: System Config Changes - -### C1. Factory Closure Updates - -In [`system_config.rs`](src/system_config.rs:241-277), the factory closures create clocks without a `VirtualClock`. In Phase 5, these remain unchanged — the `virtual_clock` field is `None` by default, and clocks use the legacy `run_legacy()` path. - -In **Phase 7**, `ConfigurableSystem` will: -1. Create `Arc` -2. After constructing each clock, call `set_virtual_clock(Arc::clone(&virtual_clock))` -3. This requires changing the factory closure signature or adding a post-construction hook - -For Phase 5, no changes to [`system_config.rs`](src/system_config.rs) are needed. - ---- - -## Part D: Backward Compatibility Analysis - -### D1. What stays the same - -| Aspect | Compatibility | -|--------|--------------| -| `new(name, frequency)` constructor signature | **Unchanged** — no new required parameters | -| `Component` trait impl | **Unchanged** — `update()` still works via delegation | -| `enable()` / `disable()` / `pause()` / `resume()` | **Unchanged** — public API identical | -| `set_frequency()` / `get_frequency()` | **Unchanged** | -| Pin names (CLK, PHI1, PHI2, ENABLE) | **Unchanged** | -| `run()` without VirtualClock | **Works** — falls back to `run_legacy()` with `thread::sleep()` | -| Existing tests | **Pass** — no Instant dependency in test assertions | - -### D2. What changes (internal only) - -| Change | Visible externally? | -|--------|-------------------| -| `last_transition: Instant` → `last_transition_vtime: Duration` | No — private field | -| `pause_start: Option` → `pause_start_vtime: Option` | No — private field | -| `should_transition()` takes `Duration` param | No — private method | -| `perform_transition()` takes `Duration` param | No — private method | -| New `virtual_clock: Option>` field | No — private, `None` by default | -| New `set_virtual_clock()` method | Yes — but opt-in, not required | - -### D3. Risk assessment - -**Risk: LOW.** All changes are internal field replacements. The public API is unchanged. The `Option>` pattern ensures clocks work identically without a VirtualClock injected. - ---- - -## Part E: Execution Checklist - -These are the implementation steps in order: - -- [ ] **E1.** Update `GenericClock` imports — remove `Instant`, add `VirtualClock` + `ComponentTiming` -- [ ] **E2.** Replace `GenericClock` struct fields: `last_transition: Instant` → `last_transition_vtime: Duration`, `pause_start: Option` → `pause_start_vtime: Option`, add `virtual_clock: Option>` -- [ ] **E3.** Update `GenericClock::new()` — initialize `last_transition_vtime: Duration::ZERO`, `virtual_clock: None` -- [ ] **E4.** Add `GenericClock::set_virtual_clock()` and `GenericClock::current_virtual_time()` helper methods -- [ ] **E5.** Update `GenericClock::enable()` — use `current_virtual_time()` instead of `Instant::now()` -- [ ] **E6.** Update `GenericClock::pause()` — use `current_virtual_time()` instead of `Instant::now()` -- [ ] **E7.** Update `GenericClock::resume()` — compute pause duration from virtual time deltas -- [ ] **E8.** Update `GenericClock::should_transition()` — accept `virtual_time: Duration` parameter -- [ ] **E9.** Update `GenericClock::perform_transition()` — accept `virtual_time: Duration`, store in `last_transition_vtime` -- [ ] **E10.** Implement `GenericClock::update_at_time()` in `Component` impl -- [ ] **E11.** Update `GenericClock::update()` to delegate to `update_at_time(current_virtual_time())` -- [ ] **E12.** Add `GenericClock::next_transition_time()` and `GenericClock::run_legacy()` helper methods -- [ ] **E13.** Rewrite `GenericClock::run()` — use `wait_until()` when VirtualClock present, else `run_legacy()` -- [ ] **E14.** Add `GenericClock::timing()` override in `Component` impl -- [ ] **E15.** Verify `GenericClock` existing tests still pass -- [ ] **E16.** Update `TwoPhaseClock` imports — remove `Instant`, add `VirtualClock` + `ComponentTiming` -- [ ] **E17.** Replace `TwoPhaseClock` struct fields: same pattern as GenericClock -- [ ] **E18.** Update `TwoPhaseClock::new()` — initialize virtual time fields -- [ ] **E19.** Add `TwoPhaseClock::set_virtual_clock()` and `TwoPhaseClock::current_virtual_time()` helpers -- [ ] **E20.** Update `TwoPhaseClock::enable()` — use `current_virtual_time()` -- [ ] **E21.** Update `TwoPhaseClock::pause()` and `resume()` — virtual time deltas -- [ ] **E22.** Update `TwoPhaseClock::should_transition()` — accept `virtual_time: Duration` -- [ ] **E23.** Update `TwoPhaseClock::perform_transition()` — accept `virtual_time: Duration` -- [ ] **E24.** Implement `TwoPhaseClock::update_at_time()` in `Component` impl -- [ ] **E25.** Update `TwoPhaseClock::update()` to delegate to `update_at_time()` -- [ ] **E26.** Add `TwoPhaseClock::run_legacy()` helper -- [ ] **E27.** Rewrite `TwoPhaseClock::run()` — use `wait_until()` when VirtualClock present -- [ ] **E28.** Add `TwoPhaseClock::timing()` override in `Component` impl -- [ ] **E29.** Run `cargo build` to verify compilation -- [ ] **E30.** Run `cargo test` to verify all existing tests pass - ---- - -## Flow Diagram: Clock Thread with Virtual Time - -```mermaid -flowchart TD - A[run called] --> B{virtual_clock set?} - B -->|No| C[run_legacy - thread::sleep loop] - B -->|Yes| D[enable - reset phase state] - D --> E[Calculate next_transition_time] - E --> F[wait_until next_transition] - F --> G{wait_until returned} - G -->|None - stopped| H[disable and exit] - G -->|Some vtime| I{is_running?} - I -->|No| H - I -->|Yes| J[update_at_time with vtime] - J --> K[Transition phase if needed] - K --> E -``` - -## Flow Diagram: Phase Transition Logic in Virtual Time - -```mermaid -flowchart LR - subgraph GenericClock - G1[last_transition_vtime = 0ns] --> G2{vtime >= high_time?} - G2 -->|Yes| G3[High to Low transition] - G3 --> G4[last_transition_vtime = vtime] - G4 --> G5{vtime >= low_time?} - G5 -->|Yes| G6[Low to High transition] - G6 --> G4 - end - - subgraph TwoPhaseClock - T1[last_transition_vtime = 0ns] --> T2{elapsed >= phase_time?} - T2 -->|Yes| T3[PHI1 High+PHI2 Low to PHI1 Low+PHI2 High] - T3 --> T4[last_transition_vtime = vtime] - T4 --> T5{elapsed >= phase_time?} - T5 -->|Yes| T6[PHI1 Low+PHI2 High to PHI1 High+PHI2 Low] - T6 --> T4 - end -``` diff --git a/plans/phase6_timing_enforcement.md b/plans/phase6_timing_enforcement.md deleted file mode 100644 index 3d6b6f8..0000000 --- a/plans/phase6_timing_enforcement.md +++ /dev/null @@ -1,870 +0,0 @@ -# Phase 6: Add Timing Enforcement to Memory/CPU Components - -## Overview - -Phase 6 adds virtual-time-based timing enforcement to the four Intel 400x series components: ROM (4001), RAM (4002), Shift Register (4003), and CPU (4004). These components currently use `Instant::now()` for address latch timestamps and check latency via `Instant::elapsed()`. This phase replaces those with virtual time from `Arc`, adds `ComponentTiming` enforcement checks, and records timing violations. - -**Files modified:** -- [`intel_400x.rs`](src/components/common/intel_400x.rs) — Shared state machine timing (highest priority) -- [`intel_4001.rs`](src/components/memory/intel_4001.rs) — Intel 4001 ROM -- [`intel_4002.rs`](src/components/memory/intel_4002.rs) — Intel 4002 RAM -- [`intel_4003.rs`](src/components/memory/intel_4003.rs) — Intel 4003 Shift Register -- [`intel_4004.rs`](src/components/cpu/intel_4004.rs) — Intel 4004 CPU - -**Files NOT modified** (Phase 7 responsibility): -- [`system_config.rs`](src/system_config.rs) — Will inject `Arc` into components -- [`virtual_clock.rs`](src/virtual_clock.rs) — Already complete -- [`component_timing.rs`](src/component_timing.rs) — Already complete - ---- - -## Implementation Order - -```mermaid -graph LR - A[Step 0: intel_400x.rs shared code] --> B[Step 1: Intel 4003 SR] - B --> C[Step 2: Intel 4001 ROM] - C --> D[Step 3: Intel 4002 RAM] - D --> E[Step 4: Intel 4004 CPU] -``` - -**Rationale:** -1. **`intel_400x.rs` first** — The shared state machine contains `address_latch_time: Option` and `is_latency_elapsed()` used by all three memory components. Changing this first unblocks all component migrations. -2. **Intel 4003 second** — Simplest component; no address latching in the traditional sense, uses shift/load operations. Good validation of the shared code changes. -3. **Intel 4001 third** — ROM is simpler than RAM (read-only, no write operations). Validates the full address→wait→drive flow. -4. **Intel 4002 fourth** — RAM adds write operations and I/O port states on top of the ROM pattern. -5. **Intel 4004 last** — CPU is the most complex (98K chars, own MemoryState enum, own `address_latch_time` field). Benefits from all patterns established by the memory components. - ---- - -## Step 0: Migrate Shared State Machine (`intel_400x.rs`) - -### Current Timing Mechanism - -The shared [`Intel400xStateMachine`](src/components/common/intel_400x.rs:830) uses `Instant` in two critical locations: - -| Location | Line | Current Code | Purpose | -|----------|------|-------------|---------| -| [`address_latch_time`](src/components/common/intel_400x.rs:824) field | 824 | `address_latch_time: Option` | Timestamp when address was fully latched | -| [`handle_address_nibble()`](src/components/common/intel_400x.rs:958) | 958 | `self.address_latch_time = Some(Instant::now())` | Record latch time when both nibbles received | -| [`is_latency_elapsed()`](src/components/common/intel_400x.rs:967) | 969 | `latch_time.elapsed() >= self.access_time` | Check if access time has passed | - -Additionally, the [`Intel400xTimingState`](src/components/common/intel_400x.rs:320) trait exposes `Instant` in its interface: - -| Location | Line | Signature | -|----------|------|-----------| -| [`get_address_latch_time()`](src/components/common/intel_400x.rs:324) | 324 | `fn get_address_latch_time(&self) -> Option` | -| [`set_address_latch_time()`](src/components/common/intel_400x.rs:325) | 325 | `fn set_address_latch_time(&mut self, time: Option)` | - -And the [`Intel400xAddressHandling`](src/components/common/intel_400x.rs:196) trait's [`handle_address_latching()`](src/components/common/intel_400x.rs:206) method takes `&mut Option`: - -| Location | Line | Current Code | -|----------|------|-------------| -| [`handle_address_latching()`](src/components/common/intel_400x.rs:212) | 212 | `address_latch_time: &mut Option` parameter | -| | 223 | `*address_latch_time = Some(Instant::now())` | -| [`handle_latency_wait()`](src/components/common/intel_400x.rs:236) | 236 | `address_latch_time: &Option` parameter | -| | 240 | `latch_time.elapsed() >= access_time` | - -### Required Changes - -#### 0.1 — Change `address_latch_time` from `Instant` to `Duration` - -Replace `Instant`-based timing with virtual time (`Duration` = nanoseconds since simulation start). - -```rust -// BEFORE (line 824): -address_latch_time: Option, - -// AFTER: -address_latch_time: Option, // Virtual time when address was latched -``` - -#### 0.2 — Add `is_latency_elapsed_at()` method (new, virtual-time-aware) - -Add a new method that takes the current virtual time as a parameter, replacing the `Instant::elapsed()`-based check: - -```rust -// NEW METHOD — add after is_latency_elapsed() at line ~973: -/// Check if latency has elapsed at a given virtual time. -/// Returns true if `virtual_time - address_latch_time >= access_time`. -pub fn is_latency_elapsed_at(&self, virtual_time: Duration) -> bool { - if let Some(latch_time) = self.address_latch_time { - virtual_time.saturating_sub(latch_time) >= self.access_time - } else { - false - } -} -``` - -**Keep the existing `is_latency_elapsed()` method** for backward compatibility — it will continue to work with `Instant::now()` for legacy code paths. Mark it with a comment that it's deprecated for virtual-time usage. - -#### 0.3 — Add `set_address_latch_vtime()` method (new) - -```rust -/// Set address latch time using virtual time directly. -/// Called from update_at_time() when both address nibbles are latched. -pub fn set_address_latch_vtime(&mut self, vtime: Duration) { - self.address_latch_time = Some(vtime); -} -``` - -#### 0.4 — Add `handle_address_nibble_at()` method (new, virtual-time-aware) - -```rust -/// Handle address nibble latching with virtual time. -/// Returns true only when BOTH nibbles have been latched. -/// Records address_latch_time as virtual_time when complete. -pub fn handle_address_nibble_at(&mut self, nibble: u8, virtual_time: Duration) -> bool { - if self.address_high_nibble.is_none() { - self.address_high_nibble = Some(nibble); - false - } else if self.address_low_nibble.is_none() { - self.address_low_nibble = Some(nibble); - self.address_latch_time = Some(virtual_time); // Virtual time, not Instant::now() - true - } else { - false - } -} -``` - -#### 0.5 — Update `Intel400xTimingState` trait (additive only) - -Add new virtual-time-aware methods to the trait alongside existing ones: - -```rust -pub trait Intel400xTimingState { - // ... existing methods unchanged ... - - /// Get address latch time as virtual time (Duration) - fn get_address_latch_vtime(&self) -> Option; - - /// Set address latch time using virtual time - fn set_address_latch_vtime(&mut self, time: Option); - - /// Check if latency has elapsed at a given virtual time - fn is_latency_elapsed_at(&self, virtual_time: Duration) -> bool; -} -``` - -#### 0.6 — Update `Intel400xAddressHandling` trait (additive only) - -Add virtual-time-aware versions alongside existing methods: - -```rust -pub trait Intel400xAddressHandling { - // ... existing methods unchanged ... - - /// Handle latency wait check using virtual time - fn handle_latency_wait_at( - &self, - address_latch_time: &Option, - access_time: Duration, - virtual_time: Duration, - ) -> bool { - if let Some(latch_time) = address_latch_time { - virtual_time.saturating_sub(*latch_time) >= access_time - } else { - false - } - } -} -``` - -#### 0.7 — Update getter/setter for `address_latch_time` - -```rust -// Change signatures (lines 916-923): -pub fn get_address_latch_time(&self) -> Option { // was Option - self.address_latch_time -} - -pub fn set_address_latch_time(&mut self, time: Option) { // was Option - self.address_latch_time = time; -} -``` - -**Impact:** This changes the return type of `get_address_latch_time()` from `Option` to `Option`. All callers (the `Intel400xTimingState` impls in each component) must be updated to match. - ---- - -## Step 1: Intel 4003 Shift Register - -### Current Timing Mechanism - -[`Intel4003`](src/components/memory/intel_4003.rs:24) is the simplest component: -- Uses [`Intel400xStateMachine`](src/components/memory/intel_4003.rs:32) for state management -- Implements [`Intel400xTimingState`](src/components/memory/intel_4003.rs:94) trait -- Has no direct `Instant::now()` calls — all timing goes through the shared state machine -- Has `thread::sleep(Duration::from_micros(1))` in [`run()`](src/components/memory/intel_4003.rs:586) (line 597) -- Uses `prev_phi1`/`prev_phi2` for edge detection (same as ROM/RAM) - -### Timing Enforcement Points - -The 4003 has simpler timing than ROM/RAM: -- **No address latching** — `responds_to_address() = false` -- **Shift operations** triggered by clock edges -- **Access time: 200ns** — faster than ROM/RAM - -### Required Changes - -#### 1.1 — Add `VirtualClock` field and helper - -```rust -// Add to struct Intel4003 (after state_machine field): -virtual_clock: Option>, - -// Add helper method: -fn current_virtual_time(&self) -> Duration { - self.virtual_clock.as_ref().map_or(Duration::ZERO, |vc| vc.now()) -} -``` - -#### 1.2 — Add `set_virtual_clock()` method - -```rust -pub fn set_virtual_clock(&mut self, clock: Arc) { - self.virtual_clock = Some(clock); -} -``` - -#### 1.3 — Update `Intel400xTimingState` impl - -Change `get_address_latch_time` / `set_address_latch_time` to use `Duration`: - -```rust -// BEFORE (line 124): -fn get_address_latch_time(&self) -> Option { - self.state_machine.get_address_latch_time() -} - -fn set_address_latch_time(&mut self, time: Option) { - self.state_machine.set_address_latch_time(time); -} - -// AFTER: -fn get_address_latch_time(&self) -> Option { - self.state_machine.get_address_latch_time() -} - -fn set_address_latch_time(&mut self, time: Option) { - self.state_machine.set_address_latch_time(time); -} - -// New virtual-time methods: -fn get_address_latch_vtime(&self) -> Option { - self.state_machine.get_address_latch_time() -} - -fn set_address_latch_vtime(&mut self, time: Option) { - self.state_machine.set_address_latch_time(time); -} - -fn is_latency_elapsed_at(&self, virtual_time: Duration) -> bool { - self.state_machine.is_latency_elapsed_at(virtual_time) -} -``` - -#### 1.4 — Implement `update_at_time()` - -```rust -fn update_at_time(&mut self, virtual_time: Duration) { - if !self.is_running() { - return; - } - - let phi1_rising = self.is_phi1_rising_edge(self.prev_phi1); - let phi1_falling = self.is_phi1_falling_edge(self.prev_phi1); - let phi2_rising = self.is_phi2_rising_edge(self.prev_phi2); - let phi2_falling = self.is_phi2_falling_edge(self.prev_phi2); - - let (phi1, phi2) = self.read_clock_pins(); - self.prev_phi1 = phi1; - self.prev_phi2 = phi2; - - let reset_active = self.read_reset_pin(); - if reset_active { - self.perform_reset(); - return; - } - - // Timing enforcement: check if clock frequency is within spec - // (frequency check happens at clock edge detection time) - - if phi2_falling { - self.handle_phi2_falling(); - } - if phi1_falling { - self.handle_phi1_falling(); - } - if phi1_rising { - self.handle_phi1_rising(); - } - if phi2_rising { - self.handle_phi2_rising(); - } -} -``` - -#### 1.5 — Update `update()` to delegate to `update_at_time()` - -```rust -fn update(&mut self) { - let vtime = self.current_virtual_time(); - self.update_at_time(vtime); -} -``` - -#### 1.6 — Update `run()` with VirtualClock wait pattern - -```rust -fn run(&mut self) { - self.base.set_running(true); - - // Initialize clock states for edge detection - let (phi1, phi2) = self.read_clock_pins(); - self.prev_phi1 = phi1; - self.prev_phi2 = phi2; - - // If no VirtualClock, fall back to legacy busy-wait loop - let vclock = match self.virtual_clock.as_ref() { - Some(vc) => Arc::clone(vc), - None => { - while self.is_running() { - self.update(); - thread::sleep(Duration::from_micros(1)); - } - return; - } - }; - - // Virtual-time run loop - let timing = ComponentTiming::intel_4003(); - while self.is_running() { - let next_update = vclock.now() + timing.min_update_interval; - match vclock.wait_until(next_update) { - Some(current_vtime) => { - if !self.is_running() { break; } - self.update_at_time(current_vtime); - } - None => break, - } - } -} -``` - -#### 1.7 — Implement `timing()` trait method - -```rust -fn timing(&self) -> ComponentTiming { - ComponentTiming::intel_4003() -} -``` - -#### 1.8 — Update constructor - -```rust -// Add to new_with_access_time(): -virtual_clock: None, -``` - -#### 1.9 — Add import - -```rust -use crate::virtual_clock::VirtualClock; -use crate::component_timing::ComponentTiming; -``` - ---- - -## Step 2: Intel 4001 ROM - -### Current Timing Mechanism - -[`Intel4001`](src/components/memory/intel_4001.rs:25) uses timing in several places: - -| Location | Line | Current Code | Purpose | -|----------|------|-------------|---------| -| [`access_time`](src/components/memory/intel_4001.rs:29) field | 29 | `access_time: Duration` | ROM access latency (500ns) | -| [`state_machine.is_latency_elapsed()`](src/components/memory/intel_4001.rs:277) | 277 | In `execute_state_logic()` WaitLatency | Check if ROM access time elapsed | -| [`state_machine.is_latency_elapsed()`](src/components/memory/intel_4001.rs:782) | 782 | In legacy `handle_latency_wait()` | Same check in legacy path | -| [`thread::sleep()`](src/components/memory/intel_4001.rs:968) | 968 | In `run()` | Busy-wait loop | - -The ROM state machine flow: -``` -Idle → AddressPhase → WaitLatency → DriveData → Idle -``` - -### Timing Enforcement Points - -```mermaid -graph TD - A[PHI1 Rising: Address Latching] -->|address_latch_time = virtual_time| B[WaitLatency State] - B -->|Check: virtual_time - latch_time >= access_time| C{Access Time Elapsed?} - C -->|Yes| D[PHI2 Rising: Drive Data onto bus] - C -->|No - Timing Violation| E[Record violation - data undefined] - D --> F[PHI2 Falling: Tri-state bus, return to Idle] -``` - -**Key enforcement point:** In `WaitLatency` state, when checking if latency has elapsed, use virtual time comparison. If the clock is overclocked (phase time < access_time), the ROM will NOT produce valid data — just like real hardware. - -### Required Changes - -#### 2.1 — Add `VirtualClock` field and helper - -```rust -// Add to struct Intel4001: -virtual_clock: Option>, - -// Add helper: -fn current_virtual_time(&self) -> Duration { - self.virtual_clock.as_ref().map_or(Duration::ZERO, |vc| vc.now()) -} -``` - -#### 2.2 — Add `set_virtual_clock()` method - -```rust -pub fn set_virtual_clock(&mut self, clock: Arc) { - self.virtual_clock = Some(clock); -} -``` - -#### 2.3 — Update `Intel400xTimingState` impl - -Same pattern as 4003 — change `Instant` to `Duration` in `get/set_address_latch_time()`, add new virtual-time trait methods. - -#### 2.4 — Implement `update_at_time()` - -```rust -fn update_at_time(&mut self, virtual_time: Duration) { - if !self.is_running() { - return; - } - - let phi1_rising = self.is_phi1_rising_edge(self.prev_phi1); - let phi1_falling = self.is_phi1_falling_edge(self.prev_phi1); - let phi2_rising = self.is_phi2_rising_edge(self.prev_phi2); - let phi2_falling = self.is_phi2_falling_edge(self.prev_phi2); - - let (phi1, phi2) = self.read_clock_pins(); - self.prev_phi1 = phi1; - self.prev_phi2 = phi2; - - let reset_active = self.read_reset_pin(); - if reset_active { - self.perform_reset(); - return; - } - - if phi2_falling { self.handle_phi2_falling(); } - if phi1_falling { self.handle_phi1_falling(); } - if phi1_rising { self.handle_phi1_rising(); } - if phi2_rising { self.handle_phi2_rising(); } -} -``` - -#### 2.5 — Update `update()` to delegate - -```rust -fn update(&mut self) { - let vtime = self.current_virtual_time(); - self.update_at_time(vtime); -} -``` - -#### 2.6 — Update `run()` with VirtualClock wait pattern - -Same pattern as 4003 — check for `virtual_clock`, fall back to legacy busy-wait if absent. - -#### 2.7 — Implement `timing()` trait method - -```rust -fn timing(&self) -> ComponentTiming { - ComponentTiming::intel_4001() -} -``` - -#### 2.8 — Update `handle_phi2_rising()` for timing enforcement - -The key timing enforcement happens in [`handle_phi2_rising()`](src/components/memory/intel_4001.rs:484) where `WaitLatency` state checks `is_latency_elapsed()`: - -```rust -// BEFORE (line 487-493): -if self.state_machine.get_state() == Intel400xUnifiedState::WaitLatency { - if self.a3_wait_cycles > 0 { - self.a3_wait_cycles -= 1; - } else { - // A3 wait complete, transition to DriveData. - self.process_latency_elapsed(); - } -} - -// AFTER — add timing check: -if self.state_machine.get_state() == Intel400xUnifiedState::WaitLatency { - if self.a3_wait_cycles > 0 { - self.a3_wait_cycles -= 1; - } else { - // Check access time using virtual time - let virtual_time = self.current_virtual_time(); - if self.state_machine.is_latency_elapsed_at(virtual_time) { - self.process_latency_elapsed(); - } else { - // TIMING VIOLATION: Access time not yet elapsed - // Component produces undefined results (just like real hardware) - // The ROM will NOT drive data — the bus remains undefined - // Violation is recorded but simulation continues - } - } -} -``` - -#### 2.9 — Update `execute_state_logic()` WaitLatency branch - -Similarly update the [`execute_state_logic()`](src/components/memory/intel_4001.rs:275) WaitLatency branch: - -```rust -// BEFORE (line 275-280): -Intel400xUnifiedState::WaitLatency => { - if self.state_machine.is_latency_elapsed() { - self.process_latency_elapsed(); - } -} - -// AFTER: -Intel400xUnifiedState::WaitLatency => { - let virtual_time = self.current_virtual_time(); - if self.state_machine.is_latency_elapsed_at(virtual_time) { - self.process_latency_elapsed(); - } -} -``` - -#### 2.10 — Update constructor and imports - -```rust -// Add to new_with_access_time(): -virtual_clock: None, - -// Add imports: -use crate::virtual_clock::VirtualClock; -use crate::component_timing::ComponentTiming; -``` - ---- - -## Step 3: Intel 4002 RAM - -### Current Timing Mechanism - -[`Intel4002`](src/components/memory/intel_4002.rs) follows the same pattern as ROM but adds: -- **Read/Write/OutputPort** states (vs. just DriveData) -- [`is_latency_elapsed()`](src/components/memory/intel_4002.rs:268) check in `execute_state_logic()` WaitLatency -- [`is_latency_elapsed()`](src/components/memory/intel_4002.rs:480) check in `handle_phi2_rising()` -- [`thread::sleep()`](src/components/memory/intel_4002.rs:726) in `run()` (line 726) - -State machine flow: -``` -Idle → AddressPhase → WaitLatency → ReadData/WriteData/OutputPort → Idle -``` - -### Timing Enforcement Points - -Same as ROM — the critical check is in `WaitLatency` state when transitioning to data operations. - -### Required Changes - -Identical pattern to ROM (Step 2): - -#### 3.1 — Add `virtual_clock: Option>` field + helper -#### 3.2 — Add `set_virtual_clock()` method -#### 3.3 — Update `Intel400xTimingState` impl (Instant → Duration) -#### 3.4 — Implement `update_at_time()` (same structure as ROM) -#### 3.5 — Update `update()` to delegate to `update_at_time()` -#### 3.6 — Update `run()` with VirtualClock wait pattern -#### 3.7 — Implement `timing()` returning `ComponentTiming::intel_4002()` -#### 3.8 — Update `handle_phi2_rising()` WaitLatency check (line ~479-481): - -```rust -// BEFORE: -if self.state_machine.is_latency_elapsed() { - self.process_latency_elapsed(); -} - -// AFTER: -let virtual_time = self.current_virtual_time(); -if self.state_machine.is_latency_elapsed_at(virtual_time) { - self.process_latency_elapsed(); -} -``` - -#### 3.9 — Update `execute_state_logic()` WaitLatency branch (line ~266-269) -#### 3.10 — Update constructor and imports - ---- - -## Step 4: Intel 4004 CPU - -### Current Timing Mechanism - -[`Intel4004`](src/components/cpu/intel_4004.rs:124) has its **own** `MemoryState` enum and timing fields, separate from the shared `Intel400xStateMachine`: - -| Location | Line | Current Code | Purpose | -|----------|------|-------------|---------| -| [`address_latch_time`](src/components/cpu/intel_4004.rs:168) field | 168 | `address_latch_time: Option` | Timestamp when address was latched | -| [`access_time`](src/components/cpu/intel_4004.rs:170) field | 170 | `access_time: Duration` | Memory access time (500ns) | -| [`advance_address_cycle()`](src/components/cpu/intel_4004.rs:596) | 596 | `self.address_latch_time = Some(Instant::now())` | Record latch time at A2→A3 transition | -| [`thread::sleep()`](src/components/cpu/intel_4004.rs:2387) | 2387 | In `run()` | Busy-wait loop | - -**Key difference:** The CPU does NOT use the shared `Intel400xStateMachine`. It has its own `MemoryState` enum and `AddressCycle` enum. The `address_latch_time` field is directly on the CPU struct. - -### Timing Enforcement Points - -The CPU's timing enforcement is different from memory components: -- **Propagation delay** (150ns): After the CPU drives address/data onto the bus, its outputs don't change instantly -- **Setup/hold time checks**: The CPU should verify that data from ROM/RAM was stable for the required setup time before the clock edge -- **Frequency check**: The CPU should verify the clock frequency is within spec (750 kHz max) - -```mermaid -graph TD - A[A1: Drive addr high nibble] --> B[A2: Drive addr low nibble] - B -->|address_latch_time = virtual_time| C[A3: Wait for ROM/RAM] - C -->|Wait access_time| D[M1: Read OPR from bus] - D --> E[M2: Read OPA from bus] - E --> F[Execute instruction] -``` - -### Required Changes - -#### 4.1 — Change `address_latch_time` from `Instant` to `Duration` - -```rust -// BEFORE (line 168): -address_latch_time: Option, - -// AFTER: -address_latch_time: Option, // Virtual time when address was latched -``` - -#### 4.2 — Add `VirtualClock` field and helper - -```rust -// Add to struct Intel4004: -virtual_clock: Option>, - -fn current_virtual_time(&self) -> Duration { - self.virtual_clock.as_ref().map_or(Duration::ZERO, |vc| vc.now()) -} -``` - -#### 4.3 — Add `set_virtual_clock()` method - -```rust -pub fn set_virtual_clock(&mut self, clock: Arc) { - self.virtual_clock = Some(clock); -} -``` - -#### 4.4 — Update `advance_address_cycle()` (line 596) - -```rust -// BEFORE: -self.address_latch_time = Some(Instant::now()); - -// AFTER: -self.address_latch_time = Some(self.current_virtual_time()); -``` - -#### 4.5 — Implement `update_at_time()` - -```rust -fn update_at_time(&mut self, virtual_time: Duration) { - if !self.is_running() { - return; - } - - // ... same clock edge detection logic as current update() ... - // The key difference: address_latch_time is now Duration-based, - // so any elapsed time checks use virtual_time - address_latch_time -} -``` - -#### 4.6 — Update `update()` to delegate - -```rust -fn update(&mut self) { - let vtime = self.current_virtual_time(); - self.update_at_time(vtime); -} -``` - -#### 4.7 — Update `run()` with VirtualClock wait pattern - -```rust -fn run(&mut self) { - self.base.set_running(true); - - let vclock = match self.virtual_clock.as_ref() { - Some(vc) => Arc::clone(vc), - None => { - // Legacy busy-wait loop - while self.is_running() { - self.update(); - thread::sleep(Duration::from_micros(1)); - if self.cycle_count > 100000 { - // ... safety check ... - } - } - return; - } - }; - - let timing = ComponentTiming::intel_4004(); - while self.is_running() { - let next_update = vclock.now() + timing.min_update_interval; - match vclock.wait_until(next_update) { - Some(current_vtime) => { - if !self.is_running() { break; } - self.update_at_time(current_vtime); - if self.cycle_count > 100000 { - // ... safety check ... - } - } - None => break, - } - } -} -``` - -#### 4.8 — Implement `timing()` trait method - -```rust -fn timing(&self) -> ComponentTiming { - ComponentTiming::intel_4004() -} -``` - -#### 4.9 — Update constructor and reset methods - -```rust -// In new() constructor: -address_latch_time: None, // Already None, just type change -virtual_clock: None, - -// In reset methods (lines 278, 497, 705): -self.address_latch_time = None; // No change needed — already None -``` - -#### 4.10 — Add imports - -```rust -use crate::virtual_clock::VirtualClock; -use crate::component_timing::ComponentTiming; -``` - ---- - -## Timing Violation Handling Strategy - -Timing violations are **recorded, not fatal**. When a violation is detected: - -1. **Access time not elapsed** (ROM/RAM): The component stays in `WaitLatency` and does NOT drive data onto the bus. The bus remains in its previous state (possibly undefined). The CPU reads garbage — just like real hardware. - -2. **Overclocked frequency**: Detected when the clock period is shorter than `min_clock_period()`. The component continues operating but may produce incorrect results. Violation is logged. - -3. **Setup/hold time violation**: The latched value may be incorrect. The component continues with the corrupted value — just like real hardware. - -### Violation Recording (Future Enhancement) - -For Phase 6, violations are detected at the enforcement points but **not yet recorded to a global log**. The component simply behaves incorrectly (doesn't drive data, latches wrong values, etc.). A `TimingViolationLog` can be added in a future phase to collect violations for display in the GUI. - -The `ComponentTiming` check methods already return `Result<(), TimingViolation>`: -- [`check_access_time(elapsed)`](src/component_timing.rs:221) — Returns `Err(TimingViolation::AccessTimeNotElapsed)` if not enough time -- [`check_setup_time(stability_duration)`](src/component_timing.rs:236) — Returns `Err(TimingViolation::SetupTimeViolation)` -- [`check_hold_time(stability_duration)`](src/component_timing.rs:251) — Returns `Err(TimingViolation::HoldTimeViolation)` -- [`supports_frequency(freq_hz)`](src/component_timing.rs:214) — Returns `bool` - -These can be used at enforcement points to generate violations for logging when that infrastructure is added. - ---- - -## Summary of Changes by File - -### [`intel_400x.rs`](src/components/common/intel_400x.rs) (Shared) -| Change | Type | -|--------|------| -| `address_latch_time: Option` → `Option` | Field type change | -| `get/set_address_latch_time()` return/param type | Signature change | -| Add `is_latency_elapsed_at(virtual_time: Duration)` | New method | -| Add `handle_address_nibble_at(nibble, virtual_time)` | New method | -| Add `set_address_latch_vtime(time: Duration)` | New method | -| Add virtual-time methods to `Intel400xTimingState` trait | Trait extension | -| Add `handle_latency_wait_at()` to `Intel400xAddressHandling` trait | Trait extension | -| Keep existing `is_latency_elapsed()` for backward compat | No change | - -### [`intel_4001.rs`](src/components/memory/intel_4001.rs) (ROM) -| Change | Type | -|--------|------| -| Add `virtual_clock: Option>` field | New field | -| Add `set_virtual_clock()` method | New method | -| Add `current_virtual_time()` helper | New method | -| Implement `update_at_time(virtual_time: Duration)` | New method | -| Update `update()` to delegate to `update_at_time()` | Modify | -| Update `run()` with VirtualClock wait pattern | Modify | -| Implement `timing()` returning `ComponentTiming::intel_4001()` | New method | -| Update `Intel400xTimingState` impl (Instant → Duration) | Modify | -| Update `handle_phi2_rising()` WaitLatency check | Modify | -| Update `execute_state_logic()` WaitLatency check | Modify | -| Update constructor (`virtual_clock: None`) | Modify | -| Add imports for `VirtualClock`, `ComponentTiming` | Modify | - -### [`intel_4002.rs`](src/components/memory/intel_4002.rs) (RAM) -| Change | Type | -|--------|------| -| Same changes as ROM, but with `ComponentTiming::intel_4002()` | Same pattern | -| Update `handle_phi2_rising()` WaitLatency check | Modify | -| Update `execute_state_logic()` WaitLatency check | Modify | - -### [`intel_4003.rs`](src/components/memory/intel_4003.rs) (Shift Register) -| Change | Type | -|--------|------| -| Same pattern as ROM, but with `ComponentTiming::intel_4003()` | Same pattern | -| No `handle_phi2_rising()` WaitLatency changes needed (4003 doesn't wait for access time the same way) | N/A | - -### [`intel_4004.rs`](src/components/cpu/intel_4004.rs) (CPU) -| Change | Type | -|--------|------| -| `address_latch_time: Option` → `Option` | Field type change | -| Add `virtual_clock: Option>` field | New field | -| Add `set_virtual_clock()` method | New method | -| Add `current_virtual_time()` helper | New method | -| Implement `update_at_time(virtual_time: Duration)` | New method | -| Update `update()` to delegate to `update_at_time()` | Modify | -| Update `run()` with VirtualClock wait pattern | Modify | -| Implement `timing()` returning `ComponentTiming::intel_4004()` | New method | -| Update `advance_address_cycle()` latch time assignment | Modify | -| Update constructor (`virtual_clock: None`) | Modify | -| Add imports for `VirtualClock`, `ComponentTiming` | Modify | - ---- - -## Backward Compatibility - -All changes follow the Phase 5 pattern: -- `Option>` — components work without a VirtualClock -- `run()` falls back to legacy `thread::sleep()` busy-wait when no VirtualClock is injected -- `update()` delegates to `update_at_time()` using `current_virtual_time()` (returns `Duration::ZERO` when no clock) -- `update_at_time()` is a new override; existing `update()` callers continue to work -- Existing `is_latency_elapsed()` method kept for backward compat (uses `Instant::elapsed()` internally) - -Phase 7 (`ConfigurableSystem`) will create the `Arc` and inject it via `set_virtual_clock()` on each component. - ---- - -## Testing Strategy - -After each component migration, verify: -1. **Legacy path still works** — Run existing tests without VirtualClock injected -2. **Virtual time path works** — Inject a VirtualClock, advance time, verify correct behavior -3. **Timing violation behavior** — Overclock the clock and verify ROM/RAM produce incorrect results while the faster shift register still works -4. **State machine transitions** — Verify Idle → AddressPhase → WaitLatency → DriveData cycle completes correctly in virtual time diff --git a/plans/phase7_system_integration.md b/plans/phase7_system_integration.md deleted file mode 100644 index 746c1f2..0000000 --- a/plans/phase7_system_integration.md +++ /dev/null @@ -1,174 +0,0 @@ -# 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 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, - governor_handle: Arc>>>, -} -``` - -#### Modified `new()` Constructor - -- Derive tick_size from clock frequency in metadata -- Create `Arc` 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` 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` to each concrete type. The order of attempts: - -1. `GenericClock` — `comp.as_any_mut().downcast_mut::()` -2. `TwoPhaseClock` — `comp.as_any_mut().downcast_mut::()` -3. `Intel4004` — `comp.as_any_mut().downcast_mut::()` -4. `Intel4001` — `comp.as_any_mut().downcast_mut::()` -5. `Intel4002` — `comp.as_any_mut().downcast_mut::()` -6. `Intel4003` — `comp.as_any_mut().downcast_mut::()` - -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 diff --git a/plans/phase8_gui_controls.md b/plans/phase8_gui_controls.md deleted file mode 100644 index 3bad835..0000000 --- a/plans/phase8_gui_controls.md +++ /dev/null @@ -1,117 +0,0 @@ -# Phase 8: Speed/Step Controls for GUI and Console - -## Overview - -Add simulation speed, mode, and step controls to both the egui-based GUI and the ratatui-based console interface. These controls leverage the existing `VirtualClock` and `RealTimeGovernor` APIs. - -## Files to Modify - -1. **`src/gui/state.rs`** — Add `TimingState` struct to `GuiState` for virtual time display data -2. **`src/gui/components.rs`** — Add `TimingControls` component (speed slider, mode selector, step button, time display) -3. **`src/gui/mod.rs`** — Integrate `TimingControls` into the main render loop -4. **`src/gui/console.rs`** — Add console commands: `speed`, `mode`, `step`, `time` - -## Detailed Design - -### 1. GUI State (`src/gui/state.rs`) - -Add a `TimingState` struct to `GuiState`: - -```rust -pub struct TimingState { - /// Current virtual time in nanoseconds - pub virtual_time_ns: u64, - /// Total ticks elapsed - pub tick_count: u64, - /// Current speed multiplier (from VirtualClock) - pub target_speed: f64, - /// Actual measured speed (from RealTimeGovernor snapshots) - pub actual_speed: f64, - /// Current simulation mode: "RealTime", "MaxSpeed", "SingleStep" - pub mode: String, - /// Whether a step is pending (SingleStep mode) - pub step_pending: bool, -} -``` - -The `update_from_system()` method will be extended to: -- Access `ConfigurableSystem::get_virtual_clock()` → `Arc` -- Read `clock.now_ns()`, `clock.tick_count()`, `clock.speed_multiplier()`, `clock.mode()`, `clock.is_step_pending()` -- Compute actual speed using `RealTimeGovernor::take_speed_snapshot()` / `compute_speed()` across frames - -To support speed snapshot computation across frames, `GuiState` will hold two `SpeedSnapshot` instances: -```rust -pub speed_snap_prev: Option, -pub speed_snap_curr: Option, -``` - -### 2. TimingControls Component (`src/gui/components.rs`) - -New component `TimingControls` with: - -#### Speed Slider (Logarithmic Scale) -- Slider range: logarithmic from 0.001× to 100.0× -- Uses `egui::Slider` with custom logarithmic mapping -- Position 0.0 → 0.001×, position 1.0 → 100.0× -- Special "MaxSpeed" position at the far right -- Calls `clock.set_speed_multiplier()` on change -- Display: "Speed: 1.00× (actual: 0.98×)" - -#### Mode Selector -- Three radio buttons: RealTime / MaxSpeed / SingleStep -- Calls `clock.set_mode()` on selection change - -#### Step Controls -- "Step" button, enabled only in SingleStep mode -- Calls `clock.trigger_step()` on click -- Shows "Step pending..." indicator when step is pending - -#### Virtual Time Display -- Human-readable format: auto-selects µs, ms, or s -- Shows tick count -- Shows target vs actual speed - -### 3. Integration (`src/gui/mod.rs`) - -Add `timing_controls: TimingControls` to `GuiComponents` and call `render_timing_controls()` between the control panel and memory sections. - -### 4. Console Commands (`src/gui/console.rs`) - -New commands in `execute_command()`: - -| Command | Description | -|---------|-------------| -| `speed ` | Set speed multiplier (e.g., `speed 0.5`) | -| `speed` | Show current speed | -| `mode ` | Set simulation mode | -| `mode` | Show current mode | -| `step` | Trigger a single step (SingleStep mode) | -| `time` | Show virtual time and tick count | - -Also add keyboard shortcuts: -- `+`/`-` already exist for frequency; add new shortcuts for speed: - - Keep existing `+`/`-` for frequency control (unchanged) - -### Logarithmic Slider Mapping - -The slider position `t` (0.0 to 1.0) maps to speed multiplier via: -``` -t=0.00 → 0.001× -t=0.25 → 0.01× -t=0.50 → 0.1× -t=0.75 → 1.0× -t=0.90 → 10.0× -t=1.00 → 100.0× -``` - -Formula: `speed = 10^(t * 5 - 3)` gives range [0.001, 100.0] - -For MaxSpeed mode, we'll use a separate toggle rather than a slider position. - -## Implementation Order - -1. Add `TimingState` to `src/gui/state.rs` -2. Add `TimingControls` component to `src/gui/components.rs` -3. Integrate into `src/gui/mod.rs` -4. Add console commands to `src/gui/console.rs` -5. Build and test diff --git a/plans/phase9_tests.md b/plans/phase9_tests.md deleted file mode 100644 index c5c73b0..0000000 --- a/plans/phase9_tests.md +++ /dev/null @@ -1,47 +0,0 @@ -# Phase 9: Update Tests for Virtual Time - -## Current State - -All 226 existing tests pass (142 unit tests, 16 fibonacci tests, 62 integration tests, 5 simple tests, 15 doc-tests). No tests are broken by the virtual time migration — the existing tests use `update()` (without virtual time) and direct component manipulation, which still works. - -## Plan - -### 1. Update `tests/lib.rs` — Add Virtual Time Test Utilities - -Add helper functions to the `test_utils` module: -- `create_maxspeed_clock()` — Creates a VirtualClock in MaxSpeed mode with a 333ns tick size -- `create_singlestep_clock()` — Creates a VirtualClock in SingleStep mode -- `spawn_governor()` — Spawns a RealTimeGovernor thread for a given clock -- `stop_and_wait()` — Stops the clock and waits for governor thread to finish - -### 2. New Test File: `tests/virtual_time_tests.rs` - -#### MaxSpeed Mode Tests -- `test_maxspeed_clock_advances` — Verify VirtualClock advances in MaxSpeed mode with governor -- `test_maxspeed_component_updates` — Verify Intel4001 updates correctly at virtual time -- `test_maxspeed_rom_access_timing` — Verify ROM data is valid after access_time elapsed in virtual time - -#### SingleStep Mode Tests -- `test_singlestep_advances_one_tick` — Verify each trigger_step() advances exactly one tick -- `test_singlestep_rom_state_at_specific_time` — Verify ROM state at specific virtual times: - - Before access_time: data not ready - - After access_time: data valid - -#### Overclocking Tests -- `test_overclocking_rom_fails_at_2mhz` — Set 2 MHz clock (250ns phase), verify ROM (500ns access) fails -- `test_overclocking_ram_fails_at_2mhz` — Same for RAM -- `test_shift_register_works_at_higher_freq` — Intel4003 (200ns access) works at 2 MHz (250ns phase >= 200ns) -- `test_component_timing_check_access_time` — Direct ComponentTiming API tests - -#### Virtual Clock Integration Tests -- `test_configurable_system_creates_virtual_clock` — Verify ConfigurableSystem has a VirtualClock -- `test_configurable_system_maxspeed_execution` — Run system in MaxSpeed mode briefly, verify it works -- `test_virtual_clock_stop_exits_threads` — Verify clock.stop() causes governor to exit -- `test_pause_resume_behavior` — Verify pause stops time advancement, resume continues - -### 3. Implementation Notes - -- All new tests go in a separate file to avoid modifying existing passing tests -- Tests use `Arc` and `RealTimeGovernor` directly (not through ConfigurableSystem) for precise control -- Overclocking tests use `ComponentTiming::check_access_time()` directly since component-internal timing checks happen via `update_at_time()` which requires the full component thread infrastructure -- Tests that spawn governor threads must ensure proper cleanup (stop + join) diff --git a/plans/timing_architecture.md b/plans/timing_architecture.md deleted file mode 100644 index df1ded7..0000000 --- a/plans/timing_architecture.md +++ /dev/null @@ -1,603 +0,0 @@ -# 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, - 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` 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 diff --git a/plans/vic20_architecture.md b/plans/vic20_architecture.md new file mode 100644 index 0000000..d9b3839 --- /dev/null +++ b/plans/vic20_architecture.md @@ -0,0 +1,1010 @@ +# VIC-20 Architecture Plan + +## Overview and Goals + +This document describes the architecture for adding Commodore VIC-20 support to the Rusty Emulator. The VIC-20 is a significantly more complex system than the Intel MCS-4, featuring a 16-bit address bus, memory-mapped I/O, a video display generator, and multiple support chips. This plan covers the full system from 6502 CPU implementation through to a bootable VIC-20 with video output. + +### Goals + +1. **Reusable 6502 CPU family** — A trait-based hierarchy supporting MOS 6502, WDC 65C02, and future variants with shared instruction decoding +2. **Generic memory components** — Reusable SRAM/ROM chips usable across any 8-bit system +3. **Accurate VIC chip** — Video, sound, and I/O from the 6560/6561 with framebuffer output +4. **6522 VIA** — Full implementation of timers, I/O ports, and shift register +5. **Bus architecture** — Memory-mapped address decoding that replaces the MCS-4 pin-level bus +6. **System integration** — JSON config, GUI display widget, keyboard input + +### Existing Infrastructure + +The emulator already provides: + +- [`Component`](src/component.rs:18) trait with `update()`, `update_at_time()`, `timing()`, virtual clock support +- [`BaseComponent`](src/component.rs:142) for common pin/state management +- [`VirtualClock`](src/virtual_clock.rs) with RealTime/MaxSpeed/SingleStep modes +- [`ConfigurableSystem`](src/system_config.rs) with JSON-based system factory and thread management +- [`ConnectionManager`](src/connection.rs:7) for bidirectional pin connections +- [`ComponentTiming`](src/component_timing.rs) for per-component timing constraints +- 6502 stubs: [`MOS6502`](src/components/cpu/mos_6502.rs:12) and [`WDC65C02`](src/components/cpu/wdc_65c02.rs:11) with skeleton structure + +--- + +## System Architecture + +### High-Level Block Diagram + +``` + +----------------------------------------------------------+ + | VIC-20 System Bus | + | 16-bit Address / 8-bit Data | + +----------------------------------------------------------+ + | | | | | + +----+ +-----+ +-----+ +-----+ +-----+---------+ + | | | | | | + +----+---+ +--+--+ +---+--+ +---+--+ +---+--+ +------------+ + | MOS | | 6560| | 6522 | | 6522 | | SRAM | | Expansion | + | 6502 | | VIC | | VIA 1| | VIA 2| | 5KB | | Port | + | CPU | | | | | | | | | | | + +--------+ +--+--+ +--+---+ +--+---+ +------+ +------------+ + | | | | + +----+---+ +-+------+ +-+------+ + | Video | |Keyboard| |Cassette| + | Output | | Scan | |Serial | + +--------+ +--------+ +--------+ +``` + +### VIC-20 Memory Map + +``` + $0000 ────────────┐ + │ Zero Page, Stack + $0100 │ (CPU internal use) + │ + $0200 ────────────┤ + │ 1KB SRAM (main RAM) + $0400 │ (screen character memory) + │ + $1000 ────────────┤ + │ 4KB Expansion RAM/ROM + $2000 │ (cartridge area) + │ + $2000 ────────────┤ + │ 8KB Expansion ROM + $4000 │ (cartridge area) + │ + $4000 ────────────┤ + │ 8KB Expansion ROM + $6000 │ (cartridge area) + │ + $6000 ────────────┤ + │ 8KB Expansion ROM + $8000 │ (cartridge area) + │ + $8000 ────────────┤ + │ 8KB Character ROM + $A000 │ (visible to VIC chip) + │ + $8000 ────────────┤ + │ 8KB BASIC ROM + $A000 │ + │ + $A000 ────────────┤ + │ I/O Block + │ $9000-$93FF: VIC + │ $9400-$97FF: VIA 1 + │ $9800-$9BFF: VIA 2 + │ $9C00-$9FFF: Color RAM + I/O + $A000 │ + │ + $C000 ────────────┤ + │ 4KB RAM (expansion) + $D000 │ + │ + $E000 ────────────┤ + │ 8KB Kernel ROM + $FFFC │ Reset vector: $FCE2 + $FFFF ────────────┘ +``` + +### Component Communication Model + +```mermaid +graph TD + CPU[MOS 6502 CPU] + BUS[Address Decoder / Bus] + VIC[6560 VIC Chip] + VIA1[6522 VIA 1] + VIA2[6522 VIA 2] + SRAM[2114 SRAM x5] + ROM_B[2364 BASIC ROM] + ROM_K[2364 Kernel ROM] + ROM_C[2332 Character ROM] + EXP[Expansion Port] + FB[Framebuffer] + SND[Audio Output] + KBD[Keyboard Matrix] + + CPU -->|address + RW + data| BUS + BUS -->|chip select + address + data| SRAM + BUS -->|chip select + address + data| ROM_B + BUS -->|chip select + address + data| ROM_K + BUS -->|chip select + address + data| ROM_C + BUS -->|chip select + address + data| VIC + BUS -->|chip select + address + data| VIA1 + BUS -->|chip select + address + data| VIA2 + BUS -->|chip select + address + data| EXP + VIC -->|pixel data| FB + VIC -->|waveform| SND + VIA1 -->|row scan + column read| KBD +``` + +--- + +## Component-by-Component Design + +### 1. 6502 CPU Family Architecture + +#### 1.1 Trait Hierarchy + +The current [`MOS6502`](src/components/cpu/mos_6502.rs:12) is a monolithic struct. For the VIC-20 and future systems, we need a trait hierarchy that separates instruction decoding from CPU state and allows variant-specific behavior. + +```rust +/// Core behavior that all 6502-family CPUs share +pub trait Cpu6502Family: Component { + /// Read a byte from the bus at the given address + fn bus_read(&mut self, addr: u16) -> u8; + /// Write a byte to the bus at the given address + fn bus_write(&mut self, addr: u8, data: u8); + /// Execute one machine cycle. Returns number of cycles consumed. + fn step(&mut self) -> u8; + /// Signal an IRQ (maskable interrupt) + fn signal_irq(&mut self); + /// Signal an NMI (non-maskable interrupt) + fn signal_nmi(&mut self); + /// Assert the reset line + fn signal_reset(&mut self); + /// Get the program counter + fn pc(&self) -> u16; + /// Get all registers for debugging + fn registers(&self) -> Cpu6502Registers; +} + +/// Variant-specific behavior for instruction differences +pub trait Cpu6502Variant { + /// Execute an illegal/undocumented opcode + fn execute_undefined(&self, opcode: u8, cpu: &mut dyn Cpu6502Family) -> u8; + /// Whether this variant has the JMP indirect bug + fn has_jmp_indirect_bug(&self) -> bool; + /// Additional opcodes specific to this variant + fn decode_extra(&self, opcode: u8) -> Option; +} +``` + +#### 1.2 Instruction Decoder + +The 6502 has 151 official opcodes (256 entries in the decode table). The decoder should be a shared module: + +``` +src/components/cpu/mos_6502/ +├── mod.rs — MOS6502 struct, Component impl +├── decoder.rs — Opcode decode table and instruction enum +├── addressing.rs — Addressing mode handlers +├── instructions.rs — Instruction execution (ALU ops, branches, stack) +├── interrupts.rs — IRQ, NMI, RESET handling +└── state.rs — CPU state machine (fetch-decode-execute) +``` + +#### 1.3 Key Differences from MCS-4 + +| Aspect | MCS-4 (4004) | 6502 Family | +|--------|-------------|-------------| +| Address bus | 4-bit multiplexed | 16-bit parallel | +| Data bus | 4-bit | 8-bit | +| I/O model | Separate I/O pins | Memory-mapped I/O | +| Clock phases | 2-phase external | Single-phase, internal | +| Interrupts | None | IRQ, NMI, RESET | +| Instruction cycle | Fixed timing | Variable (1-7 cycles) | + +#### 1.4 Bus Interface + +The 6502 communicates with the rest of the system via a **bus trait** rather than individual pin connections. This is fundamentally different from the MCS-4 model: + +```rust +/// Trait for a memory-mapped bus that the CPU can read/write +pub trait AddressBus { + /// Read a byte from the given address + fn read(&self, addr: u16) -> u8; + /// Write a byte to the given address + fn write(&mut self, addr: u16, data: u8); + /// Check if a device is asserting the bus (for DMA detection) + fn is_bus_asserted(&self) -> bool; +} +``` + +The CPU holds a reference to an `AddressBus` implementor. The bus routes reads/writes to the correct component based on address decoding logic. This replaces the pin-per-bit approach used for the MCS-4's 4-bit bus. + +#### 1.5 WDC 65C02 Differences + +The [`WDC65C02`](src/components/cpu/wdc_65c02.rs:11) currently wraps [`MOS6502`](src/components/cpu/mos_6502.rs:12) directly. In the new architecture, both should implement `Cpu6502Family` and share the same decoder. The 65C02 adds: + +- New instructions: `STZ`, `BRA`, `PHX/PHY`, `PLX/PLY`, `TRB/TSB` +- New addressing modes: indirect without indexing, relative for `BRA` +- Fixes: JMP indirect page-boundary bug, no illegal opcodes +- Wait/Stop modes: `WAI` and `STP` instructions + +--- + +### 2. Memory Chips (SRAM/ROM) + +#### 2.1 Generic Memory Component + +Create reusable memory components in a new module structure: + +``` +src/components/memory/ +├── mod.rs — Re-exports +├── intel_4001.rs — (existing) +├── intel_4002.rs — (existing) +├── intel_4003.rs — (existing) +└── generic/ + ├── mod.rs — Re-exports + ├── sram.rs — Generic SRAM + ├── rom.rs — Generic ROM + └── sram_2114.rs — 2114-specific 1Kx4-bit variant +``` + +#### 2.2 Generic SRAM + +```rust +/// Generic static RAM with configurable size and bus width +pub struct GenericSram { + base: BaseComponent, + data: Vec, + size: usize, // Size in bytes + data_width: u8, // 4 or 8 bits + address_mask: u16, + // Bus interface + bus: Option>>, +} +``` + +#### 2.3 2114 SRAM (1K×4-bit) + +The VIC-20 uses five 2114 chips. Each chip provides 1K×4 bits. Two chips are paired to form 1K×8 bits. The 2114 has: + +- 10 address inputs (A0-A9) +- 4 data I/O lines (I/O1-I/O4) +- Chip Select (CS) +- Write Enable (WE) + +For emulation purposes, we can model pairs of 2114s as a single 1K×8 block since the VIC-20 always pairs them. + +#### 2.4 Generic ROM + +```rust +/// Generic ROM with configurable size +pub struct GenericRom { + base: BaseComponent, + data: Vec, + size: usize, + address_mask: u16, + // Pin interface for chip select + loaded: bool, +} +``` + +ROM files should be loadable from binary files via the JSON config `properties.rom_file` field. + +#### 2.5 VIC-20 ROM Set + +| ROM | Chip | Size | Address Range | File | +|-----|------|------|---------------|------| +| BASIC | 2364 | 8KB | $8000-$9FFF | basic.bin | +| Kernel | 2364 | 8KB | $E000-$FFFF | kernel.bin | +| Character | 2332 | 4KB | $8000-$8FFF (VIC view) | chargen.bin | + +--- + +### 3. 6560/6561 VIC Chip + +The VIC chip is the most complex component. It generates video, sound, handles DRAM refresh, and reads joysticks. + +#### 3.1 Module Structure + +``` +src/components/video/ +├── mod.rs — Re-exports +├── mos_6560.rs — VIC chip implementation +├── framebuffer.rs — Framebuffer abstraction +└── character_rom.rs — Character generator +``` + +#### 3.2 VIC Registers (16 registers, $9000-$9013) + +| Register | Address | Function | +|----------|---------|----------| +| $9000 | Interlace / Horizontal Center | 4 bits | +| $9001 | Horizontal Center | 8 bits | +| $9002 | Vertical Center / Video Mode | 8 bits | +| $9003 | Number of Columns | 7 bits | +| $9004 | Raster Line (read-only) | 8 bits | +| $9005 | Screen Memory / Character Memory | 8 bits | +| $9006 | Light Pen X | 8 bits | +| $9007 | Light Pen Y | 8 bits | +| $9008 | Paddle X | 8 bits | +| $9009 | Paddle Y | 8 bits | +| $900A | Sound / Bass Frequency | 8 bits | +| $900B | Sound / Alto Frequency | 8 bits | +| $900C | Sound / Soprano Frequency | 8 bits | +| $900D | Sound / Noise Frequency | 8 bits | +| $900E | Sound / Volume / Auxiliary Color | 8 bits | +| $900F | Background Color / Inverted Mode | 8 bits | + +#### 3.3 Video Generation + +The VIC generates a 22-column × 23-row character display. Each character is 8×8 pixels. The output resolution is approximately 176×184 pixels (NTSC) or 176×200 pixels (PAL). + +```rust +/// Framebuffer for VIC video output +pub struct Framebuffer { + /// Pixel data: 176 x 184 (NTSC) or 176 x 200 (PAL) + pixels: Vec, // Index into color palette + width: usize, + height: usize, + /// 16-color VIC palette (RGB) + palette: [(u8, u8, u8); 16], +} +``` + +The VIC reads character data from screen memory (default $1E00-$1FFF) and character bitmaps from character ROM. For each scanline, it: + +1. Reads the character code from screen memory +2. Reads the 8-byte bitmap from character ROM +3. Applies foreground/background colors +4. Outputs one scanline of pixels to the framebuffer + +The framebuffer is shared with the GUI via `Arc>`. + +#### 3.4 Sound Generation + +The VIC has 4 sound channels: + +- **Bass**: 8-bit frequency divisor +- **Alto**: 8-bit frequency divisor +- **Soprano**: 8-bit frequency divisor +- **Noise**: 8-bit frequency divisor + +Each channel generates a square wave at the frequency determined by a clock divisor. The output is mixed and controlled by the 4-bit volume register. + +For initial implementation, sound can be stubbed with the register interface. Audio output integration with the GUI can come in a later phase. + +#### 3.5 Timing + +The VIC runs on the CPU clock (~1.02 MHz NTSC, ~1.11 MHz PAL). It generates one scanline per 71 CPU cycles (NTSC). A full frame is 262 scanlines (NTSC). The VIC steals cycles from the CPU for DRAM refresh and character fetches. + +#### 3.6 Component Interface + +```rust +pub struct Mos6560 { + base: BaseComponent, + registers: [u8; 16], + framebuffer: Arc>, + screen_memory: Arc>, + char_memory: Arc>, + // Timing + cycle_counter: u64, + raster_line: u8, + // Sound state + sound_channels: [SoundChannel; 4], + volume: u8, + variant: VicVariant, // NTSC (6560) or PAL (6561) +} + +enum VicVariant { + Ntsc, // 6560 - ~1.02 MHz + Pal, // 6561 - ~1.11 MHz +} +``` + +--- + +### 4. 6522 VIA (Versatile Interface Adapter) + +The VIC-20 has two 6522 VIAs. VIA 1 handles keyboard scanning and cassette. VIA 2 handles the serial bus (IEEE-488), user port, and joystick. + +#### 4.1 Module Structure + +``` +src/components/io/ +├── mod.rs — Re-exports +├── mos_6522.rs — VIA implementation +└── keyboard.rs — VIC-20 keyboard matrix +``` + +#### 4.2 VIA Registers (16 registers) + +| Register | Name | Function | +|----------|------|----------| +| $0 | ORB/IRB | Port B Data | +| $1 | ORA/IRA | Port A Data | +| $2 | DDRB | Port B Direction | +| $3 | DDRA | Port A Direction | +| $4 | T1CL | Timer 1 Counter Low | +| $5 | T1CH | Timer 1 Counter High | +| $6 | T1LL | Timer 1 Latch Low | +| $7 | T1LH | Timer 1 Latch High | +| $8 | T2CL | Timer 2 Counter Low | +| $9 | T2CH | Timer 2 Counter High | +| $A | SR | Shift Register | +| $B | ACR | Auxiliary Control Register | +| $C | PCR | Peripheral Control Register | +| $D | IFR | Interrupt Flag Register | +| $E | IER | Interrupt Enable Register | +| $F | ORA/IRA | Port A (no handshake) | + +#### 4.3 Key Features + +- **Port A and Port B**: 8-bit bidirectional I/O with programmable direction (DDRA/DDRB) +- **Timer 1**: 16-bit with free-run or one-shot mode, generates interrupts +- **Timer 2**: 16-bit one-shot or pulse counting mode +- **Shift Register**: Serial I/O under timer control +- **Handshake Lines**: CA1, CA2, CB1, CB2 for control signals +- **IRQ Output**: Can interrupt the CPU on timer expiry or port events + +#### 4.4 VIA 1 — Keyboard and Cassette + +- Port A: Keyboard column output (write) +- Port B: Keyboard row input (read) +- CB1: Cassette read +- CB2: Cassette write +- Timer 1: Cassette timing + +#### 4.5 VIA 2 — Serial Bus and User Port + +- Port A: Serial bus data (SRQ, ATN, CLK, DATA) +- Port B: User port +- CA1: Serial bus SRQ in +- CB2: Serial bus data out + +#### 4.6 Keyboard Matrix + +The VIC-20 keyboard is an 8×8 matrix scanned by VIA 1: + +``` + PA0 PA1 PA2 PA3 PA4 PA5 PA6 PA7 + PB0 1 Q C SP 3 E F R + PB1 2 W S Z 4 D G T + PB2 = + ; L . : @ O + PB3 3 - , P / M * ? + PB4 5 0 N > 2 9 B I + PB5 6 ^ V < 1 8 X U + PB6 7 _ H [ ` ; C ] + PB7 HOME DEL / RET ↑ ← CLR → +``` + +The keyboard module maps host keyboard events to matrix row/column assertions. + +--- + +### 5. Address Decoding and Bus Architecture + +#### 5.1 The Bus Component + +Unlike the MCS-4 where every pin is individually connected, the VIC-20 uses a shared bus with address decoding. A new `Bus` component routes CPU reads/writes to the correct chip: + +``` +src/bus/ +├── mod.rs — Re-exports +├── address_bus.rs — AddressBus trait +├── address_decoder.rs — VIC-20 specific decoder +└── system_bus.rs — Shared bus implementation +``` + +#### 5.2 Address Decoder + +```rust +/// Routes CPU bus cycles to the correct component +pub struct Vic20AddressDecoder { + /// Addressable devices mapped to address ranges + devices: Vec, + /// Open bus value (returned when no device responds) + open_bus: u8, +} + +struct AddressRange { + start: u16, + end: u16, + mask: u16, // Address bits to pass to device + device: Arc>, +} + +pub trait BusDevice: Send + Sync { + /// Read from this device at the given offset + fn read(&self, offset: u16) -> u8; + /// Write to this device at the given offset + fn write(&mut self, offset: u16, data: u8); + /// Get the device name + fn device_name(&self) -> String; +} +``` + +#### 5.3 BusDevice Trait + +The `BusDevice` trait is simpler than the full `Component` trait. Components that sit on the bus (VIC, VIA, SRAM, ROM) implement both `Component` (for the threading model) and `BusDevice` (for bus transactions). + +```rust +/// A component that can respond to bus reads/writes +pub trait BusDevice: Send + Sync { + fn read(&self, offset: u16) -> u8; + fn write(&mut self, offset: u16, data: u8); + fn device_name(&self) -> String; +} +``` + +#### 5.4 VIC-20 Address Map Implementation + +```rust +impl Vic20AddressDecoder { + pub fn new() -> Self { + // Build the address map: + // $0000-$00FF: Zero Page (SRAM) + // $0100-$01FF: Stack (SRAM) + // $0200-$03FF: System variables + screen (SRAM) + // $1000-$1FFF: Main SRAM (screen memory lives here) + // $2000-$3FFF: Expansion block 1 + // $4000-$5FFF: Expansion block 2 + // $6000-$7FFF: Expansion block 3 + // $8000-$8FFF: Character ROM (VIC view) + // $8000-$9FFF: BASIC ROM (CPU view) + // $9000-$93FF: VIC registers + // $9400-$97FF: VIA 1 registers + // $9800-$9BFF: VIA 2 registers + // $9C00-$9FFF: Color RAM + I/O + // $C000-$CFFF: Expansion RAM + // $E000-$FFFF: Kernel ROM + } +} +``` + +#### 5.5 Relationship to Existing Pin Model + +The bus architecture coexists with the existing pin model. The CPU uses the `AddressBus` trait for memory-mapped access, but control signals (IRQ, NMI, RESET, RDY) still use pins. This hybrid approach allows: + +- Fast bus transactions (no per-bit pin manipulation) +- Accurate control signal timing via pins +- DMA devices can assert the bus via the `is_bus_asserted()` method + +--- + +### 6. Expansion Port + +#### 6.1 Design + +The expansion port exposes the full CPU bus plus chip select decoding: + +```rust +/// Models the VIC-20 cartridge expansion port +pub struct ExpansionPort { + base: BaseComponent, + // Connected ROM/RAM devices + blocks: [Option>>; 4], // Blocks 1-4 + // I/O mapping + io_device: Option>>, + // DMA state + dma_active: bool, + bus_requested: bool, +} +``` + +#### 6.2 Cartridge Model + +A cartridge is a JSON config that maps ROM/RAM into the expansion blocks: + +```json +{ + "name": "VIC-20 Cartridge", + "blocks": [ + { "address": "$2000", "size": 8192, "rom_file": "cart_a.bin" }, + { "address": "$6000", "size": 8192, "rom_file": "cart_b.bin" } + ] +} +``` + +Initial implementation will focus on static ROM cartridges. Bank switching and DMA can be added later. + +--- + +### 7. System Configuration + +#### 7.1 JSON Config Format + +The existing [`SystemConfig`](src/system_config.rs:150) format needs extensions for bus-based systems: + +```json +{ + "name": "VIC20", + "description": "Commodore VIC-20", + "version": "1.0", + "metadata": { + "architecture": "vic20", + "cpu_speed": 1022700, + "region": "NTSC" + }, + "components": { + "CPU": { + "single": { + "component_type": "mos_6502", + "name": "MOS6502", + "properties": { "clock_speed": 1022700 } + } + }, + "VIC": { + "single": { + "component_type": "mos_6560", + "name": "VIC", + "properties": { "variant": "NTSC" } + } + }, + "VIA1": { + "single": { + "component_type": "mos_6522", + "name": "VIA1", + "properties": { "role": "keyboard_cassette" } + } + }, + "VIA2": { + "single": { + "component_type": "mos_6522", + "name": "VIA2", + "properties": { "role": "serial_userport" } + } + }, + "BASIC_ROM": { + "single": { + "component_type": "rom_2364", + "name": "BASIC_ROM", + "properties": { + "size": 8192, + "rom_file": "roms/vic20/basic.bin", + "address_base": "0x8000" + } + } + }, + "KERNEL_ROM": { + "single": { + "component_type": "rom_2364", + "name": "KERNEL_ROM", + "properties": { + "size": 8192, + "rom_file": "roms/vic20/kernel.bin", + "address_base": "0xE000" + } + } + }, + "CHAR_ROM": { + "single": { + "component_type": "rom_2332", + "name": "CHAR_ROM", + "properties": { + "size": 4096, + "rom_file": "roms/vic20/chargen.bin", + "address_base": "0x8000" + } + } + }, + "SRAM": { + "single": { + "component_type": "sram_5kb", + "name": "MAIN_SRAM", + "properties": { + "size": 5120, + "address_base": "0x0000" + } + } + } + }, + "bus": { + "type": "memory_mapped", + "decoder": "vic20", + "devices": [ + { "component": "MAIN_SRAM", "ranges": ["0x0000-0x03FF", "0x1000-0x1FFF"] }, + { "component": "VIC", "ranges": ["0x9000-0x93FF"] }, + { "component": "VIA1", "ranges": ["0x9400-0x97FF"] }, + { "component": "VIA2", "ranges": ["0x9800-0x9BFF"] }, + { "component": "BASIC_ROM", "ranges": ["0x8000-0x9FFF"] }, + { "component": "KERNEL_ROM", "ranges": ["0xE000-0xFFFF"] }, + { "component": "CHAR_ROM", "ranges": ["0x8000-0x8FFF"], "bus_master": "VIC" } + ] + }, + "connections": { + "cpu_irq": { + "connection_type": "interrupt", + "sources": [ + {"component": "VIC", "pin": "IRQ"}, + {"component": "VIA1", "pin": "IRQ"}, + {"component": "VIA2", "pin": "IRQ"} + ], + "target": {"component": "CPU", "pin": "IRQ"} + }, + "cpu_nmi": { + "connection_type": "interrupt", + "source": {"component": "VIA2", "pin": "IRQ"}, + "target": {"component": "CPU", "pin": "NMI"} + } + } +} +``` + +#### 7.2 ConfigurableSystem Adaptations + +The existing [`ConfigurableSystem`](src/system_config.rs) needs these changes: + +1. **Bus support**: Add a `bus` field to `SystemConfig` for memory-mapped bus configuration +2. **Bus wiring**: After creating components, wire them to the shared bus +3. **CPU injection**: Give the CPU an `Arc` reference to the bus for read/write operations +4. **Component registry**: Register new component types (mos_6502, mos_6560, mos_6522, sram, rom) +5. **ROM loading**: Support loading binary ROM files from the `properties.rom_file` path + +--- + +### 8. GUI Adaptations + +#### 8.1 New GUI Components + +``` +src/gui/ +├── mod.rs — (existing) +├── components.rs — (existing) MCS-4 widgets +├── console.rs — (existing) +├── state.rs — (existing) +├── display.rs — NEW: VIC video display widget +├── keyboard.rs — NEW: Keyboard input mapper +└── system_selector.rs — NEW: System type selection +``` + +#### 8.2 Display Widget + +The display widget renders the VIC framebuffer to an egui texture: + +```rust +/// Displays the VIC-20 video output +pub struct VicDisplay { + framebuffer: Option>>, + texture: Option, + scale: f32, // Display scaling factor +} +``` + +The widget polls the framebuffer each frame and uploads it as an egui texture. NTSC output is 176×184 pixels, so it needs scaling to be visible. + +#### 8.3 Keyboard Mapping + +Map host keyboard events to VIC-20 keyboard matrix positions: + +```rust +/// Maps host keyboard input to VIC-20 keyboard matrix +pub struct VicKeyboardMapper { + /// Current state of the 8x8 matrix + matrix: [[bool; 8]; 8], + /// Host key -> VIC matrix position mapping + key_map: HashMap, +} +``` + +#### 8.4 System Selector + +The GUI needs a way to select which system to load at startup. A simple dropdown or dialog that lists available JSON configs: + +- `configs/mcs4_basic.json` — Intel MCS-4 +- `configs/mcs4_max.json` — Intel MCS-4 (full) +- `configs/vic20.json` — Commodore VIC-20 + +The [`GuiApp`](src/gui/mod.rs:22) struct would load the appropriate config based on selection. + +#### 8.5 System-Specific Control Panels + +Different systems need different GUI layouts: + +- **MCS-4**: Current layout (4-bit registers, nibble memory view) +- **VIC-20**: 8-bit registers, 16-bit address space, video display, keyboard status + +The GUI should detect the system architecture from the config metadata and render the appropriate panel. + +--- + +## File Structure + +### New Files to Create + +``` +src/ +├── bus/ +│ ├── mod.rs — Bus module re-exports +│ ├── address_bus.rs — AddressBus trait + BusDevice trait +│ ├── address_decoder.rs — Generic address decoder +│ └── system_bus.rs — Shared bus with R/W routing +│ +├── components/ +│ ├── cpu/ +│ │ ├── mos_6502.rs — REWRITE: Full 6502 with instruction decoder +│ │ ├── mos_6502/ — NEW: 6502 instruction decoder modules +│ │ │ ├── mod.rs — Sub-module re-exports +│ │ │ ├── decoder.rs — Opcode decode table +│ │ │ ├── addressing.rs — Addressing mode handlers +│ │ │ ├── instructions.rs — Instruction execution +│ │ │ ├── interrupts.rs — IRQ/NMI/RESET handling +│ │ │ └── state.rs — CPU state machine +│ │ └── wdc_65c02.rs — REWRITE: 65C02 using shared decoder +│ │ +│ ├── memory/ +│ │ ├── generic/ — NEW: Generic memory components +│ │ │ ├── mod.rs +│ │ │ ├── sram.rs — Generic SRAM +│ │ │ ├── rom.rs — Generic ROM with file loading +│ │ │ └── sram_2114.rs — 2114 1Kx4-bit SRAM +│ │ └── vic20_sram.rs — NEW: VIC-20 5KB SRAM block +│ │ +│ ├── video/ +│ │ ├── mod.rs — NEW: Video module re-exports +│ │ ├── mos_6560.rs — NEW: VIC chip +│ │ ├── framebuffer.rs — NEW: Framebuffer abstraction +│ │ └── character_rom.rs — NEW: Character generator +│ │ +│ └── io/ +│ ├── mod.rs — NEW: I/O module re-exports +│ ├── mos_6522.rs — NEW: VIA implementation +│ └── keyboard.rs — NEW: VIC-20 keyboard matrix +│ +├── gui/ +│ ├── display.rs — NEW: Video display widget +│ ├── keyboard_input.rs — NEW: Keyboard input mapper +│ └── system_selector.rs — NEW: System type selector +│ +configs/ +└── vic20.json — NEW: VIC-20 system configuration + +roms/ +└── vic20/ — NEW: ROM directory (not in git) + ├── basic.bin — 8KB BASIC ROM + ├── kernel.bin — 8KB Kernel ROM + └── chargen.bin — 4KB Character ROM +``` + +### Files to Modify + +| File | Change | +|------|--------| +| [`src/lib.rs`](src/lib.rs) | Add `bus` module | +| [`src/components/cpu/mod.rs`](src/components/cpu/mod.rs) | Add mos_6502 sub-module | +| [`src/components/memory/mod.rs`](src/components/memory/mod.rs) | Add generic and vic20_sram modules | +| [`src/components/mod.rs`](src/components/mod.rs) | Add video and io modules | +| [`src/system_config.rs`](src/system_config.rs) | Add bus config, new component types, ROM loading | +| [`src/gui/mod.rs`](src/gui/mod.rs) | Add display, keyboard_input, system_selector modules | +| [`src/gui/components.rs`](src/gui/components.rs) | Add system-specific rendering | +| [`Cargo.toml`](Cargo.toml) | No new dependencies expected initially | + +--- + +## Implementation Phases + +### Phase 1: Bus Infrastructure and 6502 CPU + +**Deliverable**: A 6502 CPU that can execute instructions from a ROM via a memory-mapped bus. + +**Steps**: +1. Create `src/bus/` module with `AddressBus` trait, `BusDevice` trait, and `AddressDecoder` +2. Create generic SRAM and ROM components in `src/components/memory/generic/` +3. Rewrite [`mos_6502.rs`](src/components/cpu/mos_6502.rs) with full instruction decoder: + - All 151 official opcodes + - All addressing modes (Immediate, Zero Page, Zero Page X/Y, Absolute, Absolute X/Y, Indirect, Indirect X, Indirect Y, Relative, Implied, Accumulator) + - Variable cycle counts per instruction + - IRQ/NMI/RESET interrupt handling +4. Wire CPU to bus (CPU holds `Arc`, calls `bus.read()`/`bus.write()`) +5. Unit tests: All 6502 instructions tested against known behavior +6. Integration test: Load a small 6502 machine code program, run it, verify register/memory state + +**Test milestone**: A 6502 test program that computes Fibonacci numbers in RAM, verifiable via unit test. + +### Phase 2: Basic VIC-20 Boot + +**Deliverable**: A VIC-20 system that boots from ROM and enters the BASIC prompt loop (no display yet). + +**Steps**: +1. Create VIC-20 address decoder in `src/bus/address_decoder.rs` +2. Create VIC-20 5KB SRAM block +3. Add ROM loading from binary files to the generic ROM component +4. Create `configs/vic20.json` with full system definition +5. Register VIC-20 components in [`SystemFactory`](src/system_config.rs:205) +6. Create stub VIA components (registers readable/writable but no I/O functionality) +7. Create stub VIC component (registers readable/writable but no video generation) +8. Integration test: Load VIC-20 ROMs, boot CPU, verify it reaches the BASIC warm start loop + +**Test milestone**: CPU boots, reads reset vector from $FFFC/$FFFD, begins executing Kernel ROM code. + +### Phase 3: VIC Video Output + +**Deliverable**: Visible video output from the VIC chip rendered in the GUI. + +**Steps**: +1. Implement VIC character mode rendering in [`mos_6560.rs`](src/components/video/mos_6560.rs) +2. Create framebuffer abstraction in `framebuffer.rs` +3. Create character ROM reader +4. Implement VIC register read/write via `BusDevice` trait +5. Create GUI display widget in `src/gui/display.rs` +6. Wire framebuffer to GUI via `Arc>` +7. Implement VIC timing (scanline counting, raster register) +8. Integration test: Boot VIC-20, verify framebuffer contains expected startup screen + +**Test milestone**: The VIC-20 startup screen renders correctly in the framebuffer (cursor blinking on empty blue screen). + +### Phase 4: VIA and Keyboard Input + +**Deliverable**: Functional keyboard input — user can type on the host keyboard and see characters appear on the VIC-20 screen. + +**Steps**: +1. Implement full 6522 VIA with Port A, Port B, DDR registers +2. Implement VIA Timer 1 and Timer 2 with interrupt generation +3. Create keyboard matrix module in `src/components/io/keyboard.rs` +4. Create keyboard input mapper in `src/gui/keyboard_input.rs` +5. Wire VIA 1 Port A/B to keyboard matrix +6. Wire VIA IRQ to CPU IRQ pin +7. Integration test: Simulate keypress, verify character appears in screen memory + +**Test milestone**: Type `PRINT "HELLO"` on the host keyboard, see it echoed on the VIC-20 display. + +### Phase 5: Sound and Polish + +**Deliverable**: Sound output and a polished multi-system GUI. + +**Steps**: +1. Implement VIC sound generation (3 square waves + noise) +2. Add audio output to the GUI (via `cpal` or `rodio` crate — evaluate which is better) +3. Create system selector in GUI (`src/gui/system_selector.rs`) +4. Add VIC-20-specific control panel (register viewer, memory dump with 16-bit addresses) +5. Refine display widget (scanline effects, proper aspect ratio, NTSC/PAL switching) +6. Implement VIA shift register and handshake lines +7. Add DRAM refresh timing to VIC chip +8. Performance optimization (bus caching, framebuffer dirty tracking) + +**Test milestone**: Full VIC-20 with sound, keyboard, and video running at real-time speed. + +### Phase 6: Expansion Port + +**Deliverable**: Support for cartridge ROM images. + +**Steps**: +1. Create expansion port component +2. Add cartridge ROM loading to system config +3. Support 4 expansion blocks ($2000, $4000, $6000, $8000) +4. Test with a known cartridge ROM image +5. Add cartridge slot to GUI (drag-and-drop ROM loading) + +**Test milestone**: Load a cartridge ROM, verify it appears in the correct address range and the CPU can execute from it. + +--- + +## Risks and Open Questions + +### Risks + +1. **6502 Instruction Accuracy**: The 6502 has many undocumented opcodes and edge cases (e.g., the JMP indirect page-boundary bug). Getting every cycle-accurate behavior right is complex. **Mitigation**: Start with official opcodes only, add undocumented opcodes later. Use the well-known "Nestest" ROM for validation. + +2. **VIC Timing Complexity**: The VIC chip steals CPU cycles for memory access, creating a complex timing interplay. **Mitigation**: Initially run the VIC and CPU at independent timing; add cycle-stealing later as an optimization. + +3. **Performance**: The VIC-20 generates video at ~60 FPS with per-scanline rendering. This must integrate smoothly with the existing VirtualClock system. **Mitigation**: Render scanlines in batches, use dirty tracking to avoid re-rendering unchanged frames. + +4. **Bus Architecture Coexistence**: The new bus-based architecture is fundamentally different from the MCS-4 pin-based model. Both must coexist in the same codebase. **Mitigation**: The bus is a new module alongside the existing pin system. Components choose which model to use. + +5. **ROM Copyright**: The VIC-20 system ROMs are still under copyright. **Mitigation**: Do not distribute ROMs. Users must provide their own ROM files. Document the expected ROM file format and MD5 checksums. + +### Open Questions + +1. **Audio Library**: Should we use `cpal`, `rodio`, or another crate for audio output? This decision can be deferred to Phase 5. + +2. **Cycle Accuracy Level**: Should we aim for instruction-level accuracy (simpler) or cycle-level accuracy (more faithful)? The existing MCS-4 uses cycle-level timing. Recommendation: Start with instruction-level for the 6502, add cycle-level later. + +3. **65C02 Priority**: The WDC 65C02 is used in some systems but not the VIC-20. Should we implement it fully in Phase 1, or stub it and focus on the NMOS 6502? Recommendation: Focus on NMOS 6502 first; the trait hierarchy supports adding 65C02 later. + +4. **PAL vs NTSC**: The VIC-20 has different timing for PAL (6561) and NTSC (6560) variants. Should we support both from the start? Recommendation: Start with NTSC, add PAL as a configuration option. + +5. **Save States**: Should we plan for save state support from the beginning? Recommendation: Not in the initial implementation, but the `BusDevice` trait makes it straightforward to add later (serialize all device state). + +6. **Debugger Integration**: Should the 6502 get a disassembler and breakpoint system? Recommendation: Plan for it in the GUI but implement after the basic system works. The register viewer in the existing GUI is a starting point.