diff --git a/CHANGELOG.md b/CHANGELOG.md index 417d09c5..8b24e482 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Added a new crash screen which provides a mechanism for seeing a full stack trace of your program when it panics. + This requires a change to your `.cargo/config.toml`. You must add the rust flag `"-Cforce-frame-pointers=yes"` to + your rustflags field. - Initial unicode support for font rendering. - Kerning support for font rendering. diff --git a/agb-debug/Cargo.toml b/agb-debug/Cargo.toml new file mode 100644 index 00000000..2dc09f65 --- /dev/null +++ b/agb-debug/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "agb-debug" +version = "0.19.1" +edition = "2021" +authors = ["Gwilym Inzani "] +license = "MPL-2.0" +description = "CLI utility to convert agb stack trace dumps into human readable stack traces" +repository = "https://github.com/agbrs/agb" + +[dependencies] +anyhow = "1" +clap = { version = "4", features = ["derive"] } +addr2line = { version = "0.21", default-features = false, features = [ + "rustc-demangle", + "std-object", +] } +colored = "2" +rmp-serde = "1" +lz4_flex = "0.11" diff --git a/agb-debug/src/gwilym_encoding.rs b/agb-debug/src/gwilym_encoding.rs new file mode 100644 index 00000000..1ce957f5 --- /dev/null +++ b/agb-debug/src/gwilym_encoding.rs @@ -0,0 +1,154 @@ +use std::{slice::ChunksExact, sync::OnceLock}; + +const ALPHABET: &[u8] = b"0123456789=ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz"; + +pub fn gwilym_decode(input: &str) -> anyhow::Result> { + GwilymDecodeIter::new(input) +} + +pub struct GwilymDecodeIter<'a> { + chunks: ChunksExact<'a, u8>, +} + +impl<'a> GwilymDecodeIter<'a> { + fn new(input: &'a str) -> anyhow::Result { + let input = input + .strip_prefix("https://agbrs.dev/crash#") + .unwrap_or(input); + + let Some((input, version)) = input.rsplit_once('v') else { + anyhow::bail!("Does not contain version"); + }; + + if version != "1" { + anyhow::bail!("Only version 1 is supported"); + } + + if input.len() % 3 != 0 { + anyhow::bail!("Input string must have length a multiple of 3"); + } + + Ok(Self { + chunks: input.as_bytes().chunks_exact(3), + }) + } +} + +impl<'a> Iterator for GwilymDecodeIter<'a> { + type Item = u32; + + fn next(&mut self) -> Option { + let chunk = self.chunks.next()?; + + let value = decode_chunk(chunk); + if value & (1 << 16) != 0 { + let upper_bits = value << 16; + let lower_bits = self.next().unwrap_or(0) & 0xffff; + + return Some(upper_bits | lower_bits); + } + + Some(value | 0x0800_0000) + } +} + +fn decode_chunk(chunk: &[u8]) -> u32 { + let a = get_value_for_char(chunk[0]); + let b = get_value_for_char(chunk[1]); + let c = get_value_for_char(chunk[2]); + + (a << (16 - 5)) | (b << (16 - 10)) | c +} + +fn get_value_for_char(input: u8) -> u32 { + static REVERSE_ALHPABET: OnceLock<[u8; 128]> = OnceLock::new(); + + REVERSE_ALHPABET.get_or_init(|| { + let mut result = [0; 128]; + for (i, &c) in ALPHABET.iter().enumerate() { + result[c as usize] = i as u8; + } + + result + })[input as usize] as u32 +} + +#[cfg(test)] +mod test { + use super::{gwilym_decode, ALPHABET}; + use std::fmt::Write; + + #[test] + fn should_correctly_decode_16s() -> anyhow::Result<()> { + assert_eq!( + &gwilym_decode("2QI65Q69306Kv1")?.collect::>(), + &[0x0800_16d3, 0x0800_315b, 0x0800_3243, 0x0800_0195] + ); + + Ok(()) + } + + fn encode_16(input: u16) -> [u8; 3] { + let input = input as usize; + [ + ALPHABET[input >> (16 - 5)], + ALPHABET[(input >> (16 - 10)) & 0b11111], + ALPHABET[input & 0b111111], + ] + } + + fn encode_32(input: u32) -> [u8; 6] { + let input = input as usize; + let output_lower_16 = encode_16(input as u16); + let input_upper_16 = input >> 16; + [ + ALPHABET[(input_upper_16 >> (16 - 5)) | (1 << 5)], + ALPHABET[(input_upper_16 >> (16 - 10)) & 0b11111], + ALPHABET[input_upper_16 & 0b111111], + output_lower_16[0], + output_lower_16[1], + output_lower_16[2], + ] + } + + #[test] + fn should_correctly_decode_16s_and_32s() -> anyhow::Result<()> { + let trace: &[u32] = &[ + 0x0300_2990, + 0x0800_3289, + 0x0500_2993, + 0x3829_2910, + 0xffff_ffff, + 0x0000_0000, + ]; + + let mut result = String::new(); + for &ip in trace { + if ip & 0xFFFF_0000 == 0x0800_0000 { + let encoded = encode_16(ip as u16); + let encoded_s = std::str::from_utf8(&encoded)?; + write!(&mut result, "{encoded_s}")? + } else { + let encoded = encode_32(ip); + let encoded_s = std::str::from_utf8(&encoded)?; + write!(&mut result, "{encoded_s}")? + } + } + + write!(&mut result, "v1")?; + + assert_eq!(&gwilym_decode(&result)?.collect::>(), trace); + + Ok(()) + } + + #[test] + fn should_strip_the_agbrsdev_prefix() -> anyhow::Result<()> { + assert_eq!( + &gwilym_decode("https://agbrs.dev/crash#2QI65Q69306Kv1")?.collect::>(), + &[0x0800_16d3, 0x0800_315b, 0x0800_3243, 0x0800_0195] + ); + + Ok(()) + } +} diff --git a/agb-debug/src/load_dwarf.rs b/agb-debug/src/load_dwarf.rs new file mode 100644 index 00000000..6662f231 --- /dev/null +++ b/agb-debug/src/load_dwarf.rs @@ -0,0 +1,84 @@ +use std::{borrow::Cow, collections::HashMap, io::Cursor, rc::Rc}; + +use addr2line::{ + gimli, + object::{self, Object}, +}; +use anyhow::bail; + +pub fn load_dwarf( + file_content: &[u8], +) -> anyhow::Result>> { + if let Ok(object) = object::File::parse(file_content) { + return load_from_object(&object); + } + + // the file might have been padded, so ensure we skip any padding before continuing + let last_non_zero_byte = file_content + .iter() + .rposition(|&b| b != 0) + .ok_or_else(|| anyhow::anyhow!("Gba file is empty"))?; + + let file_content = &file_content[..last_non_zero_byte + 1]; + + let last_8_bytes = &file_content[file_content.len() - 8..]; + let len = u32::from_le_bytes(last_8_bytes[0..4].try_into()?) as usize; + let version = &last_8_bytes[4..]; + + if version != b"agb1" { + bail!("Failed to load debug information from ROM file, it might not have been included?"); + } + + let compressed_debug_data = &file_content[file_content.len() - len - 8..file_content.len() - 8]; + + let decompressing_reader = + lz4_flex::frame::FrameDecoder::new(Cursor::new(compressed_debug_data)); + let debug_info: HashMap> = + rmp_serde::decode::from_read(decompressing_reader) + .map_err(|e| anyhow::anyhow!("Failed to load debug information: {e}"))?; + + let dwarf = gimli::Dwarf::load(|id| { + let data = debug_info + .get(id.name()) + .map(|data| Cow::Borrowed(data.as_slice())) + .unwrap_or(Cow::Borrowed(&[])); + + Result::<_, gimli::Error>::Ok(gimli::EndianRcSlice::new( + Rc::from(&*data), + gimli::RunTimeEndian::Little, + )) + })?; + + Ok(dwarf) +} + +fn load_from_object<'file>( + object: &object::File<'file, &'file [u8]>, +) -> anyhow::Result>> { + let endian = if object.is_little_endian() { + gimli::RunTimeEndian::Little + } else { + gimli::RunTimeEndian::Big + }; + + fn load_section<'data: 'file, 'file, O, Endian>( + id: gimli::SectionId, + file: &'file O, + endian: Endian, + ) -> Result, gimli::Error> + where + O: object::Object<'data, 'file>, + Endian: gimli::Endianity, + { + use object::ObjectSection; + + let data = file + .section_by_name(id.name()) + .and_then(|section| section.uncompressed_data().ok()) + .unwrap_or(Cow::Borrowed(&[])); + Ok(gimli::EndianRcSlice::new(Rc::from(&*data), endian)) + } + + let dwarf = gimli::Dwarf::load(|id| load_section(id, object, endian))?; + Ok(dwarf) +} diff --git a/agb-debug/src/main.rs b/agb-debug/src/main.rs new file mode 100644 index 00000000..efb5bd62 --- /dev/null +++ b/agb-debug/src/main.rs @@ -0,0 +1,185 @@ +use std::{ + borrow::Cow, + fs::{self, File}, + io::Read, + path::PathBuf, + time::SystemTime, +}; + +use addr2line::gimli; +use clap::Parser; +use colored::Colorize; +use load_dwarf::load_dwarf; + +mod gwilym_encoding; +mod load_dwarf; + +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct Args { + /// The filename of the elf file + elf_path: PathBuf, + + /// The output of agb's dump + dump: String, +} + +struct Location { + filename: String, + line: u32, + col: u32, +} + +impl Default for Location { + fn default() -> Self { + Self { + filename: "??".to_string(), + line: 0, + col: 0, + } + } +} + +fn main() -> anyhow::Result<()> { + let cli = Args::parse(); + + let modification_time = fs::metadata(&cli.elf_path)? + .modified() + .unwrap_or(SystemTime::UNIX_EPOCH); + + let file = fs::read(&cli.elf_path)?; + let dwarf = load_dwarf(&file)?; + + let ctx = addr2line::Context::from_dwarf(dwarf)?; + + for (i, address) in gwilym_encoding::gwilym_decode(&cli.dump)?.enumerate() { + print_address(&ctx, i, address.into(), modification_time)?; + } + + Ok(()) +} + +fn print_address( + ctx: &addr2line::Context, + index: usize, + address: u64, + elf_modification_time: SystemTime, +) -> anyhow::Result<()> { + let mut frames = ctx.find_frames(address).skip_all_loads()?; + + let mut is_first = true; + + while let Some(frame) = frames.next()? { + let function_name = if let Some(ref func) = frame.function { + func.demangle()?.into_owned() + } else { + "unknown function".to_string() + }; + + let location = frame + .location + .as_ref() + .map(|location| Location { + filename: location.file.unwrap_or("??").to_owned(), + line: location.line.unwrap_or(0), + col: location.column.unwrap_or(0), + }) + .unwrap_or_default(); + + let is_interesting = is_interesting_function(&function_name, &location.filename); + let function_name_to_print = if is_interesting { + function_name.bold() + } else { + function_name.normal() + }; + + if is_first { + print!("{index}:\t{function_name_to_print}"); + } else { + print!("\t(inlined into) {function_name_to_print}"); + } + + println!( + " {}:{}", + prettify_path(&location.filename).green(), + location.line.to_string().green() + ); + + if location.line != 0 && is_interesting { + print_line_of_code(&frame, location, elf_modification_time)?; + } + + is_first = false; + } + + Ok(()) +} + +fn print_line_of_code( + frame: &addr2line::Frame<'_, impl gimli::Reader>, + location: Location, + elf_modification_time: SystemTime, +) -> anyhow::Result<()> { + let Some(filename) = frame.location.as_ref().and_then(|location| location.file) else { + return Ok(()); + }; + + let Ok(mut file) = File::open(filename) else { + return Ok(()); + }; + + let modification_time = fs::metadata(filename)? + .modified() + .unwrap_or(SystemTime::UNIX_EPOCH); + + if modification_time > elf_modification_time { + eprintln!("Warning: File {filename} modified more recently than the binary, line info may be incorrect"); + } + + let mut content = String::new(); + file.read_to_string(&mut content)?; + + let Some(line_of_code) = content.split('\n').nth(location.line as usize - 1) else { + eprintln!("File {filename} does not have line {}", location.line); + return Ok(()); + }; + + let trimmed = line_of_code.trim_start(); + let trimmed_len = line_of_code.len() - trimmed.len(); + println!("\t\t{}", trimmed); + + if location.col != 0 { + println!( + "\t\t{}{}", + " ".repeat(location.col as usize - trimmed_len - 1), + "^".bright_blue() + ); + } + + Ok(()) +} + +fn prettify_path(path: &str) -> Cow<'_, str> { + if let Some(src_index) = path.rfind("/src/") { + let crate_name_start = path[0..src_index].rfind('/'); + let crate_name = crate_name_start + .map(|crate_name_start| &path[crate_name_start + 1..src_index]) + .unwrap_or(""); + + Cow::Owned(format!("<{crate_name}>/{}", &path[src_index + 5..])) + } else { + Cow::Borrowed(path) + } +} + +fn is_interesting_function(function_name: &str, path: &str) -> bool { + if function_name == "rust_begin_unwind" { + return false; // this is the unwind exception call + } + + if path.ends_with("panicking.rs") { + return false; // probably part of rust's internal panic mechanisms + } + + true +} diff --git a/agb-gbafix/Cargo.lock b/agb-gbafix/Cargo.lock index c1eb433a..bf27a936 100644 --- a/agb-gbafix/Cargo.lock +++ b/agb-gbafix/Cargo.lock @@ -9,6 +9,8 @@ dependencies = [ "anyhow", "clap", "elf", + "lz4_flex", + "rmp-serde", ] [[package]] @@ -65,6 +67,24 @@ version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" +[[package]] +name = "autocfg" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + [[package]] name = "clap" version = "4.5.4" @@ -104,12 +124,129 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4445909572dbd556c457c849c4ca58623d84b27c8fff1e74b0b4227d8b90d17b" +[[package]] +name = "lz4_flex" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75761162ae2b0e580d7e7c390558127e5f01b4194debd6221fd8c207fc80e3f5" +dependencies = [ + "twox-hash", +] + +[[package]] +name = "num-traits" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + +[[package]] +name = "proc-macro2" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rmp" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9860a6cc38ed1da53456442089b4dfa35e7cedaa326df63017af88385e6b20" +dependencies = [ + "byteorder", + "num-traits", + "paste", +] + +[[package]] +name = "rmp-serde" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bffea85eea980d8a74453e5d02a8d93028f3c34725de143085a844ebe953258a" +dependencies = [ + "byteorder", + "rmp", + "serde", +] + +[[package]] +name = "serde" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "syn" +version = "2.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "twox-hash" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" +dependencies = [ + "cfg-if", + "static_assertions", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + [[package]] name = "utf8parse" version = "0.2.1" diff --git a/agb-gbafix/Cargo.toml b/agb-gbafix/Cargo.toml index c27d2b7b..b805e384 100644 --- a/agb-gbafix/Cargo.toml +++ b/agb-gbafix/Cargo.toml @@ -11,12 +11,5 @@ repository = "https://github.com/agbrs/agb" elf = "0.7" anyhow = "1" clap = "4" - -[profile.dev] -opt-level = 3 -debug = true - -[profile.release] -opt-level = 3 -lto = "fat" -debug = true +rmp-serde = "1" +lz4_flex = "0.11" diff --git a/agb-gbafix/src/lib.rs b/agb-gbafix/src/lib.rs index 36b0389f..5cfe2d9d 100644 --- a/agb-gbafix/src/lib.rs +++ b/agb-gbafix/src/lib.rs @@ -1,5 +1,5 @@ use anyhow::{anyhow, bail, ensure, Result}; -use std::io::Write; +use std::{collections::HashMap, io::Write}; const GBA_HEADER_SIZE: usize = 192; @@ -71,6 +71,7 @@ pub fn write_gba_file( input: &[u8], mut header: GbaHeader, padding_behaviour: PaddingBehaviour, + include_debug: bool, output: &mut W, ) -> Result<()> { let elf_file = elf::ElfBytes::::minimal_parse(input)?; @@ -80,7 +81,7 @@ pub fn write_gba_file( .ok_or_else(|| anyhow!("Failed to parse as elf file"))?; let mut bytes_written = 0; - for section_header in section_headers.iter() { + for section_header in section_headers { const SHT_NOBITS: u32 = 8; const SHT_NULL: u32 = 0; const SHF_ALLOC: u64 = 2; @@ -123,6 +124,10 @@ pub fn write_gba_file( bytes_written += data.len() as u64; } + if include_debug { + bytes_written += write_debug(&elf_file, output)?; + } + if !bytes_written.is_power_of_two() && padding_behaviour == PaddingBehaviour::Pad { let required_padding = bytes_written.next_power_of_two() - bytes_written; @@ -133,3 +138,42 @@ pub fn write_gba_file( Ok(()) } + +fn write_debug( + elf_file: &elf::ElfBytes<'_, elf::endian::AnyEndian>, + output: &mut W, +) -> Result { + let (Some(section_headers), Some(string_table)) = elf_file.section_headers_with_strtab()? + else { + bail!("Could not find either the section headers or the string table"); + }; + + let mut debug_sections = HashMap::new(); + + for section_header in section_headers { + let section_name = string_table.get(section_header.sh_name as usize)?; + if !section_name.starts_with(".debug") { + continue; + } + + let (data, compression) = elf_file.section_data(§ion_header)?; + if compression.is_some() { + bail!("Do not support compression in elf files, section {section_name} was compressed"); + } + + debug_sections.insert(section_name.to_owned(), data); + } + + let mut debug_data = vec![]; + { + let mut compressed_writer = lz4_flex::frame::FrameEncoder::new(&mut debug_data); + rmp_serde::encode::write(&mut compressed_writer, &debug_sections)?; + compressed_writer.flush()?; + } + + output.write_all(&debug_data)?; + output.write_all(&(debug_data.len() as u32).to_le_bytes())?; + output.write_all(b"agb1")?; + + Ok(debug_data.len() as u64 + 4) +} diff --git a/agb-gbafix/src/main.rs b/agb-gbafix/src/main.rs index 3ae0f25c..8b2db41d 100644 --- a/agb-gbafix/src/main.rs +++ b/agb-gbafix/src/main.rs @@ -19,6 +19,7 @@ fn main() -> Result<()> { .arg(arg!(-m --makercode "Set the maker code, 2 bytes")) .arg(arg!(-r --gameversion "Set the version of the game, 0-255").value_parser(value_parser!(u8))) .arg(arg!(-p --padding "Pad the ROM to the next power of 2 in size")) + .arg(arg!(-g --debug "Include debug information directly in the ROM")) .get_matches(); let input = matches.get_one::("INPUT").unwrap(); @@ -70,6 +71,8 @@ fn main() -> Result<()> { } } + let include_debug = matches.get_flag("debug"); + let pad = matches.get_flag("padding"); let pad = if pad { PaddingBehaviour::Pad @@ -80,7 +83,13 @@ fn main() -> Result<()> { let mut output = BufWriter::new(fs::File::create(output)?); let file_data = fs::read(input)?; - write_gba_file(file_data.as_slice(), header, pad, &mut output)?; + write_gba_file( + file_data.as_slice(), + header, + pad, + include_debug, + &mut output, + )?; output.flush()?; diff --git a/agb/.cargo/config.toml b/agb/.cargo/config.toml index 62ebedb7..ce415d4a 100644 --- a/agb/.cargo/config.toml +++ b/agb/.cargo/config.toml @@ -6,9 +6,17 @@ build-std-features = ["compiler-builtins-mem"] target = "thumbv4t-none-eabi" [target.thumbv4t-none-eabi] -rustflags = ["-Clink-arg=-Tgba.ld", "-Ctarget-cpu=arm7tdmi"] +rustflags = [ + "-Clink-arg=-Tgba.ld", + "-Ctarget-cpu=arm7tdmi", + "-Cforce-frame-pointers=yes", +] runner = "mgba-test-runner" [target.armv4t-none-eabi] -rustflags = ["-Clink-arg=-Tgba.ld", "-Ctarget-cpu=arm7tdmi"] +rustflags = [ + "-Clink-arg=-Tgba.ld", + "-Ctarget-cpu=arm7tdmi", + "-Cforce-frame-pointers=yes", +] runner = "mgba-test-runner" diff --git a/agb/Cargo.toml b/agb/Cargo.toml index c682638d..e6d35195 100644 --- a/agb/Cargo.toml +++ b/agb/Cargo.toml @@ -21,6 +21,7 @@ agb_fixnum = { version = "0.19.1", path = "../agb-fixnum" } agb_hashmap = { version = "0.19.1", path = "../agb-hashmap" } bare-metal = "1" bilge = "0.2" +qrcodegen-no-heap = "1.8" [package.metadata.docs.rs] default-target = "thumbv4t-none-eabi" diff --git a/agb/fnt/OFL.txt b/agb/fnt/OFL.txt new file mode 100644 index 00000000..be1dc1f3 --- /dev/null +++ b/agb/fnt/OFL.txt @@ -0,0 +1,94 @@ +Copyright (c) 2021, TakWolf (https://takwolf.com), +with Reserved Font Name 'Ark Pixel'. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/agb/fnt/ark-pixel-10px-proportional-latin.ttf b/agb/fnt/ark-pixel-10px-proportional-latin.ttf new file mode 100644 index 00000000..b1249428 Binary files /dev/null and b/agb/fnt/ark-pixel-10px-proportional-latin.ttf differ diff --git a/agb/src/backtrace.rs b/agb/src/backtrace.rs new file mode 100644 index 00000000..0673761b --- /dev/null +++ b/agb/src/backtrace.rs @@ -0,0 +1,134 @@ +use core::{arch::asm, ops::Index}; + +use alloc::vec::Vec; + +// only works for code compiled as THUMB +#[repr(C)] +#[derive(Clone, Default, Debug)] +struct Context { + registers: [u32; 11], +} + +pub struct Frames { + frames: Vec, +} + +#[allow(unused)] +enum Register { + R0, + R1, + R2, + R3, + R4, + R5, + R6, + FP, + SP, + LR, + PC, +} + +impl Index for Context { + type Output = u32; + + fn index(&self, index: Register) -> &Self::Output { + &self.registers[index as usize] + } +} + +#[inline(never)] +pub(crate) fn unwind_exception() -> Frames { + let mut context = Context::default(); + + unsafe { + let context_ptr = (&mut context) as *mut _; + + asm!( + " + str r0, [r0, #0x00] + str r1, [r0, #0x04] + str r2, [r0, #0x08] + str r3, [r0, #0x0C] + str r4, [r0, #0x10] + str r5, [r0, #0x14] + str r6, [r0, #0x18] + str r7, [r0, #0x1C] + mov r7, sp + str r7, [r0, #0x20] + mov r7, lr + str r7, [r0, #0x24] + mov r7, pc + str r7, [r0, #0x28] + ldr r7, [r0, #0x1C] + ", + in("r0") context_ptr + ); + } + + let mut frame_pointer = context[Register::FP]; + + let mut frames = Vec::new(); + + loop { + let sp = unsafe { *(frame_pointer as *const u32) }; + let lr = unsafe { *((frame_pointer as *const u32).add(1)) }; + + if sp == 0 { + break; + } + + frames.push(lr); + + frame_pointer = sp; + } + + Frames { frames } +} + +impl core::fmt::Display for Frames { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + for frame in &self.frames { + if frame & 0xFFFF_0000 == 0x0800_0000 { + let frame = *frame as u16; // intentionally truncate + let frame_encoded = gwilym_encoding::encode_16(frame); + let frame_str = unsafe { core::str::from_utf8_unchecked(&frame_encoded) }; + + write!(f, "{frame_str}")?; + } else { + let frame_encoded = gwilym_encoding::encode_32(*frame); + let frame_str = unsafe { core::str::from_utf8_unchecked(&frame_encoded) }; + + write!(f, "{frame_str}")?; + } + } + + write!(f, "v1") + } +} + +mod gwilym_encoding { + static ALPHABET: &[u8] = b"0123456789=ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz"; + + pub fn encode_16(input: u16) -> [u8; 3] { + let input = input as usize; + [ + ALPHABET[input >> (16 - 5)], + ALPHABET[(input >> (16 - 10)) & 0b11111], + ALPHABET[input & 0b111111], + ] + } + + pub fn encode_32(input: u32) -> [u8; 6] { + let input = input as usize; + let output_lower_16 = encode_16(input as u16); + let input_upper_16 = input >> 16; + [ + ALPHABET[(input_upper_16 >> (16 - 5)) | (1 << 5)], + ALPHABET[(input_upper_16 >> (16 - 10)) & 0b11111], + ALPHABET[input_upper_16 & 0b111111], + output_lower_16[0], + output_lower_16[1], + output_lower_16[2], + ] + } +} diff --git a/agb/src/dma.rs b/agb/src/dma.rs index 9e29ef61..8135a499 100644 --- a/agb/src/dma.rs +++ b/agb/src/dma.rs @@ -53,7 +53,7 @@ impl Dma { } } - fn disable(&mut self) { + pub(crate) fn disable(&mut self) { unsafe { MemoryMapped::new(dma_control_addr(self.number)) }.set(0); } diff --git a/agb/src/lib.rs b/agb/src/lib.rs index cd1fa99e..0febf2b5 100644 --- a/agb/src/lib.rs +++ b/agb/src/lib.rs @@ -150,6 +150,7 @@ extern crate alloc; mod agb_alloc; mod agbabi; +mod backtrace; mod bitarray; /// Implements everything relating to things that are displayed on screen. pub mod display; @@ -166,6 +167,7 @@ pub mod mgba; pub use agb_fixnum as fixnum; /// Contains an implementation of a hashmap which suits the gameboy advance's hardware. pub use agb_hashmap as hash_map; +mod panics_render; /// Simple random number generator pub mod rng; pub mod save; @@ -288,6 +290,8 @@ impl Gba { /// You can run the tests using `cargo test`, but it will work better through `mgba-test-runner` by /// running something along the lines of `CARGO_TARGET_THUMBV4T_NONE_EABI_RUNNER=mgba-test-runner cargo test`. pub mod test_runner { + use self::panics_render::render_backtrace; + use super::*; #[doc(hidden)] @@ -317,14 +321,13 @@ pub mod test_runner { #[panic_handler] fn panic_implementation(info: &core::panic::PanicInfo) -> ! { + let frames = backtrace::unwind_exception(); + if let Some(mut mgba) = mgba::Mgba::new() { - mgba.print(format_args!("[failed]"), mgba::DebugLevel::Error) - .unwrap(); - mgba.print(format_args!("Error: {info}"), mgba::DebugLevel::Fatal) - .unwrap(); + let _ = mgba.print(format_args!("[failed]"), mgba::DebugLevel::Error); } - loop {} + render_backtrace(&frames, info); } static mut TEST_GBA: Option = None; diff --git a/agb/src/panics_render.rs b/agb/src/panics_render.rs new file mode 100644 index 00000000..e8edd5ca --- /dev/null +++ b/agb/src/panics_render.rs @@ -0,0 +1,90 @@ +use core::{fmt::Write, panic::PanicInfo}; + +use alloc::{format, vec}; + +use crate::{ + backtrace, + display::{bitmap3::Bitmap3, busy_wait_for_vblank, HEIGHT, WIDTH}, + dma::dma3_exclusive, + interrupt, mgba, syscall, +}; + +mod text; + +pub fn render_backtrace(trace: &backtrace::Frames, info: &PanicInfo) -> ! { + interrupt::free(|_cs| { + dma3_exclusive(|| { + // SAFETY: This is not fine, but we're crashing anyway. The loop at the end should stop anything bad happening + let mut gba = unsafe { crate::Gba::new_in_entry() }; + + gba.dma.dma().dma3.disable(); + let mut gfx = gba.display.video.bitmap3(); + + let qrcode_string_data = format!("https://agbrs.dev/crash#{trace}"); + crate::println!("Stack trace: {qrcode_string_data}"); + + let location = draw_qr_code(&mut gfx, &qrcode_string_data); + + let mut trace_text_render = + text::BitmapTextRender::new(&mut gfx, (location, 8).into(), 0x0000); + let _ = write!( + &mut trace_text_render, + "The game crashed :(\nhttps://agbrs.dev/crash\n{trace}" + ); + + let mut panic_text_render = + text::BitmapTextRender::new(&mut gfx, (8, location).into(), 0x0000); + let _ = write!(&mut panic_text_render, "{info}"); + + // need to wait 2 frames to ensure that mgba finishes rendering before the fatal call below + busy_wait_for_vblank(); + busy_wait_for_vblank(); + + if let Some(mut mgba) = mgba::Mgba::new() { + let _ = mgba.print(format_args!("Error: {info}"), mgba::DebugLevel::Fatal); + } + + loop { + syscall::halt(); + } + }) + }) +} + +/// Returns the width / height of the QR code + padding in pixels +fn draw_qr_code(gfx: &mut Bitmap3<'_>, qrcode_string_data: &str) -> i32 { + const MAX_VERSION: qrcodegen_no_heap::Version = qrcodegen_no_heap::Version::new(6); + + let mut temp_buffer = vec![0; MAX_VERSION.buffer_len()]; + let mut out_buffer = vec![0; MAX_VERSION.buffer_len()]; + + let qr_code = match qrcodegen_no_heap::QrCode::encode_text( + qrcode_string_data, + &mut temp_buffer, + &mut out_buffer, + qrcodegen_no_heap::QrCodeEcc::Medium, + qrcodegen_no_heap::Version::MIN, + MAX_VERSION, + None, + true, + ) { + Ok(qr_code) => qr_code, + Err(e) => { + crate::println!("Error generating qr code: {e:?}"); + return 8; + } + }; + + for y in 0..HEIGHT { + for x in 0..WIDTH { + let colour = if qr_code.get_module(x / 2 - 4, y / 2 - 4) { + 0x0000 + } else { + 0xFFFF + }; + gfx.draw_point(x, y, colour); + } + } + + qr_code.size() * 2 + 8 * 2 +} diff --git a/agb/src/panics_render/text.rs b/agb/src/panics_render/text.rs new file mode 100644 index 00000000..d817a06d --- /dev/null +++ b/agb/src/panics_render/text.rs @@ -0,0 +1,96 @@ +use core::fmt::Write; + +use crate::{ + display::{bitmap3::Bitmap3, Font, HEIGHT, WIDTH}, + fixnum::Vector2D, +}; + +static FONT: Font = include_font!("fnt/ark-pixel-10px-proportional-latin.ttf", 10); + +pub struct BitmapTextRender<'bitmap, 'gba> { + head_position: Vector2D, + start_x: i32, + bitmap: &'bitmap mut Bitmap3<'gba>, + colour: u16, + previous_char: Option, +} + +impl<'bitmap, 'gba> BitmapTextRender<'bitmap, 'gba> { + pub fn new( + bitmap: &'bitmap mut Bitmap3<'gba>, + position: Vector2D, + start_colour: u16, + ) -> Self { + Self { + head_position: position, + start_x: position.x, + bitmap, + colour: start_colour, + previous_char: None, + } + } + + fn render_letter(&mut self, c: char) { + let letter = FONT.letter(c); + + self.head_position.x += letter.xmin as i32 + + self + .previous_char + .take() + .map_or(0, |c| letter.kerning_amount(c)); + self.previous_char = Some(c); + + if self.head_position.x + letter.width as i32 >= WIDTH { + self.newline(); + } + + if self.head_position.y + letter.height as i32 >= HEIGHT { + return; + } + + let y_position_start = + self.head_position.y + FONT.ascent() - letter.height as i32 - letter.ymin as i32; + + for y in 0..letter.height as usize { + for x in 0..letter.width as usize { + let rendered = letter.bit_absolute(x, y); + if rendered { + self.bitmap.draw_point( + x as i32 + self.head_position.x, + y as i32 + y_position_start, + self.colour, + ); + } + } + } + + self.head_position.x += letter.advance_width as i32; + } + + fn render_char(&mut self, c: char) { + match c { + '\n' => { + self.newline(); + } + ' ' => { + self.head_position.x += FONT.letter(' ').advance_width as i32; + } + letter => self.render_letter(letter), + } + } + + fn newline(&mut self) { + self.head_position.x = self.start_x; + self.head_position.y += FONT.line_height(); + } +} + +impl<'bitmap, 'gba> Write for BitmapTextRender<'bitmap, 'gba> { + fn write_str(&mut self, s: &str) -> core::fmt::Result { + for c in s.chars() { + self.render_char(c); + } + + Ok(()) + } +} diff --git a/book/games/pong/.cargo/config.toml b/book/games/pong/.cargo/config.toml index b3276236..45ce46eb 100644 --- a/book/games/pong/.cargo/config.toml +++ b/book/games/pong/.cargo/config.toml @@ -6,9 +6,17 @@ build-std-features = ["compiler-builtins-mem"] target = "thumbv4t-none-eabi" [target.thumbv4t-none-eabi] -rustflags = ["-Clink-arg=-Tgba.ld", "-Ctarget-cpu=arm7tdmi"] +rustflags = [ + "-Clink-arg=-Tgba.ld", + "-Ctarget-cpu=arm7tdmi", + "-Cforce-frame-pointers=yes", +] runner = "mgba-qt" [target.armv4t-none-eabi] -rustflags = ["-Clink-arg=-Tgba.ld", "-Ctarget-cpu=arm7tdmi"] +rustflags = [ + "-Clink-arg=-Tgba.ld", + "-Ctarget-cpu=arm7tdmi", + "-Cforce-frame-pointers=yes", +] runner = "mgba-qt" diff --git a/emulator/test-runner/src/main.rs b/emulator/test-runner/src/main.rs index 55db72f4..7f693b64 100644 --- a/emulator/test-runner/src/main.rs +++ b/emulator/test-runner/src/main.rs @@ -148,10 +148,12 @@ fn load_rom>(path: P) -> anyhow::Result> { let mut elf_buffer = Vec::new(); + let inculde_debug_info = false; if agb_gbafix::write_gba_file( &input_file_buffer, Default::default(), agb_gbafix::PaddingBehaviour::DoNotPad, + inculde_debug_info, &mut elf_buffer, ) .is_ok() diff --git a/examples/amplitude/.cargo/config.toml b/examples/amplitude/.cargo/config.toml index b3276236..45ce46eb 100644 --- a/examples/amplitude/.cargo/config.toml +++ b/examples/amplitude/.cargo/config.toml @@ -6,9 +6,17 @@ build-std-features = ["compiler-builtins-mem"] target = "thumbv4t-none-eabi" [target.thumbv4t-none-eabi] -rustflags = ["-Clink-arg=-Tgba.ld", "-Ctarget-cpu=arm7tdmi"] +rustflags = [ + "-Clink-arg=-Tgba.ld", + "-Ctarget-cpu=arm7tdmi", + "-Cforce-frame-pointers=yes", +] runner = "mgba-qt" [target.armv4t-none-eabi] -rustflags = ["-Clink-arg=-Tgba.ld", "-Ctarget-cpu=arm7tdmi"] +rustflags = [ + "-Clink-arg=-Tgba.ld", + "-Ctarget-cpu=arm7tdmi", + "-Cforce-frame-pointers=yes", +] runner = "mgba-qt" diff --git a/examples/combo/.cargo/config.toml b/examples/combo/.cargo/config.toml index b3276236..45ce46eb 100644 --- a/examples/combo/.cargo/config.toml +++ b/examples/combo/.cargo/config.toml @@ -6,9 +6,17 @@ build-std-features = ["compiler-builtins-mem"] target = "thumbv4t-none-eabi" [target.thumbv4t-none-eabi] -rustflags = ["-Clink-arg=-Tgba.ld", "-Ctarget-cpu=arm7tdmi"] +rustflags = [ + "-Clink-arg=-Tgba.ld", + "-Ctarget-cpu=arm7tdmi", + "-Cforce-frame-pointers=yes", +] runner = "mgba-qt" [target.armv4t-none-eabi] -rustflags = ["-Clink-arg=-Tgba.ld", "-Ctarget-cpu=arm7tdmi"] +rustflags = [ + "-Clink-arg=-Tgba.ld", + "-Ctarget-cpu=arm7tdmi", + "-Cforce-frame-pointers=yes", +] runner = "mgba-qt" diff --git a/examples/hyperspace-roll/.cargo/config.toml b/examples/hyperspace-roll/.cargo/config.toml index b3276236..45ce46eb 100644 --- a/examples/hyperspace-roll/.cargo/config.toml +++ b/examples/hyperspace-roll/.cargo/config.toml @@ -6,9 +6,17 @@ build-std-features = ["compiler-builtins-mem"] target = "thumbv4t-none-eabi" [target.thumbv4t-none-eabi] -rustflags = ["-Clink-arg=-Tgba.ld", "-Ctarget-cpu=arm7tdmi"] +rustflags = [ + "-Clink-arg=-Tgba.ld", + "-Ctarget-cpu=arm7tdmi", + "-Cforce-frame-pointers=yes", +] runner = "mgba-qt" [target.armv4t-none-eabi] -rustflags = ["-Clink-arg=-Tgba.ld", "-Ctarget-cpu=arm7tdmi"] +rustflags = [ + "-Clink-arg=-Tgba.ld", + "-Ctarget-cpu=arm7tdmi", + "-Cforce-frame-pointers=yes", +] runner = "mgba-qt" diff --git a/examples/the-dungeon-puzzlers-lament/.cargo/config.toml b/examples/the-dungeon-puzzlers-lament/.cargo/config.toml index b3276236..45ce46eb 100644 --- a/examples/the-dungeon-puzzlers-lament/.cargo/config.toml +++ b/examples/the-dungeon-puzzlers-lament/.cargo/config.toml @@ -6,9 +6,17 @@ build-std-features = ["compiler-builtins-mem"] target = "thumbv4t-none-eabi" [target.thumbv4t-none-eabi] -rustflags = ["-Clink-arg=-Tgba.ld", "-Ctarget-cpu=arm7tdmi"] +rustflags = [ + "-Clink-arg=-Tgba.ld", + "-Ctarget-cpu=arm7tdmi", + "-Cforce-frame-pointers=yes", +] runner = "mgba-qt" [target.armv4t-none-eabi] -rustflags = ["-Clink-arg=-Tgba.ld", "-Ctarget-cpu=arm7tdmi"] +rustflags = [ + "-Clink-arg=-Tgba.ld", + "-Ctarget-cpu=arm7tdmi", + "-Cforce-frame-pointers=yes", +] runner = "mgba-qt" diff --git a/examples/the-hat-chooses-the-wizard/.cargo/config.toml b/examples/the-hat-chooses-the-wizard/.cargo/config.toml index b3276236..45ce46eb 100644 --- a/examples/the-hat-chooses-the-wizard/.cargo/config.toml +++ b/examples/the-hat-chooses-the-wizard/.cargo/config.toml @@ -6,9 +6,17 @@ build-std-features = ["compiler-builtins-mem"] target = "thumbv4t-none-eabi" [target.thumbv4t-none-eabi] -rustflags = ["-Clink-arg=-Tgba.ld", "-Ctarget-cpu=arm7tdmi"] +rustflags = [ + "-Clink-arg=-Tgba.ld", + "-Ctarget-cpu=arm7tdmi", + "-Cforce-frame-pointers=yes", +] runner = "mgba-qt" [target.armv4t-none-eabi] -rustflags = ["-Clink-arg=-Tgba.ld", "-Ctarget-cpu=arm7tdmi"] +rustflags = [ + "-Clink-arg=-Tgba.ld", + "-Ctarget-cpu=arm7tdmi", + "-Cforce-frame-pointers=yes", +] runner = "mgba-qt" diff --git a/examples/the-purple-night/.cargo/config.toml b/examples/the-purple-night/.cargo/config.toml index b3276236..45ce46eb 100644 --- a/examples/the-purple-night/.cargo/config.toml +++ b/examples/the-purple-night/.cargo/config.toml @@ -6,9 +6,17 @@ build-std-features = ["compiler-builtins-mem"] target = "thumbv4t-none-eabi" [target.thumbv4t-none-eabi] -rustflags = ["-Clink-arg=-Tgba.ld", "-Ctarget-cpu=arm7tdmi"] +rustflags = [ + "-Clink-arg=-Tgba.ld", + "-Ctarget-cpu=arm7tdmi", + "-Cforce-frame-pointers=yes", +] runner = "mgba-qt" [target.armv4t-none-eabi] -rustflags = ["-Clink-arg=-Tgba.ld", "-Ctarget-cpu=arm7tdmi"] +rustflags = [ + "-Clink-arg=-Tgba.ld", + "-Ctarget-cpu=arm7tdmi", + "-Cforce-frame-pointers=yes", +] runner = "mgba-qt" diff --git a/justfile b/justfile index a72ca9f8..f04558b5 100644 --- a/justfile +++ b/justfile @@ -142,7 +142,10 @@ _build-rom folder name: cp -v "$GBA_FILE" "examples/target/examples/$GAME_NAME.gba" gbafix *args: - (cd agb-gbafix && cargo run --release -- {{args}}) + (cd agb-gbafix && cargo build --release && cd "{{invocation_directory()}}" && "$CARGO_TARGET_DIR/release/agb-gbafix" {{args}}) + +debug *args: + (cd agb-debug && cargo build --release && cd "{{invocation_directory()}}" && "$CARGO_TARGET_DIR/release/agb-debug" {{args}}) _all-crates target: for CARGO_PROJECT_FILE in agb-*/Cargo.toml agb/Cargo.toml tracker/agb-*/Cargo.toml; do \ diff --git a/template/.cargo/config.toml b/template/.cargo/config.toml index 655e5a26..67382136 100644 --- a/template/.cargo/config.toml +++ b/template/.cargo/config.toml @@ -6,9 +6,17 @@ build-std-features = ["compiler-builtins-mem"] target = "thumbv4t-none-eabi" [target.thumbv4t-none-eabi] -rustflags = ["-Clink-arg=-Tgba.ld", "-Ctarget-cpu=arm7tdmi"] +rustflags = [ + "-Clink-arg=-Tgba.ld", + "-Ctarget-cpu=arm7tdmi", + "-Cforce-frame-pointers=yes", +] runner = ["mgba-qt", "-C", "logToStdout=1", "-C", "logLevel.gba.debug=127"] [target.armv4t-none-eabi] -rustflags = ["-Clink-arg=-Tgba.ld", "-Ctarget-cpu=arm7tdmi"] +rustflags = [ + "-Clink-arg=-Tgba.ld", + "-Ctarget-cpu=arm7tdmi", + "-Cforce-frame-pointers=yes", +] runner = ["mgba-qt", "-C", "logToStdout=1", "-C", "logLevel.gba.debug=127"] diff --git a/tracker/agb-tracker/.cargo/config.toml b/tracker/agb-tracker/.cargo/config.toml index 62ebedb7..ce415d4a 100644 --- a/tracker/agb-tracker/.cargo/config.toml +++ b/tracker/agb-tracker/.cargo/config.toml @@ -6,9 +6,17 @@ build-std-features = ["compiler-builtins-mem"] target = "thumbv4t-none-eabi" [target.thumbv4t-none-eabi] -rustflags = ["-Clink-arg=-Tgba.ld", "-Ctarget-cpu=arm7tdmi"] +rustflags = [ + "-Clink-arg=-Tgba.ld", + "-Ctarget-cpu=arm7tdmi", + "-Cforce-frame-pointers=yes", +] runner = "mgba-test-runner" [target.armv4t-none-eabi] -rustflags = ["-Clink-arg=-Tgba.ld", "-Ctarget-cpu=arm7tdmi"] +rustflags = [ + "-Clink-arg=-Tgba.ld", + "-Ctarget-cpu=arm7tdmi", + "-Cforce-frame-pointers=yes", +] runner = "mgba-test-runner"