mirror of
https://github.com/italicsjenga/agb.git
synced 2024-12-23 08:11:33 +11:00
Backtraces (#590)
Implements a very basic backtrace and the ability to format them If you run the panic example and press A, you get: ``` [ERROR] GBA Debug: [failed] [ERROR] GBA Debug: debug data: ce3-1ee7-1fb7-24d-1ad [FATAL] GBA Debug: Error: panicked at src/memory_mapped.rs:101:24: index out of bounds: the len is 240 but the index is 240 ``` which you can then prettify with: ``` > agb-addr2line ../target/thumbv4t-none-eabi/debug/examples/panic 0ce3-1f57-2027-024d-01ad 0: rust_begin_unwind <agb>/lib.rs:321 1: core::panicking::panic_nounwind <core>/panicking.rs:151 2: core::panicking::assert_failed_inner <core>/panicking.rs:321 3: agb::memory_mapped::MemoryMapped2DArray<T,_,_>::set <agb>/memory_mapped.rs:101 (inlined by) agb::display::bitmap3::Bitmap3::draw_point <agb>/display/bitmap3.rs:31 4: main /home/gwilym/Projects/agb/agb/examples/panic.rs:15 ``` - [ ] Changelog updated / no changelog update needed
This commit is contained in:
commit
e8aed8e376
|
@ -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.
|
||||
|
||||
|
|
19
agb-debug/Cargo.toml
Normal file
19
agb-debug/Cargo.toml
Normal file
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "agb-debug"
|
||||
version = "0.19.1"
|
||||
edition = "2021"
|
||||
authors = ["Gwilym Inzani <email@gwilym.dev>"]
|
||||
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"
|
154
agb-debug/src/gwilym_encoding.rs
Normal file
154
agb-debug/src/gwilym_encoding.rs
Normal file
|
@ -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<'_>> {
|
||||
GwilymDecodeIter::new(input)
|
||||
}
|
||||
|
||||
pub struct GwilymDecodeIter<'a> {
|
||||
chunks: ChunksExact<'a, u8>,
|
||||
}
|
||||
|
||||
impl<'a> GwilymDecodeIter<'a> {
|
||||
fn new(input: &'a str) -> anyhow::Result<Self> {
|
||||
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<Self::Item> {
|
||||
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::<Vec<_>>(),
|
||||
&[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::<Vec<_>>(), trace);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_strip_the_agbrsdev_prefix() -> anyhow::Result<()> {
|
||||
assert_eq!(
|
||||
&gwilym_decode("https://agbrs.dev/crash#2QI65Q69306Kv1")?.collect::<Vec<_>>(),
|
||||
&[0x0800_16d3, 0x0800_315b, 0x0800_3243, 0x0800_0195]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
84
agb-debug/src/load_dwarf.rs
Normal file
84
agb-debug/src/load_dwarf.rs
Normal file
|
@ -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<gimli::Dwarf<gimli::EndianRcSlice<gimli::RunTimeEndian>>> {
|
||||
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<String, Vec<u8>> =
|
||||
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<gimli::Dwarf<gimli::EndianRcSlice<gimli::RunTimeEndian>>> {
|
||||
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::EndianRcSlice<Endian>, 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)
|
||||
}
|
185
agb-debug/src/main.rs
Normal file
185
agb-debug/src/main.rs
Normal file
|
@ -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<impl gimli::Reader>,
|
||||
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("<crate>");
|
||||
|
||||
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
|
||||
}
|
137
agb-gbafix/Cargo.lock
generated
137
agb-gbafix/Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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<W: Write>(
|
|||
input: &[u8],
|
||||
mut header: GbaHeader,
|
||||
padding_behaviour: PaddingBehaviour,
|
||||
include_debug: bool,
|
||||
output: &mut W,
|
||||
) -> Result<()> {
|
||||
let elf_file = elf::ElfBytes::<elf::endian::AnyEndian>::minimal_parse(input)?;
|
||||
|
@ -80,7 +81,7 @@ pub fn write_gba_file<W: Write>(
|
|||
.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<W: Write>(
|
|||
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<W: Write>(
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_debug<W: Write>(
|
||||
elf_file: &elf::ElfBytes<'_, elf::endian::AnyEndian>,
|
||||
output: &mut W,
|
||||
) -> Result<u64> {
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ fn main() -> Result<()> {
|
|||
.arg(arg!(-m --makercode <MAKER_CODE> "Set the maker code, 2 bytes"))
|
||||
.arg(arg!(-r --gameversion <VERSION> "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::<PathBuf>("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()?;
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
94
agb/fnt/OFL.txt
Normal file
94
agb/fnt/OFL.txt
Normal file
|
@ -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.
|
BIN
agb/fnt/ark-pixel-10px-proportional-latin.ttf
Normal file
BIN
agb/fnt/ark-pixel-10px-proportional-latin.ttf
Normal file
Binary file not shown.
134
agb/src/backtrace.rs
Normal file
134
agb/src/backtrace.rs
Normal file
|
@ -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<u32>,
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
enum Register {
|
||||
R0,
|
||||
R1,
|
||||
R2,
|
||||
R3,
|
||||
R4,
|
||||
R5,
|
||||
R6,
|
||||
FP,
|
||||
SP,
|
||||
LR,
|
||||
PC,
|
||||
}
|
||||
|
||||
impl Index<Register> 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],
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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<Gba> = None;
|
||||
|
|
90
agb/src/panics_render.rs
Normal file
90
agb/src/panics_render.rs
Normal file
|
@ -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
|
||||
}
|
96
agb/src/panics_render/text.rs
Normal file
96
agb/src/panics_render/text.rs
Normal file
|
@ -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<i32>,
|
||||
start_x: i32,
|
||||
bitmap: &'bitmap mut Bitmap3<'gba>,
|
||||
colour: u16,
|
||||
previous_char: Option<char>,
|
||||
}
|
||||
|
||||
impl<'bitmap, 'gba> BitmapTextRender<'bitmap, 'gba> {
|
||||
pub fn new(
|
||||
bitmap: &'bitmap mut Bitmap3<'gba>,
|
||||
position: Vector2D<i32>,
|
||||
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(())
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -148,10 +148,12 @@ fn load_rom<P: AsRef<Path>>(path: P) -> anyhow::Result<Vec<u8>> {
|
|||
|
||||
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()
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
5
justfile
5
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 \
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue