aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMiquel Sabaté Solà <mikisabate@gmail.com>2025-01-13 16:12:08 +0100
committerMiquel Sabaté Solà <mikisabate@gmail.com>2025-01-13 16:15:06 +0100
commit5f48de69f46d4da27b22c47178b2e6216f7a7801 (patch)
tree8d4411c63b714565eeae62df0ff8691cce38d899
parentab70669868a653534adb67700cacda2ffaeb46f0 (diff)
downloadtools.nes-5f48de69f46d4da27b22c47178b2e6216f7a7801.tar.gz
tools.nes-5f48de69f46d4da27b22c47178b2e6216f7a7801.zip
Add support for cfg files
In order to make tools like `xa65` work seamlessly with existing configurations, allow this format too instead of making developers switch to a new file with TOML syntax. Signed-off-by: Miquel Sabaté Solà <mikisabate@gmail.com>
-rw-r--r--lib/xixanta/src/cfg.rs546
-rw-r--r--lib/xixanta/src/lib.rs1
-rw-r--r--lib/xixanta/src/mapping.rs15
3 files changed, 559 insertions, 3 deletions
diff --git a/lib/xixanta/src/cfg.rs b/lib/xixanta/src/cfg.rs
new file mode 100644
index 0000000..a6127b9
--- /dev/null
+++ b/lib/xixanta/src/cfg.rs
@@ -0,0 +1,546 @@
+use crate::mapping::{Mapping, SectionType, Segment};
+use std::collections::HashMap;
+
+// Section of the configuration being parsed.
+enum Section {
+ Memory,
+ Segment,
+ Symbol,
+}
+
+// Same as mapping::Mapping but before translating symbols into it. Used when
+// fetching the values from the configuration file but before symbols have been
+// all loaded.
+struct RawMapping {
+ name: String,
+ start: String,
+ size: String,
+ fill: Option<String>,
+ ignore: bool,
+ line_num: usize,
+}
+
+// Fetch a line of values in the format of "Name: key1=value1, key2=value2,
+// ...;". The semicolon at the end is not considered and the given line should
+// not contain it as the caller must have check it beforehand. Returns a tuple
+// which contain the name (i.e. what's before the colon) and the line of values
+// (i.e. after the colon and before the semicolon).
+fn fetch_line_values(line: &str, line_num: usize) -> Result<(String, String), String> {
+ let mut name_values = line.split(':');
+ let Some(name) = name_values.next() else {
+ return Err(format!(
+ "line does not follow 'key: values' format (line {})",
+ line_num
+ ));
+ };
+ let Some(values_line) = name_values.next() else {
+ return Err(format!(
+ "line does not follow 'key: values' format (line {})",
+ line_num
+ ));
+ };
+ if name_values.next().is_some() {
+ return Err(format!(
+ "line does not follow 'key: values' format (line {})",
+ line_num
+ ));
+ }
+
+ Ok((name.trim().to_string(), values_line.trim().to_string()))
+}
+
+// Returns the value contained in the given `string` as an hexadecimal number.
+// If it doesn't start with '$', then it's assumed to be a symbol reference that
+// can be found in `symbols`.
+fn get_hex_from(
+ string: &String,
+ line_num: usize,
+ symbols: &HashMap<String, usize>,
+) -> Result<usize, String> {
+ if string.is_empty() {
+ return Ok(0);
+ }
+
+ if !string.starts_with('$') {
+ match symbols.get(string) {
+ Some(value) => return Ok(*value),
+ None => return Err(format!("malformed hex value (line {})", line_num)),
+ }
+ }
+
+ let val = &string[1..string.len()];
+ if val.is_empty() || val.len() > 4 {
+ return Err(format!("malformed hex value (line {})", line_num));
+ }
+
+ Ok(usize::from_str_radix(val, 16).unwrap())
+}
+
+// Returns the RawMapping that can be extracted by treating the given `line` as
+// a line from the "Memory" section.
+fn fetch_memory_definition(line: &str, line_num: usize) -> Result<RawMapping, String> {
+ let (name, values) = fetch_line_values(line, line_num)?;
+ let mut res = RawMapping {
+ name,
+ start: String::from(""),
+ size: String::from(""),
+ fill: None,
+ ignore: false,
+ line_num,
+ };
+
+ for value in values.split(',') {
+ let mut key_value = value.trim().split('=');
+
+ match key_value.next() {
+ Some(key) => {
+ let val = key_value.next().unwrap_or("").trim();
+
+ match key.trim() {
+ "file" => res.ignore = val != "%O",
+ "fill" => {
+ if res.fill.is_none() {
+ res.fill = Some(String::from(""));
+ }
+ }
+ "fillval" => res.fill = Some(val.to_string()),
+ "start" => res.start = val.to_string(),
+ "size" => res.size = val.to_string(),
+ _ => {}
+ }
+ }
+ None => return Err(format!("malformed key-value (line {})", line_num)),
+ }
+ }
+
+ Ok(res)
+}
+
+// Returns a tuple with the name of the segment and the memory section being
+// referenced in this segment.
+fn fetch_segment_definition(line: &str, line_num: usize) -> Result<(String, String), String> {
+ let (name, values) = fetch_line_values(line, line_num)?;
+
+ if find_value("start", &values, line_num).is_ok() {
+ return Err("setting a 'start' at the segment level is not supported".to_string());
+ }
+ Ok((name, find_value("load", &values, line_num)?))
+}
+
+// Returns the value for the referenced `key` which is inside of the string of
+// `values`.
+fn find_value(key: &str, values: &str, line_num: usize) -> Result<String, String> {
+ for value in values.split(',') {
+ let mut key_value = value.trim().split('=');
+
+ match key_value.next() {
+ Some(k) => {
+ if k.trim() == key {
+ let val = key_value.next().unwrap_or("").trim();
+ return Ok(val.to_string());
+ }
+ }
+ None => return Err(format!("malformed key-value (line {})", line_num)),
+ }
+ }
+
+ Err(format!(
+ "could not find '{}' definition (line {})",
+ key, line_num
+ ))
+}
+
+// Parse the given blob of `text` as a .cfg file as it's expected by ld65:
+// https://www.cc65.org/doc/ld65-5.html.
+pub fn parse_cfg_file(text: &str) -> Result<Vec<Mapping>, String> {
+ let mut values = vec![];
+ let mut raw_segments = vec![];
+ let mut should_skip = false;
+ let mut section = None;
+ let mut symbols = HashMap::new();
+
+ // 1. Get the raw data and build the `symbols` hash.
+ //
+ // This is done by considering only three sections: symbols, memory, and segments.
+ for (idx, line) in text.lines().enumerate() {
+ // For this given line, maybe it's empty or it's a comment. Another case
+ // is that it's a section we just don't care. In either case, just skip
+ // to the next line.
+ let l = line.trim();
+ if should_skip {
+ if l == "}" {
+ // End of section, we might care about the next line.
+ should_skip = false;
+ }
+ continue;
+ }
+ if l.is_empty() || l.starts_with('#') {
+ continue;
+ }
+
+ // If we are inside of a section, parse it.
+ if section.is_some() {
+ if l == "}" {
+ should_skip = false;
+ section = None;
+ continue;
+ }
+
+ // Parse up until the closing semicolon, as passed it there might be
+ // an inline comment.
+ let tline = match l.find(';') {
+ Some(idx) => &l[0..idx],
+ None => {
+ return Err(format!(
+ "line does not end with a semicolon (line {})",
+ idx + 1
+ ))
+ }
+ };
+
+ // For each section there is a different action for the current
+ // line. Handle this now.
+ match section.as_ref().unwrap() {
+ Section::Memory => {
+ let definition = fetch_memory_definition(tline, idx + 1)?;
+ if !definition.ignore {
+ values.push(definition);
+ }
+ }
+ Section::Segment => raw_segments.push(fetch_segment_definition(tline, idx + 1)?),
+ Section::Symbol => {
+ let (name, values) = fetch_line_values(tline, idx + 1)?;
+ let value = find_value("value", &values, idx + 1)?;
+ symbols.insert(name, get_hex_from(&value, idx + 1, &symbols)?);
+ }
+ }
+
+ continue;
+ }
+
+ // We are not in a section and the line is not empty. Thus, this has to
+ // be a section definition. Parse the name and check if we actually have
+ // to care about it or not.
+ match l.find('{') {
+ Some(idx) => {
+ let id = l[0..idx].trim().to_lowercase();
+ match id.as_str() {
+ "memory" => section = Some(Section::Memory),
+ "segments" => section = Some(Section::Segment),
+ "symbols" => section = Some(Section::Symbol),
+ _ => should_skip = true,
+ }
+ }
+ None => {
+ return Err(format!(
+ "section must end with an opening bracket (line {})",
+ idx + 1
+ ))
+ }
+ };
+ }
+
+ let mut header = true;
+ let mut res = vec![];
+ let mut section_type = SectionType::Header;
+
+ // 2. Parse each data point so we can build a Mapping out of it.
+ for mapping in &values {
+ // We can now figure out the `start`, the `size` and the `fill`
+ // properties given that the `symbols` should all be known at this
+ // point.
+ let start = get_hex_from(&mapping.start, mapping.line_num, &symbols)? as u16;
+ let size = get_hex_from(&mapping.size, mapping.line_num, &symbols)?;
+ let fill = match &mapping.fill {
+ Some(f) => Some(get_hex_from(f, mapping.line_num, &symbols)? as u8),
+ None => None,
+ };
+
+ // The section type is a bit tricky because that's not something
+ // considered on ld65. Hence, we make these assumptions:
+ // 1. If it's the first mapping we see, then it's the header.
+ // 2. If after the header it claims to start at 0x00, then from now on
+ // it's CHR ROM.
+ // 3. Otherwise it's PRG ROM unless in a previous iteration we
+ // realized it's CHR ROM.
+ if header {
+ header = false;
+ } else if start == 0x00 {
+ section_type = SectionType::ChrRom;
+ } else if section_type != SectionType::ChrRom {
+ section_type = SectionType::PrgRom;
+ };
+
+ // Push in order the segments which are to be loaded on the current
+ // mapping.
+ let mut segments = vec![];
+ for (segment_name, load) in &raw_segments {
+ if mapping.name == *load {
+ segments.push(Segment::from(segment_name.as_str()));
+ }
+ }
+
+ res.push(Mapping {
+ name: mapping.name.clone(),
+ start,
+ size,
+ offset: 0,
+ fill,
+ segments,
+ section_type: section_type.clone(),
+ })
+ }
+
+ Ok(res)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn parse_default_cc65_configuration() {
+ let res = parse_cfg_file(
+ r#"
+SYMBOLS {
+ __STACKSIZE__: type = weak, value = $0300; # 3 pages stack
+}
+MEMORY {
+ ZP: file = "", start = $0002, size = $001A, type = rw, define = yes;
+
+ # INES Cartridge Header
+ HEADER: file = %O, start = $0000, size = $0010, fill = yes;
+
+ # 2 16K ROM Banks
+ # - startup
+ # - code
+ # - rodata
+ # - data (load)
+ ROM0: file = %O, start = $8000, size = $7FFA, fill = yes, define = yes;
+
+ # Hardware Vectors at End of 2nd 8K ROM
+ ROMV: file = %O, start = $FFFA, size = $0006, fill = yes;
+
+ # 1 8k CHR Bank
+ ROM2: file = %O, start = $0000, size = $2000, fill = yes;
+
+ # standard 2k SRAM (-zeropage)
+ # $0100-$0200 cpu stack
+ # $0200-$0500 3 pages for ppu memory write buffer
+ # $0500-$0800 3 pages for cc65 parameter stack
+ SRAM: file = "", start = $0500, size = __STACKSIZE__, define = yes;
+
+ # additional 8K SRAM Bank
+ # - data (run)
+ # - bss
+ # - heap
+ RAM: file = "", start = $6000, size = $2000, define = yes;
+}
+SEGMENTS {
+ ZEROPAGE: load = ZP, type = zp;
+ HEADER: load = HEADER, type = ro;
+ STARTUP: load = ROM0, type = ro, define = yes;
+ LOWCODE: load = ROM0, type = ro, optional = yes;
+ ONCE: load = ROM0, type = ro, optional = yes;
+ CODE: load = ROM0, type = ro, define = yes;
+ RODATA: load = ROM0, type = ro, define = yes;
+ DATA: load = ROM0, run = RAM, type = rw, define = yes;
+ VECTORS: load = ROMV, type = rw;
+ CHARS: load = ROM2, type = rw;
+ BSS: load = RAM, type = bss, define = yes;
+}
+FEATURES {
+ CONDES: type = constructor,
+ label = __CONSTRUCTOR_TABLE__,
+ count = __CONSTRUCTOR_COUNT__,
+ segment = ONCE;
+ CONDES: type = destructor,
+ label = __DESTRUCTOR_TABLE__,
+ count = __DESTRUCTOR_COUNT__,
+ segment = RODATA;
+ CONDES: type = interruptor,
+ label = __INTERRUPTOR_TABLE__,
+ count = __INTERRUPTOR_COUNT__,
+ segment = RODATA,
+ import = __CALLIRQ__;
+}
+"#,
+ )
+ .unwrap();
+
+ assert_eq!(res.len(), 4);
+
+ let header = &res[0];
+ assert_eq!(header.name, "HEADER");
+ assert_eq!(header.start, 0x00);
+ assert_eq!(header.size, 0x10);
+ assert_eq!(header.fill, Some(0x00));
+ assert_eq!(header.section_type, SectionType::Header);
+ assert_eq!(
+ header
+ .segments
+ .iter()
+ .map(|x| x.name.clone())
+ .collect::<Vec<_>>(),
+ &["HEADER"]
+ );
+
+ let rom0 = &res[1];
+ assert_eq!(rom0.name, "ROM0");
+ assert_eq!(rom0.start, 0x8000);
+ assert_eq!(rom0.size, 0x7FFA);
+ assert_eq!(rom0.fill, Some(0x00));
+ assert_eq!(rom0.section_type, SectionType::PrgRom);
+ assert_eq!(
+ rom0.segments
+ .iter()
+ .map(|x| x.name.clone())
+ .collect::<Vec<_>>(),
+ &["STARTUP", "LOWCODE", "ONCE", "CODE", "RODATA", "DATA"]
+ );
+
+ let romv = &res[2];
+ assert_eq!(romv.name, "ROMV");
+ assert_eq!(romv.start, 0xFFFA);
+ assert_eq!(romv.size, 0x06);
+ assert_eq!(romv.fill, Some(0x00));
+ assert_eq!(romv.section_type, SectionType::PrgRom);
+ assert_eq!(
+ romv.segments
+ .iter()
+ .map(|x| x.name.clone())
+ .collect::<Vec<_>>(),
+ &["VECTORS"]
+ );
+
+ let rom2 = &res[3];
+ assert_eq!(rom2.name, "ROM2");
+ assert_eq!(rom2.start, 0x0000);
+ assert_eq!(rom2.size, 0x2000);
+ assert_eq!(rom2.fill, Some(0x00));
+ assert_eq!(rom2.section_type, SectionType::ChrRom);
+ assert_eq!(
+ rom2.segments
+ .iter()
+ .map(|x| x.name.clone())
+ .collect::<Vec<_>>(),
+ &["CHARS"]
+ );
+ }
+
+ #[test]
+ fn parse_unrom_configuration() {
+ // Coming from https://github.com/mssola/code.nes.
+ let res = parse_cfg_file(
+ r#"
+##
+# This is a very minimalistic linker configuration for UNROM chips. This
+# configuration assumes the standard UNROM configuration, with two regions
+# defined that split $8000-$FFFF, where the first half is swappable. For the
+# bank switching there are 7 banks, which coupled with the fixed region it sums
+# up 128KB of total PRG-ROM.
+#
+# NOTE: it assumes that only assembly is being used, and thus a lot of magic
+# required for C programs is missing.
+# NOTE: it is following the example of games such as Castlevania or Megaman.
+# Thus, there is no CHR segment because it's all done through RAM.
+
+MEMORY {
+ # iNES header.
+ HEADER: start = $0, size = $10, fill = yes;
+
+ # Program RAM. Available if a battery-backed RAM was requested.
+ WRAM: file = "" start = $6000, size = $2000, define = yes;
+
+ # Swappable ROM addresses. Note the filled values. This is done so it's also
+ # clear by inspecting the memory which bank we are on. This can come in
+ # handy when inspecting things with an hex editor.
+ PRG0: start = $8000, size = $4000, fill = yes, fillval = $f8, define = yes;
+ PRG1: start = $8000, size = $4000, fill = yes, fillval = $f9, define = yes;
+ PRG2: start = $8000, size = $4000, fill = yes, fillval = $fa, define = yes;
+ PRG3: start = $8000, size = $4000, fill = yes, fillval = $fb, define = yes;
+ PRG4: start = $8000, size = $4000, fill = yes, fillval = $fc, define = yes;
+ PRG5: start = $8000, size = $4000, fill = yes, fillval = $fd, define = yes;
+ PRG6: start = $8000, size = $4000, fill = yes, fillval = $fe, define = yes;
+
+ # Fixed ROM address.
+ PRG: start = $C000, size = $4000, fill = yes, fillval = $ff, define = yes;
+}
+
+SEGMENTS {
+ # iNES header.
+ HEADER: load = HEADER, type = ro;
+
+ # This is the fixed bank, that spans $C000-$FFFF. Make sure to put the reset
+ # code and basic stuff that you don't want ever to be gone here. This will
+ # include stuff like the reset code, bank switching utilities, etc.
+ FIXED: load = PRG, type = ro, define = yes;
+
+ ##
+ # Swappable banks.
+
+ BANK0: load = PRG0, type = ro, define = yes;
+ BANK1: load = PRG1, type = ro, define = yes;
+ BANK2: load = PRG2, type = ro, define = yes;
+ BANK3: load = PRG3, type = ro, define = yes;
+ BANK4: load = PRG4, type = ro, define = yes;
+ BANK5: load = PRG5, type = ro, define = yes;
+ BANK6: load = PRG6, type = ro, define = yes;
+
+ # Last but not least, the vectors must be the last thing and they are
+ # expecting a very special place on the fixed bank.
+ VECTORS: load = PRG, type = ro;
+}
+"#,
+ )
+ .unwrap();
+
+ assert_eq!(res.len(), 9);
+
+ let header = &res[0];
+ assert_eq!(header.name, "HEADER");
+ assert_eq!(header.start, 0x00);
+ assert_eq!(header.size, 0x10);
+ assert_eq!(header.fill, Some(0x00));
+ assert_eq!(header.section_type, SectionType::Header);
+ assert_eq!(
+ header
+ .segments
+ .iter()
+ .map(|x| x.name.clone())
+ .collect::<Vec<_>>(),
+ &["HEADER"]
+ );
+
+ for i in 1..=7 {
+ let prg = &res[i];
+
+ assert_eq!(prg.name, format!("PRG{}", i - 1));
+ assert_eq!(prg.start, 0x8000);
+ assert_eq!(prg.size, 0x4000);
+ assert_eq!(prg.fill, Some(0xf8 + i as u8 - 1));
+ assert_eq!(prg.section_type, SectionType::PrgRom);
+ assert_eq!(
+ prg.segments
+ .iter()
+ .map(|x| x.name.clone())
+ .collect::<Vec<_>>(),
+ &[format!("BANK{}", i - 1)]
+ );
+ }
+
+ let prg = &res[8];
+ assert_eq!(prg.name, "PRG");
+ assert_eq!(prg.start, 0xC000);
+ assert_eq!(prg.size, 0x4000);
+ assert_eq!(prg.fill, Some(0xFF));
+ assert_eq!(prg.section_type, SectionType::PrgRom);
+ assert_eq!(
+ prg.segments
+ .iter()
+ .map(|x| x.name.clone())
+ .collect::<Vec<_>>(),
+ &["FIXED", "VECTORS"]
+ );
+ }
+}
diff --git a/lib/xixanta/src/lib.rs b/lib/xixanta/src/lib.rs
index ce9be6d..6f1fc86 100644
--- a/lib/xixanta/src/lib.rs
+++ b/lib/xixanta/src/lib.rs
@@ -77,6 +77,7 @@ pub mod object;
pub mod opcodes;
pub mod parser;
+mod cfg;
/// Mapping defines structures for laying out how the code will be assembled
/// both on memory and on the ROM file itself. Notice that we take a different
/// approach than 'cc65' because that compiler has to take into account a
diff --git a/lib/xixanta/src/mapping.rs b/lib/xixanta/src/mapping.rs
index f86b513..49683ab 100644
--- a/lib/xixanta/src/mapping.rs
+++ b/lib/xixanta/src/mapping.rs
@@ -1,3 +1,4 @@
+use crate::cfg::parse_cfg_file;
use crate::object::Bundle;
use toml::{Table, Value};
@@ -28,8 +29,9 @@ pub struct Segment {
/// Name of the segment.
pub name: String,
+ /// Offset from the base address from where to push the next address for
+ /// this segment.
pub offset: usize,
- pub len: usize,
/// Bundles that have been generated when assembling the nodes that have
/// been parsed by a previous step.
@@ -41,7 +43,6 @@ impl From<&str> for Segment {
Segment {
name: name.to_string(),
offset: 0,
- len: 0,
bundles: vec![],
}
}
@@ -102,7 +103,15 @@ pub struct Mapping {
pub fn get_mapping_configuration(name: &str) -> Result<Vec<Mapping>, String> {
let configuration = if std::fs::exists(name).unwrap_or(false) {
match std::fs::read_to_string(name) {
- Ok(contents) => load_configuration_for(contents.as_str())?,
+ Ok(contents) => {
+ // If this is a file, then it might not be a TOML one, but a
+ // .cfg as used by cc65.
+ if name.ends_with(".cfg") {
+ parse_cfg_file(contents.as_str())?
+ } else {
+ load_configuration_for(contents.as_str())?
+ }
+ }
Err(_) => return Err(format!("could not read '{}'", name)),
}
} else {