diff --git a/Cargo.lock b/Cargo.lock index 13c69f7..f903bda 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -395,6 +395,15 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bimap" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "230c5f1ca6a325a32553f8640d31ac9b49f2411e901e427570154868b46da4f7" +dependencies = [ + "serde", +] + [[package]] name = "bit-set" version = "0.6.0" @@ -2852,7 +2861,7 @@ dependencies = [ [[package]] name = "objdiff-cli" -version = "2.2.2" +version = "2.3.0" dependencies = [ "anyhow", "argp", @@ -2874,10 +2883,11 @@ dependencies = [ [[package]] name = "objdiff-core" -version = "2.2.2" +version = "2.3.0" dependencies = [ "anyhow", "arm-attr", + "bimap", "byteorder", "console_error_panic_hook", "console_log", @@ -2913,7 +2923,7 @@ dependencies = [ [[package]] name = "objdiff-gui" -version = "2.2.2" +version = "2.3.0" dependencies = [ "anyhow", "bytes", diff --git a/Cargo.toml b/Cargo.toml index b395ae2..98a6055 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ strip = "debuginfo" codegen-units = 1 [workspace.package] -version = "2.2.2" +version = "2.3.0" authors = ["Luke Street "] edition = "2021" license = "MIT OR Apache-2.0" diff --git a/config.schema.json b/config.schema.json index 41addf3..eaec9fa 100644 --- a/config.schema.json +++ b/config.schema.json @@ -133,6 +133,13 @@ }, "metadata": { "ref": "#/$defs/metadata" + }, + "symbol_mappings": { + "type": "object", + "description": "Manual symbol mappings from target to base.", + "additionalProperties": { + "type": "string" + } } } }, diff --git a/objdiff-cli/src/cmd/diff.rs b/objdiff-cli/src/cmd/diff.rs index d8530f7..c24dff4 100644 --- a/objdiff-cli/src/cmd/diff.rs +++ b/objdiff-cli/src/cmd/diff.rs @@ -102,26 +102,32 @@ pub fn run(args: Args) -> Result<()> { let unit_path = PathBuf::from_str(u).ok().and_then(|p| fs::canonicalize(p).ok()); - let Some(object) = project_config.objects.iter_mut().find_map(|obj| { - if obj.name.as_deref() == Some(u) { - resolve_paths(obj); - return Some(obj); - } + let Some(object) = project_config + .units + .as_deref_mut() + .unwrap_or_default() + .iter_mut() + .find_map(|obj| { + if obj.name.as_deref() == Some(u) { + resolve_paths(obj); + return Some(obj); + } - let up = unit_path.as_deref()?; + let up = unit_path.as_deref()?; - resolve_paths(obj); + resolve_paths(obj); - if [&obj.base_path, &obj.target_path] - .into_iter() - .filter_map(|p| p.as_ref().and_then(|p| p.canonicalize().ok())) - .any(|p| p == up) - { - return Some(obj); - } + if [&obj.base_path, &obj.target_path] + .into_iter() + .filter_map(|p| p.as_ref().and_then(|p| p.canonicalize().ok())) + .any(|p| p == up) + { + return Some(obj); + } - None - }) else { + None + }) + else { bail!("Unit not found: {}", u) }; @@ -129,7 +135,13 @@ pub fn run(args: Args) -> Result<()> { } else if let Some(symbol_name) = &args.symbol { let mut idx = None; let mut count = 0usize; - for (i, obj) in project_config.objects.iter_mut().enumerate() { + for (i, obj) in project_config + .units + .as_deref_mut() + .unwrap_or_default() + .iter_mut() + .enumerate() + { resolve_paths(obj); if obj @@ -148,7 +160,7 @@ pub fn run(args: Args) -> Result<()> { } match (count, idx) { (0, None) => bail!("Symbol not found: {}", symbol_name), - (1, Some(i)) => &mut project_config.objects[i], + (1, Some(i)) => &mut project_config.units_mut()[i], (2.., Some(_)) => bail!( "Multiple instances of {} were found, try specifying a unit", symbol_name @@ -303,7 +315,7 @@ fn find_function(obj: &ObjInfo, name: &str) -> Option { None } -#[allow(dead_code)] +#[expect(dead_code)] struct FunctionDiffUi { relax_reloc_diffs: bool, left_highlight: HighlightKind, @@ -758,7 +770,7 @@ impl FunctionDiffUi { self.scroll_y += self.per_page / if half { 2 } else { 1 }; } - #[allow(clippy::too_many_arguments)] + #[expect(clippy::too_many_arguments)] fn print_sym( &self, out: &mut Text<'static>, diff --git a/objdiff-cli/src/cmd/report.rs b/objdiff-cli/src/cmd/report.rs index 169e3da..68e6c28 100644 --- a/objdiff-cli/src/cmd/report.rs +++ b/objdiff-cli/src/cmd/report.rs @@ -94,7 +94,7 @@ fn generate(args: GenerateArgs) -> Result<()> { }; info!( "Generating report for {} units (using {} threads)", - project.objects.len(), + project.units().len(), if args.deduplicate { 1 } else { rayon::current_num_threads() } ); @@ -103,7 +103,7 @@ fn generate(args: GenerateArgs) -> Result<()> { let mut existing_functions: HashSet = HashSet::new(); if args.deduplicate { // If deduplicating, we need to run single-threaded - for object in &mut project.objects { + for object in project.units.as_deref_mut().unwrap_or_default() { if let Some(unit) = report_object( object, project_dir, @@ -116,7 +116,9 @@ fn generate(args: GenerateArgs) -> Result<()> { } } else { let vec = project - .objects + .units + .as_deref_mut() + .unwrap_or_default() .par_iter_mut() .map(|object| { report_object( @@ -132,7 +134,7 @@ fn generate(args: GenerateArgs) -> Result<()> { } let measures = units.iter().flat_map(|u| u.measures.into_iter()).collect(); let mut categories = Vec::new(); - for category in &project.progress_categories { + for category in project.progress_categories() { categories.push(ReportCategory { id: category.id.clone(), name: category.name.clone(), diff --git a/objdiff-core/Cargo.toml b/objdiff-core/Cargo.toml index b1dd465..7b4deb9 100644 --- a/objdiff-core/Cargo.toml +++ b/objdiff-core/Cargo.toml @@ -17,8 +17,8 @@ crate-type = ["cdylib", "rlib"] [features] all = ["config", "dwarf", "mips", "ppc", "x86", "arm", "bindings"] -any-arch = [] # Implicit, used to check if any arch is enabled -config = ["globset", "semver", "serde_json", "serde_yaml"] +any-arch = ["bimap"] # Implicit, used to check if any arch is enabled +config = ["bimap", "globset", "semver", "serde_json", "serde_yaml"] dwarf = ["gimli"] mips = ["any-arch", "rabbitizer"] ppc = ["any-arch", "cwdemangle", "cwextab", "ppc750cl"] @@ -32,6 +32,7 @@ features = ["all"] [dependencies] anyhow = "1.0" +bimap = { version = "0.6", features = ["serde"], optional = true } byteorder = "1.5" filetime = "0.2" flagset = "0.4" diff --git a/objdiff-core/src/config/mod.rs b/objdiff-core/src/config/mod.rs index c8826d4..09d064b 100644 --- a/objdiff-core/src/config/mod.rs +++ b/objdiff-core/src/config/mod.rs @@ -1,77 +1,100 @@ use std::{ + fs, fs::File, - io::{BufReader, Read}, + io::{BufReader, BufWriter, Read}, path::{Path, PathBuf}, }; use anyhow::{anyhow, Context, Result}; +use bimap::BiBTreeMap; use filetime::FileTime; use globset::{Glob, GlobSet, GlobSetBuilder}; -#[inline] -fn bool_true() -> bool { true } - -#[derive(Default, Clone, serde::Deserialize)] +#[derive(Default, Clone, serde::Serialize, serde::Deserialize)] pub struct ProjectConfig { - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub min_version: Option, - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub custom_make: Option, - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub custom_args: Option>, - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub target_dir: Option, - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub base_dir: Option, - #[serde(default = "bool_true")] - pub build_base: bool, - #[serde(default)] - pub build_target: bool, - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] + pub build_base: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub build_target: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub watch_patterns: Option>, - #[serde(default, alias = "units")] - pub objects: Vec, - #[serde(default)] - pub progress_categories: Vec, + #[serde(default, alias = "objects", skip_serializing_if = "Option::is_none")] + pub units: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub progress_categories: Option>, +} + +impl ProjectConfig { + #[inline] + pub fn units(&self) -> &[ProjectObject] { self.units.as_deref().unwrap_or_default() } + + #[inline] + pub fn units_mut(&mut self) -> &mut Vec { + self.units.get_or_insert_with(Vec::new) + } + + #[inline] + pub fn progress_categories(&self) -> &[ProjectProgressCategory] { + self.progress_categories.as_deref().unwrap_or_default() + } + + #[inline] + pub fn progress_categories_mut(&mut self) -> &mut Vec { + self.progress_categories.get_or_insert_with(Vec::new) + } } -#[derive(Default, Clone, serde::Deserialize)] +#[derive(Default, Clone, serde::Serialize, serde::Deserialize)] pub struct ProjectObject { - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub name: Option, - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub path: Option, - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub target_path: Option, - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub base_path: Option, - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] #[deprecated(note = "Use metadata.reverse_fn_order")] pub reverse_fn_order: Option, - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] #[deprecated(note = "Use metadata.complete")] pub complete: Option, - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub scratch: Option, - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub metadata: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub symbol_mappings: Option, } -#[derive(Default, Clone, serde::Deserialize)] +pub type SymbolMappings = BiBTreeMap; + +#[derive(Default, Clone, serde::Serialize, serde::Deserialize)] pub struct ProjectObjectMetadata { - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub complete: Option, - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub reverse_fn_order: Option, - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub source_path: Option, - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub progress_categories: Option>, - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub auto_generated: Option, } -#[derive(Default, Clone, serde::Deserialize)] +#[derive(Default, Clone, serde::Serialize, serde::Deserialize)] pub struct ProjectProgressCategory { #[serde(default)] pub id: String, @@ -112,12 +135,12 @@ impl ProjectObject { } pub fn complete(&self) -> Option { - #[allow(deprecated)] + #[expect(deprecated)] self.metadata.as_ref().and_then(|m| m.complete).or(self.complete) } pub fn reverse_fn_order(&self) -> Option { - #[allow(deprecated)] + #[expect(deprecated)] self.metadata.as_ref().and_then(|m| m.reverse_fn_order).or(self.reverse_fn_order) } @@ -132,16 +155,16 @@ impl ProjectObject { #[derive(Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)] pub struct ScratchConfig { - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub platform: Option, - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub compiler: Option, - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub c_flags: Option, - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub ctx_path: Option, - #[serde(default)] - pub build_ctx: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub build_ctx: Option, } pub const CONFIG_FILENAMES: [&str; 3] = ["objdiff.json", "objdiff.yml", "objdiff.yaml"]; @@ -154,7 +177,7 @@ pub const DEFAULT_WATCH_PATTERNS: &[&str] = &[ #[derive(Clone, Eq, PartialEq)] pub struct ProjectConfigInfo { pub path: PathBuf, - pub timestamp: FileTime, + pub timestamp: Option, } pub fn try_project_config(dir: &Path) -> Option<(Result, ProjectConfigInfo)> { @@ -180,12 +203,41 @@ pub fn try_project_config(dir: &Path) -> Option<(Result, ProjectC result = Err(e); } } - return Some((result, ProjectConfigInfo { path: config_path, timestamp: ts })); + return Some((result, ProjectConfigInfo { path: config_path, timestamp: Some(ts) })); } } None } +pub fn save_project_config( + config: &ProjectConfig, + info: &ProjectConfigInfo, +) -> Result { + if let Some(last_ts) = info.timestamp { + // Check if the file has changed since we last read it + if let Ok(metadata) = fs::metadata(&info.path) { + let ts = FileTime::from_last_modification_time(&metadata); + if ts != last_ts { + return Err(anyhow!("Config file has changed since last read")); + } + } + } + let mut writer = + BufWriter::new(File::create(&info.path).context("Failed to create config file")?); + let ext = info.path.extension().and_then(|ext| ext.to_str()).unwrap_or("json"); + match ext { + "json" => serde_json::to_writer_pretty(&mut writer, config).context("Failed to write JSON"), + "yml" | "yaml" => { + serde_yaml::to_writer(&mut writer, config).context("Failed to write YAML") + } + _ => Err(anyhow!("Unknown config file extension: {ext}")), + }?; + let file = writer.into_inner().context("Failed to flush file")?; + let metadata = file.metadata().context("Failed to get file metadata")?; + let ts = FileTime::from_last_modification_time(&metadata); + Ok(ProjectConfigInfo { path: info.path.clone(), timestamp: Some(ts) }) +} + fn validate_min_version(config: &ProjectConfig) -> Result<()> { let Some(min_version) = &config.min_version else { return Ok(()) }; let version = semver::Version::parse(env!("CARGO_PKG_VERSION")) diff --git a/objdiff-core/src/diff/code.rs b/objdiff-core/src/diff/code.rs index 8e99bd5..795c296 100644 --- a/objdiff-core/src/diff/code.rs +++ b/objdiff-core/src/diff/code.rs @@ -41,7 +41,7 @@ pub fn no_diff_code(out: &ProcessCodeResult, symbol_ref: SymbolRef) -> Result= total { 0.0 } else { @@ -77,13 +77,13 @@ pub fn diff_code( Ok(( ObjSymbolDiff { symbol_ref: left_symbol_ref, - diff_symbol: Some(right_symbol_ref), + target_symbol: Some(right_symbol_ref), instructions: left_diff, match_percent: Some(percent), }, ObjSymbolDiff { symbol_ref: right_symbol_ref, - diff_symbol: Some(left_symbol_ref), + target_symbol: Some(left_symbol_ref), instructions: right_diff, match_percent: Some(percent), }, @@ -211,7 +211,7 @@ fn arg_eq( left_diff: &ObjInsDiff, right_diff: &ObjInsDiff, ) -> bool { - return match left { + match left { ObjInsArg::PlainText(l) => match right { ObjInsArg::PlainText(r) => l == r, _ => false, @@ -236,7 +236,7 @@ fn arg_eq( left_diff.branch_to.as_ref().map(|b| b.ins_idx) == right_diff.branch_to.as_ref().map(|b| b.ins_idx) } - }; + } } #[derive(Default)] diff --git a/objdiff-core/src/diff/data.rs b/objdiff-core/src/diff/data.rs index 4498bff..8498439 100644 --- a/objdiff-core/src/diff/data.rs +++ b/objdiff-core/src/diff/data.rs @@ -20,13 +20,13 @@ pub fn diff_bss_symbol( Ok(( ObjSymbolDiff { symbol_ref: left_symbol_ref, - diff_symbol: Some(right_symbol_ref), + target_symbol: Some(right_symbol_ref), instructions: vec![], match_percent: Some(percent), }, ObjSymbolDiff { symbol_ref: right_symbol_ref, - diff_symbol: Some(left_symbol_ref), + target_symbol: Some(left_symbol_ref), instructions: vec![], match_percent: Some(percent), }, @@ -34,7 +34,7 @@ pub fn diff_bss_symbol( } pub fn no_diff_symbol(_obj: &ObjInfo, symbol_ref: SymbolRef) -> ObjSymbolDiff { - ObjSymbolDiff { symbol_ref, diff_symbol: None, instructions: vec![], match_percent: None } + ObjSymbolDiff { symbol_ref, target_symbol: None, instructions: vec![], match_percent: None } } /// Compare the data sections of two object files. @@ -158,13 +158,13 @@ pub fn diff_data_symbol( Ok(( ObjSymbolDiff { symbol_ref: left_symbol_ref, - diff_symbol: Some(right_symbol_ref), + target_symbol: Some(right_symbol_ref), instructions: vec![], match_percent: Some(match_percent), }, ObjSymbolDiff { symbol_ref: right_symbol_ref, - diff_symbol: Some(left_symbol_ref), + target_symbol: Some(left_symbol_ref), instructions: vec![], match_percent: Some(match_percent), }, diff --git a/objdiff-core/src/diff/display.rs b/objdiff-core/src/diff/display.rs index 40a1ad4..3435181 100644 --- a/objdiff-core/src/diff/display.rs +++ b/objdiff-core/src/diff/display.rs @@ -29,7 +29,7 @@ pub enum DiffText<'a> { Eol, } -#[derive(Default, Clone, PartialEq, Eq)] +#[derive(Debug, Default, Clone, PartialEq, Eq)] pub enum HighlightKind { #[default] None, diff --git a/objdiff-core/src/diff/mod.rs b/objdiff-core/src/diff/mod.rs index caa35b5..f9e5783 100644 --- a/objdiff-core/src/diff/mod.rs +++ b/objdiff-core/src/diff/mod.rs @@ -3,6 +3,7 @@ use std::collections::HashSet; use anyhow::Result; use crate::{ + config::SymbolMappings, diff::{ code::{diff_code, no_diff_code, process_code_symbol}, data::{ @@ -161,6 +162,8 @@ pub struct DiffObjConfig { #[serde(default = "default_true")] pub space_between_args: bool, pub combine_data_sections: bool, + #[serde(default)] + pub symbol_mappings: MappingConfig, // x86 pub x86_formatter: X86Formatter, // MIPS @@ -182,6 +185,7 @@ impl Default for DiffObjConfig { relax_reloc_diffs: false, space_between_args: true, combine_data_sections: false, + symbol_mappings: Default::default(), x86_formatter: Default::default(), mips_abi: Default::default(), mips_instr_category: Default::default(), @@ -223,8 +227,10 @@ impl ObjSectionDiff { #[derive(Debug, Clone, Default)] pub struct ObjSymbolDiff { + /// The symbol ref this object pub symbol_ref: SymbolRef, - pub diff_symbol: Option, + /// The symbol ref in the _other_ object that this symbol was diffed against + pub target_symbol: Option, pub instructions: Vec, pub match_percent: Option, } @@ -294,8 +300,13 @@ pub struct ObjInsBranchTo { #[derive(Default)] pub struct ObjDiff { + /// A list of all section diffs in the object. pub sections: Vec, + /// Common BSS symbols don't live in a section, so they're stored separately. pub common: Vec, + /// If `selecting_left` or `selecting_right` is set, this is the list of symbols + /// that are being mapped to the other object. + pub mapping_symbols: Vec, } impl ObjDiff { @@ -303,13 +314,14 @@ impl ObjDiff { let mut result = Self { sections: Vec::with_capacity(obj.sections.len()), common: Vec::with_capacity(obj.common.len()), + mapping_symbols: vec![], }; for (section_idx, section) in obj.sections.iter().enumerate() { let mut symbols = Vec::with_capacity(section.symbols.len()); for (symbol_idx, _) in section.symbols.iter().enumerate() { symbols.push(ObjSymbolDiff { symbol_ref: SymbolRef { section_idx, symbol_idx }, - diff_symbol: None, + target_symbol: None, instructions: vec![], match_percent: None, }); @@ -328,7 +340,7 @@ impl ObjDiff { for (symbol_idx, _) in obj.common.iter().enumerate() { result.common.push(ObjSymbolDiff { symbol_ref: SymbolRef { section_idx: obj.sections.len(), symbol_idx }, - diff_symbol: None, + target_symbol: None, instructions: vec![], match_percent: None, }); @@ -378,7 +390,7 @@ pub fn diff_objs( right: Option<&ObjInfo>, prev: Option<&ObjInfo>, ) -> Result { - let symbol_matches = matching_symbols(left, right, prev)?; + let symbol_matches = matching_symbols(left, right, prev, &config.symbol_mappings)?; let section_matches = matching_sections(left, right)?; let mut left = left.map(|p| (p, ObjDiff::new_from_obj(p))); let mut right = right.map(|p| (p, ObjDiff::new_from_obj(p))); @@ -529,6 +541,17 @@ pub fn diff_objs( } } + if let (Some((right_obj, right_out)), Some((left_obj, left_out))) = + (right.as_mut(), left.as_mut()) + { + if let Some(right_name) = &config.symbol_mappings.selecting_left { + generate_mapping_symbols(right_obj, right_name, left_obj, left_out, config)?; + } + if let Some(left_name) = &config.symbol_mappings.selecting_right { + generate_mapping_symbols(left_obj, left_name, right_obj, right_out, config)?; + } + } + Ok(DiffObjsResult { left: left.map(|(_, o)| o), right: right.map(|(_, o)| o), @@ -536,6 +559,63 @@ pub fn diff_objs( }) } +/// When we're selecting a symbol to use as a comparison, we'll create comparisons for all +/// symbols in the other object that match the selected symbol's section and kind. This allows +/// us to display match percentages for all symbols in the other object that could be selected. +fn generate_mapping_symbols( + base_obj: &ObjInfo, + base_name: &str, + target_obj: &ObjInfo, + target_out: &mut ObjDiff, + config: &DiffObjConfig, +) -> Result<()> { + let Some(base_symbol_ref) = symbol_ref_by_name(base_obj, base_name) else { + return Ok(()); + }; + let (base_section, base_symbol) = base_obj.section_symbol(base_symbol_ref); + let Some(base_section) = base_section else { + return Ok(()); + }; + let base_code = match base_section.kind { + ObjSectionKind::Code => Some(process_code_symbol(base_obj, base_symbol_ref, config)?), + _ => None, + }; + for (target_section_index, target_section) in + target_obj.sections.iter().enumerate().filter(|(_, s)| s.kind == base_section.kind) + { + for (target_symbol_index, _target_symbol) in + target_section.symbols.iter().enumerate().filter(|(_, s)| s.kind == base_symbol.kind) + { + let target_symbol_ref = + SymbolRef { section_idx: target_section_index, symbol_idx: target_symbol_index }; + match base_section.kind { + ObjSectionKind::Code => { + let target_code = process_code_symbol(target_obj, target_symbol_ref, config)?; + let (left_diff, _right_diff) = diff_code( + &target_code, + base_code.as_ref().unwrap(), + target_symbol_ref, + base_symbol_ref, + config, + )?; + target_out.mapping_symbols.push(left_diff); + } + ObjSectionKind::Data => { + let (left_diff, _right_diff) = + diff_data_symbol(target_obj, base_obj, target_symbol_ref, base_symbol_ref)?; + target_out.mapping_symbols.push(left_diff); + } + ObjSectionKind::Bss => { + let (left_diff, _right_diff) = + diff_bss_symbol(target_obj, base_obj, target_symbol_ref, base_symbol_ref)?; + target_out.mapping_symbols.push(left_diff); + } + } + } + } + Ok(()) +} + #[derive(Copy, Clone, Eq, PartialEq)] struct SymbolMatch { left: Option, @@ -551,19 +631,115 @@ struct SectionMatch { section_kind: ObjSectionKind, } +#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, serde::Deserialize, serde::Serialize)] +pub struct MappingConfig { + /// Manual symbol mappings + pub mappings: SymbolMappings, + /// The right object symbol name that we're selecting a left symbol for + pub selecting_left: Option, + /// The left object symbol name that we're selecting a right symbol for + pub selecting_right: Option, +} + +fn symbol_ref_by_name(obj: &ObjInfo, name: &str) -> Option { + for (section_idx, section) in obj.sections.iter().enumerate() { + for (symbol_idx, symbol) in section.symbols.iter().enumerate() { + if symbol.name == name { + return Some(SymbolRef { section_idx, symbol_idx }); + } + } + } + None +} + +fn apply_symbol_mappings( + left: &ObjInfo, + right: &ObjInfo, + mapping_config: &MappingConfig, + left_used: &mut HashSet, + right_used: &mut HashSet, + matches: &mut Vec, +) -> Result<()> { + // If we're selecting a symbol to use as a comparison, mark it as used + // This ensures that we don't match it to another symbol at any point + if let Some(left_name) = &mapping_config.selecting_left { + if let Some(left_symbol) = symbol_ref_by_name(left, left_name) { + left_used.insert(left_symbol); + } + } + if let Some(right_name) = &mapping_config.selecting_right { + if let Some(right_symbol) = symbol_ref_by_name(right, right_name) { + right_used.insert(right_symbol); + } + } + + // Apply manual symbol mappings + for (left_name, right_name) in &mapping_config.mappings { + let Some(left_symbol) = symbol_ref_by_name(left, left_name) else { + continue; + }; + if left_used.contains(&left_symbol) { + continue; + } + let Some(right_symbol) = symbol_ref_by_name(right, right_name) else { + continue; + }; + if right_used.contains(&right_symbol) { + continue; + } + let left_section = &left.sections[left_symbol.section_idx]; + let right_section = &right.sections[right_symbol.section_idx]; + if left_section.kind != right_section.kind { + log::warn!( + "Symbol section kind mismatch: {} ({:?}) vs {} ({:?})", + left_name, + left_section.kind, + right_name, + right_section.kind + ); + continue; + } + matches.push(SymbolMatch { + left: Some(left_symbol), + right: Some(right_symbol), + prev: None, // TODO + section_kind: left_section.kind, + }); + left_used.insert(left_symbol); + right_used.insert(right_symbol); + } + Ok(()) +} + /// Find matching symbols between each object. fn matching_symbols( left: Option<&ObjInfo>, right: Option<&ObjInfo>, prev: Option<&ObjInfo>, + mappings: &MappingConfig, ) -> Result> { let mut matches = Vec::new(); + let mut left_used = HashSet::new(); let mut right_used = HashSet::new(); if let Some(left) = left { + if let Some(right) = right { + apply_symbol_mappings( + left, + right, + mappings, + &mut left_used, + &mut right_used, + &mut matches, + )?; + } for (section_idx, section) in left.sections.iter().enumerate() { for (symbol_idx, symbol) in section.symbols.iter().enumerate() { + let symbol_ref = SymbolRef { section_idx, symbol_idx }; + if left_used.contains(&symbol_ref) { + continue; + } let symbol_match = SymbolMatch { - left: Some(SymbolRef { section_idx, symbol_idx }), + left: Some(symbol_ref), right: find_symbol(right, symbol, section, Some(&right_used)), prev: find_symbol(prev, symbol, section, None), section_kind: section.kind, @@ -575,8 +751,12 @@ fn matching_symbols( } } for (symbol_idx, symbol) in left.common.iter().enumerate() { + let symbol_ref = SymbolRef { section_idx: left.sections.len(), symbol_idx }; + if left_used.contains(&symbol_ref) { + continue; + } let symbol_match = SymbolMatch { - left: Some(SymbolRef { section_idx: left.sections.len(), symbol_idx }), + left: Some(symbol_ref), right: find_common_symbol(right, symbol), prev: find_common_symbol(prev, symbol), section_kind: ObjSectionKind::Bss, diff --git a/objdiff-core/src/obj/mod.rs b/objdiff-core/src/obj/mod.rs index 6e96d85..68af116 100644 --- a/objdiff-core/src/obj/mod.rs +++ b/objdiff-core/src/obj/mod.rs @@ -112,6 +112,15 @@ pub struct ObjIns { pub orig: Option, } +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)] +pub enum ObjSymbolKind { + #[default] + Unknown, + Function, + Object, + Section, +} + #[derive(Debug, Clone)] pub struct ObjSymbol { pub name: String, @@ -120,6 +129,7 @@ pub struct ObjSymbol { pub section_address: u64, pub size: u64, pub size_known: bool, + pub kind: ObjSymbolKind, pub flags: ObjSymbolFlagSet, pub addend: i64, /// Original virtual address (from .note.split section) diff --git a/objdiff-core/src/obj/read.rs b/objdiff-core/src/obj/read.rs index c7d4265..6154bf5 100644 --- a/objdiff-core/src/obj/read.rs +++ b/objdiff-core/src/obj/read.rs @@ -23,6 +23,7 @@ use crate::{ obj::{ split_meta::{SplitMeta, SPLITMETA_SECTION}, ObjInfo, ObjReloc, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlagSet, ObjSymbolFlags, + ObjSymbolKind, }, util::{read_u16, read_u32}, }; @@ -94,6 +95,13 @@ fn to_obj_symbol( }) .unwrap_or(&[]); + let kind = match symbol.kind() { + SymbolKind::Text => ObjSymbolKind::Function, + SymbolKind::Data => ObjSymbolKind::Object, + SymbolKind::Section => ObjSymbolKind::Section, + _ => ObjSymbolKind::Unknown, + }; + Ok(ObjSymbol { name: name.to_string(), demangled_name, @@ -101,6 +109,7 @@ fn to_obj_symbol( section_address, size: symbol.size(), size_known: symbol.size() != 0, + kind, flags, addend, virtual_address, @@ -173,12 +182,19 @@ fn symbols_by_section( result.sort_by(|a, b| a.address.cmp(&b.address).then(a.size.cmp(&b.size))); let mut iter = result.iter_mut().peekable(); while let Some(symbol) = iter.next() { - if symbol.size == 0 { + if symbol.kind == ObjSymbolKind::Unknown && symbol.size == 0 { if let Some(next_symbol) = iter.peek() { symbol.size = next_symbol.address - symbol.address; } else { symbol.size = (section.address + section.size) - symbol.address; } + // Set symbol kind if we ended up with a non-zero size + if symbol.size > 0 { + symbol.kind = match section.kind { + ObjSectionKind::Code => ObjSymbolKind::Function, + ObjSectionKind::Data | ObjSectionKind::Bss => ObjSymbolKind::Object, + }; + } } } if result.is_empty() { @@ -196,6 +212,10 @@ fn symbols_by_section( section_address: 0, size: section.size, size_known: true, + kind: match section.kind { + ObjSectionKind::Code => ObjSymbolKind::Function, + ObjSectionKind::Data | ObjSectionKind::Bss => ObjSymbolKind::Object, + }, flags: Default::default(), addend: 0, virtual_address: None, @@ -281,6 +301,7 @@ fn find_section_symbol( section_address: 0, size: 0, size_known: false, + kind: ObjSymbolKind::Section, flags: Default::default(), addend: address as i64 - section.address() as i64, virtual_address: None, @@ -568,6 +589,7 @@ fn update_combined_symbol(symbol: ObjSymbol, address_change: i64) -> Result, @@ -98,6 +99,23 @@ pub struct ObjectConfig { pub complete: Option, pub scratch: Option, pub source_path: Option, + #[serde(default)] + pub symbol_mappings: SymbolMappings, +} + +impl From<&ProjectObject> for ObjectConfig { + fn from(object: &ProjectObject) -> Self { + Self { + name: object.name().to_string(), + target_path: object.target_path.clone(), + base_path: object.base_path.clone(), + reverse_fn_order: object.reverse_fn_order(), + complete: object.complete(), + scratch: object.scratch.clone(), + source_path: object.source_path().cloned(), + symbol_mappings: object.symbol_mappings.clone().unwrap_or_default(), + } + } } #[inline] @@ -117,8 +135,14 @@ pub struct AppState { pub obj_change: bool, pub queue_build: bool, pub queue_reload: bool, + pub current_project_config: Option, pub project_config_info: Option, pub last_mod_check: Instant, + /// The right object symbol name that we're selecting a left symbol for + pub selecting_left: Option, + /// The left object symbol name that we're selecting a right symbol for + pub selecting_right: Option, + pub config_error: Option, } impl Default for AppState { @@ -132,8 +156,12 @@ impl Default for AppState { obj_change: false, queue_build: false, queue_reload: false, + current_project_config: None, project_config_info: None, last_mod_check: Instant::now(), + selecting_left: None, + selecting_right: None, + config_error: None, } } } @@ -214,7 +242,10 @@ impl AppState { self.config_change = true; self.obj_change = true; self.queue_build = false; + self.current_project_config = None; self.project_config_info = None; + self.selecting_left = None; + self.selecting_right = None; } pub fn set_target_obj_dir(&mut self, path: PathBuf) { @@ -222,6 +253,8 @@ impl AppState { self.config.selected_obj = None; self.obj_change = true; self.queue_build = false; + self.selecting_left = None; + self.selecting_right = None; } pub fn set_base_obj_dir(&mut self, path: PathBuf) { @@ -229,12 +262,122 @@ impl AppState { self.config.selected_obj = None; self.obj_change = true; self.queue_build = false; + self.selecting_left = None; + self.selecting_right = None; } - pub fn set_selected_obj(&mut self, object: ObjectConfig) { - self.config.selected_obj = Some(object); + pub fn set_selected_obj(&mut self, config: ObjectConfig) { + if self.config.selected_obj.as_ref().is_some_and(|existing| existing == &config) { + // Don't reload the object if there were no changes + return; + } + self.config.selected_obj = Some(config); + self.obj_change = true; + self.queue_build = false; + self.selecting_left = None; + self.selecting_right = None; + } + + pub fn clear_selected_obj(&mut self) { + self.config.selected_obj = None; self.obj_change = true; self.queue_build = false; + self.selecting_left = None; + self.selecting_right = None; + } + + pub fn set_selecting_left(&mut self, right: &str) { + let Some(object) = self.config.selected_obj.as_mut() else { + return; + }; + object.symbol_mappings.remove_by_right(right); + self.selecting_left = Some(right.to_string()); + self.queue_reload = true; + self.save_config(); + } + + pub fn set_selecting_right(&mut self, left: &str) { + let Some(object) = self.config.selected_obj.as_mut() else { + return; + }; + object.symbol_mappings.remove_by_left(left); + self.selecting_right = Some(left.to_string()); + self.queue_reload = true; + self.save_config(); + } + + pub fn set_symbol_mapping(&mut self, left: String, right: String) { + let Some(object) = self.config.selected_obj.as_mut() else { + log::warn!("No selected object"); + return; + }; + self.selecting_left = None; + self.selecting_right = None; + if left == right { + object.symbol_mappings.remove_by_left(&left); + object.symbol_mappings.remove_by_right(&right); + } else { + object.symbol_mappings.insert(left.clone(), right.clone()); + } + self.queue_reload = true; + self.save_config(); + } + + pub fn clear_selection(&mut self) { + self.selecting_left = None; + self.selecting_right = None; + self.queue_reload = true; + } + + pub fn clear_mappings(&mut self) { + self.selecting_left = None; + self.selecting_right = None; + if let Some(object) = self.config.selected_obj.as_mut() { + object.symbol_mappings.clear(); + } + self.queue_reload = true; + self.save_config(); + } + + pub fn is_selecting_symbol(&self) -> bool { + self.selecting_left.is_some() || self.selecting_right.is_some() + } + + pub fn save_config(&mut self) { + let (Some(config), Some(info)) = + (self.current_project_config.as_mut(), self.project_config_info.as_mut()) + else { + return; + }; + // Update the project config with the current state + if let Some(object) = self.config.selected_obj.as_ref() { + if let Some(existing) = config.units.as_mut().and_then(|v| { + v.iter_mut().find(|u| u.name.as_ref().is_some_and(|n| n == &object.name)) + }) { + existing.symbol_mappings = if object.symbol_mappings.is_empty() { + None + } else { + Some(object.symbol_mappings.clone()) + }; + } + if let Some(existing) = + self.objects.iter_mut().find(|u| u.name.as_ref().is_some_and(|n| n == &object.name)) + { + existing.symbol_mappings = if object.symbol_mappings.is_empty() { + None + } else { + Some(object.symbol_mappings.clone()) + }; + } + } + // Save the updated project config + match save_project_config(config, info) { + Ok(new_info) => *info = new_info, + Err(e) => { + log::error!("Failed to save project config: {e}"); + self.config_error = Some(format!("Failed to save project config: {e}")); + } + } } } @@ -373,31 +516,41 @@ impl App { debug_assert!(jobs.results.is_empty()); } - fn post_update(&mut self, ctx: &egui::Context) { + fn post_update(&mut self, ctx: &egui::Context, action: Option) { self.appearance.post_update(ctx); let ViewState { jobs, diff_state, config_state, graphics_state, .. } = &mut self.view_state; config_state.post_update(ctx, jobs, &self.state); - diff_state.post_update(ctx, jobs, &self.state); + diff_state.post_update(action, ctx, jobs, &self.state); let Ok(mut state) = self.state.write() else { return; }; let state = &mut *state; - if let Some(info) = &state.project_config_info { - if file_modified(&info.path, info.timestamp) { - state.config_change = true; + let mut mod_check = false; + if state.last_mod_check.elapsed().as_millis() >= 500 { + state.last_mod_check = Instant::now(); + mod_check = true; + } + + if mod_check { + if let Some(info) = &state.project_config_info { + if let Some(last_ts) = info.timestamp { + if file_modified(&info.path, last_ts) { + state.config_change = true; + } + } } } if state.config_change { state.config_change = false; match load_project_config(state) { - Ok(()) => config_state.load_error = None, + Ok(()) => state.config_error = None, Err(e) => { log::error!("Failed to load project config: {e}"); - config_state.load_error = Some(format!("{e}")); + state.config_error = Some(format!("{e}")); } } } @@ -432,8 +585,7 @@ impl App { } if let Some(result) = &diff_state.build { - if state.last_mod_check.elapsed().as_millis() >= 500 { - state.last_mod_check = Instant::now(); + if mod_check { if let Some((obj, _)) = &result.first_obj { if let (Some(path), Some(timestamp)) = (&obj.path, obj.timestamp) { if file_modified(path, timestamp) { @@ -457,11 +609,11 @@ impl App { && state.config.selected_obj.is_some() && !jobs.is_running(Job::ObjDiff) { - jobs.push(start_build(ctx, ObjDiffConfig::from_config(&state.config))); + jobs.push(start_build(ctx, ObjDiffConfig::from_state(state))); state.queue_build = false; state.queue_reload = false; } else if state.queue_reload && !jobs.is_running(Job::ObjDiff) { - let mut diff_config = ObjDiffConfig::from_config(&state.config); + let mut diff_config = ObjDiffConfig::from_state(state); // Don't build, just reload the current files diff_config.build_base = false; diff_config.build_target = false; @@ -636,6 +788,11 @@ impl eframe::App for App { { state.queue_reload = true; } + if ui.button("Clear custom symbol mappings").clicked() { + state.clear_mappings(); + diff_state.post_build_nav = Some(DiffViewNavigation::symbol_diff()); + state.queue_reload = true; + } }); ui.separator(); if jobs_menu_ui(ui, jobs, appearance) { @@ -652,17 +809,18 @@ impl eframe::App for App { }); } + let mut action = None; egui::CentralPanel::default().show(ctx, |ui| { let build_success = matches!(&diff_state.build, Some(b) if b.first_status.success && b.second_status.success); - if diff_state.current_view == View::FunctionDiff && build_success { - function_diff_ui(ui, diff_state, appearance); + action = if diff_state.current_view == View::FunctionDiff && build_success { + function_diff_ui(ui, diff_state, appearance) } else if diff_state.current_view == View::DataDiff && build_success { - data_diff_ui(ui, diff_state, appearance); + data_diff_ui(ui, diff_state, appearance) } else if diff_state.current_view == View::ExtabDiff && build_success { - extab_diff_ui(ui, diff_state, appearance); + extab_diff_ui(ui, diff_state, appearance) } else { - symbol_diff_ui(ui, diff_state, appearance); - } + symbol_diff_ui(ui, diff_state, appearance) + }; }); project_window(ctx, state, show_project_config, config_state, appearance); @@ -674,10 +832,10 @@ impl eframe::App for App { graphics_window(ctx, show_graphics, frame_history, graphics_state, appearance); jobs_window(ctx, show_jobs, jobs, appearance); - self.post_update(ctx); + self.post_update(ctx, action); } - /// Called by the frame work to save state before shutdown. + /// Called by the framework to save state before shutdown. fn save(&mut self, storage: &mut dyn eframe::Storage) { if let Ok(state) = self.state.read() { eframe::set_value(storage, CONFIG_KEY, &state.config); diff --git a/objdiff-gui/src/app_config.rs b/objdiff-gui/src/app_config.rs index f686ab6..ea456ce 100644 --- a/objdiff-gui/src/app_config.rs +++ b/objdiff-gui/src/app_config.rs @@ -2,6 +2,10 @@ use std::path::PathBuf; use eframe::Storage; use globset::Glob; +use objdiff_core::{ + config::ScratchConfig, + diff::{ArmArchVersion, ArmR9Usage, DiffObjConfig, MipsAbi, MipsInstrCategory, X86Formatter}, +}; use crate::app::{AppConfig, ObjectConfig, CONFIG_KEY}; @@ -11,7 +15,7 @@ pub struct AppConfigVersion { } impl Default for AppConfigVersion { - fn default() -> Self { Self { version: 1 } } + fn default() -> Self { Self { version: 2 } } } /// Deserialize the AppConfig from storage, handling upgrades from older versions. @@ -19,7 +23,8 @@ pub fn deserialize_config(storage: &dyn Storage) -> Option { let str = storage.get_string(CONFIG_KEY)?; match ron::from_str::(&str) { Ok(version) => match version.version { - 1 => from_str::(&str), + 2 => from_str::(&str), + 1 => from_str::(&str).map(|c| c.into_config()), _ => { log::warn!("Unknown config version: {}", version.version); None @@ -44,6 +49,180 @@ where T: serde::de::DeserializeOwned { } } +#[derive(serde::Deserialize, serde::Serialize)] +pub struct ScratchConfigV1 { + #[serde(default)] + pub platform: Option, + #[serde(default)] + pub compiler: Option, + #[serde(default)] + pub c_flags: Option, + #[serde(default)] + pub ctx_path: Option, + #[serde(default)] + pub build_ctx: bool, +} + +impl ScratchConfigV1 { + fn into_config(self) -> ScratchConfig { + ScratchConfig { + platform: self.platform, + compiler: self.compiler, + c_flags: self.c_flags, + ctx_path: self.ctx_path, + build_ctx: self.build_ctx.then_some(true), + } + } +} + +#[derive(serde::Deserialize, serde::Serialize)] +pub struct ObjectConfigV1 { + pub name: String, + pub target_path: Option, + pub base_path: Option, + pub reverse_fn_order: Option, + pub complete: Option, + pub scratch: Option, + pub source_path: Option, +} + +impl ObjectConfigV1 { + fn into_config(self) -> ObjectConfig { + ObjectConfig { + name: self.name, + target_path: self.target_path, + base_path: self.base_path, + reverse_fn_order: self.reverse_fn_order, + complete: self.complete, + scratch: self.scratch.map(|scratch| scratch.into_config()), + source_path: self.source_path, + ..Default::default() + } + } +} + +#[derive(serde::Deserialize, serde::Serialize)] +#[serde(default)] +pub struct DiffObjConfigV1 { + pub relax_reloc_diffs: bool, + #[serde(default = "bool_true")] + pub space_between_args: bool, + pub combine_data_sections: bool, + // x86 + pub x86_formatter: X86Formatter, + // MIPS + pub mips_abi: MipsAbi, + pub mips_instr_category: MipsInstrCategory, + // ARM + pub arm_arch_version: ArmArchVersion, + pub arm_unified_syntax: bool, + pub arm_av_registers: bool, + pub arm_r9_usage: ArmR9Usage, + pub arm_sl_usage: bool, + pub arm_fp_usage: bool, + pub arm_ip_usage: bool, +} + +impl Default for DiffObjConfigV1 { + fn default() -> Self { + Self { + relax_reloc_diffs: false, + space_between_args: true, + combine_data_sections: false, + x86_formatter: Default::default(), + mips_abi: Default::default(), + mips_instr_category: Default::default(), + arm_arch_version: Default::default(), + arm_unified_syntax: true, + arm_av_registers: false, + arm_r9_usage: Default::default(), + arm_sl_usage: false, + arm_fp_usage: false, + arm_ip_usage: false, + } + } +} + +impl DiffObjConfigV1 { + fn into_config(self) -> DiffObjConfig { + DiffObjConfig { + relax_reloc_diffs: self.relax_reloc_diffs, + space_between_args: self.space_between_args, + combine_data_sections: self.combine_data_sections, + x86_formatter: self.x86_formatter, + mips_abi: self.mips_abi, + mips_instr_category: self.mips_instr_category, + arm_arch_version: self.arm_arch_version, + arm_unified_syntax: self.arm_unified_syntax, + arm_av_registers: self.arm_av_registers, + arm_r9_usage: self.arm_r9_usage, + arm_sl_usage: self.arm_sl_usage, + arm_fp_usage: self.arm_fp_usage, + arm_ip_usage: self.arm_ip_usage, + ..Default::default() + } + } +} + +#[inline] +fn bool_true() -> bool { true } + +#[derive(serde::Deserialize, serde::Serialize)] +pub struct AppConfigV1 { + pub version: u32, + #[serde(default)] + pub custom_make: Option, + #[serde(default)] + pub custom_args: Option>, + #[serde(default)] + pub selected_wsl_distro: Option, + #[serde(default)] + pub project_dir: Option, + #[serde(default)] + pub target_obj_dir: Option, + #[serde(default)] + pub base_obj_dir: Option, + #[serde(default)] + pub selected_obj: Option, + #[serde(default = "bool_true")] + pub build_base: bool, + #[serde(default)] + pub build_target: bool, + #[serde(default = "bool_true")] + pub rebuild_on_changes: bool, + #[serde(default)] + pub auto_update_check: bool, + #[serde(default)] + pub watch_patterns: Vec, + #[serde(default)] + pub recent_projects: Vec, + #[serde(default)] + pub diff_obj_config: DiffObjConfigV1, +} + +impl AppConfigV1 { + fn into_config(self) -> AppConfig { + log::info!("Upgrading configuration from v1"); + AppConfig { + custom_make: self.custom_make, + custom_args: self.custom_args, + selected_wsl_distro: self.selected_wsl_distro, + project_dir: self.project_dir, + target_obj_dir: self.target_obj_dir, + base_obj_dir: self.base_obj_dir, + selected_obj: self.selected_obj.map(|obj| obj.into_config()), + build_base: self.build_base, + build_target: self.build_target, + rebuild_on_changes: self.rebuild_on_changes, + auto_update_check: self.auto_update_check, + watch_patterns: self.watch_patterns, + recent_projects: self.recent_projects, + diff_obj_config: self.diff_obj_config.into_config(), + ..Default::default() + } + } +} + #[derive(serde::Deserialize, serde::Serialize)] pub struct ObjectConfigV0 { pub name: String, @@ -59,9 +238,7 @@ impl ObjectConfigV0 { target_path: Some(self.target_path), base_path: Some(self.base_path), reverse_fn_order: self.reverse_fn_order, - complete: None, - scratch: None, - source_path: None, + ..Default::default() } } } diff --git a/objdiff-gui/src/config.rs b/objdiff-gui/src/config.rs index 6721cf7..a99913e 100644 --- a/objdiff-gui/src/config.rs +++ b/objdiff-gui/src/config.rs @@ -4,11 +4,11 @@ use anyhow::Result; use globset::Glob; use objdiff_core::config::{try_project_config, ProjectObject, DEFAULT_WATCH_PATTERNS}; -use crate::app::AppState; +use crate::app::{AppState, ObjectConfig}; #[derive(Clone)] pub enum ProjectObjectNode { - File(String, Box), + Unit(String, usize), Dir(String, Vec), } @@ -33,17 +33,18 @@ fn find_dir<'a>( } fn build_nodes( - objects: &[ProjectObject], + units: &mut [ProjectObject], project_dir: &Path, target_obj_dir: Option<&Path>, base_obj_dir: Option<&Path>, ) -> Vec { let mut nodes = vec![]; - for object in objects { + for (idx, unit) in units.iter_mut().enumerate() { + unit.resolve_paths(project_dir, target_obj_dir, base_obj_dir); let mut out_nodes = &mut nodes; - let path = if let Some(name) = &object.name { + let path = if let Some(name) = &unit.name { Path::new(name) - } else if let Some(path) = &object.path { + } else if let Some(path) = &unit.path { path } else { continue; @@ -56,10 +57,8 @@ fn build_nodes( } } } - let mut object = Box::new(object.clone()); - object.resolve_paths(project_dir, target_obj_dir, base_obj_dir); let filename = path.file_name().unwrap().to_str().unwrap().to_string(); - out_nodes.push(ProjectObjectNode::File(filename, object)); + out_nodes.push(ProjectObjectNode::Unit(filename, idx)); } nodes } @@ -70,24 +69,36 @@ pub fn load_project_config(state: &mut AppState) -> Result<()> { }; if let Some((result, info)) = try_project_config(project_dir) { let project_config = result?; - state.config.custom_make = project_config.custom_make; - state.config.custom_args = project_config.custom_args; - state.config.target_obj_dir = project_config.target_dir.map(|p| project_dir.join(p)); - state.config.base_obj_dir = project_config.base_dir.map(|p| project_dir.join(p)); - state.config.build_base = project_config.build_base; - state.config.build_target = project_config.build_target; - state.config.watch_patterns = project_config.watch_patterns.unwrap_or_else(|| { + state.config.custom_make = project_config.custom_make.clone(); + state.config.custom_args = project_config.custom_args.clone(); + state.config.target_obj_dir = + project_config.target_dir.as_deref().map(|p| project_dir.join(p)); + state.config.base_obj_dir = project_config.base_dir.as_deref().map(|p| project_dir.join(p)); + state.config.build_base = project_config.build_base.unwrap_or(true); + state.config.build_target = project_config.build_target.unwrap_or(false); + state.config.watch_patterns = project_config.watch_patterns.clone().unwrap_or_else(|| { DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect() }); state.watcher_change = true; - state.objects = project_config.objects; + state.objects = project_config.units.clone().unwrap_or_default(); state.object_nodes = build_nodes( - &state.objects, + &mut state.objects, project_dir, state.config.target_obj_dir.as_deref(), state.config.base_obj_dir.as_deref(), ); + state.current_project_config = Some(project_config); state.project_config_info = Some(info); + + // Reload selected object + if let Some(selected_obj) = &state.config.selected_obj { + if let Some(obj) = state.objects.iter().find(|o| o.name() == selected_obj.name) { + let config = ObjectConfig::from(obj); + state.set_selected_obj(config); + } else { + state.clear_selected_obj(); + } + } } Ok(()) } diff --git a/objdiff-gui/src/jobs/create_scratch.rs b/objdiff-gui/src/jobs/create_scratch.rs index bd205c5..3d8ecd4 100644 --- a/objdiff-gui/src/jobs/create_scratch.rs +++ b/objdiff-gui/src/jobs/create_scratch.rs @@ -39,7 +39,7 @@ impl CreateScratchConfig { Ok(Self { build_config: BuildConfig::from_config(config), context_path: scratch_config.ctx_path.clone(), - build_context: scratch_config.build_ctx, + build_context: scratch_config.build_ctx.unwrap_or(false), compiler: scratch_config.compiler.clone().unwrap_or_default(), platform: scratch_config.platform.clone().unwrap_or_default(), compiler_flags: scratch_config.c_flags.clone().unwrap_or_default(), diff --git a/objdiff-gui/src/jobs/mod.rs b/objdiff-gui/src/jobs/mod.rs index 85ff3f5..1cf728f 100644 --- a/objdiff-gui/src/jobs/mod.rs +++ b/objdiff-gui/src/jobs/mod.rs @@ -53,7 +53,7 @@ impl JobQueue { } /// Returns whether any job is running. - #[allow(dead_code)] + #[expect(dead_code)] pub fn any_running(&self) -> bool { self.jobs.iter().any(|job| { if let Some(handle) = &job.handle { diff --git a/objdiff-gui/src/jobs/objdiff.rs b/objdiff-gui/src/jobs/objdiff.rs index 60bcd6d..0c69597 100644 --- a/objdiff-gui/src/jobs/objdiff.rs +++ b/objdiff-gui/src/jobs/objdiff.rs @@ -6,13 +6,13 @@ use std::{ use anyhow::{anyhow, Error, Result}; use objdiff_core::{ - diff::{diff_objs, DiffObjConfig, ObjDiff}, + diff::{diff_objs, DiffObjConfig, MappingConfig, ObjDiff}, obj::{read, ObjInfo}, }; use time::OffsetDateTime; use crate::{ - app::{AppConfig, ObjectConfig}, + app::{AppConfig, AppState, ObjectConfig}, jobs::{start_job, update_status, Job, JobContext, JobResult, JobState}, }; @@ -60,16 +60,20 @@ pub struct ObjDiffConfig { pub build_target: bool, pub selected_obj: Option, pub diff_obj_config: DiffObjConfig, + pub selecting_left: Option, + pub selecting_right: Option, } impl ObjDiffConfig { - pub(crate) fn from_config(config: &AppConfig) -> Self { + pub(crate) fn from_state(state: &AppState) -> Self { Self { - build_config: BuildConfig::from_config(config), - build_base: config.build_base, - build_target: config.build_target, - selected_obj: config.selected_obj.clone(), - diff_obj_config: config.diff_obj_config.clone(), + build_config: BuildConfig::from_config(&state.config), + build_base: state.config.build_base, + build_target: state.config.build_target, + selected_obj: state.config.selected_obj.clone(), + diff_obj_config: state.config.diff_obj_config.clone(), + selecting_left: state.selecting_left.clone(), + selecting_right: state.selecting_right.clone(), } } } @@ -158,9 +162,16 @@ pub(crate) fn run_make(config: &BuildConfig, arg: &Path) -> BuildStatus { fn run_build( context: &JobContext, cancel: Receiver<()>, - config: ObjDiffConfig, + mut config: ObjDiffConfig, ) -> Result> { - let obj_config = config.selected_obj.as_ref().ok_or_else(|| Error::msg("Missing obj path"))?; + let obj_config = config.selected_obj.ok_or_else(|| Error::msg("Missing obj path"))?; + // Use the per-object symbol mappings, we don't set mappings globally + config.diff_obj_config.symbol_mappings = MappingConfig { + mappings: obj_config.symbol_mappings, + selecting_left: config.selecting_left, + selecting_right: config.selecting_right, + }; + let project_dir = config .build_config .project_dir diff --git a/objdiff-gui/src/views/column_layout.rs b/objdiff-gui/src/views/column_layout.rs new file mode 100644 index 0000000..9148311 --- /dev/null +++ b/objdiff-gui/src/views/column_layout.rs @@ -0,0 +1,82 @@ +use egui::{Align, Layout, Sense, Vec2}; +use egui_extras::{Column, Size, StripBuilder, TableBuilder, TableRow}; + +pub fn render_header( + ui: &mut egui::Ui, + available_width: f32, + num_columns: usize, + mut add_contents: impl FnMut(&mut egui::Ui, usize), +) { + let column_width = available_width / num_columns as f32; + ui.allocate_ui_with_layout( + Vec2 { x: available_width, y: 100.0 }, + Layout::left_to_right(Align::Min), + |ui| { + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate); + for i in 0..num_columns { + ui.allocate_ui_with_layout( + Vec2 { x: column_width, y: 100.0 }, + Layout::top_down(Align::Min), + |ui| { + ui.set_width(column_width); + add_contents(ui, i); + }, + ); + } + }, + ); + ui.separator(); +} + +pub fn render_table( + ui: &mut egui::Ui, + available_width: f32, + num_columns: usize, + row_height: f32, + total_rows: usize, + mut add_contents: impl FnMut(&mut TableRow, usize), +) { + ui.style_mut().interaction.selectable_labels = false; + let column_width = available_width / num_columns as f32; + let available_height = ui.available_height(); + let table = TableBuilder::new(ui) + .striped(false) + .cell_layout(Layout::left_to_right(Align::Min)) + .columns(Column::exact(column_width).clip(true), num_columns) + .resizable(false) + .auto_shrink([false, false]) + .min_scrolled_height(available_height) + .sense(Sense::click()); + table.body(|body| { + body.rows(row_height, total_rows, |mut row| { + row.set_hovered(false); // Disable hover effect + for i in 0..num_columns { + add_contents(&mut row, i); + } + }); + }); +} + +pub fn render_strips( + ui: &mut egui::Ui, + available_width: f32, + num_columns: usize, + mut add_contents: impl FnMut(&mut egui::Ui, usize), +) { + let column_width = available_width / num_columns as f32; + StripBuilder::new(ui).size(Size::remainder()).clip(true).vertical(|mut strip| { + strip.strip(|builder| { + builder.sizes(Size::exact(column_width), num_columns).clip(true).horizontal( + |mut strip| { + for i in 0..num_columns { + strip.cell(|ui| { + ui.push_id(i, |ui| { + add_contents(ui, i); + }); + }); + } + }, + ); + }); + }); +} diff --git a/objdiff-gui/src/views/config.rs b/objdiff-gui/src/views/config.rs index a54b951..6d7fd9e 100644 --- a/objdiff-gui/src/views/config.rs +++ b/objdiff-gui/src/views/config.rs @@ -43,7 +43,6 @@ pub struct ConfigViewState { pub build_running: bool, pub queue_build: bool, pub watch_pattern_text: String, - pub load_error: Option, pub object_search: String, pub filter_diffable: bool, pub filter_incomplete: bool, @@ -93,10 +92,7 @@ impl ConfigViewState { name: obj_path.display().to_string(), target_path: Some(target_path), base_path: Some(path), - reverse_fn_order: None, - complete: None, - scratch: None, - source_path: None, + ..Default::default() }); } else if let Ok(obj_path) = path.strip_prefix(target_dir) { let base_path = base_dir.join(obj_path); @@ -104,10 +100,7 @@ impl ConfigViewState { name: obj_path.display().to_string(), target_path: Some(path), base_path: Some(base_path), - reverse_fn_order: None, - complete: None, - scratch: None, - source_path: None, + ..Default::default() }); } } @@ -230,7 +223,10 @@ pub fn config_ui( } }); - let mut new_selected_obj = selected_obj.clone(); + let selected_index = selected_obj.as_ref().and_then(|selected_obj| { + objects.iter().position(|obj| obj.name.as_ref() == Some(&selected_obj.name)) + }); + let mut new_selected_index = selected_index; if objects.is_empty() { if let (Some(_base_dir), Some(target_dir)) = (base_obj_dir, target_obj_dir) { if ui.button("Select object").clicked() { @@ -316,6 +312,7 @@ pub fn config_ui( ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); for node in object_nodes.iter().filter_map(|node| { filter_node( + objects, node, &search, config_state.filter_diffable, @@ -325,8 +322,9 @@ pub fn config_ui( }) { display_node( ui, - &mut new_selected_obj, + &mut new_selected_index, project_dir.as_deref(), + objects, &node, appearance, node_open, @@ -334,10 +332,11 @@ pub fn config_ui( } }); } - if new_selected_obj != *selected_obj { - if let Some(obj) = new_selected_obj { + if new_selected_index != selected_index { + if let Some(idx) = new_selected_index { // Will set obj_changed, which will trigger a rebuild - state_guard.set_selected_obj(obj); + let config = ObjectConfig::from(&objects[idx]); + state_guard.set_selected_obj(config); } } if state_guard.config.selected_obj.is_some() @@ -347,16 +346,17 @@ pub fn config_ui( } } -fn display_object( +fn display_unit( ui: &mut egui::Ui, - selected_obj: &mut Option, + selected_obj: &mut Option, project_dir: Option<&Path>, name: &str, - object: &ProjectObject, + units: &[ProjectObject], + index: usize, appearance: &Appearance, ) { - let object_name = object.name(); - let selected = matches!(selected_obj, Some(obj) if obj.name == object_name); + let object = &units[index]; + let selected = *selected_obj == Some(index); let color = if selected { appearance.emphasized_text_color } else if let Some(complete) = object.complete() { @@ -381,18 +381,8 @@ fn display_object( if get_source_path(project_dir, object).is_some() { response.context_menu(|ui| object_context_ui(ui, object, project_dir)); } - // Always recreate ObjectConfig if selected, in case the project config changed. - // ObjectConfig is compared using equality, so this won't unnecessarily trigger a rebuild. - if selected || response.clicked() { - *selected_obj = Some(ObjectConfig { - name: object_name.to_string(), - target_path: object.target_path.clone(), - base_path: object.base_path.clone(), - reverse_fn_order: object.reverse_fn_order(), - complete: object.complete(), - scratch: object.scratch.clone(), - source_path: object.source_path().cloned(), - }); + if response.clicked() { + *selected_obj = Some(index); } } @@ -427,18 +417,19 @@ enum NodeOpen { fn display_node( ui: &mut egui::Ui, - selected_obj: &mut Option, + selected_obj: &mut Option, project_dir: Option<&Path>, + units: &[ProjectObject], node: &ProjectObjectNode, appearance: &Appearance, node_open: NodeOpen, ) { match node { - ProjectObjectNode::File(name, object) => { - display_object(ui, selected_obj, project_dir, name, object, appearance); + ProjectObjectNode::Unit(name, idx) => { + display_unit(ui, selected_obj, project_dir, name, units, *idx, appearance); } ProjectObjectNode::Dir(name, children) => { - let contains_obj = selected_obj.as_ref().map(|path| contains_node(node, path)); + let contains_obj = selected_obj.map(|idx| contains_node(node, idx)); let open = match node_open { NodeOpen::Default => None, NodeOpen::Open => Some(true), @@ -461,16 +452,16 @@ fn display_node( .open(open) .show(ui, |ui| { for node in children { - display_node(ui, selected_obj, project_dir, node, appearance, node_open); + display_node(ui, selected_obj, project_dir, units, node, appearance, node_open); } }); } } } -fn contains_node(node: &ProjectObjectNode, selected_obj: &ObjectConfig) -> bool { +fn contains_node(node: &ProjectObjectNode, selected_obj: usize) -> bool { match node { - ProjectObjectNode::File(_, object) => object.name() == selected_obj.name, + ProjectObjectNode::Unit(_, idx) => *idx == selected_obj, ProjectObjectNode::Dir(_, children) => { children.iter().any(|node| contains_node(node, selected_obj)) } @@ -478,6 +469,7 @@ fn contains_node(node: &ProjectObjectNode, selected_obj: &ObjectConfig) -> bool } fn filter_node( + units: &[ProjectObject], node: &ProjectObjectNode, search: &str, filter_diffable: bool, @@ -485,12 +477,12 @@ fn filter_node( show_hidden: bool, ) -> Option { match node { - ProjectObjectNode::File(name, object) => { + ProjectObjectNode::Unit(name, idx) => { + let unit = &units[*idx]; if (search.is_empty() || name.to_ascii_lowercase().contains(search)) - && (!filter_diffable - || (object.base_path.is_some() && object.target_path.is_some())) - && (!filter_incomplete || matches!(object.complete(), None | Some(false))) - && (show_hidden || !object.hidden()) + && (!filter_diffable || (unit.base_path.is_some() && unit.target_path.is_some())) + && (!filter_incomplete || matches!(unit.complete(), None | Some(false))) + && (show_hidden || !unit.hidden()) { Some(node.clone()) } else { @@ -501,7 +493,14 @@ fn filter_node( let new_children = children .iter() .filter_map(|child| { - filter_node(child, search, filter_diffable, filter_incomplete, show_hidden) + filter_node( + units, + child, + search, + filter_diffable, + filter_incomplete, + show_hidden, + ) }) .collect::>(); if !new_children.is_empty() { @@ -570,14 +569,14 @@ pub fn project_window( split_obj_config_ui(ui, &mut state_guard, config_state, appearance); }); - if let Some(error) = &config_state.load_error { + if let Some(error) = &state_guard.config_error { let mut open = true; egui::Window::new("Error").open(&mut open).show(ctx, |ui| { ui.label("Failed to load project config:"); ui.colored_label(appearance.delete_color, error); }); if !open { - config_state.load_error = None; + state_guard.config_error = None; } } } diff --git a/objdiff-gui/src/views/data_diff.rs b/objdiff-gui/src/views/data_diff.rs index 1798596..224a169 100644 --- a/objdiff-gui/src/views/data_diff.rs +++ b/objdiff-gui/src/views/data_diff.rs @@ -1,7 +1,6 @@ use std::{cmp::min, default::Default, mem::take}; -use egui::{text::LayoutJob, Align, Label, Layout, Sense, Vec2, Widget}; -use egui_extras::{Column, TableBuilder}; +use egui::{text::LayoutJob, Id, Label, RichText, Sense, Widget}; use objdiff_core::{ diff::{ObjDataDiff, ObjDataDiffKind, ObjDiff}, obj::ObjInfo, @@ -10,14 +9,15 @@ use time::format_description; use crate::views::{ appearance::Appearance, - symbol_diff::{DiffViewState, SymbolRefByName, View}, + column_layout::{render_header, render_table}, + symbol_diff::{DiffViewAction, DiffViewNavigation, DiffViewState}, write_text, }; const BYTES_PER_ROW: usize = 16; -fn find_section(obj: &ObjInfo, selected_symbol: &SymbolRefByName) -> Option { - obj.sections.iter().position(|section| section.name == selected_symbol.section_name) +fn find_section(obj: &ObjInfo, section_name: &str) -> Option { + obj.sections.iter().position(|section| section.name == section_name) } fn data_row_ui(ui: &mut egui::Ui, address: usize, diffs: &[ObjDataDiff], appearance: &Appearance) { @@ -131,20 +131,37 @@ fn split_diffs(diffs: &[ObjDataDiff]) -> Vec> { split_diffs } +#[derive(Clone, Copy)] +struct SectionDiffContext<'a> { + obj: &'a ObjInfo, + diff: &'a ObjDiff, + section_index: Option, +} + +impl<'a> SectionDiffContext<'a> { + pub fn new(obj: Option<&'a (ObjInfo, ObjDiff)>, section_name: Option<&str>) -> Option { + obj.map(|(obj, diff)| Self { + obj, + diff, + section_index: section_name.and_then(|section_name| find_section(obj, section_name)), + }) + } + + #[inline] + pub fn has_section(&self) -> bool { self.section_index.is_some() } +} + fn data_table_ui( - table: TableBuilder<'_>, - left_obj: Option<&(ObjInfo, ObjDiff)>, - right_obj: Option<&(ObjInfo, ObjDiff)>, - selected_symbol: &SymbolRefByName, + ui: &mut egui::Ui, + available_width: f32, + left_ctx: Option>, + right_ctx: Option>, config: &Appearance, ) -> Option<()> { - let left_section = left_obj.and_then(|(obj, diff)| { - find_section(obj, selected_symbol).map(|i| (&obj.sections[i], &diff.sections[i])) - }); - let right_section = right_obj.and_then(|(obj, diff)| { - find_section(obj, selected_symbol).map(|i| (&obj.sections[i], &diff.sections[i])) - }); - + let left_section = left_ctx + .and_then(|ctx| ctx.section_index.map(|i| (&ctx.obj.sections[i], &ctx.diff.sections[i]))); + let right_section = right_ctx + .and_then(|ctx| ctx.section_index.map(|i| (&ctx.obj.sections[i], &ctx.diff.sections[i]))); let total_bytes = left_section .or(right_section)? .1 @@ -159,118 +176,117 @@ fn data_table_ui( let left_diffs = left_section.map(|(_, section)| split_diffs(§ion.data_diff)); let right_diffs = right_section.map(|(_, section)| split_diffs(§ion.data_diff)); - table.body(|body| { - body.rows(config.code_font.size, total_rows, |mut row| { - let row_index = row.index(); - let address = row_index * BYTES_PER_ROW; - row.col(|ui| { + render_table(ui, available_width, 2, config.code_font.size, total_rows, |row, column| { + let i = row.index(); + let address = i * BYTES_PER_ROW; + row.col(|ui| { + if column == 0 { if let Some(left_diffs) = &left_diffs { - data_row_ui(ui, address, &left_diffs[row_index], config); + data_row_ui(ui, address, &left_diffs[i], config); } - }); - row.col(|ui| { + } else if column == 1 { if let Some(right_diffs) = &right_diffs { - data_row_ui(ui, address, &right_diffs[row_index], config); + data_row_ui(ui, address, &right_diffs[i], config); } - }); + } }); }); Some(()) } -pub fn data_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &Appearance) { - let (Some(result), Some(selected_symbol)) = (&state.build, &state.symbol_state.selected_symbol) - else { - return; +#[must_use] +pub fn data_diff_ui( + ui: &mut egui::Ui, + state: &DiffViewState, + appearance: &Appearance, +) -> Option { + let mut ret = None; + let Some(result) = &state.build else { + return ret; }; + let section_name = + state.symbol_state.left_symbol.as_ref().and_then(|s| s.section_name.as_deref()).or_else( + || state.symbol_state.right_symbol.as_ref().and_then(|s| s.section_name.as_deref()), + ); + let left_ctx = SectionDiffContext::new(result.first_obj.as_ref(), section_name); + let right_ctx = SectionDiffContext::new(result.second_obj.as_ref(), section_name); + + // If both sides are missing a symbol, switch to symbol diff view + if !right_ctx.map_or(false, |ctx| ctx.has_section()) + && !left_ctx.map_or(false, |ctx| ctx.has_section()) + { + return Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff())); + } + // Header let available_width = ui.available_width(); - let column_width = available_width / 2.0; - ui.allocate_ui_with_layout( - Vec2 { x: available_width, y: 100.0 }, - Layout::left_to_right(Align::Min), - |ui| { - ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate); - + render_header(ui, available_width, 2, |ui, column| { + if column == 0 { // Left column - ui.allocate_ui_with_layout( - Vec2 { x: column_width, y: 100.0 }, - Layout::top_down(Align::Min), - |ui| { - ui.set_width(column_width); - - if ui.button("⏴ Back").clicked() { - state.current_view = View::SymbolDiff; - } - - ui.scope(|ui| { - ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); - ui.colored_label(appearance.highlight_color, &selected_symbol.symbol_name); - ui.label("Diff target:"); - }); - }, - ); + if ui.button("⏴ Back").clicked() { + ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff())); + } + if let Some(section) = + left_ctx.and_then(|ctx| ctx.section_index.map(|i| &ctx.obj.sections[i])) + { + ui.label( + RichText::new(section.name.clone()) + .font(appearance.code_font.clone()) + .color(appearance.highlight_color), + ); + } else { + ui.label( + RichText::new("Missing") + .font(appearance.code_font.clone()) + .color(appearance.replace_color), + ); + } + } else if column == 1 { // Right column - ui.allocate_ui_with_layout( - Vec2 { x: column_width, y: 100.0 }, - Layout::top_down(Align::Min), - |ui| { - ui.set_width(column_width); - - ui.horizontal(|ui| { - if ui - .add_enabled(!state.build_running, egui::Button::new("Build")) - .clicked() - { - state.queue_build = true; - } - ui.scope(|ui| { - ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); - if state.build_running { - ui.colored_label(appearance.replace_color, "Building…"); - } else { - ui.label("Last built:"); - let format = - format_description::parse("[hour]:[minute]:[second]").unwrap(); - ui.label( - result - .time - .to_offset(appearance.utc_offset) - .format(&format) - .unwrap(), - ); - } - }); - }); + ui.horizontal(|ui| { + if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() { + ret = Some(DiffViewAction::Build); + } + ui.scope(|ui| { + ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); + if state.build_running { + ui.colored_label(appearance.replace_color, "Building…"); + } else { + ui.label("Last built:"); + let format = format_description::parse("[hour]:[minute]:[second]").unwrap(); + ui.label( + result.time.to_offset(appearance.utc_offset).format(&format).unwrap(), + ); + } + }); + }); - ui.scope(|ui| { - ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); - ui.label(""); - ui.label("Diff base:"); - }); - }, - ); - }, - ); - ui.separator(); + if let Some(section) = + right_ctx.and_then(|ctx| ctx.section_index.map(|i| &ctx.obj.sections[i])) + { + ui.label( + RichText::new(section.name.clone()) + .font(appearance.code_font.clone()) + .color(appearance.highlight_color), + ); + } else { + ui.label( + RichText::new("Missing") + .font(appearance.code_font.clone()) + .color(appearance.replace_color), + ); + } + } + }); // Table - ui.style_mut().interaction.selectable_labels = false; - let available_height = ui.available_height(); - let table = TableBuilder::new(ui) - .striped(false) - .cell_layout(Layout::left_to_right(Align::Min)) - .columns(Column::exact(column_width).clip(true), 2) - .resizable(false) - .auto_shrink([false, false]) - .min_scrolled_height(available_height); - data_table_ui( - table, - result.first_obj.as_ref(), - result.second_obj.as_ref(), - selected_symbol, - appearance, - ); + let id = + Id::new(state.symbol_state.left_symbol.as_ref().and_then(|s| s.section_name.as_deref())) + .with(state.symbol_state.right_symbol.as_ref().and_then(|s| s.section_name.as_deref())); + ui.push_id(id, |ui| { + data_table_ui(ui, available_width, left_ctx, right_ctx, appearance); + }); + ret } diff --git a/objdiff-gui/src/views/extab_diff.rs b/objdiff-gui/src/views/extab_diff.rs index 8372b3e..b91cd62 100644 --- a/objdiff-gui/src/views/extab_diff.rs +++ b/objdiff-gui/src/views/extab_diff.rs @@ -1,28 +1,20 @@ -use egui::{Align, Layout, ScrollArea, Ui, Vec2}; -use egui_extras::{Size, StripBuilder}; +use egui::{RichText, ScrollArea}; use objdiff_core::{ arch::ppc::ExceptionInfo, - diff::ObjDiff, - obj::{ObjInfo, ObjSymbol, SymbolRef}, + obj::{ObjInfo, ObjSymbol}, }; use time::format_description; use crate::views::{ appearance::Appearance, - symbol_diff::{match_color_for_symbol, DiffViewState, SymbolRefByName, View}, + column_layout::{render_header, render_strips}, + function_diff::FunctionDiffContext, + symbol_diff::{ + match_color_for_symbol, DiffViewAction, DiffViewNavigation, DiffViewState, SymbolRefByName, + View, + }, }; -fn find_symbol(obj: &ObjInfo, selected_symbol: &SymbolRefByName) -> Option { - for (section_idx, section) in obj.sections.iter().enumerate() { - for (symbol_idx, symbol) in section.symbols.iter().enumerate() { - if symbol.name == selected_symbol.symbol_name { - return Some(SymbolRef { section_idx, symbol_idx }); - } - } - } - None -} - fn decode_extab(extab: &ExceptionInfo) -> String { let mut text = String::from(""); @@ -48,14 +40,12 @@ fn find_extab_entry<'a>(obj: &'a ObjInfo, symbol: &ObjSymbol) -> Option<&'a Exce } fn extab_text_ui( - ui: &mut Ui, - obj: &(ObjInfo, ObjDiff), - symbol_ref: SymbolRef, + ui: &mut egui::Ui, + ctx: FunctionDiffContext<'_>, + symbol: &ObjSymbol, appearance: &Appearance, ) -> Option<()> { - let (_section, symbol) = obj.0.section_symbol(symbol_ref); - - if let Some(extab_entry) = find_extab_entry(&obj.0, symbol) { + if let Some(extab_entry) = find_extab_entry(ctx.obj, symbol) { let text = decode_extab(extab_entry); ui.colored_label(appearance.replace_color, &text); return Some(()); @@ -65,137 +55,194 @@ fn extab_text_ui( } fn extab_ui( - ui: &mut Ui, - obj: Option<&(ObjInfo, ObjDiff)>, - selected_symbol: &SymbolRefByName, + ui: &mut egui::Ui, + ctx: FunctionDiffContext<'_>, appearance: &Appearance, - _left: bool, + _column: usize, ) { ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| { ui.scope(|ui| { ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); - let symbol = obj.and_then(|(obj, _)| find_symbol(obj, selected_symbol)); - - if let (Some(object), Some(symbol_ref)) = (obj, symbol) { - extab_text_ui(ui, object, symbol_ref, appearance); + if let Some((_section, symbol)) = + ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref)) + { + extab_text_ui(ui, ctx, symbol, appearance); } }); }); } -pub fn extab_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &Appearance) { - let (Some(result), Some(selected_symbol)) = (&state.build, &state.symbol_state.selected_symbol) - else { - return; +#[must_use] +pub fn extab_diff_ui( + ui: &mut egui::Ui, + state: &DiffViewState, + appearance: &Appearance, +) -> Option { + let mut ret = None; + let Some(result) = &state.build else { + return ret; }; + let mut left_ctx = FunctionDiffContext::new( + result.first_obj.as_ref(), + state.symbol_state.left_symbol.as_ref(), + ); + let mut right_ctx = FunctionDiffContext::new( + result.second_obj.as_ref(), + state.symbol_state.right_symbol.as_ref(), + ); + + // If one side is missing a symbol, but the diff process found a match, use that symbol + let left_diff_symbol = left_ctx.and_then(|ctx| { + ctx.symbol_ref.and_then(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).target_symbol) + }); + let right_diff_symbol = right_ctx.and_then(|ctx| { + ctx.symbol_ref.and_then(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).target_symbol) + }); + if left_diff_symbol.is_some() && right_ctx.map_or(false, |ctx| !ctx.has_symbol()) { + let (right_section, right_symbol) = + right_ctx.unwrap().obj.section_symbol(left_diff_symbol.unwrap()); + let symbol_ref = SymbolRefByName::new(right_symbol, right_section); + right_ctx = FunctionDiffContext::new(result.second_obj.as_ref(), Some(&symbol_ref)); + ret = Some(DiffViewAction::Navigate(DiffViewNavigation { + view: Some(View::FunctionDiff), + left_symbol: state.symbol_state.left_symbol.clone(), + right_symbol: Some(symbol_ref), + })); + } else if right_diff_symbol.is_some() && left_ctx.map_or(false, |ctx| !ctx.has_symbol()) { + let (left_section, left_symbol) = + left_ctx.unwrap().obj.section_symbol(right_diff_symbol.unwrap()); + let symbol_ref = SymbolRefByName::new(left_symbol, left_section); + left_ctx = FunctionDiffContext::new(result.first_obj.as_ref(), Some(&symbol_ref)); + ret = Some(DiffViewAction::Navigate(DiffViewNavigation { + view: Some(View::FunctionDiff), + left_symbol: Some(symbol_ref), + right_symbol: state.symbol_state.right_symbol.clone(), + })); + } + + // If both sides are missing a symbol, switch to symbol diff view + if right_ctx.map_or(false, |ctx| !ctx.has_symbol()) + && left_ctx.map_or(false, |ctx| !ctx.has_symbol()) + { + return Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff())); + } + // Header let available_width = ui.available_width(); - let column_width = available_width / 2.0; - ui.allocate_ui_with_layout( - Vec2 { x: available_width, y: 100.0 }, - Layout::left_to_right(Align::Min), - |ui| { - ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate); - + render_header(ui, available_width, 2, |ui, column| { + if column == 0 { // Left column - ui.allocate_ui_with_layout( - Vec2 { x: column_width, y: 100.0 }, - Layout::top_down(Align::Min), - |ui| { - ui.set_width(column_width); - - ui.horizontal(|ui| { - if ui.button("⏴ Back").clicked() { - state.current_view = View::SymbolDiff; - } - }); - - let name = selected_symbol - .demangled_symbol_name - .as_deref() - .unwrap_or(&selected_symbol.symbol_name); - ui.scope(|ui| { - ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); - ui.colored_label(appearance.highlight_color, name); - ui.label("Diff target:"); - }); - }, - ); + ui.horizontal(|ui| { + if ui.button("⏴ Back").clicked() { + ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff())); + } + ui.separator(); + if ui + .add_enabled( + !state.scratch_running + && state.scratch_available + && left_ctx.map_or(false, |ctx| ctx.has_symbol()), + egui::Button::new("📲 decomp.me"), + ) + .on_hover_text_at_pointer("Create a new scratch on decomp.me (beta)") + .on_disabled_hover_text("Scratch configuration missing") + .clicked() + { + if let Some((_section, symbol)) = left_ctx.and_then(|ctx| { + ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref)) + }) { + ret = Some(DiffViewAction::CreateScratch(symbol.name.clone())); + } + } + }); + if let Some((_section, symbol)) = left_ctx + .and_then(|ctx| ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref))) + { + let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name); + ui.label( + RichText::new(name) + .font(appearance.code_font.clone()) + .color(appearance.highlight_color), + ); + } else { + ui.label( + RichText::new("Missing") + .font(appearance.code_font.clone()) + .color(appearance.replace_color), + ); + } + } else if column == 1 { // Right column - ui.allocate_ui_with_layout( - Vec2 { x: column_width, y: 100.0 }, - Layout::top_down(Align::Min), - |ui| { - ui.set_width(column_width); - - ui.horizontal(|ui| { - if ui - .add_enabled(!state.build_running, egui::Button::new("Build")) - .clicked() - { - state.queue_build = true; - } - ui.scope(|ui| { - ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); - if state.build_running { - ui.colored_label(appearance.replace_color, "Building…"); - } else { - ui.label("Last built:"); - let format = - format_description::parse("[hour]:[minute]:[second]").unwrap(); - ui.label( - result - .time - .to_offset(appearance.utc_offset) - .format(&format) - .unwrap(), - ); - } - }); - }); - - ui.scope(|ui| { - ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); - if let Some(match_percent) = result - .second_obj - .as_ref() - .and_then(|(obj, diff)| { - find_symbol(obj, selected_symbol).map(|sref| { - &diff.sections[sref.section_idx].symbols[sref.symbol_idx] - }) - }) - .and_then(|symbol| symbol.match_percent) - { - ui.colored_label( - match_color_for_symbol(match_percent, appearance), - format!("{:.0}%", match_percent.floor()), - ); - } else { - ui.colored_label(appearance.replace_color, "Missing"); - } - ui.label("Diff base:"); - }); - }, - ); - }, - ); - ui.separator(); - - // Table - StripBuilder::new(ui).size(Size::remainder()).vertical(|mut strip| { - strip.strip(|builder| { - builder.sizes(Size::remainder(), 2).horizontal(|mut strip| { - strip.cell(|ui| { - extab_ui(ui, result.first_obj.as_ref(), selected_symbol, appearance, true); - }); - strip.cell(|ui| { - extab_ui(ui, result.second_obj.as_ref(), selected_symbol, appearance, false); + ui.horizontal(|ui| { + if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() { + ret = Some(DiffViewAction::Build); + } + ui.scope(|ui| { + ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); + if state.build_running { + ui.colored_label(appearance.replace_color, "Building…"); + } else { + ui.label("Last built:"); + let format = format_description::parse("[hour]:[minute]:[second]").unwrap(); + ui.label( + result.time.to_offset(appearance.utc_offset).format(&format).unwrap(), + ); + } }); + ui.separator(); + if ui + .add_enabled(state.source_path_available, egui::Button::new("🖹 Source file")) + .on_hover_text_at_pointer("Open the source file in the default editor") + .on_disabled_hover_text("Source file metadata missing") + .clicked() + { + ret = Some(DiffViewAction::OpenSourcePath); + } }); - }); + + if let Some(((_section, symbol), symbol_diff)) = right_ctx.and_then(|ctx| { + ctx.symbol_ref.map(|symbol_ref| { + (ctx.obj.section_symbol(symbol_ref), ctx.diff.symbol_diff(symbol_ref)) + }) + }) { + let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name); + ui.label( + RichText::new(name) + .font(appearance.code_font.clone()) + .color(appearance.highlight_color), + ); + if let Some(match_percent) = symbol_diff.match_percent { + ui.label( + RichText::new(format!("{:.0}%", match_percent.floor())) + .font(appearance.code_font.clone()) + .color(match_color_for_symbol(match_percent, appearance)), + ); + } + } else { + ui.label( + RichText::new("Missing") + .font(appearance.code_font.clone()) + .color(appearance.replace_color), + ); + } + } + }); + + // Table + render_strips(ui, available_width, 2, |ui, column| { + if column == 0 { + if let Some(ctx) = left_ctx { + extab_ui(ui, ctx, appearance, column); + } + } else if column == 1 { + if let Some(ctx) = right_ctx { + extab_ui(ui, ctx, appearance, column); + } + } }); + ret } diff --git a/objdiff-gui/src/views/function_diff.rs b/objdiff-gui/src/views/function_diff.rs index 65cfd2c..d372220 100644 --- a/objdiff-gui/src/views/function_diff.rs +++ b/objdiff-gui/src/views/function_diff.rs @@ -1,28 +1,29 @@ use std::default::Default; -use egui::{text::LayoutJob, Align, Label, Layout, Response, Sense, Vec2, Widget}; -use egui_extras::{Column, TableBuilder, TableRow}; +use egui::{text::LayoutJob, Id, Label, Response, RichText, Sense, Widget}; +use egui_extras::TableRow; use objdiff_core::{ arch::ObjArch, diff::{ display::{display_diff, DiffText, HighlightKind}, ObjDiff, ObjInsDiff, ObjInsDiffKind, }, - obj::{ObjInfo, ObjIns, ObjInsArg, ObjInsArgValue, ObjSection, ObjSymbol, SymbolRef}, + obj::{ + ObjInfo, ObjIns, ObjInsArg, ObjInsArgValue, ObjSection, ObjSectionKind, ObjSymbol, + SymbolRef, + }, }; use time::format_description; use crate::views::{ appearance::Appearance, - symbol_diff::{match_color_for_symbol, DiffViewState, SymbolRefByName, View}, + column_layout::{render_header, render_strips, render_table}, + symbol_diff::{ + match_color_for_symbol, symbol_list_ui, DiffViewAction, DiffViewNavigation, DiffViewState, + SymbolDiffContext, SymbolFilter, SymbolRefByName, SymbolViewState, View, + }, }; -#[derive(Copy, Clone, Eq, PartialEq)] -enum ColumnId { - Left, - Right, -} - #[derive(Default)] pub struct FunctionViewState { left_highlight: HighlightKind, @@ -30,16 +31,17 @@ pub struct FunctionViewState { } impl FunctionViewState { - fn highlight(&self, column: ColumnId) -> &HighlightKind { + pub fn highlight(&self, column: usize) -> &HighlightKind { match column { - ColumnId::Left => &self.left_highlight, - ColumnId::Right => &self.right_highlight, + 0 => &self.left_highlight, + 1 => &self.right_highlight, + _ => &HighlightKind::None, } } - fn set_highlight(&mut self, column: ColumnId, highlight: HighlightKind) { + pub fn set_highlight(&mut self, column: usize, highlight: HighlightKind) { match column { - ColumnId::Left => { + 0 => { if highlight == self.left_highlight { if highlight == self.right_highlight { self.left_highlight = HighlightKind::None; @@ -51,7 +53,7 @@ impl FunctionViewState { self.left_highlight = highlight; } } - ColumnId::Right => { + 1 => { if highlight == self.right_highlight { if highlight == self.left_highlight { self.left_highlight = HighlightKind::None; @@ -63,10 +65,11 @@ impl FunctionViewState { self.right_highlight = highlight; } } + _ => {} } } - fn clear_highlight(&mut self) { + pub fn clear_highlight(&mut self) { self.left_highlight = HighlightKind::None; self.right_highlight = HighlightKind::None; } @@ -223,17 +226,19 @@ fn find_symbol(obj: &ObjInfo, selected_symbol: &SymbolRefByName) -> Option, ins_diff: &ObjInsDiff, appearance: &Appearance, - ins_view_state: &mut FunctionViewState, - column: ColumnId, + ins_view_state: &FunctionViewState, + column: usize, space_width: f32, response_cb: impl Fn(Response) -> Response, -) { +) -> Option { + let mut ret = None; let label_text; let mut base_color = match ins_diff.kind { ObjInsDiffKind::None | ObjInsDiffKind::OpMismatch | ObjInsDiffKind::ArgMismatch => { @@ -287,7 +292,7 @@ fn diff_text_ui( } DiffText::Spacing(n) => { ui.add_space(n as f32 * space_width); - return; + return ret; } DiffText::Eol => { label_text = "\n".to_string(); @@ -304,22 +309,25 @@ fn diff_text_ui( .ui(ui); response = response_cb(response); if response.clicked() { - ins_view_state.set_highlight(column, text.into()); + ret = Some(DiffViewAction::SetDiffHighlight(column, text.into())); } if len < pad_to { ui.add_space((pad_to - len) as f32 * space_width); } + ret } +#[must_use] fn asm_row_ui( ui: &mut egui::Ui, ins_diff: &ObjInsDiff, symbol: &ObjSymbol, appearance: &Appearance, - ins_view_state: &mut FunctionViewState, - column: ColumnId, + ins_view_state: &FunctionViewState, + column: usize, response_cb: impl Fn(Response) -> Response, -) { +) -> Option { + let mut ret = None; ui.spacing_mut().item_spacing.x = 0.0; ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); if ins_diff.kind != ObjInsDiffKind::None { @@ -327,7 +335,7 @@ fn asm_row_ui( } let space_width = ui.fonts(|f| f.glyph_width(&appearance.code_font, ' ')); display_diff(ins_diff, symbol.address, |text| { - diff_text_ui( + if let Some(action) = diff_text_ui( ui, text, ins_diff, @@ -336,246 +344,476 @@ fn asm_row_ui( column, space_width, &response_cb, - ); + ) { + ret = Some(action); + } Ok::<_, ()>(()) }) .unwrap(); + ret } +#[must_use] fn asm_col_ui( row: &mut TableRow<'_, '_>, - obj: &(ObjInfo, ObjDiff), - symbol_ref: SymbolRef, + ctx: FunctionDiffContext<'_>, appearance: &Appearance, - ins_view_state: &mut FunctionViewState, - column: ColumnId, -) { - let (section, symbol) = obj.0.section_symbol(symbol_ref); - let section = section.unwrap(); - let ins_diff = &obj.1.symbol_diff(symbol_ref).instructions[row.index()]; + ins_view_state: &FunctionViewState, + column: usize, +) -> Option { + let mut ret = None; + let symbol_ref = ctx.symbol_ref?; + let (section, symbol) = ctx.obj.section_symbol(symbol_ref); + let section = section?; + let ins_diff = &ctx.diff.symbol_diff(symbol_ref).instructions[row.index()]; let response_cb = |response: Response| { if let Some(ins) = &ins_diff.ins { response.context_menu(|ui| ins_context_menu(ui, section, ins, symbol)); response.on_hover_ui_at_pointer(|ui| { - ins_hover_ui(ui, obj.0.arch.as_ref(), section, ins, symbol, appearance) + ins_hover_ui(ui, ctx.obj.arch.as_ref(), section, ins, symbol, appearance) }) } else { response } }; let (_, response) = row.col(|ui| { - asm_row_ui(ui, ins_diff, symbol, appearance, ins_view_state, column, response_cb); + if let Some(action) = + asm_row_ui(ui, ins_diff, symbol, appearance, ins_view_state, column, response_cb) + { + ret = Some(action); + } }); response_cb(response); + ret } -fn empty_col_ui(row: &mut TableRow<'_, '_>) { - row.col(|ui| { - ui.label(""); - }); -} - +#[must_use] fn asm_table_ui( - table: TableBuilder<'_>, - left_obj: Option<&(ObjInfo, ObjDiff)>, - right_obj: Option<&(ObjInfo, ObjDiff)>, - selected_symbol: &SymbolRefByName, + ui: &mut egui::Ui, + available_width: f32, + left_ctx: Option>, + right_ctx: Option>, appearance: &Appearance, - ins_view_state: &mut FunctionViewState, -) -> Option<()> { - let left_symbol = left_obj.and_then(|(obj, _)| find_symbol(obj, selected_symbol)); - let right_symbol = right_obj.and_then(|(obj, _)| find_symbol(obj, selected_symbol)); - let instructions_len = match (left_symbol, right_symbol) { - (Some(left_symbol_ref), Some(right_symbol_ref)) => { - let left_len = left_obj.unwrap().1.symbol_diff(left_symbol_ref).instructions.len(); - let right_len = right_obj.unwrap().1.symbol_diff(right_symbol_ref).instructions.len(); - debug_assert_eq!(left_len, right_len); + ins_view_state: &FunctionViewState, + symbol_state: &SymbolViewState, +) -> Option { + let mut ret = None; + let left_len = left_ctx.and_then(|ctx| { + ctx.symbol_ref.map(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).instructions.len()) + }); + let right_len = right_ctx.and_then(|ctx| { + ctx.symbol_ref.map(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).instructions.len()) + }); + let instructions_len = match (left_len, right_len) { + (Some(left_len), Some(right_len)) => { + if left_len != right_len { + ui.label("Instruction count mismatch"); + return None; + } left_len } - (Some(left_symbol_ref), None) => { - left_obj.unwrap().1.symbol_diff(left_symbol_ref).instructions.len() + (Some(left_len), None) => left_len, + (None, Some(right_len)) => right_len, + (None, None) => { + ui.label("No symbol selected"); + return None; } - (None, Some(right_symbol_ref)) => { - right_obj.unwrap().1.symbol_diff(right_symbol_ref).instructions.len() - } - (None, None) => return None, }; - table.body(|body| { - body.rows(appearance.code_font.size, instructions_len, |mut row| { - row.set_hovered(false); // Disable row hover effect - if let (Some(left_obj), Some(left_symbol_ref)) = (left_obj, left_symbol) { - asm_col_ui( - &mut row, - left_obj, - left_symbol_ref, - appearance, - ins_view_state, - ColumnId::Left, - ); - } else { - empty_col_ui(&mut row); - } - if let (Some(right_obj), Some(right_symbol_ref)) = (right_obj, right_symbol) { - asm_col_ui( - &mut row, - right_obj, - right_symbol_ref, - appearance, - ins_view_state, - ColumnId::Right, - ); - } else { - empty_col_ui(&mut row); - } - if row.response().clicked() { - ins_view_state.clear_highlight(); + if left_len.is_some() && right_len.is_some() { + // Joint view + render_table( + ui, + available_width, + 2, + appearance.code_font.size, + instructions_len, + |row, column| { + if column == 0 { + if let Some(ctx) = left_ctx { + if let Some(action) = + asm_col_ui(row, ctx, appearance, ins_view_state, column) + { + ret = Some(action); + } + } + } else if column == 1 { + if let Some(ctx) = right_ctx { + if let Some(action) = + asm_col_ui(row, ctx, appearance, ins_view_state, column) + { + ret = Some(action); + } + } + if row.response().clicked() { + ret = Some(DiffViewAction::ClearDiffHighlight); + } + } + }, + ); + } else { + // Split view, one side is the symbol list + render_strips(ui, available_width, 2, |ui, column| { + if column == 0 { + if let Some(ctx) = left_ctx { + if ctx.has_symbol() { + render_table( + ui, + available_width / 2.0, + 1, + appearance.code_font.size, + instructions_len, + |row, column| { + if let Some(action) = + asm_col_ui(row, ctx, appearance, ins_view_state, column) + { + ret = Some(action); + } + if row.response().clicked() { + ret = Some(DiffViewAction::ClearDiffHighlight); + } + }, + ); + } else if let Some((right_ctx, right_symbol_ref)) = + right_ctx.and_then(|ctx| ctx.symbol_ref.map(|symbol_ref| (ctx, symbol_ref))) + { + if let Some(action) = symbol_list_ui( + ui, + SymbolDiffContext { obj: ctx.obj, diff: ctx.diff }, + None, + symbol_state, + SymbolFilter::Mapping(right_symbol_ref), + appearance, + column, + ) { + match action { + DiffViewAction::Navigate(DiffViewNavigation { + left_symbol: Some(left_symbol_ref), + .. + }) => { + let (right_section, right_symbol) = + right_ctx.obj.section_symbol(right_symbol_ref); + ret = Some(DiffViewAction::SetMapping( + match right_section.map(|s| s.kind) { + Some(ObjSectionKind::Code) => View::FunctionDiff, + _ => View::SymbolDiff, + }, + left_symbol_ref, + SymbolRefByName::new(right_symbol, right_section), + )); + } + DiffViewAction::SetSymbolHighlight(_, _) => { + // Ignore + } + _ => { + ret = Some(action); + } + } + } + } + } else { + ui.label("No left object"); + } + } else if column == 1 { + if let Some(ctx) = right_ctx { + if ctx.has_symbol() { + render_table( + ui, + available_width / 2.0, + 1, + appearance.code_font.size, + instructions_len, + |row, column| { + if let Some(action) = + asm_col_ui(row, ctx, appearance, ins_view_state, column) + { + ret = Some(action); + } + if row.response().clicked() { + ret = Some(DiffViewAction::ClearDiffHighlight); + } + }, + ); + } else if let Some((left_ctx, left_symbol_ref)) = + left_ctx.and_then(|ctx| ctx.symbol_ref.map(|symbol_ref| (ctx, symbol_ref))) + { + if let Some(action) = symbol_list_ui( + ui, + SymbolDiffContext { obj: ctx.obj, diff: ctx.diff }, + None, + symbol_state, + SymbolFilter::Mapping(left_symbol_ref), + appearance, + column, + ) { + match action { + DiffViewAction::Navigate(DiffViewNavigation { + right_symbol: Some(right_symbol_ref), + .. + }) => { + let (left_section, left_symbol) = + left_ctx.obj.section_symbol(left_symbol_ref); + ret = Some(DiffViewAction::SetMapping( + match left_section.map(|s| s.kind) { + Some(ObjSectionKind::Code) => View::FunctionDiff, + _ => View::SymbolDiff, + }, + SymbolRefByName::new(left_symbol, left_section), + right_symbol_ref, + )); + } + DiffViewAction::SetSymbolHighlight(_, _) => { + // Ignore + } + _ => { + ret = Some(action); + } + } + } + } + } else { + ui.label("No right object"); + } } }); - }); - Some(()) + } + ret +} + +#[derive(Clone, Copy)] +pub struct FunctionDiffContext<'a> { + pub obj: &'a ObjInfo, + pub diff: &'a ObjDiff, + pub symbol_ref: Option, +} + +impl<'a> FunctionDiffContext<'a> { + pub fn new( + obj: Option<&'a (ObjInfo, ObjDiff)>, + selected_symbol: Option<&SymbolRefByName>, + ) -> Option { + obj.map(|(obj, diff)| Self { + obj, + diff, + symbol_ref: selected_symbol.and_then(|s| find_symbol(obj, s)), + }) + } + + #[inline] + pub fn has_symbol(&self) -> bool { self.symbol_ref.is_some() } } -pub fn function_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &Appearance) { - let (Some(result), Some(selected_symbol)) = (&state.build, &state.symbol_state.selected_symbol) - else { - return; +#[must_use] +pub fn function_diff_ui( + ui: &mut egui::Ui, + state: &DiffViewState, + appearance: &Appearance, +) -> Option { + let mut ret = None; + let Some(result) = &state.build else { + return ret; }; + let mut left_ctx = FunctionDiffContext::new( + result.first_obj.as_ref(), + state.symbol_state.left_symbol.as_ref(), + ); + let mut right_ctx = FunctionDiffContext::new( + result.second_obj.as_ref(), + state.symbol_state.right_symbol.as_ref(), + ); + + // If one side is missing a symbol, but the diff process found a match, use that symbol + let left_diff_symbol = left_ctx.and_then(|ctx| { + ctx.symbol_ref.and_then(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).target_symbol) + }); + let right_diff_symbol = right_ctx.and_then(|ctx| { + ctx.symbol_ref.and_then(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).target_symbol) + }); + if left_diff_symbol.is_some() && right_ctx.map_or(false, |ctx| !ctx.has_symbol()) { + let (right_section, right_symbol) = + right_ctx.unwrap().obj.section_symbol(left_diff_symbol.unwrap()); + let symbol_ref = SymbolRefByName::new(right_symbol, right_section); + right_ctx = FunctionDiffContext::new(result.second_obj.as_ref(), Some(&symbol_ref)); + ret = Some(DiffViewAction::Navigate(DiffViewNavigation { + view: Some(View::FunctionDiff), + left_symbol: state.symbol_state.left_symbol.clone(), + right_symbol: Some(symbol_ref), + })); + } else if right_diff_symbol.is_some() && left_ctx.map_or(false, |ctx| !ctx.has_symbol()) { + let (left_section, left_symbol) = + left_ctx.unwrap().obj.section_symbol(right_diff_symbol.unwrap()); + let symbol_ref = SymbolRefByName::new(left_symbol, left_section); + left_ctx = FunctionDiffContext::new(result.first_obj.as_ref(), Some(&symbol_ref)); + ret = Some(DiffViewAction::Navigate(DiffViewNavigation { + view: Some(View::FunctionDiff), + left_symbol: Some(symbol_ref), + right_symbol: state.symbol_state.right_symbol.clone(), + })); + } + + // If both sides are missing a symbol, switch to symbol diff view + if right_ctx.map_or(false, |ctx| !ctx.has_symbol()) + && left_ctx.map_or(false, |ctx| !ctx.has_symbol()) + { + return Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff())); + } + // Header let available_width = ui.available_width(); - let column_width = available_width / 2.0; - ui.allocate_ui_with_layout( - Vec2 { x: available_width, y: 100.0 }, - Layout::left_to_right(Align::Min), - |ui| { - ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate); - + render_header(ui, available_width, 2, |ui, column| { + if column == 0 { // Left column - ui.allocate_ui_with_layout( - Vec2 { x: column_width, y: 100.0 }, - Layout::top_down(Align::Min), - |ui| { - ui.set_width(column_width); - - ui.horizontal(|ui| { - if ui.button("⏴ Back").clicked() { - state.current_view = View::SymbolDiff; - } - ui.separator(); - if ui - .add_enabled( - !state.scratch_running && state.scratch_available, - egui::Button::new("📲 decomp.me"), - ) - .on_hover_text_at_pointer("Create a new scratch on decomp.me (beta)") - .on_disabled_hover_text("Scratch configuration missing") - .clicked() - { - state.queue_scratch = true; - } - }); - - let name = selected_symbol - .demangled_symbol_name - .as_deref() - .unwrap_or(&selected_symbol.symbol_name); - ui.scope(|ui| { - ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); - ui.colored_label(appearance.highlight_color, name); - ui.label("Diff target:"); - }); - }, - ); - + ui.horizontal(|ui| { + if ui.button("⏴ Back").clicked() { + ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff())); + } + ui.separator(); + if ui + .add_enabled( + !state.scratch_running + && state.scratch_available + && left_ctx.map_or(false, |ctx| ctx.has_symbol()), + egui::Button::new("📲 decomp.me"), + ) + .on_hover_text_at_pointer("Create a new scratch on decomp.me (beta)") + .on_disabled_hover_text("Scratch configuration missing") + .clicked() + { + if let Some((_section, symbol)) = left_ctx.and_then(|ctx| { + ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref)) + }) { + ret = Some(DiffViewAction::CreateScratch(symbol.name.clone())); + } + } + }); + + if let Some((_section, symbol)) = left_ctx + .and_then(|ctx| ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref))) + { + let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name); + ui.label( + RichText::new(name) + .font(appearance.code_font.clone()) + .color(appearance.highlight_color), + ); + if right_ctx.map_or(false, |m| m.has_symbol()) + && ui + .button("Change target") + .on_hover_text_at_pointer("Choose a different symbol to use as the target") + .clicked() + { + if let Some(symbol_ref) = state.symbol_state.right_symbol.as_ref() { + ret = Some(DiffViewAction::SelectingLeft(symbol_ref.clone())); + } + } + } else { + ui.label( + RichText::new("Missing") + .font(appearance.code_font.clone()) + .color(appearance.replace_color), + ); + ui.label( + RichText::new("Choose target symbol") + .font(appearance.code_font.clone()) + .color(appearance.highlight_color), + ); + } + } else if column == 1 { // Right column - ui.allocate_ui_with_layout( - Vec2 { x: column_width, y: 100.0 }, - Layout::top_down(Align::Min), - |ui| { - ui.set_width(column_width); - - ui.horizontal(|ui| { - if ui - .add_enabled(!state.build_running, egui::Button::new("Build")) - .clicked() - { - state.queue_build = true; - } - ui.scope(|ui| { - ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); - if state.build_running { - ui.colored_label(appearance.replace_color, "Building…"); - } else { - ui.label("Last built:"); - let format = - format_description::parse("[hour]:[minute]:[second]").unwrap(); - ui.label( - result - .time - .to_offset(appearance.utc_offset) - .format(&format) - .unwrap(), - ); - } - }); + ui.horizontal(|ui| { + if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() { + ret = Some(DiffViewAction::Build); + } + ui.scope(|ui| { + ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); + if state.build_running { + ui.colored_label(appearance.replace_color, "Building…"); + } else { + ui.label("Last built:"); + let format = format_description::parse("[hour]:[minute]:[second]").unwrap(); + ui.label( + result.time.to_offset(appearance.utc_offset).format(&format).unwrap(), + ); + } + }); + ui.separator(); + if ui + .add_enabled(state.source_path_available, egui::Button::new("🖹 Source file")) + .on_hover_text_at_pointer("Open the source file in the default editor") + .on_disabled_hover_text("Source file metadata missing") + .clicked() + { + ret = Some(DiffViewAction::OpenSourcePath); + } + }); + + if let Some(((_section, symbol), symbol_diff)) = right_ctx.and_then(|ctx| { + ctx.symbol_ref.map(|symbol_ref| { + (ctx.obj.section_symbol(symbol_ref), ctx.diff.symbol_diff(symbol_ref)) + }) + }) { + let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name); + ui.label( + RichText::new(name) + .font(appearance.code_font.clone()) + .color(appearance.highlight_color), + ); + ui.horizontal(|ui| { + if let Some(match_percent) = symbol_diff.match_percent { + ui.label( + RichText::new(format!("{:.0}%", match_percent.floor())) + .font(appearance.code_font.clone()) + .color(match_color_for_symbol(match_percent, appearance)), + ); + } + if left_ctx.map_or(false, |m| m.has_symbol()) { ui.separator(); if ui - .add_enabled( - state.source_path_available, - egui::Button::new("🖹 Source file"), + .button("Change base") + .on_hover_text_at_pointer( + "Choose a different symbol to use as the base", ) - .on_hover_text_at_pointer("Open the source file in the default editor") - .on_disabled_hover_text("Source file metadata missing") .clicked() { - state.queue_open_source_path = true; - } - }); - - ui.scope(|ui| { - ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); - if let Some(match_percent) = result - .second_obj - .as_ref() - .and_then(|(obj, diff)| { - find_symbol(obj, selected_symbol).map(|sref| { - &diff.sections[sref.section_idx].symbols[sref.symbol_idx] - }) - }) - .and_then(|symbol| symbol.match_percent) - { - ui.colored_label( - match_color_for_symbol(match_percent, appearance), - format!("{:.0}%", match_percent.floor()), - ); - } else { - ui.colored_label(appearance.replace_color, "Missing"); + if let Some(symbol_ref) = state.symbol_state.left_symbol.as_ref() { + ret = Some(DiffViewAction::SelectingRight(symbol_ref.clone())); + } } - ui.label("Diff base:"); - }); - }, - ); - }, - ); - ui.separator(); + } + }); + } else { + ui.label( + RichText::new("Missing") + .font(appearance.code_font.clone()) + .color(appearance.replace_color), + ); + ui.label( + RichText::new("Choose base symbol") + .font(appearance.code_font.clone()) + .color(appearance.highlight_color), + ); + } + } + }); // Table - ui.style_mut().interaction.selectable_labels = false; - let available_height = ui.available_height(); - let table = TableBuilder::new(ui) - .striped(false) - .cell_layout(Layout::left_to_right(Align::Min)) - .columns(Column::exact(column_width).clip(true), 2) - .resizable(false) - .auto_shrink([false, false]) - .min_scrolled_height(available_height) - .sense(Sense::click()); - asm_table_ui( - table, - result.first_obj.as_ref(), - result.second_obj.as_ref(), - selected_symbol, - appearance, - &mut state.function_state, - ); + let id = Id::new(state.symbol_state.left_symbol.as_ref().map(|s| s.symbol_name.as_str())) + .with(state.symbol_state.right_symbol.as_ref().map(|s| s.symbol_name.as_str())); + if let Some(action) = ui + .push_id(id, |ui| { + asm_table_ui( + ui, + available_width, + left_ctx, + right_ctx, + appearance, + &state.function_state, + &state.symbol_state, + ) + }) + .inner + { + ret = Some(action); + } + ret } diff --git a/objdiff-gui/src/views/mod.rs b/objdiff-gui/src/views/mod.rs index 7b31c54..16e9380 100644 --- a/objdiff-gui/src/views/mod.rs +++ b/objdiff-gui/src/views/mod.rs @@ -1,6 +1,7 @@ use egui::{text::LayoutJob, Color32, FontId, TextFormat}; pub(crate) mod appearance; +pub(crate) mod column_layout; pub(crate) mod config; pub(crate) mod data_diff; pub(crate) mod debug; diff --git a/objdiff-gui/src/views/symbol_diff.rs b/objdiff-gui/src/views/symbol_diff.rs index 8d9618c..cd17eb4 100644 --- a/objdiff-gui/src/views/symbol_diff.rs +++ b/objdiff-gui/src/views/symbol_diff.rs @@ -1,13 +1,12 @@ -use std::mem::take; +use std::{collections::BTreeMap, mem::take}; use egui::{ - text::LayoutJob, Align, CollapsingHeader, Color32, Id, Layout, OpenUrl, ScrollArea, - SelectableLabel, TextEdit, Ui, Vec2, Widget, + text::LayoutJob, CollapsingHeader, Color32, Id, OpenUrl, ScrollArea, SelectableLabel, TextEdit, + Ui, Widget, }; -use egui_extras::{Size, StripBuilder}; use objdiff_core::{ arch::ObjArch, - diff::{ObjDiff, ObjSymbolDiff}, + diff::{display::HighlightKind, ObjDiff, ObjSymbolDiff}, obj::{ObjInfo, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlags, SymbolRef}, }; use regex::{Regex, RegexBuilder}; @@ -19,17 +18,28 @@ use crate::{ objdiff::{BuildStatus, ObjDiffResult}, Job, JobQueue, JobResult, }, - views::{appearance::Appearance, function_diff::FunctionViewState, write_text}, + views::{ + appearance::Appearance, + column_layout::{render_header, render_strips}, + function_diff::FunctionViewState, + write_text, + }, }; +#[derive(Debug, Clone)] pub struct SymbolRefByName { pub symbol_name: String, - pub demangled_symbol_name: Option, - pub section_name: String, + pub section_name: Option, +} + +impl SymbolRefByName { + pub fn new(symbol: &ObjSymbol, section: Option<&ObjSection>) -> Self { + Self { symbol_name: symbol.name.clone(), section_name: section.map(|s| s.name.clone()) } + } } -#[allow(clippy::enum_variant_names)] -#[derive(Default, Eq, PartialEq, Copy, Clone)] +#[expect(clippy::enum_variant_names)] +#[derive(Debug, Default, Eq, PartialEq, Copy, Clone, Hash)] pub enum View { #[default] SymbolDiff, @@ -38,6 +48,71 @@ pub enum View { ExtabDiff, } +#[derive(Debug, Clone)] +pub enum DiffViewAction { + /// Queue a rebuild of the current object(s) + Build, + /// Navigate to a new diff view + Navigate(DiffViewNavigation), + /// Set the highlighted symbols in the symbols view + SetSymbolHighlight(Option, Option), + /// Set the symbols view search filter + SetSearch(String), + /// Submit the current function to decomp.me + CreateScratch(String), + /// Open the source path of the current object + OpenSourcePath, + /// Set the highlight for a diff column + SetDiffHighlight(usize, HighlightKind), + /// Clear the highlight for all diff columns + ClearDiffHighlight, + /// Start selecting a left symbol for mapping. + /// The symbol reference is the right symbol to map to. + SelectingLeft(SymbolRefByName), + /// Start selecting a right symbol for mapping. + /// The symbol reference is the left symbol to map to. + SelectingRight(SymbolRefByName), + /// Set a symbol mapping. + SetMapping(View, SymbolRefByName, SymbolRefByName), + /// Set the show_mapped_symbols flag + SetShowMappedSymbols(bool), +} + +#[derive(Debug, Clone, Default)] +pub struct DiffViewNavigation { + pub view: Option, + pub left_symbol: Option, + pub right_symbol: Option, +} + +impl DiffViewNavigation { + pub fn symbol_diff() -> Self { + Self { view: Some(View::SymbolDiff), left_symbol: None, right_symbol: None } + } + + pub fn with_symbols( + view: View, + other_ctx: Option>, + symbol: &ObjSymbol, + section: &ObjSection, + symbol_diff: &ObjSymbolDiff, + column: usize, + ) -> Self { + let symbol1 = Some(SymbolRefByName::new(symbol, Some(section))); + let symbol2 = symbol_diff.target_symbol.and_then(|symbol_ref| { + other_ctx.map(|ctx| { + let (section, symbol) = ctx.obj.section_symbol(symbol_ref); + SymbolRefByName::new(symbol, section) + }) + }); + match column { + 0 => Self { view: Some(view), left_symbol: symbol1, right_symbol: symbol2 }, + 1 => Self { view: Some(view), left_symbol: symbol2, right_symbol: symbol1 }, + _ => unreachable!("Invalid column index"), + } + } +} + #[derive(Default)] pub struct DiffViewState { pub build: Option>, @@ -47,22 +122,23 @@ pub struct DiffViewState { pub function_state: FunctionViewState, pub search: String, pub search_regex: Option, - pub queue_build: bool, pub build_running: bool, pub scratch_available: bool, - pub queue_scratch: bool, pub scratch_running: bool, pub source_path_available: bool, - pub queue_open_source_path: bool, + pub post_build_nav: Option, + pub object_name: String, } #[derive(Default)] pub struct SymbolViewState { pub highlighted_symbol: (Option, Option), - pub selected_symbol: Option, + pub left_symbol: Option, + pub right_symbol: Option, pub reverse_fn_order: bool, pub disable_reverse_fn_order: bool, pub show_hidden_symbols: bool, + pub show_mapped_symbols: bool, } impl DiffViewState { @@ -70,6 +146,16 @@ impl DiffViewState { jobs.results.retain_mut(|result| match result { JobResult::ObjDiff(result) => { self.build = take(result); + + // TODO: where should this go? + if let Some(result) = self.post_build_nav.take() { + if let Some(view) = result.view { + self.current_view = view; + } + self.symbol_state.left_symbol = result.left_symbol; + self.symbol_state.right_symbol = result.right_symbol; + } + false } JobResult::CreateScratch(result) => { @@ -93,44 +179,103 @@ impl DiffViewState { self.source_path_available = false; } self.scratch_available = CreateScratchConfig::is_available(&state.config); + self.object_name = + state.config.selected_obj.as_ref().map(|o| o.name.clone()).unwrap_or_default(); } } - pub fn post_update(&mut self, ctx: &egui::Context, jobs: &mut JobQueue, state: &AppStateRef) { + pub fn post_update( + &mut self, + action: Option, + ctx: &egui::Context, + jobs: &mut JobQueue, + state: &AppStateRef, + ) { if let Some(result) = take(&mut self.scratch) { ctx.output_mut(|o| o.open_url = Some(OpenUrl::new_tab(result.scratch_url))); } - if self.queue_build { - self.queue_build = false; - if let Ok(mut state) = state.write() { - state.queue_build = true; + let Some(action) = action else { + return; + }; + match action { + DiffViewAction::Build => { + if let Ok(mut state) = state.write() { + state.queue_build = true; + } } - } - - if self.queue_scratch { - self.queue_scratch = false; - if let Some(function_name) = - self.symbol_state.selected_symbol.as_ref().map(|sym| sym.symbol_name.clone()) - { - if let Ok(state) = state.read() { - match CreateScratchConfig::from_config(&state.config, function_name) { - Ok(config) => { - jobs.push_once(Job::CreateScratch, || { - start_create_scratch(ctx, config) - }); + DiffViewAction::Navigate(nav) => { + if self.post_build_nav.is_some() { + // Ignore action if we're already navigating + return; + } + self.symbol_state.highlighted_symbol = (None, None); + let Ok(mut state) = state.write() else { + return; + }; + if (nav.left_symbol.is_some() && nav.right_symbol.is_some()) + || (nav.left_symbol.is_none() && nav.right_symbol.is_none()) + || nav.view != Some(View::FunctionDiff) + { + // Regular navigation + if state.is_selecting_symbol() { + // Cancel selection and reload + state.clear_selection(); + self.post_build_nav = Some(nav); + } else { + // Navigate immediately + if let Some(view) = nav.view { + self.current_view = view; + } + self.symbol_state.left_symbol = nav.left_symbol; + self.symbol_state.right_symbol = nav.right_symbol; + } + } else { + // Enter selection mode + match (&nav.left_symbol, &nav.right_symbol) { + (Some(left_ref), None) => { + state.set_selecting_right(&left_ref.symbol_name); } - Err(err) => { - log::error!("Failed to create scratch config: {err}"); + (None, Some(right_ref)) => { + state.set_selecting_left(&right_ref.symbol_name); } + (Some(_), Some(_)) => unreachable!(), + (None, None) => unreachable!(), } + self.post_build_nav = Some(nav); } } - } - - if self.queue_open_source_path { - self.queue_open_source_path = false; - if let Ok(state) = state.read() { + DiffViewAction::SetSymbolHighlight(left, right) => { + self.symbol_state.highlighted_symbol = (left, right); + } + DiffViewAction::SetSearch(search) => { + self.search_regex = if search.is_empty() { + None + } else if let Ok(regex) = RegexBuilder::new(&search).case_insensitive(true).build() + { + Some(regex) + } else { + None + }; + self.search = search; + } + DiffViewAction::CreateScratch(function_name) => { + let Ok(state) = state.read() else { + return; + }; + match CreateScratchConfig::from_config(&state.config, function_name) { + Ok(config) => { + jobs.push_once(Job::CreateScratch, || start_create_scratch(ctx, config)); + } + Err(err) => { + log::error!("Failed to create scratch config: {err}"); + } + } + } + DiffViewAction::OpenSourcePath => { + let Ok(state) = state.read() else { + return; + }; if let (Some(project_dir), Some(source_path)) = ( &state.config.project_dir, state.config.selected_obj.as_ref().and_then(|obj| obj.source_path.as_ref()), @@ -142,6 +287,67 @@ impl DiffViewState { }); } } + DiffViewAction::SetDiffHighlight(column, kind) => { + self.function_state.set_highlight(column, kind); + } + DiffViewAction::ClearDiffHighlight => { + self.function_state.clear_highlight(); + } + DiffViewAction::SelectingLeft(right_ref) => { + if self.post_build_nav.is_some() { + // Ignore action if we're already navigating + return; + } + let Ok(mut state) = state.write() else { + return; + }; + state.set_selecting_left(&right_ref.symbol_name); + self.post_build_nav = Some(DiffViewNavigation { + view: Some(View::FunctionDiff), + left_symbol: None, + right_symbol: Some(right_ref), + }); + } + DiffViewAction::SelectingRight(left_ref) => { + if self.post_build_nav.is_some() { + // Ignore action if we're already navigating + return; + } + let Ok(mut state) = state.write() else { + return; + }; + state.set_selecting_right(&left_ref.symbol_name); + self.post_build_nav = Some(DiffViewNavigation { + view: Some(View::FunctionDiff), + left_symbol: Some(left_ref), + right_symbol: None, + }); + } + DiffViewAction::SetMapping(view, left_ref, right_ref) => { + if self.post_build_nav.is_some() { + // Ignore action if we're already navigating + return; + } + let Ok(mut state) = state.write() else { + return; + }; + state.set_symbol_mapping( + left_ref.symbol_name.clone(), + right_ref.symbol_name.clone(), + ); + if view == View::SymbolDiff { + self.post_build_nav = Some(DiffViewNavigation::symbol_diff()); + } else { + self.post_build_nav = Some(DiffViewNavigation { + view: Some(view), + left_symbol: Some(left_ref), + right_symbol: Some(right_ref), + }); + } + } + DiffViewAction::SetShowMappedSymbols(value) => { + self.symbol_state.show_mapped_symbols = value; + } } } } @@ -158,11 +364,13 @@ pub fn match_color_for_symbol(match_percent: f32, appearance: &Appearance) -> Co fn symbol_context_menu_ui( ui: &mut Ui, - state: &mut SymbolViewState, - arch: &dyn ObjArch, + ctx: SymbolDiffContext<'_>, + other_ctx: Option>, symbol: &ObjSymbol, + symbol_diff: &ObjSymbolDiff, section: Option<&ObjSection>, -) -> Option { + column: usize, +) -> Option { let mut ret = None; ui.scope(|ui| { ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); @@ -185,14 +393,35 @@ fn symbol_context_menu_ui( } } if let Some(section) = section { - let has_extab = arch.ppc().and_then(|ppc| ppc.extab_for_symbol(symbol)).is_some(); + let has_extab = + ctx.obj.arch.ppc().and_then(|ppc| ppc.extab_for_symbol(symbol)).is_some(); if has_extab && ui.button("Decode exception table").clicked() { - state.selected_symbol = Some(SymbolRefByName { - symbol_name: symbol.name.clone(), - demangled_symbol_name: symbol.demangled_name.clone(), - section_name: section.name.clone(), - }); - ret = Some(View::ExtabDiff); + ret = Some(DiffViewNavigation::with_symbols( + View::ExtabDiff, + other_ctx, + symbol, + section, + symbol_diff, + column, + )); + ui.close_menu(); + } + + if ui.button("Map symbol").clicked() { + let symbol_ref = SymbolRefByName::new(symbol, Some(section)); + if column == 0 { + ret = Some(DiffViewNavigation { + view: Some(View::FunctionDiff), + left_symbol: Some(symbol_ref), + right_symbol: None, + }); + } else { + ret = Some(DiffViewNavigation { + view: Some(View::FunctionDiff), + left_symbol: None, + right_symbol: Some(symbol_ref), + }); + } ui.close_menu(); } } @@ -232,27 +461,28 @@ fn symbol_hover_ui(ui: &mut Ui, arch: &dyn ObjArch, symbol: &ObjSymbol, appearan } #[must_use] -#[allow(clippy::too_many_arguments)] +#[expect(clippy::too_many_arguments)] fn symbol_ui( ui: &mut Ui, - arch: &dyn ObjArch, + ctx: SymbolDiffContext<'_>, + other_ctx: Option>, symbol: &ObjSymbol, symbol_diff: &ObjSymbolDiff, section: Option<&ObjSection>, - state: &mut SymbolViewState, + state: &SymbolViewState, appearance: &Appearance, - left: bool, -) -> Option { + column: usize, +) -> Option { + let mut ret = None; if symbol.flags.0.contains(ObjSymbolFlags::Hidden) && !state.show_hidden_symbols { - return None; + return ret; } - let mut ret = None; let mut job = LayoutJob::default(); let name: &str = if let Some(demangled) = &symbol.demangled_name { demangled } else { &symbol.name }; let mut selected = false; if let Some(sym_ref) = - if left { state.highlighted_symbol.0 } else { state.highlighted_symbol.1 } + if column == 0 { state.highlighted_symbol.0 } else { state.highlighted_symbol.1 } { selected = symbol_diff.symbol_ref == sym_ref; } @@ -292,90 +522,166 @@ fn symbol_ui( write_text(") ", appearance.text_color, &mut job, appearance.code_font.clone()); } write_text(name, appearance.highlight_color, &mut job, appearance.code_font.clone()); - let response = SelectableLabel::new(selected, job) - .ui(ui) - .on_hover_ui_at_pointer(|ui| symbol_hover_ui(ui, arch, symbol, appearance)); + let response = SelectableLabel::new(selected, job).ui(ui).on_hover_ui_at_pointer(|ui| { + symbol_hover_ui(ui, ctx.obj.arch.as_ref(), symbol, appearance) + }); response.context_menu(|ui| { - ret = ret.or(symbol_context_menu_ui(ui, state, arch, symbol, section)); + if let Some(result) = + symbol_context_menu_ui(ui, ctx, other_ctx, symbol, symbol_diff, section, column) + { + ret = Some(DiffViewAction::Navigate(result)); + } }); if response.clicked() { if let Some(section) = section { - if section.kind == ObjSectionKind::Code { - state.selected_symbol = Some(SymbolRefByName { - symbol_name: symbol.name.clone(), - demangled_symbol_name: symbol.demangled_name.clone(), - section_name: section.name.clone(), - }); - ret = Some(View::FunctionDiff); - } else if section.kind == ObjSectionKind::Data { - state.selected_symbol = Some(SymbolRefByName { - symbol_name: section.name.clone(), - demangled_symbol_name: None, - section_name: section.name.clone(), - }); - ret = Some(View::DataDiff); + match section.kind { + ObjSectionKind::Code => { + ret = Some(DiffViewAction::Navigate(DiffViewNavigation::with_symbols( + View::FunctionDiff, + other_ctx, + symbol, + section, + symbol_diff, + column, + ))); + } + ObjSectionKind::Data => { + ret = Some(DiffViewAction::Navigate(DiffViewNavigation::with_symbols( + View::DataDiff, + other_ctx, + symbol, + section, + symbol_diff, + column, + ))); + } + ObjSectionKind::Bss => {} } } } else if response.hovered() { - state.highlighted_symbol = if let Some(diff_symbol) = symbol_diff.diff_symbol { - if left { - (Some(symbol_diff.symbol_ref), Some(diff_symbol)) + ret = Some(if let Some(target_symbol) = symbol_diff.target_symbol { + if column == 0 { + DiffViewAction::SetSymbolHighlight( + Some(symbol_diff.symbol_ref), + Some(target_symbol), + ) } else { - (Some(diff_symbol), Some(symbol_diff.symbol_ref)) + DiffViewAction::SetSymbolHighlight( + Some(target_symbol), + Some(symbol_diff.symbol_ref), + ) } } else { - (None, None) - }; + DiffViewAction::SetSymbolHighlight(None, None) + }); } ret } -fn symbol_matches_search(symbol: &ObjSymbol, search_regex: Option<&Regex>) -> bool { - if let Some(search_regex) = search_regex { - search_regex.is_match(&symbol.name) - || symbol.demangled_name.as_ref().map(|s| search_regex.is_match(s)).unwrap_or(false) - } else { - true +fn symbol_matches_filter( + symbol: &ObjSymbol, + diff: &ObjSymbolDiff, + filter: SymbolFilter<'_>, +) -> bool { + match filter { + SymbolFilter::None => true, + SymbolFilter::Search(regex) => { + regex.is_match(&symbol.name) + || symbol.demangled_name.as_ref().map(|s| regex.is_match(s)).unwrap_or(false) + } + SymbolFilter::Mapping(symbol_ref) => diff.target_symbol == Some(symbol_ref), } } +#[derive(Copy, Clone)] +pub enum SymbolFilter<'a> { + None, + Search(&'a Regex), + Mapping(SymbolRef), +} + #[must_use] -fn symbol_list_ui( +pub fn symbol_list_ui( ui: &mut Ui, - obj: &(ObjInfo, ObjDiff), - state: &mut SymbolViewState, - search_regex: Option<&Regex>, + ctx: SymbolDiffContext<'_>, + other_ctx: Option>, + state: &SymbolViewState, + filter: SymbolFilter<'_>, appearance: &Appearance, - left: bool, -) -> Option { + column: usize, +) -> Option { let mut ret = None; - let arch = obj.0.arch.as_ref(); ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| { + let mut mapping = BTreeMap::new(); + if let SymbolFilter::Mapping(target_ref) = filter { + let mut show_mapped_symbols = state.show_mapped_symbols; + if ui.checkbox(&mut show_mapped_symbols, "Show mapped symbols").changed() { + ret = Some(DiffViewAction::SetShowMappedSymbols(show_mapped_symbols)); + } + for mapping_diff in &ctx.diff.mapping_symbols { + if mapping_diff.target_symbol == Some(target_ref) { + if !show_mapped_symbols { + let symbol_diff = ctx.diff.symbol_diff(mapping_diff.symbol_ref); + if symbol_diff.target_symbol.is_some() { + continue; + } + } + mapping.insert(mapping_diff.symbol_ref, mapping_diff); + } + } + } else { + for (symbol, diff) in ctx.obj.common.iter().zip(&ctx.diff.common) { + if !symbol_matches_filter(symbol, diff, filter) { + continue; + } + mapping.insert(diff.symbol_ref, diff); + } + for (section, section_diff) in ctx.obj.sections.iter().zip(&ctx.diff.sections) { + for (symbol, symbol_diff) in section.symbols.iter().zip(§ion_diff.symbols) { + if !symbol_matches_filter(symbol, symbol_diff, filter) { + continue; + } + mapping.insert(symbol_diff.symbol_ref, symbol_diff); + } + } + } + ui.scope(|ui| { ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); - if !obj.0.common.is_empty() { + // Skip sections with all symbols filtered out + if mapping.keys().any(|symbol_ref| symbol_ref.section_idx == usize::MAX) { CollapsingHeader::new(".comm").default_open(true).show(ui, |ui| { - for (symbol, symbol_diff) in obj.0.common.iter().zip(&obj.1.common) { - if !symbol_matches_search(symbol, search_regex) { - continue; - } - ret = ret.or(symbol_ui( + for (symbol_ref, symbol_diff) in mapping + .iter() + .filter(|(symbol_ref, _)| symbol_ref.section_idx == usize::MAX) + { + let symbol = ctx.obj.section_symbol(*symbol_ref).1; + if let Some(result) = symbol_ui( ui, - arch, + ctx, + other_ctx, symbol, symbol_diff, None, state, appearance, - left, - )); + column, + ) { + ret = Some(result); + } } }); } - for (section, section_diff) in obj.0.sections.iter().zip(&obj.1.sections) { + for ((section_index, section), section_diff) in + ctx.obj.sections.iter().enumerate().zip(&ctx.diff.sections) + { + // Skip sections with all symbols filtered out + if !mapping.keys().any(|symbol_ref| symbol_ref.section_idx == section_index) { + continue; + } let mut header = LayoutJob::simple_singleline( format!("{} ({:x})", section.name, section.size), appearance.code_font.clone(), @@ -406,40 +712,45 @@ fn symbol_list_ui( .default_open(true) .show(ui, |ui| { if section.kind == ObjSectionKind::Code && state.reverse_fn_order { - for (symbol, symbol_diff) in - section.symbols.iter().zip(§ion_diff.symbols).rev() + for (symbol, symbol_diff) in mapping + .iter() + .filter(|(symbol_ref, _)| symbol_ref.section_idx == section_index) + .rev() { - if !symbol_matches_search(symbol, search_regex) { - continue; - } - ret = ret.or(symbol_ui( + let symbol = ctx.obj.section_symbol(*symbol).1; + if let Some(result) = symbol_ui( ui, - arch, + ctx, + other_ctx, symbol, symbol_diff, Some(section), state, appearance, - left, - )); + column, + ) { + ret = Some(result); + } } } else { - for (symbol, symbol_diff) in - section.symbols.iter().zip(§ion_diff.symbols) + for (symbol, symbol_diff) in mapping + .iter() + .filter(|(symbol_ref, _)| symbol_ref.section_idx == section_index) { - if !symbol_matches_search(symbol, search_regex) { - continue; - } - ret = ret.or(symbol_ui( + let symbol = ctx.obj.section_symbol(*symbol).1; + if let Some(result) = symbol_ui( ui, - arch, + ctx, + other_ctx, symbol, symbol_diff, Some(section), state, appearance, - left, - )); + column, + ) { + ret = Some(result); + } } } }); @@ -487,155 +798,139 @@ fn missing_obj_ui(ui: &mut Ui, appearance: &Appearance) { }); } -pub fn symbol_diff_ui(ui: &mut Ui, state: &mut DiffViewState, appearance: &Appearance) { - let DiffViewState { build, current_view, symbol_state, search, search_regex, .. } = state; - let Some(result) = build else { - return; +#[derive(Copy, Clone)] +pub struct SymbolDiffContext<'a> { + pub obj: &'a ObjInfo, + pub diff: &'a ObjDiff, +} + +#[must_use] +pub fn symbol_diff_ui( + ui: &mut Ui, + state: &mut DiffViewState, + appearance: &Appearance, +) -> Option { + let mut ret = None; + let Some(result) = &state.build else { + return ret; }; // Header let available_width = ui.available_width(); - let column_width = available_width / 2.0; - ui.allocate_ui_with_layout( - Vec2 { x: available_width, y: 100.0 }, - Layout::left_to_right(Align::Min), - |ui| { - ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate); - + render_header(ui, available_width, 2, |ui, column| { + if column == 0 { // Left column - ui.allocate_ui_with_layout( - Vec2 { x: column_width, y: 100.0 }, - Layout::top_down(Align::Min), - |ui| { - ui.set_width(column_width); - - ui.scope(|ui| { - ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); - - ui.label("Build target:"); - if result.first_status.success { - if result.first_obj.is_none() { - ui.colored_label(appearance.replace_color, "Missing"); - } else { - ui.label("OK"); - } - } else { - ui.colored_label(appearance.delete_color, "Fail"); - } - }); - - if TextEdit::singleline(search).hint_text("Filter symbols").ui(ui).changed() { - if search.is_empty() { - *search_regex = None; - } else if let Ok(regex) = - RegexBuilder::new(search).case_insensitive(true).build() - { - *search_regex = Some(regex); - } else { - *search_regex = None; - } + ui.scope(|ui| { + ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); + + ui.label("Target object"); + if result.first_status.success { + if result.first_obj.is_none() { + ui.colored_label(appearance.replace_color, "Missing"); + } else { + ui.colored_label(appearance.highlight_color, state.object_name.clone()); } - }, - ); + } else { + ui.colored_label(appearance.delete_color, "Fail"); + } + }); + let mut search = state.search.clone(); + if TextEdit::singleline(&mut search).hint_text("Filter symbols").ui(ui).changed() { + ret = Some(DiffViewAction::SetSearch(search)); + } + } else if column == 1 { // Right column - ui.allocate_ui_with_layout( - Vec2 { x: column_width, y: 100.0 }, - Layout::top_down(Align::Min), - |ui| { - ui.set_width(column_width); - - ui.horizontal(|ui| { - ui.scope(|ui| { - ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); - ui.label("Build base:"); - }); - ui.separator(); - if ui - .add_enabled( - state.source_path_available, - egui::Button::new("🖹 Source file"), - ) - .on_hover_text_at_pointer("Open the source file in the default editor") - .on_disabled_hover_text("Source file metadata missing") - .clicked() - { - state.queue_open_source_path = true; - } - }); - - ui.scope(|ui| { - ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); - if result.second_status.success { - if result.second_obj.is_none() { - ui.colored_label(appearance.replace_color, "Missing"); - } else { - ui.label("OK"); - } - } else { - ui.colored_label(appearance.delete_color, "Fail"); - } - }); + ui.horizontal(|ui| { + ui.scope(|ui| { + ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); + ui.label("Base object"); + }); + ui.separator(); + if ui + .add_enabled(state.source_path_available, egui::Button::new("🖹 Source file")) + .on_hover_text_at_pointer("Open the source file in the default editor") + .on_disabled_hover_text("Source file metadata missing") + .clicked() + { + ret = Some(DiffViewAction::OpenSourcePath); + } + }); - if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() { - state.queue_build = true; + ui.scope(|ui| { + ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); + if result.second_status.success { + if result.second_obj.is_none() { + ui.colored_label(appearance.replace_color, "Missing"); + } else { + ui.colored_label(appearance.highlight_color, "OK"); } - }, - ); - }, - ); - ui.separator(); - - // Table - let mut ret = None; - StripBuilder::new(ui).size(Size::remainder()).vertical(|mut strip| { - strip.strip(|builder| { - builder.sizes(Size::remainder(), 2).horizontal(|mut strip| { - strip.cell(|ui| { - ui.push_id("left", |ui| { - if result.first_status.success { - if let Some(obj) = &result.first_obj { - ret = ret.or(symbol_list_ui( - ui, - obj, - symbol_state, - search_regex.as_ref(), - appearance, - true, - )); - } else { - missing_obj_ui(ui, appearance); - } - } else { - build_log_ui(ui, &result.first_status, appearance); - } - }); - }); - strip.cell(|ui| { - ui.push_id("right", |ui| { - if result.second_status.success { - if let Some(obj) = &result.second_obj { - ret = ret.or(symbol_list_ui( - ui, - obj, - symbol_state, - search_regex.as_ref(), - appearance, - false, - )); - } else { - missing_obj_ui(ui, appearance); - } - } else { - build_log_ui(ui, &result.second_status, appearance); - } - }); - }); + } else { + ui.colored_label(appearance.delete_color, "Fail"); + } }); - }); + + if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() { + ret = Some(DiffViewAction::Build); + } + } }); - if let Some(view) = ret { - *current_view = view; - } + // Table + let filter = match &state.search_regex { + Some(regex) => SymbolFilter::Search(regex), + _ => SymbolFilter::None, + }; + render_strips(ui, available_width, 2, |ui, column| { + if column == 0 { + // Left column + if result.first_status.success { + if let Some((obj, diff)) = &result.first_obj { + if let Some(result) = symbol_list_ui( + ui, + SymbolDiffContext { obj, diff }, + result + .second_obj + .as_ref() + .map(|(obj, diff)| SymbolDiffContext { obj, diff }), + &state.symbol_state, + filter, + appearance, + column, + ) { + ret = Some(result); + } + } else { + missing_obj_ui(ui, appearance); + } + } else { + build_log_ui(ui, &result.first_status, appearance); + } + } else if column == 1 { + // Right column + if result.second_status.success { + if let Some((obj, diff)) = &result.second_obj { + if let Some(result) = symbol_list_ui( + ui, + SymbolDiffContext { obj, diff }, + result + .first_obj + .as_ref() + .map(|(obj, diff)| SymbolDiffContext { obj, diff }), + &state.symbol_state, + filter, + appearance, + column, + ) { + ret = Some(result); + } + } else { + missing_obj_ui(ui, appearance); + } + } else { + build_log_ui(ui, &result.second_status, appearance); + } + } + }); + ret }