Experimental gdbserver
Former-commit-id: e78618b03c745bb9820216e6d9f8c1f4cade28d5 Former-commit-id: 5851f5930e07d8132e643bbe6773bdd0bd42fad6
This commit is contained in:
parent
9abc08fffe
commit
c47d9e1f11
112
Cargo.lock
generated
112
Cargo.lock
generated
|
@ -438,27 +438,62 @@ dependencies = [
|
||||||
"itertools 0.9.0",
|
"itertools 0.9.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam"
|
||||||
|
version = "0.8.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2801af0d36612ae591caa9568261fddce32ce6e08a7275ea334a06a4ad021a2c"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if 1.0.0",
|
||||||
|
"crossbeam-channel 0.5.6",
|
||||||
|
"crossbeam-deque 0.8.2",
|
||||||
|
"crossbeam-epoch 0.9.10",
|
||||||
|
"crossbeam-queue",
|
||||||
|
"crossbeam-utils 0.8.11",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-channel"
|
name = "crossbeam-channel"
|
||||||
version = "0.4.4"
|
version = "0.4.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b153fe7cbef478c567df0f972e02e6d736db11affe43dfc9c56a9374d1adfb87"
|
checksum = "b153fe7cbef478c567df0f972e02e6d736db11affe43dfc9c56a9374d1adfb87"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crossbeam-utils",
|
"crossbeam-utils 0.7.2",
|
||||||
"maybe-uninit",
|
"maybe-uninit",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-channel"
|
||||||
|
version = "0.5.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if 1.0.0",
|
||||||
|
"crossbeam-utils 0.8.11",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-deque"
|
name = "crossbeam-deque"
|
||||||
version = "0.7.3"
|
version = "0.7.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9f02af974daeee82218205558e51ec8768b48cf524bd01d550abe5573a608285"
|
checksum = "9f02af974daeee82218205558e51ec8768b48cf524bd01d550abe5573a608285"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crossbeam-epoch",
|
"crossbeam-epoch 0.8.2",
|
||||||
"crossbeam-utils",
|
"crossbeam-utils 0.7.2",
|
||||||
"maybe-uninit",
|
"maybe-uninit",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-deque"
|
||||||
|
version = "0.8.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if 1.0.0",
|
||||||
|
"crossbeam-epoch 0.9.10",
|
||||||
|
"crossbeam-utils 0.8.11",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-epoch"
|
name = "crossbeam-epoch"
|
||||||
version = "0.8.2"
|
version = "0.8.2"
|
||||||
|
@ -467,13 +502,37 @@ checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
"cfg-if 0.1.10",
|
"cfg-if 0.1.10",
|
||||||
"crossbeam-utils",
|
"crossbeam-utils 0.7.2",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"maybe-uninit",
|
"maybe-uninit",
|
||||||
"memoffset",
|
"memoffset 0.5.6",
|
||||||
"scopeguard",
|
"scopeguard",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-epoch"
|
||||||
|
version = "0.9.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "045ebe27666471bb549370b4b0b3e51b07f56325befa4284db65fc89c02511b1"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"cfg-if 1.0.0",
|
||||||
|
"crossbeam-utils 0.8.11",
|
||||||
|
"memoffset 0.6.5",
|
||||||
|
"once_cell",
|
||||||
|
"scopeguard",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-queue"
|
||||||
|
version = "0.3.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1cd42583b04998a5363558e5f9291ee5a5ff6b49944332103f251e7479a82aa7"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if 1.0.0",
|
||||||
|
"crossbeam-utils 0.8.11",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-utils"
|
name = "crossbeam-utils"
|
||||||
version = "0.7.2"
|
version = "0.7.2"
|
||||||
|
@ -485,6 +544,16 @@ dependencies = [
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-utils"
|
||||||
|
version = "0.8.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "51887d4adc7b564537b15adcfb307936f8075dfcd5f00dde9a9f1d29383682bc"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if 1.0.0",
|
||||||
|
"once_cell",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "csv"
|
name = "csv"
|
||||||
version = "1.1.3"
|
version = "1.1.3"
|
||||||
|
@ -945,6 +1014,15 @@ dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memoffset"
|
||||||
|
version = "0.6.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memory_units"
|
name = "memory_units"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
|
@ -1116,6 +1194,12 @@ version = "0.2.26"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4eae0151b9dacf24fcc170d9995e511669a082856a91f958a2fe380bfab3fb22"
|
checksum = "4eae0151b9dacf24fcc170d9995e511669a082856a91f958a2fe380bfab3fb22"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell"
|
||||||
|
version = "1.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "oorandom"
|
name = "oorandom"
|
||||||
version = "11.1.2"
|
version = "11.1.2"
|
||||||
|
@ -1243,7 +1327,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cfd016f0c045ad38b5251be2c9c0ab806917f82da4d36b2a327e5166adad9270"
|
checksum = "cfd016f0c045ad38b5251be2c9c0ab806917f82da4d36b2a327e5166adad9270"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
"crossbeam-deque",
|
"crossbeam-deque 0.7.3",
|
||||||
"either",
|
"either",
|
||||||
"rayon-core",
|
"rayon-core",
|
||||||
]
|
]
|
||||||
|
@ -1254,9 +1338,9 @@ version = "1.8.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e8c4fec834fb6e6d2dd5eece3c7b432a52f0ba887cf40e595190c4107edc08bf"
|
checksum = "e8c4fec834fb6e6d2dd5eece3c7b432a52f0ba887cf40e595190c4107edc08bf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crossbeam-channel",
|
"crossbeam-channel 0.4.4",
|
||||||
"crossbeam-deque",
|
"crossbeam-deque 0.7.3",
|
||||||
"crossbeam-utils",
|
"crossbeam-utils 0.7.2",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"num_cpus",
|
"num_cpus",
|
||||||
]
|
]
|
||||||
|
@ -1320,7 +1404,7 @@ dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"blake2b_simd",
|
"blake2b_simd",
|
||||||
"constant_time_eq",
|
"constant_time_eq",
|
||||||
"crossbeam-utils",
|
"crossbeam-utils 0.7.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1342,6 +1426,7 @@ dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"colored 1.9.3",
|
"colored 1.9.3",
|
||||||
"criterion",
|
"criterion",
|
||||||
|
"crossbeam",
|
||||||
"debug_stub_derive",
|
"debug_stub_derive",
|
||||||
"enum-primitive-derive",
|
"enum-primitive-derive",
|
||||||
"fuzzy-matcher",
|
"fuzzy-matcher",
|
||||||
|
@ -1359,6 +1444,7 @@ dependencies = [
|
||||||
"sha2",
|
"sha2",
|
||||||
"smart-default",
|
"smart-default",
|
||||||
"time 0.2.22",
|
"time 0.2.22",
|
||||||
|
"xml-builder",
|
||||||
"yaml-rust",
|
"yaml-rust",
|
||||||
"zip",
|
"zip",
|
||||||
]
|
]
|
||||||
|
@ -2149,6 +2235,12 @@ dependencies = [
|
||||||
"toml",
|
"toml",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "xml-builder"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6aff3eebeb8c12d38dd6dcbd07082929a55764d13ad695632c35132ff7cf61ec"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yaml-rust"
|
name = "yaml-rust"
|
||||||
version = "0.4.4"
|
version = "0.4.4"
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use log::debug;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
pub use super::exception::Exception;
|
pub use super::exception::Exception;
|
||||||
|
@ -118,7 +119,7 @@ pub struct Arm7tdmiCore<I: MemoryInterface> {
|
||||||
pub banks: BankedRegisters,
|
pub banks: BankedRegisters,
|
||||||
|
|
||||||
/// Hardware breakpoints for use by gdb
|
/// Hardware breakpoints for use by gdb
|
||||||
pub breakpoints: Vec<Addr>,
|
breakpoints: Vec<Addr>,
|
||||||
|
|
||||||
/// Deprecated in-house debugger state
|
/// Deprecated in-house debugger state
|
||||||
#[cfg(feature = "debugger")]
|
#[cfg(feature = "debugger")]
|
||||||
|
@ -196,6 +197,28 @@ impl<I: MemoryInterface> Arm7tdmiCore<I> {
|
||||||
self.bus = i;
|
self.bus = i;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn add_breakpoint(&mut self, addr: Addr) {
|
||||||
|
debug!("adding breakpoint {:08x}", addr);
|
||||||
|
self.breakpoints.push(addr);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn del_breakpoint(&mut self, addr: Addr) {
|
||||||
|
if let Some(pos) = self.breakpoints.iter().position(|x| *x == addr) {
|
||||||
|
debug!("deleting breakpoint {:08x}", addr);
|
||||||
|
self.breakpoints.remove(pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check_breakpoint(&self) -> Option<u32> {
|
||||||
|
let next_pc = self.get_next_pc();
|
||||||
|
for bp in &self.breakpoints {
|
||||||
|
if (*bp & !1) == next_pc {
|
||||||
|
return Some(*bp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "debugger")]
|
#[cfg(feature = "debugger")]
|
||||||
pub fn set_verbose(&mut self, v: bool) {
|
pub fn set_verbose(&mut self, v: bool) {
|
||||||
self.dbg.verbose = v;
|
self.dbg.verbose = v;
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
use gdbstub::target;
|
use gdbstub::target;
|
||||||
use gdbstub::target::TargetResult;
|
use gdbstub::target::TargetResult;
|
||||||
use log::debug;
|
|
||||||
|
|
||||||
use crate::Arm7tdmiCore;
|
use crate::Arm7tdmiCore;
|
||||||
|
|
||||||
|
@ -22,8 +21,7 @@ impl<I: MemoryGdbInterface> target::ext::breakpoints::SwBreakpoint for Arm7tdmiC
|
||||||
addr: u32,
|
addr: u32,
|
||||||
_kind: gdbstub_arch::arm::ArmBreakpointKind,
|
_kind: gdbstub_arch::arm::ArmBreakpointKind,
|
||||||
) -> TargetResult<bool, Self> {
|
) -> TargetResult<bool, Self> {
|
||||||
debug!("adding breakpoint {:08x}", addr);
|
self.add_breakpoint(addr);
|
||||||
self.breakpoints.push(addr);
|
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,25 +30,7 @@ impl<I: MemoryGdbInterface> target::ext::breakpoints::SwBreakpoint for Arm7tdmiC
|
||||||
addr: u32,
|
addr: u32,
|
||||||
_kind: gdbstub_arch::arm::ArmBreakpointKind,
|
_kind: gdbstub_arch::arm::ArmBreakpointKind,
|
||||||
) -> TargetResult<bool, Self> {
|
) -> TargetResult<bool, Self> {
|
||||||
match self.breakpoints.iter().position(|x| *x == addr) {
|
self.del_breakpoint(addr);
|
||||||
None => Ok(false),
|
Ok(true)
|
||||||
Some(pos) => {
|
|
||||||
debug!("deleting breakpoint {:08x}", addr);
|
|
||||||
self.breakpoints.remove(pos);
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<I: MemoryGdbInterface> Arm7tdmiCore<I> {
|
|
||||||
pub fn check_breakpoint(&self) -> Option<u32> {
|
|
||||||
let next_pc = self.get_next_pc();
|
|
||||||
for bp in &self.breakpoints {
|
|
||||||
if (*bp & !1) == next_pc {
|
|
||||||
return Some(*bp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,7 +41,7 @@ impl<I: MemoryGdbInterface> SingleThreadBase for Arm7tdmiCore<I> {
|
||||||
&mut self,
|
&mut self,
|
||||||
regs: &mut gdbstub_arch::arm::reg::ArmCoreRegs,
|
regs: &mut gdbstub_arch::arm::reg::ArmCoreRegs,
|
||||||
) -> TargetResult<(), Self> {
|
) -> TargetResult<(), Self> {
|
||||||
regs.pc = self.get_reg(REG_PC);
|
regs.pc = self.get_next_pc();
|
||||||
regs.lr = self.get_reg(REG_LR);
|
regs.lr = self.get_reg(REG_LR);
|
||||||
regs.sp = self.get_reg(REG_SP);
|
regs.sp = self.get_reg(REG_SP);
|
||||||
regs.r[..].copy_from_slice(&self.gpr[..13]);
|
regs.r[..].copy_from_slice(&self.gpr[..13]);
|
||||||
|
|
|
@ -24,6 +24,7 @@ use memory::Addr;
|
||||||
pub mod disass;
|
pub mod disass;
|
||||||
pub mod exception;
|
pub mod exception;
|
||||||
pub mod gdb;
|
pub mod gdb;
|
||||||
|
pub use gdb::{gdbstub, gdbstub_arch};
|
||||||
pub mod psr;
|
pub mod psr;
|
||||||
mod simple_memory;
|
mod simple_memory;
|
||||||
pub use simple_memory::SimpleMemory;
|
pub use simple_memory::SimpleMemory;
|
||||||
|
|
|
@ -5,8 +5,8 @@ authors = ["Michel Heily <michelheily@gmail.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
arm7tdmi = { "path" = "../arm7tdmi" }
|
arm7tdmi = { path = '../arm7tdmi' }
|
||||||
rustboyadvance-utils = { "path" = "../utils" }
|
rustboyadvance-utils = { path = "../utils" }
|
||||||
cfg-if = "1.0.0"
|
cfg-if = "1.0.0"
|
||||||
serde = { version = "1.0.104", features = ["derive", "rc"] }
|
serde = { version = "1.0.104", features = ["derive", "rc"] }
|
||||||
bincode = "1.2.1"
|
bincode = "1.2.1"
|
||||||
|
@ -41,6 +41,8 @@ bit_reverse = "0.1.8"
|
||||||
yaml-rust = "0.4"
|
yaml-rust = "0.4"
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
smart-default = "0.6.0"
|
smart-default = "0.6.0"
|
||||||
|
crossbeam = "0.8.2"
|
||||||
|
xml-builder = "0.5.0"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
criterion = "0.3"
|
criterion = "0.3"
|
||||||
|
|
|
@ -34,6 +34,11 @@ impl Bios {
|
||||||
fn read_allowed(&self) -> bool {
|
fn read_allowed(&self) -> bool {
|
||||||
self.arm_core.pc < 0x4000
|
self.arm_core.pc < 0x4000
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn len(&self) -> usize {
|
||||||
|
self.rom.len()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Impl of Bus trait for Bios
|
/// Impl of Bus trait for Bios
|
||||||
|
|
|
@ -11,6 +11,9 @@ use rustboyadvance_utils::elf::{load_elf, GoblinError};
|
||||||
use rustboyadvance_utils::read_bin_file;
|
use rustboyadvance_utils::read_bin_file;
|
||||||
use zip::ZipArchive;
|
use zip::ZipArchive;
|
||||||
|
|
||||||
|
#[cfg(feature = "elf_support")]
|
||||||
|
use crate::sysbus::consts::CART_BASE;
|
||||||
|
|
||||||
pub enum LoadRom {
|
pub enum LoadRom {
|
||||||
#[cfg(feature = "elf_support")]
|
#[cfg(feature = "elf_support")]
|
||||||
Elf {
|
Elf {
|
||||||
|
@ -30,8 +33,7 @@ impl From<GoblinError> for GBAError {
|
||||||
|
|
||||||
#[cfg(feature = "elf_support")]
|
#[cfg(feature = "elf_support")]
|
||||||
pub(super) fn try_load_elf(elf_bytes: &[u8]) -> LoadRomResult {
|
pub(super) fn try_load_elf(elf_bytes: &[u8]) -> LoadRomResult {
|
||||||
const CART_BASE: usize = 0x0800_0000;
|
let elf = load_elf(elf_bytes, CART_BASE as usize)?;
|
||||||
let elf = load_elf(elf_bytes, CART_BASE)?;
|
|
||||||
Ok(LoadRom::Elf {
|
Ok(LoadRom::Elf {
|
||||||
data: elf.data,
|
data: elf.data,
|
||||||
symbols: elf.symbols,
|
symbols: elf.symbols,
|
||||||
|
|
|
@ -2,9 +2,12 @@
|
||||||
use std::cell::Cell;
|
use std::cell::Cell;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use arm7tdmi::gdbstub::stub::SingleThreadStopReason;
|
||||||
use bincode;
|
use bincode;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::gdb_support::{gdb_thread::start_gdb_server_thread, DebuggerState};
|
||||||
|
|
||||||
use super::cartridge::Cartridge;
|
use super::cartridge::Cartridge;
|
||||||
use super::dma::DmaController;
|
use super::dma::DmaController;
|
||||||
use super::gpu::*;
|
use super::gpu::*;
|
||||||
|
@ -21,12 +24,13 @@ use arm7tdmi::{self, Arm7tdmiCore};
|
||||||
use rustboyadvance_utils::Shared;
|
use rustboyadvance_utils::Shared;
|
||||||
|
|
||||||
pub struct GameBoyAdvance {
|
pub struct GameBoyAdvance {
|
||||||
pub cpu: Box<Arm7tdmiCore<SysBus>>,
|
pub(crate) cpu: Box<Arm7tdmiCore<SysBus>>,
|
||||||
pub sysbus: Shared<SysBus>,
|
pub(crate) sysbus: Shared<SysBus>,
|
||||||
pub io_devs: Shared<IoDevices>,
|
pub(crate) io_devs: Shared<IoDevices>,
|
||||||
pub scheduler: SharedScheduler,
|
pub(crate) scheduler: SharedScheduler,
|
||||||
interrupt_flags: SharedInterruptFlags,
|
interrupt_flags: SharedInterruptFlags,
|
||||||
pub audio_interface: DynAudioInterface,
|
audio_interface: DynAudioInterface,
|
||||||
|
pub(crate) debugger: Option<DebuggerState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
|
@ -106,6 +110,7 @@ impl GameBoyAdvance {
|
||||||
audio_interface,
|
audio_interface,
|
||||||
scheduler,
|
scheduler,
|
||||||
interrupt_flags,
|
interrupt_flags,
|
||||||
|
debugger: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
gba.sysbus.init(gba.cpu.weak_ptr());
|
gba.sysbus.init(gba.cpu.weak_ptr());
|
||||||
|
@ -150,6 +155,7 @@ impl GameBoyAdvance {
|
||||||
interrupt_flags: interrupts,
|
interrupt_flags: interrupts,
|
||||||
audio_interface,
|
audio_interface,
|
||||||
scheduler,
|
scheduler,
|
||||||
|
debugger: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -206,10 +212,62 @@ impl GameBoyAdvance {
|
||||||
&mut self.sysbus.io.keyinput
|
&mut self.sysbus.io.keyinput
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Advance the emulation for one frame worth of time
|
||||||
pub fn frame(&mut self) {
|
pub fn frame(&mut self) {
|
||||||
static mut OVERSHOOT: usize = 0;
|
static mut OVERSHOOT: usize = 0;
|
||||||
unsafe {
|
unsafe {
|
||||||
OVERSHOOT = self.run(CYCLES_FULL_REFRESH - OVERSHOOT);
|
OVERSHOOT = CYCLES_FULL_REFRESH - self.run::<false>(CYCLES_FULL_REFRESH - OVERSHOOT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// like frame() but stop if a breakpoint is reached
|
||||||
|
fn frame_interruptible(&mut self) {
|
||||||
|
static mut OVERSHOOT: usize = 0;
|
||||||
|
unsafe {
|
||||||
|
OVERSHOOT = CYCLES_FULL_REFRESH - self.run::<true>(CYCLES_FULL_REFRESH - OVERSHOOT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start_gdbserver(&mut self, port: u16) {
|
||||||
|
if self.is_debugger_attached() {
|
||||||
|
warn!("debugger already attached!");
|
||||||
|
} else {
|
||||||
|
match start_gdb_server_thread(self, port) {
|
||||||
|
Ok(debugger) => {
|
||||||
|
info!("attached to the debugger, have fun!");
|
||||||
|
self.debugger = Some(debugger)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("failed to start the debugger: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn is_debugger_attached(&self) -> bool {
|
||||||
|
self.debugger.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recv & handle messages from the debugger, and return if we are stopped or not
|
||||||
|
pub fn debugger_run(&mut self) {
|
||||||
|
let mut should_stop = false;
|
||||||
|
let debugger = self.debugger.take().expect("debugger should be None here");
|
||||||
|
let debugger = debugger
|
||||||
|
.handle_message(self, &mut should_stop)
|
||||||
|
.map_err(|_| "Failed to handle message")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
self.debugger = debugger;
|
||||||
|
|
||||||
|
if let Some(debugger) = &mut self.debugger {
|
||||||
|
if should_stop {
|
||||||
|
debugger.notify_stop_reason(SingleThreadStopReason::DoneStep);
|
||||||
|
} else {
|
||||||
|
self.frame_interruptible();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error!("debugger was disconnected!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -237,9 +295,9 @@ impl GameBoyAdvance {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Runs the emulation for a given amount of cycles
|
/// Runs the emulation for a given amount of cycles
|
||||||
/// @return number of extra cycle ran in this iteration
|
/// @return number of cycle actually ran
|
||||||
#[inline]
|
#[inline]
|
||||||
fn run(&mut self, cycles_to_run: usize) -> usize {
|
pub(super) fn run<const CHECK_BREAKPOINTS: bool>(&mut self, cycles_to_run: usize) -> usize {
|
||||||
let run_start_time = self.scheduler.timestamp();
|
let run_start_time = self.scheduler.timestamp();
|
||||||
|
|
||||||
// Register an event to mark the end of this run
|
// Register an event to mark the end of this run
|
||||||
|
@ -251,14 +309,31 @@ impl GameBoyAdvance {
|
||||||
// The tricky part is to avoid unnecessary calls for Scheduler::process_pending,
|
// The tricky part is to avoid unnecessary calls for Scheduler::process_pending,
|
||||||
// performance-wise it would be best to run as many cycles as fast as possible while we know there are no pending events.
|
// performance-wise it would be best to run as many cycles as fast as possible while we know there are no pending events.
|
||||||
// Fast forward emulation until an event occurs
|
// Fast forward emulation until an event occurs
|
||||||
while self.scheduler.timestamp() <= self.scheduler.timestamp_of_next_event() {
|
'run_unitl_next_event: while self.scheduler.timestamp()
|
||||||
|
<= self.scheduler.timestamp_of_next_event()
|
||||||
|
{
|
||||||
// 3 Options:
|
// 3 Options:
|
||||||
// 1. DMA is active - thus CPU is blocked
|
// 1. DMA is active - thus CPU is blocked
|
||||||
// 2. DMA inactive and halt state is RUN - CPU can run
|
// 2. DMA inactive and halt state is RUN - CPU can run
|
||||||
// 3. DMA inactive and halt state is HALT - CPU is blocked
|
// 3. DMA inactive and halt state is HALT - CPU is blocked
|
||||||
match self.get_bus_master() {
|
match self.get_bus_master() {
|
||||||
Some(BusMaster::Dma) => self.dma_step(),
|
Some(BusMaster::Dma) => self.dma_step(),
|
||||||
Some(BusMaster::Cpu) => self.cpu_step(),
|
Some(BusMaster::Cpu) => {
|
||||||
|
self.cpu_step();
|
||||||
|
if CHECK_BREAKPOINTS {
|
||||||
|
if let Some(bp) = self.cpu.check_breakpoint() {
|
||||||
|
debug!("Arm7tdmi breakpoint hit 0x{:08x}", bp);
|
||||||
|
self.scheduler.cancel_pending(EventType::RunLimitReached);
|
||||||
|
running = false;
|
||||||
|
|
||||||
|
if let Some(debugger) = &mut self.debugger {
|
||||||
|
debugger.notify_breakpoint(bp);
|
||||||
|
}
|
||||||
|
|
||||||
|
break 'run_unitl_next_event;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
None => {
|
None => {
|
||||||
if self.io_devs.intc.irq_pending() {
|
if self.io_devs.intc.irq_pending() {
|
||||||
self.io_devs.haltcnt = HaltState::Running;
|
self.io_devs.haltcnt = HaltState::Running;
|
||||||
|
@ -273,8 +348,7 @@ impl GameBoyAdvance {
|
||||||
self.handle_events(&mut running);
|
self.handle_events(&mut running);
|
||||||
}
|
}
|
||||||
|
|
||||||
let total_cycles_ran = self.scheduler.timestamp() - run_start_time;
|
self.scheduler.timestamp() - run_start_time
|
||||||
total_cycles_ran - cycles_to_run
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_events(&mut self, run_limit_flag: &mut bool) {
|
fn handle_events(&mut self, run_limit_flag: &mut bool) {
|
||||||
|
|
168
core/src/gdb_support.rs
Normal file
168
core/src/gdb_support.rs
Normal file
|
@ -0,0 +1,168 @@
|
||||||
|
use std::result;
|
||||||
|
use std::sync::{Arc, Condvar, Mutex};
|
||||||
|
use std::thread::JoinHandle;
|
||||||
|
|
||||||
|
type SendSync<T> = Arc<Mutex<T>>;
|
||||||
|
|
||||||
|
use arm7tdmi::gdbstub::stub::{DisconnectReason, SingleThreadStopReason};
|
||||||
|
use arm7tdmi::gdbstub::target::TargetError;
|
||||||
|
use arm7tdmi::gdbstub::target::{ext::base::singlethread::SingleThreadBase, Target};
|
||||||
|
use arm7tdmi::gdbstub_arch::arm::reg::ArmCoreRegs;
|
||||||
|
use arm7tdmi::memory::Addr;
|
||||||
|
use crossbeam::channel::{Receiver, Sender};
|
||||||
|
|
||||||
|
// mod target;
|
||||||
|
mod event_loop;
|
||||||
|
pub(crate) mod gdb_thread;
|
||||||
|
mod memory_map;
|
||||||
|
mod target;
|
||||||
|
|
||||||
|
use crate::GameBoyAdvance;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) enum DebuggerMessage {
|
||||||
|
ReadRegs(SendSync<ArmCoreRegs>),
|
||||||
|
WriteRegs(ArmCoreRegs),
|
||||||
|
ReadAddrs(Addr, SendSync<Box<[u8]>>),
|
||||||
|
#[allow(unused)]
|
||||||
|
WriteAddrs(Addr, Box<[u8]>),
|
||||||
|
AddSwBreakpoint(Addr),
|
||||||
|
DelSwBreakpoint(Addr),
|
||||||
|
Stop,
|
||||||
|
Resume,
|
||||||
|
SingleStep,
|
||||||
|
Disconnected(DisconnectReason),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DebuggerTarget {
|
||||||
|
tx: Sender<DebuggerMessage>,
|
||||||
|
operation_signal: Arc<(Mutex<bool>, Condvar)>,
|
||||||
|
stop_signal: Arc<(Mutex<SingleThreadStopReason<u32>>, Condvar)>,
|
||||||
|
memory_map: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DebuggerTarget {
|
||||||
|
#[inline]
|
||||||
|
pub fn wait_for_operation(&mut self) {
|
||||||
|
let (lock, cvar) = &*self.operation_signal;
|
||||||
|
let mut finished = lock.lock().unwrap();
|
||||||
|
while !*finished {
|
||||||
|
finished = cvar.wait(finished).unwrap();
|
||||||
|
}
|
||||||
|
*finished = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct DebuggerState {
|
||||||
|
rx: Receiver<DebuggerMessage>,
|
||||||
|
operation_signal: Arc<(Mutex<bool>, Condvar)>,
|
||||||
|
stop_signal: Arc<(Mutex<SingleThreadStopReason<u32>>, Condvar)>,
|
||||||
|
thread: JoinHandle<()>,
|
||||||
|
stopped: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DebuggerState {
|
||||||
|
pub fn handle_message(
|
||||||
|
mut self,
|
||||||
|
gba: &mut GameBoyAdvance,
|
||||||
|
should_stop: &mut bool,
|
||||||
|
) -> Result<Option<DebuggerState>, TargetError<<DebuggerTarget as Target>::Error>> {
|
||||||
|
if self.thread.is_finished() {
|
||||||
|
warn!("gdb server thread unexpectdly died");
|
||||||
|
*should_stop = true;
|
||||||
|
self.thread.join().unwrap();
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
if let Ok(msg) = self.rx.try_recv() {
|
||||||
|
use DebuggerMessage::*;
|
||||||
|
let mut result = match msg {
|
||||||
|
ReadRegs(regs) => {
|
||||||
|
let mut regs = regs.lock().unwrap();
|
||||||
|
gba.cpu.read_registers(&mut regs)?;
|
||||||
|
debug!("Debugger requested to read regs: {:?}", regs);
|
||||||
|
Ok(Some(self))
|
||||||
|
}
|
||||||
|
WriteRegs(regs) => {
|
||||||
|
debug!("Debugger requested to write regs: {:?}", regs);
|
||||||
|
gba.cpu.write_registers(®s)?;
|
||||||
|
Ok(Some(self))
|
||||||
|
}
|
||||||
|
ReadAddrs(addr, data) => {
|
||||||
|
let mut data = data.lock().unwrap();
|
||||||
|
debug!(
|
||||||
|
"Debugger requested to read {} bytes from 0x{:08x}",
|
||||||
|
data.len(),
|
||||||
|
addr
|
||||||
|
);
|
||||||
|
gba.cpu.read_addrs(addr, &mut data)?;
|
||||||
|
Ok(Some(self))
|
||||||
|
}
|
||||||
|
WriteAddrs(addr, data) => {
|
||||||
|
debug!(
|
||||||
|
"Debugger requested to write {} bytes at 0x{:08x}",
|
||||||
|
data.len(),
|
||||||
|
addr
|
||||||
|
);
|
||||||
|
gba.cpu.write_addrs(addr, &data)?;
|
||||||
|
Ok(Some(self))
|
||||||
|
}
|
||||||
|
Stop => {
|
||||||
|
debug!("Debugger requested stopped");
|
||||||
|
self.stopped = true;
|
||||||
|
Ok(Some(self))
|
||||||
|
}
|
||||||
|
Resume => {
|
||||||
|
debug!("Debugger requested resume");
|
||||||
|
self.stopped = false;
|
||||||
|
Ok(Some(self))
|
||||||
|
}
|
||||||
|
SingleStep => {
|
||||||
|
debug!("Debugger requested single step");
|
||||||
|
gba.run::<true>(1);
|
||||||
|
self.notify_stop_reason(SingleThreadStopReason::DoneStep);
|
||||||
|
self.stopped = true;
|
||||||
|
Ok(Some(self))
|
||||||
|
}
|
||||||
|
AddSwBreakpoint(addr) => {
|
||||||
|
gba.cpu.add_breakpoint(addr);
|
||||||
|
Ok(Some(self))
|
||||||
|
}
|
||||||
|
DelSwBreakpoint(addr) => {
|
||||||
|
gba.cpu.del_breakpoint(addr);
|
||||||
|
Ok(Some(self))
|
||||||
|
}
|
||||||
|
Disconnected(reason) => {
|
||||||
|
debug!("Debugger disconnected due to {:?}", reason);
|
||||||
|
debug!("closing gdbserver thread");
|
||||||
|
self.thread.join().unwrap();
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Ok(Some(result)) = &mut result {
|
||||||
|
let (lock, cvar) = &*result.operation_signal;
|
||||||
|
let mut finished = lock.lock().unwrap();
|
||||||
|
*finished = true;
|
||||||
|
cvar.notify_one();
|
||||||
|
*should_stop = result.stopped;
|
||||||
|
} else {
|
||||||
|
*should_stop = true;
|
||||||
|
}
|
||||||
|
result
|
||||||
|
} else {
|
||||||
|
*should_stop = self.stopped;
|
||||||
|
Ok(Some(self))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn notify_stop_reason(&mut self, reason: SingleThreadStopReason<u32>) {
|
||||||
|
self.stopped = true;
|
||||||
|
let (lock, cvar) = &*self.stop_signal;
|
||||||
|
let mut stop_reason = lock.lock().unwrap();
|
||||||
|
*stop_reason = reason;
|
||||||
|
cvar.notify_one();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn notify_breakpoint(&mut self, _bp: Addr) {
|
||||||
|
self.notify_stop_reason(SingleThreadStopReason::SwBreak(()));
|
||||||
|
}
|
||||||
|
}
|
65
core/src/gdb_support/event_loop.rs
Normal file
65
core/src/gdb_support/event_loop.rs
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use arm7tdmi::gdb::gdbstub::{
|
||||||
|
conn::{Connection, ConnectionExt},
|
||||||
|
stub::{run_blocking, SingleThreadStopReason},
|
||||||
|
target::Target,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{DebuggerMessage, DebuggerTarget};
|
||||||
|
|
||||||
|
pub struct DebuggerEventLoop {}
|
||||||
|
|
||||||
|
impl run_blocking::BlockingEventLoop for DebuggerEventLoop {
|
||||||
|
type Target = DebuggerTarget;
|
||||||
|
type Connection = Box<dyn ConnectionExt<Error = std::io::Error>>;
|
||||||
|
type StopReason = SingleThreadStopReason<u32>;
|
||||||
|
|
||||||
|
fn wait_for_stop_reason(
|
||||||
|
target: &mut Self::Target,
|
||||||
|
conn: &mut Self::Connection,
|
||||||
|
) -> Result<
|
||||||
|
run_blocking::Event<SingleThreadStopReason<u32>>,
|
||||||
|
run_blocking::WaitForStopReasonError<
|
||||||
|
<Self::Target as Target>::Error,
|
||||||
|
<Self::Connection as Connection>::Error,
|
||||||
|
>,
|
||||||
|
> {
|
||||||
|
let mut poll_incoming_data = || conn.peek().map(|b| b.is_some()).unwrap_or(true);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if poll_incoming_data() {
|
||||||
|
let byte = conn
|
||||||
|
.read()
|
||||||
|
.map_err(run_blocking::WaitForStopReasonError::Connection)?;
|
||||||
|
return Ok(run_blocking::Event::IncomingData(byte));
|
||||||
|
} else {
|
||||||
|
// try and wait for the stop reason
|
||||||
|
let (lock, cvar) = &*target.stop_signal;
|
||||||
|
let stop_reason = lock.lock().unwrap();
|
||||||
|
let (stop_reason, timeout_result) = cvar
|
||||||
|
.wait_timeout(stop_reason, Duration::from_millis(10))
|
||||||
|
.unwrap();
|
||||||
|
if timeout_result.timed_out() {
|
||||||
|
// timed-out, try again later
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
info!("Target stopped due to {:?}!", stop_reason);
|
||||||
|
return Ok(run_blocking::Event::TargetStopped(*stop_reason));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_interrupt(
|
||||||
|
target: &mut DebuggerTarget,
|
||||||
|
) -> Result<Option<SingleThreadStopReason<u32>>, <DebuggerTarget as Target>::Error> {
|
||||||
|
info!("on_interrupt: sending stop message");
|
||||||
|
target.tx.send(DebuggerMessage::Stop).unwrap();
|
||||||
|
target.wait_for_operation();
|
||||||
|
info!("Waiting for target to stop <blocking>");
|
||||||
|
let (lock, cvar) = &*target.stop_signal;
|
||||||
|
let stop_signal = lock.lock().unwrap();
|
||||||
|
let stop_signal = cvar.wait(stop_signal).unwrap();
|
||||||
|
Ok(Some(*stop_signal))
|
||||||
|
}
|
||||||
|
}
|
63
core/src/gdb_support/gdb_thread.rs
Normal file
63
core/src/gdb_support/gdb_thread.rs
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
use std::sync::{Arc, Condvar, Mutex};
|
||||||
|
|
||||||
|
use arm7tdmi::{
|
||||||
|
gdb::wait_for_connection,
|
||||||
|
gdbstub::{
|
||||||
|
common::Signal,
|
||||||
|
conn::ConnectionExt,
|
||||||
|
stub::{GdbStub, SingleThreadStopReason},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{GBAError, GameBoyAdvance};
|
||||||
|
|
||||||
|
use super::{event_loop::DebuggerEventLoop, DebuggerMessage, DebuggerState, DebuggerTarget};
|
||||||
|
|
||||||
|
/// Starts a gdbserver thread
|
||||||
|
pub(crate) fn start_gdb_server_thread(
|
||||||
|
gba: &mut GameBoyAdvance,
|
||||||
|
port: u16,
|
||||||
|
) -> Result<DebuggerState, GBAError> {
|
||||||
|
let (tx, rx) = crossbeam::channel::unbounded();
|
||||||
|
let operation_signal = Arc::new((Mutex::new(false), Condvar::new()));
|
||||||
|
let stop_signal = Arc::new((
|
||||||
|
Mutex::new(SingleThreadStopReason::Signal(Signal::SIGINT)),
|
||||||
|
Condvar::new(),
|
||||||
|
));
|
||||||
|
let stop_signal_2 = stop_signal.clone();
|
||||||
|
let operation_signal_2 = operation_signal.clone();
|
||||||
|
let memory_map = gba.sysbus.generate_memory_map_xml().unwrap();
|
||||||
|
|
||||||
|
let conn = wait_for_connection(port)?;
|
||||||
|
let thread = std::thread::spawn(move || {
|
||||||
|
debug!("starting GDB Server thread");
|
||||||
|
let conn: Box<dyn ConnectionExt<Error = std::io::Error>> = Box::new(conn);
|
||||||
|
|
||||||
|
let mut target = DebuggerTarget {
|
||||||
|
tx,
|
||||||
|
operation_signal: operation_signal_2,
|
||||||
|
stop_signal: stop_signal_2,
|
||||||
|
memory_map,
|
||||||
|
};
|
||||||
|
let gdbserver = GdbStub::new(conn);
|
||||||
|
let disconnect_reason = gdbserver
|
||||||
|
.run_blocking::<DebuggerEventLoop>(&mut target)
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
.unwrap();
|
||||||
|
target
|
||||||
|
.tx
|
||||||
|
.send(DebuggerMessage::Disconnected(disconnect_reason))
|
||||||
|
.unwrap();
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut debugger = DebuggerState {
|
||||||
|
rx,
|
||||||
|
operation_signal,
|
||||||
|
stop_signal,
|
||||||
|
thread,
|
||||||
|
stopped: true,
|
||||||
|
};
|
||||||
|
debugger.notify_stop_reason(SingleThreadStopReason::Signal(Signal::SIGINT));
|
||||||
|
|
||||||
|
Ok(debugger)
|
||||||
|
}
|
56
core/src/gdb_support/memory_map.rs
Normal file
56
core/src/gdb_support/memory_map.rs
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
use xml_builder::{XMLBuilder, XMLElement, XMLVersion};
|
||||||
|
|
||||||
|
use arm7tdmi::{
|
||||||
|
gdb::{copy_range_to_buf, target::MemoryGdbInterface},
|
||||||
|
memory::Addr,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::sysbus::{consts, SysBus};
|
||||||
|
|
||||||
|
impl SysBus {
|
||||||
|
pub fn generate_memory_map_xml(&self) -> Result<String, Box<dyn std::error::Error>> {
|
||||||
|
let mut xml = XMLBuilder::new()
|
||||||
|
.version(XMLVersion::XML1_1)
|
||||||
|
.encoding("UTF-8".into())
|
||||||
|
.build();
|
||||||
|
let mut memory_map = XMLElement::new("memory-map");
|
||||||
|
|
||||||
|
let mut add_memory = |start: Addr, length: usize| -> Result<(), String> {
|
||||||
|
let mut memory = XMLElement::new("memory");
|
||||||
|
memory.add_attribute("type", "ram"); // using "ram" for everything to allow use of sw-breakpoints
|
||||||
|
memory.add_attribute("start", &start.to_string());
|
||||||
|
memory.add_attribute("length", &length.to_string());
|
||||||
|
memory_map
|
||||||
|
.add_child(memory)
|
||||||
|
.map_err(|e| format!("failed to add child: {:?}", e))?;
|
||||||
|
Ok(())
|
||||||
|
};
|
||||||
|
|
||||||
|
add_memory(consts::BIOS_ADDR, self.bios.len())?;
|
||||||
|
add_memory(consts::EWRAM_ADDR, self.ewram.len())?;
|
||||||
|
add_memory(consts::IWRAM_ADDR, self.iwram.len())?;
|
||||||
|
add_memory(consts::IOMEM_ADDR, 0x400)?;
|
||||||
|
add_memory(consts::PALRAM_ADDR, self.io.gpu.palette_ram.len())?;
|
||||||
|
add_memory(consts::VRAM_ADDR, self.io.gpu.vram.len())?;
|
||||||
|
add_memory(consts::OAM_ADDR, self.io.gpu.oam.len())?;
|
||||||
|
add_memory(consts::CART_BASE, self.cartridge.get_rom_bytes().len())?;
|
||||||
|
|
||||||
|
xml.set_root_element(memory_map);
|
||||||
|
let mut writer = Vec::new();
|
||||||
|
xml.generate(&mut writer)
|
||||||
|
.map_err(|e| format!("failed to generate xml: {:?}", e))?;
|
||||||
|
|
||||||
|
Ok(String::from_utf8(writer)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MemoryGdbInterface for SysBus {
|
||||||
|
fn memory_map_xml(&self, offset: u64, length: usize, buf: &mut [u8]) -> usize {
|
||||||
|
copy_range_to_buf(
|
||||||
|
self.generate_memory_map_xml().unwrap().as_bytes(),
|
||||||
|
offset,
|
||||||
|
length,
|
||||||
|
buf,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
155
core/src/gdb_support/target.rs
Normal file
155
core/src/gdb_support/target.rs
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
/// Implementing the Target trait for gdbstub
|
||||||
|
use arm7tdmi::gdb::{copy_range_to_buf, gdbstub, gdbstub_arch};
|
||||||
|
use gdbstub::common::Signal;
|
||||||
|
use gdbstub::target::ext::base::singlethread::{
|
||||||
|
SingleThreadBase, SingleThreadResume, SingleThreadSingleStep,
|
||||||
|
};
|
||||||
|
use gdbstub::target::ext::base::singlethread::{SingleThreadResumeOps, SingleThreadSingleStepOps};
|
||||||
|
use gdbstub::target::ext::base::BaseOps;
|
||||||
|
use gdbstub::target::ext::breakpoints::BreakpointsOps;
|
||||||
|
use gdbstub::target::{self, Target, TargetError, TargetResult};
|
||||||
|
use gdbstub_arch::arm::reg::ArmCoreRegs;
|
||||||
|
|
||||||
|
use super::{DebuggerMessage, DebuggerTarget};
|
||||||
|
|
||||||
|
impl Target for DebuggerTarget {
|
||||||
|
type Error = ();
|
||||||
|
type Arch = gdbstub_arch::arm::Armv4t; // as an example
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn base_ops(&mut self) -> BaseOps<Self::Arch, Self::Error> {
|
||||||
|
BaseOps::SingleThread(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
// opt-in to support for setting/removing breakpoints
|
||||||
|
#[inline(always)]
|
||||||
|
fn support_breakpoints(&mut self) -> Option<BreakpointsOps<Self>> {
|
||||||
|
Some(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn support_memory_map(&mut self) -> Option<target::ext::memory_map::MemoryMapOps<Self>> {
|
||||||
|
Some(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SingleThreadBase for DebuggerTarget {
|
||||||
|
fn read_registers(&mut self, regs: &mut ArmCoreRegs) -> TargetResult<(), Self> {
|
||||||
|
let regs_copy = Arc::new(Mutex::new(ArmCoreRegs::default()));
|
||||||
|
self.tx
|
||||||
|
.send(DebuggerMessage::ReadRegs(regs_copy.clone()))
|
||||||
|
.unwrap();
|
||||||
|
self.wait_for_operation();
|
||||||
|
regs_copy.lock().unwrap().clone_into(regs);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_registers(&mut self, regs: &ArmCoreRegs) -> TargetResult<(), Self> {
|
||||||
|
self.tx
|
||||||
|
.send(DebuggerMessage::WriteRegs(regs.clone()))
|
||||||
|
.unwrap();
|
||||||
|
self.wait_for_operation();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_addrs(&mut self, start_addr: u32, data: &mut [u8]) -> TargetResult<(), Self> {
|
||||||
|
let buffer = Arc::new(Mutex::new(vec![0; data.len()].into_boxed_slice()));
|
||||||
|
self.tx
|
||||||
|
.send(DebuggerMessage::ReadAddrs(start_addr, buffer.clone()))
|
||||||
|
.unwrap();
|
||||||
|
self.wait_for_operation();
|
||||||
|
data.copy_from_slice(&buffer.lock().unwrap());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_addrs(&mut self, _start_addr: u32, _data: &[u8]) -> TargetResult<(), Self> {
|
||||||
|
// todo!("implement DebugWrite bus extention")
|
||||||
|
Err(TargetError::NonFatal)
|
||||||
|
}
|
||||||
|
|
||||||
|
// most targets will want to support at resumption as well...
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn support_resume(&mut self) -> Option<SingleThreadResumeOps<Self>> {
|
||||||
|
Some(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SingleThreadResume for DebuggerTarget {
|
||||||
|
fn resume(&mut self, _signal: Option<Signal>) -> Result<(), Self::Error> {
|
||||||
|
self.tx.send(DebuggerMessage::Resume).unwrap();
|
||||||
|
self.wait_for_operation();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...and if the target supports resumption, it'll likely want to support
|
||||||
|
// single-step resume as well
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn support_single_step(&mut self) -> Option<SingleThreadSingleStepOps<'_, Self>> {
|
||||||
|
Some(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SingleThreadSingleStep for DebuggerTarget {
|
||||||
|
fn step(&mut self, _signal: Option<Signal>) -> Result<(), Self::Error> {
|
||||||
|
self.tx.send(DebuggerMessage::SingleStep).unwrap();
|
||||||
|
self.wait_for_operation();
|
||||||
|
self.tx.send(DebuggerMessage::Stop).unwrap();
|
||||||
|
self.wait_for_operation();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl target::ext::memory_map::MemoryMap for DebuggerTarget {
|
||||||
|
fn memory_map_xml(
|
||||||
|
&self,
|
||||||
|
offset: u64,
|
||||||
|
length: usize,
|
||||||
|
buf: &mut [u8],
|
||||||
|
) -> TargetResult<usize, Self> {
|
||||||
|
Ok(copy_range_to_buf(
|
||||||
|
self.memory_map.as_bytes(),
|
||||||
|
offset,
|
||||||
|
length,
|
||||||
|
buf,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl target::ext::breakpoints::Breakpoints for DebuggerTarget {
|
||||||
|
// there are several kinds of breakpoints - this target uses software breakpoints
|
||||||
|
#[inline(always)]
|
||||||
|
fn support_sw_breakpoint(
|
||||||
|
&mut self,
|
||||||
|
) -> Option<target::ext::breakpoints::SwBreakpointOps<'_, Self>> {
|
||||||
|
Some(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl target::ext::breakpoints::SwBreakpoint for DebuggerTarget {
|
||||||
|
fn add_sw_breakpoint(
|
||||||
|
&mut self,
|
||||||
|
addr: u32,
|
||||||
|
_kind: gdbstub_arch::arm::ArmBreakpointKind,
|
||||||
|
) -> TargetResult<bool, Self> {
|
||||||
|
self.tx
|
||||||
|
.send(DebuggerMessage::AddSwBreakpoint(addr))
|
||||||
|
.unwrap();
|
||||||
|
self.wait_for_operation();
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_sw_breakpoint(
|
||||||
|
&mut self,
|
||||||
|
addr: u32,
|
||||||
|
_kind: gdbstub_arch::arm::ArmBreakpointKind,
|
||||||
|
) -> TargetResult<bool, Self> {
|
||||||
|
self.tx
|
||||||
|
.send(DebuggerMessage::DelSwBreakpoint(addr))
|
||||||
|
.unwrap();
|
||||||
|
self.wait_for_operation();
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
}
|
|
@ -145,7 +145,7 @@ impl BusIO for IoDevices {
|
||||||
|
|
||||||
REG_POSTFLG => io.post_boot_flag as u16,
|
REG_POSTFLG => io.post_boot_flag as u16,
|
||||||
REG_HALTCNT => 0,
|
REG_HALTCNT => 0,
|
||||||
REG_KEYINPUT => io.keyinput as u16,
|
REG_KEYINPUT => io.keyinput,
|
||||||
|
|
||||||
x if DebugPort::is_debug_access(x) => io.debug.read(io_addr),
|
x if DebugPort::is_debug_access(x) => io.debug.read(io_addr),
|
||||||
|
|
||||||
|
|
|
@ -45,11 +45,14 @@ pub use interrupt::SharedInterruptFlags;
|
||||||
pub mod gba;
|
pub mod gba;
|
||||||
pub use gba::GameBoyAdvance;
|
pub use gba::GameBoyAdvance;
|
||||||
pub mod dma;
|
pub mod dma;
|
||||||
|
pub mod gdb_support;
|
||||||
pub mod keypad;
|
pub mod keypad;
|
||||||
mod mgba_debug;
|
mod mgba_debug;
|
||||||
pub(crate) mod overrides;
|
pub(crate) mod overrides;
|
||||||
pub mod timer;
|
pub mod timer;
|
||||||
|
|
||||||
|
use arm7tdmi::gdb::gdbstub::stub::GdbStubError;
|
||||||
|
|
||||||
#[cfg(feature = "debugger")]
|
#[cfg(feature = "debugger")]
|
||||||
pub mod debugger;
|
pub mod debugger;
|
||||||
|
|
||||||
|
@ -59,6 +62,7 @@ pub enum GBAError {
|
||||||
CartridgeLoadError(String),
|
CartridgeLoadError(String),
|
||||||
#[cfg(feature = "debugger")]
|
#[cfg(feature = "debugger")]
|
||||||
DebuggerError(debugger::DebuggerError),
|
DebuggerError(debugger::DebuggerError),
|
||||||
|
GdbError(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for GBAError {
|
impl fmt::Display for GBAError {
|
||||||
|
@ -94,6 +98,12 @@ impl From<zip::result::ZipError> for GBAError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<GdbStubError<(), std::io::Error>> for GBAError {
|
||||||
|
fn from(err: GdbStubError<(), std::io::Error>) -> Self {
|
||||||
|
GBAError::GdbError(err.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub mod prelude {
|
pub mod prelude {
|
||||||
pub use super::cartridge::{Cartridge, GamepakBuilder};
|
pub use super::cartridge::{Cartridge, GamepakBuilder};
|
||||||
#[cfg(feature = "debugger")]
|
#[cfg(feature = "debugger")]
|
||||||
|
|
|
@ -22,7 +22,8 @@ pub mod consts {
|
||||||
pub const PALRAM_ADDR: u32 = 0x0500_0000;
|
pub const PALRAM_ADDR: u32 = 0x0500_0000;
|
||||||
pub const VRAM_ADDR: u32 = 0x0600_0000;
|
pub const VRAM_ADDR: u32 = 0x0600_0000;
|
||||||
pub const OAM_ADDR: u32 = 0x0700_0000;
|
pub const OAM_ADDR: u32 = 0x0700_0000;
|
||||||
pub const GAMEPAK_WS0_LO: u32 = 0x0800_0000;
|
pub const CART_BASE: u32 = 0x0800_0000;
|
||||||
|
pub const GAMEPAK_WS0_LO: u32 = CART_BASE;
|
||||||
pub const GAMEPAK_WS0_HI: u32 = 0x0900_0000;
|
pub const GAMEPAK_WS0_HI: u32 = 0x0900_0000;
|
||||||
pub const GAMEPAK_WS1_LO: u32 = 0x0A00_0000;
|
pub const GAMEPAK_WS1_LO: u32 = 0x0A00_0000;
|
||||||
pub const GAMEPAK_WS1_HI: u32 = 0x0B00_0000;
|
pub const GAMEPAK_WS1_HI: u32 = 0x0B00_0000;
|
||||||
|
@ -148,9 +149,9 @@ pub struct SysBus {
|
||||||
scheduler: Shared<Scheduler>,
|
scheduler: Shared<Scheduler>,
|
||||||
arm_core: WeakPointer<Arm7tdmiCore<SysBus>>,
|
arm_core: WeakPointer<Arm7tdmiCore<SysBus>>,
|
||||||
|
|
||||||
bios: Bios,
|
pub(crate) bios: Bios,
|
||||||
ewram: Box<[u8]>,
|
pub(crate) ewram: Box<[u8]>,
|
||||||
iwram: Box<[u8]>,
|
pub(crate) iwram: Box<[u8]>,
|
||||||
pub cartridge: Cartridge,
|
pub cartridge: Cartridge,
|
||||||
|
|
||||||
cycle_luts: CycleLookupTables,
|
cycle_luts: CycleLookupTables,
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
use rustboyadvance_core::GameBoyAdvance;
|
|
||||||
use sdl2::controller::Axis;
|
use sdl2::controller::Axis;
|
||||||
use sdl2::controller::Button;
|
use sdl2::controller::Button;
|
||||||
use sdl2::keyboard::Scancode;
|
use sdl2::keyboard::Scancode;
|
||||||
|
@ -8,31 +7,31 @@ use rustboyadvance_core::keypad as gba_keypad;
|
||||||
use bit;
|
use bit;
|
||||||
use bit::BitIndex;
|
use bit::BitIndex;
|
||||||
|
|
||||||
pub fn on_keyboard_key_down(gba: &mut GameBoyAdvance, scancode: Scancode) {
|
pub fn on_keyboard_key_down(key_state: &mut u16, scancode: Scancode) {
|
||||||
if let Some(key) = scancode_to_keypad(scancode) {
|
if let Some(key) = scancode_to_keypad(scancode) {
|
||||||
gba.get_key_state_mut().set_bit(key as usize, false);
|
key_state.set_bit(key as usize, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_keyboard_key_up(gba: &mut GameBoyAdvance, scancode: Scancode) {
|
pub fn on_keyboard_key_up(key_state: &mut u16, scancode: Scancode) {
|
||||||
if let Some(key) = scancode_to_keypad(scancode) {
|
if let Some(key) = scancode_to_keypad(scancode) {
|
||||||
gba.get_key_state_mut().set_bit(key as usize, true);
|
key_state.set_bit(key as usize, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_controller_button_down(gba: &mut GameBoyAdvance, button: Button) {
|
pub fn on_controller_button_down(key_state: &mut u16, button: Button) {
|
||||||
if let Some(key) = controller_button_to_keypad(button) {
|
if let Some(key) = controller_button_to_keypad(button) {
|
||||||
gba.get_key_state_mut().set_bit(key as usize, false);
|
key_state.set_bit(key as usize, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_controller_button_up(gba: &mut GameBoyAdvance, button: Button) {
|
pub fn on_controller_button_up(key_state: &mut u16, button: Button) {
|
||||||
if let Some(key) = controller_button_to_keypad(button) {
|
if let Some(key) = controller_button_to_keypad(button) {
|
||||||
gba.get_key_state_mut().set_bit(key as usize, true);
|
key_state.set_bit(key as usize, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_axis_motion(gba: &mut GameBoyAdvance, axis: Axis, val: i16) {
|
pub fn on_axis_motion(key_state: &mut u16, axis: Axis, val: i16) {
|
||||||
use gba_keypad::Keys as GbaKeys;
|
use gba_keypad::Keys as GbaKeys;
|
||||||
let keys = match axis {
|
let keys = match axis {
|
||||||
Axis::LeftX => (GbaKeys::Left, GbaKeys::Right),
|
Axis::LeftX => (GbaKeys::Left, GbaKeys::Right),
|
||||||
|
@ -44,7 +43,6 @@ pub fn on_axis_motion(gba: &mut GameBoyAdvance, axis: Axis, val: i16) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let key_state = gba.get_key_state_mut();
|
|
||||||
// Axis motion is an absolute value in the range
|
// Axis motion is an absolute value in the range
|
||||||
// [-32768, 32767]. Let's simulate a very rough dead
|
// [-32768, 32767]. Let's simulate a very rough dead
|
||||||
// zone to ignore spurious events.
|
// zone to ignore spurious events.
|
||||||
|
|
|
@ -27,7 +27,7 @@ use rustboyadvance_core::prelude::*;
|
||||||
use rustboyadvance_utils::FpsCounter;
|
use rustboyadvance_utils::FpsCounter;
|
||||||
|
|
||||||
const LOG_DIR: &str = ".logs";
|
const LOG_DIR: &str = ".logs";
|
||||||
const DEFAULT_GDB_SERVER_ADDR: &'static str = "localhost:1337";
|
const DEFAULT_GDB_SERVER_PORT: u16 = 1337;
|
||||||
|
|
||||||
fn ask_download_bios() {
|
fn ask_download_bios() {
|
||||||
const OPEN_SOURCE_BIOS_URL: &'static str =
|
const OPEN_SOURCE_BIOS_URL: &'static str =
|
||||||
|
@ -109,7 +109,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let mut event_pump = sdl_context.event_pump()?;
|
let mut event_pump = sdl_context.event_pump()?;
|
||||||
'running: loop {
|
'running: loop {
|
||||||
let start_time = time::Instant::now();
|
let start_time = time::Instant::now();
|
||||||
|
|
||||||
for event in event_pump.poll_iter() {
|
for event in event_pump.poll_iter() {
|
||||||
match event {
|
match event {
|
||||||
Event::KeyDown {
|
Event::KeyDown {
|
||||||
|
@ -117,7 +116,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
..
|
..
|
||||||
} => match scancode {
|
} => match scancode {
|
||||||
Scancode::Space => vsync = false,
|
Scancode::Space => vsync = false,
|
||||||
k => input::on_keyboard_key_down(&mut gba, k),
|
k => input::on_keyboard_key_down(gba.get_key_state_mut(), k),
|
||||||
},
|
},
|
||||||
Event::KeyUp {
|
Event::KeyUp {
|
||||||
scancode: Some(scancode),
|
scancode: Some(scancode),
|
||||||
|
@ -132,8 +131,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
info!("ending debugger...")
|
info!("ending debugger...")
|
||||||
}
|
}
|
||||||
#[cfg(feature = "gdb")]
|
Scancode::F2 => gba.start_gdbserver(DEFAULT_GDB_SERVER_PORT),
|
||||||
Scancode::F2 => todo!("gdb"),
|
|
||||||
Scancode::F5 => {
|
Scancode::F5 => {
|
||||||
info!("Saving state ...");
|
info!("Saving state ...");
|
||||||
let save = gba.save_state()?;
|
let save = gba.save_state()?;
|
||||||
|
@ -155,17 +153,17 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Scancode::Space => vsync = true,
|
Scancode::Space => vsync = true,
|
||||||
k => input::on_keyboard_key_up(&mut gba, k),
|
k => input::on_keyboard_key_up(gba.get_key_state_mut(), k),
|
||||||
},
|
},
|
||||||
Event::ControllerButtonDown { button, .. } => match button {
|
Event::ControllerButtonDown { button, .. } => match button {
|
||||||
Button::RightStick => vsync = !vsync,
|
Button::RightStick => vsync = !vsync,
|
||||||
b => input::on_controller_button_down(&mut gba, b),
|
b => input::on_controller_button_down(gba.get_key_state_mut(), b),
|
||||||
},
|
},
|
||||||
Event::ControllerButtonUp { button, .. } => {
|
Event::ControllerButtonUp { button, .. } => {
|
||||||
input::on_controller_button_up(&mut gba, button);
|
input::on_controller_button_up(gba.get_key_state_mut(), button);
|
||||||
}
|
}
|
||||||
Event::ControllerAxisMotion { axis, value, .. } => {
|
Event::ControllerAxisMotion { axis, value, .. } => {
|
||||||
input::on_axis_motion(&mut gba, axis, value);
|
input::on_axis_motion(gba.get_key_state_mut(), axis, value);
|
||||||
}
|
}
|
||||||
Event::ControllerDeviceRemoved { which, .. } => {
|
Event::ControllerDeviceRemoved { which, .. } => {
|
||||||
let removed = if let Some(active_controller) = &active_controller {
|
let removed = if let Some(active_controller) = &active_controller {
|
||||||
|
@ -196,7 +194,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
gba.frame();
|
if gba.is_debugger_attached() {
|
||||||
|
gba.debugger_run()
|
||||||
|
} else {
|
||||||
|
gba.frame();
|
||||||
|
}
|
||||||
renderer.render(gba.get_frame_buffer());
|
renderer.render(gba.get_frame_buffer());
|
||||||
|
|
||||||
if let Some(fps) = fps_counter.tick() {
|
if let Some(fps) = fps_counter.tick() {
|
||||||
|
|
Reference in a new issue