diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cee0ec4..01c1d4e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: Ronald CI on: push: - branches: [$default-branch] + branches: [master] pull_request: - branches: [$default-branch] + branches: [master] env: CARGO_TERM_COLOR: always @@ -16,7 +16,17 @@ jobs: steps: - uses: actions/checkout@v4 - name: Check formatting - run: cargo fmt --verbose + run: cargo fmt --verbose --all --check + - name: Install dependencies + run: sudo apt install -y libasound2-dev + - name: Cache + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}-ronald - name: Build run: cargo build --verbose - name: Run tests diff --git a/.gitignore b/.gitignore index bd83f18..2f0a066 100644 --- a/.gitignore +++ b/.gitignore @@ -12,5 +12,8 @@ keymap.backup.json /ronald-egui/assets/keys/examples /ronald-egui/dist +/ronald-egui/tests/snapshots/*.old.png +/ronald-egui/tests/snapshots/*.new.png +/ronald-egui/tests/snapshots/*.diff.png CLAUDE.md diff --git a/Cargo.lock b/Cargo.lock index 6df649c..96f06c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -234,6 +234,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + [[package]] name = "anstream" version = "0.6.19" @@ -756,6 +762,12 @@ dependencies = [ "wayland-client", ] +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "cc" version = "1.0.104" @@ -794,6 +806,58 @@ dependencies = [ "libc", ] +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clap" +version = "4.5.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + [[package]] name = "clipboard-win" version = "5.4.0" @@ -810,7 +874,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" dependencies = [ "termcolor", - "unicode-width", + "unicode-width 0.1.13", ] [[package]] @@ -819,6 +883,16 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "colored" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" +dependencies = [ + "lazy_static", + "windows-sys 0.52.0", +] + [[package]] name = "combine" version = "4.6.7" @@ -956,12 +1030,70 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "criterion" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1c047a62b0cc3e145fa84415a3191f628e980b194c2755aa12300a4e6cbd928" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "itertools", + "num-traits", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b1bcc0dc7dfae599d84ad0b1a55f80cde8af3725da8313b528da95ef783e338" +dependencies = [ + "cast", + "itertools", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-common" version = "0.1.6" @@ -1048,6 +1180,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "dify" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11217d469eafa3b809ad84651eb9797ccbb440b4a916d5d85cb1b994e89787f6" +dependencies = [ + "anyhow", + "colored", + "getopts", + "image", + "rayon", +] + [[package]] name = "digest" version = "0.10.7" @@ -1258,10 +1403,22 @@ name = "egui_kittest" version = "0.31.1" source = "git+https://github.com/mdm/egui.git?branch=latest-patched#a1a40880f466a72c043b6e40178a1331b7e05310" dependencies = [ + "dify", + "eframe", "egui", + "egui-wgpu", + "image", "kittest 0.1.0", + "pollster", + "wgpu", ] +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "emath" version = "0.31.1" @@ -1598,6 +1755,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "getopts" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" +dependencies = [ + "unicode-width 0.2.2", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -1766,6 +1932,16 @@ dependencies = [ "bitflags 2.9.1", ] +[[package]] +name = "half" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +dependencies = [ + "cfg-if", + "crunchy", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -2067,6 +2243,15 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.11" @@ -2192,6 +2377,12 @@ dependencies = [ "arrayvec", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.172" @@ -2473,18 +2664,19 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.2" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02339744ee7253741199f897151b38e72257d13802d4ee837285cc2990a90845" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" dependencies = [ "num_enum_derive", + "rustversion", ] [[package]] name = "num_enum_derive" -version = "0.7.2" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" dependencies = [ "proc-macro-crate 3.1.0", "proc-macro2", @@ -2831,6 +3023,12 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + [[package]] name = "orbclient" version = "0.3.48" @@ -3008,6 +3206,34 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + [[package]] name = "png" version = "0.17.16" @@ -3232,6 +3458,26 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "rctree" version = "0.5.0" @@ -3389,9 +3635,11 @@ dependencies = [ name = "ronald-core" version = "0.2.0" dependencies = [ + "criterion", "deku", "log", "nom", + "num_enum", "psg", "serde", "serde_json", @@ -3922,6 +4170,16 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "tokio" version = "1.47.1" @@ -4086,6 +4344,12 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "unicode-xid" version = "0.2.6" diff --git a/Cargo.toml b/Cargo.toml index cb0a58d..f837cf0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,3 +2,6 @@ resolver = "2" members = ["ronald-core", "ronald-egui"] default-members = ["ronald-egui"] + +[profile.dev] +opt-level = 3 diff --git a/ronald-core/Cargo.toml b/ronald-core/Cargo.toml index 2e5ff3d..75ecb1b 100644 --- a/ronald-core/Cargo.toml +++ b/ronald-core/Cargo.toml @@ -11,6 +11,14 @@ edition = "2021" deku = "0.16.0" log = "0.4.17" nom = "7.1.1" +num_enum = { version = "0.7.5", features = ["complex-expressions"] } psg = "1.0.1" serde = { version = "1.0.188", features = ["derive"] } serde_json = "1.0.105" + +[dev-dependencies] +criterion = { version = "0.7.0", features = ["html_reports"] } + +[[bench]] +name = "debug_event_throughput" +harness = false diff --git a/ronald-core/benches/debug_event_throughput.rs b/ronald-core/benches/debug_event_throughput.rs new file mode 100644 index 0000000..7d8e456 --- /dev/null +++ b/ronald-core/benches/debug_event_throughput.rs @@ -0,0 +1,64 @@ +use ronald_core::{ + debug::{emit_event, event::CpuDebugEvent, DebugSource, EventSubscription}, + system::{clock::MasterClockTick, cpu::Register16}, +}; + +fn debug_event_throughput(total_events: u64, batch_size: u64, multiple_subs: bool) { + let mut subscriptions = if multiple_subs { + vec![ + EventSubscription::new(DebugSource::Any), + EventSubscription::new(DebugSource::Any), + ] + } else { + vec![EventSubscription::new(DebugSource::Any)] + }; + + let mut count = 0; + while count < total_events { + for _ in 0..batch_size { + let source = DebugSource::Cpu; + let event = CpuDebugEvent::Register16Written { + register: Register16::PC, + is: 0x1000, + was: 0x0000, + } + .into(); + let master_clock = MasterClockTick::default(); + + emit_event(source, event, master_clock); + } + + subscriptions[0].with_events(|_record| {}); + + count += batch_size; + } + + if multiple_subs { + subscriptions[1].with_events(|_record| {}); + } +} + +fn criterion_benchmark(c: &mut criterion::Criterion) { + let mut group = c.benchmark_group("debug_event_throughput"); + + for &total_events in &[1_000, 10_000] { + for &batch_size in &[10, 100, 1_000] { + for &multiple_subscribers in &[false, true] { + let id = format!( + "events_{}_batch_{}_multi_{}", + total_events, batch_size, multiple_subscribers + ); + group.bench_function(&id, |b| { + b.iter(|| { + debug_event_throughput(total_events, batch_size, multiple_subscribers) + }) + }); + } + } + } + + group.finish(); +} + +criterion::criterion_group!(benches, criterion_benchmark); +criterion::criterion_main!(benches); diff --git a/ronald-core/src/constants.rs b/ronald-core/src/constants.rs index 5bdb3cd..aeeba3e 100644 --- a/ronald-core/src/constants.rs +++ b/ronald-core/src/constants.rs @@ -650,3 +650,71 @@ pub const KEYS: [(&str, KeyDefinition); 80] = [ pub const SCREEN_BUFFER_WIDTH: usize = 48 * 16; pub const SCREEN_BUFFER_HEIGHT: usize = 35 * 16; + +// CPC hardware palette colors (RGBA format) +#[allow(clippy::identity_op, clippy::eq_op)] +pub const FIRMWARE_COLORS: [[u8; 4]; 27] = [ + [0x00, 0x00, 0x00, 0xff], // 0 + [0x00, 0x00, 0x80, 0xff], // 1 + [0x00, 0x00, 0xff, 0xff], // 2 + [0x80, 0x00, 0x00, 0xff], // 3 + [0x80, 0x00, 0x80, 0xff], // 4 + [0x80, 0x00, 0xff, 0xff], // 5 + [0xff, 0x00, 0x00, 0xff], // 6 + [0xff, 0x00, 0x80, 0xff], // 7 + [0xff, 0x00, 0xff, 0xff], // 8 + [0x00, 0x80, 0x00, 0xff], // 9 + [0x00, 0x80, 0x80, 0xff], // 10 + [0x00, 0x80, 0xff, 0xff], // 11 + [0x80, 0x80, 0x00, 0xff], // 12 + [0x80, 0x80, 0x80, 0xff], // 13 + [0x80, 0x80, 0xff, 0xff], // 14 + [0xff, 0x80, 0x00, 0xff], // 15 + [0xff, 0x80, 0x80, 0xff], // 16 + [0xff, 0x80, 0xff, 0xff], // 17 + [0x00, 0xff, 0x00, 0xff], // 18 + [0x00, 0xff, 0x80, 0xff], // 19 + [0x00, 0xff, 0xff, 0xff], // 20 + [0x80, 0xff, 0x00, 0xff], // 21 + [0x80, 0xff, 0x80, 0xff], // 22 + [0x80, 0xff, 0xff, 0xff], // 23 + [0xff, 0xff, 0x00, 0xff], // 24 + [0xff, 0xff, 0x80, 0xff], // 25 + [0xff, 0xff, 0xff, 0xff], // 26 +]; + +// Hardware to firmware color mapping +pub const HARDWARE_TO_FIRMWARE_COLORS: [usize; 32] = [ + 13, // 0 (0x40) + 13, // 1 (0x41) + 19, // 2 (0x42) + 25, // 3 (0x43) + 1, // 4 (0x44) + 7, // 5 (0x45) + 10, // 6 (0x46) + 16, // 7 (0x47) + 7, // 8 (0x48) + 25, // 9 (0x49) + 24, // 10 (0x4a) + 26, // 11 (0x4b) + 6, // 12 (0x4c) + 8, // 13 (0x4d) + 15, // 14 (0x4e) + 17, // 15 (0x4f) + 1, // 16 (0x50) + 19, // 17 (0x51) + 18, // 18 (0x52) + 20, // 19 (0x53) + 0, // 20 (0x54) + 2, // 21 (0x55) + 9, // 22 (0x56) + 11, // 23 (0x57) + 4, // 24 (0x58) + 22, // 25 (0x59) + 21, // 26 (0x5a) + 23, // 27 (0x5b) + 3, // 28 (0x5c) + 5, // 29 (0x5d) + 12, // 30 (0x5e) + 14, // 31 (0x5f) +]; diff --git a/ronald-core/src/debug.rs b/ronald-core/src/debug.rs new file mode 100644 index 0000000..8e86d33 --- /dev/null +++ b/ronald-core/src/debug.rs @@ -0,0 +1,661 @@ +use std::{cell::RefCell, collections::HashMap}; + +use event::DebugEvent; + +use crate::system::clock::MasterClockTick; + +pub mod breakpoint; +pub mod event; +pub mod view; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct EventSequence(u64); + +impl EventSequence { + fn next(self) -> Self { + Self(self.0 + 1) + } +} + +#[derive(Debug, Clone)] +pub struct EventRecord { + sequence: EventSequence, + pub source: DebugSource, + pub event: DebugEvent, + pub master_clock: MasterClockTick, +} + +struct EventLog { + enabled: bool, + events: Vec, + first_sequence: EventSequence, + next_sequence: EventSequence, +} + +impl EventLog { + fn new() -> Self { + Self { + enabled: true, + events: Vec::new(), + first_sequence: EventSequence(0), + next_sequence: EventSequence(0), + } + } + + fn append(&mut self, source: DebugSource, event: DebugEvent, master_clock: MasterClockTick) { + if !self.enabled { + return; + } + + debug_assert!( + self.events.len() < 100_000, + "Debug event log is full ({} events, last={:?}). Are all subscriptions consuming events?", + self.events.len(), + self.events.last() + ); + + let sequence = self.next_sequence; + self.next_sequence = self.next_sequence.next(); + self.events.push(EventRecord { + sequence, + source, + event, + master_clock, + }); + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +struct SubscriptionId(usize); + +#[derive(Debug, Clone)] +pub struct EventSubscription { + id: SubscriptionId, + source: DebugSource, + first_unconsumed: EventSequence, +} + +impl EventSubscription { + pub fn new(source: DebugSource) -> Self { + let first_unconsumed = DEBUG_EVENT_LOG.with(|log| log.borrow().next_sequence); + let id = DEBUG_SUBSCRIPTION_REGISTRY + .with(|registry| registry.borrow_mut().subcribe(first_unconsumed)); + + Self { + id, + source, + first_unconsumed, + } + } + + pub fn with_events(&mut self, mut callback: F) + where + F: FnMut(&EventRecord), + { + DEBUG_EVENT_LOG.with(|log| { + let log = log.borrow(); + let first_unconsumed = self.first_unconsumed.0 - log.first_sequence.0; + + for record in &log.events[first_unconsumed as usize..] { + self.first_unconsumed = record.sequence.next(); + if self.source == DebugSource::Any || self.source == record.source { + callback(record); + } + } + }); + + DEBUG_SUBSCRIPTION_REGISTRY.with(|registry| { + registry + .borrow_mut() + .consume_events(self.id, self.first_unconsumed); + }); + } + + pub fn has_pending(&self) -> bool { + DEBUG_EVENT_LOG.with(|log| log.borrow().next_sequence > self.first_unconsumed) + } + + pub fn pending_count(&self) -> u64 { + if !self.has_pending() { + return 0; + } + + DEBUG_EVENT_LOG.with(|log| { + let log = log.borrow(); + let mut count = 0; + + for record in &log.events { + if record.sequence >= self.first_unconsumed + && (self.source == DebugSource::Any || self.source == record.source) + { + count += 1; + } + } + + count + }) + } +} + +impl Drop for EventSubscription { + fn drop(&mut self) { + DEBUG_SUBSCRIPTION_REGISTRY.with(|registry| { + registry.borrow_mut().unsubscribe(self.id); + }); + } +} + +struct SubscriptionRegistry { + active_subscriptions: HashMap, + next_id: SubscriptionId, +} + +impl SubscriptionRegistry { + fn new() -> Self { + Self { + active_subscriptions: HashMap::new(), + next_id: SubscriptionId(0), + } + } + + fn subcribe(&mut self, current_sequence: EventSequence) -> SubscriptionId { + let id = self.next_id; + self.next_id.0 += 1; + + self.active_subscriptions.insert(id, current_sequence); + id + } + + fn unsubscribe(&mut self, id: SubscriptionId) { + self.active_subscriptions.remove(&id); + } + + fn consume_events(&mut self, id: SubscriptionId, first_unconsumed: EventSequence) { + self.active_subscriptions.insert(id, first_unconsumed); + + let min_first_unconsumed = self + .active_subscriptions + .values() + .copied() + .min() + .unwrap_or(EventSequence(0)); + + if min_first_unconsumed < first_unconsumed { + // There is a subscription that has consumed less than the caller. No cleanup needed. + return; + } + + DEBUG_EVENT_LOG.with(|log| { + let mut log = log.borrow_mut(); + + let retain_from = min_first_unconsumed.0 - log.first_sequence.0; + log.events.drain(0..retain_from as usize); + log.first_sequence = min_first_unconsumed; + }); + } +} + +thread_local! { + static DEBUG_EVENT_LOG: RefCell = RefCell::new(EventLog::new()); + static DEBUG_SUBSCRIPTION_REGISTRY: RefCell = + RefCell::new(SubscriptionRegistry::new()); +} + +pub fn emit_event(source: DebugSource, event: DebugEvent, master_clock: MasterClockTick) { + DEBUG_EVENT_LOG.with(|log| log.borrow_mut().append(source, event, master_clock)); +} + +pub fn record_debug_events(enabled: bool) { + DEBUG_EVENT_LOG.with(|log| log.borrow_mut().enabled = enabled); +} + +pub trait Snapshottable { + type View; + + fn debug_view(&self) -> Self::View; +} + +pub trait Debuggable: Snapshottable { + const SOURCE: DebugSource; + type Event: Into; + + fn emit_debug_event(&self, event: Self::Event, master_clock: MasterClockTick) { + emit_event(Self::SOURCE, event.into(), master_clock); + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum DebugSource { + Any, + Cpu, + Memory, + Crtc, + GateArray, + Fdc, + Ppi, + Psg, + Tape, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_subscribe_single_subscription() { + let mut subscription = EventSubscription::new(DebugSource::Cpu); + + emit_event( + DebugSource::Cpu, + DebugEvent::Cpu(event::CpuDebugEvent::Register8Written { + register: crate::system::cpu::Register8::A, + is: 0x42, + was: 0x00, + }), + MasterClockTick::default(), + ); + + assert_eq!(subscription.pending_count(), 1); + subscription.with_events(|record| { + assert!(matches!(record.event, DebugEvent::Cpu(_))); + }); + } + + #[test] + fn test_subscribe_multiple_subscriptions_same_source() { + let subscription1 = EventSubscription::new(DebugSource::Memory); + let subscription2 = EventSubscription::new(DebugSource::Memory); + + emit_event( + DebugSource::Memory, + DebugEvent::Memory(event::MemoryDebugEvent::MemoryRead { + address: 0x1000, + value: 0x42, + }), + MasterClockTick::default(), + ); + + assert_eq!(subscription1.pending_count(), 1); + assert_eq!(subscription2.pending_count(), 1); + } + + #[test] + fn test_subscribe_multiple_sources() { + let mut cpu_subscription = EventSubscription::new(DebugSource::Cpu); + let mut memory_subscription = EventSubscription::new(DebugSource::Memory); + + emit_event( + DebugSource::Cpu, + DebugEvent::Cpu(event::CpuDebugEvent::Register8Written { + register: crate::system::cpu::Register8::A, + is: 0x42, + was: 0x00, + }), + MasterClockTick::default(), + ); + emit_event( + DebugSource::Memory, + DebugEvent::Memory(event::MemoryDebugEvent::MemoryRead { + address: 0x1000, + value: 0x42, + }), + MasterClockTick::default(), + ); + + assert_eq!(cpu_subscription.pending_count(), 1); + cpu_subscription.with_events(|record| { + assert!(matches!(record.event, DebugEvent::Cpu(_))); + }); + + assert_eq!(memory_subscription.pending_count(), 1); + memory_subscription.with_events(|record| { + assert!(matches!(record.event, DebugEvent::Memory(_))); + }); + } + + #[test] + fn test_any_receives_all_sources() { + let subscription = EventSubscription::new(DebugSource::Any); + + emit_event( + DebugSource::Cpu, + DebugEvent::Cpu(event::CpuDebugEvent::Register8Written { + register: crate::system::cpu::Register8::A, + is: 0x42, + was: 0x00, + }), + MasterClockTick::default(), + ); + emit_event( + DebugSource::Memory, + DebugEvent::Memory(event::MemoryDebugEvent::MemoryRead { + address: 0x1000, + value: 0x42, + }), + MasterClockTick::default(), + ); + + assert_eq!(subscription.pending_count(), 2); + } + + #[test] + fn test_has_pending_events() { + let mut subscription = EventSubscription::new(DebugSource::Cpu); + assert!(!subscription.has_pending()); + + emit_event( + DebugSource::Cpu, + DebugEvent::Cpu(event::CpuDebugEvent::Register8Written { + register: crate::system::cpu::Register8::A, + is: 0x42, + was: 0x00, + }), + MasterClockTick::default(), + ); + + assert!(subscription.has_pending()); + + subscription.with_events(|_record| {}); + assert!(!subscription.has_pending()); + } + + #[test] + fn test_cleanup_old_events() { + let mut subscription1 = EventSubscription::new(DebugSource::Cpu); + let mut subscription2 = EventSubscription::new(DebugSource::Cpu); + + // Emit some events + for i in 0..10 { + emit_event( + DebugSource::Cpu, + DebugEvent::Cpu(event::CpuDebugEvent::Register8Written { + register: crate::system::cpu::Register8::A, + is: i, + was: 0x00, + }), + MasterClockTick::default(), + ); + } + + // First subscription consumes all events + subscription1.with_events(|_record| {}); + + // Cleanup should not remove events that subscription2 hasn't consumed + assert!(!subscription1.has_pending()); + assert_eq!(subscription2.pending_count(), 10); + + // Second subscription consumes all events + subscription2.with_events(|_record| {}); + + // Now cleanup should remove events since both have consumed them + assert!(!subscription1.has_pending()); + assert!(!subscription2.has_pending()); + } + + #[test] + fn test_emit_with_no_subscriptions() { + emit_event( + DebugSource::Cpu, + DebugEvent::Cpu(event::CpuDebugEvent::Register8Written { + register: crate::system::cpu::Register8::A, + is: 0x42, + was: 0x00, + }), + MasterClockTick::default(), + ); // Should not panic + } + + #[test] + fn test_event_sequence_ordering() { + let seq1 = EventSequence(1); + let seq2 = EventSequence(2); + let seq3 = seq1.next(); + + assert!(seq1 < seq2); + assert_eq!(seq3.0, 2); + assert_eq!(seq3, seq2); + } + + #[test] + fn test_sequence_boundary_subscription_created_before_events() { + let subscription = EventSubscription::new(DebugSource::Cpu); + + emit_event( + DebugSource::Cpu, + DebugEvent::Cpu(event::CpuDebugEvent::Register8Written { + register: crate::system::cpu::Register8::A, + is: 0x42, + was: 0x00, + }), + MasterClockTick::default(), + ); + + assert_eq!(subscription.pending_count(), 1); + assert!(subscription.has_pending()); + } + + #[test] + fn test_sequence_boundary_subscription_created_after_events() { + emit_event( + DebugSource::Cpu, + DebugEvent::Cpu(event::CpuDebugEvent::Register8Written { + register: crate::system::cpu::Register8::A, + is: 0x42, + was: 0x00, + }), + MasterClockTick::default(), + ); + + let subscription = EventSubscription::new(DebugSource::Cpu); + + assert_eq!(subscription.pending_count(), 0); + assert!(!subscription.has_pending()); + } + + #[test] + fn test_sequence_boundary_exact_consumption() { + let mut subscription = EventSubscription::new(DebugSource::Cpu); + + emit_event( + DebugSource::Cpu, + DebugEvent::Cpu(event::CpuDebugEvent::Register8Written { + register: crate::system::cpu::Register8::A, + is: 0x01, + was: 0x00, + }), + MasterClockTick::default(), + ); + emit_event( + DebugSource::Cpu, + DebugEvent::Cpu(event::CpuDebugEvent::Register8Written { + register: crate::system::cpu::Register8::A, + is: 0x02, + was: 0x01, + }), + MasterClockTick::default(), + ); + + assert_eq!(subscription.pending_count(), 2); + + let mut consumed_count = 0; + subscription.with_events(|_record| { + consumed_count += 1; + }); + + assert_eq!(consumed_count, 2); + assert_eq!(subscription.pending_count(), 0); + assert!(!subscription.has_pending()); + } + + #[test] + fn test_sequence_boundary_multiple_consumption_calls() { + let mut subscription = EventSubscription::new(DebugSource::Cpu); + + for i in 0..5 { + emit_event( + DebugSource::Cpu, + DebugEvent::Cpu(event::CpuDebugEvent::Register8Written { + register: crate::system::cpu::Register8::A, + is: i, + was: 0x00, + }), + MasterClockTick::default(), + ); + } + + assert_eq!(subscription.pending_count(), 5); + + let mut first_batch_count = 0; + subscription.with_events(|_record| { + first_batch_count += 1; + }); + + assert_eq!(first_batch_count, 5); + assert_eq!(subscription.pending_count(), 0); + assert!(!subscription.has_pending()); + + for i in 5..8 { + emit_event( + DebugSource::Cpu, + DebugEvent::Cpu(event::CpuDebugEvent::Register8Written { + register: crate::system::cpu::Register8::A, + is: i, + was: 0x00, + }), + MasterClockTick::default(), + ); + } + + assert_eq!(subscription.pending_count(), 3); + + let mut second_batch_count = 0; + subscription.with_events(|_record| { + second_batch_count += 1; + }); + + assert_eq!(second_batch_count, 3); + assert_eq!(subscription.pending_count(), 0); + } + + #[test] + fn test_sequence_boundary_interleaved_subscriptions() { + let mut subscription1 = EventSubscription::new(DebugSource::Cpu); + + emit_event( + DebugSource::Cpu, + DebugEvent::Cpu(event::CpuDebugEvent::Register8Written { + register: crate::system::cpu::Register8::A, + is: 0x01, + was: 0x00, + }), + MasterClockTick::default(), + ); + + let mut subscription2 = EventSubscription::new(DebugSource::Cpu); + + emit_event( + DebugSource::Cpu, + DebugEvent::Cpu(event::CpuDebugEvent::Register8Written { + register: crate::system::cpu::Register8::A, + is: 0x02, + was: 0x01, + }), + MasterClockTick::default(), + ); + + assert_eq!(subscription1.pending_count(), 2); + assert_eq!(subscription2.pending_count(), 1); + + subscription1.with_events(|_record| {}); + + assert_eq!(subscription1.pending_count(), 0); + assert_eq!(subscription2.pending_count(), 1); + + subscription2.with_events(|_record| {}); + + assert_eq!(subscription1.pending_count(), 0); + assert_eq!(subscription2.pending_count(), 0); + } + + #[test] + fn test_sequence_boundary_event_cleanup_precision() { + let mut subscription1 = EventSubscription::new(DebugSource::Cpu); + let mut subscription2 = EventSubscription::new(DebugSource::Cpu); + + for i in 0..10 { + emit_event( + DebugSource::Cpu, + DebugEvent::Cpu(event::CpuDebugEvent::Register8Written { + register: crate::system::cpu::Register8::A, + is: i, + was: 0x00, + }), + MasterClockTick::default(), + ); + } + + let mut subscription1_events = Vec::new(); + subscription1.with_events(|record| { + subscription1_events.push(record.sequence); + }); + + assert_eq!(subscription1_events.len(), 10); + assert_eq!(subscription2.pending_count(), 10); + + let mut subscription2_events = Vec::new(); + subscription2.with_events(|record| { + subscription2_events.push(record.sequence); + }); + + assert_eq!(subscription2_events.len(), 10); + assert_eq!(subscription1_events, subscription2_events); + } + + #[test] + fn test_sequence_boundary_first_event_zero() { + DEBUG_EVENT_LOG.with(|log| { + let initial_sequence = log.borrow().next_sequence; + assert_eq!(initial_sequence.0, 0); + }); + + emit_event( + DebugSource::Cpu, + DebugEvent::Cpu(event::CpuDebugEvent::Register8Written { + register: crate::system::cpu::Register8::A, + is: 0x42, + was: 0x00, + }), + MasterClockTick::default(), + ); + + DEBUG_EVENT_LOG.with(|log| { + let log = log.borrow(); + assert_eq!(log.next_sequence.0, 1); + assert_eq!(log.events[0].sequence.0, 0); + }); + } + + #[test] + fn test_sequence_boundary_consecutive_events() { + for i in 0..5 { + emit_event( + DebugSource::Cpu, + DebugEvent::Cpu(event::CpuDebugEvent::Register8Written { + register: crate::system::cpu::Register8::A, + is: i, + was: 0x00, + }), + MasterClockTick::default(), + ); + } + + DEBUG_EVENT_LOG.with(|log| { + let log = log.borrow(); + assert_eq!(log.next_sequence.0, 5); + + for (idx, event) in log.events.iter().enumerate() { + assert_eq!(event.sequence.0, idx as u64); + } + }); + } +} diff --git a/ronald-core/src/debug/breakpoint.rs b/ronald-core/src/debug/breakpoint.rs new file mode 100644 index 0000000..d1507c9 --- /dev/null +++ b/ronald-core/src/debug/breakpoint.rs @@ -0,0 +1,1914 @@ +use std::collections::HashMap; +use std::fmt; + +use serde::de::value; + +use crate::debug::event::{CpuDebugEvent, CrtcDebugEvent, GateArrayDebugEvent, MemoryDebugEvent}; +use crate::debug::{DebugEvent, DebugSource, EventSubscription}; +use crate::system::bus::crtc::Register as CrtcRegister; +use crate::system::clock::MasterClockTick; +use crate::system::cpu::{Register16, Register8}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct BreakpointId(usize); + +pub trait Breakpoint { + fn should_break(&mut self, source: DebugSource, event: &DebugEvent) -> bool; + fn enabled(&self) -> bool; + fn set_enabled(&mut self, enabled: bool); + fn one_shot(&self) -> bool; + fn set_one_shot(&mut self, one_shot: bool); + fn triggered(&self) -> Option; + fn set_triggered(&mut self, triggered: Option); +} + +#[derive(Debug, Clone)] +pub struct CpuRegister8Breakpoint { + pub register: Register8, + pub value: Option, + enabled: bool, + one_shot: bool, + triggered: Option, +} + +impl CpuRegister8Breakpoint { + pub fn new(register: Register8, value: Option) -> Self { + Self { + register, + value, + enabled: true, + one_shot: false, + triggered: None, + } + } +} + +impl Breakpoint for CpuRegister8Breakpoint { + fn should_break(&mut self, source: DebugSource, event: &DebugEvent) -> bool { + if !self.enabled || source != DebugSource::Cpu { + return false; + } + + match event { + DebugEvent::Cpu(CpuDebugEvent::Register8Written { register, is, .. }) => { + *register == self.register && self.value.is_none_or(|v| v == *is) + } + _ => false, + } + } + + fn enabled(&self) -> bool { + self.enabled + } + + fn set_enabled(&mut self, enabled: bool) { + self.enabled = enabled; + } + + fn one_shot(&self) -> bool { + self.one_shot + } + + fn set_one_shot(&mut self, one_shot: bool) { + self.one_shot = one_shot; + } + + fn triggered(&self) -> Option { + self.triggered + } + + fn set_triggered(&mut self, triggered: Option) { + self.triggered = triggered; + } +} + +impl fmt::Display for CpuRegister8Breakpoint { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.value { + Some(val) => write!(f, "{:?} = 0x{:02X}", self.register, val), + None => write!(f, "{:?} written", self.register), + } + } +} + +#[derive(Debug, Clone)] +pub struct CpuRegister16Breakpoint { + pub register: Register16, + pub value: Option, + enabled: bool, + one_shot: bool, + triggered: Option, +} + +impl CpuRegister16Breakpoint { + pub fn new(register: Register16, value: Option) -> Self { + Self { + register, + value, + enabled: true, + one_shot: false, + triggered: None, + } + } + + pub fn pc_breakpoint(address: u16) -> Self { + Self::new(Register16::PC, Some(address)) + } + + pub fn step_into() -> Self { + let mut breakpoint = Self::new(Register16::PC, None); + breakpoint.set_one_shot(true); + + breakpoint + } +} + +impl Breakpoint for CpuRegister16Breakpoint { + fn should_break(&mut self, source: DebugSource, event: &DebugEvent) -> bool { + if !self.enabled || source != DebugSource::Cpu { + return false; + } + + match event { + DebugEvent::Cpu(CpuDebugEvent::Register16Written { register, is, .. }) => { + *register == self.register && self.value.is_none_or(|v| v == *is) + } + _ => false, + } + } + + fn enabled(&self) -> bool { + self.enabled + } + + fn set_enabled(&mut self, enabled: bool) { + self.enabled = enabled; + } + + fn one_shot(&self) -> bool { + self.one_shot + } + + fn set_one_shot(&mut self, one_shot: bool) { + self.one_shot = one_shot; + } + + fn triggered(&self) -> Option { + self.triggered + } + + fn set_triggered(&mut self, triggered: Option) { + self.triggered = triggered; + } +} + +impl fmt::Display for CpuRegister16Breakpoint { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.value { + Some(val) => write!(f, "{:?} = 0x{:04X}", self.register, val), + None => write!(f, "{:?} written", self.register), + } + } +} + +#[derive(Debug, Clone)] +pub struct MemoryBreakpoint { + pub address: u16, + pub on_read: bool, + pub on_write: bool, + pub value: Option, + enabled: bool, + one_shot: bool, + triggered: Option, +} + +impl MemoryBreakpoint { + pub fn new(address: u16, on_read: bool, on_write: bool, value: Option) -> Self { + Self { + address, + on_read, + on_write, + value, + enabled: true, + one_shot: false, + triggered: None, + } + } +} + +impl Breakpoint for MemoryBreakpoint { + fn should_break(&mut self, source: DebugSource, event: &DebugEvent) -> bool { + if !self.enabled || source != DebugSource::Memory { + return false; + } + + match event { + DebugEvent::Memory(MemoryDebugEvent::MemoryRead { address, value, .. }) => { + self.on_read + && *address == self.address as usize + && self.value.is_none_or(|v| v == *value) + } + DebugEvent::Memory(MemoryDebugEvent::MemoryWritten { address, is, .. }) => { + self.on_write + && *address == self.address as usize + && self.value.is_none_or(|v| v == *is) + } + _ => false, + } + } + + fn enabled(&self) -> bool { + self.enabled + } + + fn set_enabled(&mut self, enabled: bool) { + self.enabled = enabled; + } + + fn one_shot(&self) -> bool { + self.one_shot + } + + fn set_one_shot(&mut self, one_shot: bool) { + self.one_shot = one_shot; + } + + fn triggered(&self) -> Option { + self.triggered + } + + fn set_triggered(&mut self, triggered: Option) { + self.triggered = triggered; + } +} + +impl fmt::Display for MemoryBreakpoint { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let access = match (self.on_read, self.on_write) { + (true, true) => "access", + (true, false) => "read", + (false, true) => "write", + (false, false) => "never", + }; + + match self.value { + Some(val) => write!(f, "0x{:04X} {} = 0x{:02X}", self.address, access, val), + None => write!(f, "0x{:04X} {}", self.address, access), + } + } +} + +#[derive(Debug, Clone)] +pub struct CallStackBreakpoint { + depth: usize, + enabled: bool, + one_shot: bool, + triggered: Option, +} + +impl CallStackBreakpoint { + pub fn new(depth: usize) -> Self { + Self { + depth, + enabled: true, + one_shot: false, + triggered: None, + } + } +} + +impl Breakpoint for CallStackBreakpoint { + fn should_break(&mut self, source: DebugSource, event: &DebugEvent) -> bool { + if !self.enabled || source != DebugSource::Cpu { + return false; + } + + match event { + DebugEvent::Cpu(CpuDebugEvent::CallFetched { interrupt: _ }) => self.depth += 1, + DebugEvent::Cpu(CpuDebugEvent::ReturnFetched { interrupt: _ }) => self.depth -= 1, + DebugEvent::Cpu(CpuDebugEvent::Register16Written { register, .. }) + if *register == Register16::PC => + { + return self.depth == 0; + } + _ => {} + } + + false + } + + fn enabled(&self) -> bool { + self.enabled + } + + fn set_enabled(&mut self, enabled: bool) { + self.enabled = enabled; + } + + fn one_shot(&self) -> bool { + self.one_shot + } + + fn set_one_shot(&mut self, one_shot: bool) { + self.one_shot = one_shot; + } + + fn triggered(&self) -> Option { + self.triggered + } + + fn set_triggered(&mut self, triggered: Option) { + self.triggered = triggered; + } +} + +impl fmt::Display for CallStackBreakpoint { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Step out") + } +} + +#[derive(Debug, Clone)] +pub struct CpuShadowRegister16Breakpoint { + pub register: Register16, + pub value: Option, + enabled: bool, + one_shot: bool, + triggered: Option, +} + +impl CpuShadowRegister16Breakpoint { + pub fn new(register: Register16, value: Option) -> Self { + Self { + register, + value, + enabled: true, + one_shot: false, + triggered: None, + } + } +} + +impl Breakpoint for CpuShadowRegister16Breakpoint { + fn should_break(&mut self, source: DebugSource, event: &DebugEvent) -> bool { + if !self.enabled || source != DebugSource::Cpu { + return false; + } + + match event { + DebugEvent::Cpu(CpuDebugEvent::ShadowRegister16Written { register, is, .. }) => { + *register == self.register && self.value.is_none_or(|v| v == *is) + } + _ => false, + } + } + + fn enabled(&self) -> bool { + self.enabled + } + + fn set_enabled(&mut self, enabled: bool) { + self.enabled = enabled; + } + + fn one_shot(&self) -> bool { + self.one_shot + } + + fn set_one_shot(&mut self, one_shot: bool) { + self.one_shot = one_shot; + } + + fn triggered(&self) -> Option { + self.triggered + } + + fn set_triggered(&mut self, triggered: Option) { + self.triggered = triggered; + } +} + +impl fmt::Display for CpuShadowRegister16Breakpoint { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.value { + Some(val) => write!(f, "{:?}' = 0x{:04X}", self.register, val), + None => write!(f, "{:?}' written", self.register), + } + } +} + +#[derive(Debug, Clone)] +pub struct GateArrayScreenModeBreakpoint { + pub mode: Option, + pub applied: bool, + enabled: bool, + one_shot: bool, + triggered: Option, +} + +impl GateArrayScreenModeBreakpoint { + pub fn new(mode: Option, applied: bool) -> Self { + Self { + mode, + applied, + enabled: true, + one_shot: false, + triggered: None, + } + } +} + +impl Breakpoint for GateArrayScreenModeBreakpoint { + fn should_break(&mut self, source: DebugSource, event: &DebugEvent) -> bool { + if !self.enabled || source != DebugSource::GateArray { + return false; + } + + match event { + DebugEvent::GateArray(GateArrayDebugEvent::ScreenModeChanged { + is, applied, .. + }) => *applied == self.applied && self.mode.is_none_or(|m| m == *is), + _ => false, + } + } + + fn enabled(&self) -> bool { + self.enabled + } + + fn set_enabled(&mut self, enabled: bool) { + self.enabled = enabled; + } + + fn one_shot(&self) -> bool { + self.one_shot + } + + fn set_one_shot(&mut self, one_shot: bool) { + self.one_shot = one_shot; + } + + fn triggered(&self) -> Option { + self.triggered + } + + fn set_triggered(&mut self, triggered: Option) { + self.triggered = triggered; + } +} + +impl fmt::Display for GateArrayScreenModeBreakpoint { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let action = if self.applied { "applied" } else { "requested" }; + match self.mode { + Some(mode) => write!(f, "Screen mode {} = {}", action, mode), + None => write!(f, "Screen mode {}", action), + } + } +} + +#[derive(Debug, Clone)] +pub struct GateArrayPenColorBreakpoint { + pub pen: Option, + pub color: Option, + enabled: bool, + one_shot: bool, + triggered: Option, +} + +impl GateArrayPenColorBreakpoint { + pub fn new(pen: Option, color: Option) -> Self { + Self { + pen, + color, + enabled: true, + one_shot: false, + triggered: None, + } + } +} + +impl Breakpoint for GateArrayPenColorBreakpoint { + fn should_break(&mut self, source: DebugSource, event: &DebugEvent) -> bool { + if !self.enabled || source != DebugSource::GateArray { + return false; + } + + match event { + DebugEvent::GateArray(GateArrayDebugEvent::PenColorChanged { pen, is, .. }) => { + self.pen.is_none_or(|p| p == *pen) && self.color.is_none_or(|c| c == *is) + } + _ => false, + } + } + + fn enabled(&self) -> bool { + self.enabled + } + + fn set_enabled(&mut self, enabled: bool) { + self.enabled = enabled; + } + + fn one_shot(&self) -> bool { + self.one_shot + } + + fn set_one_shot(&mut self, one_shot: bool) { + self.one_shot = one_shot; + } + + fn triggered(&self) -> Option { + self.triggered + } + + fn set_triggered(&mut self, triggered: Option) { + self.triggered = triggered; + } +} + +impl fmt::Display for GateArrayPenColorBreakpoint { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match (self.pen, self.color) { + (Some(16), Some(color)) => write!(f, "Border = 0x{:02X}", color), + (Some(16), None) => write!(f, "Border changed"), + (Some(pen), Some(color)) => write!(f, "Pen {} = 0x{:02X}", pen, color), + (Some(pen), None) => write!(f, "Pen {} changed", pen), + (None, Some(color)) => write!(f, "Any pen = 0x{:02X}", color), + (None, None) => write!(f, "Any pen changed"), + } + } +} + +#[derive(Debug, Clone)] +pub struct GateArrayInterruptBreakpoint { + enabled: bool, + one_shot: bool, + triggered: Option, +} + +impl GateArrayInterruptBreakpoint { + pub fn new() -> Self { + Self { + enabled: true, + one_shot: false, + triggered: None, + } + } +} + +impl Breakpoint for GateArrayInterruptBreakpoint { + fn should_break(&mut self, source: DebugSource, event: &DebugEvent) -> bool { + if !self.enabled || source != DebugSource::GateArray { + return false; + } + + matches!( + event, + DebugEvent::GateArray(GateArrayDebugEvent::InterruptGenerated) + ) + } + + fn enabled(&self) -> bool { + self.enabled + } + + fn set_enabled(&mut self, enabled: bool) { + self.enabled = enabled; + } + + fn one_shot(&self) -> bool { + self.one_shot + } + + fn set_one_shot(&mut self, one_shot: bool) { + self.one_shot = one_shot; + } + + fn triggered(&self) -> Option { + self.triggered + } + + fn set_triggered(&mut self, triggered: Option) { + self.triggered = triggered; + } +} + +impl fmt::Display for GateArrayInterruptBreakpoint { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Interrupt generated") + } +} + +#[derive(Debug, Clone)] +pub struct CrtcRegisterWriteBreakpoint { + pub register: Option, + pub value: Option, + enabled: bool, + one_shot: bool, + triggered: Option, +} + +impl CrtcRegisterWriteBreakpoint { + pub fn new(register: Option, value: Option) -> Self { + Self { + register, + value, + enabled: true, + one_shot: false, + triggered: None, + } + } +} + +impl Breakpoint for CrtcRegisterWriteBreakpoint { + fn should_break(&mut self, source: DebugSource, event: &DebugEvent) -> bool { + if !self.enabled || source != DebugSource::Crtc { + return false; + } + + match event { + DebugEvent::Crtc(CrtcDebugEvent::RegisterWritten { register, is, .. }) => { + self.register.is_none_or(|r| r == *register) && self.value.is_none_or(|v| v == *is) + } + _ => false, + } + } + + fn enabled(&self) -> bool { + self.enabled + } + + fn set_enabled(&mut self, enabled: bool) { + self.enabled = enabled; + } + + fn one_shot(&self) -> bool { + self.one_shot + } + + fn set_one_shot(&mut self, one_shot: bool) { + self.one_shot = one_shot; + } + + fn triggered(&self) -> Option { + self.triggered + } + + fn set_triggered(&mut self, triggered: Option) { + self.triggered = triggered; + } +} + +impl fmt::Display for CrtcRegisterWriteBreakpoint { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match (self.register, self.value) { + (Some(reg), Some(val)) => write!(f, "{} = 0x{:02X}", reg, val), + (Some(reg), None) => write!(f, "{} written", reg), + (None, Some(val)) => write!(f, "Any register = 0x{:02X}", val), + (None, None) => write!(f, "Any register written"), + } + } +} + +#[derive(Debug, Clone)] +pub struct CrtcHorizontalSyncBreakpoint { + pub on_start: bool, + pub on_end: bool, + enabled: bool, + one_shot: bool, + triggered: Option, +} + +impl CrtcHorizontalSyncBreakpoint { + pub fn new(on_start: bool, on_end: bool) -> Self { + Self { + on_start, + on_end, + enabled: true, + one_shot: false, + triggered: None, + } + } +} + +impl Breakpoint for CrtcHorizontalSyncBreakpoint { + fn should_break(&mut self, source: DebugSource, event: &DebugEvent) -> bool { + if !self.enabled || source != DebugSource::Crtc { + return false; + } + + match event { + DebugEvent::Crtc(CrtcDebugEvent::HorizontalSync { enabled }) => { + (*enabled && self.on_start) || (!*enabled && self.on_end) + } + _ => false, + } + } + + fn enabled(&self) -> bool { + self.enabled + } + + fn set_enabled(&mut self, enabled: bool) { + self.enabled = enabled; + } + + fn one_shot(&self) -> bool { + self.one_shot + } + + fn set_one_shot(&mut self, one_shot: bool) { + self.one_shot = one_shot; + } + + fn triggered(&self) -> Option { + self.triggered + } + + fn set_triggered(&mut self, triggered: Option) { + self.triggered = triggered; + } +} + +impl fmt::Display for CrtcHorizontalSyncBreakpoint { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match (self.on_start, self.on_end) { + (true, true) => write!(f, "HSYNC start or end"), + (true, false) => write!(f, "HSYNC start"), + (false, true) => write!(f, "HSYNC end"), + (false, false) => write!(f, "HSYNC (never)"), + } + } +} + +#[derive(Debug, Clone)] +pub struct CrtcVerticalSyncBreakpoint { + pub on_start: bool, + pub on_end: bool, + enabled: bool, + one_shot: bool, + triggered: Option, +} + +impl CrtcVerticalSyncBreakpoint { + pub fn new(on_start: bool, on_end: bool) -> Self { + Self { + on_start, + on_end, + enabled: true, + one_shot: false, + triggered: None, + } + } +} + +impl Breakpoint for CrtcVerticalSyncBreakpoint { + fn should_break(&mut self, source: DebugSource, event: &DebugEvent) -> bool { + if !self.enabled || source != DebugSource::Crtc { + return false; + } + + match event { + DebugEvent::Crtc(CrtcDebugEvent::VerticalSync { enabled }) => { + (*enabled && self.on_start) || (!*enabled && self.on_end) + } + _ => false, + } + } + + fn enabled(&self) -> bool { + self.enabled + } + + fn set_enabled(&mut self, enabled: bool) { + self.enabled = enabled; + } + + fn one_shot(&self) -> bool { + self.one_shot + } + + fn set_one_shot(&mut self, one_shot: bool) { + self.one_shot = one_shot; + } + + fn triggered(&self) -> Option { + self.triggered + } + + fn set_triggered(&mut self, triggered: Option) { + self.triggered = triggered; + } +} + +impl fmt::Display for CrtcVerticalSyncBreakpoint { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match (self.on_start, self.on_end) { + (true, true) => write!(f, "VSYNC start or end"), + (true, false) => write!(f, "VSYNC start"), + (false, true) => write!(f, "VSYNC end"), + (false, false) => write!(f, "VSYNC (never)"), + } + } +} + +#[derive(Debug, Clone)] +pub struct CrtcDisplayEnableBreakpoint { + pub on_start: bool, + pub on_end: bool, + enabled: bool, + one_shot: bool, + triggered: Option, +} + +impl CrtcDisplayEnableBreakpoint { + pub fn new(on_start: bool, on_end: bool) -> Self { + Self { + on_start, + on_end, + enabled: true, + one_shot: false, + triggered: None, + } + } +} + +impl Breakpoint for CrtcDisplayEnableBreakpoint { + fn should_break(&mut self, source: DebugSource, event: &DebugEvent) -> bool { + if !self.enabled || source != DebugSource::Crtc { + return false; + } + + match event { + DebugEvent::Crtc(CrtcDebugEvent::DisplayEnableChanged { enabled }) => { + (*enabled && self.on_start) || (!*enabled && self.on_end) + } + _ => false, + } + } + + fn enabled(&self) -> bool { + self.enabled + } + + fn set_enabled(&mut self, enabled: bool) { + self.enabled = enabled; + } + + fn one_shot(&self) -> bool { + self.one_shot + } + + fn set_one_shot(&mut self, one_shot: bool) { + self.one_shot = one_shot; + } + + fn triggered(&self) -> Option { + self.triggered + } + + fn set_triggered(&mut self, triggered: Option) { + self.triggered = triggered; + } +} + +impl fmt::Display for CrtcDisplayEnableBreakpoint { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match (self.on_start, self.on_end) { + (true, true) => write!(f, "DISP. ENABLE start or end"), + (true, false) => write!(f, "DISP. ENABLE start"), + (false, true) => write!(f, "DISP. ENABLE end"), + (false, false) => write!(f, "DISP. ENABLE (never)"), + } + } +} + +#[derive(Debug, Clone)] +pub struct CrtcCountersBreakpoint { + pub character_row: Option, + pub scan_line: Option, + pub horizontal_counter: Option, + enabled: bool, + one_shot: bool, + triggered: Option, +} + +impl CrtcCountersBreakpoint { + pub fn new( + character_row: Option, + scan_line: Option, + horizontal_counter: Option, + ) -> Self { + Self { + character_row, + scan_line, + horizontal_counter, + enabled: true, + one_shot: false, + triggered: None, + } + } +} + +impl Breakpoint for CrtcCountersBreakpoint { + fn should_break(&mut self, source: DebugSource, event: &DebugEvent) -> bool { + if !self.enabled || source != DebugSource::Crtc { + return false; + } + + match event { + DebugEvent::Crtc(CrtcDebugEvent::CountersChanged { + character_row_is, + scan_line_is, + horizontal_counter_is, + .. + }) => { + self.character_row.is_none_or(|i| i == *character_row_is) + && self.scan_line.is_none_or(|i| i == *scan_line_is) + && self + .horizontal_counter + .is_none_or(|i| i == *horizontal_counter_is) + } + _ => false, + } + } + + fn enabled(&self) -> bool { + self.enabled + } + + fn set_enabled(&mut self, enabled: bool) { + self.enabled = enabled; + } + + fn one_shot(&self) -> bool { + self.one_shot + } + + fn set_one_shot(&mut self, one_shot: bool) { + self.one_shot = one_shot; + } + + fn triggered(&self) -> Option { + self.triggered + } + + fn set_triggered(&mut self, triggered: Option) { + self.triggered = triggered; + } +} + +impl fmt::Display for CrtcCountersBreakpoint { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Counters = ")?; + match self.character_row { + Some(character_row) => write!(f, "{:#04X}/", character_row)?, + None => write!(f, "Any/")?, + } + match self.scan_line { + Some(scan_line) => write!(f, "{:#04X}/", scan_line)?, + None => write!(f, "Any/")?, + } + match self.horizontal_counter { + Some(horiontal_counter) => write!(f, "{:#04X}", horiontal_counter), + None => write!(f, "Any"), + } + } +} + +#[derive(Debug, Clone)] +pub struct CrtcAddressBreakpoint { + pub value: Option, + enabled: bool, + one_shot: bool, + triggered: Option, +} + +impl CrtcAddressBreakpoint { + pub fn new(value: Option) -> Self { + Self { + value, + enabled: true, + one_shot: false, + triggered: None, + } + } +} + +impl Breakpoint for CrtcAddressBreakpoint { + fn should_break(&mut self, source: DebugSource, event: &DebugEvent) -> bool { + if !self.enabled || source != DebugSource::Crtc { + return false; + } + + match event { + DebugEvent::Crtc(CrtcDebugEvent::AddressChanged { is, .. }) => { + self.value.is_none_or(|value| value == *is) + } + _ => false, + } + } + + fn enabled(&self) -> bool { + self.enabled + } + + fn set_enabled(&mut self, enabled: bool) { + self.enabled = enabled; + } + + fn one_shot(&self) -> bool { + self.one_shot + } + + fn set_one_shot(&mut self, one_shot: bool) { + self.one_shot = one_shot; + } + + fn triggered(&self) -> Option { + self.triggered + } + + fn set_triggered(&mut self, triggered: Option) { + self.triggered = triggered; + } +} + +impl fmt::Display for CrtcAddressBreakpoint { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.value { + Some(value) => write!(f, "Address = {:#06X}", value), + None => write!(f, "Any address change"), + } + } +} + +#[derive(Debug, Clone)] +pub enum AnyBreakpoint { + CpuRegister8(CpuRegister8Breakpoint), + CpuRegister16(CpuRegister16Breakpoint), + CpuShadowRegister16(CpuShadowRegister16Breakpoint), + Memory(MemoryBreakpoint), + CallStack(CallStackBreakpoint), + GateArrayScreenMode(GateArrayScreenModeBreakpoint), + GateArrayPenColor(GateArrayPenColorBreakpoint), + GateArrayInterrupt(GateArrayInterruptBreakpoint), + CrtcRegisterWrite(CrtcRegisterWriteBreakpoint), + CrtcCounters(CrtcCountersBreakpoint), + CrtcAddress(CrtcAddressBreakpoint), + CrtcHorizontalSync(CrtcHorizontalSyncBreakpoint), + CrtcVerticalSync(CrtcVerticalSyncBreakpoint), + CrtcDisplayEnable(CrtcDisplayEnableBreakpoint), +} + +impl AnyBreakpoint { + pub fn pc_breakpoint(address: u16) -> Self { + Self::CpuRegister16(CpuRegister16Breakpoint::pc_breakpoint(address)) + } + + pub fn step_into() -> Self { + Self::CpuRegister16(CpuRegister16Breakpoint::step_into()) + } + + pub fn step_out() -> Self { + let mut breakpoint = CallStackBreakpoint::new(1); + breakpoint.set_one_shot(true); + + Self::CallStack(breakpoint) + } + + pub fn step_over() -> Self { + let mut breakpoint = CallStackBreakpoint::new(0); + breakpoint.set_one_shot(true); + + Self::CallStack(breakpoint) + } + + pub fn cpu_register8_breakpoint(register: Register8, value: Option) -> Self { + Self::CpuRegister8(CpuRegister8Breakpoint::new(register, value)) + } + + pub fn cpu_register16_breakpoint(register: Register16, value: Option) -> Self { + Self::CpuRegister16(CpuRegister16Breakpoint::new(register, value)) + } + + pub fn cpu_shadow_register16_breakpoint(register: Register16, value: Option) -> Self { + Self::CpuShadowRegister16(CpuShadowRegister16Breakpoint::new(register, value)) + } + + pub fn memory_breakpoint( + address: u16, + on_read: bool, + on_write: bool, + value: Option, + ) -> Self { + Self::Memory(MemoryBreakpoint::new(address, on_read, on_write, value)) + } + + pub fn gate_array_screen_mode_breakpoint(mode: Option, applied: bool) -> Self { + Self::GateArrayScreenMode(GateArrayScreenModeBreakpoint::new(mode, applied)) + } + + pub fn gate_array_pen_color_breakpoint(pen: Option, color: Option) -> Self { + Self::GateArrayPenColor(GateArrayPenColorBreakpoint::new(pen, color)) + } + + pub fn gate_array_interrupt_breakpoint() -> Self { + Self::GateArrayInterrupt(GateArrayInterruptBreakpoint::new()) + } + + pub fn crtc_register_write_breakpoint( + register: Option, + value: Option, + ) -> Self { + Self::CrtcRegisterWrite(CrtcRegisterWriteBreakpoint::new(register, value)) + } + + pub fn crtc_counters_breakpoint( + character_row: Option, + scan_line: Option, + horizontal_counter: Option, + ) -> Self { + Self::CrtcCounters(CrtcCountersBreakpoint::new( + character_row, + scan_line, + horizontal_counter, + )) + } + + pub fn crtc_address_breakpoint(value: Option) -> Self { + Self::CrtcAddress(CrtcAddressBreakpoint::new(value)) + } + + pub fn crtc_horizontal_sync_breakpoint(on_start: bool, on_end: bool) -> Self { + Self::CrtcHorizontalSync(CrtcHorizontalSyncBreakpoint::new(on_start, on_end)) + } + + pub fn crtc_vertical_sync_breakpoint(on_start: bool, on_end: bool) -> Self { + Self::CrtcVerticalSync(CrtcVerticalSyncBreakpoint::new(on_start, on_end)) + } + + pub fn crtc_dispaly_enable_breakpoint(on_start: bool, on_end: bool) -> Self { + Self::CrtcDisplayEnable(CrtcDisplayEnableBreakpoint::new(on_start, on_end)) + } +} + +impl Breakpoint for AnyBreakpoint { + fn should_break(&mut self, source: DebugSource, event: &DebugEvent) -> bool { + match self { + AnyBreakpoint::CpuRegister8(bp) => bp.should_break(source, event), + AnyBreakpoint::CpuRegister16(bp) => bp.should_break(source, event), + AnyBreakpoint::CpuShadowRegister16(bp) => bp.should_break(source, event), + AnyBreakpoint::Memory(bp) => bp.should_break(source, event), + AnyBreakpoint::CallStack(bp) => bp.should_break(source, event), + AnyBreakpoint::GateArrayScreenMode(bp) => bp.should_break(source, event), + AnyBreakpoint::GateArrayPenColor(bp) => bp.should_break(source, event), + AnyBreakpoint::GateArrayInterrupt(bp) => bp.should_break(source, event), + AnyBreakpoint::CrtcRegisterWrite(bp) => bp.should_break(source, event), + AnyBreakpoint::CrtcCounters(bp) => bp.should_break(source, event), + AnyBreakpoint::CrtcAddress(bp) => bp.should_break(source, event), + AnyBreakpoint::CrtcHorizontalSync(bp) => bp.should_break(source, event), + AnyBreakpoint::CrtcVerticalSync(bp) => bp.should_break(source, event), + AnyBreakpoint::CrtcDisplayEnable(bp) => bp.should_break(source, event), + } + } + + fn enabled(&self) -> bool { + match self { + AnyBreakpoint::CpuRegister8(bp) => bp.enabled(), + AnyBreakpoint::CpuRegister16(bp) => bp.enabled(), + AnyBreakpoint::CpuShadowRegister16(bp) => bp.enabled(), + AnyBreakpoint::Memory(bp) => bp.enabled(), + AnyBreakpoint::CallStack(bp) => bp.enabled(), + AnyBreakpoint::GateArrayScreenMode(bp) => bp.enabled(), + AnyBreakpoint::GateArrayPenColor(bp) => bp.enabled(), + AnyBreakpoint::GateArrayInterrupt(bp) => bp.enabled(), + AnyBreakpoint::CrtcRegisterWrite(bp) => bp.enabled(), + AnyBreakpoint::CrtcCounters(bp) => bp.enabled(), + AnyBreakpoint::CrtcAddress(bp) => bp.enabled(), + AnyBreakpoint::CrtcHorizontalSync(bp) => bp.enabled(), + AnyBreakpoint::CrtcVerticalSync(bp) => bp.enabled(), + AnyBreakpoint::CrtcDisplayEnable(bp) => bp.enabled(), + } + } + + fn set_enabled(&mut self, enabled: bool) { + match self { + AnyBreakpoint::CpuRegister8(bp) => bp.set_enabled(enabled), + AnyBreakpoint::CpuRegister16(bp) => bp.set_enabled(enabled), + AnyBreakpoint::CpuShadowRegister16(bp) => bp.set_enabled(enabled), + AnyBreakpoint::Memory(bp) => bp.set_enabled(enabled), + AnyBreakpoint::CallStack(bp) => bp.set_enabled(enabled), + AnyBreakpoint::GateArrayScreenMode(bp) => bp.set_enabled(enabled), + AnyBreakpoint::GateArrayPenColor(bp) => bp.set_enabled(enabled), + AnyBreakpoint::GateArrayInterrupt(bp) => bp.set_enabled(enabled), + AnyBreakpoint::CrtcRegisterWrite(bp) => bp.set_enabled(enabled), + AnyBreakpoint::CrtcCounters(bp) => bp.set_enabled(enabled), + AnyBreakpoint::CrtcAddress(bp) => bp.set_enabled(enabled), + AnyBreakpoint::CrtcHorizontalSync(bp) => bp.set_enabled(enabled), + AnyBreakpoint::CrtcVerticalSync(bp) => bp.set_enabled(enabled), + AnyBreakpoint::CrtcDisplayEnable(bp) => bp.set_enabled(enabled), + } + } + + fn one_shot(&self) -> bool { + match self { + AnyBreakpoint::CpuRegister8(bp) => bp.one_shot(), + AnyBreakpoint::CpuRegister16(bp) => bp.one_shot(), + AnyBreakpoint::CpuShadowRegister16(bp) => bp.one_shot(), + AnyBreakpoint::Memory(bp) => bp.one_shot(), + AnyBreakpoint::CallStack(bp) => bp.one_shot(), + AnyBreakpoint::GateArrayScreenMode(bp) => bp.one_shot(), + AnyBreakpoint::GateArrayPenColor(bp) => bp.one_shot(), + AnyBreakpoint::GateArrayInterrupt(bp) => bp.one_shot(), + AnyBreakpoint::CrtcRegisterWrite(bp) => bp.one_shot(), + AnyBreakpoint::CrtcCounters(bp) => bp.one_shot(), + AnyBreakpoint::CrtcAddress(bp) => bp.one_shot(), + AnyBreakpoint::CrtcHorizontalSync(bp) => bp.one_shot(), + AnyBreakpoint::CrtcVerticalSync(bp) => bp.one_shot(), + AnyBreakpoint::CrtcDisplayEnable(bp) => bp.one_shot(), + } + } + + fn set_one_shot(&mut self, one_shot: bool) { + match self { + AnyBreakpoint::CpuRegister8(bp) => bp.set_one_shot(one_shot), + AnyBreakpoint::CpuRegister16(bp) => bp.set_one_shot(one_shot), + AnyBreakpoint::CpuShadowRegister16(bp) => bp.set_one_shot(one_shot), + AnyBreakpoint::Memory(bp) => bp.set_one_shot(one_shot), + AnyBreakpoint::CallStack(bp) => bp.set_one_shot(one_shot), + AnyBreakpoint::GateArrayScreenMode(bp) => bp.set_one_shot(one_shot), + AnyBreakpoint::GateArrayPenColor(bp) => bp.set_one_shot(one_shot), + AnyBreakpoint::GateArrayInterrupt(bp) => bp.set_one_shot(one_shot), + AnyBreakpoint::CrtcRegisterWrite(bp) => bp.set_one_shot(one_shot), + AnyBreakpoint::CrtcCounters(bp) => bp.set_one_shot(one_shot), + AnyBreakpoint::CrtcAddress(bp) => bp.set_one_shot(one_shot), + AnyBreakpoint::CrtcHorizontalSync(bp) => bp.set_one_shot(one_shot), + AnyBreakpoint::CrtcVerticalSync(bp) => bp.set_one_shot(one_shot), + AnyBreakpoint::CrtcDisplayEnable(bp) => bp.set_one_shot(one_shot), + } + } + + fn triggered(&self) -> Option { + match self { + AnyBreakpoint::CpuRegister8(bp) => bp.triggered(), + AnyBreakpoint::CpuRegister16(bp) => bp.triggered(), + AnyBreakpoint::CpuShadowRegister16(bp) => bp.triggered(), + AnyBreakpoint::Memory(bp) => bp.triggered(), + AnyBreakpoint::CallStack(bp) => bp.triggered(), + AnyBreakpoint::GateArrayScreenMode(bp) => bp.triggered(), + AnyBreakpoint::GateArrayPenColor(bp) => bp.triggered(), + AnyBreakpoint::GateArrayInterrupt(bp) => bp.triggered(), + AnyBreakpoint::CrtcRegisterWrite(bp) => bp.triggered(), + AnyBreakpoint::CrtcCounters(bp) => bp.triggered(), + AnyBreakpoint::CrtcAddress(bp) => bp.triggered(), + AnyBreakpoint::CrtcHorizontalSync(bp) => bp.triggered(), + AnyBreakpoint::CrtcVerticalSync(bp) => bp.triggered(), + AnyBreakpoint::CrtcDisplayEnable(bp) => bp.triggered(), + } + } + + fn set_triggered(&mut self, triggered: Option) { + match self { + AnyBreakpoint::CpuRegister8(bp) => bp.set_triggered(triggered), + AnyBreakpoint::CpuRegister16(bp) => bp.set_triggered(triggered), + AnyBreakpoint::CpuShadowRegister16(bp) => bp.set_triggered(triggered), + AnyBreakpoint::Memory(bp) => bp.set_triggered(triggered), + AnyBreakpoint::CallStack(bp) => bp.set_triggered(triggered), + AnyBreakpoint::GateArrayScreenMode(bp) => bp.set_triggered(triggered), + AnyBreakpoint::GateArrayPenColor(bp) => bp.set_triggered(triggered), + AnyBreakpoint::GateArrayInterrupt(bp) => bp.set_triggered(triggered), + AnyBreakpoint::CrtcRegisterWrite(bp) => bp.set_triggered(triggered), + AnyBreakpoint::CrtcCounters(bp) => bp.set_triggered(triggered), + AnyBreakpoint::CrtcAddress(bp) => bp.set_triggered(triggered), + AnyBreakpoint::CrtcHorizontalSync(bp) => bp.set_triggered(triggered), + AnyBreakpoint::CrtcVerticalSync(bp) => bp.set_triggered(triggered), + AnyBreakpoint::CrtcDisplayEnable(bp) => bp.set_triggered(triggered), + } + } +} + +impl fmt::Display for AnyBreakpoint { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + AnyBreakpoint::CpuRegister8(bp) => bp.fmt(f), + AnyBreakpoint::CpuRegister16(bp) => bp.fmt(f), + AnyBreakpoint::CpuShadowRegister16(bp) => bp.fmt(f), + AnyBreakpoint::Memory(bp) => bp.fmt(f), + AnyBreakpoint::CallStack(bp) => bp.fmt(f), + AnyBreakpoint::GateArrayScreenMode(bp) => bp.fmt(f), + AnyBreakpoint::GateArrayPenColor(bp) => bp.fmt(f), + AnyBreakpoint::GateArrayInterrupt(bp) => bp.fmt(f), + AnyBreakpoint::CrtcRegisterWrite(bp) => bp.fmt(f), + AnyBreakpoint::CrtcCounters(bp) => bp.fmt(f), + AnyBreakpoint::CrtcAddress(bp) => bp.fmt(f), + AnyBreakpoint::CrtcHorizontalSync(bp) => bp.fmt(f), + AnyBreakpoint::CrtcVerticalSync(bp) => bp.fmt(f), + AnyBreakpoint::CrtcDisplayEnable(bp) => bp.fmt(f), + } + } +} + +pub struct BreakpointManager { + breakpoints: HashMap, + next_id: usize, + subscription: EventSubscription, +} + +impl BreakpointManager { + pub fn new() -> Self { + Self { + breakpoints: HashMap::new(), + next_id: 0, + subscription: EventSubscription::new(DebugSource::Any), + } + } + + pub fn add_breakpoint(&mut self, breakpoint: AnyBreakpoint) -> BreakpointId { + let id = BreakpointId(self.next_id); + self.next_id += 1; + self.breakpoints.insert(id, breakpoint); + id + } + + pub fn remove_breakpoint(&mut self, id: BreakpointId) -> bool { + self.breakpoints.remove(&id).is_some() + } + + pub fn enable_breakpoint(&mut self, id: BreakpointId, enabled: bool) -> bool { + if let Some(breakpoint) = self.breakpoints.get_mut(&id) { + breakpoint.set_enabled(enabled); + true + } else { + false + } + } + + pub fn breakpoint(&self, id: BreakpointId) -> Option<&AnyBreakpoint> { + self.breakpoints.get(&id) + } + + pub fn breakpoint_mut(&mut self, id: BreakpointId) -> Option<&mut AnyBreakpoint> { + self.breakpoints.get_mut(&id) + } + + pub fn breakpoints_iter(&self) -> impl Iterator { + self.breakpoints.iter() + } + + pub fn breakpoints_iter_mut( + &mut self, + ) -> impl Iterator { + self.breakpoints.iter_mut() + } + + pub fn clear_all(&mut self) { + self.breakpoints.clear(); + } + + pub fn any_triggered(&self) -> bool { + self.breakpoints.values().any(|bp| bp.triggered().is_some()) + } + + pub fn evaluate_breakpoints(&mut self) { + // Remove triggered one-shot breakpoints + self.breakpoints + .retain(|_id, bp| bp.triggered().is_none() || !bp.one_shot()); + + // Reset all triggered flags + for (_id, breakpoint) in self.breakpoints.iter_mut() { + breakpoint.set_triggered(None); + } + + self.subscription.with_events(|record| { + for (_id, breakpoint) in self.breakpoints.iter_mut() { + if breakpoint.should_break(record.source, &record.event) { + breakpoint.set_triggered(Some(record.master_clock)); + } + } + }); + } +} + +impl Default for BreakpointManager { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use crate::{debug::emit_event, system::clock::MasterClockTick}; + + use super::*; + + #[test] + fn test_pc_breakpoint_triggers_on_correct_address() { + let mut bp = AnyBreakpoint::pc_breakpoint(0x1000); + + let event = DebugEvent::Cpu(CpuDebugEvent::Register16Written { + register: Register16::PC, + is: 0x1000, + was: 0x0000, + }); + + assert!(bp.should_break(DebugSource::Cpu, &event)); + } + + #[test] + fn test_pc_breakpoint_ignores_wrong_address() { + let mut bp = AnyBreakpoint::pc_breakpoint(0x1000); + + let event = DebugEvent::Cpu(CpuDebugEvent::Register16Written { + register: Register16::PC, + is: 0x2000, + was: 0x0000, + }); + + assert!(!bp.should_break(DebugSource::Cpu, &event)); + } + + #[test] + fn test_pc_breakpoint_ignores_wrong_register() { + let mut bp = AnyBreakpoint::pc_breakpoint(0x1000); + + let event = DebugEvent::Cpu(CpuDebugEvent::Register16Written { + register: Register16::SP, + is: 0x1000, + was: 0x0000, + }); + + assert!(!bp.should_break(DebugSource::Cpu, &event)); + } + + #[test] + fn test_step_into_triggers_on_any_pc_change() { + let mut bp = AnyBreakpoint::step_into(); + + let event1 = DebugEvent::Cpu(CpuDebugEvent::Register16Written { + register: Register16::PC, + is: 0x1000, + was: 0x0000, + }); + + let event2 = DebugEvent::Cpu(CpuDebugEvent::Register16Written { + register: Register16::PC, + is: 0x2000, + was: 0x1000, + }); + + assert!(bp.should_break(DebugSource::Cpu, &event1)); + assert!(bp.should_break(DebugSource::Cpu, &event2)); + assert!(bp.one_shot()); + } + + #[test] + fn test_register8_breakpoint_with_specific_value() { + let mut bp = AnyBreakpoint::cpu_register8_breakpoint(Register8::A, Some(0x42)); + + let correct_event = DebugEvent::Cpu(CpuDebugEvent::Register8Written { + register: Register8::A, + is: 0x42, + was: 0x00, + }); + + let wrong_value_event = DebugEvent::Cpu(CpuDebugEvent::Register8Written { + register: Register8::A, + is: 0x43, + was: 0x00, + }); + + let wrong_register_event = DebugEvent::Cpu(CpuDebugEvent::Register8Written { + register: Register8::B, + is: 0x42, + was: 0x00, + }); + + assert!(bp.should_break(DebugSource::Cpu, &correct_event)); + assert!(!bp.should_break(DebugSource::Cpu, &wrong_value_event)); + assert!(!bp.should_break(DebugSource::Cpu, &wrong_register_event)); + } + + #[test] + fn test_register8_breakpoint_any_value() { + let mut bp = AnyBreakpoint::cpu_register8_breakpoint(Register8::A, None); + + let event1 = DebugEvent::Cpu(CpuDebugEvent::Register8Written { + register: Register8::A, + is: 0x42, + was: 0x00, + }); + + let event2 = DebugEvent::Cpu(CpuDebugEvent::Register8Written { + register: Register8::A, + is: 0xFF, + was: 0x42, + }); + + assert!(bp.should_break(DebugSource::Cpu, &event1)); + assert!(bp.should_break(DebugSource::Cpu, &event2)); + } + + #[test] + fn test_memory_breakpoint_write_only() { + let mut bp = AnyBreakpoint::memory_breakpoint(0x4000, false, true, None); + + let write_event = DebugEvent::Memory(MemoryDebugEvent::MemoryWritten { + address: 0x4000, + is: 0x55, + was: 0x00, + }); + + let read_event = DebugEvent::Memory(MemoryDebugEvent::MemoryRead { + address: 0x4000, + value: 0x55, + }); + + assert!(bp.should_break(DebugSource::Memory, &write_event)); + assert!(!bp.should_break(DebugSource::Memory, &read_event)); + } + + #[test] + fn test_memory_breakpoint_read_only() { + let mut bp = AnyBreakpoint::memory_breakpoint(0x4000, true, false, None); + + let write_event = DebugEvent::Memory(MemoryDebugEvent::MemoryWritten { + address: 0x4000, + is: 0x55, + was: 0x00, + }); + + let read_event = DebugEvent::Memory(MemoryDebugEvent::MemoryRead { + address: 0x4000, + value: 0x55, + }); + + assert!(!bp.should_break(DebugSource::Memory, &write_event)); + assert!(bp.should_break(DebugSource::Memory, &read_event)); + } + + #[test] + fn test_memory_breakpoint_with_value_filter() { + let mut bp = AnyBreakpoint::memory_breakpoint(0x4000, false, true, Some(0x42)); + + let correct_write = DebugEvent::Memory(MemoryDebugEvent::MemoryWritten { + address: 0x4000, + is: 0x42, + was: 0x00, + }); + + let wrong_value_write = DebugEvent::Memory(MemoryDebugEvent::MemoryWritten { + address: 0x4000, + is: 0x43, + was: 0x00, + }); + + assert!(bp.should_break(DebugSource::Memory, &correct_write)); + assert!(!bp.should_break(DebugSource::Memory, &wrong_value_write)); + } + + #[test] + fn test_breakpoint_enabled_disabled() { + let mut bp = AnyBreakpoint::pc_breakpoint(0x1000); + assert!(bp.enabled()); + + bp.set_enabled(false); + assert!(!bp.enabled()); + + let event = DebugEvent::Cpu(CpuDebugEvent::Register16Written { + register: Register16::PC, + is: 0x1000, + was: 0x0000, + }); + + assert!(!bp.should_break(DebugSource::Cpu, &event)); + + bp.set_enabled(true); + assert!(bp.should_break(DebugSource::Cpu, &event)); + } + + #[test] + fn test_breakpoint_one_shot_flag() { + let mut bp = AnyBreakpoint::pc_breakpoint(0x1000); + assert!(!bp.one_shot()); + + bp.set_one_shot(true); + assert!(bp.one_shot()); + + bp.set_one_shot(false); + assert!(!bp.one_shot()); + } + + #[test] + fn test_breakpoint_manager_add_remove() { + let mut manager = BreakpointManager::new(); + assert_eq!(manager.breakpoints.len(), 0); + + let id1 = manager.add_breakpoint(AnyBreakpoint::pc_breakpoint(0x1000)); + let id2 = manager.add_breakpoint(AnyBreakpoint::pc_breakpoint(0x2000)); + assert_eq!(manager.breakpoints.len(), 2); + + assert!(manager.remove_breakpoint(id1)); + assert_eq!(manager.breakpoints.len(), 1); + + assert!(!manager.remove_breakpoint(id1)); // Already removed + assert_eq!(manager.breakpoints.len(), 1); + + assert!(manager.remove_breakpoint(id2)); + assert_eq!(manager.breakpoints.len(), 0); + } + + #[test] + fn test_breakpoint_manager_enable_disable() { + let mut manager = BreakpointManager::new(); + let id = manager.add_breakpoint(AnyBreakpoint::pc_breakpoint(0x1000)); + + assert!(manager.breakpoint(id).unwrap().enabled()); + + assert!(manager.enable_breakpoint(id, false)); + assert!(!manager.breakpoint(id).unwrap().enabled()); + + assert!(manager.enable_breakpoint(id, true)); + assert!(manager.breakpoint(id).unwrap().enabled()); + + let invalid_id = BreakpointId(999); + assert!(!manager.enable_breakpoint(invalid_id, false)); + } + + #[test] + fn test_breakpoint_display_formats() { + let pc_bp = AnyBreakpoint::pc_breakpoint(0x1000); + assert_eq!(pc_bp.to_string(), "PC = 0x1000"); + + let reg8_specific = AnyBreakpoint::cpu_register8_breakpoint(Register8::A, Some(0x42)); + assert_eq!(reg8_specific.to_string(), "A = 0x42"); + + let reg8_any = AnyBreakpoint::cpu_register8_breakpoint(Register8::B, None); + assert_eq!(reg8_any.to_string(), "B written"); + + let mem_read = AnyBreakpoint::memory_breakpoint(0x4000, true, false, None); + assert_eq!(mem_read.to_string(), "0x4000 read"); + + let mem_write = AnyBreakpoint::memory_breakpoint(0x4000, false, true, None); + assert_eq!(mem_write.to_string(), "0x4000 write"); + + let mem_access = AnyBreakpoint::memory_breakpoint(0x4000, true, true, None); + assert_eq!(mem_access.to_string(), "0x4000 access"); + + let mem_write_value = AnyBreakpoint::memory_breakpoint(0x4000, false, true, Some(0xFF)); + assert_eq!(mem_write_value.to_string(), "0x4000 write = 0xFF"); + } + + #[test] + fn test_breakpoint_ignores_wrong_debug_source() { + let mut cpu_bp = AnyBreakpoint::pc_breakpoint(0x1000); + let mem_event = DebugEvent::Memory(MemoryDebugEvent::MemoryRead { + address: 0x1000, + value: 0x42, + }); + assert!(!cpu_bp.should_break(DebugSource::Memory, &mem_event)); + + let mut mem_bp = AnyBreakpoint::memory_breakpoint(0x1000, true, false, None); + let cpu_event = DebugEvent::Cpu(CpuDebugEvent::Register16Written { + register: Register16::PC, + is: 0x1000, + was: 0x0000, + }); + assert!(!mem_bp.should_break(DebugSource::Cpu, &cpu_event)); + } + + #[test] + fn test_breakpoint_manager_one_shot_removal() { + let mut manager = BreakpointManager::new(); + + let id = manager.add_breakpoint(AnyBreakpoint::step_into()); + assert_eq!(manager.breakpoints.len(), 1); + assert!(manager.breakpoint(id).unwrap().one_shot()); + + let event = DebugEvent::Cpu(CpuDebugEvent::Register16Written { + register: Register16::PC, + is: 0x1000, + was: 0x0000, + }); + + emit_event(DebugSource::Cpu, event.clone(), MasterClockTick::default()); + manager.evaluate_breakpoints(); + emit_event(DebugSource::Cpu, event, MasterClockTick::default()); + manager.evaluate_breakpoints(); + + // One-shot breakpoint should be removed after triggering + assert_eq!(manager.breakpoints.len(), 0); + assert!(manager.breakpoint(id).is_none()); + } + + #[test] + fn test_step_into_breakpoint_behavior() { + let mut manager = BreakpointManager::new(); + let step_into_id = manager.add_breakpoint(AnyBreakpoint::step_into()); + + // Verify step_into is a one-shot breakpoint + assert!(manager.breakpoint(step_into_id).unwrap().one_shot()); + assert!(manager.breakpoint(step_into_id).unwrap().enabled()); + + // Simulate PC change (any instruction execution) + let pc_event = DebugEvent::Cpu(CpuDebugEvent::Register16Written { + register: Register16::PC, + is: 0x1001, + was: 0x1000, + }); + + // Emit the event and evaluate breakpoints + emit_event(DebugSource::Cpu, pc_event, MasterClockTick::default()); + manager.evaluate_breakpoints(); + + // The breakpoint should have triggered + assert!(manager.any_triggered()); + assert!(manager + .breakpoint(step_into_id) + .unwrap() + .triggered() + .is_some()); + + // After evaluation, the one-shot breakpoint should be removed + manager.evaluate_breakpoints(); + assert_eq!(manager.breakpoints.len(), 0); + assert!(manager.breakpoint(step_into_id).is_none()); + } + + #[test] + fn test_step_over_breakpoint_behavior() { + let mut manager = BreakpointManager::new(); + let step_over_id = manager.add_breakpoint(AnyBreakpoint::step_over()); + + // Verify step_over is a one-shot CallStackBreakpoint with depth 0 + assert!(manager.breakpoint(step_over_id).unwrap().one_shot()); + assert!(manager.breakpoint(step_over_id).unwrap().enabled()); + + // Simulate a call instruction (increases call stack depth) + let call_event = DebugEvent::Cpu(CpuDebugEvent::CallFetched { interrupt: false }); + emit_event(DebugSource::Cpu, call_event, MasterClockTick::default()); + manager.evaluate_breakpoints(); + + // Should not trigger while in call + assert!(!manager.any_triggered()); + + // Simulate PC change while inside call + let pc_in_call = DebugEvent::Cpu(CpuDebugEvent::Register16Written { + register: Register16::PC, + is: 0x2000, + was: 0x1000, + }); + emit_event(DebugSource::Cpu, pc_in_call, MasterClockTick::default()); + manager.evaluate_breakpoints(); + + // Should still not trigger (we're in a deeper call level) + assert!(!manager.any_triggered()); + + // Simulate return instruction (decreases call stack depth back to 0) + let return_event = DebugEvent::Cpu(CpuDebugEvent::ReturnFetched { interrupt: false }); + emit_event(DebugSource::Cpu, return_event, MasterClockTick::default()); + manager.evaluate_breakpoints(); + + // Should still not trigger yet (return just adjusts depth) + assert!(!manager.any_triggered()); + + // Simulate PC change after return (back at original call level) + let pc_after_return = DebugEvent::Cpu(CpuDebugEvent::Register16Written { + register: Register16::PC, + is: 0x1001, + was: 0x1000, + }); + emit_event( + DebugSource::Cpu, + pc_after_return, + MasterClockTick::default(), + ); + manager.evaluate_breakpoints(); + + // Now it should trigger (we're back at depth 0) + assert!(manager.any_triggered()); + assert!(manager + .breakpoint(step_over_id) + .unwrap() + .triggered() + .is_some()); + + // After evaluation, the one-shot breakpoint should be removed + manager.evaluate_breakpoints(); + assert_eq!(manager.breakpoints.len(), 0); + } + + #[test] + fn test_step_over_breakpoint_no_calls() { + let mut manager = BreakpointManager::new(); + let step_over_id = manager.add_breakpoint(AnyBreakpoint::step_over()); + + // Verify step_over is a one-shot CallStackBreakpoint with depth 0 + assert!(manager.breakpoint(step_over_id).unwrap().one_shot()); + assert!(manager.breakpoint(step_over_id).unwrap().enabled()); + + // Simulate simple PC change without any calls (normal instruction execution) + let pc_event = DebugEvent::Cpu(CpuDebugEvent::Register16Written { + register: Register16::PC, + is: 0x1001, + was: 0x1000, + }); + emit_event(DebugSource::Cpu, pc_event, MasterClockTick::default()); + manager.evaluate_breakpoints(); + + // Should trigger immediately since we're already at depth 0 + assert!(manager.any_triggered()); + assert!(manager + .breakpoint(step_over_id) + .unwrap() + .triggered() + .is_some()); + + // After evaluation, the one-shot breakpoint should be removed + manager.evaluate_breakpoints(); + assert_eq!(manager.breakpoints.len(), 0); + } + + #[test] + fn test_step_out_from_inside_function() { + let mut manager = BreakpointManager::new(); + + // Simulate that we're already inside a function by creating the breakpoint + // after a call has been made (this is the typical debugging scenario) + let call_event = DebugEvent::Cpu(CpuDebugEvent::CallFetched { interrupt: false }); + emit_event(DebugSource::Cpu, call_event, MasterClockTick::default()); + + // Now create step_out breakpoint (starts with depth 1) + let step_out_id = manager.add_breakpoint(AnyBreakpoint::step_out()); + + // Evaluate breakpoints so the CallStackBreakpoint can process the call event + // This will increment its internal depth from 1 to 2 + manager.evaluate_breakpoints(); + + assert!(manager.breakpoint(step_out_id).unwrap().one_shot()); + assert!(manager.breakpoint(step_out_id).unwrap().enabled()); + assert!(!manager.any_triggered()); // Should not trigger yet + + // Simulate PC changes while inside function + let pc_in_function = DebugEvent::Cpu(CpuDebugEvent::Register16Written { + register: Register16::PC, + is: 0x2000, + was: 0x1000, + }); + emit_event(DebugSource::Cpu, pc_in_function, MasterClockTick::default()); + manager.evaluate_breakpoints(); + + // Should not trigger (we're at depth 2, not 0) + assert!(!manager.any_triggered()); + + // Simulate return from function + let return_event = DebugEvent::Cpu(CpuDebugEvent::ReturnFetched { interrupt: false }); + emit_event(DebugSource::Cpu, return_event, MasterClockTick::default()); + manager.evaluate_breakpoints(); + + // Should still not trigger yet (return just decrements depth to 1) + assert!(!manager.any_triggered()); + + // Simulate PC change after return (now at depth 1, but we need depth 0) + let pc_after_return = DebugEvent::Cpu(CpuDebugEvent::Register16Written { + register: Register16::PC, + is: 0x1001, + was: 0x1000, + }); + emit_event( + DebugSource::Cpu, + pc_after_return, + MasterClockTick::default(), + ); + manager.evaluate_breakpoints(); + + // Still should not trigger (depth is 1, not 0) + assert!(!manager.any_triggered()); + + // Need another return to get to depth 0 + let final_return = DebugEvent::Cpu(CpuDebugEvent::ReturnFetched { interrupt: false }); + emit_event(DebugSource::Cpu, final_return, MasterClockTick::default()); + + // Now PC change should trigger step_out + let final_pc = DebugEvent::Cpu(CpuDebugEvent::Register16Written { + register: Register16::PC, + is: 0x1002, + was: 0x1001, + }); + emit_event(DebugSource::Cpu, final_pc, MasterClockTick::default()); + manager.evaluate_breakpoints(); + + // Now it should trigger (we've stepped out to depth 0) + assert!(manager.any_triggered()); + assert!(manager + .breakpoint(step_out_id) + .unwrap() + .triggered() + .is_some()); + + // After evaluation, the one-shot breakpoint should be removed + manager.evaluate_breakpoints(); + assert_eq!(manager.breakpoints.len(), 0); + } +} diff --git a/ronald-core/src/debug/event.rs b/ronald-core/src/debug/event.rs new file mode 100644 index 0000000..f85ff63 --- /dev/null +++ b/ronald-core/src/debug/event.rs @@ -0,0 +1,137 @@ +use crate::system::bus::crtc::Register as CrtcRegister; +use crate::system::cpu::{Register16, Register8}; + +/// A DebugEvent is any internal state change and any input or output +#[derive(Debug, Clone)] +pub enum DebugEvent { + Cpu(CpuDebugEvent), + Memory(MemoryDebugEvent), + Crtc(CrtcDebugEvent), + GateArray(GateArrayDebugEvent), + Fdc(FdcDebugEvent), + Ppi(PpiDebugEvent), + Psg(PsgDebugEvent), + Tape(TapeDebugEvent), +} + +#[derive(Debug, Clone)] +pub enum CpuDebugEvent { + Register8Written { + register: Register8, + is: u8, + was: u8, + }, + Register16Written { + register: Register16, + is: u16, + was: u16, + }, + ShadowRegister16Written { + register: Register16, + is: u16, + was: u16, + }, + CallFetched { + interrupt: bool, + }, + ReturnFetched { + interrupt: bool, + }, +} + +impl From for DebugEvent { + fn from(event: CpuDebugEvent) -> Self { + DebugEvent::Cpu(event) + } +} + +#[derive(Debug, Clone)] +pub enum MemoryDebugEvent { + MemoryRead { address: usize, value: u8 }, + MemoryWritten { address: usize, is: u8, was: u8 }, +} + +impl From for DebugEvent { + fn from(event: MemoryDebugEvent) -> Self { + DebugEvent::Memory(event) + } +} + +#[derive(Debug, Clone)] +pub enum CrtcDebugEvent { + RegisterSelected { + register: CrtcRegister, + }, + RegisterWritten { + register: CrtcRegister, + is: u8, + was: u8, + }, + CountersChanged { + character_row_is: u8, + character_row_was: u8, + scan_line_is: u8, + scan_line_was: u8, + horizontal_counter_is: u8, + horizontal_counter_was: u8, + }, + AddressChanged { + is: usize, + was: usize, + }, + HorizontalSync { + enabled: bool, + }, + VerticalSync { + enabled: bool, + }, + DisplayEnableChanged { + enabled: bool, + }, +} + +impl From for DebugEvent { + fn from(event: CrtcDebugEvent) -> Self { + DebugEvent::Crtc(event) + } +} + +#[derive(Debug, Clone)] +pub enum GateArrayDebugEvent { + ScreenModeChanged { + is: u8, + was: u8, + applied: bool, + }, + PenSelected { + pen: usize, + }, + PenColorChanged { + pen: usize, + is: u8, + was: u8, + }, + InterruptGenerated, + RomConfigChanged { + lower_rom_enabled: bool, + upper_rom_enabled: bool, + }, +} + +impl From for DebugEvent { + fn from(event: GateArrayDebugEvent) -> Self { + DebugEvent::GateArray(event) + } +} + +#[derive(Debug, Clone)] +pub enum FdcDebugEvent {} + +#[derive(Debug, Clone)] +pub enum PpiDebugEvent {} + +#[derive(Debug, Clone)] +pub enum PsgDebugEvent {} + +#[derive(Debug, Clone)] +pub enum TapeDebugEvent {} diff --git a/ronald-core/src/debug/view.rs b/ronald-core/src/debug/view.rs new file mode 100644 index 0000000..c7f343e --- /dev/null +++ b/ronald-core/src/debug/view.rs @@ -0,0 +1,83 @@ +use std::collections::HashMap; + +use crate::system::{ + bus::crtc::Register as CrtcRegister, clock::MasterClockTick, instruction::InterruptMode, +}; + +pub struct SystemDebugView { + pub master_clock: MasterClockTick, + pub cpu: CpuDebugView, + pub memory: MemoryDebugView, + pub gate_array: GateArrayDebugView, + pub crtc: CrtcDebugView, +} + +pub struct CpuDebugView { + pub register_a: u8, + pub register_f: u8, + pub register_b: u8, + pub register_c: u8, + pub register_d: u8, + pub register_e: u8, + pub register_h: u8, + pub register_l: u8, + pub shadow_register_a: u8, + pub shadow_register_f: u8, + pub shadow_register_b: u8, + pub shadow_register_c: u8, + pub shadow_register_d: u8, + pub shadow_register_e: u8, + pub shadow_register_h: u8, + pub shadow_register_l: u8, + pub register_i: u8, + pub register_r: u8, + pub register_ixh: u8, + pub register_ixl: u8, + pub register_iyh: u8, + pub register_iyl: u8, + pub register_sp: u16, + pub register_pc: u16, + pub iff1: bool, + pub iff2: bool, + pub halted: bool, + pub interrupt_mode: InterruptMode, + pub enable_interrupt: bool, + pub irq_received: bool, +} + +pub struct MemoryDebugView { + pub ram: Vec, + pub ram_extension: Vec, + pub lower_rom: Vec, + pub lower_rom_enabled: bool, + pub upper_roms: HashMap>, + pub selected_upper_rom: u8, + pub upper_rom_enabled: bool, + pub composite_rom_ram: Vec, + pub composite_ram: Vec, +} + +pub struct GateArrayDebugView { + pub current_screen_mode: u8, + pub requested_screen_mode: Option, + pub hsync_active: bool, + pub vsync_active: bool, + pub hsyncs_since_last_vsync: u16, + pub interrupt_counter: u8, + pub hold_interrupt: bool, + pub selected_pen: usize, + pub pen_colors: Vec, // Hardware color values (0-31) +} + +pub struct CrtcDebugView { + pub registers: [u8; 18], + pub selected_register: CrtcRegister, + pub horizontal_counter: u8, + pub character_row_counter: u8, + pub scan_line_counter: u8, + pub display_start_address: u16, + pub hsync_active: bool, + pub vsync_active: bool, + pub display_enabled: bool, + pub current_address: usize, +} diff --git a/ronald-core/src/debugger.rs b/ronald-core/src/debugger.rs deleted file mode 100644 index 894b32c..0000000 --- a/ronald-core/src/debugger.rs +++ /dev/null @@ -1,227 +0,0 @@ -use std::io::Write; -use nom::{ - IResult, - branch::alt, - bytes::complete::{tag, take_while, take_while1, take_while_m_n}, - combinator::{map_res, opt}, - sequence::{delimited, pair, separated_pair} -}; - -use crate::{cpu::{Cpu, Register16}, memory::AnyMemory}; - -#[derive(Debug)] -enum Command { - ToggleBreakpoint(u16), - ShowCpuRegisters, - Step(u16), - Continue, - Disassemble(u16), -} - -impl Command { - fn parse(input: &str) -> IResult<&str, Command> { - alt(( - parse_toggle_breakpoint, - parse_show_cpu_registers, - parse_step, - parse_continue, - parse_disassemble, - ))(input) - } -} - -fn is_decimal_digit(c: char) -> bool { - c.is_ascii_digit() -} - -fn from_decimal_str(input: &str) -> Result { - input.parse::() -} - -fn parse_decimal(input: &str) -> IResult<&str, u16> { - map_res( - take_while_m_n(1, 5, is_decimal_digit), - from_decimal_str - )(input) -} - -fn is_hex_digit(c: char) -> bool { - c.is_ascii_hexdigit() -} - -fn from_hex_str(input: &str) -> Result { - u16::from_str_radix(input, 16) -} - -fn parse_hex(input: &str) -> IResult<&str, u16> { - let (input, _) = tag("0x")(input)?; - map_res( - take_while_m_n(1, 4, is_hex_digit), - from_hex_str - )(input) -} - -fn is_whitespace(c: char) -> bool { - c.is_whitespace() -} - -fn parse_toggle_breakpoint(input: &str) -> IResult<&str, Command> { - let (input, (_, address)) = delimited( - take_while(is_whitespace), - separated_pair( - alt((tag("breakpoint"), tag("break"), tag("b"))), - take_while1(is_whitespace), - alt((parse_hex, parse_decimal)) - ), - take_while(is_whitespace) - )(input)?; - - Ok((input, Command::ToggleBreakpoint(address))) -} - -fn parse_show_cpu_registers(input: &str) -> IResult<&str, Command> { - let (input, _) = alt((tag("registers"), tag("reg"), tag("r")))(input)?; - - Ok((input, Command::ShowCpuRegisters)) -} - -fn parse_step(input: &str) -> IResult<&str, Command> { - let (input, _) = take_while(is_whitespace)(input)?; - let (input, _) = alt((tag("step"), tag("s")))(input)?; - - let (input, argument) = opt(pair( - take_while1(is_whitespace), - alt((parse_hex, parse_decimal)) - ))(input)?; - - let (input, _) = take_while(is_whitespace)(input)?; - - match argument { - Some((_, count)) => Ok((input, Command::Step(count))), - None => Ok((input, Command::Step(0))), - } -} - -fn parse_continue(input: &str) -> IResult<&str, Command> { - let (input, _) = alt((tag("continue"), tag("cont"), tag("c")))(input)?; - - Ok((input, Command::Continue)) -} - -fn parse_disassemble(input: &str) -> IResult<&str, Command> { - let (input, _) = alt((tag("disassemble"), tag("dis"), tag("d")))(input)?; - - Ok((input, Command::Disassemble(10))) -} - - -pub struct Debugger { - breakpoints: Vec, - countdown: Option, -} - -impl Debugger { - pub fn new() -> Self { - Debugger { - breakpoints: Vec::new(), - countdown: None, - } - } - - pub fn activate(&mut self) { - self.countdown = Some(0); - } - - pub fn is_active(&mut self, cpu: &Cpu) -> bool { - let address = cpu.registers.read_word(&Register16::PC); - if self.breakpoint_at(address) { - return true; - } - - match self.countdown { - Some(countdown) => { - if countdown == 0 { - self.countdown = None; - true - } else { - self.countdown = Some(countdown - 1); - false - } - } - None => false, - } - } - - pub fn run_command_shell(&mut self, cpu: &mut Cpu, memory: &AnyMemory) { - let address = cpu.registers.read_word(&Register16::PC) as usize; - let (instruction, _) = cpu.decoder.decode_at(memory, address); - println!("{:#06x}: {}", address, &instruction); - - loop { - print!("> "); - std::io::stdout().flush().unwrap(); // TODO: is the a better way to handle the result? - - let mut input = String::new(); - match std::io::stdin().read_line(&mut input) { - Ok(_) => { - if let Ok((_, command)) = Command::parse(&input) { - match command { - Command::ToggleBreakpoint(address) => { - if self.breakpoint_at(address) { - self.remove_breakpoint(address); - } else { - self.add_breakpoint(address); - } - - println!("Active breakpoints:"); - for breakpoint in &self.breakpoints { - println!("{:#06x}", breakpoint); - } - } - Command::ShowCpuRegisters => { - cpu.print_state(); - } - Command::Step(skip) => { - self.countdown = Some(skip); - break; - } - Command::Continue => { - break; - } - Command::Disassemble(count) => { - let mut address = cpu.registers.read_word(&Register16::PC) as usize; - for _ in 0..count { - let (instruction, next_adress) = cpu.decoder.decode_at(memory, address); - println!("{:#06x}: {}", address, &instruction); - address = next_adress; - } - } - } - } - } - Err(error) => { - log::error!("Error reading from stdin: {}", error); - break; - } - } - } - } - - fn breakpoint_at(&self, address: u16) -> bool { - for breakpoint in &self.breakpoints { - if *breakpoint == address { - return true; - } - } - - false - } - - fn add_breakpoint(&mut self, address: u16) { - self.breakpoints.push(address); - } - - fn remove_breakpoint(&mut self, address: u16) { - self.breakpoints = self.breakpoints.iter().filter(|breakpoint| **breakpoint != address ).copied().collect(); - } -} \ No newline at end of file diff --git a/ronald-core/src/lib.rs b/ronald-core/src/lib.rs index 0766a92..e91c6a1 100644 --- a/ronald-core/src/lib.rs +++ b/ronald-core/src/lib.rs @@ -1,13 +1,17 @@ use std::{collections::HashMap, path::PathBuf}; use constants::KeyDefinition; +use debug::{breakpoint::BreakpointManager, view::SystemDebugView, Snapshottable}; use system::bus::{crtc::AnyCrtController, gate_array::AnyGateArray, StandardBus}; use system::cpu::ZilogZ80; use system::instruction::AlgorithmicDecoder; use system::memory::AnyMemory; use system::{AmstradCpc, SystemConfig}; +use crate::system::instruction::DecodedInstruction; + pub mod constants; +pub mod debug; pub mod system; pub trait VideoSink { @@ -28,6 +32,8 @@ pub struct Driver { StandardBus, >, keys: HashMap<&'static str, KeyDefinition>, + breakpoint_manager: BreakpointManager, + cached_debug_view: Option, } impl Driver { @@ -36,6 +42,8 @@ impl Driver { Self { system: AmstradCpc::default(), keys, + breakpoint_manager: BreakpointManager::default(), + cached_debug_view: None, } } @@ -44,23 +52,30 @@ impl Driver { Self { system: config.clone().into(), keys, + breakpoint_manager: BreakpointManager::default(), + cached_debug_view: None, } } - pub fn step(&mut self, usecs: usize, video: &mut impl VideoSink, audio: &mut impl AudioSink) { + pub fn step( + &mut self, + usecs: usize, + video: &mut impl VideoSink, + audio: &mut impl AudioSink, + ) -> bool { + self.cached_debug_view = None; + let mut elapsed_microseconds = 0; while elapsed_microseconds < usecs { // TODO: tie this to vsync instead of fixed value elapsed_microseconds += self.system.emulate(video, audio) as usize; - } - } - pub fn step_single(&mut self, video: &mut impl VideoSink, audio: &mut impl AudioSink) { - self.system.emulate(video, audio); - } - - pub fn activate_debugger(&self) { - todo!() + self.breakpoint_manager.evaluate_breakpoints(); + if self.breakpoint_manager.any_triggered() { + return true; + } + } + false // No breakpoint hit } pub fn press_key(&mut self, key: &str) { @@ -85,11 +100,6 @@ impl Driver { serde_json::to_string(&self.system) } - pub fn disassemble(&mut self, count: usize) -> serde_json::Result { - let disassembly = self.system.disassemble(count); - serde_json::to_string(&disassembly) - } - pub fn save_rom(&self) -> Vec { todo!() } @@ -101,6 +111,22 @@ impl Driver { pub fn save_snapshot(&self) -> Vec { todo!() } + + pub fn debug_view(&mut self) -> &SystemDebugView { + if self.cached_debug_view.is_none() { + self.cached_debug_view = Some(self.system.debug_view()); + } + + self.cached_debug_view.as_ref().unwrap() + } + + pub fn disassemble(&self, start_address: u16, count: usize) -> Vec { + self.system.disassemble(start_address, count) + } + + pub fn breakpoint_manager(&mut self) -> &mut BreakpointManager { + &mut self.breakpoint_manager + } } impl Default for Driver { diff --git a/ronald-core/src/system.rs b/ronald-core/src/system.rs index d5ac989..8ddab74 100644 --- a/ronald-core/src/system.rs +++ b/ronald-core/src/system.rs @@ -1,4 +1,5 @@ pub mod bus; +pub mod clock; pub mod cpu; pub mod instruction; pub mod memory; @@ -7,20 +8,20 @@ use std::path::PathBuf; use serde::{Deserialize, Serialize}; +use crate::debug::view::{CpuDebugView, GateArrayDebugView, MemoryDebugView, SystemDebugView}; +use crate::debug::{record_debug_events, Snapshottable}; +use crate::system::bus::BusDebugView; +use crate::system::clock::{MasterClock, MasterClockTick}; +use crate::system::instruction::{DecodedInstruction, Instruction}; use crate::{AudioSink, VideoSink}; use bus::crtc::AnyCrtController; use bus::gate_array::AnyGateArray; use bus::{Bus, StandardBus}; use cpu::Cpu; +use instruction::{AlgorithmicDecoder, Decoder}; use memory::{AnyMemory, MemManage, MemRead, MemWrite, MemoryCpc6128, MemoryCpcX64}; -#[derive(Serialize, Deserialize)] -pub struct DisassembledInstruction { - address: u16, - instruction: String, -} - #[derive(Default, Serialize, Deserialize)] #[serde(bound( serialize = "C: Serialize, M: Serialize, B: Serialize", @@ -30,33 +31,42 @@ pub struct DisassembledInstruction { pub struct AmstradCpc where C: Cpu, - M: MemRead + MemWrite + MemManage + Default, + M: MemRead + MemWrite + MemManage, B: Bus, { cpu: C, memory: M, #[serde(flatten)] bus: B, - master_clock: u64, + master_clock: MasterClock, disk_drives: DiskDrives, + last_instruction: Option, } impl AmstradCpc where C: Cpu, - M: MemRead + MemWrite + MemManage + Default, + M: MemRead + MemWrite + MemManage, B: Bus, { pub fn emulate(&mut self, video: &mut impl VideoSink, audio: &mut impl AudioSink) -> u8 { - let (cycles, interrupt_acknowledged) = - self.cpu.fetch_and_execute(&mut self.memory, &mut self.bus); + let (cycles, interrupt_acknowledged, executed_instruction) = self.cpu.fetch_and_execute( + &mut self.memory, + &mut self.bus, + self.master_clock.current(), + ); + if executed_instruction.is_some() { + self.last_instruction = executed_instruction; + } // Master clock runs at 16MHz // CPU runs at 4MHz (master clock / 4) // cycles represents NOP time units, where 1 NOP = 4 CPU cycles = 16 master clock ticks for _ in 0..cycles { - self.master_clock += 16; - let interrupt = self.bus.step(&mut self.memory, video, audio); + self.master_clock.step(16); + let interrupt = + self.bus + .step(&mut self.memory, video, audio, self.master_clock.current()); if interrupt { self.cpu.request_interrupt(); } @@ -83,16 +93,58 @@ where // TODO: allow loading tapes as well self.bus.load_disk(drive, rom, path); } +} - pub fn disassemble(&mut self, count: usize) -> Vec { - self.cpu - .disassemble(&mut self.memory, count) - .into_iter() - .map(|(address, instruction)| DisassembledInstruction { +impl AmstradCpc +where + C: Cpu, + M: MemRead + MemWrite + MemManage, + B: Bus, +{ + pub fn disassemble(&self, start_address: u16, count: usize) -> Vec { + record_debug_events(false); + let mut decoder = AlgorithmicDecoder::default(); + let mut disassembly = Vec::with_capacity(count + 1); + let mut address = start_address; + if let Some(last_instruction) = &self.last_instruction { + disassembly.push(last_instruction.clone()); + } + for _ in 0..count { + let (instruction, next_address) = decoder.decode(&self.memory, address as usize); + let length = next_address - address as usize; + disassembly.push(DecodedInstruction { address, instruction, - }) - .collect() + length, + }); + address = next_address as u16; + } + record_debug_events(true); + disassembly + } +} + +impl Snapshottable for AmstradCpc +where + C: Cpu + Snapshottable, + M: MemRead + MemWrite + MemManage + Snapshottable + Default, + B: Bus + Snapshottable, +{ + type View = SystemDebugView; + + fn debug_view(&self) -> Self::View { + record_debug_events(false); + let bus_debug_view = self.bus.debug_view(); + let debug_view = Self::View { + master_clock: self.master_clock.current(), + cpu: self.cpu.debug_view(), + memory: self.memory.debug_view(), + gate_array: bus_debug_view.gate_array, + crtc: bus_debug_view.crtc, + }; + record_debug_events(true); + + debug_view } } @@ -191,8 +243,9 @@ where cpu, memory, bus, - master_clock: 0, + master_clock: MasterClock::default(), disk_drives: config.disk_drives, + last_instruction: None, } } } diff --git a/ronald-core/src/system/bus.rs b/ronald-core/src/system/bus.rs index 2b60d4c..2de3f94 100644 --- a/ronald-core/src/system/bus.rs +++ b/ronald-core/src/system/bus.rs @@ -2,6 +2,9 @@ use std::path::PathBuf; use serde::{Deserialize, Serialize}; +use crate::debug::view::{CrtcDebugView, GateArrayDebugView}; +use crate::debug::Snapshottable; +use crate::system::clock::MasterClockTick; use crate::system::memory::{AnyMemory, MemManage, MemRead}; use crate::{AudioSink, VideoSink}; @@ -23,6 +26,11 @@ use psg::SoundGenerator; use screen::Screen; use tape::TapeController; +pub struct BusDebugView { + pub gate_array: GateArrayDebugView, + pub crtc: CrtcDebugView, +} + pub trait Bus: Default { // TODO: replace by BusDevice fn read_byte(&mut self, port: u16) -> u8; @@ -32,6 +40,7 @@ pub trait Bus: Default { memory: &mut (impl MemRead + MemManage), video: &mut impl VideoSink, audio: &mut impl AudioSink, + master_clock: MasterClockTick, ) -> bool; fn acknowledge_interrupt(&mut self); fn set_key(&mut self, line: usize, bit: u8); @@ -67,7 +76,7 @@ where _ if port & 0x0800 == 0 => self.ppi.read_byte(&self.crtc, &self.psg, &self.tape, port), 0xfb7e | 0xfb7f => self.fdc.read_byte(port), _ => { - log::error!("Unhandled read from port {port:#06x}"); + log::error!("Unhandled read from port {port:#06X}"); unimplemented!(); } } @@ -92,7 +101,7 @@ where 0xfa7e | 0xfb7f => self.fdc.write_byte(port, value), 0xf8ff => (), // peripheral soft reset (ignored) _ => { - log::error!("Unhandled write to port {port:#06x}: {value:#010b}"); + log::error!("Unhandled write to port {port:#06X}: {value:#010b}"); unimplemented!(); } } @@ -103,11 +112,12 @@ where memory: &mut (impl MemRead + MemManage), video: &mut impl VideoSink, audio: &mut impl AudioSink, + master_clock: MasterClockTick, ) -> bool { self.psg.step(audio); - self.crtc.step(); + self.crtc.step(master_clock); self.gate_array - .step(&self.crtc, memory, &mut self.screen, video) + .step(&self.crtc, memory, &mut self.screen, video, master_clock) } fn acknowledge_interrupt(&mut self) { @@ -126,3 +136,18 @@ where self.fdc.load_disk(drive, rom, path); } } + +impl Snapshottable for StandardBus +where + C: CrtController + Snapshottable, + G: GateArray + Snapshottable, +{ + type View = BusDebugView; + + fn debug_view(&self) -> Self::View { + BusDebugView { + gate_array: self.gate_array.debug_view(), + crtc: self.crtc.debug_view(), + } + } +} diff --git a/ronald-core/src/system/bus/crtc.rs b/ronald-core/src/system/bus/crtc.rs index f6c53a3..7e44dd9 100644 --- a/ronald-core/src/system/bus/crtc.rs +++ b/ronald-core/src/system/bus/crtc.rs @@ -1,17 +1,26 @@ +use std::fmt; + +use num_enum::{IntoPrimitive, TryFromPrimitive}; use serde::{Deserialize, Serialize}; +use crate::debug::event::CrtcDebugEvent; +use crate::debug::view::CrtcDebugView; +use crate::debug::{DebugSource, Debuggable, Snapshottable}; +use crate::system::clock::MasterClockTick; + pub trait CrtController: Default { fn read_byte(&mut self, port: u16) -> u8; fn write_byte(&mut self, port: u16, value: u8); - fn step(&mut self); + fn step(&mut self, master_clock: MasterClockTick); fn read_address(&self) -> usize; fn read_display_enabled(&self) -> bool; fn read_horizontal_sync(&self) -> bool; fn read_vertical_sync(&self) -> bool; } -enum Register { - // TODO: use all registers +#[derive(Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, TryFromPrimitive)] +#[repr(usize)] +pub enum Register { HorizontalTotal, HorizontalDisplayed, HorizontalSyncPosition, @@ -30,6 +39,54 @@ enum Register { CursorAddressLow, LightPenAddressHigh, LightPenAddressLow, + #[num_enum(alternatives = [19..31])] + Unused, + Dummy = 31, +} + +impl fmt::Display for Register { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Register::HorizontalTotal => write!(f, "R{} (Horizontal Total)", usize::from(*self)), + Register::HorizontalDisplayed => { + write!(f, "R{} (Horizontal Displayed)", usize::from(*self)) + } + Register::HorizontalSyncPosition => { + write!(f, "R{} (H. Sync Position)", usize::from(*self)) + } + Register::HorizontalAndVerticalSyncWidths => { + write!(f, "R{} (H/V Sync Widths)", usize::from(*self)) + } + Register::VerticalTotal => write!(f, "R{} (Vertical Total)", usize::from(*self)), + Register::VerticalTotalAdjust => write!(f, "R{} (V. Total Adjust)", usize::from(*self)), + Register::VerticalDisplayed => { + write!(f, "R{} (Vertical Displayed)", usize::from(*self)) + } + Register::VerticalSyncPosition => { + write!(f, "R{} (V. Sync Position)", usize::from(*self)) + } + Register::InterlaceAndSkew => write!(f, "R{} (Interlace/Skew)", usize::from(*self)), + Register::MaximumRasterAddress => { + write!(f, "R{} (Max Raster Address)", usize::from(*self)) + } + Register::CursorStartRaster => write!(f, "R{} (Cursor Start)", usize::from(*self)), + Register::CursorEndRaster => write!(f, "R{} (Cursor End)", usize::from(*self)), + Register::DisplayStartAddressHigh => { + write!(f, "R{} (Display Start High)", usize::from(*self)) + } + Register::DisplayStartAddressLow => { + write!(f, "R{} (Display Start Low)", usize::from(*self)) + } + Register::CursorAddressHigh => { + write!(f, "R{} (Cursor Address High)", usize::from(*self)) + } + Register::CursorAddressLow => write!(f, "R{} (Cursor Address Low)", usize::from(*self)), + Register::LightPenAddressHigh => write!(f, "R{} (Light Pen High)", usize::from(*self)), + Register::LightPenAddressLow => write!(f, "R{} (Light Pen Low)", usize::from(*self)), + Register::Unused => write!(f, "R18-R30 (Unused)"), + Register::Dummy => write!(f, "R{} (Dummy)", usize::from(*self)), + } + } } #[derive(Default, Serialize, Deserialize)] @@ -42,27 +99,79 @@ pub struct HitachiHd6845s { character_row_counter: u8, scan_line_counter: u8, display_start_address: u16, + master_clock: MasterClockTick, + previous_hsync: bool, + previous_vsync: bool, + previous_display_enabled: bool, + previous_address: usize, } impl HitachiHd6845s { fn select_register(&mut self, register: usize) { self.selected_register = register; + self.emit_debug_event( + CrtcDebugEvent::RegisterSelected { + register: Register::try_from(register).expect("Invalid CRTC register selected"), + }, + self.master_clock, + ); } fn write_register(&mut self, value: u8) { - self.registers[self.selected_register] = value; // TODO: restrict to registers 0-15, TODO: restrict bit-width + // TODO: restrict to writable registers + let was = self.registers[self.selected_register]; + self.registers[self.selected_register] = value; + + self.emit_debug_event( + CrtcDebugEvent::RegisterWritten { + register: Register::try_from(self.selected_register).unwrap(), + is: value, + was, + }, + self.master_clock, + ); } fn read_register(&self) -> u8 { - self.registers[self.selected_register] // TODO: restrict to registers 14-17 + // TODO: restrict to readable registers + // TODO: handle type 4 reads (see https://www.cpcwiki.eu/index.php/Extra_CPC_Plus_Hardware_Information#CRTC) + self.registers[self.selected_register] } } +impl Snapshottable for HitachiHd6845s { + type View = CrtcDebugView; + + fn debug_view(&self) -> Self::View { + CrtcDebugView { + registers: self.registers, + selected_register: Register::try_from(self.selected_register).unwrap(), + horizontal_counter: self.horizontal_counter, + character_row_counter: self.character_row_counter, + scan_line_counter: self.scan_line_counter, + display_start_address: self.display_start_address, + hsync_active: self.read_horizontal_sync(), + vsync_active: self.read_vertical_sync(), + display_enabled: self.read_display_enabled(), + current_address: self.read_address(), + } + } +} + +impl Debuggable for HitachiHd6845s { + const SOURCE: DebugSource = DebugSource::Crtc; + type Event = CrtcDebugEvent; +} + impl CrtController for HitachiHd6845s { fn read_byte(&mut self, port: u16) -> u8 { - // TODO: get rid of port parameter? - log::error!("Unexpected read from CRT controller"); - unimplemented!() + let function = (port >> 8) & 0x03; + + match function { + 2 => todo!("handle read depending on CRTC type"), + 3 => self.read_register(), + _ => 0xff, // TODO: properly emulate floating bus + } } fn write_byte(&mut self, port: u16, value: u8) { @@ -75,7 +184,13 @@ impl CrtController for HitachiHd6845s { } } - fn step(&mut self) { + fn step(&mut self, master_clock: MasterClockTick) { + self.master_clock = master_clock; + + let horizontal_counter_was = self.horizontal_counter; + let scan_line_was = self.scan_line_counter; + let character_row_was = self.character_row_counter; + self.horizontal_counter += 1; if self.horizontal_counter > self.registers[Register::HorizontalTotal as usize] { @@ -93,11 +208,66 @@ impl CrtController for HitachiHd6845s { self.character_row_counter = 0; } + self.emit_debug_event( + CrtcDebugEvent::CountersChanged { + character_row_is: self.character_row_counter, + character_row_was, + scan_line_is: self.scan_line_counter, + scan_line_was, + horizontal_counter_is: self.horizontal_counter, + horizontal_counter_was, + }, + master_clock, + ); + if self.horizontal_counter == 0 && self.character_row_counter == 0 { self.display_start_address = ((self.registers[Register::DisplayStartAddressHigh as usize] as u16) << 8) + self.registers[Register::DisplayStartAddressLow as usize] as u16; } + + // Check for sync state changes + let new_hsync = self.read_horizontal_sync(); + let new_vsync = self.read_vertical_sync(); + let new_display_enabled = self.read_display_enabled(); + let new_address = self.read_address(); + + if new_hsync != self.previous_hsync { + self.emit_debug_event( + CrtcDebugEvent::HorizontalSync { enabled: new_hsync }, + master_clock, + ); + self.previous_hsync = new_hsync; + } + + if new_vsync != self.previous_vsync { + self.emit_debug_event( + CrtcDebugEvent::VerticalSync { enabled: new_vsync }, + master_clock, + ); + self.previous_vsync = new_vsync; + } + + if new_display_enabled != self.previous_display_enabled { + self.emit_debug_event( + CrtcDebugEvent::DisplayEnableChanged { + enabled: new_display_enabled, + }, + master_clock, + ); + self.previous_display_enabled = new_display_enabled; + } + + if new_address != self.previous_address { + self.emit_debug_event( + CrtcDebugEvent::AddressChanged { + is: new_address, + was: self.previous_address, + }, + master_clock, + ); + self.previous_address = new_address; + } } fn read_address(&self) -> usize { @@ -150,6 +320,16 @@ impl Default for AnyCrtController { } } +impl Snapshottable for AnyCrtController { + type View = CrtcDebugView; + + fn debug_view(&self) -> Self::View { + match self { + AnyCrtController::HitachiHd6845s(crtc) => crtc.debug_view(), + } + } +} + impl CrtController for AnyCrtController { fn read_byte(&mut self, port: u16) -> u8 { match self { @@ -163,9 +343,9 @@ impl CrtController for AnyCrtController { } } - fn step(&mut self) { + fn step(&mut self, master_clock: MasterClockTick) { match self { - AnyCrtController::HitachiHd6845s(crtc) => crtc.step(), + AnyCrtController::HitachiHd6845s(crtc) => crtc.step(master_clock), } } diff --git a/ronald-core/src/system/bus/fdc.rs b/ronald-core/src/system/bus/fdc.rs index bf0d971..a173e0a 100644 --- a/ronald-core/src/system/bus/fdc.rs +++ b/ronald-core/src/system/bus/fdc.rs @@ -144,7 +144,7 @@ impl FloppyDiskController { match self.phase { Phase::Execution => { let data = if let Some(data) = self.data_buffer.pop_front() { - log::trace!("Reading data from FDC: {data:#04x}"); + log::trace!("Reading data from FDC: {data:#04X}"); data } else { unreachable!() @@ -159,7 +159,7 @@ impl FloppyDiskController { } Phase::Result => { let result = if let Some(result) = self.result_buffer.pop_front() { - log::debug!("Reading result from FDC: {result:#04x}"); + log::debug!("Reading result from FDC: {result:#04X}"); result } else { // TODO: we hit this if no disk is loaded and CAT is executed @@ -223,13 +223,13 @@ impl FloppyDiskController { }, _ => { log::error!( - "FDC write outside command phase using port {port:#06x}: {value:#010b}" + "FDC write outside command phase using port {port:#06X}: {value:#010b}" ); unimplemented!(); } }, _ => { - log::error!("Unexpected FDC write using port {port:#06x}: {value:#010b}"); + log::error!("Unexpected FDC write using port {port:#06X}: {value:#010b}"); unreachable!(); } } diff --git a/ronald-core/src/system/bus/gate_array.rs b/ronald-core/src/system/bus/gate_array.rs index 19fe64d..d001a55 100644 --- a/ronald-core/src/system/bus/gate_array.rs +++ b/ronald-core/src/system/bus/gate_array.rs @@ -1,7 +1,13 @@ use serde::{Deserialize, Serialize}; +use crate::debug::event::GateArrayDebugEvent; +use crate::debug::view::GateArrayDebugView; +use crate::debug::DebugSource; +use crate::debug::Debuggable; +use crate::debug::Snapshottable; use crate::system::bus::crtc; use crate::system::bus::screen; +use crate::system::clock::MasterClockTick; use crate::system::memory::MemManage; use crate::system::memory::MemRead; use crate::VideoSink; @@ -15,6 +21,7 @@ pub trait GateArray: Default { memory: &mut (impl MemRead + MemManage), screen: &mut screen::Screen, video: &mut impl VideoSink, + master_clock: MasterClockTick, ) -> bool; } @@ -22,21 +29,22 @@ pub trait GateArray: Default { #[serde(rename_all = "camelCase")] pub struct Amstrad40007 { current_screen_mode: u8, - requested_screen_mode: u8, + requested_screen_mode: Option, hsync_active: bool, vsync_active: bool, - hsyncs_since_last_vsync: u8, + hsyncs_since_last_vsync: u16, interrupt_counter: u8, hold_interrupt: bool, selected_pen: usize, pen_colors: Vec, + master_clock: MasterClockTick, } impl Default for Amstrad40007 { fn default() -> Self { Amstrad40007 { current_screen_mode: 0, - requested_screen_mode: 0, + requested_screen_mode: None, hsync_active: false, vsync_active: false, hsyncs_since_last_vsync: 0, @@ -44,6 +52,7 @@ impl Default for Amstrad40007 { hold_interrupt: false, selected_pen: 0, pen_colors: vec![0; 17], // 16 colors + border + master_clock: MasterClockTick::default(), } } } @@ -72,13 +81,28 @@ impl Amstrad40007 { generate_interrupt = self.interrupt_counter & 0x20 == 0; } + if generate_interrupt { + self.emit_debug_event(GateArrayDebugEvent::InterruptGenerated, self.master_clock); + } + generate_interrupt } fn update_screen_mode(&mut self, crtc: &impl crtc::CrtController) { if !self.hsync_active && crtc.read_horizontal_sync() { - self.current_screen_mode = self.requested_screen_mode; - log::trace!("New screen mode: {}", self.current_screen_mode); + if let Some(requested) = self.requested_screen_mode.take() { + let was = self.current_screen_mode; + self.current_screen_mode = requested; + log::trace!("New screen mode: {}", self.current_screen_mode); + self.emit_debug_event( + GateArrayDebugEvent::ScreenModeChanged { + is: self.current_screen_mode, + was, + applied: true, + }, + self.master_clock, + ); + } } } @@ -171,6 +195,12 @@ impl GateArray for Amstrad40007 { } else { self.selected_pen = 0x10; // select border } + self.emit_debug_event( + GateArrayDebugEvent::PenSelected { + pen: self.selected_pen, + }, + self.master_clock, + ); } 1 => { log::trace!( @@ -179,13 +209,47 @@ impl GateArray for Amstrad40007 { value, value & 0x1f ); + let was = self.pen_colors[self.selected_pen]; self.pen_colors[self.selected_pen] = value & 0x1f; + self.emit_debug_event( + GateArrayDebugEvent::PenColorChanged { + pen: self.selected_pen, + is: value & 0x1f, + was, + }, + self.master_clock, + ); } 2 => { - self.requested_screen_mode = value & 0x03; + let new_mode = value & 0x03; + let was = self + .requested_screen_mode + .unwrap_or(self.current_screen_mode); + self.requested_screen_mode = Some(new_mode); - memory.enable_lower_rom(value & 0x04 == 0); - memory.enable_upper_rom(value & 0x08 == 0); + // Emit requested event + self.emit_debug_event( + GateArrayDebugEvent::ScreenModeChanged { + is: new_mode, + was, + applied: false, + }, + self.master_clock, + ); + + let lower_rom_enabled = value & 0x04 == 0; + let upper_rom_enabled = value & 0x08 == 0; + + memory.enable_lower_rom(lower_rom_enabled); + memory.enable_upper_rom(upper_rom_enabled); + + self.emit_debug_event( + GateArrayDebugEvent::RomConfigChanged { + lower_rom_enabled, + upper_rom_enabled, + }, + self.master_clock, + ); if value & 0x10 != 0 { self.interrupt_counter = 0; @@ -213,7 +277,10 @@ impl GateArray for Amstrad40007 { memory: &mut (impl MemRead + MemManage), screen: &mut screen::Screen, video: &mut impl VideoSink, + master_clock: MasterClockTick, ) -> bool { + self.master_clock = master_clock; + let generate_interrupt = self.update_interrupt_counter(crtc); self.update_screen_mode(crtc); self.write_to_screen(crtc, memory, screen, video); @@ -225,6 +292,29 @@ impl GateArray for Amstrad40007 { } } +impl Snapshottable for Amstrad40007 { + type View = GateArrayDebugView; + + fn debug_view(&self) -> Self::View { + GateArrayDebugView { + current_screen_mode: self.current_screen_mode, + requested_screen_mode: self.requested_screen_mode, + hsync_active: self.hsync_active, + vsync_active: self.vsync_active, + hsyncs_since_last_vsync: self.hsyncs_since_last_vsync, + interrupt_counter: self.interrupt_counter, + hold_interrupt: self.hold_interrupt, + selected_pen: self.selected_pen, + pen_colors: self.pen_colors.clone(), + } + } +} + +impl Debuggable for Amstrad40007 { + const SOURCE: DebugSource = DebugSource::GateArray; + type Event = GateArrayDebugEvent; +} + #[derive(Serialize, Deserialize)] pub enum AnyGateArray { Amstrad40007(Amstrad40007), @@ -255,9 +345,22 @@ impl GateArray for AnyGateArray { memory: &mut (impl MemRead + MemManage), screen: &mut screen::Screen, video: &mut impl VideoSink, + master_clock: MasterClockTick, ) -> bool { match self { - AnyGateArray::Amstrad40007(gate_array) => gate_array.step(crtc, memory, screen, video), + AnyGateArray::Amstrad40007(gate_array) => { + gate_array.step(crtc, memory, screen, video, master_clock) + } + } + } +} + +impl Snapshottable for AnyGateArray { + type View = GateArrayDebugView; + + fn debug_view(&self) -> Self::View { + match self { + AnyGateArray::Amstrad40007(gate_array) => gate_array.debug_view(), } } } diff --git a/ronald-core/src/system/bus/psg.rs b/ronald-core/src/system/bus/psg.rs index bee8847..6027ecb 100644 --- a/ronald-core/src/system/bus/psg.rs +++ b/ronald-core/src/system/bus/psg.rs @@ -27,7 +27,7 @@ impl SoundGenerator { }, 2 => { log::trace!( - "Writing to PSG register {:#04x}: {:#04x}", + "Writing to PSG register {:#04X}: {:#04X}", self.selected_register, self.buffer ); diff --git a/ronald-core/src/system/bus/screen.rs b/ronald-core/src/system/bus/screen.rs index ae039ee..ff2b01b 100644 --- a/ronald-core/src/system/bus/screen.rs +++ b/ronald-core/src/system/bus/screen.rs @@ -1,78 +1,14 @@ use serde::{Deserialize, Serialize}; -use crate::constants::{SCREEN_BUFFER_HEIGHT, SCREEN_BUFFER_WIDTH}; +use crate::constants::{ + FIRMWARE_COLORS, HARDWARE_TO_FIRMWARE_COLORS, SCREEN_BUFFER_HEIGHT, SCREEN_BUFFER_WIDTH, +}; use crate::VideoSink; const VIRTUAL_BUFFER_WIDTH: usize = 64 * 16; const VIRTUAL_BUFFER_HEIGHT: usize = 39 * 16; const BORDER_WIDTH: usize = 4 * 16; -#[allow(clippy::identity_op, clippy::eq_op)] -const FIRMWARE_COLORS: [[u8; 4]; 27] = [ - [0x00, 0x00, 0x00, 0xff], // 0 - [0x00, 0x00, 0x80, 0xff], // 1 - [0x00, 0x00, 0xff, 0xff], // 2 - [0x80, 0x00, 0x00, 0xff], // 3 - [0x80, 0x00, 0x80, 0xff], // 4 - [0x80, 0x00, 0xff, 0xff], // 5 - [0xff, 0x00, 0x00, 0xff], // 6 - [0xff, 0x00, 0x80, 0xff], // 7 - [0xff, 0x00, 0xff, 0xff], // 8 - [0x00, 0x80, 0x00, 0xff], // 9 - [0x00, 0x80, 0x80, 0xff], // 10 - [0x00, 0x80, 0xff, 0xff], // 11 - [0x80, 0x80, 0x00, 0xff], // 12 - [0x80, 0x80, 0x80, 0xff], // 13 - [0x80, 0x80, 0xff, 0xff], // 14 - [0xff, 0x80, 0x00, 0xff], // 15 - [0xff, 0x80, 0x80, 0xff], // 16 - [0xff, 0x80, 0xff, 0xff], // 17 - [0x00, 0xff, 0x00, 0xff], // 18 - [0x00, 0xff, 0x80, 0xff], // 19 - [0x00, 0xff, 0xff, 0xff], // 20 - [0x80, 0xff, 0x00, 0xff], // 21 - [0x80, 0xff, 0x80, 0xff], // 22 - [0x80, 0xff, 0xff, 0xff], // 23 - [0xff, 0xff, 0x00, 0xff], // 24 - [0xff, 0xff, 0x80, 0xff], // 25 - [0xff, 0xff, 0xff, 0xff], // 26 -]; - -const HARDWARE_TO_FIRMWARE_COLORS: [usize; 32] = [ - 13, // 0 (0x40) - 13, // 1 (0x41) - 19, // 2 (0x42) - 25, // 3 (0x43) - 1, // 4 (0x44) - 7, // 5 (0x45) - 10, // 6 (0x46) - 16, // 7 (0x47) - 7, // 8 (0x48) - 25, // 9 (0x49) - 24, // 10 (0x4a) - 26, // 11 (0x4b) - 6, // 12 (0x4c) - 8, // 13 (0x4d) - 15, // 14 (0x4e) - 17, // 15 (0x4f) - 1, // 16 (0x50) - 19, // 17 (0x51) - 18, // 18 (0x52) - 20, // 19 (0x53) - 0, // 20 (0x54) - 2, // 21 (0x55) - 9, // 22 (0x56) - 11, // 23 (0x57) - 4, // 24 (0x58) - 22, // 25 (0x59) - 21, // 26 (0x5a) - 23, // 27 (0x5b) - 3, // 28 (0x5c) - 5, // 29 (0x5d) - 12, // 30 (0x5e) - 14, // 31 (0x5f) -]; - #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Screen { diff --git a/ronald-core/src/system/clock.rs b/ronald-core/src/system/clock.rs new file mode 100644 index 0000000..9e96d72 --- /dev/null +++ b/ronald-core/src/system/clock.rs @@ -0,0 +1,50 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +pub struct MasterClockTick(u64); + +impl MasterClockTick { + pub fn value(self) -> u64 { + self.0 + } +} + +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct MasterClock { + current: MasterClockTick, +} + +impl MasterClock { + pub fn current(&self) -> MasterClockTick { + self.current + } + + pub fn step(&mut self, cycles: u64) { + self.current.0 += cycles; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_master_clock() { + let mut clock = MasterClock::default(); + assert_eq!(clock.current().value(), 0); + clock.step(1); + assert_eq!(clock.current().value(), 1); + clock.step(5); + assert_eq!(clock.current().value(), 6); + } + + #[test] + fn test_master_clock_tick() { + let tick1 = MasterClockTick(100); + let tick2 = MasterClockTick(200); + + assert!(tick1 < tick2); + assert_eq!(tick1.value(), 100); + assert_eq!(tick2.value(), 200); + } +} diff --git a/ronald-core/src/system/cpu.rs b/ronald-core/src/system/cpu.rs index 3bfee46..70ab638 100644 --- a/ronald-core/src/system/cpu.rs +++ b/ronald-core/src/system/cpu.rs @@ -2,11 +2,18 @@ use std::fmt; use serde::{Deserialize, Serialize}; +use crate::debug::event::CpuDebugEvent; +use crate::debug::view::CpuDebugView; +use crate::debug::{DebugSource, Debuggable, Snapshottable}; use crate::system::bus::Bus; -use crate::system::instruction::{Decoder, Instruction, InterruptMode, JumpTest, Operand}; +use crate::system::instruction::{ + DecodedInstruction, Decoder, Instruction, InterruptMode, JumpTest, Operand, +}; use crate::system::memory::{MemManage, MemRead, MemWrite}; +use crate::system::MasterClockTick; #[allow(clippy::upper_case_acronyms)] // Registers are names as in the CPU manual +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub enum Register8 { A, F, @@ -24,6 +31,7 @@ pub enum Register8 { IYL, } +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub enum Register16 { AF, BC, @@ -36,17 +44,25 @@ pub enum Register16 { } #[derive(Clone, Serialize, Deserialize)] -pub struct RegisterFile { +struct RegisterFile { #[serde(rename = "registers")] data: Vec, + master_clock: MasterClockTick, } impl RegisterFile { fn new() -> RegisterFile { - RegisterFile { data: vec![0; 14] } + RegisterFile { + data: vec![0; 14], + master_clock: MasterClockTick::default(), + } + } + + fn step(&mut self, master_clock: MasterClockTick) { + self.master_clock = master_clock; } - pub fn read_byte(&self, register: &Register8) -> u8 { + fn read_byte(&self, register: &Register8) -> u8 { let value = match register { Register8::A => self.data[0] >> 8, Register8::F => self.data[0] & 0xff, @@ -73,51 +89,273 @@ impl RegisterFile { match register { Register8::A => { + let was = self.data[0]; self.data[0] = (value << 8) + (self.data[0] & 0xff); + self.emit_debug_event( + CpuDebugEvent::Register8Written { + register: Register8::A, + is: value as u8, + was: (was >> 8) as u8, + }, + self.master_clock, + ); + self.emit_debug_event( + CpuDebugEvent::Register16Written { + register: Register16::AF, + is: self.data[0], + was, + }, + self.master_clock, + ); } Register8::F => { + let was = self.data[0]; self.data[0] = (self.data[0] & 0xff00) + value; + self.emit_debug_event( + CpuDebugEvent::Register8Written { + register: Register8::F, + is: value as u8, + was: (was & 0xff) as u8, + }, + self.master_clock, + ); + self.emit_debug_event( + CpuDebugEvent::Register16Written { + register: Register16::AF, + is: self.data[0], + was, + }, + self.master_clock, + ); } Register8::B => { + let was = self.data[1]; self.data[1] = (value << 8) + (self.data[1] & 0xff); + self.emit_debug_event( + CpuDebugEvent::Register8Written { + register: Register8::B, + is: value as u8, + was: (was >> 8) as u8, + }, + self.master_clock, + ); + self.emit_debug_event( + CpuDebugEvent::Register16Written { + register: Register16::BC, + is: self.data[1], + was, + }, + self.master_clock, + ); } Register8::C => { + let was = self.data[1]; self.data[1] = (self.data[1] & 0xff00) + value; + self.emit_debug_event( + CpuDebugEvent::Register8Written { + register: Register8::C, + is: value as u8, + was: (was & 0xff) as u8, + }, + self.master_clock, + ); + self.emit_debug_event( + CpuDebugEvent::Register16Written { + register: Register16::BC, + is: self.data[1], + was, + }, + self.master_clock, + ); } Register8::D => { + let was = self.data[2]; self.data[2] = (value << 8) + (self.data[2] & 0xff); + self.emit_debug_event( + CpuDebugEvent::Register8Written { + register: Register8::D, + is: value as u8, + was: (was >> 8) as u8, + }, + self.master_clock, + ); + self.emit_debug_event( + CpuDebugEvent::Register16Written { + register: Register16::DE, + is: self.data[2], + was, + }, + self.master_clock, + ); } Register8::E => { + let was = self.data[2]; self.data[2] = (self.data[2] & 0xff00) + value; + self.emit_debug_event( + CpuDebugEvent::Register8Written { + register: Register8::E, + is: value as u8, + was: (was & 0xff) as u8, + }, + self.master_clock, + ); + self.emit_debug_event( + CpuDebugEvent::Register16Written { + register: Register16::DE, + is: self.data[2], + was, + }, + self.master_clock, + ); } Register8::H => { + let was = self.data[3]; self.data[3] = (value << 8) + (self.data[3] & 0xff); + self.emit_debug_event( + CpuDebugEvent::Register8Written { + register: Register8::H, + is: value as u8, + was: (was >> 8) as u8, + }, + self.master_clock, + ); + self.emit_debug_event( + CpuDebugEvent::Register16Written { + register: Register16::HL, + is: self.data[3], + was, + }, + self.master_clock, + ); } Register8::L => { + let was = self.data[3]; self.data[3] = (self.data[3] & 0xff00) + value; + self.emit_debug_event( + CpuDebugEvent::Register8Written { + register: Register8::L, + is: value as u8, + was: (was & 0xff) as u8, + }, + self.master_clock, + ); + self.emit_debug_event( + CpuDebugEvent::Register16Written { + register: Register16::HL, + is: self.data[3], + was, + }, + self.master_clock, + ); } Register8::I => { + let was = self.data[8]; self.data[8] = (self.data[8] & 0xff00) + value; + self.emit_debug_event( + CpuDebugEvent::Register8Written { + register: Register8::I, + is: value as u8, + was: (was & 0xff) as u8, + }, + self.master_clock, + ); } Register8::R => { + let was = self.data[9]; self.data[9] = (self.data[9] & 0xff00) + value; + self.emit_debug_event( + CpuDebugEvent::Register8Written { + register: Register8::R, + is: value as u8, + was: (was & 0xff) as u8, + }, + self.master_clock, + ); } Register8::IXH => { + let was = self.data[10]; self.data[10] = (value << 8) + (self.data[10] & 0xff); + self.emit_debug_event( + CpuDebugEvent::Register8Written { + register: Register8::IXH, + is: value as u8, + was: (was >> 8) as u8, + }, + self.master_clock, + ); + self.emit_debug_event( + CpuDebugEvent::Register16Written { + register: Register16::IX, + is: self.data[10], + was, + }, + self.master_clock, + ); } Register8::IXL => { + let was = self.data[10]; self.data[10] = (self.data[10] & 0xff00) + value; + self.emit_debug_event( + CpuDebugEvent::Register8Written { + register: Register8::IXL, + is: value as u8, + was: (was & 0xff) as u8, + }, + self.master_clock, + ); + self.emit_debug_event( + CpuDebugEvent::Register16Written { + register: Register16::IX, + is: self.data[10], + was, + }, + self.master_clock, + ); } Register8::IYH => { + let was = self.data[11]; self.data[11] = (value << 8) + (self.data[11] & 0xff); + self.emit_debug_event( + CpuDebugEvent::Register8Written { + register: Register8::IYH, + is: value as u8, + was: (was >> 8) as u8, + }, + self.master_clock, + ); + self.emit_debug_event( + CpuDebugEvent::Register16Written { + register: Register16::IY, + is: self.data[11], + was, + }, + self.master_clock, + ); } Register8::IYL => { + let was = self.data[11]; self.data[11] = (self.data[11] & 0xff00) + value; + self.emit_debug_event( + CpuDebugEvent::Register8Written { + register: Register8::IYL, + is: value as u8, + was: (was & 0xff) as u8, + }, + self.master_clock, + ); + self.emit_debug_event( + CpuDebugEvent::Register16Written { + register: Register16::IY, + is: self.data[11], + was, + }, + self.master_clock, + ); } } } - pub fn read_word(&self, register: &Register16) -> u16 { + fn read_word(&self, register: &Register16) -> u16 { match register { Register16::AF => self.data[0], Register16::BC => self.data[1], @@ -133,38 +371,182 @@ impl RegisterFile { fn write_word(&mut self, register: &Register16, value: u16) { match register { Register16::AF => { + let was = self.data[0]; self.data[0] = value; + self.emit_debug_event( + CpuDebugEvent::Register16Written { + register: Register16::AF, + is: self.data[0], + was, + }, + self.master_clock, + ); } Register16::BC => { + let was = self.data[1]; self.data[1] = value; + self.emit_debug_event( + CpuDebugEvent::Register16Written { + register: Register16::BC, + is: self.data[1], + was, + }, + self.master_clock, + ); } Register16::DE => { + let was = self.data[2]; self.data[2] = value; + self.emit_debug_event( + CpuDebugEvent::Register16Written { + register: Register16::DE, + is: self.data[2], + was, + }, + self.master_clock, + ); } Register16::HL => { + let was = self.data[3]; self.data[3] = value; + self.emit_debug_event( + CpuDebugEvent::Register16Written { + register: Register16::HL, + is: self.data[3], + was, + }, + self.master_clock, + ); } Register16::IX => { + let was = self.data[10]; self.data[10] = value; + self.emit_debug_event( + CpuDebugEvent::Register16Written { + register: Register16::IX, + is: self.data[10], + was, + }, + self.master_clock, + ); } Register16::IY => { + let was = self.data[11]; self.data[11] = value; + self.emit_debug_event( + CpuDebugEvent::Register16Written { + register: Register16::IY, + is: self.data[11], + was, + }, + self.master_clock, + ); } Register16::SP => { + let was = self.data[12]; self.data[12] = value; + self.emit_debug_event( + CpuDebugEvent::Register16Written { + register: Register16::SP, + is: self.data[12], + was, + }, + self.master_clock, + ); } Register16::PC => { + let was = self.data[13]; self.data[13] = value; + self.emit_debug_event( + CpuDebugEvent::Register16Written { + register: Register16::PC, + is: self.data[13], + was, + }, + self.master_clock, + ); } } } fn swap_word(&mut self, register: &Register16) { match register { - Register16::AF => self.data.swap(0, 4), - Register16::BC => self.data.swap(1, 5), - Register16::DE => self.data.swap(2, 6), - Register16::HL => self.data.swap(3, 7), + Register16::AF => { + self.data.swap(0, 4); + self.emit_debug_event( + CpuDebugEvent::Register16Written { + register: Register16::AF, + is: self.data[0], + was: self.data[4], + }, + self.master_clock, + ); + self.emit_debug_event( + CpuDebugEvent::ShadowRegister16Written { + register: Register16::AF, + is: self.data[4], + was: self.data[0], + }, + self.master_clock, + ); + } + Register16::BC => { + self.data.swap(1, 5); + self.emit_debug_event( + CpuDebugEvent::Register16Written { + register: Register16::BC, + is: self.data[1], + was: self.data[5], + }, + self.master_clock, + ); + self.emit_debug_event( + CpuDebugEvent::ShadowRegister16Written { + register: Register16::BC, + is: self.data[5], + was: self.data[1], + }, + self.master_clock, + ); + } + Register16::DE => { + self.data.swap(2, 6); + self.emit_debug_event( + CpuDebugEvent::Register16Written { + register: Register16::DE, + is: self.data[2], + was: self.data[6], + }, + self.master_clock, + ); + self.emit_debug_event( + CpuDebugEvent::ShadowRegister16Written { + register: Register16::DE, + is: self.data[6], + was: self.data[2], + }, + self.master_clock, + ); + } + Register16::HL => { + self.data.swap(3, 7); + self.emit_debug_event( + CpuDebugEvent::Register16Written { + register: Register16::HL, + is: self.data[3], + was: self.data[7], + }, + self.master_clock, + ); + self.emit_debug_event( + CpuDebugEvent::ShadowRegister16Written { + register: Register16::HL, + is: self.data[7], + was: self.data[3], + }, + self.master_clock, + ); + } _ => unreachable!(), } } @@ -172,23 +554,118 @@ impl RegisterFile { impl fmt::Display for RegisterFile { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "AF: {:#06x} ", self.data[0])?; - writeln!(f, "AF': {:#06x}", self.data[4])?; - write!(f, "BC: {:#06x} ", self.data[1])?; - writeln!(f, "BC': {:#06x}", self.data[5])?; - write!(f, "DE: {:#06x} ", self.data[2])?; - writeln!(f, "DE': {:#06x}", self.data[6])?; - write!(f, "HL: {:#06x} ", self.data[3])?; - writeln!(f, "HL': {:#06x}", self.data[7])?; - writeln!(f, " I: {:#04x}", self.data[8] & 0xff)?; - writeln!(f, " R: {:#04x}", self.data[9] & 0xff)?; - writeln!(f, "IX: {:#06x}", self.data[10])?; - writeln!(f, "IY: {:#06x}", self.data[11])?; - writeln!(f, "SP: {:#06x}", self.data[12])?; - writeln!(f, "PC: {:#06x}", self.data[13]) + write!(f, "AF: {:#06X} ", self.data[0])?; + writeln!(f, "AF': {:#06X}", self.data[4])?; + write!(f, "BC: {:#06X} ", self.data[1])?; + writeln!(f, "BC': {:#06X}", self.data[5])?; + write!(f, "DE: {:#06X} ", self.data[2])?; + writeln!(f, "DE': {:#06X}", self.data[6])?; + write!(f, "HL: {:#06X} ", self.data[3])?; + writeln!(f, "HL': {:#06X}", self.data[7])?; + writeln!(f, " I: {:#04X}", self.data[8] & 0xff)?; + writeln!(f, " R: {:#04X}", self.data[9] & 0xff)?; + writeln!(f, "IX: {:#06X}", self.data[10])?; + writeln!(f, "IY: {:#06X}", self.data[11])?; + writeln!(f, "SP: {:#06X}", self.data[12])?; + writeln!(f, "PC: {:#06X}", self.data[13]) } } +struct RegisterFileDebugView { + pub register_a: u8, + pub register_f: u8, + pub register_b: u8, + pub register_c: u8, + pub register_d: u8, + pub register_e: u8, + pub register_h: u8, + pub register_l: u8, + pub shadow_register_a: u8, + pub shadow_register_f: u8, + pub shadow_register_b: u8, + pub shadow_register_c: u8, + pub shadow_register_d: u8, + pub shadow_register_e: u8, + pub shadow_register_h: u8, + pub shadow_register_l: u8, + pub register_i: u8, + pub register_r: u8, + pub register_ixh: u8, + pub register_ixl: u8, + pub register_iyh: u8, + pub register_iyl: u8, + pub register_sp: u16, + pub register_pc: u16, +} + +impl Snapshottable for RegisterFile { + type View = RegisterFileDebugView; + + fn debug_view(&self) -> Self::View { + let mut registers = self.clone(); + let register_a = registers.read_byte(&Register8::A); + let register_f = registers.read_byte(&Register8::F); + let register_b = registers.read_byte(&Register8::B); + let register_c = registers.read_byte(&Register8::C); + let register_d = registers.read_byte(&Register8::D); + let register_e = registers.read_byte(&Register8::E); + let register_h = registers.read_byte(&Register8::H); + let register_l = registers.read_byte(&Register8::L); + registers.swap_word(&Register16::AF); + registers.swap_word(&Register16::BC); + registers.swap_word(&Register16::DE); + registers.swap_word(&Register16::HL); + let shadow_register_a = registers.read_byte(&Register8::A); + let shadow_register_f = registers.read_byte(&Register8::F); + let shadow_register_b = registers.read_byte(&Register8::B); + let shadow_register_c = registers.read_byte(&Register8::C); + let shadow_register_d = registers.read_byte(&Register8::D); + let shadow_register_e = registers.read_byte(&Register8::E); + let shadow_register_h = registers.read_byte(&Register8::H); + let shadow_register_l = registers.read_byte(&Register8::L); + let register_i = registers.read_byte(&Register8::I); + let register_r = registers.read_byte(&Register8::R); + let register_ixh = registers.read_byte(&Register8::IXH); + let register_ixl = registers.read_byte(&Register8::IXL); + let register_iyh = registers.read_byte(&Register8::IYH); + let register_iyl = registers.read_byte(&Register8::IYL); + let register_sp = registers.read_word(&Register16::SP); + let register_pc = registers.read_word(&Register16::PC); + + Self::View { + register_a, + register_f, + register_b, + register_c, + register_d, + register_e, + register_h, + register_l, + shadow_register_a, + shadow_register_f, + shadow_register_b, + shadow_register_c, + shadow_register_d, + shadow_register_e, + shadow_register_h, + shadow_register_l, + register_i, + register_r, + register_ixh, + register_ixl, + register_iyh, + register_iyl, + register_sp, + register_pc, + } + } +} + +impl Debuggable for RegisterFile { + const SOURCE: DebugSource = DebugSource::Cpu; + type Event = CpuDebugEvent; +} + enum Flag { Carry, AddSubtract, @@ -216,13 +693,9 @@ pub trait Cpu: Default { &mut self, memory: &mut (impl MemRead + MemWrite + MemManage), bus: &mut impl Bus, - ) -> (u8, bool); + master_clock: MasterClockTick, + ) -> (u8, bool, Option); fn request_interrupt(&mut self); - fn disassemble( - &mut self, - memory: &mut (impl MemRead + MemWrite + MemManage), - count: usize, - ) -> Vec<(u16, String)>; } #[derive(Serialize, Deserialize)] @@ -232,14 +705,15 @@ where D: Decoder, { #[serde(flatten)] - pub registers: RegisterFile, // TODO: make this private - pub decoder: D, // TODO: make this private + registers: RegisterFile, + decoder: D, // TODO: make this private iff1: bool, iff2: bool, halted: bool, interrupt_mode: InterruptMode, enable_interrupt: bool, irq_received: bool, + master_clock: MasterClockTick, } impl Default for ZilogZ80 @@ -268,6 +742,7 @@ where interrupt_mode: InterruptMode::default(), enable_interrupt: false, irq_received: false, + master_clock: MasterClockTick::default(), }; cpu.reset(); @@ -390,6 +865,10 @@ where match self.interrupt_mode { InterruptMode::Mode1 => { log::trace!("Handling interrupt"); + self.emit_debug_event( + CpuDebugEvent::CallFetched { interrupt: true }, + self.master_clock, + ); let old_pc = self.registers.read_word(&Register16::PC); // PC has already been set to next instruction let new_sp = self.registers.read_word(&Register16::SP) - 2; self.registers.write_word(&Register16::SP, new_sp); @@ -417,13 +896,17 @@ where &mut self, memory: &mut (impl MemRead + MemWrite + MemManage), bus: &mut impl Bus, - ) -> (u8, bool) { + master_clock: MasterClockTick, + ) -> (u8, bool, Option) { + self.master_clock = master_clock; + self.registers.step(self.master_clock); + if self.halted { if self.handle_interrupt(memory) { - return (4, true); + return (4, true, None); } - return (1, false); + return (1, false, None); } if self.enable_interrupt { @@ -438,7 +921,7 @@ where let (instruction, next_address) = self.decoder.decode(memory, pc as usize); - log::trace!("{:#06x}: {}", pc, &instruction); + log::trace!("{:#06X}: {}", pc, &instruction); let mut timing_in_nops = instruction.timing(); @@ -559,6 +1042,10 @@ where unreachable!(); } Instruction::Call(jump_test, Operand::Immediate16(address)) => { + self.emit_debug_event( + CpuDebugEvent::CallFetched { interrupt: false }, + self.master_clock, + ); if self.check_jump(jump_test) { let new_sp = self.registers.read_word(&Register16::SP) - 2; self.registers.write_word(&Register16::SP, new_sp); @@ -711,12 +1198,13 @@ where carry = true; } + // TODO: verify overflow behavior and add tests if self.check_flag(Flag::AddSubtract) { half_carry = half_carry && (value & 0xf) < 0x6; - value -= correction; + value = value.wrapping_sub(correction); } else { half_carry = (value & 0xf) > 0x9; - value += correction; + value = value.wrapping_add(correction); } self.registers.write_byte(&Register8::A, value); @@ -1295,6 +1783,10 @@ where unreachable!(); } Instruction::Ret(jump_test) => { + self.emit_debug_event( + CpuDebugEvent::ReturnFetched { interrupt: false }, + self.master_clock, + ); if self.check_jump(jump_test) { let old_sp = self.registers.read_word(&Register16::SP); self.registers.write_word(&Register16::SP, old_sp + 2); @@ -1306,12 +1798,20 @@ where } } Instruction::Reti => { + self.emit_debug_event( + CpuDebugEvent::ReturnFetched { interrupt: true }, + self.master_clock, + ); let old_sp = self.registers.read_word(&Register16::SP); self.registers.write_word(&Register16::SP, old_sp + 2); self.registers .write_word(&Register16::PC, memory.read_word(old_sp as usize)); } Instruction::Retn => { + self.emit_debug_event( + CpuDebugEvent::ReturnFetched { interrupt: true }, + self.master_clock, + ); self.iff1 = self.iff2; let old_sp = self.registers.read_word(&Register16::SP); self.registers.write_word(&Register16::SP, old_sp + 2); @@ -1662,54 +2162,106 @@ where } } + let decoded_instruction = DecodedInstruction { + address: pc, + instruction, + length: next_address - pc as usize, + }; + if !prevent_interrupt && self.handle_interrupt(memory) { - return (timing_in_nops + 4, true); + return (timing_in_nops + 4, true, Some(decoded_instruction)); } - (timing_in_nops, false) + (timing_in_nops, false, Some(decoded_instruction)) } fn request_interrupt(&mut self) { self.irq_received = true; } +} - fn disassemble( - &mut self, - memory: &mut (impl MemRead + MemWrite + MemManage), - count: usize, - ) -> Vec<(u16, String)> { - let mut address = self.registers.read_word(&Register16::PC); - - let mut assembly = Vec::with_capacity(count); - for _ in 0..count { - let (instruction, next_address) = self.decoder.decode(memory, address as usize); - assembly.push((address, format!("{instruction}"))); - address = next_address as u16; +impl Snapshottable for ZilogZ80 +where + D: Decoder, +{ + type View = CpuDebugView; + + fn debug_view(&self) -> Self::View { + let registers = self.registers.debug_view(); + let iff1 = self.iff1; + let iff2 = self.iff2; + let halted = self.halted; + let interrupt_mode = self.interrupt_mode; + let enable_interrupt = self.enable_interrupt; + let irq_received = self.irq_received; + + Self::View { + register_a: registers.register_a, + register_f: registers.register_f, + register_b: registers.register_b, + register_c: registers.register_c, + register_d: registers.register_d, + register_e: registers.register_e, + register_h: registers.register_h, + register_l: registers.register_l, + shadow_register_a: registers.shadow_register_a, + shadow_register_f: registers.shadow_register_f, + shadow_register_b: registers.shadow_register_b, + shadow_register_c: registers.shadow_register_c, + shadow_register_d: registers.shadow_register_d, + shadow_register_e: registers.shadow_register_e, + shadow_register_h: registers.shadow_register_h, + shadow_register_l: registers.shadow_register_l, + register_i: registers.register_i, + register_r: registers.register_r, + register_ixh: registers.register_ixh, + register_ixl: registers.register_ixl, + register_iyh: registers.register_iyh, + register_iyl: registers.register_iyl, + register_sp: registers.register_sp, + register_pc: registers.register_pc, + iff1, + iff2, + halted, + interrupt_mode, + enable_interrupt, + irq_received, } - - assembly } } +impl Debuggable for ZilogZ80 +where + D: Decoder, +{ + const SOURCE: DebugSource = DebugSource::Cpu; + type Event = CpuDebugEvent; +} + #[cfg(test)] mod tests { use std::path::PathBuf; use crate::{ + debug::{event::DebugEvent, record_debug_events, DebugSource, EventSubscription}, system::{ bus::Bus, - instruction::AlgorithmicDecoder, - memory::{MemManage, Ram}, + clock::MasterClock, + instruction::{AlgorithmicDecoder, Instruction, JumpTest, Operand, TestDecoder}, + memory::{MemManage, Ram, TestMemory}, }, AudioSink, VideoSink, }; use super::*; + // helper structs + struct ZexHarness { cpu: ZilogZ80, memory: Ram, bus: BlackHole, + master_clock: MasterClock, } impl ZexHarness { @@ -1722,10 +2274,12 @@ mod tests { cpu: ZilogZ80::new(0x100), memory, bus: BlackHole::new(), + master_clock: MasterClock::default(), } } pub fn emulate(&mut self) -> usize { + record_debug_events(false); let mut output = String::new(); let start = std::time::Instant::now(); @@ -1756,14 +2310,22 @@ mod tests { } _ => unreachable!(), } - let (cycles, _) = - self.cpu.fetch_and_execute(&mut self.memory, &mut self.bus); + let (cycles, _, _) = self.cpu.fetch_and_execute( + &mut self.memory, + &mut self.bus, + self.master_clock.current(), + ); total_cycles += cycles as usize; + self.master_clock.step(cycles as u64); } _ => { - let (cycles, _) = - self.cpu.fetch_and_execute(&mut self.memory, &mut self.bus); + let (cycles, _, _) = self.cpu.fetch_and_execute( + &mut self.memory, + &mut self.bus, + self.master_clock.current(), + ); total_cycles += cycles as usize; + self.master_clock.step(cycles as u64); } } } @@ -1771,9 +2333,10 @@ mod tests { let elapsed_seconds = start.elapsed().as_secs_f64(); println!( - "Executed {total_cycles} in {elapsed_seconds} seconds ({} MHz).", + "Executed {total_cycles} cycles in {elapsed_seconds} seconds ({} MHz).", total_cycles as f64 / 1_000_000.0 / elapsed_seconds ); + record_debug_events(true); output.matches("OK").count() } @@ -1802,6 +2365,7 @@ mod tests { _memory: &mut impl MemManage, _video: &mut impl VideoSink, _audio: &mut impl AudioSink, + _master_clock: MasterClockTick, ) -> bool { unimplemented!(); } @@ -1823,6 +2387,8 @@ mod tests { } } + // tests start here + #[test] #[ignore = "this is an extremely slow test"] fn test_zexdoc_slow() { @@ -1830,4 +2396,714 @@ mod tests { let mut harness = ZexHarness::new(rom); assert_eq!(harness.emulate(), 67); } + + #[test] + fn test_register_file_write_byte_all_registers() { + let test_cases = [ + // (register, test_value, expected_16bit_reg, expected_16bit_value) + (Register8::A, 0x42, Register16::AF, 0x4200), + (Register8::F, 0x80, Register16::AF, 0x0080), + (Register8::B, 0x12, Register16::BC, 0x1200), + (Register8::C, 0x34, Register16::BC, 0x0034), + (Register8::D, 0x56, Register16::DE, 0x5600), + (Register8::E, 0x78, Register16::DE, 0x0078), + (Register8::H, 0x9A, Register16::HL, 0x9A00), + (Register8::L, 0xBC, Register16::HL, 0x00BC), + (Register8::I, 0xDE, Register16::AF, 0x0000), // I doesn't affect 16-bit pairs + (Register8::R, 0xF0, Register16::AF, 0x0000), // R doesn't affect 16-bit pairs + (Register8::IXH, 0x11, Register16::IX, 0x1100), + (Register8::IXL, 0x22, Register16::IX, 0x0022), + (Register8::IYH, 0x33, Register16::IY, 0x3300), + (Register8::IYL, 0x44, Register16::IY, 0x0044), + ]; + + for (register, test_value, expected_16bit_reg, expected_16bit_value) in test_cases { + let mut subscription = EventSubscription::new(DebugSource::Cpu); + + let mut register_file = RegisterFile::new(); + register_file.write_byte(®ister, test_value); + + let event_count = subscription.pending_count(); + let mut has_8bit_event = false; + let mut has_16bit_event = false; + subscription.with_events(|record| { + if matches!((record.source, &record.event), (DebugSource::Cpu, DebugEvent::Cpu(CpuDebugEvent::Register8Written { + register: r, + is: v, + was: 0x00, + })) if *r == register && *v == test_value) { + has_8bit_event = true; + } + + if matches!((record.source, &record.event), (DebugSource::Cpu, DebugEvent::Cpu(CpuDebugEvent::Register16Written { + register: r, + is: v, + was: 0x0000, + })) if *r == expected_16bit_reg && *v == expected_16bit_value) { + has_16bit_event = true; + } + }); + + // Check for 8-bit register change event + assert!( + has_8bit_event, + "Should emit 8-bit register change event for {:?}", + register + ); + + // For I and R registers, no 16-bit event should be emitted + if matches!(register, Register8::I | Register8::R) { + assert_eq!( + event_count, 1, + "I and R registers should only emit 8-bit events" + ); + } else { + // Should emit both 8-bit and 16-bit events + assert_eq!( + event_count, 2, + "Should emit both 8-bit and 16-bit events for {:?}", + register + ); + + // Check for 16-bit register change event + assert!( + has_16bit_event, + "Should emit 16-bit register change event for {:?} -> {:?}", + register, expected_16bit_reg + ); + } + } + } + + #[test] + fn test_register_file_write_word_all_registers() { + let test_cases = [ + (Register16::AF, 0x1234), + (Register16::BC, 0x5678), + (Register16::DE, 0x9ABC), + (Register16::HL, 0xDEF0), + (Register16::IX, 0x1122), + (Register16::IY, 0x3344), + (Register16::SP, 0x5566), + (Register16::PC, 0x7788), + ]; + + for (register, test_value) in test_cases { + let mut subscription = EventSubscription::new(DebugSource::Cpu); + + let mut register_file = RegisterFile::new(); + register_file.write_word(®ister, test_value); + + assert_eq!( + subscription.pending_count(), + 1, + "Should emit only 16-bit register event for {:?}", + register + ); + + // Check for 16-bit register change event + let mut has_16bit_event = false; + subscription.with_events(|record| { + if matches!((record.source, &record.event), (DebugSource::Cpu, DebugEvent::Cpu(CpuDebugEvent::Register16Written { + register: r, + is: v, + was: 0x0000, + })) if *r == register && *v == test_value) { + has_16bit_event = true; + } + }); + assert!( + has_16bit_event, + "Should emit correct 16-bit register change event for {:?}", + register + ); + } + } + + #[test] + fn test_register_file_swap_word_all_registers() { + let test_cases = [ + (Register16::AF, 0x1234, 0x5678), + (Register16::BC, 0x9ABC, 0xDEF0), + (Register16::DE, 0x1122, 0x3344), + (Register16::HL, 0x5566, 0x7788), + ]; + + for (register, main_value, shadow_value) in test_cases { + let mut subscription = EventSubscription::new(DebugSource::Cpu); + + let mut register_file = RegisterFile::new(); + + // Set up initial values - write to main register and shadow register + register_file.write_word(®ister, main_value); + + // Set shadow register value by direct data manipulation for testing + let shadow_index = match register { + Register16::AF => 4, + Register16::BC => 5, + Register16::DE => 6, + Register16::HL => 7, + _ => unreachable!(), + }; + register_file.data[shadow_index] = shadow_value; + + // Clear events from setup + subscription.with_events(|_| {}); + assert!(!subscription.has_pending()); + + // Perform the swap + register_file.swap_word(®ister); + + assert_eq!( + subscription.pending_count(), + 2, + "Should emit both Register16Written and ShadowRegister16Written events for {:?}", + register + ); + + // Check for Register16Written event (main register gets shadow value) + // Check for ShadowRegister16Written event (shadow register gets main value) + let mut has_main_event = false; + let mut has_shadow_event = false; + subscription.with_events(|record| { + if matches!((record.source, &record.event), (DebugSource::Cpu, DebugEvent::Cpu(CpuDebugEvent::Register16Written { + register: r, + is: v, + was: old_v, + })) if *r == register && *v == shadow_value && *old_v == main_value) { + has_main_event = true; + } + + if matches!((record.source, &record.event), (DebugSource::Cpu, DebugEvent::Cpu(CpuDebugEvent::ShadowRegister16Written { + register: r, + is: v, + was: old_v, + })) if *r == register && *v == main_value && *old_v == shadow_value) { + has_shadow_event = true; + } + }); + assert!( + has_main_event, + "Should emit Register16Written event for {:?}", + register + ); + assert!( + has_shadow_event, + "Should emit ShadowRegister16Written event for {:?}", + register + ); + } + } + + #[test] + fn test_call_instruction_debug_event_ordering() { + let mut subscription = EventSubscription::new(DebugSource::Cpu); + + // Create CPU with TestDecoder that returns CALL instruction + let mut cpu: ZilogZ80 = ZilogZ80::new(0x1000); + cpu.decoder = TestDecoder::new(vec![Instruction::Call( + JumpTest::Unconditional, + Operand::Immediate16(0x2000), + )]); + + // Set up stack pointer to avoid underflow + cpu.registers.write_word(&Register16::SP, 0x8000); + + let mut memory = TestMemory::new(vec![]); + let mut bus = BlackHole::new(); + + // Execute the CALL instruction + cpu.fetch_and_execute(&mut memory, &mut bus, MasterClockTick::default()); + + let mut events = Vec::new(); + subscription.with_events(|record| { + events.push(record.event.clone()); + }); + + // Verify that CallFetched event comes before PC change event + let call_event_pos = events.iter().position(|event| { + matches!( + event, + DebugEvent::Cpu(CpuDebugEvent::CallFetched { interrupt: false }) + ) + }); + + let pc_change_pos = events.iter().position(|event| { + matches!( + event, + DebugEvent::Cpu(CpuDebugEvent::Register16Written { + register: Register16::PC, + is: 0x2000, + .. + }) + ) + }); + + assert!( + call_event_pos.is_some(), + "CallFetched event should be emitted" + ); + assert!(pc_change_pos.is_some(), "PC change event should be emitted"); + assert!( + call_event_pos.unwrap() < pc_change_pos.unwrap(), + "CallFetched event should come before PC change event" + ); + } + + #[test] + fn test_conditional_call_instruction_debug_event_ordering_taken() { + let mut subscription = EventSubscription::new(DebugSource::Cpu); + + let mut cpu: ZilogZ80 = ZilogZ80::new(0x1000); + cpu.decoder = TestDecoder::new(vec![Instruction::Call( + JumpTest::Zero, + Operand::Immediate16(0x2000), + )]); + + // Set up stack pointer to avoid underflow + cpu.registers.write_word(&Register16::SP, 0x8000); + + let mut memory = TestMemory::new(vec![]); + let mut bus = BlackHole::new(); + + // Set zero flag to make conditional call taken + cpu.set_flag(Flag::Zero, true); + + // Execute the CALL instruction + cpu.fetch_and_execute(&mut memory, &mut bus, MasterClockTick::default()); + + let mut events = Vec::new(); + subscription.with_events(|record| { + events.push(record.event.clone()); + }); + + // Verify that CallFetched event comes before PC change event + let call_event_pos = events.iter().position(|event| { + matches!( + event, + DebugEvent::Cpu(CpuDebugEvent::CallFetched { interrupt: false }) + ) + }); + + let pc_change_pos = events.iter().position(|event| { + matches!( + event, + DebugEvent::Cpu(CpuDebugEvent::Register16Written { + register: Register16::PC, + is: 0x2000, + .. + }) + ) + }); + + assert!( + call_event_pos.is_some(), + "CallFetched event should be emitted for taken conditional call" + ); + assert!( + pc_change_pos.is_some(), + "PC change event should be emitted for taken conditional call" + ); + assert!( + call_event_pos.unwrap() < pc_change_pos.unwrap(), + "CallFetched event should come before PC change event for taken conditional call" + ); + } + + #[test] + fn test_conditional_call_instruction_debug_event_ordering_not_taken() { + let mut subscription = EventSubscription::new(DebugSource::Cpu); + + let mut cpu: ZilogZ80 = ZilogZ80::new(0x1000); + cpu.decoder = TestDecoder::new(vec![Instruction::Call( + JumpTest::Zero, + Operand::Immediate16(0x2000), + )]); + + // Set up stack pointer to avoid underflow + cpu.registers.write_word(&Register16::SP, 0x8000); + + let mut memory = TestMemory::new(vec![]); + let mut bus = BlackHole::new(); + + // Clear zero flag to make conditional call not taken + cpu.set_flag(Flag::Zero, false); + + // Execute the CALL instruction + cpu.fetch_and_execute(&mut memory, &mut bus, MasterClockTick::default()); + + let mut events = Vec::new(); + subscription.with_events(|record| { + events.push(record.event.clone()); + }); + + // Verify that CallFetched event comes before PC change event + let call_event_pos = events.iter().position(|event| { + matches!( + event, + DebugEvent::Cpu(CpuDebugEvent::CallFetched { interrupt: false }) + ) + }); + + let pc_change_pos = events.iter().position(|event| { + matches!( + event, + DebugEvent::Cpu(CpuDebugEvent::Register16Written { + register: Register16::PC, + is: 0x1001, // Should advance to next instruction + .. + }) + ) + }); + + assert!( + call_event_pos.is_some(), + "CallFetched event should be emitted even for not taken conditional call" + ); + assert!( + pc_change_pos.is_some(), + "PC change event should be emitted for not taken conditional call" + ); + assert!( + call_event_pos.unwrap() < pc_change_pos.unwrap(), + "CallFetched event should come before PC change event for not taken conditional call" + ); + } + + #[test] + fn test_return_instruction_debug_event_ordering() { + let mut subscription = EventSubscription::new(DebugSource::Cpu); + + let mut cpu: ZilogZ80 = ZilogZ80::new(0x2000); + cpu.decoder = TestDecoder::new(vec![Instruction::Ret(JumpTest::Unconditional)]); + + // Set up memory to return 0x1500 when stack is read + let mut memory = TestMemory::new(vec![0x00, 0x15]); // Little endian 0x1500 + let mut bus = BlackHole::new(); + + // Set up stack with return address + cpu.registers.write_word(&Register16::SP, 0x8000); + + // Execute the RET instruction + cpu.fetch_and_execute(&mut memory, &mut bus, MasterClockTick::default()); + + let mut events = Vec::new(); + subscription.with_events(|record| { + events.push(record.event.clone()); + }); + + // Verify that ReturnFetched event comes before PC change event + let return_event_pos = events.iter().position(|event| { + matches!( + event, + DebugEvent::Cpu(CpuDebugEvent::ReturnFetched { interrupt: false }) + ) + }); + + let pc_change_pos = events.iter().position(|event| { + matches!( + event, + DebugEvent::Cpu(CpuDebugEvent::Register16Written { + register: Register16::PC, + is: 0x1500, // Return address from stack + .. + }) + ) + }); + + assert!( + return_event_pos.is_some(), + "ReturnFetched event should be emitted" + ); + assert!(pc_change_pos.is_some(), "PC change event should be emitted"); + assert!( + return_event_pos.unwrap() < pc_change_pos.unwrap(), + "ReturnFetched event should come before PC change event" + ); + } + + #[test] + fn test_conditional_return_instruction_debug_event_ordering_taken() { + let mut subscription = EventSubscription::new(DebugSource::Cpu); + + let mut cpu: ZilogZ80 = ZilogZ80::new(0x2000); + cpu.decoder = TestDecoder::new(vec![Instruction::Ret(JumpTest::Zero)]); + + let mut memory = TestMemory::new(vec![0x00, 0x15]); // Little endian 0x1500 + let mut bus = BlackHole::new(); + + // Set zero flag to make conditional return taken + cpu.set_flag(Flag::Zero, true); + + // Set up stack with return address + cpu.registers.write_word(&Register16::SP, 0x8000); + + // Execute the RET instruction + cpu.fetch_and_execute(&mut memory, &mut bus, MasterClockTick::default()); + + let mut events = Vec::new(); + subscription.with_events(|record| { + events.push(record.event.clone()); + }); + + // Verify that ReturnFetched event comes before PC change event + let return_event_pos = events.iter().position(|event| { + matches!( + event, + DebugEvent::Cpu(CpuDebugEvent::ReturnFetched { interrupt: false }) + ) + }); + + let pc_change_pos = events.iter().position(|event| { + matches!( + event, + DebugEvent::Cpu(CpuDebugEvent::Register16Written { + register: Register16::PC, + is: 0x1500, // Return address from stack + .. + }) + ) + }); + + assert!( + return_event_pos.is_some(), + "ReturnFetched event should be emitted for taken conditional return" + ); + assert!( + pc_change_pos.is_some(), + "PC change event should be emitted for taken conditional return" + ); + assert!( + return_event_pos.unwrap() < pc_change_pos.unwrap(), + "ReturnFetched event should come before PC change event for taken conditional return" + ); + } + + #[test] + fn test_conditional_return_instruction_debug_event_ordering_not_taken() { + let mut subscription = EventSubscription::new(DebugSource::Cpu); + + let mut cpu: ZilogZ80 = ZilogZ80::new(0x2000); + cpu.decoder = TestDecoder::new(vec![Instruction::Ret(JumpTest::Zero)]); + + let mut memory = TestMemory::new(vec![]); + let mut bus = BlackHole::new(); + + // Clear zero flag to make conditional return not taken + cpu.set_flag(Flag::Zero, false); + + // Set up stack with return address + cpu.registers.write_word(&Register16::SP, 0x8000); + + // Execute the RET instruction + cpu.fetch_and_execute(&mut memory, &mut bus, MasterClockTick::default()); + + let mut events = Vec::new(); + subscription.with_events(|record| { + events.push(record.event.clone()); + }); + + // Verify that ReturnFetched event comes before PC change event + let return_event_pos = events.iter().position(|event| { + matches!( + event, + DebugEvent::Cpu(CpuDebugEvent::ReturnFetched { interrupt: false }) + ) + }); + + let pc_change_pos = events.iter().position(|event| { + matches!( + event, + DebugEvent::Cpu(CpuDebugEvent::Register16Written { + register: Register16::PC, + is: 0x2001, // Should advance to next instruction + .. + }) + ) + }); + + assert!( + return_event_pos.is_some(), + "ReturnFetched event should be emitted even for not taken conditional return" + ); + assert!( + pc_change_pos.is_some(), + "PC change event should be emitted for not taken conditional return" + ); + assert!( + return_event_pos.unwrap() < pc_change_pos.unwrap(), + "ReturnFetched event should come before PC change event for not taken conditional return" + ); + } + + #[test] + fn test_reti_instruction_debug_event_ordering() { + let mut subscription = EventSubscription::new(DebugSource::Cpu); + + let mut cpu: ZilogZ80 = ZilogZ80::new(0x2000); + cpu.decoder = TestDecoder::new(vec![Instruction::Reti]); + + let mut memory = TestMemory::new(vec![0x00, 0x15]); // Little endian 0x1500 + let mut bus = BlackHole::new(); + + // Set up stack with return address + cpu.registers.write_word(&Register16::SP, 0x8000); + + // Execute the RETI instruction + cpu.fetch_and_execute(&mut memory, &mut bus, MasterClockTick::default()); + + let mut events = Vec::new(); + subscription.with_events(|record| { + events.push(record.event.clone()); + }); + + // Verify that ReturnFetched event comes before PC change event + let return_event_pos = events.iter().position(|event| { + matches!( + event, + DebugEvent::Cpu(CpuDebugEvent::ReturnFetched { interrupt: true }) + ) + }); + + let pc_change_pos = events.iter().position(|event| { + matches!( + event, + DebugEvent::Cpu(CpuDebugEvent::Register16Written { + register: Register16::PC, + is: 0x1500, // Return address from stack + .. + }) + ) + }); + + assert!( + return_event_pos.is_some(), + "ReturnFetched event should be emitted for RETI" + ); + assert!( + pc_change_pos.is_some(), + "PC change event should be emitted for RETI" + ); + assert!( + return_event_pos.unwrap() < pc_change_pos.unwrap(), + "ReturnFetched event should come before PC change event for RETI" + ); + } + + #[test] + fn test_retn_instruction_debug_event_ordering() { + let mut subscription = EventSubscription::new(DebugSource::Cpu); + + let mut cpu: ZilogZ80 = ZilogZ80::new(0x2000); + cpu.decoder = TestDecoder::new(vec![Instruction::Retn]); + + let mut memory = TestMemory::new(vec![0x00, 0x15]); // Little endian 0x1500 + let mut bus = BlackHole::new(); + + // Set up stack with return address + cpu.registers.write_word(&Register16::SP, 0x8000); + + // Execute the RETN instruction + cpu.fetch_and_execute(&mut memory, &mut bus, MasterClockTick::default()); + + let mut events = Vec::new(); + subscription.with_events(|record| { + events.push(record.event.clone()); + }); + + // Verify that ReturnFetched event comes before PC change event + let return_event_pos = events.iter().position(|event| { + matches!( + event, + DebugEvent::Cpu(CpuDebugEvent::ReturnFetched { interrupt: true }) + ) + }); + + let pc_change_pos = events.iter().position(|event| { + matches!( + event, + DebugEvent::Cpu(CpuDebugEvent::Register16Written { + register: Register16::PC, + is: 0x1500, // Return address from stack + .. + }) + ) + }); + + assert!( + return_event_pos.is_some(), + "ReturnFetched event should be emitted for RETN" + ); + assert!( + pc_change_pos.is_some(), + "PC change event should be emitted for RETN" + ); + assert!( + return_event_pos.unwrap() < pc_change_pos.unwrap(), + "ReturnFetched event should come before PC change event for RETN" + ); + } + + #[test] + fn test_interrupt_handling_debug_event_ordering() { + let mut subscription = EventSubscription::new(DebugSource::Cpu); + + let mut cpu: ZilogZ80 = ZilogZ80::new(0x1000); + cpu.decoder = TestDecoder::new(vec![ + Instruction::Nop, // Simple instruction at current PC + ]); + + // Set up stack pointer to avoid underflow + cpu.registers.write_word(&Register16::SP, 0x8000); + + let mut memory = TestMemory::new(vec![]); + let mut bus = BlackHole::new(); + + // Enable interrupts and set interrupt mode 1 + cpu.iff1 = true; + cpu.interrupt_mode = InterruptMode::Mode1; + + // Request an interrupt + cpu.request_interrupt(); + + // Execute instruction - this should handle the interrupt + cpu.fetch_and_execute(&mut memory, &mut bus, MasterClockTick::default()); + + let mut events = Vec::new(); + subscription.with_events(|record| { + events.push(record.event.clone()); + }); + + // Verify that CallFetched interrupt event comes before PC change event + let call_event_pos = events.iter().position(|event| { + matches!( + event, + DebugEvent::Cpu(CpuDebugEvent::CallFetched { interrupt: true }) + ) + }); + + let pc_change_pos = events.iter().position(|event| { + matches!( + event, + DebugEvent::Cpu(CpuDebugEvent::Register16Written { + register: Register16::PC, + is: 0x0038, // Interrupt vector for mode 1 + .. + }) + ) + }); + + assert!( + call_event_pos.is_some(), + "CallFetched interrupt event should be emitted" + ); + assert!( + pc_change_pos.is_some(), + "PC change event should be emitted for interrupt" + ); + assert!( + call_event_pos.unwrap() < pc_change_pos.unwrap(), + "CallFetched interrupt event should come before PC change event" + ); + } } diff --git a/ronald-core/src/system/instruction.rs b/ronald-core/src/system/instruction.rs index 6179438..4a7b19f 100644 --- a/ronald-core/src/system/instruction.rs +++ b/ronald-core/src/system/instruction.rs @@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize}; use crate::system::cpu; use crate::system::memory::MemRead; +#[derive(Clone, Serialize, Deserialize)] pub enum Operand { Immediate8(u8), Immediate16(u16), @@ -20,8 +21,8 @@ pub enum Operand { impl fmt::Display for Operand { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Operand::Immediate8(value) => write!(f, "{value:#04x}"), - Operand::Immediate16(value) => write!(f, "{value:#06x}"), + Operand::Immediate8(value) => write!(f, "{value:#04X}"), + Operand::Immediate16(value) => write!(f, "{value:#06X}"), Operand::Register8(register) => match register { cpu::Register8::A => write!(f, "a"), cpu::Register8::F => write!(f, "f"), @@ -59,8 +60,8 @@ impl fmt::Display for Operand { cpu::Register16::SP => write!(f, "(sp)"), cpu::Register16::PC => write!(f, "(pc)"), }, - Operand::Direct8(address) => write!(f, "({address:#04x})"), - Operand::Direct16(address) => write!(f, "({address:#06x})"), + Operand::Direct8(address) => write!(f, "({address:#04X})"), + Operand::Direct16(address) => write!(f, "({address:#06X})"), Operand::Indexed(register, displacement) => match register { cpu::Register16::IX => write!(f, "(ix{displacement:+})"), cpu::Register16::IY => write!(f, "(iy{displacement:+})"), @@ -82,6 +83,17 @@ pub enum InterruptMode { Mode2, } +impl fmt::Display for InterruptMode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + InterruptMode::Mode0 => write!(f, "IM 0"), + InterruptMode::Mode1 => write!(f, "IM 1"), + InterruptMode::Mode2 => write!(f, "IM 2"), + } + } +} + +#[derive(Clone, Serialize, Deserialize)] pub enum JumpTest { Unconditional, NonZero, @@ -127,6 +139,7 @@ impl fmt::Display for JumpTest { } } +#[derive(Clone, Serialize, Deserialize)] pub enum Instruction { Adc(Operand, Operand), Add(Operand, Operand), @@ -1613,3 +1626,42 @@ impl AlgorithmicDecoder { } } } + +#[derive(Clone, Serialize, Deserialize)] +pub struct DecodedInstruction { + pub address: u16, + pub instruction: Instruction, + pub length: usize, +} + +#[cfg(test)] +/// A test decoder that can return specific instructions without decoding +#[derive(Default)] +pub struct TestDecoder { + instructions: Vec, + current_index: usize, +} + +#[cfg(test)] +impl TestDecoder { + pub fn new(instructions: Vec) -> Self { + Self { + instructions, + current_index: 0, + } + } +} + +#[cfg(test)] +impl Decoder for TestDecoder { + fn decode(&mut self, _memory: &impl MemRead, address: usize) -> (Instruction, usize) { + if self.current_index < self.instructions.len() { + let instruction = self.instructions[self.current_index].clone(); + self.current_index += 1; + (instruction, address + 1) // Simple increment for next address + } else { + // Return NOP if we run out of instructions + (Instruction::Nop, address + 1) + } + } +} diff --git a/ronald-core/src/system/memory.rs b/ronald-core/src/system/memory.rs index 1bfd06a..e1fa7a5 100644 --- a/ronald-core/src/system/memory.rs +++ b/ronald-core/src/system/memory.rs @@ -1,9 +1,14 @@ use std::collections::HashMap; -use std::fs::*; -use std::io; use serde::{Deserialize, Serialize}; +use crate::debug::event::MemoryDebugEvent; +use crate::debug::view::MemoryDebugView; +use crate::debug::DebugSource; +use crate::debug::Debuggable; +use crate::debug::Snapshottable; +use crate::system::clock::MasterClockTick; + pub trait MemRead { fn read_byte(&self, address: usize) -> u8; @@ -38,34 +43,82 @@ pub trait MemManage { pub struct Rom { #[serde(rename = "rom")] data: Vec, + address_mask: usize, + master_clock: MasterClockTick, } impl Rom { + pub fn step(&mut self, master_clock: MasterClockTick) { + self.master_clock = master_clock; + } + pub fn from_bytes(bytes: &[u8]) -> Rom { // TODO: better error handling // TODO: check ROM size (should be 16k) + let data = bytes.to_vec(); + debug_assert!(data.len().is_power_of_two()); + let address_mask = data.len() - 1; + let master_clock = MasterClockTick::default(); + Rom { - data: bytes.to_vec(), + data, + address_mask, + master_clock, } } } impl MemRead for Rom { fn read_byte(&self, address: usize) -> u8 { - self.data[address] + let address = address & self.address_mask; + let value = self.data[address]; + self.emit_debug_event( + MemoryDebugEvent::MemoryRead { address, value }, + self.master_clock, + ); + + value } } +pub struct RomDebugView { + data: Vec, +} + +impl Snapshottable for Rom { + type View = RomDebugView; + + fn debug_view(&self) -> Self::View { + Self::View { + data: self.data.clone(), + } + } +} + +impl Debuggable for Rom { + const SOURCE: DebugSource = DebugSource::Memory; + type Event = MemoryDebugEvent; +} + #[derive(Serialize, Deserialize)] pub struct Ram { #[serde(rename = "ram")] data: Vec, + address_mask: usize, + master_clock: MasterClockTick, } impl Ram { pub fn new(size: usize) -> Ram { + let data = vec![0; size]; + debug_assert!(data.len().is_power_of_two()); + let address_mask = data.len() - 1; + let master_clock = MasterClockTick::default(); + Ram { - data: vec![0; size], + data, + address_mask, + master_clock, } } @@ -80,20 +133,42 @@ impl Ram { ram } + + pub fn step(&mut self, master_clock: MasterClockTick) { + self.master_clock = master_clock; + } } impl MemRead for Ram { fn read_byte(&self, address: usize) -> u8 { - self.data[address] + let address = address & self.address_mask; + let value = self.data[address]; + self.emit_debug_event( + MemoryDebugEvent::MemoryRead { address, value }, + self.master_clock, + ); + + value } } impl MemWrite for Ram { fn write_byte(&mut self, address: usize, value: u8) { + let address = address & self.address_mask; + let was = self.data[address]; self.data[address] = value; + self.emit_debug_event( + MemoryDebugEvent::MemoryWritten { + address, + is: value, + was, + }, + self.master_clock, + ); } } +// TODO: can we get rid of this empty impl? Currently required for bus writes. impl MemManage for Ram { fn enable_lower_rom(&mut self, enable: bool) {} @@ -104,6 +179,25 @@ impl MemManage for Ram { fn force_ram_read(&mut self, force: bool) {} } +pub struct RamDebugView { + data: Vec, +} + +impl Snapshottable for Ram { + type View = RamDebugView; + + fn debug_view(&self) -> Self::View { + Self::View { + data: self.data.clone(), + } + } +} + +impl Debuggable for Ram { + const SOURCE: DebugSource = DebugSource::Memory; + type Event = MemoryDebugEvent; +} + #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct MemoryCpcX64 { @@ -199,6 +293,57 @@ impl MemManage for MemoryCpcX64 { } } +impl Snapshottable for MemoryCpcX64 { + type View = MemoryDebugView; + + fn debug_view(&self) -> Self::View { + let mut upper_roms = HashMap::new(); + for (key, rom) in &self.upper_roms { + upper_roms.insert(*key, rom.debug_view().data); + } + + let ram = self.ram.debug_view().data; + let ram_extension = vec![]; + let lower_rom = self.lower_rom.debug_view().data; + let lower_rom_enabled = self.lower_rom_enabled; + let selected_upper_rom = self.selected_upper_rom; + let upper_rom_enabled = self.upper_rom_enabled; + + // Create composite RAM view first (just RAM for now, extension RAM not implemented yet) + let composite_ram = ram.clone(); + + // Create composite ROM/RAM view based on composite_ram + let mut composite_rom_ram = composite_ram.clone(); + + if lower_rom_enabled { + composite_rom_ram[0x0000..0x4000].copy_from_slice(&lower_rom); + } + + if upper_rom_enabled { + if let Some(upper_rom_data) = upper_roms.get(&selected_upper_rom) { + composite_rom_ram[0xC000..0x10000].copy_from_slice(upper_rom_data); + } + } + + MemoryDebugView { + ram, + ram_extension, + lower_rom, + lower_rom_enabled, + upper_roms, + selected_upper_rom, + upper_rom_enabled, + composite_rom_ram, + composite_ram, + } + } +} + +impl Debuggable for MemoryCpcX64 { + const SOURCE: DebugSource = DebugSource::Memory; + type Event = MemoryDebugEvent; +} + #[derive(Serialize, Deserialize)] pub struct MemoryCpc6128 { memory: MemoryCpcX64, @@ -256,6 +401,14 @@ impl MemManage for MemoryCpc6128 { } } +impl Snapshottable for MemoryCpc6128 { + type View = MemoryDebugView; + + fn debug_view(&self) -> Self::View { + self.memory.debug_view() + } +} + #[derive(Serialize, Deserialize)] pub enum AnyMemory { CpcX64(MemoryCpcX64), @@ -329,3 +482,74 @@ impl MemManage for AnyMemory { } } } + +impl Snapshottable for AnyMemory { + type View = MemoryDebugView; + + fn debug_view(&self) -> Self::View { + match self { + AnyMemory::CpcX64(memory) => memory.debug_view(), + AnyMemory::Cpc6128(memory) => memory.debug_view(), + } + } +} + +#[cfg(test)] +/// A test memory implementation that returns predefined values and ignores writes +#[derive(Default)] +pub struct TestMemory { + read_values: Vec, + current_index: std::cell::Cell, +} + +#[cfg(test)] +impl TestMemory { + pub fn new(read_values: Vec) -> Self { + use std::cell::Cell; + + Self { + read_values, + current_index: Cell::new(0), + } + } +} + +#[cfg(test)] +impl MemRead for TestMemory { + fn read_byte(&self, _address: usize) -> u8 { + let current_index = self.current_index.get(); + if current_index < self.read_values.len() { + let value = self.read_values[current_index]; + self.current_index.set(current_index + 1); + value + } else { + 0x00 // Default value when no more data + } + } +} + +#[cfg(test)] +impl MemWrite for TestMemory { + fn write_byte(&mut self, _address: usize, _value: u8) { + // Noop - writes are ignored in test memory + } +} + +#[cfg(test)] +impl MemManage for TestMemory { + fn enable_lower_rom(&mut self, _enable: bool) { + // Noop + } + + fn enable_upper_rom(&mut self, _enable: bool) { + // Noop + } + + fn select_upper_rom(&mut self, _upper_rom_nr: u8) { + // Noop + } + + fn force_ram_read(&mut self, _force: bool) { + // Noop + } +} diff --git a/ronald-egui/Cargo.toml b/ronald-egui/Cargo.toml index 9b032d6..d5529ab 100644 --- a/ronald-egui/Cargo.toml +++ b/ronald-egui/Cargo.toml @@ -48,4 +48,7 @@ web-sys = { version = "0.3", features = [ [dev-dependencies] kittest = "0.2" -egui_kittest = { git = "https://github.com/mdm/egui.git", branch = "latest-patched" } +egui_kittest = { git = "https://github.com/mdm/egui.git", branch = "latest-patched", features = [ + "snapshot", + "wgpu", +] } diff --git a/ronald-egui/src/app.rs b/ronald-egui/src/app.rs index 4fbec9a..015f202 100644 --- a/ronald-egui/src/app.rs +++ b/ronald-egui/src/app.rs @@ -1,8 +1,8 @@ use eframe::egui; use serde::{Deserialize, Serialize}; +use web_time::Instant; -use ronald_core::system::AmstradCpc; - +use crate::debug::{CpuDebugWindow, CrtcDebugWindow, GateArrayDebugWindow, MemoryDebugWindow}; use crate::frontend::Frontend; use crate::key_map_editor::KeyMapEditor; use crate::key_mapper::KeyMapper; @@ -10,7 +10,7 @@ use crate::system_config::SystemConfigModal; pub use ronald_core::system::SystemConfig; -pub use crate::key_mapper::{KeyMap, KeyMapStore}; +pub use crate::key_mapper::KeyMapStore; pub use ronald_core::constants::{SCREEN_BUFFER_HEIGHT, SCREEN_BUFFER_WIDTH}; #[derive(Deserialize, Serialize)] @@ -30,6 +30,10 @@ where key_mapper: KeyMapper, #[serde(skip)] system_config_modal: SystemConfigModal, + cpu_debug_window: CpuDebugWindow, + crtc_debug_window: CrtcDebugWindow, + gate_array_debug_window: GateArrayDebugWindow, + memory_debug_window: MemoryDebugWindow, } impl Default for RonaldApp @@ -45,6 +49,10 @@ where key_map_editor: KeyMapEditor::default(), key_mapper: KeyMapper::default(), system_config_modal: SystemConfigModal::default(), + cpu_debug_window: CpuDebugWindow::default(), + crtc_debug_window: CrtcDebugWindow::default(), + gate_array_debug_window: GateArrayDebugWindow::default(), + memory_debug_window: MemoryDebugWindow::default(), } } } @@ -60,7 +68,6 @@ where Default::default() }; - // Apply the saved theme preference cc.egui_ctx .set_theme(egui::Theme::from_dark_mode(app.dark_mode)); @@ -72,6 +79,7 @@ where S: KeyMapStore, { fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { + let start = Instant::now(); egui_extras::install_image_loaders(ctx); self.render_menu_bar(ctx); @@ -80,15 +88,23 @@ where self.render_workbench_mode(ctx); self.key_map_editor.ui(ctx, &mut self.key_mapper); let config_changed = self.system_config_modal.ui(ctx, &mut self.system_config); - if config_changed { - // Recreate frontend with new config - if let Some(render_state) = frame.wgpu_render_state() { - let new_frontend = Frontend::with_config(render_state, &self.system_config); - self.frontend = Some(new_frontend); - } + if config_changed && let Some(render_state) = frame.wgpu_render_state() { + let new_frontend = Frontend::with_config(render_state, &self.system_config); + self.frontend = Some(new_frontend); + } + + if self.workbench + && let Some(frontend) = &mut self.frontend + { + self.cpu_debug_window.ui(ctx, frontend); + self.crtc_debug_window.ui(ctx, frontend); + self.gate_array_debug_window.ui(ctx, frontend); + self.memory_debug_window.ui(ctx, frontend); } ctx.request_repaint(); + let elapsed = Instant::now() - start; + log::debug!("Frame time: {} us", elapsed.as_micros()); } fn save(&mut self, storage: &mut dyn eframe::Storage) { @@ -119,6 +135,40 @@ where ui.close_menu(); } if self.workbench { + ui.separator(); + if ui + .add(egui::Button::new("CPU").selected(self.cpu_debug_window.show)) + .clicked() + { + self.cpu_debug_window.show = !self.cpu_debug_window.show; + ui.close_menu(); + } + if ui + .add( + egui::Button::new("Memory").selected(self.memory_debug_window.show), + ) + .clicked() + { + self.memory_debug_window.show = !self.memory_debug_window.show; + ui.close_menu(); + } + if ui + .add(egui::Button::new("CRTC").selected(self.crtc_debug_window.show)) + .clicked() + { + self.crtc_debug_window.show = !self.crtc_debug_window.show; + ui.close_menu(); + } + if ui + .add( + egui::Button::new("Gate Array") + .selected(self.gate_array_debug_window.show), + ) + .clicked() + { + self.gate_array_debug_window.show = !self.gate_array_debug_window.show; + ui.close_menu(); + } ui.separator(); if ui.button("Organize Windows").clicked() { ui.ctx().memory_mut(|mem| mem.reset_areas()); @@ -219,7 +269,7 @@ where ctx, Some(ui), &mut self.key_mapper, - !self.key_map_editor.show, + !self.key_map_editor.show && !self.system_config_modal.show, ); }, ); @@ -231,7 +281,12 @@ where if let Some(frontend) = &mut self.frontend && self.workbench { - frontend.ui(ctx, None, &mut self.key_mapper, !self.key_map_editor.show); + frontend.ui( + ctx, + None, + &mut self.key_mapper, + !self.key_map_editor.show && !self.system_config_modal.show, + ); } } } diff --git a/ronald-egui/src/colors.rs b/ronald-egui/src/colors.rs new file mode 100644 index 0000000..d896fc1 --- /dev/null +++ b/ronald-egui/src/colors.rs @@ -0,0 +1,12 @@ +pub const BLACK: egui::Color32 = egui::Color32::BLACK; +pub const WHITE: egui::Color32 = egui::Color32::WHITE; +pub const MEDIUM_GRAY: egui::Color32 = egui::Color32::from_gray(120); +pub const DARK_GRAY: egui::Color32 = egui::Color32::from_gray(160); +pub const DARK_RED: egui::Color32 = egui::Color32::from_rgb(200, 50, 50); +pub const LIGHT_RED: egui::Color32 = egui::Color32::from_rgb(255, 100, 100); +pub const DEEP_MAGENTA: egui::Color32 = egui::Color32::from_rgb(220, 20, 120); +pub const DARK_ORANGE: egui::Color32 = egui::Color32::from_rgb(255, 140, 0); +pub const DARK_YELLOW_GOLD: egui::Color32 = egui::Color32::from_rgb(200, 150, 0); +pub const FORREST_GREEN: egui::Color32 = egui::Color32::from_rgb(0, 150, 0); +pub const STEEL_BLUE: egui::Color32 = egui::Color32::from_rgb(70, 130, 180); +pub const DODGER_BLUE: egui::Color32 = egui::Color32::from_rgb(30, 144, 255); diff --git a/ronald-egui/src/debug.rs b/ronald-egui/src/debug.rs new file mode 100644 index 0000000..af515af --- /dev/null +++ b/ronald-egui/src/debug.rs @@ -0,0 +1,45 @@ +mod cpu; +mod crtc; +mod gate_array; +mod memory; + +use ronald_core::{ + debug::{breakpoint::BreakpointManager, view::SystemDebugView}, + system::instruction::DecodedInstruction, +}; + +pub use cpu::CpuDebugWindow; +pub use crtc::CrtcDebugWindow; +pub use gate_array::GateArrayDebugWindow; +pub use memory::MemoryDebugWindow; + +pub trait Debugger { + fn debug_view(&mut self) -> &SystemDebugView; + fn breakpoint_manager(&mut self) -> &mut BreakpointManager; + fn disassemble(&self, start_address: u16, count: usize) -> Vec; +} + +#[cfg(test)] +mod mock { + use super::*; + use ronald_core::Driver; + + #[derive(Default)] + pub struct TestDebugger { + driver: Driver, + } + + impl Debugger for TestDebugger { + fn debug_view(&mut self) -> &SystemDebugView { + self.driver.debug_view() + } + + fn breakpoint_manager(&mut self) -> &mut BreakpointManager { + self.driver.breakpoint_manager() + } + + fn disassemble(&self, start_address: u16, count: usize) -> Vec { + self.driver.disassemble(start_address, count) + } + } +} diff --git a/ronald-egui/src/debug/cpu.rs b/ronald-egui/src/debug/cpu.rs new file mode 100644 index 0000000..9f7156a --- /dev/null +++ b/ronald-egui/src/debug/cpu.rs @@ -0,0 +1,1033 @@ +use eframe::egui; +use serde::{Deserialize, Serialize}; + +use ronald_core::debug::breakpoint::{AnyBreakpoint, Breakpoint}; +use ronald_core::system::cpu::{Register8, Register16 as PrimaryRegister16}; + +use crate::colors; +use crate::debug::Debugger; + +fn default_register8() -> Register8 { + Register8::A +} + +fn default_ui_register16() -> Register16 { + Register16::PC +} + +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +enum Register16 { + PC, + SP, + AF, + BC, + DE, + HL, + IX, + IY, + ShadowAF, + ShadowBC, + ShadowDE, + ShadowHL, +} + +impl Register16 { + fn is_shadow(&self) -> bool { + matches!( + self, + Register16::ShadowAF + | Register16::ShadowBC + | Register16::ShadowDE + | Register16::ShadowHL + ) + } + + fn all_options() -> &'static [Register16] { + &[ + Register16::PC, + Register16::SP, + Register16::AF, + Register16::BC, + Register16::DE, + Register16::HL, + Register16::IX, + Register16::IY, + Register16::ShadowAF, + Register16::ShadowBC, + Register16::ShadowDE, + Register16::ShadowHL, + ] + } +} + +impl std::fmt::Display for Register16 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Register16::PC => write!(f, "PC"), + Register16::SP => write!(f, "SP"), + Register16::AF => write!(f, "AF"), + Register16::BC => write!(f, "BC"), + Register16::DE => write!(f, "DE"), + Register16::HL => write!(f, "HL"), + Register16::IX => write!(f, "IX"), + Register16::IY => write!(f, "IY"), + Register16::ShadowAF => write!(f, "AF'"), + Register16::ShadowBC => write!(f, "BC'"), + Register16::ShadowDE => write!(f, "DE'"), + Register16::ShadowHL => write!(f, "HL'"), + } + } +} + +impl From for PrimaryRegister16 { + fn from(reg: Register16) -> Self { + match reg { + Register16::PC => PrimaryRegister16::PC, + Register16::SP => PrimaryRegister16::SP, + Register16::AF | Register16::ShadowAF => PrimaryRegister16::AF, + Register16::BC | Register16::ShadowBC => PrimaryRegister16::BC, + Register16::DE | Register16::ShadowDE => PrimaryRegister16::DE, + Register16::HL | Register16::ShadowHL => PrimaryRegister16::HL, + Register16::IX => PrimaryRegister16::IX, + Register16::IY => PrimaryRegister16::IY, + } + } +} + +#[derive(Deserialize, Serialize)] +pub struct CpuDebugWindow { + pub show: bool, + + // 8-bit register breakpoints + #[serde(skip, default = "default_register8")] + selected_register8: Register8, + #[serde(skip, default)] + register8_value_input: String, + #[serde(skip, default)] + register8_any_value: bool, + + // 16-bit register breakpoints (main + shadow) + #[serde(skip, default = "default_ui_register16")] + selected_register16: Register16, + #[serde(skip, default)] + register16_value_input: String, + #[serde(skip, default)] + register16_any_value: bool, +} + +impl Default for CpuDebugWindow { + fn default() -> Self { + Self { + show: false, + selected_register8: Register8::A, + register8_value_input: String::new(), + register8_any_value: false, + selected_register16: Register16::PC, + register16_value_input: String::new(), + register16_any_value: false, + } + } +} + +impl CpuDebugWindow { + pub fn ui(&mut self, ctx: &egui::Context, debugger: &mut impl Debugger) { + if !self.show { + return; + } + + let mut open = self.show; + egui::Window::new("CPU Internals") + .resizable(false) + .open(&mut open) + .show(ctx, |ui| { + self.render_cpu_registers(ui, debugger); + ui.separator(); + self.render_breakpoints_section(ui, debugger); + }); + self.show = open; + } + + fn render_cpu_registers(&self, ui: &mut egui::Ui, debugger: &mut impl Debugger) { + let data = &debugger.debug_view().cpu; + ui.heading("Main Registers"); + egui::Grid::new("cpu_main_registers_grid") + .num_columns(8) + .show(ui, |ui| { + ui.label("A:"); + ui.monospace(format!("{:02X}", data.register_a)); + ui.label("F:"); + ui.monospace(format!("{:02X}", data.register_f)); + ui.label("B:"); + ui.monospace(format!("{:02X}", data.register_b)); + ui.label("C:"); + ui.monospace(format!("{:02X}", data.register_c)); + ui.end_row(); + + ui.label("D:"); + ui.monospace(format!("{:02X}", data.register_d)); + ui.label("E:"); + ui.monospace(format!("{:02X}", data.register_e)); + ui.label("H:"); + ui.monospace(format!("{:02X}", data.register_h)); + ui.label("L:"); + ui.monospace(format!("{:02X}", data.register_l)); + ui.end_row(); + }); + + ui.heading("Shadow Registers"); + egui::Grid::new("cpu_shadow_registers_grid") + .num_columns(8) + .show(ui, |ui| { + ui.label("A':"); + ui.monospace(format!("{:02X}", data.shadow_register_a)); + ui.label("F':"); + ui.monospace(format!("{:02X}", data.shadow_register_f)); + ui.label("B':"); + ui.monospace(format!("{:02X}", data.shadow_register_b)); + ui.label("C':"); + ui.monospace(format!("{:02X}", data.shadow_register_c)); + ui.end_row(); + + ui.label("D':"); + ui.monospace(format!("{:02X}", data.shadow_register_d)); + ui.label("E':"); + ui.monospace(format!("{:02X}", data.shadow_register_e)); + ui.label("H':"); + ui.monospace(format!("{:02X}", data.shadow_register_h)); + ui.label("L':"); + ui.monospace(format!("{:02X}", data.shadow_register_l)); + ui.end_row(); + }); + + ui.heading("Index & Special Registers"); + egui::Grid::new("cpu_special_registers_grid") + .num_columns(4) + .show(ui, |ui| { + ui.label("IX:"); + ui.monospace(format!( + "{:02X}{:02X}", + data.register_ixh, data.register_ixl + )); + ui.label("IY:"); + ui.monospace(format!( + "{:02X}{:02X}", + data.register_iyh, data.register_iyl + )); + ui.end_row(); + + ui.label("SP:"); + ui.monospace(format!("{:04X}", data.register_sp)); + ui.label("PC:"); + ui.monospace(format!("{:04X}", data.register_pc)); + ui.end_row(); + + ui.label("I:"); + ui.monospace(format!("{:02X}", data.register_i)); + ui.label("R:"); + ui.monospace(format!("{:02X}", data.register_r)); + ui.end_row(); + + ui.label("IM:"); + ui.monospace(format!("{}", data.interrupt_mode)); + ui.end_row(); + }); + + ui.heading("Flags (F Register)"); + self.render_flags(ui, data.register_f); + + ui.heading("Status"); + ui.horizontal(|ui| { + ui.label("IFF1:"); + ui.colored_label( + if data.iff1 { + colors::FORREST_GREEN + } else { + colors::DARK_RED + }, + if data.iff1 { "ON" } else { "OFF" }, + ); + ui.label("IFF2:"); + ui.colored_label( + if data.iff2 { + colors::FORREST_GREEN + } else { + colors::DARK_RED + }, + if data.iff2 { "ON" } else { "OFF" }, + ); + ui.label("Halted:"); + ui.colored_label( + if data.halted { + colors::DARK_YELLOW_GOLD + } else { + colors::FORREST_GREEN + }, + if data.halted { "YES" } else { "NO" }, + ); + }); + } + + fn render_flags(&self, ui: &mut egui::Ui, flags: u8) { + ui.horizontal(|ui| { + for (bit, name) in [ + (7, "S"), + (6, "Z"), + (5, "Y"), + (4, "H"), + (3, "X"), + (2, "P/V"), + (1, "N"), + (0, "C"), + ] { + let is_set = (flags >> bit) & 1 != 0; + ui.colored_label( + if is_set { + colors::FORREST_GREEN + } else { + colors::MEDIUM_GRAY + }, + format!("{}: {}", name, if is_set { "1" } else { "0" }), + ); + } + }); + } + + fn render_breakpoints_section(&mut self, ui: &mut egui::Ui, debugger: &mut impl Debugger) { + ui.heading("CPU Breakpoints"); + + egui::Grid::new("breakpoint_grid") + .num_columns(2) + .spacing([10.0, 4.0]) + .show(ui, |ui| { + // 8-bit register breakpoints + ui.label("8-bit register:"); + + ui.horizontal(|ui| { + egui::ComboBox::from_id_salt("reg8") + .selected_text(format!("{:?}", self.selected_register8)) + .width(50.0) + .show_ui(ui, |ui| { + for reg in [ + Register8::A, + Register8::F, + Register8::B, + Register8::C, + Register8::D, + Register8::E, + Register8::H, + Register8::L, + Register8::I, + Register8::R, + Register8::IXH, + Register8::IXL, + Register8::IYH, + Register8::IYL, + ] { + ui.selectable_value( + &mut self.selected_register8, + reg, + format!("{:?}", reg), + ); + } + }); + + let label = ui.label("Value:"); + ui.add_enabled( + !self.register8_any_value, + egui::TextEdit::singleline(&mut self.register8_value_input) + .desired_width(60.0), + ) + .labelled_by(label.id) + .on_hover_text("Hex value (e.g., 42 or 0x42)"); + + if ui.checkbox(&mut self.register8_any_value, "Any").changed() + && self.register8_any_value + { + self.register8_value_input.clear(); + } + + if ui.button("Add").clicked() { + self.add_register8_breakpoint(debugger); + } + }); + ui.end_row(); + + // 16-bit register breakpoints + ui.label("16-bit register:"); + + ui.horizontal(|ui| { + egui::ComboBox::from_id_salt("reg16") + .selected_text(self.selected_register16.to_string()) + .width(50.0) + .show_ui(ui, |ui| { + for reg in Register16::all_options() { + ui.selectable_value( + &mut self.selected_register16, + *reg, + reg.to_string(), + ); + } + }); + + let label = ui.label("Value:"); + ui.add_enabled( + !self.register16_any_value, + egui::TextEdit::singleline(&mut self.register16_value_input) + .desired_width(60.0), + ) + .labelled_by(label.id) + .on_hover_text("Hex value (e.g., 1000 or 0x1000)"); + + if ui.checkbox(&mut self.register16_any_value, "Any").changed() + && self.register16_any_value + { + self.register16_value_input.clear(); + } + + if ui.button("Add").clicked() { + self.add_register16_breakpoint(debugger); + } + }); + ui.end_row(); + }); + + // List active CPU breakpoints only + ui.separator(); + ui.label("Active CPU Breakpoints:"); + + let mut breakpoint_found = false; + let mut to_remove = None; + let mut to_toggle = None; + + let breakpoint_manager = debugger.breakpoint_manager(); + for (id, breakpoint) in breakpoint_manager.breakpoints_iter() { + if breakpoint.one_shot() + || !matches!( + breakpoint, + AnyBreakpoint::CpuRegister8(_) + | AnyBreakpoint::CpuRegister16(_) + | AnyBreakpoint::CpuShadowRegister16(_) + ) + { + continue; + } + + breakpoint_found = true; + + ui.horizontal(|ui| { + let mut enabled = breakpoint.enabled(); + if ui.checkbox(&mut enabled, breakpoint.to_string()).changed() { + to_toggle = Some((*id, enabled)); + } + + if ui.button("Remove").clicked() { + to_remove = Some(*id); + } + + if let Some(master_clock) = breakpoint.triggered() { + ui.colored_label( + colors::DARK_RED, + format!("(triggered at {})", master_clock.value()), + ); + } + }); + } + + if !breakpoint_found { + ui.label("No CPU breakpoints set"); + } + + // Apply changes (done outside the loop to avoid borrow issues) + if let Some((id, enabled)) = to_toggle { + breakpoint_manager.enable_breakpoint(id, enabled); + } + if let Some(id) = to_remove { + breakpoint_manager.remove_breakpoint(id); + } + } + + fn add_register8_breakpoint(&mut self, debugger: &mut impl Debugger) { + let value = if self.register8_any_value { + None + } else { + match usize::from_str_radix(self.register8_value_input.trim_start_matches("0x"), 16) { + Ok(val) => Some((val & 0xFF) as u8), + Err(_) => return, // Invalid input, don't add breakpoint + } + }; + + let breakpoint = AnyBreakpoint::cpu_register8_breakpoint(self.selected_register8, value); + debugger.breakpoint_manager().add_breakpoint(breakpoint); + self.register8_value_input.clear(); + self.register8_any_value = false; + } + + fn add_register16_breakpoint(&mut self, debugger: &mut impl Debugger) { + let value = if self.register16_any_value { + None + } else { + match usize::from_str_radix(self.register16_value_input.trim_start_matches("0x"), 16) { + Ok(val) => Some((val & 0xFFFF) as u16), + Err(_) => return, // Invalid input, don't add breakpoint + } + }; + + let primary_register = self.selected_register16.into(); + let breakpoint = if self.selected_register16.is_shadow() { + AnyBreakpoint::cpu_shadow_register16_breakpoint(primary_register, value) + } else { + AnyBreakpoint::cpu_register16_breakpoint(primary_register, value) + }; + + debugger.breakpoint_manager().add_breakpoint(breakpoint); + self.register16_value_input.clear(); + self.register16_any_value = false; + } +} + +#[cfg(test)] +mod gui_tests { + use super::*; + + use egui::accesskit; + use egui_kittest::{Harness, kittest::Queryable}; + + use ronald_core::debug::breakpoint::{ + CpuRegister8Breakpoint, CpuRegister16Breakpoint, CpuShadowRegister16Breakpoint, + }; + + use crate::debug::mock::TestDebugger; + + #[test] + fn test_cpu_debug_window_opens_and_closes() { + let mut debugger = TestDebugger::default(); + let mut window = CpuDebugWindow { + show: true, + ..Default::default() + }; + + let app = |ctx: &egui::Context| { + window.ui(ctx, &mut debugger); + }; + + let mut harness = Harness::new(app); + harness.run(); + + // Check that the window title is rendered + harness.get_by_label("CPU Internals"); + + // Click close button + harness.get_by_label("Close window").click(); + harness.run(); + + // Window should no longer be visible + assert!(harness.query_by_label("CPU Internals").is_none()); + } + + #[test] + fn test_cpu_debug_window_8bit_register_breakpoint_with_value() { + let mut debugger = TestDebugger::default(); + let mut window = CpuDebugWindow { + show: true, + ..Default::default() + }; + + let app = |ctx: &egui::Context| { + window.ui(ctx, &mut debugger); + }; + + let mut harness = Harness::new(app); + harness.run(); + + let i = 0; + + // Select register B + harness + .get_all_by_role(accesskit::Role::ComboBox) + .nth(i) + .unwrap() + .click(); + harness.run(); + harness.get_by_label("B").click(); + harness.run(); + + // Enter value "0x42" + harness + .get_all_by_role_and_label(accesskit::Role::TextInput, "Value:") + .nth(i) + .unwrap() + .type_text("0x42"); + harness.run(); + + // Add breakpoint + harness + .get_all_by_role_and_label(accesskit::Role::Button, "Add") + .nth(i) + .unwrap() + .click(); + harness.run(); + + assert!(harness.query_by_label("B = 0x42").is_some()); + + // Remove breakpoint + harness + .get_by_role_and_label(accesskit::Role::Button, "Remove") + .click(); + harness.run(); + + assert!(harness.query_by_label("B = 0x42").is_none()); + } + + #[test] + fn test_cpu_debug_window_8bit_register_breakpoint_invalid_value() { + let mut debugger = TestDebugger::default(); + let mut window = CpuDebugWindow { + show: true, + ..Default::default() + }; + + let app = |ctx: &egui::Context| { + window.ui(ctx, &mut debugger); + }; + + let mut harness = Harness::new(app); + harness.run(); + + let i = 0; + + // Select register B + harness + .get_all_by_role(accesskit::Role::ComboBox) + .nth(i) + .unwrap() + .click(); + harness.run(); + harness.get_by_label("B").click(); + harness.run(); + + // Enter value "invalid" + harness + .get_all_by_role_and_label(accesskit::Role::TextInput, "Value:") + .nth(i) + .unwrap() + .type_text("invalid"); + harness.run(); + + // Add breakpoint + harness + .get_all_by_role_and_label(accesskit::Role::Button, "Add") + .nth(i) + .unwrap() + .click(); + harness.run(); + drop(harness); + + // Breakpoint should not be added + assert_eq!(debugger.breakpoint_manager().breakpoints_iter().count(), 0); + } + + #[test] + fn test_cpu_debug_window_8bit_register_breakpoint_any_value() { + let mut debugger = TestDebugger::default(); + let mut window = CpuDebugWindow { + show: true, + ..Default::default() + }; + + let app = |ctx: &egui::Context| { + window.ui(ctx, &mut debugger); + }; + + let mut harness = Harness::new(app); + harness.run(); + + let i = 0; + + // Select register B + harness + .get_all_by_role(accesskit::Role::ComboBox) + .nth(i) + .unwrap() + .click(); + harness.run(); + harness.get_by_label("B").click(); + harness.run(); + + // Check "Any" checkbox + harness + .get_all_by_role_and_label(accesskit::Role::CheckBox, "Any") + .nth(i) + .unwrap() + .click(); + harness.run(); + + // Add breakpoint + harness + .get_all_by_role_and_label(accesskit::Role::Button, "Add") + .nth(i) + .unwrap() + .click(); + harness.run(); + + // Disable breakpoint + harness.get_by_label("B written").click(); + harness.run(); + drop(harness); + + // Breakpoint should be added + assert_eq!(debugger.breakpoint_manager().breakpoints_iter().count(), 1); + assert!( + debugger + .breakpoint_manager() + .breakpoints_iter() + .any(|(_, bp)| { + matches!( + bp, + AnyBreakpoint::CpuRegister8(CpuRegister8Breakpoint { + register: Register8::B, + value: None, + .. + }) + ) && !bp.enabled() + }) + ); + } + + #[test] + fn test_cpu_debug_window_16bit_register_breakpoint_with_value() { + let mut debugger = TestDebugger::default(); + let mut window = CpuDebugWindow { + show: true, + ..Default::default() + }; + + let app = |ctx: &egui::Context| { + window.ui(ctx, &mut debugger); + }; + + let mut harness = Harness::new(app); + harness.run(); + + let i = 1; + + // Select register SP + harness + .get_all_by_role(accesskit::Role::ComboBox) + .nth(i) + .unwrap() + .click(); + harness.run(); + harness.get_by_label("SP").click(); + harness.run(); + + // Enter value "0xbeef" + harness + .get_all_by_role_and_label(accesskit::Role::TextInput, "Value:") + .nth(i) + .unwrap() + .type_text("0xbeef"); + harness.run(); + + // Add breakpoint + harness + .get_all_by_role_and_label(accesskit::Role::Button, "Add") + .nth(i) + .unwrap() + .click(); + harness.run(); + + assert!(harness.query_by_label("SP = 0xBEEF").is_some()); + + // Remove breakpoint + harness + .get_by_role_and_label(accesskit::Role::Button, "Remove") + .click(); + harness.run(); + + assert!(harness.query_by_label("SP = 0xBEEF").is_none()); + } + + #[test] + fn test_cpu_debug_window_16bit_register_breakpoint_invalid_value() { + let mut debugger = TestDebugger::default(); + let mut window = CpuDebugWindow { + show: true, + ..Default::default() + }; + + let app = |ctx: &egui::Context| { + window.ui(ctx, &mut debugger); + }; + + let mut harness = Harness::new(app); + harness.run(); + + let i = 1; + + // Select register SP + harness + .get_all_by_role(accesskit::Role::ComboBox) + .nth(i) + .unwrap() + .click(); + harness.run(); + harness.get_by_label("SP").click(); + harness.run(); + + // Enter value "invalid" + harness + .get_all_by_role_and_label(accesskit::Role::TextInput, "Value:") + .nth(i) + .unwrap() + .type_text("invalid"); + harness.run(); + + // Add breakpoint + harness + .get_all_by_role_and_label(accesskit::Role::Button, "Add") + .nth(i) + .unwrap() + .click(); + harness.run(); + drop(harness); + + // Breakpoint should not be added + assert_eq!(debugger.breakpoint_manager().breakpoints_iter().count(), 0); + } + + #[test] + fn test_cpu_debug_window_16bit_register_breakpoint_any_value() { + let mut debugger = TestDebugger::default(); + let mut window = CpuDebugWindow { + show: true, + ..Default::default() + }; + + let app = |ctx: &egui::Context| { + window.ui(ctx, &mut debugger); + }; + + let mut harness = Harness::new(app); + harness.run(); + + let i = 1; + + // Select register SP + harness + .get_all_by_role(accesskit::Role::ComboBox) + .nth(i) + .unwrap() + .click(); + harness.run(); + harness.get_by_label("SP").click(); + harness.run(); + + // Check "Any" checkbox + harness + .get_all_by_role_and_label(accesskit::Role::CheckBox, "Any") + .nth(i) + .unwrap() + .click(); + harness.run(); + + // Add breakpoint + harness + .get_all_by_role_and_label(accesskit::Role::Button, "Add") + .nth(i) + .unwrap() + .click(); + harness.run(); + + // Disable breakpoint + harness.get_by_label("SP written").click(); + harness.run(); + drop(harness); + + // Breakpoint should be added + assert_eq!(debugger.breakpoint_manager().breakpoints_iter().count(), 1); + assert!( + debugger + .breakpoint_manager() + .breakpoints_iter() + .any(|(_, bp)| { + matches!( + bp, + AnyBreakpoint::CpuRegister16(CpuRegister16Breakpoint { + register: PrimaryRegister16::SP, + value: None, + .. + }) + ) && !bp.enabled() + }) + ); + } + + #[test] + fn test_cpu_debug_window_16bit_shadow_register_breakpoint_with_value() { + let mut debugger = TestDebugger::default(); + let mut window = CpuDebugWindow { + show: true, + ..Default::default() + }; + + let app = |ctx: &egui::Context| { + window.ui(ctx, &mut debugger); + }; + + let mut harness = Harness::new(app); + harness.run(); + + let i = 1; + + // Select shadow register HL' + harness + .get_all_by_role(accesskit::Role::ComboBox) + .nth(i) + .unwrap() + .click(); + harness.run(); + harness.get_by_label("HL'").click(); + harness.run(); + + // Enter value "0xbeef" + harness + .get_all_by_role_and_label(accesskit::Role::TextInput, "Value:") + .nth(i) + .unwrap() + .type_text("0xbeef"); + harness.run(); + + // Add breakpoint + harness + .get_all_by_role_and_label(accesskit::Role::Button, "Add") + .nth(i) + .unwrap() + .click(); + harness.run(); + + assert!(harness.query_by_label("HL' = 0xBEEF").is_some()); + + // Remove breakpoint + harness + .get_by_role_and_label(accesskit::Role::Button, "Remove") + .click(); + harness.run(); + + assert!(harness.query_by_label("HL' = 0xBEEF").is_none()); + } + + #[test] + fn test_cpu_debug_window_16bit_shadow_register_breakpoint_invalid_value() { + let mut debugger = TestDebugger::default(); + let mut window = CpuDebugWindow { + show: true, + ..Default::default() + }; + + let app = |ctx: &egui::Context| { + window.ui(ctx, &mut debugger); + }; + + let mut harness = Harness::new(app); + harness.run(); + + let i = 1; + + // Select shadow register HL' + harness + .get_all_by_role(accesskit::Role::ComboBox) + .nth(i) + .unwrap() + .click(); + harness.run(); + harness.get_by_label("HL'").click(); + harness.run(); + + // Enter value "invalid" + harness + .get_all_by_role_and_label(accesskit::Role::TextInput, "Value:") + .nth(i) + .unwrap() + .type_text("invalid"); + harness.run(); + + // Add breakpoint + harness + .get_all_by_role_and_label(accesskit::Role::Button, "Add") + .nth(i) + .unwrap() + .click(); + harness.run(); + drop(harness); + + // Breakpoint should not be added + assert_eq!(debugger.breakpoint_manager().breakpoints_iter().count(), 0); + } + + #[test] + fn test_cpu_debug_window_16bit_shadow_register_breakpoint_any_value() { + let mut debugger = TestDebugger::default(); + let mut window = CpuDebugWindow { + show: true, + ..Default::default() + }; + + let app = |ctx: &egui::Context| { + window.ui(ctx, &mut debugger); + }; + + let mut harness = Harness::new(app); + harness.run(); + + let i = 1; + + // Select shadow register HL' + harness + .get_all_by_role(accesskit::Role::ComboBox) + .nth(i) + .unwrap() + .click(); + harness.run(); + harness.get_by_label("HL'").click(); + harness.run(); + + // Check "Any" checkbox + harness + .get_all_by_role_and_label(accesskit::Role::CheckBox, "Any") + .nth(i) + .unwrap() + .click(); + harness.run(); + + // Add breakpoint + harness + .get_all_by_role_and_label(accesskit::Role::Button, "Add") + .nth(i) + .unwrap() + .click(); + harness.run(); + + // Disable breakpoint + harness.get_by_label("HL' written").click(); + harness.run(); + drop(harness); + + // Breakpoint should be added + assert_eq!(debugger.breakpoint_manager().breakpoints_iter().count(), 1); + assert!( + debugger + .breakpoint_manager() + .breakpoints_iter() + .any(|(_, bp)| { + matches!( + bp, + AnyBreakpoint::CpuShadowRegister16(CpuShadowRegister16Breakpoint { + register: PrimaryRegister16::HL, + value: None, + .. + }) + ) && !bp.enabled() + }) + ); + } +} diff --git a/ronald-egui/src/debug/crtc.rs b/ronald-egui/src/debug/crtc.rs new file mode 100644 index 0000000..6241418 --- /dev/null +++ b/ronald-egui/src/debug/crtc.rs @@ -0,0 +1,1455 @@ +use eframe::egui; +use serde::{Deserialize, Serialize}; + +use ronald_core::debug::breakpoint::{AnyBreakpoint, Breakpoint}; +use ronald_core::system::bus::crtc::Register as CrtcRegister; + +use crate::colors; +use crate::debug::Debugger; + +#[derive(Default, Serialize, Deserialize)] +#[serde(default)] +pub struct CrtcDebugWindow { + pub show: bool, + + // Register write breakpoint + #[serde(skip, default)] + register_write_register: Option, + #[serde(skip, default)] + register_write_value_input: String, + #[serde(skip, default)] + register_write_any_register: bool, + #[serde(skip, default)] + register_write_any_value: bool, + + // Counters breakpoint + #[serde(skip, default)] + character_row_value_input: String, + #[serde(skip, default)] + scan_line_value_input: String, + #[serde(skip, default)] + horizontal_counter_value_input: String, + #[serde(skip, default)] + character_row_any_value: bool, + #[serde(skip, default)] + scan_line_any_value: bool, + #[serde(skip, default)] + horizontal_counter_any_value: bool, + + // Address breakpoint + #[serde(skip, default)] + address_value_input: String, + #[serde(skip, default)] + address_any_value: bool, + + // Horizontal sync breakpoint + #[serde(skip, default)] + hsync_on_start: bool, + #[serde(skip, default)] + hsync_on_end: bool, + + // Vertical sync breakpoint + #[serde(skip, default)] + vsync_on_start: bool, + #[serde(skip, default)] + vsync_on_end: bool, + + // Display enable breakpoint + #[serde(skip, default)] + display_enable_on_start: bool, + #[serde(skip, default)] + display_enable_on_end: bool, +} + +impl CrtcDebugWindow { + pub fn ui(&mut self, ctx: &egui::Context, debugger: &mut impl Debugger) { + let mut open = self.show; + egui::Window::new("CRTC Internals") + .resizable(false) + .open(&mut open) + .show(ctx, |ui| { + self.render_crtc_state(ui, debugger); + ui.separator(); + self.render_breakpoints_section(ui, debugger); + }); + self.show = open; + } + + fn render_crtc_state(&self, ui: &mut egui::Ui, debugger: &mut impl Debugger) { + let debug_view = debugger.debug_view(); + let crtc = &debug_view.crtc; + + // Registers Section + ui.heading("CRTC Registers"); + + egui::Grid::new("crtc_registers_grid") + .num_columns(4) + .spacing([10.0, 4.0]) + .show(ui, |ui| { + for (i, value) in crtc.registers.iter().enumerate() { + let register = CrtcRegister::try_from(i).unwrap(); + let is_selected = register == crtc.selected_register; + + let label = format!("{}:", register); + if is_selected { + ui.colored_label(colors::DARK_YELLOW_GOLD, label); + } else { + ui.label(label); + } + + ui.monospace(format!("{:02X}", value)); + + if (i + 1) % 2 == 0 { + ui.end_row(); + } else { + ui.separator(); + } + } + + let register = CrtcRegister::Unused; + let is_selected = register == crtc.selected_register; + + let label = format!("{}:", register); + if is_selected { + ui.colored_label(colors::DARK_YELLOW_GOLD, label); + } else { + ui.label(label); + } + + ui.monospace("-"); + ui.separator(); + + let register = CrtcRegister::Dummy; + let is_selected = register == crtc.selected_register; + + let label = format!("{}:", register); + if is_selected { + ui.colored_label(colors::DARK_YELLOW_GOLD, label); + } else { + ui.label(label); + } + + ui.monospace("-"); + ui.end_row(); + }); + + ui.add_space(8.0); + + // Counters Section + ui.heading("Counters"); + egui::Grid::new("crtc_counters_grid") + .num_columns(2) + .spacing([10.0, 4.0]) + .show(ui, |ui| { + ui.label("Horizontal Counter:"); + ui.label(format!("{:02X}", crtc.horizontal_counter)); + ui.end_row(); + + ui.label("Character Row Counter:"); + ui.label(format!("{:02X}", crtc.character_row_counter)); + ui.end_row(); + + ui.label("Scan Line Counter:"); + ui.label(format!("{:02X}", crtc.scan_line_counter)); + ui.end_row(); + }); + + ui.add_space(8.0); + + // Address Section + ui.heading("Addresses"); + egui::Grid::new("crtc_addresses_grid") + .num_columns(2) + .spacing([10.0, 4.0]) + .show(ui, |ui| { + ui.label("Display Start Address:"); + ui.monospace(format!("{:04X}", crtc.display_start_address)); + ui.end_row(); + + ui.label("Current Address:"); + ui.monospace(format!("{:04X}", crtc.current_address)); + ui.end_row(); + }); + + ui.add_space(8.0); + + // Status Section + ui.heading("Status"); + ui.horizontal(|ui| { + let hsync_color = if crtc.hsync_active { + colors::FORREST_GREEN + } else { + colors::MEDIUM_GRAY + }; + ui.colored_label(hsync_color, "HSYNC"); + + ui.separator(); + + let vsync_color = if crtc.vsync_active { + colors::FORREST_GREEN + } else { + colors::MEDIUM_GRAY + }; + ui.colored_label(vsync_color, "VSYNC"); + + ui.separator(); + + let display_color = if crtc.display_enabled { + colors::FORREST_GREEN + } else { + colors::MEDIUM_GRAY + }; + ui.colored_label(display_color, "DISPLAY"); + }); + } + + fn render_breakpoints_section(&mut self, ui: &mut egui::Ui, debugger: &mut impl Debugger) { + ui.heading("CRTC Breakpoints"); + + egui::Grid::new("crtc_breakpoint_grid") + .num_columns(2) + .spacing([10.0, 4.0]) + .show(ui, |ui| { + // Register write breakpoint + ui.label("Register write:"); + ui.horizontal(|ui| { + ui.add_enabled_ui(!self.register_write_any_register, |ui| { + egui::ComboBox::from_id_salt("crtc_register_selector") + .width(180.0) + .selected_text(match self.register_write_register { + Some(ref reg) => format!("{}", reg), + None => "Select register...".to_string(), + }) + .show_ui(ui, |ui| { + for i in 0..18 { + let reg = CrtcRegister::try_from(i).unwrap(); + ui.selectable_value( + &mut self.register_write_register, + Some(reg), + format!("{}", reg), + ); + } + let reg = CrtcRegister::Unused; + ui.selectable_value( + &mut self.register_write_register, + Some(reg), + format!("{}", reg), + ); + let reg = CrtcRegister::Dummy; + ui.selectable_value( + &mut self.register_write_register, + Some(reg), + format!("{}", reg), + ); + }); + }); + + if ui + .checkbox(&mut self.register_write_any_register, "Any") + .changed() + && self.register_write_any_register + { + self.register_write_register = None; + } + + let label = ui.label("Value:"); + ui.add_enabled( + !self.register_write_any_value, + egui::TextEdit::singleline(&mut self.register_write_value_input) + .desired_width(40.0), + ) + .labelled_by(label.id) + .on_hover_text("Hex value (e.g., 1000 or 0x1000)"); + + if ui + .checkbox(&mut self.register_write_any_value, "Any") + .changed() + && self.register_write_any_value + { + self.register_write_value_input.clear(); + } + + if ui.button("Add").clicked() { + self.add_register_write_breakpoint(debugger); + } + }); + ui.end_row(); + + // Counters breakpoint + ui.label("Counters:"); + ui.horizontal(|ui| { + let label = ui.label("Char. row:"); + ui.add_enabled( + !self.character_row_any_value, + egui::TextEdit::singleline(&mut self.character_row_value_input) + .desired_width(40.0), + ) + .labelled_by(label.id) + .on_hover_text("Hex value (e.g., 10 or 0x10)"); + + if ui + .checkbox(&mut self.character_row_any_value, "Any") + .changed() + && self.character_row_any_value + { + self.character_row_value_input.clear(); + } + + let label = ui.label("Scan line:"); + ui.add_enabled( + !self.scan_line_any_value, + egui::TextEdit::singleline(&mut self.scan_line_value_input) + .desired_width(40.0), + ) + .labelled_by(label.id) + .on_hover_text("Hex value (e.g., 10 or 0x10)"); + + if ui.checkbox(&mut self.scan_line_any_value, "Any").changed() + && self.scan_line_any_value + { + self.scan_line_value_input.clear(); + } + + let label = ui.label("Horizontal:"); + ui.add_enabled( + !self.horizontal_counter_any_value, + egui::TextEdit::singleline(&mut self.horizontal_counter_value_input) + .desired_width(40.0), + ) + .labelled_by(label.id) + .on_hover_text("Hex value (e.g., 10 or 0x10)"); + + if ui + .checkbox(&mut self.horizontal_counter_any_value, "Any") + .changed() + && self.horizontal_counter_any_value + { + self.horizontal_counter_value_input.clear(); + } + + if ui.button("Add").clicked() { + self.add_counters_breakpoint(debugger); + } + }); + ui.end_row(); + + // Address breakpoint + let label = ui.label("Address:"); + ui.horizontal(|ui| { + ui.add_enabled( + !self.address_any_value, + egui::TextEdit::singleline(&mut self.address_value_input) + .desired_width(60.0), + ) + .labelled_by(label.id) + .on_hover_text("Hex value (e.g., 1000 or 0x1000)"); + + ui.checkbox(&mut self.address_any_value, "Any"); + + if ui.button("Add").clicked() { + self.add_address_breakpoint(debugger); + } + }); + ui.end_row(); + + // Horizontal sync breakpoint + ui.label("Horizontal sync:"); + ui.horizontal(|ui| { + ui.checkbox(&mut self.hsync_on_start, "Start"); + ui.checkbox(&mut self.hsync_on_end, "End"); + + if ui.button("Add").clicked() { + self.add_horizontal_sync_breakpoint(debugger); + } + }); + ui.end_row(); + + // Vertical sync breakpoint + ui.label("Vertical sync:"); + ui.horizontal(|ui| { + ui.checkbox(&mut self.vsync_on_start, "Start"); + ui.checkbox(&mut self.vsync_on_end, "End"); + + if ui.button("Add").clicked() { + self.add_vertical_sync_breakpoint(debugger); + } + }); + ui.end_row(); + + // Display enable breakpoint + ui.label("Display enable:"); + ui.horizontal(|ui| { + ui.checkbox(&mut self.display_enable_on_start, "Start"); + ui.checkbox(&mut self.display_enable_on_end, "End"); + + if ui.button("Add").clicked() { + self.add_display_enable_breakpoint(debugger); + } + }); + ui.end_row(); + }); + + // List active CRTC breakpoints + ui.separator(); + ui.label("Active CRTC Breakpoints:"); + + let mut breakpoint_found = false; + let mut to_remove = None; + let mut to_toggle = None; + + let breakpoint_manager = debugger.breakpoint_manager(); + for (id, breakpoint) in breakpoint_manager.breakpoints_iter() { + if breakpoint.one_shot() + || !matches!( + breakpoint, + AnyBreakpoint::CrtcRegisterWrite(_) + | AnyBreakpoint::CrtcCounters(_) + | AnyBreakpoint::CrtcAddress(_) + | AnyBreakpoint::CrtcHorizontalSync(_) + | AnyBreakpoint::CrtcVerticalSync(_) + | AnyBreakpoint::CrtcDisplayEnable(_) + ) + { + continue; + } + + breakpoint_found = true; + + ui.horizontal(|ui| { + let mut enabled = breakpoint.enabled(); + if ui.checkbox(&mut enabled, breakpoint.to_string()).changed() { + to_toggle = Some((*id, enabled)); + } + + if ui.button("Remove").clicked() { + to_remove = Some(*id); + } + + if let Some(master_clock) = breakpoint.triggered() { + ui.colored_label( + colors::DARK_RED, + format!("(triggered at {})", master_clock.value()), + ); + } + }); + } + + if !breakpoint_found { + ui.label("No CRTC breakpoints set"); + } + + // Apply changes + if let Some((id, enabled)) = to_toggle { + breakpoint_manager.enable_breakpoint(id, enabled); + } + if let Some(id) = to_remove { + breakpoint_manager.remove_breakpoint(id); + } + } + + fn add_register_write_breakpoint(&mut self, debugger: &mut impl Debugger) { + let register = if self.register_write_any_register { + None + } else { + match self.register_write_register { + Some(reg) => Some(reg), + None => return, // No register selected, don't add breakpoint + } + }; + + let value = if self.register_write_any_value { + None + } else { + match usize::from_str_radix( + self.register_write_value_input.trim_start_matches("0x"), + 16, + ) { + Ok(val) => Some((val & 0xFF) as u8), + Err(_) => return, // Invalid input, don't add breakpoint + } + }; + + let breakpoint = AnyBreakpoint::crtc_register_write_breakpoint(register, value); + debugger.breakpoint_manager().add_breakpoint(breakpoint); + + self.register_write_register = None; + self.register_write_any_register = false; + self.register_write_value_input.clear(); + self.register_write_any_value = false; + } + + fn add_counters_breakpoint(&mut self, debugger: &mut impl Debugger) { + let character_row = if self.character_row_any_value { + None + } else { + match usize::from_str_radix(self.character_row_value_input.trim_start_matches("0x"), 16) + { + Ok(val) => Some((val & 0xFF) as u8), + Err(_) => return, // Invalid input, don't add breakpoint + } + }; + + let scan_line = if self.scan_line_any_value { + None + } else { + match usize::from_str_radix(self.scan_line_value_input.trim_start_matches("0x"), 16) { + Ok(val) => Some((val & 0xFF) as u8), + Err(_) => return, // Invalid input, don't add breakpoint + } + }; + + let horizontal_counter = if self.horizontal_counter_any_value { + None + } else { + match usize::from_str_radix( + self.horizontal_counter_value_input.trim_start_matches("0x"), + 16, + ) { + Ok(val) => Some((val & 0xFF) as u8), + Err(_) => return, // Invalid input, don't add breakpoint + } + }; + + let breakpoint = + AnyBreakpoint::crtc_counters_breakpoint(character_row, scan_line, horizontal_counter); + debugger.breakpoint_manager().add_breakpoint(breakpoint); + + self.character_row_value_input.clear(); + self.character_row_any_value = false; + self.scan_line_value_input.clear(); + self.scan_line_any_value = false; + self.horizontal_counter_value_input.clear(); + self.horizontal_counter_any_value = false; + } + + fn add_address_breakpoint(&mut self, debugger: &mut impl Debugger) { + let address = if self.address_any_value { + None + } else { + match usize::from_str_radix(self.address_value_input.trim_start_matches("0x"), 16) { + Ok(val) => Some(val & 0xFFFF), + Err(_) => return, // Invalid input, don't add breakpoint + } + }; + + let breakpoint = AnyBreakpoint::crtc_address_breakpoint(address); + debugger.breakpoint_manager().add_breakpoint(breakpoint); + + self.address_value_input.clear(); + self.address_any_value = false; + } + + fn add_horizontal_sync_breakpoint(&mut self, debugger: &mut impl Debugger) { + if !self.hsync_on_start && !self.hsync_on_end { + return; + } + + let breakpoint = + AnyBreakpoint::crtc_horizontal_sync_breakpoint(self.hsync_on_start, self.hsync_on_end); + debugger.breakpoint_manager().add_breakpoint(breakpoint); + + self.hsync_on_start = false; + self.hsync_on_end = false; + } + + fn add_vertical_sync_breakpoint(&mut self, debugger: &mut impl Debugger) { + if !self.vsync_on_start && !self.vsync_on_end { + return; + } + + let breakpoint = + AnyBreakpoint::crtc_vertical_sync_breakpoint(self.vsync_on_start, self.vsync_on_end); + debugger.breakpoint_manager().add_breakpoint(breakpoint); + + self.vsync_on_start = false; + self.vsync_on_end = false; + } + + fn add_display_enable_breakpoint(&mut self, debugger: &mut impl Debugger) { + if !self.display_enable_on_start && !self.display_enable_on_end { + return; + } + + let breakpoint = AnyBreakpoint::crtc_dispaly_enable_breakpoint( + self.display_enable_on_start, + self.display_enable_on_end, + ); + debugger.breakpoint_manager().add_breakpoint(breakpoint); + + self.display_enable_on_start = false; + self.display_enable_on_end = false; + } +} + +#[cfg(test)] +mod gui_tests { + use super::*; + + use egui::accesskit; + use egui_kittest::{Harness, kittest::Queryable}; + + use ronald_core::debug::breakpoint::{ + CrtcAddressBreakpoint, CrtcCountersBreakpoint, CrtcRegisterWriteBreakpoint, + }; + + use crate::debug::mock::TestDebugger; + + #[test] + fn test_crtc_debug_window_opens_and_closes() { + let mut debugger = TestDebugger::default(); + let mut window = CrtcDebugWindow { + show: true, + ..Default::default() + }; + + let app = |ctx: &egui::Context| { + window.ui(ctx, &mut debugger); + }; + + let mut harness = Harness::new(app); + harness.run(); + + // Check that the window title is rendered + harness.get_by_label("CRTC Internals"); + + // Click close button + harness.get_by_label("Close window").click(); + harness.run(); + + // Window should no longer be visible + assert!(harness.query_by_label("CRTC Internals").is_none()); + } + + #[test] + fn test_crtc_debug_window_register_breakpoint_with_register_and_value() { + let mut debugger = TestDebugger::default(); + let mut window = CrtcDebugWindow { + show: true, + ..Default::default() + }; + + let app = |ctx: &egui::Context| { + window.ui(ctx, &mut debugger); + }; + + let mut harness = Harness::new(app); + harness.run(); + + let i = 0; + + // Select register "Vertical Total" + harness + .get_all_by_role(accesskit::Role::ComboBox) + .nth(i) + .unwrap() + .click(); + harness.run(); + harness.get_by_label("R4 (Vertical Total)").click(); + harness.run(); + + // Enter value "0x42" + harness + .get_all_by_role_and_label(accesskit::Role::TextInput, "Value:") + .nth(i) + .unwrap() + .type_text("0x42"); + harness.run(); + + // Add breakpoint + harness + .get_all_by_role_and_label(accesskit::Role::Button, "Add") + .nth(i) + .unwrap() + .click(); + harness.run(); + + assert!( + harness + .query_by_label("R4 (Vertical Total) = 0x42") + .is_some() + ); + + // Remove breakpoint + harness + .get_by_role_and_label(accesskit::Role::Button, "Remove") + .click(); + harness.run(); + + assert!( + harness + .query_by_label("R4 (Vertical Total) = 0x42") + .is_none() + ); + } + + #[test] + fn test_crtc_debug_window_register_breakpoint_any_register() { + let mut debugger = TestDebugger::default(); + let mut window = CrtcDebugWindow { + show: true, + ..Default::default() + }; + + let app = |ctx: &egui::Context| { + window.ui(ctx, &mut debugger); + }; + + let mut harness = Harness::new(app); + harness.run(); + + let i = 0; + + // Select any register + harness + .get_all_by_role_and_label(accesskit::Role::CheckBox, "Any") + .nth(i) + .unwrap() + .click(); + harness.run(); + + // Enter value "0x42" + harness + .get_all_by_role_and_label(accesskit::Role::TextInput, "Value:") + .nth(i) + .unwrap() + .type_text("0x42"); + harness.run(); + + // Add breakpoint + harness + .get_all_by_role_and_label(accesskit::Role::Button, "Add") + .nth(i) + .unwrap() + .click(); + harness.run(); + + harness.get_by_label("Any register = 0x42").click(); + harness.run(); + drop(harness); + + assert_eq!(debugger.breakpoint_manager().breakpoints_iter().count(), 1); + assert!( + debugger + .breakpoint_manager() + .breakpoints_iter() + .any(|(_, bp)| { + matches!( + bp, + AnyBreakpoint::CrtcRegisterWrite(CrtcRegisterWriteBreakpoint { + register: None, + value: Some(0x42), + .. + }) + ) && !bp.enabled() + }) + ); + } + + #[test] + fn test_crtc_debug_window_register_breakpoint_any_value() { + let mut debugger = TestDebugger::default(); + let mut window = CrtcDebugWindow { + show: true, + ..Default::default() + }; + + let app = |ctx: &egui::Context| { + window.ui(ctx, &mut debugger); + }; + + let mut harness = Harness::new(app); + harness.run(); + + let i = 0; + + // Select register "Vertical Total" + harness + .get_all_by_role(accesskit::Role::ComboBox) + .nth(i) + .unwrap() + .click(); + harness.run(); + harness.get_by_label("R4 (Vertical Total)").click(); + harness.run(); + + // Check any value + harness + .get_all_by_role_and_label(accesskit::Role::CheckBox, "Any") + .nth(i + 1) + .unwrap() + .click(); + harness.run(); + + // Add breakpoint + harness + .get_all_by_role_and_label(accesskit::Role::Button, "Add") + .nth(i) + .unwrap() + .click(); + harness.run(); + + // Disable breakpoint + harness.get_by_label("R4 (Vertical Total) written").click(); + harness.run(); + drop(harness); + + assert_eq!(debugger.breakpoint_manager().breakpoints_iter().count(), 1); + assert!( + debugger + .breakpoint_manager() + .breakpoints_iter() + .any(|(_, bp)| { + matches!( + bp, + AnyBreakpoint::CrtcRegisterWrite(CrtcRegisterWriteBreakpoint { + register: Some(CrtcRegister::VerticalTotal), + value: None, + .. + }) + ) && !bp.enabled() + }) + ); + } + + #[test] + fn test_crtc_debug_window_register_breakpoint_invalid_value() { + let mut debugger = TestDebugger::default(); + let mut window = CrtcDebugWindow { + show: true, + ..Default::default() + }; + + let app = |ctx: &egui::Context| { + window.ui(ctx, &mut debugger); + }; + + let mut harness = Harness::new(app); + harness.run(); + + let i = 0; + + // Select register "Vertical Total" + harness + .get_all_by_role(accesskit::Role::ComboBox) + .nth(i) + .unwrap() + .click(); + harness.run(); + harness.get_by_label("R4 (Vertical Total)").click(); + harness.run(); + + // Enter value "invalid" + harness + .get_all_by_role_and_label(accesskit::Role::TextInput, "Value:") + .nth(i) + .unwrap() + .type_text("invalid"); + harness.run(); + + // Add breakpoint + harness + .get_all_by_role_and_label(accesskit::Role::Button, "Add") + .nth(i) + .unwrap() + .click(); + harness.run(); + drop(harness); + + assert_eq!(debugger.breakpoint_manager().breakpoints_iter().count(), 0); + } + + #[test] + fn test_crtc_debug_window_counters_breakpoint_with_values() { + let mut debugger = TestDebugger::default(); + let mut window = CrtcDebugWindow { + show: true, + ..Default::default() + }; + + let app = |ctx: &egui::Context| { + window.ui(ctx, &mut debugger); + }; + + let mut harness = Harness::new(app); + harness.run(); + + // Enter character row value "0x42" + harness + .get_by_role_and_label(accesskit::Role::TextInput, "Char. row:") + .type_text("0x42"); + harness.run(); + + // Enter scan line value "0x08" + harness + .get_by_role_and_label(accesskit::Role::TextInput, "Scan line:") + .type_text("0x08"); + harness.run(); + + // Enter horizontal value "0xaf" + harness + .get_by_role_and_label(accesskit::Role::TextInput, "Horizontal:") + .type_text("0xaf"); + harness.run(); + + // Add breakpoint + harness + .get_all_by_role_and_label(accesskit::Role::Button, "Add") + .nth(1) + .unwrap() + .click(); + harness.run(); + + assert!( + harness + .query_by_label("Counters = 0x42/0x08/0xAF") + .is_some() + ); + + // Remove breakpoint + harness + .get_by_role_and_label(accesskit::Role::Button, "Remove") + .click(); + harness.run(); + + assert!( + harness + .query_by_label("Counters = 0x42/0x08/0xAF") + .is_none() + ); + } + + #[test] + fn test_crtc_debug_window_counters_breakpoint_invalid_character_row() { + let mut debugger = TestDebugger::default(); + let mut window = CrtcDebugWindow { + show: true, + ..Default::default() + }; + + let app = |ctx: &egui::Context| { + window.ui(ctx, &mut debugger); + }; + + let mut harness = Harness::new(app); + harness.run(); + + // Enter character row value "invalid" + harness + .get_by_role_and_label(accesskit::Role::TextInput, "Char. row:") + .type_text("invalid"); + harness.run(); + + // Enter scan line value "0x08" + harness + .get_by_role_and_label(accesskit::Role::TextInput, "Scan line:") + .type_text("0x08"); + harness.run(); + + // Enter horizontal value "0xaf" + harness + .get_by_role_and_label(accesskit::Role::TextInput, "Horizontal:") + .type_text("0xaf"); + harness.run(); + + // Add breakpoint + harness + .get_all_by_role_and_label(accesskit::Role::Button, "Add") + .nth(1) + .unwrap() + .click(); + harness.run(); + drop(harness); + + assert_eq!(debugger.breakpoint_manager().breakpoints_iter().count(), 0); + } + + #[test] + fn test_crtc_debug_window_counters_breakpoint_invalid_scan_line() { + let mut debugger = TestDebugger::default(); + let mut window = CrtcDebugWindow { + show: true, + ..Default::default() + }; + + let app = |ctx: &egui::Context| { + window.ui(ctx, &mut debugger); + }; + + let mut harness = Harness::new(app); + harness.run(); + + // Enter character row value "0x42" + harness + .get_by_role_and_label(accesskit::Role::TextInput, "Char. row:") + .type_text("0x42"); + harness.run(); + + // Enter scan line value "invalid" + harness + .get_by_role_and_label(accesskit::Role::TextInput, "Scan line:") + .type_text("invalid"); + harness.run(); + + // Enter horizontal value "0xaf" + harness + .get_by_role_and_label(accesskit::Role::TextInput, "Horizontal:") + .type_text("0xaf"); + harness.run(); + + // Add breakpoint + harness + .get_all_by_role_and_label(accesskit::Role::Button, "Add") + .nth(1) + .unwrap() + .click(); + harness.run(); + drop(harness); + + assert_eq!(debugger.breakpoint_manager().breakpoints_iter().count(), 0); + } + + #[test] + fn test_crtc_debug_window_counters_breakpoint_invalid_horizontal() { + let mut debugger = TestDebugger::default(); + let mut window = CrtcDebugWindow { + show: true, + ..Default::default() + }; + + let app = |ctx: &egui::Context| { + window.ui(ctx, &mut debugger); + }; + + let mut harness = Harness::new(app); + harness.run(); + + // Enter character row value "0x42" + harness + .get_by_role_and_label(accesskit::Role::TextInput, "Char. row:") + .type_text("0x42"); + harness.run(); + + // Enter scan line value "0x08" + harness + .get_by_role_and_label(accesskit::Role::TextInput, "Scan line:") + .type_text("0x08"); + harness.run(); + + // Enter horizontal value "invalid" + harness + .get_by_role_and_label(accesskit::Role::TextInput, "Horizontal:") + .type_text("invalid"); + harness.run(); + + // Add breakpoint + harness + .get_all_by_role_and_label(accesskit::Role::Button, "Add") + .nth(1) + .unwrap() + .click(); + harness.run(); + drop(harness); + + assert_eq!(debugger.breakpoint_manager().breakpoints_iter().count(), 0); + } + + #[test] + fn test_crtc_debug_window_counters_breakpoint_any_values() { + let mut debugger = TestDebugger::default(); + let mut window = CrtcDebugWindow { + show: true, + ..Default::default() + }; + + let app = |ctx: &egui::Context| { + window.ui(ctx, &mut debugger); + }; + + let mut harness = Harness::new(app); + harness.run(); + + // Enter character row value "0x42" + harness + .get_all_by_role_and_label(accesskit::Role::CheckBox, "Any") + .nth(2) + .unwrap() + .click(); + harness.run(); + + // Enter scan line value "0x42" + harness + .get_all_by_role_and_label(accesskit::Role::CheckBox, "Any") + .nth(3) + .unwrap() + .click(); + harness.run(); + + // Enter horizontal value "0x42" + harness + .get_all_by_role_and_label(accesskit::Role::CheckBox, "Any") + .nth(4) + .unwrap() + .click(); + harness.run(); + + // Add breakpoint + harness + .get_all_by_role_and_label(accesskit::Role::Button, "Add") + .nth(1) + .unwrap() + .click(); + harness.run(); + + // Disable breakpoint + harness.get_by_label("Counters = Any/Any/Any").click(); + harness.run(); + drop(harness); + + assert_eq!(debugger.breakpoint_manager().breakpoints_iter().count(), 1); + assert!( + debugger + .breakpoint_manager() + .breakpoints_iter() + .any(|(_, bp)| { + matches!( + bp, + AnyBreakpoint::CrtcCounters(CrtcCountersBreakpoint { + character_row: None, + scan_line: None, + horizontal_counter: None, + .. + }) + ) && !bp.enabled() + }) + ); + } + + #[test] + fn test_crtc_debug_window_address_breakpoint_with_value() { + let mut debugger = TestDebugger::default(); + let mut window = CrtcDebugWindow { + show: true, + ..Default::default() + }; + + let app = |ctx: &egui::Context| { + window.ui(ctx, &mut debugger); + }; + + let mut harness = Harness::new(app); + harness.run(); + + // Enter character row value "0x42" + harness + .get_by_role_and_label(accesskit::Role::TextInput, "Address:") + .type_text("0xbeef"); + harness.run(); + + // Add breakpoint + harness + .get_all_by_role_and_label(accesskit::Role::Button, "Add") + .nth(2) + .unwrap() + .click(); + harness.run(); + + assert!(harness.query_by_label("Address = 0xBEEF").is_some()); + + // Remove breakpoint + harness + .get_by_role_and_label(accesskit::Role::Button, "Remove") + .click(); + harness.run(); + + assert!(harness.query_by_label("Address = 0xBEEF").is_none()); + } + + #[test] + fn test_crtc_debug_window_address_breakpoint_any_value() { + let mut debugger = TestDebugger::default(); + let mut window = CrtcDebugWindow { + show: true, + ..Default::default() + }; + + let app = |ctx: &egui::Context| { + window.ui(ctx, &mut debugger); + }; + + let mut harness = Harness::new(app); + harness.run(); + + // Enter character row value "0x42" + harness + .get_all_by_role_and_label(accesskit::Role::CheckBox, "Any") + .nth(5) + .unwrap() + .click(); + harness.run(); + + // Add breakpoint + harness + .get_all_by_role_and_label(accesskit::Role::Button, "Add") + .nth(2) + .unwrap() + .click(); + harness.run(); + + // Disable breakpoint + harness.get_by_label("Any address change").click(); + harness.run(); + drop(harness); + + assert_eq!(debugger.breakpoint_manager().breakpoints_iter().count(), 1); + assert!( + debugger + .breakpoint_manager() + .breakpoints_iter() + .any(|(_, bp)| { + matches!( + bp, + AnyBreakpoint::CrtcAddress(CrtcAddressBreakpoint { value: None, .. }) + ) && !bp.enabled() + }) + ); + } + + #[test] + fn test_crtc_debug_window_hsync_breakpoint() { + let mut debugger = TestDebugger::default(); + let mut window = CrtcDebugWindow { + show: true, + ..Default::default() + }; + + let app = |ctx: &egui::Context| { + window.ui(ctx, &mut debugger); + }; + + let mut harness = Harness::new(app); + harness.run(); + + // Check start + harness + .get_all_by_role_and_label(accesskit::Role::CheckBox, "Start") + .next() + .unwrap() + .click(); + harness.run(); + + // Check end + harness + .get_all_by_role_and_label(accesskit::Role::CheckBox, "End") + .next() + .unwrap() + .click(); + harness.run(); + + // Add breakpoint + harness + .get_all_by_role_and_label(accesskit::Role::Button, "Add") + .nth(3) + .unwrap() + .click(); + harness.run(); + + assert!(harness.query_by_label("HSYNC start or end").is_some()); + + // Remove breakpoint + harness + .get_by_role_and_label(accesskit::Role::Button, "Remove") + .click(); + harness.run(); + + assert!(harness.query_by_label("HSYNC start or end").is_none()); + } + + #[test] + fn test_crtc_debug_window_hsync_breakpoint_invalid() { + let mut debugger = TestDebugger::default(); + let mut window = CrtcDebugWindow { + show: true, + ..Default::default() + }; + + let app = |ctx: &egui::Context| { + window.ui(ctx, &mut debugger); + }; + + let mut harness = Harness::new(app); + harness.run(); + + // Add breakpoint + harness + .get_all_by_role_and_label(accesskit::Role::Button, "Add") + .nth(3) + .unwrap() + .click(); + harness.run(); + drop(harness); + + assert_eq!(debugger.breakpoint_manager().breakpoints_iter().count(), 0); + } + + #[test] + fn test_crtc_debug_window_vsync_breakpoint() { + let mut debugger = TestDebugger::default(); + let mut window = CrtcDebugWindow { + show: true, + ..Default::default() + }; + + let app = |ctx: &egui::Context| { + window.ui(ctx, &mut debugger); + }; + + let mut harness = Harness::new(app); + harness.run(); + + // Check start + harness + .get_all_by_role_and_label(accesskit::Role::CheckBox, "Start") + .nth(1) + .unwrap() + .click(); + harness.run(); + + // Check end + harness + .get_all_by_role_and_label(accesskit::Role::CheckBox, "End") + .nth(1) + .unwrap() + .click(); + harness.run(); + + // Add breakpoint + harness + .get_all_by_role_and_label(accesskit::Role::Button, "Add") + .nth(4) + .unwrap() + .click(); + harness.run(); + + assert!(harness.query_by_label("VSYNC start or end").is_some()); + + // Remove breakpoint + harness + .get_by_role_and_label(accesskit::Role::Button, "Remove") + .click(); + harness.run(); + + assert!(harness.query_by_label("VSYNC start or end").is_none()); + } + + #[test] + fn test_crtc_debug_window_vsync_breakpoint_invalid() { + let mut debugger = TestDebugger::default(); + let mut window = CrtcDebugWindow { + show: true, + ..Default::default() + }; + + let app = |ctx: &egui::Context| { + window.ui(ctx, &mut debugger); + }; + + let mut harness = Harness::new(app); + harness.run(); + + // Add breakpoint + harness + .get_all_by_role_and_label(accesskit::Role::Button, "Add") + .nth(4) + .unwrap() + .click(); + harness.run(); + drop(harness); + + assert_eq!(debugger.breakpoint_manager().breakpoints_iter().count(), 0); + } + + #[test] + fn test_crtc_debug_window_display_enable_breakpoint() { + let mut debugger = TestDebugger::default(); + let mut window = CrtcDebugWindow { + show: true, + ..Default::default() + }; + + let app = |ctx: &egui::Context| { + window.ui(ctx, &mut debugger); + }; + + let mut harness = Harness::new(app); + harness.run(); + + // Check start + harness + .get_all_by_role_and_label(accesskit::Role::CheckBox, "Start") + .nth(2) + .unwrap() + .click(); + harness.run(); + + // Check end + harness + .get_all_by_role_and_label(accesskit::Role::CheckBox, "End") + .nth(2) + .unwrap() + .click(); + harness.run(); + + // Add breakpoint + harness + .get_all_by_role_and_label(accesskit::Role::Button, "Add") + .nth(5) + .unwrap() + .click(); + harness.run(); + + assert!( + harness + .query_by_label("DISP. ENABLE start or end") + .is_some() + ); + + // Remove breakpoint + harness + .get_by_role_and_label(accesskit::Role::Button, "Remove") + .click(); + harness.run(); + + assert!( + harness + .query_by_label("DISP. ENABLE start or end") + .is_none() + ); + } + + #[test] + fn test_crtc_debug_window_display_enable_breakpoint_invalid() { + let mut debugger = TestDebugger::default(); + let mut window = CrtcDebugWindow { + show: true, + ..Default::default() + }; + + let app = |ctx: &egui::Context| { + window.ui(ctx, &mut debugger); + }; + + let mut harness = Harness::new(app); + harness.run(); + + // Add breakpoint + harness + .get_all_by_role_and_label(accesskit::Role::Button, "Add") + .nth(5) + .unwrap() + .click(); + harness.run(); + drop(harness); + + assert_eq!(debugger.breakpoint_manager().breakpoints_iter().count(), 0); + } +} diff --git a/ronald-egui/src/debug/gate_array.rs b/ronald-egui/src/debug/gate_array.rs new file mode 100644 index 0000000..4b427cb --- /dev/null +++ b/ronald-egui/src/debug/gate_array.rs @@ -0,0 +1,828 @@ +use eframe::egui; +use serde::{Deserialize, Serialize}; + +use ronald_core::constants::{FIRMWARE_COLORS, HARDWARE_TO_FIRMWARE_COLORS}; +use ronald_core::debug::breakpoint::{AnyBreakpoint, Breakpoint}; + +use crate::colors; +use crate::debug::Debugger; + +#[derive(Default, Serialize, Deserialize)] +#[serde(default)] +pub struct GateArrayDebugWindow { + pub show: bool, + + // Screen mode breakpoint + #[serde(skip, default)] + screen_mode_value: Option, + #[serde(skip, default)] + screen_mode_any_change: bool, + #[serde(skip, default)] + screen_mode_applied: bool, + + // Pen color breakpoint + #[serde(skip, default)] + pen_number: Option, + #[serde(skip, default)] + pen_color: Option, + #[serde(skip, default)] + pen_any_number: bool, + #[serde(skip, default)] + pen_any_color: bool, +} + +impl GateArrayDebugWindow { + fn get_all_hardware_colors() -> Vec<(usize, egui::Color32, String)> { + let mut colors = Vec::new(); + + for (hardware_index, firmware_color_index) in HARDWARE_TO_FIRMWARE_COLORS.iter().enumerate() + { + let rgba = FIRMWARE_COLORS[*firmware_color_index]; + let egui_color = + egui::Color32::from_rgba_premultiplied(rgba[0], rgba[1], rgba[2], rgba[3]); + + let color_name = match firmware_color_index { + 0 => "Black", + 1 => "Blue", + 2 => "Bright Blue", + 3 => "Red", + 4 => "Magenta", + 5 => "Mauve", + 6 => "Bright Red", + 7 => "Purple", + 8 => "Bright Magenta", + 9 => "Green", + 10 => "Cyan", + 11 => "Sky Blue", + 12 => "Yellow", + 13 => "White", // Actually grey in some contexts + 14 => "Pastel Blue", + 15 => "Orange", + 16 => "Pink", + 17 => "Pastel Magenta", + 18 => "Bright Green", + 19 => "Sea Green", + 20 => "Bright Cyan", + 21 => "Lime", + 22 => "Pastel Green", + 23 => "Pastel Cyan", + 24 => "Bright Yellow", + 25 => "Pastel Yellow", + 26 => "Bright White", + _ => "Unknown", + }; + + let display_name = format!("{} (0x{:02X})", color_name, hardware_index); + colors.push((hardware_index, egui_color, display_name)); + } + + colors + } + + pub fn ui(&mut self, ctx: &egui::Context, debugger: &mut impl Debugger) { + let mut open = self.show; + egui::Window::new("Gate Array Internals") + .resizable(false) + .open(&mut open) + .show(ctx, |ui| { + self.render_gate_array_state(ui, debugger); + ui.separator(); + self.render_breakpoints_section(ui, debugger); + }); + self.show = open; + } + + fn render_gate_array_state(&self, ui: &mut egui::Ui, debugger: &mut impl Debugger) { + let debug_view = debugger.debug_view(); + let ga = &debug_view.gate_array; + + // Screen Mode Section + ui.heading("Screen Mode"); + ui.horizontal(|ui| { + ui.label("Current:"); + ui.label(format!("{}", ga.current_screen_mode)); + ui.separator(); + ui.label("Requested:"); + let requested_text = match ga.requested_screen_mode { + Some(mode) => format!("{}", mode), + None => "-".to_string(), + }; + ui.label(requested_text); + }); + ui.add_space(8.0); + + // Sync Status Section + ui.heading("Sync Status"); + ui.horizontal(|ui| { + let hsync_color = if ga.hsync_active { + colors::FORREST_GREEN + } else { + colors::MEDIUM_GRAY + }; + ui.colored_label(hsync_color, "HSYNC"); + + ui.separator(); + + let vsync_color = if ga.vsync_active { + colors::FORREST_GREEN + } else { + colors::MEDIUM_GRAY + }; + ui.colored_label(vsync_color, "VSYNC"); + + ui.separator(); + + ui.label("HSYNCs since VSYNC:"); + ui.label(format!("{}", ga.hsyncs_since_last_vsync)); + }); + ui.add_space(8.0); + + // Interrupt System Section + ui.heading("Interrupt System"); + ui.horizontal(|ui| { + ui.label("Counter:"); + ui.label(format!("{}", ga.interrupt_counter)); + ui.separator(); + ui.label("Hold:"); + let hold_text = if ga.hold_interrupt { "Yes" } else { "No" }; + ui.label(hold_text); + }); + ui.add_space(8.0); + + // Palette Section + ui.heading("Palette"); + ui.label("Selected Pen:"); + ui.label(format!("{}", ga.selected_pen)); + ui.add_space(4.0); + + // Display pens 0-15 in a 4x4 grid + egui::Grid::new("pen_colors_grid") + .num_columns(4) + .spacing([8.0, 8.0]) + .show(ui, |ui| { + for pen in 0..16 { + ui.vertical(|ui| { + let color_index = ga.pen_colors[pen] as usize; + let firmware_color_index = HARDWARE_TO_FIRMWARE_COLORS[color_index]; + let rgba = FIRMWARE_COLORS[firmware_color_index]; + let egui_color = egui::Color32::from_rgba_premultiplied( + rgba[0], rgba[1], rgba[2], rgba[3], + ); + + ui.label(format!("Pen {}", pen)); + let (rect, _) = + ui.allocate_exact_size(egui::vec2(40.0, 30.0), egui::Sense::hover()); + ui.painter().rect_filled(rect, 2.0, egui_color); + + // Show hardware color value + ui.label(format!("{:#04X}", ga.pen_colors[pen])); + }); + + if (pen + 1) % 4 == 0 { + ui.end_row(); + } + } + }); + + ui.add_space(8.0); + + // Border Color + ui.label("Border:"); + ui.horizontal(|ui| { + let color_index = ga.pen_colors[0x10] as usize; + let firmware_color_index = HARDWARE_TO_FIRMWARE_COLORS[color_index]; + let rgba = FIRMWARE_COLORS[firmware_color_index]; + let egui_color = + egui::Color32::from_rgba_premultiplied(rgba[0], rgba[1], rgba[2], rgba[3]); + + let (rect, _) = ui.allocate_exact_size(egui::vec2(40.0, 30.0), egui::Sense::hover()); + ui.painter().rect_filled(rect, 2.0, egui_color); + + ui.label(format!("{:#04x}", ga.pen_colors[0x10])); + }); + } + + fn render_breakpoints_section(&mut self, ui: &mut egui::Ui, debugger: &mut impl Debugger) { + ui.heading("Gate Array Breakpoints"); + + egui::Grid::new("ga_breakpoint_grid") + .num_columns(2) + .spacing([10.0, 4.0]) + .show(ui, |ui| { + // Screen mode breakpoint + ui.label("Screen mode:"); + + ui.horizontal(|ui| { + ui.add_enabled_ui(!self.screen_mode_any_change, |ui| { + egui::ComboBox::from_id_salt("screen_mode_selector") + .width(60.0) + .selected_text(match self.screen_mode_value { + Some(mode) => format!("Mode {}", mode), + None => "Select...".to_string(), + }) + .show_ui(ui, |ui| { + ui.selectable_value(&mut self.screen_mode_value, Some(0), "Mode 0"); + ui.selectable_value(&mut self.screen_mode_value, Some(1), "Mode 1"); + ui.selectable_value(&mut self.screen_mode_value, Some(2), "Mode 2"); + ui.selectable_value(&mut self.screen_mode_value, Some(3), "Mode 3"); + }); + }); + + if ui + .checkbox(&mut self.screen_mode_any_change, "Any") + .changed() + && self.screen_mode_any_change + { + self.screen_mode_value = None; + } + + ui.checkbox(&mut self.screen_mode_applied, "Applied") + .on_hover_text("Break when mode is applied (at next HSYNC) vs when requested (written to register)"); + + if ui.button("Add").clicked() { + self.add_screen_mode_breakpoint(debugger); + } + }); + ui.end_row(); + + // Pen color breakpoint + ui.label("Pen:"); + + ui.horizontal(|ui| { + ui.add_enabled_ui(!self.pen_any_number, |ui| { + egui::ComboBox::from_id_salt("pen_number_selector") + .width(80.0) + .selected_text(match self.pen_number { + Some(16) => "Border".to_string(), + Some(pen) => format!("Pen {}", pen), + None => "Select...".to_string(), + }) + .show_ui(ui, |ui| { + for pen in 0..16 { + ui.selectable_value(&mut self.pen_number, Some(pen), format!("Pen {}", pen)); + } + ui.selectable_value(&mut self.pen_number, Some(16), "Border"); + }); + }); + + if ui.checkbox(&mut self.pen_any_number, "Any").changed() && self.pen_any_number { + self.pen_number = None; + } + + ui.label("Color:"); + + let colors = Self::get_all_hardware_colors(); + let (selected_color, selected_text) = match self.pen_color { + Some(selected) => { + let (_, color, name) = &colors[selected]; + (*color, name.as_str()) + } + None => (colors::BLACK, "Select..."), + }; + + + ui.add_enabled_ui(!self.pen_any_color, |ui| { + ui.horizontal(|ui| { + // Show swatch for currently selected color + let (swatch_rect, _) = ui.allocate_exact_size(egui::vec2(20.0, 15.0), egui::Sense::hover()); + ui.painter().rect_filled(swatch_rect, 2.0, selected_color); + + // ComboBox with text only, disabled when pen_any_color is true + egui::ComboBox::from_id_salt("pen_color_selector") + .width(150.0) + .selected_text(selected_text) + .show_ui(ui, |ui| { + for (hardware_index, color, name) in colors.iter() { + ui.horizontal(|ui| { + // Color swatch + let (swatch_rect, _) = ui.allocate_exact_size(egui::vec2(20.0, 15.0), egui::Sense::hover()); + ui.painter().rect_filled(swatch_rect, 2.0, *color); + + // Color name with hardware value + ui.selectable_value(&mut self.pen_color, Some(*hardware_index), name); + }); + } + }) + }) + }); + + if ui.checkbox(&mut self.pen_any_color, "Any").changed() && self.pen_any_color { + self.pen_color = None + } + + if ui.button("Add").clicked() { + self.add_pen_color_breakpoint(debugger); + } + }); + ui.end_row(); + + // Interrupt breakpoint + ui.label("Interrupt:"); + + ui.horizontal(|ui| { + if ui.button("Add Interrupt Breakpoint").clicked() { + self.add_interrupt_breakpoint(debugger); + } + }); + ui.end_row(); + }); + + // List active Gate Array breakpoints + ui.separator(); + ui.label("Active Gate Array Breakpoints:"); + + let mut breakpoint_found = false; + let mut to_remove = None; + let mut to_toggle = None; + + let breakpoint_manager = debugger.breakpoint_manager(); + for (id, breakpoint) in breakpoint_manager.breakpoints_iter() { + if breakpoint.one_shot() + || !matches!( + breakpoint, + AnyBreakpoint::GateArrayScreenMode(_) + | AnyBreakpoint::GateArrayPenColor(_) + | AnyBreakpoint::GateArrayInterrupt(_) + ) + { + continue; + } + + breakpoint_found = true; + + ui.horizontal(|ui| { + let mut enabled = breakpoint.enabled(); + if ui.checkbox(&mut enabled, breakpoint.to_string()).changed() { + to_toggle = Some((*id, enabled)); + } + + if ui.button("Remove").clicked() { + to_remove = Some(*id); + } + + if let Some(master_clock) = breakpoint.triggered() { + ui.colored_label( + colors::DARK_RED, + format!("(triggered at {})", master_clock.value()), + ); + } + }); + } + + if !breakpoint_found { + ui.label("No Gate Array breakpoints set"); + } + + // Apply changes + if let Some((id, enabled)) = to_toggle { + breakpoint_manager.enable_breakpoint(id, enabled); + } + if let Some(id) = to_remove { + breakpoint_manager.remove_breakpoint(id); + } + } + + fn add_screen_mode_breakpoint(&mut self, debugger: &mut impl Debugger) { + let mode = if self.screen_mode_any_change { + None + } else { + self.screen_mode_value + }; + + let breakpoint = + AnyBreakpoint::gate_array_screen_mode_breakpoint(mode, self.screen_mode_applied); + debugger.breakpoint_manager().add_breakpoint(breakpoint); + self.screen_mode_value = None; + self.screen_mode_any_change = false; + self.screen_mode_applied = false; + } + + fn add_pen_color_breakpoint(&mut self, debugger: &mut impl Debugger) { + let pen = if self.pen_any_number { + None + } else { + self.pen_number + }; + let color = self.pen_color.map(|c| c as u8); + + let breakpoint = AnyBreakpoint::gate_array_pen_color_breakpoint(pen, color); + debugger.breakpoint_manager().add_breakpoint(breakpoint); + self.pen_number = None; + self.pen_color = None; + self.pen_any_number = false; + self.pen_any_color = false; + } + + fn add_interrupt_breakpoint(&mut self, debugger: &mut impl Debugger) { + let breakpoint = AnyBreakpoint::gate_array_interrupt_breakpoint(); + debugger.breakpoint_manager().add_breakpoint(breakpoint); + } +} + +#[cfg(test)] +mod gui_tests { + use super::*; + + use egui::accesskit; + use egui_kittest::{Harness, kittest::Queryable}; + + use ronald_core::debug::breakpoint::GateArrayScreenModeBreakpoint; + + use crate::debug::mock::TestDebugger; + + #[test] + fn test_gate_array_debug_window_opens_and_closes() { + let mut debugger = TestDebugger::default(); + let mut window = GateArrayDebugWindow { + show: true, + ..Default::default() + }; + + let app = |ctx: &egui::Context| { + window.ui(ctx, &mut debugger); + }; + + let mut harness = Harness::new(app); + harness.run(); + + // Check that the window title is rendered + harness.get_by_label("Gate Array Internals"); + + // Click close button + harness.get_by_label("Close window").click(); + harness.run(); + + // Window should no longer be visible + assert!(harness.query_by_label("Gate Array Internals").is_none()); + } + + #[test] + fn test_gate_array_debug_window_screen_mode_requested_breakpoint() { + let mut debugger = TestDebugger::default(); + let mut window = GateArrayDebugWindow { + show: true, + ..Default::default() + }; + + let app = |ctx: &egui::Context| { + window.ui(ctx, &mut debugger); + }; + + let mut harness = Harness::new(app); + harness.run(); + + let i = 0; + + // Select "Mode 1" + harness + .get_all_by_role(accesskit::Role::ComboBox) + .nth(i) + .unwrap() + .click(); + harness.run(); + harness.get_by_label("Mode 1").click(); + harness.run(); + + // Add breakpoint + harness + .get_all_by_role_and_label(accesskit::Role::Button, "Add") + .nth(i) + .unwrap() + .click(); + harness.run(); + + assert!( + harness + .query_by_label("Screen mode requested = 1") + .is_some() + ); + + // Remove breakpoint + harness + .get_by_role_and_label(accesskit::Role::Button, "Remove") + .click(); + harness.run(); + + assert!( + harness + .query_by_label("Screen mode requested = 1") + .is_none() + ); + } + + #[test] + fn test_gate_array_debug_window_screen_mode_requested_breakpoint_any() { + let mut debugger = TestDebugger::default(); + let mut window = GateArrayDebugWindow { + show: true, + ..Default::default() + }; + + let app = |ctx: &egui::Context| { + window.ui(ctx, &mut debugger); + }; + + let mut harness = Harness::new(app); + harness.run(); + + let i = 0; + + // Check any + harness + .get_all_by_role_and_label(accesskit::Role::CheckBox, "Any") + .nth(i) + .unwrap() + .click(); + harness.run(); + + // Add breakpoint + harness + .get_all_by_role_and_label(accesskit::Role::Button, "Add") + .nth(i) + .unwrap() + .click(); + harness.run(); + + // Disable breakpoint + harness.get_by_label("Screen mode requested").click(); + harness.run(); + drop(harness); + + assert_eq!(debugger.breakpoint_manager().breakpoints_iter().count(), 1); + assert!( + debugger + .breakpoint_manager() + .breakpoints_iter() + .any(|(_, bp)| { + matches!( + bp, + AnyBreakpoint::GateArrayScreenMode(GateArrayScreenModeBreakpoint { + mode: None, + applied: false, + .. + }) + ) && !bp.enabled() + }) + ); + } + + #[test] + fn test_gate_array_debug_window_screen_mode_applied_breakpoint() { + let mut debugger = TestDebugger::default(); + let mut window = GateArrayDebugWindow { + show: true, + ..Default::default() + }; + + let app = |ctx: &egui::Context| { + window.ui(ctx, &mut debugger); + }; + + let mut harness = Harness::new(app); + harness.run(); + + let i = 0; + + // Select "Mode 1" + harness + .get_all_by_role(accesskit::Role::ComboBox) + .nth(i) + .unwrap() + .click(); + harness.run(); + harness.get_by_label("Mode 1").click(); + harness.run(); + + // Check applied + harness + .get_by_role_and_label(accesskit::Role::CheckBox, "Applied") + .click(); + harness.run(); + + // Add breakpoint + harness + .get_all_by_role_and_label(accesskit::Role::Button, "Add") + .nth(i) + .unwrap() + .click(); + harness.run(); + + assert!(harness.query_by_label("Screen mode applied = 1").is_some()); + + // Remove breakpoint + harness + .get_by_role_and_label(accesskit::Role::Button, "Remove") + .click(); + harness.run(); + + assert!(harness.query_by_label("Screen mode applied = 1").is_none()); + } + + #[test] + fn test_gate_array_debug_window_pen_breakpoint_with_pen_and_color() { + let mut debugger = TestDebugger::default(); + let mut window = GateArrayDebugWindow { + show: true, + ..Default::default() + }; + + let app = |ctx: &egui::Context| { + window.ui(ctx, &mut debugger); + }; + + let mut harness = Harness::new(app); + harness.run(); + + // Select "Pen 8" + harness + .get_all_by_role(accesskit::Role::ComboBox) + .nth(1) + .unwrap() + .click(); + harness.run(); + harness + .get_by_role_and_label(accesskit::Role::Button, "Pen 8") + .click(); + harness.run(); + + // Select "Blue (0x04)" + harness + .get_all_by_role(accesskit::Role::ComboBox) + .nth(2) + .unwrap() + .click(); + harness.run(); + harness.get_by_label("Blue (0x04)").click(); + harness.run(); + + // Add breakpoint + harness + .get_all_by_role_and_label(accesskit::Role::Button, "Add") + .nth(1) + .unwrap() + .click(); + harness.run(); + + assert!(harness.query_by_label("Pen 8 = 0x04").is_some()); + + // Remove breakpoint + harness + .get_by_role_and_label(accesskit::Role::Button, "Remove") + .click(); + harness.run(); + + assert!(harness.query_by_label("Pen 8 = 0x04").is_none()); + } + + #[test] + fn test_gate_array_debug_window_pen_breakpoint_with_border_and_any_color() { + let mut debugger = TestDebugger::default(); + let mut window = GateArrayDebugWindow { + show: true, + ..Default::default() + }; + + let app = |ctx: &egui::Context| { + window.ui(ctx, &mut debugger); + }; + + let mut harness = Harness::new(app); + harness.run(); + + // Select "Border" + harness + .get_all_by_role(accesskit::Role::ComboBox) + .nth(1) + .unwrap() + .click(); + harness.run(); + harness + .get_by_role_and_label(accesskit::Role::Button, "Border") + .click(); + harness.run(); + + // Check any color + harness + .get_all_by_role_and_label(accesskit::Role::CheckBox, "Any") + .nth(2) + .unwrap() + .click(); + harness.run(); + + // Add breakpoint + harness + .get_all_by_role_and_label(accesskit::Role::Button, "Add") + .nth(1) + .unwrap() + .click(); + harness.run(); + + assert!(harness.query_by_label("Border changed").is_some()); + + // Remove breakpoint + harness + .get_by_role_and_label(accesskit::Role::Button, "Remove") + .click(); + harness.run(); + + assert!(harness.query_by_label("Border changed").is_none()); + } + + #[test] + fn test_gate_array_debug_window_pen_breakpoint_with_any_pen_and_any_color() { + let mut debugger = TestDebugger::default(); + let mut window = GateArrayDebugWindow { + show: true, + ..Default::default() + }; + + let app = |ctx: &egui::Context| { + window.ui(ctx, &mut debugger); + }; + + let mut harness = Harness::new(app); + harness.run(); + + // Check any pen + harness + .get_all_by_role_and_label(accesskit::Role::CheckBox, "Any") + .nth(1) + .unwrap() + .click(); + harness.run(); + + // Check any color + harness + .get_all_by_role_and_label(accesskit::Role::CheckBox, "Any") + .nth(2) + .unwrap() + .click(); + harness.run(); + + // Add breakpoint + harness + .get_all_by_role_and_label(accesskit::Role::Button, "Add") + .nth(1) + .unwrap() + .click(); + harness.run(); + + // Disable breakpoint + harness.get_by_label("Any pen changed").click(); + harness.run(); + drop(harness); + + assert_eq!(debugger.breakpoint_manager().breakpoints_iter().count(), 1); + assert!( + debugger + .breakpoint_manager() + .breakpoints_iter() + .any(|(_, bp)| { + matches!( + bp, + AnyBreakpoint::GateArrayPenColor( + ronald_core::debug::breakpoint::GateArrayPenColorBreakpoint { + pen: None, + color: None, + .. + } + ) + ) && !bp.enabled() + }) + ); + } + + #[test] + fn test_gate_array_debug_window_interrupt_breakpoint() { + let mut debugger = TestDebugger::default(); + let mut window = GateArrayDebugWindow { + show: true, + ..Default::default() + }; + + let app = |ctx: &egui::Context| { + window.ui(ctx, &mut debugger); + }; + + let mut harness = Harness::new(app); + harness.run(); + + let i = 0; + + // Add breakpoint + harness + .get_by_role_and_label(accesskit::Role::Button, "Add Interrupt Breakpoint") + .click(); + harness.run(); + + assert!(harness.query_by_label("Interrupt generated").is_some()); + + // Remove breakpoint + harness + .get_by_role_and_label(accesskit::Role::Button, "Remove") + .click(); + harness.run(); + + assert!(harness.query_by_label("Interrupt generated").is_none()); + } +} diff --git a/ronald-egui/src/debug/memory.rs b/ronald-egui/src/debug/memory.rs new file mode 100644 index 0000000..e531e41 --- /dev/null +++ b/ronald-egui/src/debug/memory.rs @@ -0,0 +1,1102 @@ +use std::collections::HashMap; + +use eframe::egui; +use serde::{Deserialize, Serialize}; + +use ronald_core::{ + debug::{ + breakpoint::{AnyBreakpoint, Breakpoint, BreakpointId, BreakpointManager}, + view::MemoryDebugView, + }, + system::{cpu::Register16, instruction::DecodedInstruction}, +}; + +use crate::{colors, debug::Debugger}; + +#[derive(Deserialize, Serialize)] +pub struct MemoryDebugWindow { + pub show: bool, + jump_to_address: Option, + address_input: String, + view_mode: MemoryViewMode, + memory_colors: MemorySourceColors, + pc_breakpoint_input: String, + disassembly_start: Option, + #[serde(skip)] + cached_disassembly: Option>, +} + +#[derive(Deserialize, Serialize)] +struct MemorySourceColors { + lower_rom: egui::Color32, + upper_rom: egui::Color32, + ram: egui::Color32, + extension_ram: egui::Color32, +} + +#[derive(Deserialize, Serialize, PartialEq)] +enum MemoryViewMode { + Disassembly, + CompositeRomRam, + CompositeRam, + LowerRomOnly, + UpperRomOnly(u8), + RamOnly, + ExtensionRamOnly, +} + +impl Default for MemoryDebugWindow { + fn default() -> Self { + Self { + show: false, + jump_to_address: None, + address_input: String::new(), + view_mode: MemoryViewMode::CompositeRomRam, + memory_colors: MemorySourceColors::default(), + pc_breakpoint_input: String::new(), + disassembly_start: None, + cached_disassembly: None, + } + } +} + +impl Default for MemorySourceColors { + fn default() -> Self { + Self { + lower_rom: colors::DARK_ORANGE, + upper_rom: colors::DEEP_MAGENTA, + ram: colors::FORREST_GREEN, + extension_ram: colors::DODGER_BLUE, + } + } +} + +impl MemorySourceColors { + fn get_color_for_mode(&self, mode: &MemoryViewMode) -> egui::Color32 { + match mode { + MemoryViewMode::LowerRomOnly => self.lower_rom, + MemoryViewMode::UpperRomOnly(_) => self.upper_rom, + MemoryViewMode::RamOnly => self.ram, + MemoryViewMode::ExtensionRamOnly => self.extension_ram, + _ => colors::DARK_GRAY, + } + } +} + +impl MemoryDebugWindow { + fn get_memory_source_color(&self, addr: usize, data: &MemoryDebugView) -> egui::Color32 { + match &self.view_mode { + MemoryViewMode::Disassembly | MemoryViewMode::CompositeRomRam => { + // CPC 464 memory map: + // 0x0000-0x3FFF: Lower ROM (if enabled) or RAM + // 0x4000-0x7FFF: RAM + // 0x8000-0xBFFF: RAM + // 0xC000-0xFFFF: Upper ROM (if enabled) or RAM + if addr < 0x4000 && data.lower_rom_enabled { + self.memory_colors.lower_rom + } else if addr >= 0xC000 && data.upper_rom_enabled { + self.memory_colors.upper_rom + } else { + self.memory_colors.ram + } + } + MemoryViewMode::CompositeRam => { + // All composite RAM uses RAM color + self.memory_colors.ram + } + _ => { + // For single-source modes, use the mode's color + self.memory_colors.get_color_for_mode(&self.view_mode) + } + } + } + fn render_view_mode_selector(&mut self, ui: &mut egui::Ui, debugger: &mut impl Debugger) { + let data = &debugger.debug_view().memory; + ui.horizontal(|ui| { + ui.label("View:"); + egui::ComboBox::from_label("") + .selected_text(match &self.view_mode { + MemoryViewMode::Disassembly => "Disassembly".to_string(), + MemoryViewMode::CompositeRomRam => "Composite ROM/RAM".to_string(), + MemoryViewMode::CompositeRam => "Composite RAM".to_string(), + MemoryViewMode::LowerRomOnly => "Lower ROM only".to_string(), + MemoryViewMode::UpperRomOnly(bank) => format!("Upper ROM #{:02X} only", bank), + MemoryViewMode::RamOnly => "RAM only".to_string(), + MemoryViewMode::ExtensionRamOnly => "Extension RAM only".to_string(), + }) + .show_ui(ui, |ui| { + ui.selectable_value( + &mut self.view_mode, + MemoryViewMode::Disassembly, + "Disassembly", + ); + ui.separator(); + ui.selectable_value( + &mut self.view_mode, + MemoryViewMode::CompositeRomRam, + "Composite ROM/RAM", + ); + ui.selectable_value( + &mut self.view_mode, + MemoryViewMode::CompositeRam, + "Composite RAM", + ); + let lower_rom_mode = MemoryViewMode::LowerRomOnly; + let color = self.memory_colors.get_color_for_mode(&lower_rom_mode); + ui.selectable_value( + &mut self.view_mode, + lower_rom_mode, + egui::RichText::new("Lower ROM only").color(color), + ); + + // Display all available upper ROM banks + let mut banks: Vec<_> = data.upper_roms.keys().collect(); + banks.sort(); + for &bank in banks { + let upper_rom_mode = MemoryViewMode::UpperRomOnly(bank); + let color = self.memory_colors.get_color_for_mode(&upper_rom_mode); + ui.selectable_value( + &mut self.view_mode, + upper_rom_mode, + egui::RichText::new(format!("Upper ROM #{:02X} only", bank)) + .color(color), + ); + } + + let ram_mode = MemoryViewMode::RamOnly; + let color = self.memory_colors.get_color_for_mode(&ram_mode); + ui.selectable_value( + &mut self.view_mode, + ram_mode, + egui::RichText::new("RAM only").color(color), + ); + + let ext_ram_mode = MemoryViewMode::ExtensionRamOnly; + let color = self.memory_colors.get_color_for_mode(&ext_ram_mode); + ui.selectable_value( + &mut self.view_mode, + ext_ram_mode, + egui::RichText::new("Extension RAM only").color(color), + ); + }); + }); + } + + fn render_color_configuration(&mut self, ui: &mut egui::Ui) { + ui.collapsing("Address Color Coding", |ui| { + ui.horizontal(|ui| { + let label = ui.label("Lower ROM:"); + ui.color_edit_button_srgba(&mut self.memory_colors.lower_rom) + .labelled_by(label.id); + + ui.separator(); + + let label = ui.label("Upper ROM:"); + ui.color_edit_button_srgba(&mut self.memory_colors.upper_rom) + .labelled_by(label.id); + + ui.separator(); + + let label = ui.label("RAM:"); + ui.color_edit_button_srgba(&mut self.memory_colors.ram) + .labelled_by(label.id); + + ui.separator(); + + let label = ui.label("Extension RAM:"); + ui.color_edit_button_srgba(&mut self.memory_colors.extension_ram) + .labelled_by(label.id); + }); + + if ui.button("Restore Defaults").clicked() { + self.memory_colors = MemorySourceColors::default(); + } + }); + } + + pub fn ui(&mut self, ctx: &egui::Context, debugger: &mut impl Debugger) { + if !self.show { + return; + } + + let mut open = self.show; + egui::Window::new("Memory Internals") + .open(&mut open) + .show(ctx, |ui| { + ui.heading("View Config"); + self.render_view_mode_selector(ui, debugger); + self.render_color_configuration(ui); + self.render_memory_controls(ui); + ui.separator(); + self.render_memory_status(ui, debugger); + self.render_memory_view(ui, debugger); + + // Reserve remaining vertical space so the window can grow larger + // ui.allocate_space(ui.available_size()); + }); + self.show = open; + } + + fn render_memory_controls(&mut self, ui: &mut egui::Ui) { + let size = match &self.view_mode { + MemoryViewMode::Disassembly => 0x10000, + MemoryViewMode::CompositeRomRam => 0x10000, + MemoryViewMode::CompositeRam => 0x10000, + MemoryViewMode::LowerRomOnly => 0x4000, + MemoryViewMode::UpperRomOnly(_) => 0x4000, + MemoryViewMode::RamOnly => 0x10000, + MemoryViewMode::ExtensionRamOnly => 0x10000, + }; + + ui.horizontal(|ui| { + let label = ui.label("Jump to address:"); + let text_edit = ui + .text_edit_singleline(&mut self.address_input) + .labelled_by(label.id); + let enter_pressed = + text_edit.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)); + let go_clicked = ui.button("Go").clicked(); + + if enter_pressed || go_clicked { + if self.view_mode == MemoryViewMode::Disassembly { + if let Ok(addr) = + usize::from_str_radix(self.address_input.trim_start_matches("0x"), 16) + { + log::debug!("Setting disassembly start address: {:04X}", addr); + // TODO: show toast when wrapping + self.disassembly_start = Some((addr % size) as u16); + self.cached_disassembly = None; // Invalidate cache + } else { + log::warn!("Failed to parse address: '{}'", self.address_input); + } + } else if let Ok(addr) = + usize::from_str_radix(self.address_input.trim_start_matches("0x"), 16) + { + log::debug!("Setting scroll target to address: {:04X}", addr); + // TODO: show toast when wrapping + self.jump_to_address = Some(addr % size); + } else { + log::warn!("Failed to parse address: '{}'", self.address_input); + } + } + + if self.view_mode == MemoryViewMode::Disassembly + && ui + .add( + egui::Button::new("Track Current PC") + .selected(self.disassembly_start.is_none()), + ) + .clicked() + { + self.disassembly_start = None; + } + }); + } + + fn render_memory_status(&self, ui: &mut egui::Ui, debugger: &mut impl Debugger) { + let data = &debugger.debug_view().memory; + match &self.view_mode { + MemoryViewMode::CompositeRomRam => { + ui.horizontal(|ui| { + ui.label("Lower ROM:"); + ui.colored_label( + if data.lower_rom_enabled { + colors::FORREST_GREEN + } else { + colors::DARK_RED + }, + if data.lower_rom_enabled { + "ENABLED" + } else { + "DISABLED" + }, + ); + ui.separator(); + ui.label("Upper ROM:"); + ui.colored_label( + if data.upper_rom_enabled { + colors::FORREST_GREEN + } else { + colors::DARK_RED + }, + if data.upper_rom_enabled { + "ENABLED" + } else { + "DISABLED" + }, + ); + ui.label(format!("(Selected: #{:02X})", data.selected_upper_rom)); + }); + ui.separator(); + } + MemoryViewMode::LowerRomOnly => { + ui.horizontal(|ui| { + ui.label("Lower ROM:"); + ui.colored_label( + if data.lower_rom_enabled { + colors::FORREST_GREEN + } else { + colors::DARK_RED + }, + if data.lower_rom_enabled { + "ENABLED" + } else { + "DISABLED" + }, + ); + }); + ui.separator(); + } + MemoryViewMode::UpperRomOnly(bank) => { + ui.horizontal(|ui| { + ui.label("Upper ROM:"); + ui.colored_label( + if data.upper_rom_enabled { + colors::FORREST_GREEN + } else { + colors::DARK_RED + }, + if data.upper_rom_enabled { + "ENABLED" + } else { + "DISABLED" + }, + ); + ui.label(format!( + "(Selected: #{:02X}, Viewing: #{:02X})", + data.selected_upper_rom, bank + )); + }); + ui.separator(); + } + _ => { + // No ROM status to show for other view modes + } + } + } + + fn render_memory_view(&mut self, ui: &mut egui::Ui, debugger: &mut impl Debugger) { + match &self.view_mode { + MemoryViewMode::Disassembly => { + self.render_disassembly_view(ui, debugger); + } + _ => { + self.render_memory_hex_dump(ui, debugger); + } + } + } + + fn render_memory_hex_dump(&mut self, ui: &mut egui::Ui, debugger: &mut impl Debugger) { + let data = &debugger.debug_view().memory; + ui.heading("Memory Contents:"); + + let memory_data = match &self.view_mode { + MemoryViewMode::Disassembly => { + unreachable!("Disassembly mode should be handled separately") + } + MemoryViewMode::CompositeRomRam => &data.composite_rom_ram, + MemoryViewMode::CompositeRam => &data.composite_ram, + MemoryViewMode::LowerRomOnly => &data.lower_rom, + MemoryViewMode::UpperRomOnly(bank) => { + if let Some(upper_rom) = data.upper_roms.get(bank) { + upper_rom + } else { + &Vec::::new()[..] + } + } + MemoryViewMode::RamOnly => &data.ram, + MemoryViewMode::ExtensionRamOnly => &data.ram_extension, + }; + + egui::ScrollArea::vertical() + .auto_shrink([false, false]) + .show(ui, |ui| { + ui.style_mut().override_font_id = Some(egui::FontId::monospace(12.0)); + + let target_addr = self.jump_to_address.take(); + for (row, chunk) in memory_data.chunks(16).enumerate() { + let addr = row * 16; + let response = self.render_hex_row(ui, chunk, addr, data); + + // If this row contains our target address, scroll to it immediately + if let Some(target) = target_addr + && target >= addr + && target < addr + 16 + { + response.scroll_to_me(Some(egui::Align::Min)); + } + } + }); + } + + fn render_hex_row( + &self, + ui: &mut egui::Ui, + chunk: &[u8], + addr: usize, + data: &MemoryDebugView, + ) -> egui::Response { + let row_color = self.get_memory_source_color(addr, data); + + ui.horizontal(|ui| { + // Address column with color coding + ui.colored_label(row_color, format!("{:04X}:", addr)); + + // Hex bytes + for (i, byte) in chunk.iter().enumerate() { + if i == 8 { + ui.label(" "); + } + ui.label(format!("{:02X}", byte)); + } + + ui.separator(); + + // ASCII column + let ascii: String = chunk + .iter() + .map(|&b| { + if (32..127).contains(&b) { + b as char + } else { + '.' + } + }) + .collect(); + + ui.monospace(ascii); + }) + .response + } + + fn render_disassembly_view(&mut self, ui: &mut egui::Ui, debugger: &mut impl Debugger) { + // Generate disassembly if we are tracking the current PC or if cache is empty + if self.disassembly_start.is_none() || self.cached_disassembly.is_none() { + let current_pc = debugger.debug_view().cpu.register_pc; + let start_addr = self.disassembly_start.unwrap_or(current_pc); + self.cached_disassembly = Some(debugger.disassemble(start_addr, 100)); + } + + // Add PC breakpoint controls + self.render_pc_breakpoint_controls(ui, debugger.breakpoint_manager()); + ui.separator(); + + // Build a HashMap from PC addresses to BreakpointIds for efficient lookup + let pc_breakpoints: HashMap = debugger + .breakpoint_manager() + .breakpoints_iter() + .filter_map(|(id, breakpoint)| { + if let AnyBreakpoint::CpuRegister16(bp) = breakpoint + && let Some(addr) = bp.value + && !bp.one_shot() + { + Some((addr, *id)) + } else { + None + } + }) + .collect(); + + ui.label("💡 Click on addresses to toggle PC breakpoints"); + ui.separator(); + + ui.heading("Disassembly"); + egui::ScrollArea::vertical() + .auto_shrink([false, false]) + .show(ui, |ui| { + ui.style_mut().override_font_id = Some(egui::FontId::monospace(12.0)); + + let target_addr = self.jump_to_address.take(); + let mut to_toggle = None; + + use egui_extras::{Column, TableBuilder}; + TableBuilder::new(ui) + .column(Column::auto()) + .column(Column::remainder()) + .column(Column::remainder()) + .body(|mut body| { + let data = debugger.debug_view(); + + for instruction in self + .cached_disassembly + .as_ref() + .expect("ensured to exist above") + { + let is_current_instruction = + instruction.address == data.cpu.register_pc; + let has_breakpoint = pc_breakpoints.contains_key(&instruction.address); + + body.row(18.0, |mut row| { + // Highlight the entire row if it's the current instruction + if is_current_instruction { + row.set_selected(true); + } + + row.col(|ui| { + let mut color = if is_current_instruction { + colors::WHITE + } else { + self.get_memory_source_color( + instruction.address as usize, + &data.memory, + ) + }; + + let mut addr_text = format!(" {:04X}:", instruction.address); + + // Add breakpoint indicator and change color + if has_breakpoint { + addr_text = format!("● {:04X}:", instruction.address); + if !is_current_instruction { + color = colors::LIGHT_RED; + } + } + + // Make address clickable to toggle breakpoint + if ui.colored_label(color, &addr_text).clicked() { + to_toggle = Some(instruction.address); + } + }); + + row.col(|ui| { + let color = if is_current_instruction { + colors::WHITE + } else { + colors::STEEL_BLUE + }; + ui.colored_label(color, instruction.instruction.to_string()); + }); + + row.col(|ui| { + let mut hex_bytes = String::new(); + for i in 0..instruction.length { + if i > 0 { + hex_bytes.push(' '); + } + let addr = instruction.address as usize + i; + if let Some(byte) = data.memory.composite_rom_ram.get(addr) + { + hex_bytes.push_str(&format!("{:02X}", byte)); + } else { + hex_bytes.push_str("??"); + } + } + let response = if is_current_instruction { + ui.colored_label(colors::WHITE, hex_bytes) + } else { + ui.monospace(hex_bytes) + }; + + if let Some(target) = target_addr + && target as u16 == instruction.address + { + response.scroll_to_me(Some(egui::Align::Min)); + } + }); + }); + } + }); + + // Handle breakpoint toggling after the table to avoid borrowing issues + if let Some(addr) = to_toggle { + let breakpoint_manager = debugger.breakpoint_manager(); + if let Some(&id) = pc_breakpoints.get(&addr) { + // Remove existing breakpoint + breakpoint_manager.remove_breakpoint(id); + } else { + // Add new breakpoint + breakpoint_manager.add_breakpoint(AnyBreakpoint::pc_breakpoint(addr)); + } + } + }); + } + + fn render_pc_breakpoint_controls( + &mut self, + ui: &mut egui::Ui, + breakpoint_manager: &mut BreakpointManager, + ) { + ui.heading("PC Breakpoints"); + + // PC breakpoint input + ui.horizontal(|ui| { + let label = ui.label("PC:"); + ui.text_edit_singleline(&mut self.pc_breakpoint_input) + .labelled_by(label.id); + + if ui.button("Add").clicked() + && let Ok(addr) = + u16::from_str_radix(self.pc_breakpoint_input.trim_start_matches("0x"), 16) + { + breakpoint_manager.add_breakpoint(AnyBreakpoint::pc_breakpoint(addr)); + self.pc_breakpoint_input.clear(); + } + }); + + // List active PC breakpoints + ui.separator(); + ui.label("Active PC Breakpoints:"); + + let mut breakpoint_found = false; + let mut to_remove = None; + let mut to_toggle = None; + + for (id, breakpoint) in breakpoint_manager.breakpoints_iter() { + if breakpoint.one_shot() { + continue; + } + if let AnyBreakpoint::CpuRegister16(bp) = breakpoint { + if bp.register != Register16::PC { + continue; + } + } else { + continue; + } + + breakpoint_found = true; + + ui.horizontal(|ui| { + let mut enabled = breakpoint.enabled(); + if ui.checkbox(&mut enabled, breakpoint.to_string()).changed() { + to_toggle = Some((*id, enabled)); + } + + if ui.button("Remove").clicked() { + to_remove = Some(*id); + } + + if let Some(master_clock) = breakpoint.triggered() { + ui.colored_label( + colors::DARK_RED, + format!("(triggered at {})", master_clock.value()), + ); + } + }); + } + + if !breakpoint_found { + ui.label("No PC breakpoints set"); + } + + // Apply changes outside the with_breakpoints closure + if let Some((id, enabled)) = to_toggle { + breakpoint_manager.enable_breakpoint(id, enabled); + } + if let Some(id) = to_remove { + breakpoint_manager.remove_breakpoint(id); + } + } +} + +#[cfg(test)] +mod gui_tests { + use super::*; + + use egui::accesskit; + use egui_kittest::{Harness, kittest::Queryable}; + + use ronald_core::debug::breakpoint::{CpuRegister16Breakpoint, GateArrayScreenModeBreakpoint}; + + use crate::debug::mock::TestDebugger; + + #[test] + fn test_memory_debug_window_opens_and_closes() { + let mut debugger = TestDebugger::default(); + let mut window = MemoryDebugWindow { + show: true, + ..Default::default() + }; + + let app = |ctx: &egui::Context| { + window.ui(ctx, &mut debugger); + }; + + let mut harness = Harness::new(app); + harness.run(); + + // Check that the window title is rendered + harness.get_by_label("Memory Internals"); + + // Click close button + harness.get_by_label("Close window").click(); + harness.run(); + + // Window should no longer be visible + assert!(harness.query_by_label("Memory Internals").is_none()); + } + + #[test] + fn test_memory_debug_window_pc_breakpoint_add_via_input_and_remove() { + let mut debugger = TestDebugger::default(); + let mut window = MemoryDebugWindow { + show: true, + ..Default::default() + }; + + let app = |ctx: &egui::Context| { + window.ui(ctx, &mut debugger); + }; + + let mut harness = Harness::new(app); + harness.run(); + + // Select "Disassembly" view mode + harness + .get_all_by_role(accesskit::Role::ComboBox) + .next() + .unwrap() + .click(); + harness.run(); + harness.get_by_label("Disassembly").click(); + harness.run(); + + // Type in PC address + harness + .get_by_role_and_label(accesskit::Role::TextInput, "PC:") + .type_text("0x0000"); + harness.run(); + + // Add breakpoint + harness + .get_by_role_and_label(accesskit::Role::Button, "Add") + .click(); + harness.run(); + + assert!(harness.query_by_label("PC = 0x0000").is_some()); + + // Remove breakpoint + harness + .get_by_role_and_label(accesskit::Role::Button, "Remove") + .click(); + harness.run(); + + assert!(harness.query_by_label("PC = 0x0000").is_none()); + } + + #[test] + fn test_memory_debug_window_pc_breakpoint_add_via_input_and_toggle() { + let mut debugger = TestDebugger::default(); + let mut window = MemoryDebugWindow { + show: true, + ..Default::default() + }; + + let app = |ctx: &egui::Context| { + window.ui(ctx, &mut debugger); + }; + + let mut harness = Harness::new(app); + harness.run(); + + // Select "Disassembly" view mode + harness + .get_all_by_role(accesskit::Role::ComboBox) + .next() + .unwrap() + .click(); + harness.run(); + harness.get_by_label("Disassembly").click(); + harness.run(); + + // Type in PC address + harness + .get_by_role_and_label(accesskit::Role::TextInput, "PC:") + .type_text("0x0000"); + harness.run(); + + // Add breakpoint + harness + .get_by_role_and_label(accesskit::Role::Button, "Add") + .click(); + harness.run(); + + // Disable breakpoint + harness.get_by_label("PC = 0x0000").click(); + harness.run(); + drop(harness); + + assert_eq!(debugger.breakpoint_manager().breakpoints_iter().count(), 1); + assert!( + debugger + .breakpoint_manager() + .breakpoints_iter() + .any(|(_, bp)| { + matches!( + bp, + AnyBreakpoint::CpuRegister16(CpuRegister16Breakpoint { + register: Register16::PC, + value: Some(0x0000), + .. + }) + ) && !bp.enabled() + }) + ); + } + + #[test] + fn test_memory_debug_window_pc_breakpoint_via_disassembly() { + let mut debugger = TestDebugger::default(); + let mut window = MemoryDebugWindow { + show: true, + ..Default::default() + }; + + let app = |ctx: &egui::Context| { + window.ui(ctx, &mut debugger); + }; + + let mut harness = Harness::new(app); + harness.run(); + + // Select "Disassembly" view mode + harness + .get_all_by_role(accesskit::Role::ComboBox) + .next() + .unwrap() + .click(); + harness.run(); + harness.get_by_label("Disassembly").click(); + harness.run(); + + // Click on address + harness.get_by_label(" 0000:").click(); + harness.run(); + + assert!(harness.query_by_label("PC = 0x0000").is_some()); + + // Click on address + harness.get_by_label("● 0000:").click(); + harness.run(); + + assert!(harness.query_by_label("PC = 0x0000").is_none()); + } + + #[test] + fn test_memory_debug_window_jump_to_address_works_disassembly() { + let mut debugger = TestDebugger::default(); + let mut window = MemoryDebugWindow { + show: true, + ..Default::default() + }; + + let app = |ctx: &egui::Context| { + window.ui(ctx, &mut debugger); + }; + + let mut harness = Harness::new(app); + harness.run(); + + // Select "Disassembly" view mode + harness + .get_all_by_role(accesskit::Role::ComboBox) + .next() + .unwrap() + .click(); + harness.run(); + harness.get_by_label("Disassembly").click(); + harness.run(); + + // Type 0xc000 + harness + .get_by_role_and_label(accesskit::Role::TextInput, "Jump to address:") + .type_text("0xc000"); + harness.run(); + + // Jump to address + harness + .get_by_role_and_label(accesskit::Role::Button, "Go") + .click(); + harness.run(); + + assert!(harness.query_by_label(" C000:").is_some()); + assert!(harness.query_by_label(" 0000:").is_none()); + + // Track current PC + harness + .get_by_role_and_label(accesskit::Role::Button, "Track Current PC") + .click(); + harness.run(); + + assert!(harness.query_by_label(" 0000:").is_some()); + assert!(harness.query_by_label(" C000:").is_none()); + } + + fn jump_to_address_works(view: &str) { + let mut debugger = TestDebugger::default(); + let mut window = MemoryDebugWindow { + show: true, + ..Default::default() + }; + + let app = |ctx: &egui::Context| { + window.ui(ctx, &mut debugger); + }; + + let mut harness = Harness::new(app); + harness.run(); + + // Select view mode + harness + .get_all_by_role(accesskit::Role::ComboBox) + .next() + .unwrap() + .click(); + harness.run(); + harness.get_by_label(view).click(); + harness.run(); + + // Type 0x2000 + harness + .get_by_role_and_label(accesskit::Role::TextInput, "Jump to address:") + .type_text("0x2000"); + harness.run(); + + let scroll_area = harness + .get_by_label("2000:") + .parent() + .unwrap() + .bounding_box() + .unwrap(); + + let address_label = harness.get_by_label("2000:").bounding_box().unwrap(); + assert!(!scroll_area.contains(address_label.origin())); + + // Jump to address + harness + .get_by_role_and_label(accesskit::Role::Button, "Go") + .click(); + harness.run(); + + let address_label = harness.get_by_label("2000:").bounding_box().unwrap(); + assert!(scroll_area.contains(address_label.origin())); + } + + #[test] + fn test_memory_debug_window_jump_to_address_works_composite_rom_ram() { + jump_to_address_works("Composite ROM/RAM"); + } + + #[test] + fn test_memory_debug_window_jump_to_address_works_composite_ram() { + jump_to_address_works("Composite RAM"); + } + + #[test] + fn test_memory_debug_window_jump_to_address_works_lower_rom() { + jump_to_address_works("Lower ROM only"); + } + + #[test] + fn test_memory_debug_window_jump_to_address_works_upper_rom() { + jump_to_address_works("Upper ROM #00 only"); + } + + #[test] + fn test_memory_debug_window_jump_to_address_works_ram() { + jump_to_address_works("RAM only"); + } + + #[test] + #[ignore = "extension ram not implemented"] + fn test_memory_debug_window_jump_to_address_works_extension_ram() { + jump_to_address_works("Extension RAM only"); + } +} + +#[cfg(test)] +mod snapshot_tests { + use super::*; + + use egui::accesskit; + use egui_kittest::{Harness, kittest, kittest::Queryable}; + + use crate::debug::mock::TestDebugger; + + fn pick_color_works(label: &str, snaphot: &str) { + let mut debugger = TestDebugger::default(); + let mut window = MemoryDebugWindow { + show: true, + ..Default::default() + }; + + let app = |ctx: &egui::Context| { + window.ui(ctx, &mut debugger); + }; + + let mut harness = Harness::new(app); + harness.run(); + + // Show color configuration + harness.get_by_label("Address Color Coding").click(); + harness.run(); + + harness.snapshot("memory_colors_defaults"); + + // Open color picker + harness + .get_by_role_and_label(accesskit::Role::ColorWell, label) + .click(); + harness.run(); + + // Edit red + let value = harness + .get_all_by_role(accesskit::Role::SpinButton) + .next() + .unwrap(); + value.click(); + value.type_text("255"); + harness.run(); + + // Edit green + let value = harness + .get_all_by_role(accesskit::Role::SpinButton) + .nth(1) + .unwrap(); + value.click(); + value.type_text("255"); + harness.run(); + + // Edit blue + let value = harness + .get_all_by_role(accesskit::Role::SpinButton) + .nth(2) + .unwrap(); + value.click(); + value.type_text("255"); + value.key_press(kittest::Key::Escape); // close color picker + harness.run(); + + harness.snapshot(snaphot); + + harness + .get_by_role_and_label(accesskit::Role::Button, "Restore Defaults") + .click(); + harness.run(); + + harness.snapshot("memory_colors_defaults"); + } + + #[test] + #[ignore = "snapshot test"] + fn test_memory_debug_window_pick_color_lower_rom() { + pick_color_works("Lower ROM:", "memory_colors_lower_rom_changed"); + } + + #[test] + #[ignore = "snapshot test"] + fn test_memory_debug_window_pick_color_upper_rom() { + pick_color_works("Upper ROM:", "memory_colors_upper_rom_changed"); + } + + #[test] + #[ignore = "snapshot test"] + fn test_memory_debug_window_pick_color_ram() { + pick_color_works("RAM:", "memory_colors_ram_changed"); + } + + #[test] + #[ignore = "snapshot test"] + fn test_memory_debug_window_pick_color_extension_ram() { + pick_color_works("Extension RAM:", "memory_colors_extension_ram_changed"); + } +} diff --git a/ronald-egui/src/frontend.rs b/ronald-egui/src/frontend.rs index 74dd58f..6d79576 100644 --- a/ronald-egui/src/frontend.rs +++ b/ronald-egui/src/frontend.rs @@ -1,6 +1,7 @@ use std::{path::PathBuf, thread::spawn}; use eframe::{egui, egui_wgpu}; +use egui::Vec2; use serde::{Deserialize, Serialize}; use web_time::Instant; @@ -10,12 +11,19 @@ use web_sys; use ronald_core::{ AudioSink, Driver, constants::{SCREEN_BUFFER_HEIGHT, SCREEN_BUFFER_WIDTH}, - system::SystemConfig, + debug::{ + breakpoint::{AnyBreakpoint, Breakpoint, BreakpointManager}, + view::SystemDebugView, + }, + system::{SystemConfig, instruction::DecodedInstruction}, }; -use crate::frontend::{audio::CpalAudio, video::EguiWgpuVideo}; use crate::key_mapper::{KeyEvent, KeyMapStore, KeyMapper}; use crate::utils::sync::{Shared, SharedExt, shared}; +use crate::{ + debug::Debugger, + frontend::{audio::CpalAudio, video::EguiWgpuVideo}, +}; mod audio; mod video; @@ -27,14 +35,15 @@ struct File { } pub struct Frontend { + initialized: bool, driver: Driver, audio: CpalAudio, video: EguiWgpuVideo, frame_start: Instant, time_available: usize, - input_test: String, can_interact: bool, paused: bool, + hovered: Option, picked_file_disk_a: Shared>, picked_file_disk_b: Shared>, picked_file_tape: Shared>, @@ -42,11 +51,6 @@ pub struct Frontend { } impl Frontend { - pub fn new(render_state: &egui_wgpu::RenderState) -> Self { - let driver = Driver::new(); - Self::with_driver_and_render_state(driver, render_state) - } - pub fn with_config(render_state: &egui_wgpu::RenderState, config: &SystemConfig) -> Self { let driver = Driver::with_config(config); Self::with_driver_and_render_state(driver, render_state) @@ -62,14 +66,15 @@ impl Frontend { let video = EguiWgpuVideo::new(render_state); Self { + initialized: false, driver, audio, video, frame_start: Instant::now(), time_available: 0, - input_test: String::new(), can_interact: true, paused: false, + hovered: None, picked_file_disk_a: shared(None), picked_file_disk_b: shared(None), picked_file_tape: shared(None), @@ -92,19 +97,21 @@ impl Frontend { self.run_frame(ctx, ui, size, false, can_interact, key_mapper); } None => { - egui::Window::new("Input Test").show(ctx, |ui| { - ui.text_edit_singleline(&mut self.input_test); - }); - let size = egui::Vec2::new(SCREEN_BUFFER_WIDTH as f32, SCREEN_BUFFER_HEIGHT as f32); - egui::Window::new("Screen") + if let Some(window) = egui::Window::new("Screen") .collapsible(false) .resizable(false) .default_size(size) .show(ctx, |ui| { self.run_frame(ctx, ui, size, true, can_interact, key_mapper); - }); + }) + && !self.initialized + { + let layer_id = window.response.layer_id; + ctx.move_to_top(layer_id); + self.initialized = true; + } } } } @@ -223,20 +230,16 @@ impl Frontend { self.handle_picked_files(); #[cfg(target_arch = "wasm32")] - if self.has_window_focus() && self.can_interact { - self.resume(); - } else { + if !self.has_window_focus() || !self.can_interact { self.pause(); } #[cfg(not(target_arch = "wasm32"))] - if self.can_interact { - self.resume(); - } else { + if !self.can_interact { self.pause(); } self.step_emulation(); - self.draw_framebuffer(ctx, ui, size) + self.draw_framebuffer(ctx, ui, size, workbench) } fn handle_input(&mut self, input: &egui::InputState, key_mapper: &mut KeyMapper) where @@ -351,15 +354,26 @@ impl Frontend { // TODO: Allow running at 60Hz??? Does CPC really support that? while self.time_available >= 20_000 { log::trace!("Stepping emulator for 20_000 microseconds"); - self.driver.step(20_000, &mut self.video, &mut self.audio); + let breakpoint_hit = self.driver.step(20_000, &mut self.video, &mut self.audio); self.time_available -= 20_000; // TODO:: take into account actually executed cycles + + if breakpoint_hit { + self.pause(); + return; + } } if self.time_available > 0 { log::trace!("Stepping emulator for {} microseconds", self.time_available); - self.driver - .step(self.time_available, &mut self.video, &mut self.audio); + let breakpoint_hit = + self.driver + .step(self.time_available, &mut self.video, &mut self.audio); self.time_available = 0; // TODO:: take into account actually executed cycles + + if breakpoint_hit { + self.pause(); + return; + } } log::trace!( @@ -370,12 +384,35 @@ impl Frontend { fn draw_framebuffer( &mut self, - _ctx: &egui::Context, + ctx: &egui::Context, ui: &mut egui::Ui, size: egui::Vec2, + workbench: bool, ) -> egui::Response { let texture = egui::load::SizedTexture::new(self.video.framebuffer(), size); - ui.image(texture) + let response = ui.image(texture).interact(egui::Sense::click()); + + let hovered = response + .rect + .contains(ctx.input(|i| i.pointer.hover_pos()).unwrap_or_default()); + + let moved = ctx.input(|i| i.pointer.delta() != Vec2::new(0.0, 0.0)) || response.clicked(); + + if hovered && moved { + self.hovered = Some(Instant::now()); + } else if let Some(hovered) = self.hovered + && Instant::now().duration_since(hovered).as_secs_f32() > 2.0 + { + self.hovered = None; + } else if !hovered { + self.hovered = None; + } + + if self.paused || self.hovered.is_some() { + self.draw_pause_overlay(ui, &response, workbench); + } + + response } fn pause(&mut self) { @@ -421,4 +458,101 @@ impl Frontend { ) } } + + fn draw_pause_overlay( + &mut self, + ui: &mut egui::Ui, + screen_response: &egui::Response, + workbench: bool, + ) { + let rect = screen_response.rect; + let overlay_height = 40.0; + let overlay_rect = egui::Rect::from_min_size( + egui::Pos2::new(rect.min.x, rect.min.y), + egui::Vec2::new(rect.width(), overlay_height), + ); + + // Draw semi-transparent background + ui.painter().rect_filled( + overlay_rect, + egui::CornerRadius::default(), + egui::Color32::from_black_alpha(128), + ); + + // Draw the overlay content in a child UI + let mut child_ui = ui.new_child( + egui::UiBuilder::new() + .max_rect(overlay_rect) + .layout(egui::Layout::left_to_right(egui::Align::Center)), + ); + + child_ui.spacing_mut().item_spacing.x = 8.0; + child_ui.add_space(8.0); + + let status_text = if self.paused { "Paused" } else { "Running" }; + + child_ui.colored_label(egui::Color32::WHITE, status_text); + child_ui.add_space(16.0); + + if self.paused { + if child_ui.button("Run").clicked() { + self.resume(); + } + + if !workbench { + return; + } + + child_ui.add_space(24.0); + + if child_ui.button("Step Into").clicked() { + self.step_into(); + } + + if child_ui.button("Step Over").clicked() { + self.step_over(); + } + + if child_ui.button("Step Out").clicked() { + self.step_out(); + } + } else if child_ui.button("Pause").clicked() { + self.pause(); + } + } + + fn step_into(&mut self) { + let breakpoint = AnyBreakpoint::step_into(); + self.driver.breakpoint_manager().add_breakpoint(breakpoint); + + self.resume(); + } + + fn step_over(&mut self) { + let breakpoint = AnyBreakpoint::step_over(); + self.driver.breakpoint_manager().add_breakpoint(breakpoint); + + self.resume(); + } + + fn step_out(&mut self) { + let breakpoint = AnyBreakpoint::step_out(); + self.driver.breakpoint_manager().add_breakpoint(breakpoint); + + self.resume(); + } +} + +impl Debugger for Frontend { + fn debug_view(&mut self) -> &SystemDebugView { + self.driver.debug_view() + } + + fn breakpoint_manager(&mut self) -> &mut BreakpointManager { + self.driver.breakpoint_manager() + } + + fn disassemble(&self, start_address: u16, count: usize) -> Vec { + self.driver.disassemble(start_address, count) + } } diff --git a/ronald-egui/src/main.rs b/ronald-egui/src/main.rs index 9102d8a..6513cc7 100644 --- a/ronald-egui/src/main.rs +++ b/ronald-egui/src/main.rs @@ -1,4 +1,6 @@ mod app; +mod colors; +mod debug; mod frontend; mod key_map_editor; mod key_mapper; diff --git a/ronald-egui/tests/snapshots/memory_colors_defaults.png b/ronald-egui/tests/snapshots/memory_colors_defaults.png new file mode 100644 index 0000000..fa1cb58 Binary files /dev/null and b/ronald-egui/tests/snapshots/memory_colors_defaults.png differ diff --git a/ronald-egui/tests/snapshots/memory_colors_extension_ram_changed.png b/ronald-egui/tests/snapshots/memory_colors_extension_ram_changed.png new file mode 100644 index 0000000..e7a9907 Binary files /dev/null and b/ronald-egui/tests/snapshots/memory_colors_extension_ram_changed.png differ diff --git a/ronald-egui/tests/snapshots/memory_colors_lower_rom_changed.png b/ronald-egui/tests/snapshots/memory_colors_lower_rom_changed.png new file mode 100644 index 0000000..5bf80bb Binary files /dev/null and b/ronald-egui/tests/snapshots/memory_colors_lower_rom_changed.png differ diff --git a/ronald-egui/tests/snapshots/memory_colors_ram_changed.png b/ronald-egui/tests/snapshots/memory_colors_ram_changed.png new file mode 100644 index 0000000..3ec2b48 Binary files /dev/null and b/ronald-egui/tests/snapshots/memory_colors_ram_changed.png differ diff --git a/ronald-egui/tests/snapshots/memory_colors_upper_rom_changed.png b/ronald-egui/tests/snapshots/memory_colors_upper_rom_changed.png new file mode 100644 index 0000000..ef1df21 Binary files /dev/null and b/ronald-egui/tests/snapshots/memory_colors_upper_rom_changed.png differ