Implement GDB/LLDB compatible server #3
			
				
			
		
		
		
	| 
						 | 
				
			
			@ -28,6 +28,15 @@ dependencies = [
 | 
			
		|||
 "serde",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "addr2line"
 | 
			
		||||
version = "0.24.2"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "gimli",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "adler2"
 | 
			
		||||
version = "2.0.0"
 | 
			
		||||
| 
						 | 
				
			
			@ -388,6 +397,24 @@ dependencies = [
 | 
			
		|||
 "syn",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "atoi"
 | 
			
		||||
version = "2.0.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "num-traits",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "atomic"
 | 
			
		||||
version = "0.6.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "8d818003e740b63afc82337e3160717f4f63078720a810b7b903e70a5d1d2994"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "bytemuck",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "atomic-waker"
 | 
			
		||||
version = "1.1.2"
 | 
			
		||||
| 
						 | 
				
			
			@ -400,6 +427,21 @@ version = "1.4.0"
 | 
			
		|||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "backtrace"
 | 
			
		||||
version = "0.3.74"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "addr2line",
 | 
			
		||||
 "cfg-if",
 | 
			
		||||
 "libc",
 | 
			
		||||
 "miniz_oxide",
 | 
			
		||||
 "object",
 | 
			
		||||
 "rustc-demangle",
 | 
			
		||||
 "windows-targets 0.52.6",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "bindgen"
 | 
			
		||||
version = "0.70.1"
 | 
			
		||||
| 
						 | 
				
			
			@ -1306,6 +1348,12 @@ dependencies = [
 | 
			
		|||
 "windows 0.58.0",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "gimli"
 | 
			
		||||
version = "0.31.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "gl_generator"
 | 
			
		||||
version = "0.14.0"
 | 
			
		||||
| 
						 | 
				
			
			@ -1707,11 +1755,19 @@ version = "3.1.0"
 | 
			
		|||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "lazy_static"
 | 
			
		||||
version = "1.5.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "lemur"
 | 
			
		||||
version = "0.2.7"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "anyhow",
 | 
			
		||||
 "atoi",
 | 
			
		||||
 "atomic",
 | 
			
		||||
 "bitflags 2.6.0",
 | 
			
		||||
 "bytemuck",
 | 
			
		||||
 "cc",
 | 
			
		||||
| 
						 | 
				
			
			@ -1724,6 +1780,7 @@ dependencies = [
 | 
			
		|||
 "egui-winit",
 | 
			
		||||
 "egui_extras",
 | 
			
		||||
 "gilrs",
 | 
			
		||||
 "hex",
 | 
			
		||||
 "image",
 | 
			
		||||
 "itertools",
 | 
			
		||||
 "num-derive",
 | 
			
		||||
| 
						 | 
				
			
			@ -1736,6 +1793,9 @@ dependencies = [
 | 
			
		|||
 "serde",
 | 
			
		||||
 "serde_json",
 | 
			
		||||
 "thread-priority",
 | 
			
		||||
 "tokio",
 | 
			
		||||
 "tracing",
 | 
			
		||||
 "tracing-subscriber",
 | 
			
		||||
 "wgpu",
 | 
			
		||||
 "windows 0.58.0",
 | 
			
		||||
 "winit",
 | 
			
		||||
| 
						 | 
				
			
			@ -1831,6 +1891,15 @@ dependencies = [
 | 
			
		|||
 "libc",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "matchers"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "regex-automata 0.1.10",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "memchr"
 | 
			
		||||
version = "2.7.4"
 | 
			
		||||
| 
						 | 
				
			
			@ -1902,6 +1971,17 @@ dependencies = [
 | 
			
		|||
 "simd-adler32",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "mio"
 | 
			
		||||
version = "1.0.3"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "libc",
 | 
			
		||||
 "wasi",
 | 
			
		||||
 "windows-sys 0.52.0",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "naga"
 | 
			
		||||
version = "23.1.0"
 | 
			
		||||
| 
						 | 
				
			
			@ -2005,6 +2085,16 @@ dependencies = [
 | 
			
		|||
 "minimal-lexical",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "nu-ansi-term"
 | 
			
		||||
version = "0.46.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "overload",
 | 
			
		||||
 "winapi",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "num-complex"
 | 
			
		||||
version = "0.4.6"
 | 
			
		||||
| 
						 | 
				
			
			@ -2276,6 +2366,15 @@ dependencies = [
 | 
			
		|||
 "objc2-foundation",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "object"
 | 
			
		||||
version = "0.36.7"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "memchr",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "oboe"
 | 
			
		||||
version = "0.6.1"
 | 
			
		||||
| 
						 | 
				
			
			@ -2336,6 +2435,12 @@ dependencies = [
 | 
			
		|||
 "pin-project-lite",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "overload"
 | 
			
		||||
version = "0.1.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "owned_ttf_parser"
 | 
			
		||||
version = "0.25.0"
 | 
			
		||||
| 
						 | 
				
			
			@ -2629,8 +2734,17 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
 | 
			
		|||
dependencies = [
 | 
			
		||||
 "aho-corasick",
 | 
			
		||||
 "memchr",
 | 
			
		||||
 "regex-automata",
 | 
			
		||||
 "regex-syntax",
 | 
			
		||||
 "regex-automata 0.4.9",
 | 
			
		||||
 "regex-syntax 0.8.5",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "regex-automata"
 | 
			
		||||
version = "0.1.10"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "regex-syntax 0.6.29",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
| 
						 | 
				
			
			@ -2641,9 +2755,15 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
 | 
			
		|||
dependencies = [
 | 
			
		||||
 "aho-corasick",
 | 
			
		||||
 "memchr",
 | 
			
		||||
 "regex-syntax",
 | 
			
		||||
 "regex-syntax 0.8.5",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "regex-syntax"
 | 
			
		||||
version = "0.6.29"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "regex-syntax"
 | 
			
		||||
version = "0.8.5"
 | 
			
		||||
| 
						 | 
				
			
			@ -2696,6 +2816,12 @@ dependencies = [
 | 
			
		|||
 "realfft",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "rustc-demangle"
 | 
			
		||||
version = "0.1.24"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "rustc-hash"
 | 
			
		||||
version = "1.1.0"
 | 
			
		||||
| 
						 | 
				
			
			@ -2828,6 +2954,15 @@ dependencies = [
 | 
			
		|||
 "serde",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "sharded-slab"
 | 
			
		||||
version = "0.1.7"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "lazy_static",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "shlex"
 | 
			
		||||
version = "1.3.0"
 | 
			
		||||
| 
						 | 
				
			
			@ -2918,6 +3053,16 @@ dependencies = [
 | 
			
		|||
 "serde",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "socket2"
 | 
			
		||||
version = "0.5.8"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "libc",
 | 
			
		||||
 "windows-sys 0.52.0",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "spirv"
 | 
			
		||||
version = "0.3.0+sdk-1.3.268.0"
 | 
			
		||||
| 
						 | 
				
			
			@ -3035,6 +3180,16 @@ dependencies = [
 | 
			
		|||
 "winapi",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "thread_local"
 | 
			
		||||
version = "1.1.8"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "cfg-if",
 | 
			
		||||
 "once_cell",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "tiny-skia"
 | 
			
		||||
version = "0.11.4"
 | 
			
		||||
| 
						 | 
				
			
			@ -3070,6 +3225,33 @@ dependencies = [
 | 
			
		|||
 "zerovec",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "tokio"
 | 
			
		||||
version = "1.42.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "backtrace",
 | 
			
		||||
 "bytes",
 | 
			
		||||
 "libc",
 | 
			
		||||
 "mio",
 | 
			
		||||
 "pin-project-lite",
 | 
			
		||||
 "socket2",
 | 
			
		||||
 "tokio-macros",
 | 
			
		||||
 "windows-sys 0.52.0",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "tokio-macros"
 | 
			
		||||
version = "2.4.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "proc-macro2",
 | 
			
		||||
 "quote",
 | 
			
		||||
 "syn",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "toml"
 | 
			
		||||
version = "0.8.19"
 | 
			
		||||
| 
						 | 
				
			
			@ -3133,6 +3315,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		|||
checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "once_cell",
 | 
			
		||||
 "valuable",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "tracing-log"
 | 
			
		||||
version = "0.2.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "log",
 | 
			
		||||
 "once_cell",
 | 
			
		||||
 "tracing-core",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "tracing-subscriber"
 | 
			
		||||
version = "0.3.19"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "matchers",
 | 
			
		||||
 "nu-ansi-term",
 | 
			
		||||
 "once_cell",
 | 
			
		||||
 "regex",
 | 
			
		||||
 "sharded-slab",
 | 
			
		||||
 "smallvec",
 | 
			
		||||
 "thread_local",
 | 
			
		||||
 "tracing",
 | 
			
		||||
 "tracing-core",
 | 
			
		||||
 "tracing-log",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
| 
						 | 
				
			
			@ -3243,6 +3455,12 @@ version = "1.11.0"
 | 
			
		|||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "valuable"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "vec_map"
 | 
			
		||||
version = "0.8.2"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,6 +9,8 @@ edition = "2021"
 | 
			
		|||
 | 
			
		||||
[dependencies]
 | 
			
		||||
anyhow = "1"
 | 
			
		||||
atoi = "2"
 | 
			
		||||
atomic = "0.6"
 | 
			
		||||
bitflags = { version = "2", features = ["serde"] }
 | 
			
		||||
bytemuck = { version = "1", features = ["derive"] }
 | 
			
		||||
clap = { version = "4", features = ["derive"] }
 | 
			
		||||
| 
						 | 
				
			
			@ -20,6 +22,7 @@ egui-toast = { git = "https://github.com/urholaukkarinen/egui-toast.git", rev =
 | 
			
		|||
egui-winit = "0.30"
 | 
			
		||||
egui-wgpu = { version = "0.30", features = ["winit"] }
 | 
			
		||||
gilrs = { version = "0.11", features = ["serde-serialize"] }
 | 
			
		||||
hex = "0.4"
 | 
			
		||||
image = { version = "0.25", default-features = false, features = ["png"] }
 | 
			
		||||
itertools = "0.13"
 | 
			
		||||
num-derive = "0.4"
 | 
			
		||||
| 
						 | 
				
			
			@ -32,6 +35,9 @@ rubato = "0.16"
 | 
			
		|||
serde = { version = "1", features = ["derive"] }
 | 
			
		||||
serde_json = "1"
 | 
			
		||||
thread-priority = "1"
 | 
			
		||||
tokio = { version = "1", features = ["io-util", "macros", "net", "rt", "sync"] }
 | 
			
		||||
tracing = { version = "0.1", features = ["release_max_level_info"] }
 | 
			
		||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
 | 
			
		||||
wgpu = "23"
 | 
			
		||||
winit = { version = "0.30", features = ["serde"] }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,7 +5,7 @@ A Virtual Boy emulator built around the shrooms-vb core. Written in Rust, using
 | 
			
		|||
## Setup
 | 
			
		||||
 | 
			
		||||
Install the following dependencies:
 | 
			
		||||
 - `cargo`
 | 
			
		||||
 - `cargo` (via [rustup](https://rustup.rs/), the version from your package manager is too old)
 | 
			
		||||
 | 
			
		||||
Run
 | 
			
		||||
```sh
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -23,6 +23,10 @@ ENV PATH="/osxcross/bin:$PATH" \
 | 
			
		|||
    CXX_x86_64-apple-darwin="o64-clang++" \
 | 
			
		||||
    CC_aarch64-apple-darwin="oa64-clang" \
 | 
			
		||||
    CXX_aarch64-apple-darwin="o6a4-clang++" \
 | 
			
		||||
    SHROOMS_CFLAGS_x86_64-unknown-linux-gnu="-flto" \
 | 
			
		||||
    CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS="-Clinker-plugin-lto -Clinker=clang -Clink-arg=-fuse-ld=lld" \
 | 
			
		||||
    SHROOMS_CFLAGS_x86_64-pc-windows-msvc="-flto" \
 | 
			
		||||
    CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUSTFLAGS="-Lnative=/xwin/crt/lib/x86_64 -Lnative=/xwin/sdk/lib/um/x86_64 -Lnative=/xwin/sdk/lib/ucrt/x86_64 -Clinker-plugin-lto -Clink-arg=-fuse-ld=lld" \
 | 
			
		||||
    CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER="lld-link-19" \
 | 
			
		||||
    CARGO_TARGET_X86_64_APPLE_DARWIN_LINKER="o64-clang" \
 | 
			
		||||
    CARGO_TARGET_X86_64_APPLE_DARWIN_AR="llvm-ar-19" \
 | 
			
		||||
| 
						 | 
				
			
			@ -30,5 +34,4 @@ ENV PATH="/osxcross/bin:$PATH" \
 | 
			
		|||
    CARGO_TARGET_AARCH64_APPLE_DARWIN_AR="llvm-ar-19" \
 | 
			
		||||
    CROSS_COMPILE="setting-this-to-silence-a-warning-" \
 | 
			
		||||
    RC_PATH="llvm-rc-19" \
 | 
			
		||||
    RUSTFLAGS="-Lnative=/xwin/crt/lib/x86_64 -Lnative=/xwin/sdk/lib/um/x86_64 -Lnative=/xwin/sdk/lib/ucrt/x86_64" \
 | 
			
		||||
    MACOSX_DEPLOYMENT_TARGET="14.5"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										16
									
								
								build.rs
								
								
								
								
							
							
						
						
									
										16
									
								
								build.rs
								
								
								
								
							| 
						 | 
				
			
			@ -8,13 +8,25 @@ fn main() -> Result<(), Box<dyn Error>> {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    println!("cargo::rerun-if-changed=shrooms-vb-core");
 | 
			
		||||
    cc::Build::new()
 | 
			
		||||
 | 
			
		||||
    let mut builder = cc::Build::new();
 | 
			
		||||
    let _ = builder.try_flags_from_environment("SHROOMS_CFLAGS");
 | 
			
		||||
    let opt_level = if builder.get_compiler().is_like_msvc() {
 | 
			
		||||
        2
 | 
			
		||||
    } else {
 | 
			
		||||
        3
 | 
			
		||||
    };
 | 
			
		||||
    builder
 | 
			
		||||
        .include(Path::new("shrooms-vb-core/core"))
 | 
			
		||||
        .opt_level(2)
 | 
			
		||||
        .opt_level(opt_level)
 | 
			
		||||
        .flag_if_supported("-fno-strict-aliasing")
 | 
			
		||||
        .define("VB_LITTLE_ENDIAN", None)
 | 
			
		||||
        .define("VB_SIGNED_PROPAGATE", None)
 | 
			
		||||
        .define("VB_DIV_GENERIC", None)
 | 
			
		||||
        .define("VB_DIRECT_EXECUTE", "on_execute")
 | 
			
		||||
        .define("VB_DIRECT_FRAME", "on_frame")
 | 
			
		||||
        .define("VB_DIRECT_READ", "on_read")
 | 
			
		||||
        .define("VB_DIRECT_WRITE", "on_write")
 | 
			
		||||
        .file(Path::new("shrooms-vb-core/core/vb.c"))
 | 
			
		||||
        .compile("vb");
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										46
									
								
								src/app.rs
								
								
								
								
							
							
						
						
									
										46
									
								
								src/app.rs
								
								
								
								
							| 
						 | 
				
			
			@ -6,6 +6,7 @@ use egui::{
 | 
			
		|||
    ViewportCommand, ViewportId, ViewportInfo,
 | 
			
		||||
};
 | 
			
		||||
use gilrs::{EventType, Gilrs};
 | 
			
		||||
use tracing::{error, warn};
 | 
			
		||||
use winit::{
 | 
			
		||||
    application::ApplicationHandler,
 | 
			
		||||
    event::WindowEvent,
 | 
			
		||||
| 
						 | 
				
			
			@ -18,7 +19,7 @@ use crate::{
 | 
			
		|||
    emulator::{EmulatorClient, EmulatorCommand, SimId},
 | 
			
		||||
    input::MappingProvider,
 | 
			
		||||
    persistence::Persistence,
 | 
			
		||||
    window::{AboutWindow, AppWindow, GameWindow, InputWindow},
 | 
			
		||||
    window::{AboutWindow, AppWindow, GameWindow, GdbServerWindow, InputWindow},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
fn load_icon() -> anyhow::Result<IconData> {
 | 
			
		||||
| 
						 | 
				
			
			@ -41,10 +42,15 @@ pub struct Application {
 | 
			
		|||
    persistence: Persistence,
 | 
			
		||||
    viewports: HashMap<ViewportId, Viewport>,
 | 
			
		||||
    focused: Option<ViewportId>,
 | 
			
		||||
    init_debug_port: Option<u16>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Application {
 | 
			
		||||
    pub fn new(client: EmulatorClient, proxy: EventLoopProxy<UserEvent>) -> Self {
 | 
			
		||||
    pub fn new(
 | 
			
		||||
        client: EmulatorClient,
 | 
			
		||||
        proxy: EventLoopProxy<UserEvent>,
 | 
			
		||||
        debug_port: Option<u16>,
 | 
			
		||||
    ) -> Self {
 | 
			
		||||
        let icon = load_icon().ok().map(Arc::new);
 | 
			
		||||
        let persistence = Persistence::new();
 | 
			
		||||
        let mappings = MappingProvider::new(persistence.clone());
 | 
			
		||||
| 
						 | 
				
			
			@ -63,6 +69,7 @@ impl Application {
 | 
			
		|||
            persistence,
 | 
			
		||||
            viewports: HashMap::new(),
 | 
			
		||||
            focused: None,
 | 
			
		||||
            init_debug_port: debug_port,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -80,15 +87,19 @@ impl Application {
 | 
			
		|||
 | 
			
		||||
impl ApplicationHandler<UserEvent> for Application {
 | 
			
		||||
    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
 | 
			
		||||
        if let Some(port) = self.init_debug_port {
 | 
			
		||||
            let mut server =
 | 
			
		||||
                GdbServerWindow::new(SimId::Player1, self.client.clone(), self.proxy.clone());
 | 
			
		||||
            server.launch(port);
 | 
			
		||||
            self.open(event_loop, Box::new(server));
 | 
			
		||||
        }
 | 
			
		||||
        let app = GameWindow::new(
 | 
			
		||||
            self.client.clone(),
 | 
			
		||||
            self.proxy.clone(),
 | 
			
		||||
            self.persistence.clone(),
 | 
			
		||||
            SimId::Player1,
 | 
			
		||||
        );
 | 
			
		||||
        let wrapper = Viewport::new(event_loop, self.icon.clone(), Box::new(app));
 | 
			
		||||
        self.focused = Some(wrapper.id());
 | 
			
		||||
        self.viewports.insert(wrapper.id(), wrapper);
 | 
			
		||||
        self.open(event_loop, Box::new(app));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn window_event(
 | 
			
		||||
| 
						 | 
				
			
			@ -183,6 +194,11 @@ impl ApplicationHandler<UserEvent> for Application {
 | 
			
		|||
                let about = AboutWindow;
 | 
			
		||||
                self.open(event_loop, Box::new(about));
 | 
			
		||||
            }
 | 
			
		||||
            UserEvent::OpenDebugger(sim_id) => {
 | 
			
		||||
                let debugger =
 | 
			
		||||
                    GdbServerWindow::new(sim_id, self.client.clone(), self.proxy.clone());
 | 
			
		||||
                self.open(event_loop, Box::new(debugger));
 | 
			
		||||
            }
 | 
			
		||||
            UserEvent::OpenInput => {
 | 
			
		||||
                let input = InputWindow::new(self.mappings.clone());
 | 
			
		||||
                self.open(event_loop, Box::new(input));
 | 
			
		||||
| 
						 | 
				
			
			@ -196,6 +212,13 @@ impl ApplicationHandler<UserEvent> for Application {
 | 
			
		|||
                );
 | 
			
		||||
                self.open(event_loop, Box::new(p2));
 | 
			
		||||
            }
 | 
			
		||||
            UserEvent::Quit(sim_id) => {
 | 
			
		||||
                self.viewports
 | 
			
		||||
                    .retain(|_, viewport| viewport.app.sim_id() != sim_id);
 | 
			
		||||
                if !self.viewports.contains_key(&ViewportId::ROOT) {
 | 
			
		||||
                    event_loop.exit();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -208,8 +231,8 @@ impl ApplicationHandler<UserEvent> for Application {
 | 
			
		|||
    fn exiting(&mut self, _event_loop: &ActiveEventLoop) {
 | 
			
		||||
        let (sender, receiver) = oneshot::channel();
 | 
			
		||||
        if self.client.send_command(EmulatorCommand::Exit(sender)) {
 | 
			
		||||
            if let Err(err) = receiver.recv_timeout(Duration::from_secs(5)) {
 | 
			
		||||
                eprintln!("could not gracefully exit: {}", err);
 | 
			
		||||
            if let Err(error) = receiver.recv_timeout(Duration::from_secs(5)) {
 | 
			
		||||
                error!(%error, "could not gracefully exit.");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -380,8 +403,10 @@ impl Drop for Viewport {
 | 
			
		|||
pub enum UserEvent {
 | 
			
		||||
    GamepadEvent(gilrs::Event),
 | 
			
		||||
    OpenAbout,
 | 
			
		||||
    OpenDebugger(SimId),
 | 
			
		||||
    OpenInput,
 | 
			
		||||
    OpenPlayer2,
 | 
			
		||||
    Quit(SimId),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub enum Action {
 | 
			
		||||
| 
						 | 
				
			
			@ -410,9 +435,12 @@ fn create_window_and_state(
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
fn process_gamepad_input(mappings: MappingProvider, proxy: EventLoopProxy<UserEvent>) {
 | 
			
		||||
    let Ok(mut gilrs) = Gilrs::new() else {
 | 
			
		||||
        eprintln!("could not connect gamepad listener");
 | 
			
		||||
    let mut gilrs = match Gilrs::new() {
 | 
			
		||||
        Ok(gilrs) => gilrs,
 | 
			
		||||
        Err(error) => {
 | 
			
		||||
            warn!(%error, "could not connect gamepad listener");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
    while let Some(event) = gilrs.next_event_blocking(None) {
 | 
			
		||||
        if event.event == EventType::Connected {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,6 +4,7 @@ use anyhow::{bail, Result};
 | 
			
		|||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
 | 
			
		||||
use itertools::Itertools;
 | 
			
		||||
use rubato::{FftFixedInOut, Resampler};
 | 
			
		||||
use tracing::error;
 | 
			
		||||
 | 
			
		||||
pub struct Audio {
 | 
			
		||||
    #[allow(unused)]
 | 
			
		||||
| 
						 | 
				
			
			@ -54,7 +55,7 @@ impl Audio {
 | 
			
		|||
                }
 | 
			
		||||
                chunk.commit_all();
 | 
			
		||||
            },
 | 
			
		||||
            move |err| eprintln!("stream error: {err}"),
 | 
			
		||||
            move |error| error!(%error, "stream error"),
 | 
			
		||||
            None,
 | 
			
		||||
        )?;
 | 
			
		||||
        stream.play()?;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										404
									
								
								src/emulator.rs
								
								
								
								
							
							
						
						
									
										404
									
								
								src/emulator.rs
								
								
								
								
							| 
						 | 
				
			
			@ -1,34 +1,29 @@
 | 
			
		|||
use std::{
 | 
			
		||||
    collections::HashMap,
 | 
			
		||||
    fmt::Display,
 | 
			
		||||
    fs::{self, File},
 | 
			
		||||
    io::{Read, Seek, SeekFrom, Write},
 | 
			
		||||
    path::{Path, PathBuf},
 | 
			
		||||
    sync::{
 | 
			
		||||
        atomic::{AtomicBool, AtomicUsize, Ordering},
 | 
			
		||||
        atomic::{AtomicBool, Ordering},
 | 
			
		||||
        mpsc::{self, RecvError, TryRecvError},
 | 
			
		||||
        Arc,
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
use anyhow::Result;
 | 
			
		||||
use atomic::Atomic;
 | 
			
		||||
use bytemuck::NoUninit;
 | 
			
		||||
use egui_toast::{Toast, ToastKind, ToastOptions};
 | 
			
		||||
use tracing::{error, warn};
 | 
			
		||||
 | 
			
		||||
use crate::{audio::Audio, graphics::TextureSink};
 | 
			
		||||
pub use shrooms_vb_core::VBKey;
 | 
			
		||||
use shrooms_vb_core::{Sim, EXPECTED_FRAME_SIZE};
 | 
			
		||||
use shrooms_vb_core::{Sim, StopReason, EXPECTED_FRAME_SIZE};
 | 
			
		||||
pub use shrooms_vb_core::{VBKey, VBRegister, VBWatchpointType};
 | 
			
		||||
 | 
			
		||||
mod address_set;
 | 
			
		||||
mod shrooms_vb_core;
 | 
			
		||||
 | 
			
		||||
pub struct EmulatorBuilder {
 | 
			
		||||
    rom: Option<PathBuf>,
 | 
			
		||||
    commands: mpsc::Receiver<EmulatorCommand>,
 | 
			
		||||
    sim_count: Arc<AtomicUsize>,
 | 
			
		||||
    running: Arc<[AtomicBool; 2]>,
 | 
			
		||||
    has_game: Arc<[AtomicBool; 2]>,
 | 
			
		||||
    audio_on: Arc<[AtomicBool; 2]>,
 | 
			
		||||
    linked: Arc<AtomicBool>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)]
 | 
			
		||||
pub enum SimId {
 | 
			
		||||
    Player1,
 | 
			
		||||
| 
						 | 
				
			
			@ -45,6 +40,14 @@ impl SimId {
 | 
			
		|||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
impl Display for SimId {
 | 
			
		||||
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 | 
			
		||||
        f.write_str(match self {
 | 
			
		||||
            Self::Player1 => "Player 1",
 | 
			
		||||
            Self::Player2 => "Player 2",
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct Cart {
 | 
			
		||||
    rom_path: PathBuf,
 | 
			
		||||
| 
						 | 
				
			
			@ -83,23 +86,35 @@ fn sram_path(rom_path: &Path, sim_id: SimId) -> PathBuf {
 | 
			
		|||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub struct EmulatorBuilder {
 | 
			
		||||
    rom: Option<PathBuf>,
 | 
			
		||||
    commands: mpsc::Receiver<EmulatorCommand>,
 | 
			
		||||
    sim_state: Arc<[Atomic<SimState>; 2]>,
 | 
			
		||||
    state: Arc<Atomic<EmulatorState>>,
 | 
			
		||||
    audio_on: Arc<[AtomicBool; 2]>,
 | 
			
		||||
    linked: Arc<AtomicBool>,
 | 
			
		||||
    start_paused: bool,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl EmulatorBuilder {
 | 
			
		||||
    pub fn new() -> (Self, EmulatorClient) {
 | 
			
		||||
        let (queue, commands) = mpsc::channel();
 | 
			
		||||
        let builder = Self {
 | 
			
		||||
            rom: None,
 | 
			
		||||
            commands,
 | 
			
		||||
            sim_count: Arc::new(AtomicUsize::new(0)),
 | 
			
		||||
            running: Arc::new([AtomicBool::new(false), AtomicBool::new(false)]),
 | 
			
		||||
            has_game: Arc::new([AtomicBool::new(false), AtomicBool::new(false)]),
 | 
			
		||||
            sim_state: Arc::new([
 | 
			
		||||
                Atomic::new(SimState::Uninitialized),
 | 
			
		||||
                Atomic::new(SimState::Uninitialized),
 | 
			
		||||
            ]),
 | 
			
		||||
            state: Arc::new(Atomic::new(EmulatorState::Paused)),
 | 
			
		||||
            audio_on: Arc::new([AtomicBool::new(true), AtomicBool::new(true)]),
 | 
			
		||||
            linked: Arc::new(AtomicBool::new(false)),
 | 
			
		||||
            start_paused: false,
 | 
			
		||||
        };
 | 
			
		||||
        let client = EmulatorClient {
 | 
			
		||||
            queue,
 | 
			
		||||
            sim_count: builder.sim_count.clone(),
 | 
			
		||||
            running: builder.running.clone(),
 | 
			
		||||
            has_game: builder.has_game.clone(),
 | 
			
		||||
            sim_state: builder.sim_state.clone(),
 | 
			
		||||
            state: builder.state.clone(),
 | 
			
		||||
            audio_on: builder.audio_on.clone(),
 | 
			
		||||
            linked: builder.linked.clone(),
 | 
			
		||||
        };
 | 
			
		||||
| 
						 | 
				
			
			@ -113,18 +128,27 @@ impl EmulatorBuilder {
 | 
			
		|||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn start_paused(self, paused: bool) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            start_paused: paused,
 | 
			
		||||
            ..self
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn build(self) -> Result<Emulator> {
 | 
			
		||||
        let mut emulator = Emulator::new(
 | 
			
		||||
            self.commands,
 | 
			
		||||
            self.sim_count,
 | 
			
		||||
            self.running,
 | 
			
		||||
            self.has_game,
 | 
			
		||||
            self.sim_state,
 | 
			
		||||
            self.state,
 | 
			
		||||
            self.audio_on,
 | 
			
		||||
            self.linked,
 | 
			
		||||
        )?;
 | 
			
		||||
        if let Some(path) = self.rom {
 | 
			
		||||
            emulator.load_cart(SimId::Player1, &path)?;
 | 
			
		||||
        }
 | 
			
		||||
        if self.start_paused {
 | 
			
		||||
            emulator.pause_sims()?;
 | 
			
		||||
        }
 | 
			
		||||
        Ok(emulator)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -134,13 +158,13 @@ pub struct Emulator {
 | 
			
		|||
    carts: [Option<Cart>; 2],
 | 
			
		||||
    audio: Audio,
 | 
			
		||||
    commands: mpsc::Receiver<EmulatorCommand>,
 | 
			
		||||
    sim_count: Arc<AtomicUsize>,
 | 
			
		||||
    running: Arc<[AtomicBool; 2]>,
 | 
			
		||||
    has_game: Arc<[AtomicBool; 2]>,
 | 
			
		||||
    sim_state: Arc<[Atomic<SimState>; 2]>,
 | 
			
		||||
    state: Arc<Atomic<EmulatorState>>,
 | 
			
		||||
    audio_on: Arc<[AtomicBool; 2]>,
 | 
			
		||||
    linked: Arc<AtomicBool>,
 | 
			
		||||
    renderers: HashMap<SimId, TextureSink>,
 | 
			
		||||
    messages: HashMap<SimId, mpsc::Sender<Toast>>,
 | 
			
		||||
    debuggers: HashMap<SimId, DebugInfo>,
 | 
			
		||||
    eye_contents: Vec<u8>,
 | 
			
		||||
    audio_samples: Vec<f32>,
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -148,9 +172,8 @@ pub struct Emulator {
 | 
			
		|||
impl Emulator {
 | 
			
		||||
    fn new(
 | 
			
		||||
        commands: mpsc::Receiver<EmulatorCommand>,
 | 
			
		||||
        sim_count: Arc<AtomicUsize>,
 | 
			
		||||
        running: Arc<[AtomicBool; 2]>,
 | 
			
		||||
        has_game: Arc<[AtomicBool; 2]>,
 | 
			
		||||
        sim_state: Arc<[Atomic<SimState>; 2]>,
 | 
			
		||||
        state: Arc<Atomic<EmulatorState>>,
 | 
			
		||||
        audio_on: Arc<[AtomicBool; 2]>,
 | 
			
		||||
        linked: Arc<AtomicBool>,
 | 
			
		||||
    ) -> Result<Self> {
 | 
			
		||||
| 
						 | 
				
			
			@ -159,15 +182,15 @@ impl Emulator {
 | 
			
		|||
            carts: [None, None],
 | 
			
		||||
            audio: Audio::init()?,
 | 
			
		||||
            commands,
 | 
			
		||||
            sim_count,
 | 
			
		||||
            running,
 | 
			
		||||
            has_game,
 | 
			
		||||
            sim_state,
 | 
			
		||||
            state,
 | 
			
		||||
            audio_on,
 | 
			
		||||
            linked,
 | 
			
		||||
            renderers: HashMap::new(),
 | 
			
		||||
            messages: HashMap::new(),
 | 
			
		||||
            debuggers: HashMap::new(),
 | 
			
		||||
            eye_contents: vec![0u8; 384 * 224 * 2],
 | 
			
		||||
            audio_samples: vec![0.0; EXPECTED_FRAME_SIZE],
 | 
			
		||||
            audio_samples: Vec::with_capacity(EXPECTED_FRAME_SIZE),
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -198,17 +221,17 @@ impl Emulator {
 | 
			
		|||
        let index = sim_id.to_index();
 | 
			
		||||
        while self.sims.len() <= index {
 | 
			
		||||
            self.sims.push(Sim::new());
 | 
			
		||||
            self.sim_state[index].store(SimState::NoGame, Ordering::Release);
 | 
			
		||||
        }
 | 
			
		||||
        self.sim_count.store(self.sims.len(), Ordering::Relaxed);
 | 
			
		||||
        let sim = &mut self.sims[index];
 | 
			
		||||
        sim.reset();
 | 
			
		||||
        if let Some(cart) = new_cart {
 | 
			
		||||
            sim.load_cart(cart.rom.clone(), cart.sram.clone())?;
 | 
			
		||||
            self.carts[index] = Some(cart);
 | 
			
		||||
            self.has_game[index].store(true, Ordering::Release);
 | 
			
		||||
            self.sim_state[index].store(SimState::Ready, Ordering::Release);
 | 
			
		||||
        }
 | 
			
		||||
        if self.has_game[index].load(Ordering::Acquire) {
 | 
			
		||||
            self.running[index].store(true, Ordering::Release);
 | 
			
		||||
        if self.sim_state[index].load(Ordering::Acquire) == SimState::Ready {
 | 
			
		||||
            self.resume_sims();
 | 
			
		||||
        }
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -233,9 +256,31 @@ impl Emulator {
 | 
			
		|||
        self.linked.store(false, Ordering::Release);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn pause_sim(&mut self, sim_id: SimId) -> Result<()> {
 | 
			
		||||
        self.running[sim_id.to_index()].store(false, Ordering::Release);
 | 
			
		||||
        self.save_sram(sim_id)
 | 
			
		||||
    fn pause_sims(&mut self) -> Result<()> {
 | 
			
		||||
        if self
 | 
			
		||||
            .state
 | 
			
		||||
            .compare_exchange(
 | 
			
		||||
                EmulatorState::Running,
 | 
			
		||||
                EmulatorState::Paused,
 | 
			
		||||
                Ordering::AcqRel,
 | 
			
		||||
                Ordering::Relaxed,
 | 
			
		||||
            )
 | 
			
		||||
            .is_ok()
 | 
			
		||||
        {
 | 
			
		||||
            for sim_id in SimId::values() {
 | 
			
		||||
                self.save_sram(sim_id)?;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn resume_sims(&mut self) {
 | 
			
		||||
        let _ = self.state.compare_exchange(
 | 
			
		||||
            EmulatorState::Paused,
 | 
			
		||||
            EmulatorState::Running,
 | 
			
		||||
            Ordering::AcqRel,
 | 
			
		||||
            Ordering::Relaxed,
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn save_sram(&mut self, sim_id: SimId) -> Result<()> {
 | 
			
		||||
| 
						 | 
				
			
			@ -253,13 +298,75 @@ impl Emulator {
 | 
			
		|||
        self.save_sram(SimId::Player2)?;
 | 
			
		||||
        self.renderers.remove(&SimId::Player2);
 | 
			
		||||
        self.sims.truncate(1);
 | 
			
		||||
        self.sim_count.store(self.sims.len(), Ordering::Relaxed);
 | 
			
		||||
        self.running[SimId::Player2.to_index()].store(false, Ordering::Release);
 | 
			
		||||
        self.has_game[SimId::Player2.to_index()].store(false, Ordering::Release);
 | 
			
		||||
        self.sim_state[SimId::Player2.to_index()].store(SimState::Uninitialized, Ordering::Release);
 | 
			
		||||
        self.stop_debugging(SimId::Player2);
 | 
			
		||||
        self.linked.store(false, Ordering::Release);
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn start_debugging(&mut self, sim_id: SimId, sender: DebugSender) {
 | 
			
		||||
        if self.sim_state[sim_id.to_index()].load(Ordering::Acquire) != SimState::Ready {
 | 
			
		||||
            // Can't debug unless a game is connected
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        let debug = DebugInfo {
 | 
			
		||||
            sender,
 | 
			
		||||
            stop_reason: Some(DebugStopReason::Paused),
 | 
			
		||||
        };
 | 
			
		||||
        self.debuggers.insert(sim_id, debug);
 | 
			
		||||
        self.state
 | 
			
		||||
            .store(EmulatorState::Debugging, Ordering::Release);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn stop_debugging(&mut self, sim_id: SimId) {
 | 
			
		||||
        if let Some(sim) = self.sims.get_mut(sim_id.to_index()) {
 | 
			
		||||
            sim.clear_debug_state();
 | 
			
		||||
        }
 | 
			
		||||
        self.debuggers.remove(&sim_id);
 | 
			
		||||
        if self.debuggers.is_empty() {
 | 
			
		||||
            let _ = self.state.compare_exchange(
 | 
			
		||||
                EmulatorState::Debugging,
 | 
			
		||||
                EmulatorState::Running,
 | 
			
		||||
                Ordering::AcqRel,
 | 
			
		||||
                Ordering::Relaxed,
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn debug_interrupt(&mut self, sim_id: SimId) {
 | 
			
		||||
        self.debug_stop(sim_id, DebugStopReason::Paused);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn debug_stop(&mut self, sim_id: SimId, reason: DebugStopReason) {
 | 
			
		||||
        let Some(debugger) = self.debuggers.get_mut(&sim_id) else {
 | 
			
		||||
            self.stop_debugging(sim_id);
 | 
			
		||||
            return;
 | 
			
		||||
        };
 | 
			
		||||
        if debugger.stop_reason != Some(reason) {
 | 
			
		||||
            debugger.stop_reason = Some(reason);
 | 
			
		||||
            if debugger.sender.send(DebugEvent::Stopped(reason)).is_err() {
 | 
			
		||||
                self.stop_debugging(sim_id);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn debug_continue(&mut self, sim_id: SimId) -> bool {
 | 
			
		||||
        let Some(debugger) = self.debuggers.get_mut(&sim_id) else {
 | 
			
		||||
            self.stop_debugging(sim_id);
 | 
			
		||||
            return false;
 | 
			
		||||
        };
 | 
			
		||||
        debugger.stop_reason = None;
 | 
			
		||||
        true
 | 
			
		||||
    }
 | 
			
		||||
    fn debug_step(&mut self, sim_id: SimId) {
 | 
			
		||||
        if self.debug_continue(sim_id) {
 | 
			
		||||
            let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
 | 
			
		||||
                return;
 | 
			
		||||
            };
 | 
			
		||||
            sim.step();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn run(&mut self) {
 | 
			
		||||
        loop {
 | 
			
		||||
            let idle = self.tick();
 | 
			
		||||
| 
						 | 
				
			
			@ -289,9 +396,20 @@ impl Emulator {
 | 
			
		|||
 | 
			
		||||
    // returns true if the emulator is "idle" (i.e. this didn't output anything)
 | 
			
		||||
    pub fn tick(&mut self) -> bool {
 | 
			
		||||
        let p1_running = self.running[SimId::Player1.to_index()].load(Ordering::Acquire);
 | 
			
		||||
        let p2_running = self.running[SimId::Player2.to_index()].load(Ordering::Acquire);
 | 
			
		||||
        let mut idle = p1_running || p2_running;
 | 
			
		||||
        let p1_state = self.sim_state[SimId::Player1.to_index()].load(Ordering::Acquire);
 | 
			
		||||
        let p2_state = self.sim_state[SimId::Player2.to_index()].load(Ordering::Acquire);
 | 
			
		||||
        let state = self.state.load(Ordering::Acquire);
 | 
			
		||||
 | 
			
		||||
        // Emulation
 | 
			
		||||
        // Don't emulate if the state is "paused", or if any sim is paused in the debugger
 | 
			
		||||
        let running = match state {
 | 
			
		||||
            EmulatorState::Paused => false,
 | 
			
		||||
            EmulatorState::Running => true,
 | 
			
		||||
            EmulatorState::Debugging => self.debuggers.values().all(|d| d.stop_reason.is_none()),
 | 
			
		||||
        };
 | 
			
		||||
        let p1_running = running && p1_state == SimState::Ready;
 | 
			
		||||
        let p2_running = running && p2_state == SimState::Ready;
 | 
			
		||||
        let mut idle = !p1_running && !p2_running;
 | 
			
		||||
        if p1_running && p2_running {
 | 
			
		||||
            Sim::emulate_many(&mut self.sims);
 | 
			
		||||
        } else if p1_running {
 | 
			
		||||
| 
						 | 
				
			
			@ -300,6 +418,26 @@ impl Emulator {
 | 
			
		|||
            self.sims[SimId::Player2.to_index()].emulate();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Debug state
 | 
			
		||||
        if state == EmulatorState::Debugging {
 | 
			
		||||
            for sim_id in SimId::values() {
 | 
			
		||||
                let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
 | 
			
		||||
                    continue;
 | 
			
		||||
                };
 | 
			
		||||
                if let Some(reason) = sim.stop_reason() {
 | 
			
		||||
                    let stop_reason = match reason {
 | 
			
		||||
                        StopReason::Stepped => DebugStopReason::Trace,
 | 
			
		||||
                        StopReason::Watchpoint(watch, address) => {
 | 
			
		||||
                            DebugStopReason::Watchpoint(watch, address)
 | 
			
		||||
                        }
 | 
			
		||||
                        StopReason::Breakpoint => DebugStopReason::Breakpoint,
 | 
			
		||||
                    };
 | 
			
		||||
                    self.debug_stop(sim_id, stop_reason);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Video
 | 
			
		||||
        for sim_id in SimId::values() {
 | 
			
		||||
            let Some(renderer) = self.renderers.get_mut(&sim_id) else {
 | 
			
		||||
                continue;
 | 
			
		||||
| 
						 | 
				
			
			@ -314,24 +452,27 @@ impl Emulator {
 | 
			
		|||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Audio
 | 
			
		||||
        // Audio playback speed is how we keep the emulator running in real time.
 | 
			
		||||
        // Even if we're muted, call `read_samples` to know how many frames of silence to play.
 | 
			
		||||
        let p1_audio =
 | 
			
		||||
            p1_running && self.audio_on[SimId::Player1.to_index()].load(Ordering::Acquire);
 | 
			
		||||
        let p2_audio =
 | 
			
		||||
            p2_running && self.audio_on[SimId::Player2.to_index()].load(Ordering::Acquire);
 | 
			
		||||
        let weight = if p1_audio && p2_audio { 0.5 } else { 1.0 };
 | 
			
		||||
        if p1_audio {
 | 
			
		||||
        let (p1_weight, p2_weight) = match (p1_audio, p2_audio) {
 | 
			
		||||
            (true, true) => (0.5, 0.5),
 | 
			
		||||
            (true, false) => (1.0, 0.0),
 | 
			
		||||
            (false, true) => (0.0, 1.0),
 | 
			
		||||
            (false, false) => (0.0, 0.0),
 | 
			
		||||
        };
 | 
			
		||||
        if let Some(sim) = self.sims.get_mut(SimId::Player1.to_index()) {
 | 
			
		||||
                sim.read_samples(&mut self.audio_samples, weight);
 | 
			
		||||
            sim.read_samples(&mut self.audio_samples, p1_weight);
 | 
			
		||||
        }
 | 
			
		||||
        }
 | 
			
		||||
        if p2_audio {
 | 
			
		||||
        if let Some(sim) = self.sims.get_mut(SimId::Player2.to_index()) {
 | 
			
		||||
                sim.read_samples(&mut self.audio_samples, weight);
 | 
			
		||||
            sim.read_samples(&mut self.audio_samples, p2_weight);
 | 
			
		||||
        }
 | 
			
		||||
        }
 | 
			
		||||
        if self.audio_samples.is_empty() {
 | 
			
		||||
            self.audio_samples.resize(EXPECTED_FRAME_SIZE, 0.0);
 | 
			
		||||
        } else {
 | 
			
		||||
        if !self.audio_samples.is_empty() {
 | 
			
		||||
            idle = false;
 | 
			
		||||
        }
 | 
			
		||||
        self.audio.update(&self.audio_samples);
 | 
			
		||||
| 
						 | 
				
			
			@ -367,19 +508,78 @@ impl Emulator {
 | 
			
		|||
                }
 | 
			
		||||
            }
 | 
			
		||||
            EmulatorCommand::Pause => {
 | 
			
		||||
                for sim_id in SimId::values() {
 | 
			
		||||
                    if let Err(error) = self.pause_sim(sim_id) {
 | 
			
		||||
                        self.report_error(sim_id, format!("Error pausing: {error}"));
 | 
			
		||||
                    }
 | 
			
		||||
                if let Err(error) = self.pause_sims() {
 | 
			
		||||
                    self.report_error(SimId::Player1, format!("Error pausing: {error}"));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            EmulatorCommand::Resume => {
 | 
			
		||||
                for sim_id in SimId::values() {
 | 
			
		||||
                    let index = sim_id.to_index();
 | 
			
		||||
                    if self.has_game[index].load(Ordering::Acquire) {
 | 
			
		||||
                        self.running[index].store(true, Ordering::Relaxed);
 | 
			
		||||
                self.resume_sims();
 | 
			
		||||
            }
 | 
			
		||||
            EmulatorCommand::StartDebugging(sim_id, debugger) => {
 | 
			
		||||
                self.start_debugging(sim_id, debugger);
 | 
			
		||||
            }
 | 
			
		||||
            EmulatorCommand::StopDebugging(sim_id) => {
 | 
			
		||||
                self.stop_debugging(sim_id);
 | 
			
		||||
            }
 | 
			
		||||
            EmulatorCommand::DebugInterrupt(sim_id) => {
 | 
			
		||||
                self.debug_interrupt(sim_id);
 | 
			
		||||
            }
 | 
			
		||||
            EmulatorCommand::DebugContinue(sim_id) => {
 | 
			
		||||
                self.debug_continue(sim_id);
 | 
			
		||||
            }
 | 
			
		||||
            EmulatorCommand::DebugStep(sim_id) => {
 | 
			
		||||
                self.debug_step(sim_id);
 | 
			
		||||
            }
 | 
			
		||||
            EmulatorCommand::ReadRegister(sim_id, register, done) => {
 | 
			
		||||
                let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
 | 
			
		||||
                    return;
 | 
			
		||||
                };
 | 
			
		||||
                let value = sim.read_register(register);
 | 
			
		||||
                let _ = done.send(value);
 | 
			
		||||
            }
 | 
			
		||||
            EmulatorCommand::WriteRegister(sim_id, register, value) => {
 | 
			
		||||
                let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
 | 
			
		||||
                    return;
 | 
			
		||||
                };
 | 
			
		||||
                sim.write_register(register, value);
 | 
			
		||||
            }
 | 
			
		||||
            EmulatorCommand::ReadMemory(sim_id, start, length, mut buffer, done) => {
 | 
			
		||||
                let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
 | 
			
		||||
                    return;
 | 
			
		||||
                };
 | 
			
		||||
                sim.read_memory(start, length, &mut buffer);
 | 
			
		||||
                let _ = done.send(buffer);
 | 
			
		||||
            }
 | 
			
		||||
            EmulatorCommand::WriteMemory(sim_id, start, buffer, done) => {
 | 
			
		||||
                let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
 | 
			
		||||
                    return;
 | 
			
		||||
                };
 | 
			
		||||
                sim.write_memory(start, &buffer);
 | 
			
		||||
                let _ = done.send(buffer);
 | 
			
		||||
            }
 | 
			
		||||
            EmulatorCommand::AddBreakpoint(sim_id, address) => {
 | 
			
		||||
                let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
 | 
			
		||||
                    return;
 | 
			
		||||
                };
 | 
			
		||||
                sim.add_breakpoint(address);
 | 
			
		||||
            }
 | 
			
		||||
            EmulatorCommand::RemoveBreakpoint(sim_id, address) => {
 | 
			
		||||
                let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
 | 
			
		||||
                    return;
 | 
			
		||||
                };
 | 
			
		||||
                sim.remove_breakpoint(address);
 | 
			
		||||
            }
 | 
			
		||||
            EmulatorCommand::AddWatchpoint(sim_id, address, length, watch) => {
 | 
			
		||||
                let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
 | 
			
		||||
                    return;
 | 
			
		||||
                };
 | 
			
		||||
                sim.add_watchpoint(address, length, watch);
 | 
			
		||||
            }
 | 
			
		||||
            EmulatorCommand::RemoveWatchpoint(sim_id, address, length, watch) => {
 | 
			
		||||
                let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
 | 
			
		||||
                    return;
 | 
			
		||||
                };
 | 
			
		||||
                sim.remove_watchpoint(address, length, watch);
 | 
			
		||||
            }
 | 
			
		||||
            EmulatorCommand::SetAudioEnabled(p1, p2) => {
 | 
			
		||||
                self.audio_on[SimId::Player1.to_index()].store(p1, Ordering::Release);
 | 
			
		||||
| 
						 | 
				
			
			@ -426,7 +626,7 @@ impl Emulator {
 | 
			
		|||
                return;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        eprintln!("{}", message);
 | 
			
		||||
        error!("{}", message);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -438,6 +638,19 @@ pub enum EmulatorCommand {
 | 
			
		|||
    StopSecondSim,
 | 
			
		||||
    Pause,
 | 
			
		||||
    Resume,
 | 
			
		||||
    StartDebugging(SimId, DebugSender),
 | 
			
		||||
    StopDebugging(SimId),
 | 
			
		||||
    DebugInterrupt(SimId),
 | 
			
		||||
    DebugContinue(SimId),
 | 
			
		||||
    DebugStep(SimId),
 | 
			
		||||
    ReadRegister(SimId, VBRegister, oneshot::Sender<u32>),
 | 
			
		||||
    WriteRegister(SimId, VBRegister, u32),
 | 
			
		||||
    ReadMemory(SimId, u32, usize, Vec<u8>, oneshot::Sender<Vec<u8>>),
 | 
			
		||||
    WriteMemory(SimId, u32, Vec<u8>, oneshot::Sender<Vec<u8>>),
 | 
			
		||||
    AddBreakpoint(SimId, u32),
 | 
			
		||||
    RemoveBreakpoint(SimId, u32),
 | 
			
		||||
    AddWatchpoint(SimId, u32, usize, VBWatchpointType),
 | 
			
		||||
    RemoveWatchpoint(SimId, u32, usize, VBWatchpointType),
 | 
			
		||||
    SetAudioEnabled(bool, bool),
 | 
			
		||||
    Link,
 | 
			
		||||
    Unlink,
 | 
			
		||||
| 
						 | 
				
			
			@ -446,37 +659,72 @@ pub enum EmulatorCommand {
 | 
			
		|||
    Exit(oneshot::Sender<()>),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, NoUninit)]
 | 
			
		||||
#[repr(usize)]
 | 
			
		||||
pub enum SimState {
 | 
			
		||||
    Uninitialized,
 | 
			
		||||
    NoGame,
 | 
			
		||||
    Ready,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, NoUninit)]
 | 
			
		||||
#[repr(usize)]
 | 
			
		||||
pub enum EmulatorState {
 | 
			
		||||
    Paused,
 | 
			
		||||
    Running,
 | 
			
		||||
    Debugging,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type DebugSender = tokio::sync::mpsc::UnboundedSender<DebugEvent>;
 | 
			
		||||
 | 
			
		||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
 | 
			
		||||
pub enum DebugStopReason {
 | 
			
		||||
    // We are stepping
 | 
			
		||||
    Trace,
 | 
			
		||||
    // We hit a breakpoint
 | 
			
		||||
    Breakpoint,
 | 
			
		||||
    // We hit a watchpoint
 | 
			
		||||
    Watchpoint(VBWatchpointType, u32),
 | 
			
		||||
    // The debugger told us to pause
 | 
			
		||||
    Paused,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct DebugInfo {
 | 
			
		||||
    sender: DebugSender,
 | 
			
		||||
    stop_reason: Option<DebugStopReason>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub enum DebugEvent {
 | 
			
		||||
    Stopped(DebugStopReason),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Clone)]
 | 
			
		||||
pub struct EmulatorClient {
 | 
			
		||||
    queue: mpsc::Sender<EmulatorCommand>,
 | 
			
		||||
    sim_count: Arc<AtomicUsize>,
 | 
			
		||||
    running: Arc<[AtomicBool; 2]>,
 | 
			
		||||
    has_game: Arc<[AtomicBool; 2]>,
 | 
			
		||||
    sim_state: Arc<[Atomic<SimState>; 2]>,
 | 
			
		||||
    state: Arc<Atomic<EmulatorState>>,
 | 
			
		||||
    audio_on: Arc<[AtomicBool; 2]>,
 | 
			
		||||
    linked: Arc<AtomicBool>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl EmulatorClient {
 | 
			
		||||
    pub fn has_player_2(&self) -> bool {
 | 
			
		||||
        self.sim_count.load(Ordering::Acquire) == 2
 | 
			
		||||
    pub fn sim_state(&self, sim_id: SimId) -> SimState {
 | 
			
		||||
        self.sim_state[sim_id.to_index()].load(Ordering::Acquire)
 | 
			
		||||
    }
 | 
			
		||||
    pub fn is_running(&self, sim_id: SimId) -> bool {
 | 
			
		||||
        self.running[sim_id.to_index()].load(Ordering::Acquire)
 | 
			
		||||
    }
 | 
			
		||||
    pub fn has_game(&self, sim_id: SimId) -> bool {
 | 
			
		||||
        self.has_game[sim_id.to_index()].load(Ordering::Acquire)
 | 
			
		||||
    }
 | 
			
		||||
    pub fn are_sims_linked(&self) -> bool {
 | 
			
		||||
        self.linked.load(Ordering::Acquire)
 | 
			
		||||
    pub fn emulator_state(&self) -> EmulatorState {
 | 
			
		||||
        self.state.load(Ordering::Acquire)
 | 
			
		||||
    }
 | 
			
		||||
    pub fn is_audio_enabled(&self, sim_id: SimId) -> bool {
 | 
			
		||||
        self.audio_on[sim_id.to_index()].load(Ordering::Acquire)
 | 
			
		||||
    }
 | 
			
		||||
    pub fn are_sims_linked(&self) -> bool {
 | 
			
		||||
        self.linked.load(Ordering::Acquire)
 | 
			
		||||
    }
 | 
			
		||||
    pub fn send_command(&self, command: EmulatorCommand) -> bool {
 | 
			
		||||
        match self.queue.send(command) {
 | 
			
		||||
            Ok(()) => true,
 | 
			
		||||
            Err(err) => {
 | 
			
		||||
                eprintln!(
 | 
			
		||||
                warn!(
 | 
			
		||||
                    "could not send command {:?} as emulator is shut down",
 | 
			
		||||
                    err.0
 | 
			
		||||
                );
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,270 @@
 | 
			
		|||
use std::{
 | 
			
		||||
    collections::{BTreeMap, BTreeSet},
 | 
			
		||||
    ops::Bound,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Default)]
 | 
			
		||||
pub struct AddressSet {
 | 
			
		||||
    ranges: BTreeSet<(u32, usize)>,
 | 
			
		||||
    bounds: BTreeMap<u32, usize>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl AddressSet {
 | 
			
		||||
    pub fn new() -> Self {
 | 
			
		||||
        Self::default()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn add(&mut self, address: u32, length: usize) {
 | 
			
		||||
        if length == 0 || !self.ranges.insert((address, length)) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        let end = (address as usize)
 | 
			
		||||
            .checked_add(length)
 | 
			
		||||
            .and_then(|e| u32::try_from(e).ok());
 | 
			
		||||
 | 
			
		||||
        if let Some(end) = end {
 | 
			
		||||
            let val_before = self.bounds.range(..=end).next_back().map_or(0, |(_, &v)| v);
 | 
			
		||||
            self.bounds.insert(end, val_before);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let val_before = self
 | 
			
		||||
            .bounds
 | 
			
		||||
            .range(..address)
 | 
			
		||||
            .next_back()
 | 
			
		||||
            .map_or(0, |(_, &v)| v);
 | 
			
		||||
        if let Some(&val_at) = self.bounds.get(&address) {
 | 
			
		||||
            if val_before == val_at + 1 {
 | 
			
		||||
                self.bounds.remove(&address);
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            self.bounds.insert(address, val_before);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let start_bound = Bound::Included(address);
 | 
			
		||||
        let end_bound = match end {
 | 
			
		||||
            Some(e) => Bound::Excluded(e),
 | 
			
		||||
            None => Bound::Unbounded,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        for (_, val) in self.bounds.range_mut((start_bound, end_bound)) {
 | 
			
		||||
            *val += 1;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn remove(&mut self, address: u32, length: usize) {
 | 
			
		||||
        if !self.ranges.remove(&(address, length)) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        let end = (address as usize)
 | 
			
		||||
            .checked_add(length)
 | 
			
		||||
            .and_then(|e| u32::try_from(e).ok());
 | 
			
		||||
 | 
			
		||||
        if let Some(end) = end {
 | 
			
		||||
            let val_before = self.bounds.range(..end).next_back().map_or(0, |(_, &v)| v);
 | 
			
		||||
            if let Some(&val_at) = self.bounds.get(&end) {
 | 
			
		||||
                if val_at + 1 == val_before {
 | 
			
		||||
                    self.bounds.remove(&end);
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                self.bounds.insert(end, val_before);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let val_before = self
 | 
			
		||||
            .bounds
 | 
			
		||||
            .range(..address)
 | 
			
		||||
            .next_back()
 | 
			
		||||
            .map_or(0, |(_, &v)| v);
 | 
			
		||||
        if let Some(&val_at) = self.bounds.get(&address) {
 | 
			
		||||
            if val_before + 1 == val_at {
 | 
			
		||||
                self.bounds.remove(&address);
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            self.bounds.insert(address, val_before);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let start_bound = Bound::Included(address);
 | 
			
		||||
        let end_bound = match end {
 | 
			
		||||
            Some(e) => Bound::Excluded(e),
 | 
			
		||||
            None => Bound::Unbounded,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        for (_, val) in self.bounds.range_mut((start_bound, end_bound)) {
 | 
			
		||||
            *val -= 1;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn clear(&mut self) {
 | 
			
		||||
        self.ranges.clear();
 | 
			
		||||
        self.bounds.clear();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn is_empty(&self) -> bool {
 | 
			
		||||
        self.bounds.is_empty()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn contains(&self, address: u32) -> bool {
 | 
			
		||||
        self.bounds
 | 
			
		||||
            .range(..=address)
 | 
			
		||||
            .next_back()
 | 
			
		||||
            .is_some_and(|(_, &val)| val > 0)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn start_of_range_containing(&self, address: u32) -> Option<u32> {
 | 
			
		||||
        if !self.contains(address) {
 | 
			
		||||
            return None;
 | 
			
		||||
        }
 | 
			
		||||
        self.ranges
 | 
			
		||||
            .range(..=(address, usize::MAX))
 | 
			
		||||
            .rev()
 | 
			
		||||
            .find_map(|&(start, length)| {
 | 
			
		||||
                let contains = start <= address
 | 
			
		||||
                    && (start as usize)
 | 
			
		||||
                        .checked_add(length)
 | 
			
		||||
                        .is_none_or(|end| end > address as usize);
 | 
			
		||||
                contains.then_some(start)
 | 
			
		||||
            })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(test)]
 | 
			
		||||
mod tests {
 | 
			
		||||
    use super::AddressSet;
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn should_not_include_addresses_when_empty() {
 | 
			
		||||
        let set = AddressSet::new();
 | 
			
		||||
        assert!(set.is_empty());
 | 
			
		||||
        assert!(!set.contains(0x13374200));
 | 
			
		||||
        assert_eq!(set.start_of_range_containing(0x13374200), None);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn should_include_addresses_when_full() {
 | 
			
		||||
        let mut set = AddressSet::new();
 | 
			
		||||
        set.add(0x00000000, 0x100000000);
 | 
			
		||||
        assert!(set.contains(0x13374200));
 | 
			
		||||
        assert_eq!(set.start_of_range_containing(0x13374200), Some(0x00000000));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn should_ignore_empty_address_ranges() {
 | 
			
		||||
        let mut set = AddressSet::new();
 | 
			
		||||
        set.add(0x13374200, 0);
 | 
			
		||||
        assert!(set.is_empty());
 | 
			
		||||
        assert!(!set.contains(0x13374200));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn should_add_addresses_idempotently() {
 | 
			
		||||
        let mut set = AddressSet::new();
 | 
			
		||||
        set.add(0x13374200, 1);
 | 
			
		||||
        set.add(0x13374200, 1);
 | 
			
		||||
        set.remove(0x13374200, 1);
 | 
			
		||||
        assert!(set.is_empty());
 | 
			
		||||
        assert!(!set.contains(0x13374200));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn should_remove_addresses_idempotently() {
 | 
			
		||||
        let mut set = AddressSet::new();
 | 
			
		||||
        set.add(0x13374200, 1);
 | 
			
		||||
        set.remove(0x13374200, 1);
 | 
			
		||||
        set.remove(0x13374200, 1);
 | 
			
		||||
        assert!(set.is_empty());
 | 
			
		||||
        assert!(!set.contains(0x13374200));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn should_report_address_in_range() {
 | 
			
		||||
        let mut set = AddressSet::new();
 | 
			
		||||
        set.add(0x13374200, 4);
 | 
			
		||||
        assert!(!set.contains(0x133741ff));
 | 
			
		||||
        for address in 0x13374200..0x13374204 {
 | 
			
		||||
            assert!(set.contains(address));
 | 
			
		||||
        }
 | 
			
		||||
        assert!(!set.contains(0x13374204));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn should_allow_overlapping_addresses() {
 | 
			
		||||
        let mut set = AddressSet::new();
 | 
			
		||||
        set.add(0x13374200, 4);
 | 
			
		||||
        set.add(0x13374201, 1);
 | 
			
		||||
        set.add(0x13374202, 2);
 | 
			
		||||
 | 
			
		||||
        assert!(!set.contains(0x133741ff));
 | 
			
		||||
        for address in 0x13374200..0x13374204 {
 | 
			
		||||
            assert!(set.contains(address));
 | 
			
		||||
        }
 | 
			
		||||
        assert!(!set.contains(0x13374204));
 | 
			
		||||
 | 
			
		||||
        set.remove(0x13374200, 4);
 | 
			
		||||
        assert!(!set.contains(0x13374200));
 | 
			
		||||
        for address in 0x13374201..0x13374204 {
 | 
			
		||||
            assert!(set.contains(address));
 | 
			
		||||
        }
 | 
			
		||||
        assert!(!set.contains(0x13374204));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn should_allow_removing_overlapped_address_ranges() {
 | 
			
		||||
        let mut set = AddressSet::new();
 | 
			
		||||
        set.add(0x13374200, 8);
 | 
			
		||||
        set.add(0x13374204, 8);
 | 
			
		||||
        set.remove(0x13374204, 8);
 | 
			
		||||
 | 
			
		||||
        for address in 0x13374200..0x13374208 {
 | 
			
		||||
            assert!(set.contains(address));
 | 
			
		||||
        }
 | 
			
		||||
        for address in 0x13374208..0x1337420c {
 | 
			
		||||
            assert!(!set.contains(address));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn should_merge_adjacent_ranges() {
 | 
			
		||||
        let mut set = AddressSet::new();
 | 
			
		||||
        set.add(0x13374200, 4);
 | 
			
		||||
        set.add(0x13374204, 4);
 | 
			
		||||
        set.add(0x13374208, 4);
 | 
			
		||||
        set.add(0x1337420c, 4);
 | 
			
		||||
 | 
			
		||||
        assert!(!set.contains(0x133741ff));
 | 
			
		||||
        for address in 0x13374200..0x13374210 {
 | 
			
		||||
            assert!(set.contains(address));
 | 
			
		||||
        }
 | 
			
		||||
        assert!(!set.contains(0x13374210));
 | 
			
		||||
 | 
			
		||||
        set.remove(0x13374200, 4);
 | 
			
		||||
        set.remove(0x13374204, 4);
 | 
			
		||||
        set.remove(0x13374208, 4);
 | 
			
		||||
        set.remove(0x1337420c, 4);
 | 
			
		||||
 | 
			
		||||
        assert!(set.is_empty());
 | 
			
		||||
        for address in 0x133741ff..=0x13374210 {
 | 
			
		||||
            assert!(!set.contains(address));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn should_find_start_of_range() {
 | 
			
		||||
        let mut set = AddressSet::new();
 | 
			
		||||
        set.add(0x13374200, 4);
 | 
			
		||||
        assert_eq!(set.start_of_range_containing(0x133741ff), None);
 | 
			
		||||
        for address in 0x13374200..0x13374204 {
 | 
			
		||||
            assert_eq!(set.start_of_range_containing(address), Some(0x13374200));
 | 
			
		||||
        }
 | 
			
		||||
        assert_eq!(set.start_of_range_containing(0x13374204), None);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn should_ignore_ranges_not_containing_address() {
 | 
			
		||||
        let mut set = AddressSet::new();
 | 
			
		||||
        set.add(0x10000000, 1024);
 | 
			
		||||
        set.add(0x30000000, 1024);
 | 
			
		||||
 | 
			
		||||
        assert!(!set.contains(0x20000000));
 | 
			
		||||
        assert_eq!(set.start_of_range_containing(0x20000000), None);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -5,6 +5,8 @@ use bitflags::bitflags;
 | 
			
		|||
use num_derive::{FromPrimitive, ToPrimitive};
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
 | 
			
		||||
use super::address_set::AddressSet;
 | 
			
		||||
 | 
			
		||||
#[repr(C)]
 | 
			
		||||
struct VB {
 | 
			
		||||
    _data: [u8; 0],
 | 
			
		||||
| 
						 | 
				
			
			@ -55,7 +57,38 @@ bitflags! {
 | 
			
		|||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Clone, Copy, Debug)]
 | 
			
		||||
pub enum VBRegister {
 | 
			
		||||
    Program(u32),
 | 
			
		||||
    System(u32),
 | 
			
		||||
    PC,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
 | 
			
		||||
pub enum VBWatchpointType {
 | 
			
		||||
    Read,
 | 
			
		||||
    Write,
 | 
			
		||||
    Access,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type OnExecute =
 | 
			
		||||
    extern "C" fn(sim: *mut VB, address: u32, code: *const u16, length: c_int) -> c_int;
 | 
			
		||||
type OnFrame = extern "C" fn(sim: *mut VB) -> c_int;
 | 
			
		||||
type OnRead = extern "C" fn(
 | 
			
		||||
    sim: *mut VB,
 | 
			
		||||
    address: u32,
 | 
			
		||||
    type_: VBDataType,
 | 
			
		||||
    value: *mut i32,
 | 
			
		||||
    cycles: *mut u32,
 | 
			
		||||
) -> c_int;
 | 
			
		||||
type OnWrite = extern "C" fn(
 | 
			
		||||
    sim: *mut VB,
 | 
			
		||||
    address: u32,
 | 
			
		||||
    type_: VBDataType,
 | 
			
		||||
    value: *mut i32,
 | 
			
		||||
    cycles: *mut u32,
 | 
			
		||||
    cancel: *mut c_int,
 | 
			
		||||
) -> c_int;
 | 
			
		||||
 | 
			
		||||
#[link(name = "vb")]
 | 
			
		||||
extern "C" {
 | 
			
		||||
| 
						 | 
				
			
			@ -77,6 +110,10 @@ extern "C" {
 | 
			
		|||
        right_stride_x: c_int,
 | 
			
		||||
        right_stride_y: c_int,
 | 
			
		||||
    );
 | 
			
		||||
    #[link_name = "vbGetProgramCounter"]
 | 
			
		||||
    fn vb_get_program_counter(sim: *mut VB) -> u32;
 | 
			
		||||
    #[link_name = "vbGetProgramRegister"]
 | 
			
		||||
    fn vb_get_program_register(sim: *mut VB, index: c_uint) -> i32;
 | 
			
		||||
    #[link_name = "vbGetSamples"]
 | 
			
		||||
    fn vb_get_samples(
 | 
			
		||||
        sim: *mut VB,
 | 
			
		||||
| 
						 | 
				
			
			@ -84,24 +121,36 @@ extern "C" {
 | 
			
		|||
        capacity: *mut c_uint,
 | 
			
		||||
        position: *mut c_uint,
 | 
			
		||||
    ) -> *mut c_void;
 | 
			
		||||
    #[link_name = "vbGetSystemRegister"]
 | 
			
		||||
    fn vb_get_system_register(sim: *mut VB, index: c_uint) -> i32;
 | 
			
		||||
    #[link_name = "vbGetUserData"]
 | 
			
		||||
    fn vb_get_user_data(sim: *mut VB) -> *mut c_void;
 | 
			
		||||
    #[link_name = "vbInit"]
 | 
			
		||||
    fn vb_init(sim: *mut VB) -> *mut VB;
 | 
			
		||||
    #[link_name = "vbRead"]
 | 
			
		||||
    fn vb_read(sim: *mut VB, address: u32, typ_: VBDataType) -> i32;
 | 
			
		||||
    #[link_name = "vbReset"]
 | 
			
		||||
    fn vb_reset(sim: *mut VB);
 | 
			
		||||
    #[link_name = "vbSetCartRAM"]
 | 
			
		||||
    fn vb_set_cart_ram(sim: *mut VB, sram: *mut c_void, size: u32) -> c_int;
 | 
			
		||||
    #[link_name = "vbSetCartROM"]
 | 
			
		||||
    fn vb_set_cart_rom(sim: *mut VB, rom: *mut c_void, size: u32) -> c_int;
 | 
			
		||||
    #[link_name = "vbSetExecuteCallback"]
 | 
			
		||||
    fn vb_set_execute_callback(sim: *mut VB, callback: Option<OnExecute>) -> Option<OnExecute>;
 | 
			
		||||
    #[link_name = "vbSetFrameCallback"]
 | 
			
		||||
    fn vb_set_frame_callback(sim: *mut VB, on_frame: OnFrame);
 | 
			
		||||
    fn vb_set_frame_callback(sim: *mut VB, callback: Option<OnFrame>) -> Option<OnFrame>;
 | 
			
		||||
    #[link_name = "vbSetKeys"]
 | 
			
		||||
    fn vb_set_keys(sim: *mut VB, keys: u16) -> u16;
 | 
			
		||||
    #[link_name = "vbSetOption"]
 | 
			
		||||
    fn vb_set_option(sim: *mut VB, key: VBOption, value: c_int);
 | 
			
		||||
    #[link_name = "vbSetPeer"]
 | 
			
		||||
    fn vb_set_peer(sim: *mut VB, peer: *mut VB);
 | 
			
		||||
    #[link_name = "vbSetProgramCounter"]
 | 
			
		||||
    fn vb_set_program_counter(sim: *mut VB, value: u32) -> u32;
 | 
			
		||||
    #[link_name = "vbSetProgramRegister"]
 | 
			
		||||
    fn vb_set_program_register(sim: *mut VB, index: c_uint, value: i32) -> i32;
 | 
			
		||||
    #[link_name = "vbSetReadCallback"]
 | 
			
		||||
    fn vb_set_read_callback(sim: *mut VB, callback: Option<OnRead>) -> Option<OnRead>;
 | 
			
		||||
    #[link_name = "vbSetSamples"]
 | 
			
		||||
    fn vb_set_samples(
 | 
			
		||||
        sim: *mut VB,
 | 
			
		||||
| 
						 | 
				
			
			@ -109,13 +158,20 @@ extern "C" {
 | 
			
		|||
        typ_: VBDataType,
 | 
			
		||||
        capacity: c_uint,
 | 
			
		||||
    ) -> c_int;
 | 
			
		||||
    #[link_name = "vbSetSystemRegister"]
 | 
			
		||||
    fn vb_set_system_register(sim: *mut VB, index: c_uint, value: u32) -> u32;
 | 
			
		||||
    #[link_name = "vbSetUserData"]
 | 
			
		||||
    fn vb_set_user_data(sim: *mut VB, tag: *mut c_void);
 | 
			
		||||
    #[link_name = "vbSetWriteCallback"]
 | 
			
		||||
    fn vb_set_write_callback(sim: *mut VB, callback: Option<OnWrite>) -> Option<OnWrite>;
 | 
			
		||||
    #[link_name = "vbSizeOf"]
 | 
			
		||||
    fn vb_size_of() -> usize;
 | 
			
		||||
    #[link_name = "vbWrite"]
 | 
			
		||||
    fn vb_write(sim: *mut VB, address: u32, _type: VBDataType, value: i32) -> i32;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extern "C" fn on_frame(sim: *mut VB) -> i32 {
 | 
			
		||||
#[no_mangle]
 | 
			
		||||
extern "C" fn on_frame(sim: *mut VB) -> c_int {
 | 
			
		||||
    // SAFETY: the *mut VB owns its userdata.
 | 
			
		||||
    // There is no way for the userdata to be null or otherwise invalid.
 | 
			
		||||
    let data: &mut VBState = unsafe { &mut *vb_get_user_data(sim).cast() };
 | 
			
		||||
| 
						 | 
				
			
			@ -123,12 +179,109 @@ extern "C" fn on_frame(sim: *mut VB) -> i32 {
 | 
			
		|||
    1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[no_mangle]
 | 
			
		||||
extern "C" fn on_execute(sim: *mut VB, address: u32, _code: *const u16, _length: c_int) -> c_int {
 | 
			
		||||
    // SAFETY: the *mut VB owns its userdata.
 | 
			
		||||
    // There is no way for the userdata to be null or otherwise invalid.
 | 
			
		||||
    let data: &mut VBState = unsafe { &mut *vb_get_user_data(sim).cast() };
 | 
			
		||||
 | 
			
		||||
    let mut stopped = data.stop_reason.is_some();
 | 
			
		||||
    if data.step_from.is_some_and(|s| s != address) {
 | 
			
		||||
        data.step_from = None;
 | 
			
		||||
        data.stop_reason = Some(StopReason::Stepped);
 | 
			
		||||
        stopped = true;
 | 
			
		||||
    }
 | 
			
		||||
    if data.breakpoints.binary_search(&address).is_ok() {
 | 
			
		||||
        data.stop_reason = Some(StopReason::Breakpoint);
 | 
			
		||||
        stopped = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if stopped {
 | 
			
		||||
        1
 | 
			
		||||
    } else {
 | 
			
		||||
        0
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[no_mangle]
 | 
			
		||||
extern "C" fn on_read(
 | 
			
		||||
    sim: *mut VB,
 | 
			
		||||
    address: u32,
 | 
			
		||||
    _type: VBDataType,
 | 
			
		||||
    _value: *mut i32,
 | 
			
		||||
    _cycles: *mut u32,
 | 
			
		||||
) -> c_int {
 | 
			
		||||
    // SAFETY: the *mut VB owns its userdata.
 | 
			
		||||
    // There is no way for the userdata to be null or otherwise invalid.
 | 
			
		||||
    let data: &mut VBState = unsafe { &mut *vb_get_user_data(sim).cast() };
 | 
			
		||||
 | 
			
		||||
    if let Some(start) = data.read_watchpoints.start_of_range_containing(address) {
 | 
			
		||||
        let watch = if data.write_watchpoints.contains(address) {
 | 
			
		||||
            VBWatchpointType::Access
 | 
			
		||||
        } else {
 | 
			
		||||
            VBWatchpointType::Read
 | 
			
		||||
        };
 | 
			
		||||
        data.stop_reason = Some(StopReason::Watchpoint(watch, start));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Don't stop here, the debugger expects us to break after the memory access.
 | 
			
		||||
    // We'll stop in on_execute instead.
 | 
			
		||||
    0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[no_mangle]
 | 
			
		||||
extern "C" fn on_write(
 | 
			
		||||
    sim: *mut VB,
 | 
			
		||||
    address: u32,
 | 
			
		||||
    _type: VBDataType,
 | 
			
		||||
    _value: *mut i32,
 | 
			
		||||
    _cycles: *mut u32,
 | 
			
		||||
    _cancel: *mut c_int,
 | 
			
		||||
) -> c_int {
 | 
			
		||||
    // SAFETY: the *mut VB owns its userdata.
 | 
			
		||||
    // There is no way for the userdata to be null or otherwise invalid.
 | 
			
		||||
    let data: &mut VBState = unsafe { &mut *vb_get_user_data(sim).cast() };
 | 
			
		||||
 | 
			
		||||
    if let Some(start) = data.write_watchpoints.start_of_range_containing(address) {
 | 
			
		||||
        let watch = if data.read_watchpoints.contains(address) {
 | 
			
		||||
            VBWatchpointType::Access
 | 
			
		||||
        } else {
 | 
			
		||||
            VBWatchpointType::Write
 | 
			
		||||
        };
 | 
			
		||||
        data.stop_reason = Some(StopReason::Watchpoint(watch, start));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Don't stop here, the debugger expects us to break after the memory access.
 | 
			
		||||
    // We'll stop in on_execute instead.
 | 
			
		||||
    0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const AUDIO_CAPACITY_SAMPLES: usize = 834 * 4;
 | 
			
		||||
const AUDIO_CAPACITY_FLOATS: usize = AUDIO_CAPACITY_SAMPLES * 2;
 | 
			
		||||
pub const EXPECTED_FRAME_SIZE: usize = 834 * 2;
 | 
			
		||||
 | 
			
		||||
struct VBState {
 | 
			
		||||
    frame_seen: bool,
 | 
			
		||||
    stop_reason: Option<StopReason>,
 | 
			
		||||
    step_from: Option<u32>,
 | 
			
		||||
    breakpoints: Vec<u32>,
 | 
			
		||||
    read_watchpoints: AddressSet,
 | 
			
		||||
    write_watchpoints: AddressSet,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl VBState {
 | 
			
		||||
    fn needs_execute_callback(&self) -> bool {
 | 
			
		||||
        self.step_from.is_some()
 | 
			
		||||
            || !self.breakpoints.is_empty()
 | 
			
		||||
            || !self.read_watchpoints.is_empty()
 | 
			
		||||
            || !self.write_watchpoints.is_empty()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub enum StopReason {
 | 
			
		||||
    Breakpoint,
 | 
			
		||||
    Watchpoint(VBWatchpointType, u32),
 | 
			
		||||
    Stepped,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[repr(transparent)]
 | 
			
		||||
| 
						 | 
				
			
			@ -136,9 +289,6 @@ pub struct Sim {
 | 
			
		|||
    sim: *mut VB,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SAFETY: the memory pointed to by sim is valid
 | 
			
		||||
unsafe impl Send for Sim {}
 | 
			
		||||
 | 
			
		||||
impl Sim {
 | 
			
		||||
    pub fn new() -> Self {
 | 
			
		||||
        // init the VB instance itself
 | 
			
		||||
| 
						 | 
				
			
			@ -149,12 +299,20 @@ impl Sim {
 | 
			
		|||
        unsafe { vb_init(sim) };
 | 
			
		||||
        // pseudohalt is disabled due to breaking red alarm
 | 
			
		||||
        unsafe { vb_set_option(sim, VBOption::PseudoHalt, 0) };
 | 
			
		||||
        unsafe { vb_set_keys(sim, VBKey::SGN.bits()) };
 | 
			
		||||
        unsafe { vb_reset(sim) };
 | 
			
		||||
 | 
			
		||||
        // set up userdata
 | 
			
		||||
        let state = VBState { frame_seen: false };
 | 
			
		||||
        let state = VBState {
 | 
			
		||||
            frame_seen: false,
 | 
			
		||||
            stop_reason: None,
 | 
			
		||||
            step_from: None,
 | 
			
		||||
            breakpoints: vec![],
 | 
			
		||||
            read_watchpoints: AddressSet::new(),
 | 
			
		||||
            write_watchpoints: AddressSet::new(),
 | 
			
		||||
        };
 | 
			
		||||
        unsafe { vb_set_user_data(sim, Box::into_raw(Box::new(state)).cast()) };
 | 
			
		||||
        unsafe { vb_set_frame_callback(sim, on_frame) };
 | 
			
		||||
        unsafe { vb_set_frame_callback(sim, Some(on_frame)) };
 | 
			
		||||
 | 
			
		||||
        // set up audio buffer
 | 
			
		||||
        let audio_buffer = vec![0.0f32; AUDIO_CAPACITY_FLOATS];
 | 
			
		||||
| 
						 | 
				
			
			@ -243,9 +401,7 @@ impl Sim {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn read_pixels(&mut self, buffers: &mut [u8]) -> bool {
 | 
			
		||||
        // SAFETY: the *mut VB owns its userdata.
 | 
			
		||||
        // There is no way for the userdata to be null or otherwise invalid.
 | 
			
		||||
        let data: &mut VBState = unsafe { &mut *vb_get_user_data(self.sim).cast() };
 | 
			
		||||
        let data = self.get_state();
 | 
			
		||||
        if !data.frame_seen {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -292,6 +448,167 @@ impl Sim {
 | 
			
		|||
    pub fn set_keys(&mut self, keys: VBKey) {
 | 
			
		||||
        unsafe { vb_set_keys(self.sim, keys.bits()) };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn read_register(&mut self, register: VBRegister) -> u32 {
 | 
			
		||||
        match register {
 | 
			
		||||
            VBRegister::Program(index) => unsafe {
 | 
			
		||||
                vb_get_program_register(self.sim, index) as u32
 | 
			
		||||
            },
 | 
			
		||||
            VBRegister::System(index) => unsafe { vb_get_system_register(self.sim, index) as u32 },
 | 
			
		||||
            VBRegister::PC => unsafe { vb_get_program_counter(self.sim) },
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn write_register(&mut self, register: VBRegister, value: u32) {
 | 
			
		||||
        match register {
 | 
			
		||||
            VBRegister::Program(index) => unsafe {
 | 
			
		||||
                vb_set_program_register(self.sim, index, value as i32);
 | 
			
		||||
            },
 | 
			
		||||
            VBRegister::System(index) => unsafe {
 | 
			
		||||
                vb_set_system_register(self.sim, index, value);
 | 
			
		||||
            },
 | 
			
		||||
            VBRegister::PC => unsafe {
 | 
			
		||||
                vb_set_program_counter(self.sim, value);
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn read_memory(&mut self, start: u32, length: usize, into: &mut Vec<u8>) {
 | 
			
		||||
        let mut address = start;
 | 
			
		||||
        for _ in 0..length {
 | 
			
		||||
            let byte = unsafe { vb_read(self.sim, address, VBDataType::U8) };
 | 
			
		||||
            into.push(byte as u8);
 | 
			
		||||
            address = address.wrapping_add(1);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn write_memory(&mut self, start: u32, buffer: &[u8]) {
 | 
			
		||||
        let mut address = start;
 | 
			
		||||
        for byte in buffer {
 | 
			
		||||
            unsafe { vb_write(self.sim, address, VBDataType::U8, *byte as i32) };
 | 
			
		||||
            address = address.wrapping_add(1);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn add_breakpoint(&mut self, address: u32) {
 | 
			
		||||
        let data = self.get_state();
 | 
			
		||||
        if let Err(index) = data.breakpoints.binary_search(&address) {
 | 
			
		||||
            data.breakpoints.insert(index, address);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        unsafe {
 | 
			
		||||
            vb_set_execute_callback(self.sim, Some(on_execute));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn remove_breakpoint(&mut self, address: u32) {
 | 
			
		||||
        let data = self.get_state();
 | 
			
		||||
        if let Ok(index) = data.breakpoints.binary_search(&address) {
 | 
			
		||||
            data.breakpoints.remove(index);
 | 
			
		||||
            if !data.needs_execute_callback() {
 | 
			
		||||
                unsafe { vb_set_execute_callback(self.sim, None) };
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn add_watchpoint(&mut self, address: u32, length: usize, watch: VBWatchpointType) {
 | 
			
		||||
        match watch {
 | 
			
		||||
            VBWatchpointType::Read => self.add_read_watchpoint(address, length),
 | 
			
		||||
            VBWatchpointType::Write => self.add_write_watchpoint(address, length),
 | 
			
		||||
            VBWatchpointType::Access => {
 | 
			
		||||
                self.add_read_watchpoint(address, length);
 | 
			
		||||
                self.add_write_watchpoint(address, length);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn add_read_watchpoint(&mut self, address: u32, length: usize) {
 | 
			
		||||
        let state = self.get_state();
 | 
			
		||||
        state.read_watchpoints.add(address, length);
 | 
			
		||||
        if !state.read_watchpoints.is_empty() {
 | 
			
		||||
            unsafe { vb_set_read_callback(self.sim, Some(on_read)) };
 | 
			
		||||
            unsafe { vb_set_execute_callback(self.sim, Some(on_execute)) };
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn add_write_watchpoint(&mut self, address: u32, length: usize) {
 | 
			
		||||
        let state = self.get_state();
 | 
			
		||||
        state.write_watchpoints.add(address, length);
 | 
			
		||||
        if !state.write_watchpoints.is_empty() {
 | 
			
		||||
            unsafe { vb_set_write_callback(self.sim, Some(on_write)) };
 | 
			
		||||
            unsafe { vb_set_execute_callback(self.sim, Some(on_execute)) };
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn remove_watchpoint(&mut self, address: u32, length: usize, watch: VBWatchpointType) {
 | 
			
		||||
        match watch {
 | 
			
		||||
            VBWatchpointType::Read => self.remove_read_watchpoint(address, length),
 | 
			
		||||
            VBWatchpointType::Write => self.remove_write_watchpoint(address, length),
 | 
			
		||||
            VBWatchpointType::Access => {
 | 
			
		||||
                self.remove_read_watchpoint(address, length);
 | 
			
		||||
                self.remove_write_watchpoint(address, length);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn remove_read_watchpoint(&mut self, address: u32, length: usize) {
 | 
			
		||||
        let state = self.get_state();
 | 
			
		||||
        state.read_watchpoints.remove(address, length);
 | 
			
		||||
        let needs_execute = state.needs_execute_callback();
 | 
			
		||||
        if state.read_watchpoints.is_empty() {
 | 
			
		||||
            unsafe { vb_set_read_callback(self.sim, None) };
 | 
			
		||||
            if !needs_execute {
 | 
			
		||||
                unsafe { vb_set_execute_callback(self.sim, None) };
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn remove_write_watchpoint(&mut self, address: u32, length: usize) {
 | 
			
		||||
        let state = self.get_state();
 | 
			
		||||
        state.write_watchpoints.remove(address, length);
 | 
			
		||||
        let needs_execute = state.needs_execute_callback();
 | 
			
		||||
        if state.write_watchpoints.is_empty() {
 | 
			
		||||
            unsafe { vb_set_write_callback(self.sim, None) };
 | 
			
		||||
            if !needs_execute {
 | 
			
		||||
                unsafe { vb_set_execute_callback(self.sim, None) };
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn step(&mut self) {
 | 
			
		||||
        let current_pc = unsafe { vb_get_program_counter(self.sim) };
 | 
			
		||||
        let data = self.get_state();
 | 
			
		||||
        data.step_from = Some(current_pc);
 | 
			
		||||
        unsafe {
 | 
			
		||||
            vb_set_execute_callback(self.sim, Some(on_execute));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn clear_debug_state(&mut self) {
 | 
			
		||||
        let data = self.get_state();
 | 
			
		||||
        data.step_from = None;
 | 
			
		||||
        data.breakpoints.clear();
 | 
			
		||||
        data.read_watchpoints.clear();
 | 
			
		||||
        data.write_watchpoints.clear();
 | 
			
		||||
        unsafe { vb_set_read_callback(self.sim, None) };
 | 
			
		||||
        unsafe { vb_set_write_callback(self.sim, None) };
 | 
			
		||||
        unsafe { vb_set_execute_callback(self.sim, None) };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn stop_reason(&mut self) -> Option<StopReason> {
 | 
			
		||||
        let data = self.get_state();
 | 
			
		||||
        let reason = data.stop_reason.take();
 | 
			
		||||
        if !data.needs_execute_callback() {
 | 
			
		||||
            unsafe { vb_set_execute_callback(self.sim, None) };
 | 
			
		||||
        }
 | 
			
		||||
        reason
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn get_state(&mut self) -> &mut VBState {
 | 
			
		||||
        // SAFETY: the *mut VB owns its userdata.
 | 
			
		||||
        // There is no way for the userdata to be null or otherwise invalid.
 | 
			
		||||
        unsafe { &mut *vb_get_user_data(self.sim).cast() }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Drop for Sim {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,560 @@
 | 
			
		|||
use anyhow::{bail, Result};
 | 
			
		||||
use registers::REGISTERS;
 | 
			
		||||
use request::{Request, RequestKind, RequestSource};
 | 
			
		||||
use response::Response;
 | 
			
		||||
use std::{
 | 
			
		||||
    sync::{Arc, Mutex},
 | 
			
		||||
    thread,
 | 
			
		||||
};
 | 
			
		||||
use tokio::{
 | 
			
		||||
    io::{AsyncWriteExt as _, BufReader},
 | 
			
		||||
    net::{TcpListener, TcpStream},
 | 
			
		||||
    pin, select,
 | 
			
		||||
    sync::{mpsc, oneshot},
 | 
			
		||||
};
 | 
			
		||||
use tracing::{debug, enabled, error, info, Level};
 | 
			
		||||
 | 
			
		||||
use crate::emulator::{
 | 
			
		||||
    DebugEvent, DebugStopReason, EmulatorClient, EmulatorCommand, SimId, VBWatchpointType,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
mod registers;
 | 
			
		||||
mod request;
 | 
			
		||||
mod response;
 | 
			
		||||
 | 
			
		||||
pub struct GdbServer {
 | 
			
		||||
    sim_id: SimId,
 | 
			
		||||
    client: EmulatorClient,
 | 
			
		||||
    status: Arc<Mutex<GdbServerStatus>>,
 | 
			
		||||
    killer: Option<oneshot::Sender<()>>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl GdbServer {
 | 
			
		||||
    pub fn new(sim_id: SimId, client: EmulatorClient) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            sim_id,
 | 
			
		||||
            client,
 | 
			
		||||
            status: Arc::new(Mutex::new(GdbServerStatus::Stopped)),
 | 
			
		||||
            killer: None,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn status(&self) -> GdbServerStatus {
 | 
			
		||||
        self.status.lock().unwrap().clone()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn start(&mut self, port: u16) {
 | 
			
		||||
        *self.status.lock().unwrap() = GdbServerStatus::Connecting;
 | 
			
		||||
        let sim_id = self.sim_id;
 | 
			
		||||
        let client = self.client.clone();
 | 
			
		||||
        let status = self.status.clone();
 | 
			
		||||
        let (tx, rx) = oneshot::channel();
 | 
			
		||||
        self.killer = Some(tx);
 | 
			
		||||
        thread::spawn(move || {
 | 
			
		||||
            tokio::runtime::Builder::new_current_thread()
 | 
			
		||||
                .enable_all()
 | 
			
		||||
                .build()
 | 
			
		||||
                .unwrap()
 | 
			
		||||
                .block_on(async move {
 | 
			
		||||
                    select! {
 | 
			
		||||
                        _ = run_server(sim_id, client.clone(), port, &status) => {}
 | 
			
		||||
                        _ = rx => {
 | 
			
		||||
                            client.send_command(EmulatorCommand::StopDebugging(sim_id));
 | 
			
		||||
                            *status.lock().unwrap() = GdbServerStatus::Stopped;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                })
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn stop(&mut self) {
 | 
			
		||||
        if let Some(killer) = self.killer.take() {
 | 
			
		||||
            let _ = killer.send(());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Drop for GdbServer {
 | 
			
		||||
    fn drop(&mut self) {
 | 
			
		||||
        self.stop();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn run_server(
 | 
			
		||||
    sim_id: SimId,
 | 
			
		||||
    client: EmulatorClient,
 | 
			
		||||
    port: u16,
 | 
			
		||||
    status: &Mutex<GdbServerStatus>,
 | 
			
		||||
) {
 | 
			
		||||
    let (debug_sink, mut debug_source) = mpsc::unbounded_channel();
 | 
			
		||||
    client.send_command(EmulatorCommand::StartDebugging(sim_id, debug_sink));
 | 
			
		||||
 | 
			
		||||
    info!("Connecting to debugger on port {port}...");
 | 
			
		||||
    let connect_future = try_connect(port, status);
 | 
			
		||||
    pin!(connect_future);
 | 
			
		||||
 | 
			
		||||
    let stream = loop {
 | 
			
		||||
        select! {
 | 
			
		||||
            stream = &mut connect_future => {
 | 
			
		||||
                if let Some(stream) = stream {
 | 
			
		||||
                    break stream;
 | 
			
		||||
                } else {
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            event = debug_source.recv() => {
 | 
			
		||||
                if event.is_none() {
 | 
			
		||||
                    // The sim has stopped (or was never started)
 | 
			
		||||
                    *status.lock().unwrap() = GdbServerStatus::Stopped;
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
    info!("Connected!");
 | 
			
		||||
    let mut connection = GdbConnection::new(sim_id, client);
 | 
			
		||||
    match connection.run(stream, debug_source).await {
 | 
			
		||||
        Ok(()) => {
 | 
			
		||||
            info!("Finished debugging.");
 | 
			
		||||
            *status.lock().unwrap() = GdbServerStatus::Stopped;
 | 
			
		||||
        }
 | 
			
		||||
        Err(error) => {
 | 
			
		||||
            error!(%error, "Error from debugger.");
 | 
			
		||||
            *status.lock().unwrap() = GdbServerStatus::Error(error.to_string());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn try_connect(port: u16, status: &Mutex<GdbServerStatus>) -> Option<TcpStream> {
 | 
			
		||||
    *status.lock().unwrap() = GdbServerStatus::Connecting;
 | 
			
		||||
    let listener = match TcpListener::bind(("127.0.0.1", port)).await {
 | 
			
		||||
        Ok(l) => l,
 | 
			
		||||
        Err(err) => {
 | 
			
		||||
            error!(%err, "Could not open port.");
 | 
			
		||||
            *status.lock().unwrap() = GdbServerStatus::Error(err.to_string());
 | 
			
		||||
            return None;
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
    match listener.accept().await {
 | 
			
		||||
        Ok((stream, _)) => {
 | 
			
		||||
            *status.lock().unwrap() = GdbServerStatus::Running;
 | 
			
		||||
            Some(stream)
 | 
			
		||||
        }
 | 
			
		||||
        Err(err) => {
 | 
			
		||||
            error!(%err, "Could not connect to debugger.");
 | 
			
		||||
            *status.lock().unwrap() = GdbServerStatus::Error(err.to_string());
 | 
			
		||||
            None
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Clone)]
 | 
			
		||||
pub enum GdbServerStatus {
 | 
			
		||||
    Stopped,
 | 
			
		||||
    Connecting,
 | 
			
		||||
    Running,
 | 
			
		||||
    Error(String),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl GdbServerStatus {
 | 
			
		||||
    pub fn running(&self) -> bool {
 | 
			
		||||
        matches!(self, Self::Connecting | Self::Running)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct GdbConnection {
 | 
			
		||||
    sim_id: SimId,
 | 
			
		||||
    client: EmulatorClient,
 | 
			
		||||
    ack_messages: bool,
 | 
			
		||||
    stop_reason: Option<DebugStopReason>,
 | 
			
		||||
    response_buf: Option<Vec<u8>>,
 | 
			
		||||
    memory_buf: Option<Vec<u8>>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl GdbConnection {
 | 
			
		||||
    fn new(sim_id: SimId, client: EmulatorClient) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            sim_id,
 | 
			
		||||
            client,
 | 
			
		||||
            ack_messages: true,
 | 
			
		||||
            stop_reason: None,
 | 
			
		||||
            response_buf: None,
 | 
			
		||||
            memory_buf: None,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    async fn run(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        stream: TcpStream,
 | 
			
		||||
        mut debug_source: mpsc::UnboundedReceiver<DebugEvent>,
 | 
			
		||||
    ) -> Result<()> {
 | 
			
		||||
        let (rx, mut tx) = stream.into_split();
 | 
			
		||||
        let mut request_source = RequestSource::new(BufReader::new(rx));
 | 
			
		||||
        loop {
 | 
			
		||||
            let response = select! {
 | 
			
		||||
                maybe_event = debug_source.recv() => {
 | 
			
		||||
                    let Some(event) = maybe_event else {
 | 
			
		||||
                        // debugger has stopped running
 | 
			
		||||
                        break;
 | 
			
		||||
                    };
 | 
			
		||||
                    self.handle_event(event)
 | 
			
		||||
                }
 | 
			
		||||
                maybe_request = request_source.recv() => {
 | 
			
		||||
                    let req = maybe_request?;
 | 
			
		||||
                    self.handle_request(req)?
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
            if let Some(res) = response {
 | 
			
		||||
                let buffer = res.finish();
 | 
			
		||||
                if enabled!(Level::DEBUG) {
 | 
			
		||||
                    match std::str::from_utf8(&buffer) {
 | 
			
		||||
                        Ok(text) => debug!("response: {text}"),
 | 
			
		||||
                        Err(_) => debug!("response: {buffer:02x?}"),
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                tx.write_all(&buffer).await?;
 | 
			
		||||
                self.response_buf = Some(buffer);
 | 
			
		||||
                tx.flush().await?;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn handle_event(&mut self, event: DebugEvent) -> Option<Response> {
 | 
			
		||||
        let res = match event {
 | 
			
		||||
            DebugEvent::Stopped(reason) => {
 | 
			
		||||
                if self.stop_reason.is_some_and(|r| r == reason) {
 | 
			
		||||
                    return None;
 | 
			
		||||
                }
 | 
			
		||||
                self.stop_reason = Some(reason);
 | 
			
		||||
                self.response()
 | 
			
		||||
                    .write_str(&debug_stop_reason_string(self.stop_reason))
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        Some(res)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn handle_request(&mut self, mut req: Request<'_>) -> Result<Option<Response>> {
 | 
			
		||||
        debug!("received {:02x?}", req);
 | 
			
		||||
 | 
			
		||||
        if req.kind == RequestKind::Signal {
 | 
			
		||||
            self.client
 | 
			
		||||
                .send_command(EmulatorCommand::DebugInterrupt(self.sim_id));
 | 
			
		||||
            return Ok(None); // we'll send a message when the emulator reports it has stopped
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let res = if req.match_str("QStartNoAckMode") {
 | 
			
		||||
            let res = self.response().write_str("OK");
 | 
			
		||||
            self.ack_messages = false;
 | 
			
		||||
            res
 | 
			
		||||
        } else if req.match_str("qSupported:") {
 | 
			
		||||
            self.response()
 | 
			
		||||
                .write_str("multiprocess+;swbreak+;vContSupported+;PacketSize=10000;SupportedWatchpointTypes=x86_64,aarch64-bas,aarch64-mask")
 | 
			
		||||
        } else if req
 | 
			
		||||
            .match_some_str([
 | 
			
		||||
                "QThreadSuffixSupported",
 | 
			
		||||
                "QListThreadsInStopReply",
 | 
			
		||||
                "QEnableErrorStrings",
 | 
			
		||||
            ])
 | 
			
		||||
            .is_some()
 | 
			
		||||
        {
 | 
			
		||||
            self.response().write_str("OK")
 | 
			
		||||
        } else if req.match_str("qHostInfo") {
 | 
			
		||||
            self.response().write_str(&format!(
 | 
			
		||||
                "triple:{};endian:little;ptrsize:4;",
 | 
			
		||||
                hex::encode("v810-unknown-vb")
 | 
			
		||||
            ))
 | 
			
		||||
        } else if req.match_str("qProcessInfo") {
 | 
			
		||||
            self.response().write_str(&format!(
 | 
			
		||||
                "pid:1;triple:{};endian:little;ptrsize:4;",
 | 
			
		||||
                hex::encode("v810-unknown-vb")
 | 
			
		||||
            ))
 | 
			
		||||
        } else if req.match_str("qRegisterInfo") {
 | 
			
		||||
            let mut get_reg_info = || {
 | 
			
		||||
                let register = req.match_hex::<usize>()?;
 | 
			
		||||
                REGISTERS.get(register)
 | 
			
		||||
            };
 | 
			
		||||
            if let Some(reg_info) = get_reg_info() {
 | 
			
		||||
                self.response().write_str(®_info.to_description())
 | 
			
		||||
            } else {
 | 
			
		||||
                self.response()
 | 
			
		||||
            }
 | 
			
		||||
        } else if req.match_str("vCont?") {
 | 
			
		||||
            self.response().write_str("vCont;c;s;")
 | 
			
		||||
        } else if req.match_str("qC") {
 | 
			
		||||
            // The v810 has no threads, so report that the "current thread" is 1.
 | 
			
		||||
            self.response().write_str("QCp1.t1")
 | 
			
		||||
        } else if req.match_str("qfThreadInfo") {
 | 
			
		||||
            self.response().write_str("mp1.t1")
 | 
			
		||||
        } else if req.match_str("qsThreadInfo") {
 | 
			
		||||
            self.response().write_str("l")
 | 
			
		||||
        } else if req.match_str("k") {
 | 
			
		||||
            bail!("debug process was killed");
 | 
			
		||||
        } else if req.match_str("?") {
 | 
			
		||||
            self.response()
 | 
			
		||||
                .write_str(&debug_stop_reason_string(self.stop_reason))
 | 
			
		||||
        } else if req.match_some_str(["c", "vCont;c:"]).is_some() {
 | 
			
		||||
            self.client
 | 
			
		||||
                .send_command(EmulatorCommand::DebugContinue(self.sim_id));
 | 
			
		||||
            self.stop_reason = None;
 | 
			
		||||
            // Don't send a response until we hit a breakpoint or get interrupted
 | 
			
		||||
            return Ok(None);
 | 
			
		||||
        } else if req.match_some_str(["s", "vCont;s:"]).is_some() {
 | 
			
		||||
            self.client
 | 
			
		||||
                .send_command(EmulatorCommand::DebugStep(self.sim_id));
 | 
			
		||||
            self.stop_reason = None;
 | 
			
		||||
            // Don't send a response until we hit a breakpoint or get interrupted
 | 
			
		||||
            return Ok(None);
 | 
			
		||||
        } else if req.match_str("p") {
 | 
			
		||||
            let mut read_register = || {
 | 
			
		||||
                let register_index = req.match_hex::<usize>()?;
 | 
			
		||||
                let register = REGISTERS.get(register_index)?.to_vb_register();
 | 
			
		||||
                let (tx, rx) = ::oneshot::channel();
 | 
			
		||||
                self.client
 | 
			
		||||
                    .send_command(EmulatorCommand::ReadRegister(self.sim_id, register, tx));
 | 
			
		||||
                rx.recv().ok()
 | 
			
		||||
            };
 | 
			
		||||
            if let Some(value) = read_register() {
 | 
			
		||||
                self.response().write_hex(value)
 | 
			
		||||
            } else {
 | 
			
		||||
                self.response()
 | 
			
		||||
            }
 | 
			
		||||
        } else if req.match_str("P") {
 | 
			
		||||
            let mut write_register = || {
 | 
			
		||||
                let register_index = req.match_hex::<usize>()?;
 | 
			
		||||
                let register = REGISTERS.get(register_index)?.to_vb_register();
 | 
			
		||||
                if !req.match_str("=") {
 | 
			
		||||
                    return None;
 | 
			
		||||
                }
 | 
			
		||||
                let value = {
 | 
			
		||||
                    let mut buffer = [0; 4];
 | 
			
		||||
                    if !req.match_hex_bytes(&mut buffer) {
 | 
			
		||||
                        return None;
 | 
			
		||||
                    }
 | 
			
		||||
                    u32::from_le_bytes(buffer)
 | 
			
		||||
                };
 | 
			
		||||
                self.client.send_command(EmulatorCommand::WriteRegister(
 | 
			
		||||
                    self.sim_id,
 | 
			
		||||
                    register,
 | 
			
		||||
                    value,
 | 
			
		||||
                ));
 | 
			
		||||
                Some(())
 | 
			
		||||
            };
 | 
			
		||||
            if let Some(()) = write_register() {
 | 
			
		||||
                self.response().write_str("OK")
 | 
			
		||||
            } else {
 | 
			
		||||
                self.response()
 | 
			
		||||
            }
 | 
			
		||||
        } else if let Some(op) = req.match_some_str(["m", "x"]) {
 | 
			
		||||
            let mut read_memory = || {
 | 
			
		||||
                let (start, length) = parse_memory_range(&mut req)?;
 | 
			
		||||
                let mut buf = self.memory_buf.take().unwrap_or_default();
 | 
			
		||||
                buf.clear();
 | 
			
		||||
                if length == 0 {
 | 
			
		||||
                    return Some(buf);
 | 
			
		||||
                }
 | 
			
		||||
                let (tx, rx) = ::oneshot::channel();
 | 
			
		||||
                self.client.send_command(EmulatorCommand::ReadMemory(
 | 
			
		||||
                    self.sim_id,
 | 
			
		||||
                    start,
 | 
			
		||||
                    length,
 | 
			
		||||
                    buf,
 | 
			
		||||
                    tx,
 | 
			
		||||
                ));
 | 
			
		||||
                rx.recv().ok()
 | 
			
		||||
            };
 | 
			
		||||
            if let Some(memory) = read_memory() {
 | 
			
		||||
                let mut res = self.response();
 | 
			
		||||
                if memory.is_empty() {
 | 
			
		||||
                    res = res.write_str("OK");
 | 
			
		||||
                } else if op == "m" {
 | 
			
		||||
                    // send the hex-encoded byte stream
 | 
			
		||||
                    for byte in &memory {
 | 
			
		||||
                        res = res.write_hex(*byte);
 | 
			
		||||
                    }
 | 
			
		||||
                } else {
 | 
			
		||||
                    // send the raw byte stream
 | 
			
		||||
                    for byte in &memory {
 | 
			
		||||
                        res = res.write_byte(*byte);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                self.memory_buf = Some(memory);
 | 
			
		||||
                res
 | 
			
		||||
            } else {
 | 
			
		||||
                self.response()
 | 
			
		||||
            }
 | 
			
		||||
        } else if let Some(op) = req.match_some_str(["M", "X"]) {
 | 
			
		||||
            let mut write_memory = || {
 | 
			
		||||
                let (start, length) = parse_memory_range(&mut req)?;
 | 
			
		||||
                if length == 0 {
 | 
			
		||||
                    return Some(());
 | 
			
		||||
                }
 | 
			
		||||
                if !req.match_str(":") {
 | 
			
		||||
                    return None;
 | 
			
		||||
                }
 | 
			
		||||
                let mut buf = self.memory_buf.take().unwrap_or_default();
 | 
			
		||||
                buf.resize(length, 0);
 | 
			
		||||
                let successful_read = if op == "M" {
 | 
			
		||||
                    req.match_hex_bytes(&mut buf)
 | 
			
		||||
                } else {
 | 
			
		||||
                    req.match_bytes(&mut buf)
 | 
			
		||||
                };
 | 
			
		||||
                if !successful_read {
 | 
			
		||||
                    self.memory_buf = Some(buf);
 | 
			
		||||
                    return None;
 | 
			
		||||
                }
 | 
			
		||||
                let (tx, rx) = ::oneshot::channel();
 | 
			
		||||
                self.client
 | 
			
		||||
                    .send_command(EmulatorCommand::WriteMemory(self.sim_id, start, buf, tx));
 | 
			
		||||
                let buf = rx.recv().ok()?;
 | 
			
		||||
                self.memory_buf = Some(buf);
 | 
			
		||||
                Some(())
 | 
			
		||||
            };
 | 
			
		||||
            if write_memory().is_some() {
 | 
			
		||||
                self.response().write_str("OK")
 | 
			
		||||
            } else {
 | 
			
		||||
                self.response()
 | 
			
		||||
            }
 | 
			
		||||
        } else if req.match_str("Z") {
 | 
			
		||||
            let mut parse_request = || {
 | 
			
		||||
                let type_ = req.match_hex::<u8>()?;
 | 
			
		||||
                if !req.match_str(",") {
 | 
			
		||||
                    return None;
 | 
			
		||||
                }
 | 
			
		||||
                let address = req.match_hex()?;
 | 
			
		||||
                if type_ == 0 || type_ == 1 {
 | 
			
		||||
                    return Some(EmulatorCommand::AddBreakpoint(self.sim_id, address));
 | 
			
		||||
                }
 | 
			
		||||
                if !req.match_str(",") {
 | 
			
		||||
                    return None;
 | 
			
		||||
                }
 | 
			
		||||
                let length = req.match_hex()?;
 | 
			
		||||
                let watch = match type_ {
 | 
			
		||||
                    2 => VBWatchpointType::Write,
 | 
			
		||||
                    3 => VBWatchpointType::Read,
 | 
			
		||||
                    4 => VBWatchpointType::Access,
 | 
			
		||||
                    _ => return None,
 | 
			
		||||
                };
 | 
			
		||||
                Some(EmulatorCommand::AddWatchpoint(
 | 
			
		||||
                    self.sim_id,
 | 
			
		||||
                    address,
 | 
			
		||||
                    length,
 | 
			
		||||
                    watch,
 | 
			
		||||
                ))
 | 
			
		||||
            };
 | 
			
		||||
            if let Some(command) = parse_request() {
 | 
			
		||||
                self.client.send_command(command);
 | 
			
		||||
                self.response().write_str("OK")
 | 
			
		||||
            } else {
 | 
			
		||||
                self.response()
 | 
			
		||||
            }
 | 
			
		||||
        } else if req.match_str("z") {
 | 
			
		||||
            let mut parse_request = || {
 | 
			
		||||
                let type_ = req.match_hex::<u8>()?;
 | 
			
		||||
                if !req.match_str(",") {
 | 
			
		||||
                    return None;
 | 
			
		||||
                }
 | 
			
		||||
                let address = req.match_hex()?;
 | 
			
		||||
                if type_ == 0 || type_ == 1 {
 | 
			
		||||
                    return Some(EmulatorCommand::RemoveBreakpoint(self.sim_id, address));
 | 
			
		||||
                }
 | 
			
		||||
                if !req.match_str(",") {
 | 
			
		||||
                    return None;
 | 
			
		||||
                }
 | 
			
		||||
                let length = req.match_hex()?;
 | 
			
		||||
                let watch = match type_ {
 | 
			
		||||
                    2 => VBWatchpointType::Write,
 | 
			
		||||
                    3 => VBWatchpointType::Read,
 | 
			
		||||
                    4 => VBWatchpointType::Access,
 | 
			
		||||
                    _ => return None,
 | 
			
		||||
                };
 | 
			
		||||
                Some(EmulatorCommand::RemoveWatchpoint(
 | 
			
		||||
                    self.sim_id,
 | 
			
		||||
                    address,
 | 
			
		||||
                    length,
 | 
			
		||||
                    watch,
 | 
			
		||||
                ))
 | 
			
		||||
            };
 | 
			
		||||
            if let Some(command) = parse_request() {
 | 
			
		||||
                self.client.send_command(command);
 | 
			
		||||
                self.response().write_str("OK")
 | 
			
		||||
            } else {
 | 
			
		||||
                self.response()
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            // unrecognized command
 | 
			
		||||
            self.response()
 | 
			
		||||
        };
 | 
			
		||||
        Ok(Some(res))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn response(&mut self) -> Response {
 | 
			
		||||
        Response::new(
 | 
			
		||||
            self.response_buf.take().unwrap_or_default(),
 | 
			
		||||
            self.ack_messages,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Drop for GdbConnection {
 | 
			
		||||
    fn drop(&mut self) {
 | 
			
		||||
        self.client
 | 
			
		||||
            .send_command(EmulatorCommand::StopDebugging(self.sim_id));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// parse a memory range into a start and a length.
 | 
			
		||||
fn parse_memory_range(req: &mut Request<'_>) -> Option<(u32, usize)> {
 | 
			
		||||
    let start = req.match_hex::<u64>()?;
 | 
			
		||||
    if !req.match_str(",") {
 | 
			
		||||
        return None;
 | 
			
		||||
    };
 | 
			
		||||
    let length = req.match_hex::<usize>()?;
 | 
			
		||||
    let Ok(start) = u32::try_from(start) else {
 | 
			
		||||
        // The v810 has a 32-bit address space.
 | 
			
		||||
        // Addresses wrap within that space, but we don't need to implement that for 64-bit addresses.
 | 
			
		||||
        // Just refuse to return any info for addresses above 0xffffffff.
 | 
			
		||||
        return Some((0, 0));
 | 
			
		||||
    };
 | 
			
		||||
    let length = length.min((u32::MAX - start) as usize + 1);
 | 
			
		||||
    Some((start, length))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn debug_stop_reason_string(reason: Option<DebugStopReason>) -> String {
 | 
			
		||||
    let mut result = String::new();
 | 
			
		||||
    result += if reason.is_some_and(|r| r != DebugStopReason::Paused) {
 | 
			
		||||
        "T05;"
 | 
			
		||||
    } else {
 | 
			
		||||
        "T00;"
 | 
			
		||||
    };
 | 
			
		||||
    if let Some(DebugStopReason::Breakpoint) = reason {
 | 
			
		||||
        result += "swbreak;";
 | 
			
		||||
    }
 | 
			
		||||
    if let Some(DebugStopReason::Watchpoint(watch, address)) = reason {
 | 
			
		||||
        result += match watch {
 | 
			
		||||
            VBWatchpointType::Write => "watch:",
 | 
			
		||||
            VBWatchpointType::Read => "rwatch:",
 | 
			
		||||
            VBWatchpointType::Access => "awatch:",
 | 
			
		||||
        };
 | 
			
		||||
        result += &format!("{address:08x};");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    result += "thread:p1.t1;threads:p1.t1;";
 | 
			
		||||
 | 
			
		||||
    if let Some(reason) = reason {
 | 
			
		||||
        result += "reason:";
 | 
			
		||||
        result += match reason {
 | 
			
		||||
            DebugStopReason::Trace => "trace;",
 | 
			
		||||
            DebugStopReason::Breakpoint => "breakpoint;",
 | 
			
		||||
            DebugStopReason::Watchpoint(_, _) => "watchpoint;",
 | 
			
		||||
            DebugStopReason::Paused => "trap;",
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if let Some(DebugStopReason::Watchpoint(_, address)) = reason {
 | 
			
		||||
        result += "description:";
 | 
			
		||||
        result += &hex::encode(address.to_string());
 | 
			
		||||
        result += ";";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    result
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,130 @@
 | 
			
		|||
use crate::emulator::VBRegister;
 | 
			
		||||
 | 
			
		||||
pub struct RegisterInfo {
 | 
			
		||||
    dwarf: u32,
 | 
			
		||||
    name: &'static str,
 | 
			
		||||
    set: &'static str,
 | 
			
		||||
    alt_name: Option<&'static str>,
 | 
			
		||||
    generic: Option<&'static str>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl RegisterInfo {
 | 
			
		||||
    pub fn to_description(&self) -> String {
 | 
			
		||||
        let mut string = format!("name:{}", self.name);
 | 
			
		||||
        if let Some(alt) = self.alt_name {
 | 
			
		||||
            string.push_str(&format!(";alt-name:{}", alt));
 | 
			
		||||
        }
 | 
			
		||||
        string.push_str(&format!(
 | 
			
		||||
            ";bitsize:32;offset:{};encoding:uint;format:hex;set:{};dwarf:{}",
 | 
			
		||||
            self.dwarf * 4,
 | 
			
		||||
            self.set,
 | 
			
		||||
            self.dwarf
 | 
			
		||||
        ));
 | 
			
		||||
        if let Some(generic) = self.generic {
 | 
			
		||||
            string.push_str(&format!(";generic:{}", generic));
 | 
			
		||||
        }
 | 
			
		||||
        string
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn to_vb_register(&self) -> VBRegister {
 | 
			
		||||
        match self.dwarf {
 | 
			
		||||
            0..32 => VBRegister::Program(self.dwarf),
 | 
			
		||||
            32..40 => VBRegister::System(self.dwarf - 32),
 | 
			
		||||
            40..42 => VBRegister::System(self.dwarf - 16),
 | 
			
		||||
            42..45 => VBRegister::System(self.dwarf - 13),
 | 
			
		||||
            45 => VBRegister::PC,
 | 
			
		||||
            other => panic!("unexpected DWARF register {other}"),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
macro_rules! register {
 | 
			
		||||
    ($set:expr, $dwarf:expr, $name:expr) => {
 | 
			
		||||
        RegisterInfo {
 | 
			
		||||
            dwarf: $dwarf,
 | 
			
		||||
            name: $name,
 | 
			
		||||
            set: $set,
 | 
			
		||||
            alt_name: None,
 | 
			
		||||
            generic: None,
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
    ($set:expr, $dwarf:expr, $name:expr, alt: $alt:expr) => {
 | 
			
		||||
        RegisterInfo {
 | 
			
		||||
            dwarf: $dwarf,
 | 
			
		||||
            name: $name,
 | 
			
		||||
            set: $set,
 | 
			
		||||
            alt_name: Some($alt),
 | 
			
		||||
            generic: None,
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
    ($set:expr, $dwarf:expr, $name:expr, generic: $generic:expr) => {
 | 
			
		||||
        RegisterInfo {
 | 
			
		||||
            dwarf: $dwarf,
 | 
			
		||||
            name: $name,
 | 
			
		||||
            set: $set,
 | 
			
		||||
            alt_name: None,
 | 
			
		||||
            generic: Some($generic),
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
    ($set:expr, $dwarf:expr, $name:expr, alt: $alt:expr, generic: $generic:expr) => {
 | 
			
		||||
        RegisterInfo {
 | 
			
		||||
            dwarf: $dwarf,
 | 
			
		||||
            name: $name,
 | 
			
		||||
            set: $set,
 | 
			
		||||
            alt_name: Some($alt),
 | 
			
		||||
            generic: Some($generic),
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const GENERAL: &str = "General Purpose Registers";
 | 
			
		||||
const SPECIAL: &str = "Special Registers";
 | 
			
		||||
 | 
			
		||||
pub const REGISTERS: [RegisterInfo; 46] = [
 | 
			
		||||
    register!(GENERAL, 0, "r0"),
 | 
			
		||||
    register!(GENERAL, 1, "r1"),
 | 
			
		||||
    register!(GENERAL, 2, "fp", alt: "r2", generic: "fp"),
 | 
			
		||||
    register!(GENERAL, 3, "sp", alt: "r3", generic: "sp"),
 | 
			
		||||
    register!(GENERAL, 4, "gp", alt: "r4"),
 | 
			
		||||
    register!(GENERAL, 5, "tp", alt: "r5"),
 | 
			
		||||
    register!(GENERAL, 6, "r6", generic: "arg1"),
 | 
			
		||||
    register!(GENERAL, 7, "r7", generic: "arg2"),
 | 
			
		||||
    register!(GENERAL, 8, "r8", generic: "arg3"),
 | 
			
		||||
    register!(GENERAL, 9, "r9", generic: "arg4"),
 | 
			
		||||
    register!(GENERAL, 10, "r10"),
 | 
			
		||||
    register!(GENERAL, 11, "r11"),
 | 
			
		||||
    register!(GENERAL, 12, "r12"),
 | 
			
		||||
    register!(GENERAL, 13, "r13"),
 | 
			
		||||
    register!(GENERAL, 14, "r14"),
 | 
			
		||||
    register!(GENERAL, 15, "r15"),
 | 
			
		||||
    register!(GENERAL, 16, "r16"),
 | 
			
		||||
    register!(GENERAL, 17, "r17"),
 | 
			
		||||
    register!(GENERAL, 18, "r18"),
 | 
			
		||||
    register!(GENERAL, 19, "r19"),
 | 
			
		||||
    register!(GENERAL, 20, "r20"),
 | 
			
		||||
    register!(GENERAL, 21, "r21"),
 | 
			
		||||
    register!(GENERAL, 22, "r22"),
 | 
			
		||||
    register!(GENERAL, 23, "r23"),
 | 
			
		||||
    register!(GENERAL, 24, "r24"),
 | 
			
		||||
    register!(GENERAL, 25, "r25"),
 | 
			
		||||
    register!(GENERAL, 26, "r26"),
 | 
			
		||||
    register!(GENERAL, 27, "r27"),
 | 
			
		||||
    register!(GENERAL, 28, "r28"),
 | 
			
		||||
    register!(GENERAL, 29, "r29"),
 | 
			
		||||
    register!(GENERAL, 30, "r30"),
 | 
			
		||||
    register!(GENERAL, 31, "lp", alt: "r31", generic: "ra"),
 | 
			
		||||
    register!(SPECIAL, 32, "eipc", alt: "sr0"),
 | 
			
		||||
    register!(SPECIAL, 33, "eipsw", alt: "sr1"),
 | 
			
		||||
    register!(SPECIAL, 34, "fepc", alt: "sr2"),
 | 
			
		||||
    register!(SPECIAL, 35, "fepsw", alt: "sr3"),
 | 
			
		||||
    register!(SPECIAL, 36, "ecr", alt: "sr4"),
 | 
			
		||||
    register!(SPECIAL, 37, "psw", alt: "sr5", generic: "flags"),
 | 
			
		||||
    register!(SPECIAL, 38, "pir", alt: "sr6"),
 | 
			
		||||
    register!(SPECIAL, 39, "tkcw", alt: "sr7"),
 | 
			
		||||
    register!(SPECIAL, 40, "chcw", alt: "sr24"),
 | 
			
		||||
    register!(SPECIAL, 41, "adtre", alt: "sr25"),
 | 
			
		||||
    register!(SPECIAL, 42, "sr29"),
 | 
			
		||||
    register!(SPECIAL, 43, "sr30"),
 | 
			
		||||
    register!(SPECIAL, 44, "sr31"),
 | 
			
		||||
    register!(SPECIAL, 45, "pc", generic: "pc"),
 | 
			
		||||
];
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,202 @@
 | 
			
		|||
use anyhow::{bail, Result};
 | 
			
		||||
use atoi::FromRadix16;
 | 
			
		||||
use tokio::io::{AsyncRead, AsyncReadExt as _};
 | 
			
		||||
 | 
			
		||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
 | 
			
		||||
pub enum RequestKind {
 | 
			
		||||
    Signal,
 | 
			
		||||
    Command,
 | 
			
		||||
}
 | 
			
		||||
impl RequestKind {
 | 
			
		||||
    fn name(self) -> &'static str {
 | 
			
		||||
        match self {
 | 
			
		||||
            Self::Signal => "Signal",
 | 
			
		||||
            Self::Command => "Command",
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub struct Request<'a> {
 | 
			
		||||
    pub kind: RequestKind,
 | 
			
		||||
    buffer: &'a [u8],
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl std::fmt::Debug for Request<'_> {
 | 
			
		||||
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 | 
			
		||||
        let mut ds = f.debug_tuple(self.kind.name());
 | 
			
		||||
        match self.kind {
 | 
			
		||||
            RequestKind::Signal => ds.field(&self.buffer),
 | 
			
		||||
            RequestKind::Command => match std::str::from_utf8(self.buffer) {
 | 
			
		||||
                Ok(str) => ds.field(&str),
 | 
			
		||||
                Err(_) => ds.field(&self.buffer),
 | 
			
		||||
            },
 | 
			
		||||
        };
 | 
			
		||||
        ds.finish()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Request<'_> {
 | 
			
		||||
    pub fn match_str(&mut self, prefix: &str) -> bool {
 | 
			
		||||
        if let Some(new_buffer) = self.buffer.strip_prefix(prefix.as_bytes()) {
 | 
			
		||||
            self.buffer = new_buffer;
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
        false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn match_some_str<'a, I: IntoIterator<Item = &'a str>>(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        prefixes: I,
 | 
			
		||||
    ) -> Option<&'a str> {
 | 
			
		||||
        prefixes.into_iter().find(|&prefix| self.match_str(prefix))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn match_hex<I: FromRadix16>(&mut self) -> Option<I> {
 | 
			
		||||
        match I::from_radix_16(self.buffer) {
 | 
			
		||||
            (_, 0) => None,
 | 
			
		||||
            (val, used) => {
 | 
			
		||||
                self.buffer = self.buffer.split_at(used).1;
 | 
			
		||||
                Some(val)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn match_hex_bytes(&mut self, buffer: &mut [u8]) -> bool {
 | 
			
		||||
        if self.buffer.len() < buffer.len() * 2 {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        for (i, item) in buffer.iter_mut().enumerate() {
 | 
			
		||||
            match u8::from_radix_16(&self.buffer[(i * 2)..(i * 2) + 2]) {
 | 
			
		||||
                (byte, 2) => *item = byte,
 | 
			
		||||
                _ => return false,
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
        self.buffer = self.buffer.split_at(buffer.len()).1;
 | 
			
		||||
        true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn match_bytes(&mut self, buffer: &mut [u8]) -> bool {
 | 
			
		||||
        if self.buffer.len() < buffer.len() {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        buffer.copy_from_slice(&self.buffer[0..buffer.len()]);
 | 
			
		||||
        self.buffer = self.buffer.split_at(buffer.len()).1;
 | 
			
		||||
        true
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub struct RequestSource<R> {
 | 
			
		||||
    reader: R,
 | 
			
		||||
    buffer: Vec<u8>,
 | 
			
		||||
    state: RequestReadState,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<R: AsyncRead + Unpin> RequestSource<R> {
 | 
			
		||||
    pub fn new(reader: R) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            reader,
 | 
			
		||||
            buffer: vec![],
 | 
			
		||||
            state: RequestReadState::Header,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn recv(&mut self) -> Result<Request<'_>> {
 | 
			
		||||
        let mut char = self.reader.read_u8().await?;
 | 
			
		||||
        if matches!(self.state, RequestReadState::Start) {
 | 
			
		||||
            self.buffer.clear();
 | 
			
		||||
            self.state = RequestReadState::Header;
 | 
			
		||||
        }
 | 
			
		||||
        if matches!(self.state, RequestReadState::Header) {
 | 
			
		||||
            // Just ignore positive acks
 | 
			
		||||
            while char == b'+' {
 | 
			
		||||
                char = self.reader.read_u8().await?;
 | 
			
		||||
            }
 | 
			
		||||
            if char == b'-' {
 | 
			
		||||
                bail!("no support for negative acks");
 | 
			
		||||
            }
 | 
			
		||||
            if char == 0x03 {
 | 
			
		||||
                // This is how the client "cancels an in-flight request"
 | 
			
		||||
                self.buffer.push(char);
 | 
			
		||||
                self.state = RequestReadState::Start;
 | 
			
		||||
                return Ok(Request {
 | 
			
		||||
                    kind: RequestKind::Signal,
 | 
			
		||||
                    buffer: &self.buffer,
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
            if char != b'$' {
 | 
			
		||||
                // Messages are supposed to start with a dollar sign
 | 
			
		||||
                bail!("malformed message");
 | 
			
		||||
            }
 | 
			
		||||
            self.state = RequestReadState::Body {
 | 
			
		||||
                checksum: 0,
 | 
			
		||||
                escaping: false,
 | 
			
		||||
            };
 | 
			
		||||
            char = self.reader.read_u8().await?;
 | 
			
		||||
        }
 | 
			
		||||
        while let RequestReadState::Body { checksum, escaping } = &mut self.state {
 | 
			
		||||
            if char == b'#' && !*escaping {
 | 
			
		||||
                self.state = RequestReadState::Checksum {
 | 
			
		||||
                    expected: *checksum,
 | 
			
		||||
                    actual: 0,
 | 
			
		||||
                    digits: 0,
 | 
			
		||||
                };
 | 
			
		||||
                char = self.reader.read_u8().await?;
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
            *checksum = checksum.wrapping_add(char);
 | 
			
		||||
 | 
			
		||||
            if *escaping {
 | 
			
		||||
                // escaped character
 | 
			
		||||
                self.buffer.push(char ^ 0x20);
 | 
			
		||||
                *escaping = false;
 | 
			
		||||
            } else if char == b'}' {
 | 
			
		||||
                // next character will be escaped
 | 
			
		||||
                *escaping = true;
 | 
			
		||||
            } else {
 | 
			
		||||
                self.buffer.push(char);
 | 
			
		||||
            }
 | 
			
		||||
            char = self.reader.read_u8().await?;
 | 
			
		||||
        }
 | 
			
		||||
        while let RequestReadState::Checksum {
 | 
			
		||||
            expected,
 | 
			
		||||
            actual,
 | 
			
		||||
            digits,
 | 
			
		||||
        } = &mut self.state
 | 
			
		||||
        {
 | 
			
		||||
            let digit = match char {
 | 
			
		||||
                b'0'..=b'9' => char - b'0',
 | 
			
		||||
                b'a'..=b'f' => char - b'a' + 10,
 | 
			
		||||
                b'A'..=b'F' => char - b'A' + 10,
 | 
			
		||||
                _ => bail!("invalid checksum"),
 | 
			
		||||
            };
 | 
			
		||||
            *actual = (*actual << 4) + digit;
 | 
			
		||||
            *digits += 1;
 | 
			
		||||
            if *digits == 2 {
 | 
			
		||||
                if *expected != *actual {
 | 
			
		||||
                    bail!("mismatched checksum");
 | 
			
		||||
                }
 | 
			
		||||
                self.state = RequestReadState::Start;
 | 
			
		||||
                return Ok(Request {
 | 
			
		||||
                    kind: RequestKind::Command,
 | 
			
		||||
                    buffer: &self.buffer,
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
            char = self.reader.read_u8().await?;
 | 
			
		||||
        }
 | 
			
		||||
        unreachable!();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum RequestReadState {
 | 
			
		||||
    Start,
 | 
			
		||||
    Header,
 | 
			
		||||
    Body {
 | 
			
		||||
        checksum: u8,
 | 
			
		||||
        escaping: bool,
 | 
			
		||||
    },
 | 
			
		||||
    Checksum {
 | 
			
		||||
        expected: u8,
 | 
			
		||||
        actual: u8,
 | 
			
		||||
        digits: u8,
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,63 @@
 | 
			
		|||
use num_traits::ToBytes;
 | 
			
		||||
 | 
			
		||||
pub struct Response {
 | 
			
		||||
    buffer: Vec<u8>,
 | 
			
		||||
    checksum: u8,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Response {
 | 
			
		||||
    pub fn new(mut buffer: Vec<u8>, ack: bool) -> Self {
 | 
			
		||||
        buffer.clear();
 | 
			
		||||
        if ack {
 | 
			
		||||
            buffer.push(b'+');
 | 
			
		||||
        }
 | 
			
		||||
        buffer.push(b'$');
 | 
			
		||||
        Self {
 | 
			
		||||
            buffer,
 | 
			
		||||
            checksum: 0,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn write_str(self, str: &str) -> Self {
 | 
			
		||||
        let mut me = self;
 | 
			
		||||
        for byte in str.as_bytes() {
 | 
			
		||||
            me = me.write_byte(*byte);
 | 
			
		||||
        }
 | 
			
		||||
        me
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn write_byte(mut self, byte: u8) -> Self {
 | 
			
		||||
        if byte == b'}' || byte == b'#' || byte == b'$' || byte == b'*' {
 | 
			
		||||
            self.buffer.push(b'}');
 | 
			
		||||
            self.checksum = self.checksum.wrapping_add(b'}');
 | 
			
		||||
            let escaped = byte ^ 0x20;
 | 
			
		||||
            self.buffer.push(escaped);
 | 
			
		||||
            self.checksum = self.checksum.wrapping_add(escaped);
 | 
			
		||||
        } else {
 | 
			
		||||
            self.buffer.push(byte);
 | 
			
		||||
            self.checksum = self.checksum.wrapping_add(byte);
 | 
			
		||||
        }
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn write_hex<T: ToBytes>(mut self, value: T) -> Self {
 | 
			
		||||
        for byte in value.to_le_bytes().as_ref() {
 | 
			
		||||
            for digit in [(byte >> 4), (byte & 0xf)] {
 | 
			
		||||
                let char = if digit > 9 {
 | 
			
		||||
                    b'a' + digit - 10
 | 
			
		||||
                } else {
 | 
			
		||||
                    b'0' + digit
 | 
			
		||||
                };
 | 
			
		||||
                self.buffer.push(char);
 | 
			
		||||
                self.checksum = self.checksum.wrapping_add(char);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn finish(mut self) -> Vec<u8> {
 | 
			
		||||
        let checksum = self.checksum;
 | 
			
		||||
        self.buffer.push(b'#');
 | 
			
		||||
        self.write_hex(checksum).buffer
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										34
									
								
								src/main.rs
								
								
								
								
							
							
						
						
									
										34
									
								
								src/main.rs
								
								
								
								
							| 
						 | 
				
			
			@ -3,17 +3,20 @@
 | 
			
		|||
 | 
			
		||||
use std::{path::PathBuf, process, time::SystemTime};
 | 
			
		||||
 | 
			
		||||
use anyhow::Result;
 | 
			
		||||
use anyhow::{bail, Result};
 | 
			
		||||
use app::Application;
 | 
			
		||||
use clap::Parser;
 | 
			
		||||
use emulator::EmulatorBuilder;
 | 
			
		||||
use thread_priority::{ThreadBuilder, ThreadPriority};
 | 
			
		||||
use tracing::error;
 | 
			
		||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer};
 | 
			
		||||
use winit::event_loop::{ControlFlow, EventLoop};
 | 
			
		||||
 | 
			
		||||
mod app;
 | 
			
		||||
mod audio;
 | 
			
		||||
mod controller;
 | 
			
		||||
mod emulator;
 | 
			
		||||
mod gdbserver;
 | 
			
		||||
mod graphics;
 | 
			
		||||
mod input;
 | 
			
		||||
mod persistence;
 | 
			
		||||
| 
						 | 
				
			
			@ -21,7 +24,18 @@ mod window;
 | 
			
		|||
 | 
			
		||||
#[derive(Parser)]
 | 
			
		||||
struct Args {
 | 
			
		||||
    /// The path to a virtual boy ROM to run.
 | 
			
		||||
    rom: Option<PathBuf>,
 | 
			
		||||
    /// Start a GDB/LLDB debug server on this port.
 | 
			
		||||
    #[arg(short, long)]
 | 
			
		||||
    debug_port: Option<u16>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn init_logger() {
 | 
			
		||||
    let directives = std::env::var("RUST_LOG").unwrap_or("error,lemur=info".into());
 | 
			
		||||
    let filter = EnvFilter::builder().parse_lossy(directives);
 | 
			
		||||
    let layer = tracing_subscriber::fmt::layer().with_filter(filter);
 | 
			
		||||
    tracing_subscriber::registry().with(layer).init();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn set_panic_handler() {
 | 
			
		||||
| 
						 | 
				
			
			@ -71,6 +85,8 @@ fn set_process_priority_to_high() -> Result<()> {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
fn main() -> Result<()> {
 | 
			
		||||
    init_logger();
 | 
			
		||||
 | 
			
		||||
    set_panic_handler();
 | 
			
		||||
 | 
			
		||||
    #[cfg(windows)]
 | 
			
		||||
| 
						 | 
				
			
			@ -79,8 +95,14 @@ fn main() -> Result<()> {
 | 
			
		|||
    let args = Args::parse();
 | 
			
		||||
 | 
			
		||||
    let (mut builder, client) = EmulatorBuilder::new();
 | 
			
		||||
    if let Some(path) = args.rom {
 | 
			
		||||
        builder = builder.with_rom(&path);
 | 
			
		||||
    if let Some(path) = &args.rom {
 | 
			
		||||
        builder = builder.with_rom(path);
 | 
			
		||||
    }
 | 
			
		||||
    if args.debug_port.is_some() {
 | 
			
		||||
        if args.rom.is_none() {
 | 
			
		||||
            bail!("to start debugging, please select a game.");
 | 
			
		||||
        }
 | 
			
		||||
        builder = builder.start_paused(true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ThreadBuilder::default()
 | 
			
		||||
| 
						 | 
				
			
			@ -89,8 +111,8 @@ fn main() -> Result<()> {
 | 
			
		|||
        .spawn_careless(move || {
 | 
			
		||||
            let mut emulator = match builder.build() {
 | 
			
		||||
                Ok(e) => e,
 | 
			
		||||
                Err(err) => {
 | 
			
		||||
                    eprintln!("Error initializing emulator: {err}");
 | 
			
		||||
                Err(error) => {
 | 
			
		||||
                    error!(%error, "Error initializing emulator");
 | 
			
		||||
                    process::exit(1);
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
| 
						 | 
				
			
			@ -100,6 +122,6 @@ fn main() -> Result<()> {
 | 
			
		|||
    let event_loop = EventLoop::with_user_event().build().unwrap();
 | 
			
		||||
    event_loop.set_control_flow(ControlFlow::Poll);
 | 
			
		||||
    let proxy = event_loop.create_proxy();
 | 
			
		||||
    event_loop.run_app(&mut Application::new(client, proxy))?;
 | 
			
		||||
    event_loop.run_app(&mut Application::new(client, proxy, args.debug_port))?;
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,16 +1,23 @@
 | 
			
		|||
pub use about::AboutWindow;
 | 
			
		||||
use egui::{Context, ViewportBuilder, ViewportId};
 | 
			
		||||
pub use game::GameWindow;
 | 
			
		||||
pub use gdb::GdbServerWindow;
 | 
			
		||||
pub use input::InputWindow;
 | 
			
		||||
use winit::event::KeyEvent;
 | 
			
		||||
 | 
			
		||||
use crate::emulator::SimId;
 | 
			
		||||
 | 
			
		||||
mod about;
 | 
			
		||||
mod game;
 | 
			
		||||
mod game_screen;
 | 
			
		||||
mod gdb;
 | 
			
		||||
mod input;
 | 
			
		||||
 | 
			
		||||
pub trait AppWindow {
 | 
			
		||||
    fn viewport_id(&self) -> ViewportId;
 | 
			
		||||
    fn sim_id(&self) -> SimId {
 | 
			
		||||
        SimId::Player1
 | 
			
		||||
    }
 | 
			
		||||
    fn initial_viewport(&self) -> ViewportBuilder;
 | 
			
		||||
    fn show(&mut self, ctx: &Context);
 | 
			
		||||
    fn on_init(&mut self, render_state: &egui_wgpu::RenderState) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,7 +2,7 @@ use std::sync::mpsc;
 | 
			
		|||
 | 
			
		||||
use crate::{
 | 
			
		||||
    app::UserEvent,
 | 
			
		||||
    emulator::{EmulatorClient, EmulatorCommand, SimId},
 | 
			
		||||
    emulator::{EmulatorClient, EmulatorCommand, EmulatorState, SimId, SimState},
 | 
			
		||||
    persistence::Persistence,
 | 
			
		||||
};
 | 
			
		||||
use egui::{
 | 
			
		||||
| 
						 | 
				
			
			@ -73,26 +73,29 @@ impl GameWindow {
 | 
			
		|||
                    .pick_file();
 | 
			
		||||
                if let Some(path) = rom {
 | 
			
		||||
                    self.client
 | 
			
		||||
                        .send_command(EmulatorCommand::LoadGame(SimId::Player1, path));
 | 
			
		||||
                        .send_command(EmulatorCommand::LoadGame(self.sim_id, path));
 | 
			
		||||
                }
 | 
			
		||||
                ui.close_menu();
 | 
			
		||||
            }
 | 
			
		||||
            if ui.button("Quit").clicked() {
 | 
			
		||||
                ctx.send_viewport_cmd(ViewportCommand::Close);
 | 
			
		||||
                let _ = self.proxy.send_event(UserEvent::Quit(self.sim_id));
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        ui.menu_button("Emulation", |ui| {
 | 
			
		||||
            let has_game = self.client.has_game(self.sim_id);
 | 
			
		||||
            if self.client.is_running(self.sim_id) {
 | 
			
		||||
                if ui.add_enabled(has_game, Button::new("Pause")).clicked() {
 | 
			
		||||
            let state = self.client.emulator_state();
 | 
			
		||||
            let is_ready = self.client.sim_state(self.sim_id) == SimState::Ready;
 | 
			
		||||
            let can_pause = is_ready && state == EmulatorState::Running;
 | 
			
		||||
            let can_resume = is_ready && state == EmulatorState::Paused;
 | 
			
		||||
            if state == EmulatorState::Running {
 | 
			
		||||
                if ui.add_enabled(can_pause, Button::new("Pause")).clicked() {
 | 
			
		||||
                    self.client.send_command(EmulatorCommand::Pause);
 | 
			
		||||
                    ui.close_menu();
 | 
			
		||||
                }
 | 
			
		||||
            } else if ui.add_enabled(has_game, Button::new("Resume")).clicked() {
 | 
			
		||||
            } else if ui.add_enabled(can_resume, Button::new("Resume")).clicked() {
 | 
			
		||||
                self.client.send_command(EmulatorCommand::Resume);
 | 
			
		||||
                ui.close_menu();
 | 
			
		||||
            }
 | 
			
		||||
            if ui.add_enabled(has_game, Button::new("Reset")).clicked() {
 | 
			
		||||
            if ui.add_enabled(is_ready, Button::new("Reset")).clicked() {
 | 
			
		||||
                self.client
 | 
			
		||||
                    .send_command(EmulatorCommand::Reset(self.sim_id));
 | 
			
		||||
                ui.close_menu();
 | 
			
		||||
| 
						 | 
				
			
			@ -100,8 +103,9 @@ impl GameWindow {
 | 
			
		|||
        });
 | 
			
		||||
        ui.menu_button("Options", |ui| self.show_options_menu(ctx, ui));
 | 
			
		||||
        ui.menu_button("Multiplayer", |ui| {
 | 
			
		||||
            let has_player_2 = self.client.sim_state(SimId::Player2) != SimState::Uninitialized;
 | 
			
		||||
            if self.sim_id == SimId::Player1
 | 
			
		||||
                && !self.client.has_player_2()
 | 
			
		||||
                && !has_player_2
 | 
			
		||||
                && ui.button("Open Player 2").clicked()
 | 
			
		||||
            {
 | 
			
		||||
                self.client
 | 
			
		||||
| 
						 | 
				
			
			@ -109,7 +113,7 @@ impl GameWindow {
 | 
			
		|||
                self.proxy.send_event(UserEvent::OpenPlayer2).unwrap();
 | 
			
		||||
                ui.close_menu();
 | 
			
		||||
            }
 | 
			
		||||
            if self.client.has_player_2() {
 | 
			
		||||
            if has_player_2 {
 | 
			
		||||
                let linked = self.client.are_sims_linked();
 | 
			
		||||
                if linked && ui.button("Unlink").clicked() {
 | 
			
		||||
                    self.client.send_command(EmulatorCommand::Unlink);
 | 
			
		||||
| 
						 | 
				
			
			@ -121,6 +125,14 @@ impl GameWindow {
 | 
			
		|||
                }
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        ui.menu_button("Tools", |ui| {
 | 
			
		||||
            if ui.button("GDB Server").clicked() {
 | 
			
		||||
                self.proxy
 | 
			
		||||
                    .send_event(UserEvent::OpenDebugger(self.sim_id))
 | 
			
		||||
                    .unwrap();
 | 
			
		||||
                ui.close_menu();
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        ui.menu_button("About", |ui| {
 | 
			
		||||
            self.proxy.send_event(UserEvent::OpenAbout).unwrap();
 | 
			
		||||
            ui.close_menu();
 | 
			
		||||
| 
						 | 
				
			
			@ -199,7 +211,7 @@ impl GameWindow {
 | 
			
		|||
                        let color_str = |color: Color32| {
 | 
			
		||||
                            format!("{:02x}{:02x}{:02x}", color.r(), color.g(), color.b())
 | 
			
		||||
                        };
 | 
			
		||||
                        let is_running = self.client.is_running(self.sim_id);
 | 
			
		||||
                        let is_running = self.client.emulator_state() == EmulatorState::Running;
 | 
			
		||||
                        if is_running {
 | 
			
		||||
                            self.client.send_command(EmulatorCommand::Pause);
 | 
			
		||||
                        }
 | 
			
		||||
| 
						 | 
				
			
			@ -306,6 +318,10 @@ impl AppWindow for GameWindow {
 | 
			
		|||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn sim_id(&self) -> SimId {
 | 
			
		||||
        self.sim_id
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn initial_viewport(&self) -> ViewportBuilder {
 | 
			
		||||
        ViewportBuilder::default()
 | 
			
		||||
            .with_title("Lemur")
 | 
			
		||||
| 
						 | 
				
			
			@ -369,6 +385,7 @@ impl AppWindow for GameWindow {
 | 
			
		|||
        if self.sim_id == SimId::Player2 {
 | 
			
		||||
            self.client.send_command(EmulatorCommand::StopSecondSim);
 | 
			
		||||
        }
 | 
			
		||||
        let _ = self.proxy.send_event(UserEvent::Quit(self.sim_id));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,111 @@
 | 
			
		|||
use egui::{Button, CentralPanel, TextEdit, ViewportBuilder, ViewportId};
 | 
			
		||||
use winit::event_loop::EventLoopProxy;
 | 
			
		||||
 | 
			
		||||
use crate::{
 | 
			
		||||
    app::UserEvent,
 | 
			
		||||
    emulator::{EmulatorClient, SimId, SimState},
 | 
			
		||||
    gdbserver::{GdbServer, GdbServerStatus},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
use super::AppWindow;
 | 
			
		||||
 | 
			
		||||
pub struct GdbServerWindow {
 | 
			
		||||
    sim_id: SimId,
 | 
			
		||||
    client: EmulatorClient,
 | 
			
		||||
    port_str: String,
 | 
			
		||||
    connected: bool,
 | 
			
		||||
    quit_on_disconnect: bool,
 | 
			
		||||
    server: GdbServer,
 | 
			
		||||
    proxy: EventLoopProxy<UserEvent>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl GdbServerWindow {
 | 
			
		||||
    pub fn new(sim_id: SimId, client: EmulatorClient, proxy: EventLoopProxy<UserEvent>) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            sim_id,
 | 
			
		||||
            client: client.clone(),
 | 
			
		||||
            port_str: (8080 + sim_id.to_index()).to_string(),
 | 
			
		||||
            connected: false,
 | 
			
		||||
            quit_on_disconnect: false,
 | 
			
		||||
            server: GdbServer::new(sim_id, client),
 | 
			
		||||
            proxy,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn launch(&mut self, port: u16) {
 | 
			
		||||
        self.server.stop();
 | 
			
		||||
        self.port_str = port.to_string();
 | 
			
		||||
        self.quit_on_disconnect = true;
 | 
			
		||||
        self.server.start(port);
 | 
			
		||||
        self.connected = true;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl AppWindow for GdbServerWindow {
 | 
			
		||||
    fn viewport_id(&self) -> ViewportId {
 | 
			
		||||
        ViewportId::from_hash_of(format!("Debugger-{}", self.sim_id))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn sim_id(&self) -> SimId {
 | 
			
		||||
        self.sim_id
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn initial_viewport(&self) -> ViewportBuilder {
 | 
			
		||||
        ViewportBuilder::default()
 | 
			
		||||
            .with_title(format!("GDB Server ({})", self.sim_id))
 | 
			
		||||
            .with_inner_size((300.0, 200.0))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn show(&mut self, ctx: &egui::Context) {
 | 
			
		||||
        let port_num: Option<u16> = self.port_str.parse().ok();
 | 
			
		||||
        let status = self.server.status();
 | 
			
		||||
        CentralPanel::default().show(ctx, |ui| {
 | 
			
		||||
            ui.horizontal(|ui| {
 | 
			
		||||
                if port_num.is_none() {
 | 
			
		||||
                    let style = ui.style_mut();
 | 
			
		||||
                    let error = style.visuals.error_fg_color;
 | 
			
		||||
                    style.visuals.widgets.active.bg_stroke.color = error;
 | 
			
		||||
                    style.visuals.widgets.hovered.bg_stroke.color = error;
 | 
			
		||||
                }
 | 
			
		||||
                ui.label("Port");
 | 
			
		||||
                let port_editor = TextEdit::singleline(&mut self.port_str).desired_width(100.0);
 | 
			
		||||
                ui.add_enabled(!status.running(), port_editor);
 | 
			
		||||
                ui.checkbox(&mut self.quit_on_disconnect, "Quit on disconnect");
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if !status.running() {
 | 
			
		||||
                if self.connected && self.quit_on_disconnect {
 | 
			
		||||
                    self.proxy.send_event(UserEvent::Quit(self.sim_id)).unwrap();
 | 
			
		||||
                } else {
 | 
			
		||||
                    self.connected = false;
 | 
			
		||||
                }
 | 
			
		||||
                let start_button = Button::new("Start");
 | 
			
		||||
                let can_start =
 | 
			
		||||
                    port_num.is_some() && self.client.sim_state(self.sim_id) == SimState::Ready;
 | 
			
		||||
                if ui.add_enabled(can_start, start_button).clicked() {
 | 
			
		||||
                    let port = port_num.unwrap();
 | 
			
		||||
                    self.server.start(port);
 | 
			
		||||
                    self.connected = true;
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                let stop_button = Button::new("Stop");
 | 
			
		||||
                if ui.add(stop_button).clicked() {
 | 
			
		||||
                    self.server.stop();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            match &status {
 | 
			
		||||
                GdbServerStatus::Stopped => {}
 | 
			
		||||
                GdbServerStatus::Connecting => {
 | 
			
		||||
                    ui.label("Connecting...");
 | 
			
		||||
                }
 | 
			
		||||
                GdbServerStatus::Running => {
 | 
			
		||||
                    ui.label("Running");
 | 
			
		||||
                }
 | 
			
		||||
                GdbServerStatus::Error(message) => {
 | 
			
		||||
                    ui.label(message);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue