Add symbol mapping feature (#118)

This allows users to "map" (or "link") symbols with different names so that they can be compared without having to update either the target or base objects. Symbol mappings are persisted in objdiff.json, so generators will need to ensure that they're preserved when updating. (Example: d1334bb79e)

Resolves #117
This commit is contained in:
Luke Street 2024-10-09 21:44:18 -06:00 committed by GitHub
parent 603dbd6882
commit 741d93e211
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 2259 additions and 928 deletions

16
Cargo.lock generated
View File

@ -395,6 +395,15 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bimap"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "230c5f1ca6a325a32553f8640d31ac9b49f2411e901e427570154868b46da4f7"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "bit-set" name = "bit-set"
version = "0.6.0" version = "0.6.0"
@ -2852,7 +2861,7 @@ dependencies = [
[[package]] [[package]]
name = "objdiff-cli" name = "objdiff-cli"
version = "2.2.2" version = "2.3.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"argp", "argp",
@ -2874,10 +2883,11 @@ dependencies = [
[[package]] [[package]]
name = "objdiff-core" name = "objdiff-core"
version = "2.2.2" version = "2.3.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"arm-attr", "arm-attr",
"bimap",
"byteorder", "byteorder",
"console_error_panic_hook", "console_error_panic_hook",
"console_log", "console_log",
@ -2913,7 +2923,7 @@ dependencies = [
[[package]] [[package]]
name = "objdiff-gui" name = "objdiff-gui"
version = "2.2.2" version = "2.3.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",

View File

@ -13,7 +13,7 @@ strip = "debuginfo"
codegen-units = 1 codegen-units = 1
[workspace.package] [workspace.package]
version = "2.2.2" version = "2.3.0"
authors = ["Luke Street <luke@street.dev>"] authors = ["Luke Street <luke@street.dev>"]
edition = "2021" edition = "2021"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"

View File

@ -133,6 +133,13 @@
}, },
"metadata": { "metadata": {
"ref": "#/$defs/metadata" "ref": "#/$defs/metadata"
},
"symbol_mappings": {
"type": "object",
"description": "Manual symbol mappings from target to base.",
"additionalProperties": {
"type": "string"
}
} }
} }
}, },

View File

@ -102,7 +102,12 @@ pub fn run(args: Args) -> Result<()> {
let unit_path = let unit_path =
PathBuf::from_str(u).ok().and_then(|p| fs::canonicalize(p).ok()); PathBuf::from_str(u).ok().and_then(|p| fs::canonicalize(p).ok());
let Some(object) = project_config.objects.iter_mut().find_map(|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) { if obj.name.as_deref() == Some(u) {
resolve_paths(obj); resolve_paths(obj);
return Some(obj); return Some(obj);
@ -121,7 +126,8 @@ pub fn run(args: Args) -> Result<()> {
} }
None None
}) else { })
else {
bail!("Unit not found: {}", u) bail!("Unit not found: {}", u)
}; };
@ -129,7 +135,13 @@ pub fn run(args: Args) -> Result<()> {
} else if let Some(symbol_name) = &args.symbol { } else if let Some(symbol_name) = &args.symbol {
let mut idx = None; let mut idx = None;
let mut count = 0usize; 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); resolve_paths(obj);
if obj if obj
@ -148,7 +160,7 @@ pub fn run(args: Args) -> Result<()> {
} }
match (count, idx) { match (count, idx) {
(0, None) => bail!("Symbol not found: {}", symbol_name), (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!( (2.., Some(_)) => bail!(
"Multiple instances of {} were found, try specifying a unit", "Multiple instances of {} were found, try specifying a unit",
symbol_name symbol_name
@ -303,7 +315,7 @@ fn find_function(obj: &ObjInfo, name: &str) -> Option<SymbolRef> {
None None
} }
#[allow(dead_code)] #[expect(dead_code)]
struct FunctionDiffUi { struct FunctionDiffUi {
relax_reloc_diffs: bool, relax_reloc_diffs: bool,
left_highlight: HighlightKind, left_highlight: HighlightKind,
@ -758,7 +770,7 @@ impl FunctionDiffUi {
self.scroll_y += self.per_page / if half { 2 } else { 1 }; self.scroll_y += self.per_page / if half { 2 } else { 1 };
} }
#[allow(clippy::too_many_arguments)] #[expect(clippy::too_many_arguments)]
fn print_sym( fn print_sym(
&self, &self,
out: &mut Text<'static>, out: &mut Text<'static>,

View File

@ -94,7 +94,7 @@ fn generate(args: GenerateArgs) -> Result<()> {
}; };
info!( info!(
"Generating report for {} units (using {} threads)", "Generating report for {} units (using {} threads)",
project.objects.len(), project.units().len(),
if args.deduplicate { 1 } else { rayon::current_num_threads() } if args.deduplicate { 1 } else { rayon::current_num_threads() }
); );
@ -103,7 +103,7 @@ fn generate(args: GenerateArgs) -> Result<()> {
let mut existing_functions: HashSet<String> = HashSet::new(); let mut existing_functions: HashSet<String> = HashSet::new();
if args.deduplicate { if args.deduplicate {
// If deduplicating, we need to run single-threaded // 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( if let Some(unit) = report_object(
object, object,
project_dir, project_dir,
@ -116,7 +116,9 @@ fn generate(args: GenerateArgs) -> Result<()> {
} }
} else { } else {
let vec = project let vec = project
.objects .units
.as_deref_mut()
.unwrap_or_default()
.par_iter_mut() .par_iter_mut()
.map(|object| { .map(|object| {
report_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 measures = units.iter().flat_map(|u| u.measures.into_iter()).collect();
let mut categories = Vec::new(); let mut categories = Vec::new();
for category in &project.progress_categories { for category in project.progress_categories() {
categories.push(ReportCategory { categories.push(ReportCategory {
id: category.id.clone(), id: category.id.clone(),
name: category.name.clone(), name: category.name.clone(),

View File

@ -17,8 +17,8 @@ crate-type = ["cdylib", "rlib"]
[features] [features]
all = ["config", "dwarf", "mips", "ppc", "x86", "arm", "bindings"] all = ["config", "dwarf", "mips", "ppc", "x86", "arm", "bindings"]
any-arch = [] # Implicit, used to check if any arch is enabled any-arch = ["bimap"] # Implicit, used to check if any arch is enabled
config = ["globset", "semver", "serde_json", "serde_yaml"] config = ["bimap", "globset", "semver", "serde_json", "serde_yaml"]
dwarf = ["gimli"] dwarf = ["gimli"]
mips = ["any-arch", "rabbitizer"] mips = ["any-arch", "rabbitizer"]
ppc = ["any-arch", "cwdemangle", "cwextab", "ppc750cl"] ppc = ["any-arch", "cwdemangle", "cwextab", "ppc750cl"]
@ -32,6 +32,7 @@ features = ["all"]
[dependencies] [dependencies]
anyhow = "1.0" anyhow = "1.0"
bimap = { version = "0.6", features = ["serde"], optional = true }
byteorder = "1.5" byteorder = "1.5"
filetime = "0.2" filetime = "0.2"
flagset = "0.4" flagset = "0.4"

View File

@ -1,77 +1,100 @@
use std::{ use std::{
fs,
fs::File, fs::File,
io::{BufReader, Read}, io::{BufReader, BufWriter, Read},
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use bimap::BiBTreeMap;
use filetime::FileTime; use filetime::FileTime;
use globset::{Glob, GlobSet, GlobSetBuilder}; use globset::{Glob, GlobSet, GlobSetBuilder};
#[inline] #[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
fn bool_true() -> bool { true }
#[derive(Default, Clone, serde::Deserialize)]
pub struct ProjectConfig { pub struct ProjectConfig {
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub min_version: Option<String>, pub min_version: Option<String>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub custom_make: Option<String>, pub custom_make: Option<String>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub custom_args: Option<Vec<String>>, pub custom_args: Option<Vec<String>>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub target_dir: Option<PathBuf>, pub target_dir: Option<PathBuf>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub base_dir: Option<PathBuf>, pub base_dir: Option<PathBuf>,
#[serde(default = "bool_true")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub build_base: bool, pub build_base: Option<bool>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub build_target: bool, pub build_target: Option<bool>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub watch_patterns: Option<Vec<Glob>>, pub watch_patterns: Option<Vec<Glob>>,
#[serde(default, alias = "units")] #[serde(default, alias = "objects", skip_serializing_if = "Option::is_none")]
pub objects: Vec<ProjectObject>, pub units: Option<Vec<ProjectObject>>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub progress_categories: Vec<ProjectProgressCategory>, pub progress_categories: Option<Vec<ProjectProgressCategory>>,
} }
#[derive(Default, Clone, serde::Deserialize)] impl ProjectConfig {
#[inline]
pub fn units(&self) -> &[ProjectObject] { self.units.as_deref().unwrap_or_default() }
#[inline]
pub fn units_mut(&mut self) -> &mut Vec<ProjectObject> {
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<ProjectProgressCategory> {
self.progress_categories.get_or_insert_with(Vec::new)
}
}
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
pub struct ProjectObject { pub struct ProjectObject {
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>, pub name: Option<String>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub path: Option<PathBuf>, pub path: Option<PathBuf>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub target_path: Option<PathBuf>, pub target_path: Option<PathBuf>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub base_path: Option<PathBuf>, pub base_path: Option<PathBuf>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
#[deprecated(note = "Use metadata.reverse_fn_order")] #[deprecated(note = "Use metadata.reverse_fn_order")]
pub reverse_fn_order: Option<bool>, pub reverse_fn_order: Option<bool>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
#[deprecated(note = "Use metadata.complete")] #[deprecated(note = "Use metadata.complete")]
pub complete: Option<bool>, pub complete: Option<bool>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub scratch: Option<ScratchConfig>, pub scratch: Option<ScratchConfig>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<ProjectObjectMetadata>, pub metadata: Option<ProjectObjectMetadata>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub symbol_mappings: Option<SymbolMappings>,
} }
#[derive(Default, Clone, serde::Deserialize)] pub type SymbolMappings = BiBTreeMap<String, String>;
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
pub struct ProjectObjectMetadata { pub struct ProjectObjectMetadata {
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub complete: Option<bool>, pub complete: Option<bool>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub reverse_fn_order: Option<bool>, pub reverse_fn_order: Option<bool>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub source_path: Option<String>, pub source_path: Option<String>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub progress_categories: Option<Vec<String>>, pub progress_categories: Option<Vec<String>>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub auto_generated: Option<bool>, pub auto_generated: Option<bool>,
} }
#[derive(Default, Clone, serde::Deserialize)] #[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
pub struct ProjectProgressCategory { pub struct ProjectProgressCategory {
#[serde(default)] #[serde(default)]
pub id: String, pub id: String,
@ -112,12 +135,12 @@ impl ProjectObject {
} }
pub fn complete(&self) -> Option<bool> { pub fn complete(&self) -> Option<bool> {
#[allow(deprecated)] #[expect(deprecated)]
self.metadata.as_ref().and_then(|m| m.complete).or(self.complete) self.metadata.as_ref().and_then(|m| m.complete).or(self.complete)
} }
pub fn reverse_fn_order(&self) -> Option<bool> { pub fn reverse_fn_order(&self) -> Option<bool> {
#[allow(deprecated)] #[expect(deprecated)]
self.metadata.as_ref().and_then(|m| m.reverse_fn_order).or(self.reverse_fn_order) 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)] #[derive(Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct ScratchConfig { pub struct ScratchConfig {
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub platform: Option<String>, pub platform: Option<String>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub compiler: Option<String>, pub compiler: Option<String>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub c_flags: Option<String>, pub c_flags: Option<String>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub ctx_path: Option<PathBuf>, pub ctx_path: Option<PathBuf>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub build_ctx: bool, pub build_ctx: Option<bool>,
} }
pub const CONFIG_FILENAMES: [&str; 3] = ["objdiff.json", "objdiff.yml", "objdiff.yaml"]; 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)] #[derive(Clone, Eq, PartialEq)]
pub struct ProjectConfigInfo { pub struct ProjectConfigInfo {
pub path: PathBuf, pub path: PathBuf,
pub timestamp: FileTime, pub timestamp: Option<FileTime>,
} }
pub fn try_project_config(dir: &Path) -> Option<(Result<ProjectConfig>, ProjectConfigInfo)> { pub fn try_project_config(dir: &Path) -> Option<(Result<ProjectConfig>, ProjectConfigInfo)> {
@ -180,12 +203,41 @@ pub fn try_project_config(dir: &Path) -> Option<(Result<ProjectConfig>, ProjectC
result = Err(e); result = Err(e);
} }
} }
return Some((result, ProjectConfigInfo { path: config_path, timestamp: ts })); return Some((result, ProjectConfigInfo { path: config_path, timestamp: Some(ts) }));
} }
} }
None None
} }
pub fn save_project_config(
config: &ProjectConfig,
info: &ProjectConfigInfo,
) -> Result<ProjectConfigInfo> {
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<()> { fn validate_min_version(config: &ProjectConfig) -> Result<()> {
let Some(min_version) = &config.min_version else { return Ok(()) }; let Some(min_version) = &config.min_version else { return Ok(()) };
let version = semver::Version::parse(env!("CARGO_PKG_VERSION")) let version = semver::Version::parse(env!("CARGO_PKG_VERSION"))

View File

@ -41,7 +41,7 @@ pub fn no_diff_code(out: &ProcessCodeResult, symbol_ref: SymbolRef) -> Result<Ob
}); });
} }
resolve_branches(&mut diff); resolve_branches(&mut diff);
Ok(ObjSymbolDiff { symbol_ref, diff_symbol: None, instructions: diff, match_percent: None }) Ok(ObjSymbolDiff { symbol_ref, target_symbol: None, instructions: diff, match_percent: None })
} }
pub fn diff_code( pub fn diff_code(
@ -67,7 +67,7 @@ pub fn diff_code(
right.arg_diff = result.right_args_diff; right.arg_diff = result.right_args_diff;
} }
let total = left_out.insts.len(); let total = left_out.insts.len().max(right_out.insts.len());
let percent = if diff_state.diff_count >= total { let percent = if diff_state.diff_count >= total {
0.0 0.0
} else { } else {
@ -77,13 +77,13 @@ pub fn diff_code(
Ok(( Ok((
ObjSymbolDiff { ObjSymbolDiff {
symbol_ref: left_symbol_ref, symbol_ref: left_symbol_ref,
diff_symbol: Some(right_symbol_ref), target_symbol: Some(right_symbol_ref),
instructions: left_diff, instructions: left_diff,
match_percent: Some(percent), match_percent: Some(percent),
}, },
ObjSymbolDiff { ObjSymbolDiff {
symbol_ref: right_symbol_ref, symbol_ref: right_symbol_ref,
diff_symbol: Some(left_symbol_ref), target_symbol: Some(left_symbol_ref),
instructions: right_diff, instructions: right_diff,
match_percent: Some(percent), match_percent: Some(percent),
}, },
@ -211,7 +211,7 @@ fn arg_eq(
left_diff: &ObjInsDiff, left_diff: &ObjInsDiff,
right_diff: &ObjInsDiff, right_diff: &ObjInsDiff,
) -> bool { ) -> bool {
return match left { match left {
ObjInsArg::PlainText(l) => match right { ObjInsArg::PlainText(l) => match right {
ObjInsArg::PlainText(r) => l == r, ObjInsArg::PlainText(r) => l == r,
_ => false, _ => false,
@ -236,7 +236,7 @@ fn arg_eq(
left_diff.branch_to.as_ref().map(|b| b.ins_idx) left_diff.branch_to.as_ref().map(|b| b.ins_idx)
== right_diff.branch_to.as_ref().map(|b| b.ins_idx) == right_diff.branch_to.as_ref().map(|b| b.ins_idx)
} }
}; }
} }
#[derive(Default)] #[derive(Default)]

View File

@ -20,13 +20,13 @@ pub fn diff_bss_symbol(
Ok(( Ok((
ObjSymbolDiff { ObjSymbolDiff {
symbol_ref: left_symbol_ref, symbol_ref: left_symbol_ref,
diff_symbol: Some(right_symbol_ref), target_symbol: Some(right_symbol_ref),
instructions: vec![], instructions: vec![],
match_percent: Some(percent), match_percent: Some(percent),
}, },
ObjSymbolDiff { ObjSymbolDiff {
symbol_ref: right_symbol_ref, symbol_ref: right_symbol_ref,
diff_symbol: Some(left_symbol_ref), target_symbol: Some(left_symbol_ref),
instructions: vec![], instructions: vec![],
match_percent: Some(percent), match_percent: Some(percent),
}, },
@ -34,7 +34,7 @@ pub fn diff_bss_symbol(
} }
pub fn no_diff_symbol(_obj: &ObjInfo, symbol_ref: SymbolRef) -> ObjSymbolDiff { 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. /// Compare the data sections of two object files.
@ -158,13 +158,13 @@ pub fn diff_data_symbol(
Ok(( Ok((
ObjSymbolDiff { ObjSymbolDiff {
symbol_ref: left_symbol_ref, symbol_ref: left_symbol_ref,
diff_symbol: Some(right_symbol_ref), target_symbol: Some(right_symbol_ref),
instructions: vec![], instructions: vec![],
match_percent: Some(match_percent), match_percent: Some(match_percent),
}, },
ObjSymbolDiff { ObjSymbolDiff {
symbol_ref: right_symbol_ref, symbol_ref: right_symbol_ref,
diff_symbol: Some(left_symbol_ref), target_symbol: Some(left_symbol_ref),
instructions: vec![], instructions: vec![],
match_percent: Some(match_percent), match_percent: Some(match_percent),
}, },

View File

@ -29,7 +29,7 @@ pub enum DiffText<'a> {
Eol, Eol,
} }
#[derive(Default, Clone, PartialEq, Eq)] #[derive(Debug, Default, Clone, PartialEq, Eq)]
pub enum HighlightKind { pub enum HighlightKind {
#[default] #[default]
None, None,

View File

@ -3,6 +3,7 @@ use std::collections::HashSet;
use anyhow::Result; use anyhow::Result;
use crate::{ use crate::{
config::SymbolMappings,
diff::{ diff::{
code::{diff_code, no_diff_code, process_code_symbol}, code::{diff_code, no_diff_code, process_code_symbol},
data::{ data::{
@ -161,6 +162,8 @@ pub struct DiffObjConfig {
#[serde(default = "default_true")] #[serde(default = "default_true")]
pub space_between_args: bool, pub space_between_args: bool,
pub combine_data_sections: bool, pub combine_data_sections: bool,
#[serde(default)]
pub symbol_mappings: MappingConfig,
// x86 // x86
pub x86_formatter: X86Formatter, pub x86_formatter: X86Formatter,
// MIPS // MIPS
@ -182,6 +185,7 @@ impl Default for DiffObjConfig {
relax_reloc_diffs: false, relax_reloc_diffs: false,
space_between_args: true, space_between_args: true,
combine_data_sections: false, combine_data_sections: false,
symbol_mappings: Default::default(),
x86_formatter: Default::default(), x86_formatter: Default::default(),
mips_abi: Default::default(), mips_abi: Default::default(),
mips_instr_category: Default::default(), mips_instr_category: Default::default(),
@ -223,8 +227,10 @@ impl ObjSectionDiff {
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct ObjSymbolDiff { pub struct ObjSymbolDiff {
/// The symbol ref this object
pub symbol_ref: SymbolRef, pub symbol_ref: SymbolRef,
pub diff_symbol: Option<SymbolRef>, /// The symbol ref in the _other_ object that this symbol was diffed against
pub target_symbol: Option<SymbolRef>,
pub instructions: Vec<ObjInsDiff>, pub instructions: Vec<ObjInsDiff>,
pub match_percent: Option<f32>, pub match_percent: Option<f32>,
} }
@ -294,8 +300,13 @@ pub struct ObjInsBranchTo {
#[derive(Default)] #[derive(Default)]
pub struct ObjDiff { pub struct ObjDiff {
/// A list of all section diffs in the object.
pub sections: Vec<ObjSectionDiff>, pub sections: Vec<ObjSectionDiff>,
/// Common BSS symbols don't live in a section, so they're stored separately.
pub common: Vec<ObjSymbolDiff>, pub common: Vec<ObjSymbolDiff>,
/// 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<ObjSymbolDiff>,
} }
impl ObjDiff { impl ObjDiff {
@ -303,13 +314,14 @@ impl ObjDiff {
let mut result = Self { let mut result = Self {
sections: Vec::with_capacity(obj.sections.len()), sections: Vec::with_capacity(obj.sections.len()),
common: Vec::with_capacity(obj.common.len()), common: Vec::with_capacity(obj.common.len()),
mapping_symbols: vec![],
}; };
for (section_idx, section) in obj.sections.iter().enumerate() { for (section_idx, section) in obj.sections.iter().enumerate() {
let mut symbols = Vec::with_capacity(section.symbols.len()); let mut symbols = Vec::with_capacity(section.symbols.len());
for (symbol_idx, _) in section.symbols.iter().enumerate() { for (symbol_idx, _) in section.symbols.iter().enumerate() {
symbols.push(ObjSymbolDiff { symbols.push(ObjSymbolDiff {
symbol_ref: SymbolRef { section_idx, symbol_idx }, symbol_ref: SymbolRef { section_idx, symbol_idx },
diff_symbol: None, target_symbol: None,
instructions: vec![], instructions: vec![],
match_percent: None, match_percent: None,
}); });
@ -328,7 +340,7 @@ impl ObjDiff {
for (symbol_idx, _) in obj.common.iter().enumerate() { for (symbol_idx, _) in obj.common.iter().enumerate() {
result.common.push(ObjSymbolDiff { result.common.push(ObjSymbolDiff {
symbol_ref: SymbolRef { section_idx: obj.sections.len(), symbol_idx }, symbol_ref: SymbolRef { section_idx: obj.sections.len(), symbol_idx },
diff_symbol: None, target_symbol: None,
instructions: vec![], instructions: vec![],
match_percent: None, match_percent: None,
}); });
@ -378,7 +390,7 @@ pub fn diff_objs(
right: Option<&ObjInfo>, right: Option<&ObjInfo>,
prev: Option<&ObjInfo>, prev: Option<&ObjInfo>,
) -> Result<DiffObjsResult> { ) -> Result<DiffObjsResult> {
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 section_matches = matching_sections(left, right)?;
let mut left = left.map(|p| (p, ObjDiff::new_from_obj(p))); let mut left = left.map(|p| (p, ObjDiff::new_from_obj(p)));
let mut right = right.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 { Ok(DiffObjsResult {
left: left.map(|(_, o)| o), left: left.map(|(_, o)| o),
right: right.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)] #[derive(Copy, Clone, Eq, PartialEq)]
struct SymbolMatch { struct SymbolMatch {
left: Option<SymbolRef>, left: Option<SymbolRef>,
@ -551,19 +631,115 @@ struct SectionMatch {
section_kind: ObjSectionKind, 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<String>,
/// The left object symbol name that we're selecting a right symbol for
pub selecting_right: Option<String>,
}
fn symbol_ref_by_name(obj: &ObjInfo, name: &str) -> Option<SymbolRef> {
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<SymbolRef>,
right_used: &mut HashSet<SymbolRef>,
matches: &mut Vec<SymbolMatch>,
) -> 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. /// Find matching symbols between each object.
fn matching_symbols( fn matching_symbols(
left: Option<&ObjInfo>, left: Option<&ObjInfo>,
right: Option<&ObjInfo>, right: Option<&ObjInfo>,
prev: Option<&ObjInfo>, prev: Option<&ObjInfo>,
mappings: &MappingConfig,
) -> Result<Vec<SymbolMatch>> { ) -> Result<Vec<SymbolMatch>> {
let mut matches = Vec::new(); let mut matches = Vec::new();
let mut left_used = HashSet::new();
let mut right_used = HashSet::new(); let mut right_used = HashSet::new();
if let Some(left) = left { 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 (section_idx, section) in left.sections.iter().enumerate() {
for (symbol_idx, symbol) in section.symbols.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 { let symbol_match = SymbolMatch {
left: Some(SymbolRef { section_idx, symbol_idx }), left: Some(symbol_ref),
right: find_symbol(right, symbol, section, Some(&right_used)), right: find_symbol(right, symbol, section, Some(&right_used)),
prev: find_symbol(prev, symbol, section, None), prev: find_symbol(prev, symbol, section, None),
section_kind: section.kind, section_kind: section.kind,
@ -575,8 +751,12 @@ fn matching_symbols(
} }
} }
for (symbol_idx, symbol) in left.common.iter().enumerate() { 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 { let symbol_match = SymbolMatch {
left: Some(SymbolRef { section_idx: left.sections.len(), symbol_idx }), left: Some(symbol_ref),
right: find_common_symbol(right, symbol), right: find_common_symbol(right, symbol),
prev: find_common_symbol(prev, symbol), prev: find_common_symbol(prev, symbol),
section_kind: ObjSectionKind::Bss, section_kind: ObjSectionKind::Bss,

View File

@ -112,6 +112,15 @@ pub struct ObjIns {
pub orig: Option<String>, pub orig: Option<String>,
} }
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
pub enum ObjSymbolKind {
#[default]
Unknown,
Function,
Object,
Section,
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ObjSymbol { pub struct ObjSymbol {
pub name: String, pub name: String,
@ -120,6 +129,7 @@ pub struct ObjSymbol {
pub section_address: u64, pub section_address: u64,
pub size: u64, pub size: u64,
pub size_known: bool, pub size_known: bool,
pub kind: ObjSymbolKind,
pub flags: ObjSymbolFlagSet, pub flags: ObjSymbolFlagSet,
pub addend: i64, pub addend: i64,
/// Original virtual address (from .note.split section) /// Original virtual address (from .note.split section)

View File

@ -23,6 +23,7 @@ use crate::{
obj::{ obj::{
split_meta::{SplitMeta, SPLITMETA_SECTION}, split_meta::{SplitMeta, SPLITMETA_SECTION},
ObjInfo, ObjReloc, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlagSet, ObjSymbolFlags, ObjInfo, ObjReloc, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlagSet, ObjSymbolFlags,
ObjSymbolKind,
}, },
util::{read_u16, read_u32}, util::{read_u16, read_u32},
}; };
@ -94,6 +95,13 @@ fn to_obj_symbol(
}) })
.unwrap_or(&[]); .unwrap_or(&[]);
let kind = match symbol.kind() {
SymbolKind::Text => ObjSymbolKind::Function,
SymbolKind::Data => ObjSymbolKind::Object,
SymbolKind::Section => ObjSymbolKind::Section,
_ => ObjSymbolKind::Unknown,
};
Ok(ObjSymbol { Ok(ObjSymbol {
name: name.to_string(), name: name.to_string(),
demangled_name, demangled_name,
@ -101,6 +109,7 @@ fn to_obj_symbol(
section_address, section_address,
size: symbol.size(), size: symbol.size(),
size_known: symbol.size() != 0, size_known: symbol.size() != 0,
kind,
flags, flags,
addend, addend,
virtual_address, 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))); result.sort_by(|a, b| a.address.cmp(&b.address).then(a.size.cmp(&b.size)));
let mut iter = result.iter_mut().peekable(); let mut iter = result.iter_mut().peekable();
while let Some(symbol) = iter.next() { 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() { if let Some(next_symbol) = iter.peek() {
symbol.size = next_symbol.address - symbol.address; symbol.size = next_symbol.address - symbol.address;
} else { } else {
symbol.size = (section.address + section.size) - symbol.address; 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() { if result.is_empty() {
@ -196,6 +212,10 @@ fn symbols_by_section(
section_address: 0, section_address: 0,
size: section.size, size: section.size,
size_known: true, size_known: true,
kind: match section.kind {
ObjSectionKind::Code => ObjSymbolKind::Function,
ObjSectionKind::Data | ObjSectionKind::Bss => ObjSymbolKind::Object,
},
flags: Default::default(), flags: Default::default(),
addend: 0, addend: 0,
virtual_address: None, virtual_address: None,
@ -281,6 +301,7 @@ fn find_section_symbol(
section_address: 0, section_address: 0,
size: 0, size: 0,
size_known: false, size_known: false,
kind: ObjSymbolKind::Section,
flags: Default::default(), flags: Default::default(),
addend: address as i64 - section.address() as i64, addend: address as i64 - section.address() as i64,
virtual_address: None, virtual_address: None,
@ -568,6 +589,7 @@ fn update_combined_symbol(symbol: ObjSymbol, address_change: i64) -> Result<ObjS
section_address: (symbol.section_address as i64 + address_change).try_into()?, section_address: (symbol.section_address as i64 + address_change).try_into()?,
size: symbol.size, size: symbol.size,
size_known: symbol.size_known, size_known: symbol.size_known,
kind: symbol.kind,
flags: symbol.flags, flags: symbol.flags,
addend: symbol.addend, addend: symbol.addend,
virtual_address: if let Some(virtual_address) = symbol.virtual_address { virtual_address: if let Some(virtual_address) = symbol.virtual_address {

View File

@ -15,7 +15,8 @@ use globset::{Glob, GlobSet};
use notify::{RecursiveMode, Watcher}; use notify::{RecursiveMode, Watcher};
use objdiff_core::{ use objdiff_core::{
config::{ config::{
build_globset, ProjectConfigInfo, ProjectObject, ScratchConfig, DEFAULT_WATCH_PATTERNS, build_globset, save_project_config, ProjectConfig, ProjectConfigInfo, ProjectObject,
ScratchConfig, SymbolMappings, DEFAULT_WATCH_PATTERNS,
}, },
diff::DiffObjConfig, diff::DiffObjConfig,
}; };
@ -42,7 +43,7 @@ use crate::{
graphics::{graphics_window, GraphicsConfig, GraphicsViewState}, graphics::{graphics_window, GraphicsConfig, GraphicsViewState},
jobs::{jobs_menu_ui, jobs_window}, jobs::{jobs_menu_ui, jobs_window},
rlwinm::{rlwinm_decode_window, RlwinmDecodeViewState}, rlwinm::{rlwinm_decode_window, RlwinmDecodeViewState},
symbol_diff::{symbol_diff_ui, DiffViewState, View}, symbol_diff::{symbol_diff_ui, DiffViewAction, DiffViewNavigation, DiffViewState, View},
}, },
}; };
@ -89,7 +90,7 @@ impl Default for ViewState {
} }
/// The configuration for a single object file. /// The configuration for a single object file.
#[derive(Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)] #[derive(Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct ObjectConfig { pub struct ObjectConfig {
pub name: String, pub name: String,
pub target_path: Option<PathBuf>, pub target_path: Option<PathBuf>,
@ -98,6 +99,23 @@ pub struct ObjectConfig {
pub complete: Option<bool>, pub complete: Option<bool>,
pub scratch: Option<ScratchConfig>, pub scratch: Option<ScratchConfig>,
pub source_path: Option<String>, pub source_path: Option<String>,
#[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] #[inline]
@ -117,8 +135,14 @@ pub struct AppState {
pub obj_change: bool, pub obj_change: bool,
pub queue_build: bool, pub queue_build: bool,
pub queue_reload: bool, pub queue_reload: bool,
pub current_project_config: Option<ProjectConfig>,
pub project_config_info: Option<ProjectConfigInfo>, pub project_config_info: Option<ProjectConfigInfo>,
pub last_mod_check: Instant, pub last_mod_check: Instant,
/// The right object symbol name that we're selecting a left symbol for
pub selecting_left: Option<String>,
/// The left object symbol name that we're selecting a right symbol for
pub selecting_right: Option<String>,
pub config_error: Option<String>,
} }
impl Default for AppState { impl Default for AppState {
@ -132,8 +156,12 @@ impl Default for AppState {
obj_change: false, obj_change: false,
queue_build: false, queue_build: false,
queue_reload: false, queue_reload: false,
current_project_config: None,
project_config_info: None, project_config_info: None,
last_mod_check: Instant::now(), 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.config_change = true;
self.obj_change = true; self.obj_change = true;
self.queue_build = false; self.queue_build = false;
self.current_project_config = None;
self.project_config_info = None; self.project_config_info = None;
self.selecting_left = None;
self.selecting_right = None;
} }
pub fn set_target_obj_dir(&mut self, path: PathBuf) { pub fn set_target_obj_dir(&mut self, path: PathBuf) {
@ -222,6 +253,8 @@ impl AppState {
self.config.selected_obj = None; self.config.selected_obj = None;
self.obj_change = true; self.obj_change = true;
self.queue_build = false; self.queue_build = false;
self.selecting_left = None;
self.selecting_right = None;
} }
pub fn set_base_obj_dir(&mut self, path: PathBuf) { pub fn set_base_obj_dir(&mut self, path: PathBuf) {
@ -229,12 +262,122 @@ impl AppState {
self.config.selected_obj = None; self.config.selected_obj = None;
self.obj_change = true; self.obj_change = true;
self.queue_build = false; self.queue_build = false;
self.selecting_left = None;
self.selecting_right = None;
} }
pub fn set_selected_obj(&mut self, object: ObjectConfig) { pub fn set_selected_obj(&mut self, config: ObjectConfig) {
self.config.selected_obj = Some(object); 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.obj_change = true;
self.queue_build = false; 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()); debug_assert!(jobs.results.is_empty());
} }
fn post_update(&mut self, ctx: &egui::Context) { fn post_update(&mut self, ctx: &egui::Context, action: Option<DiffViewAction>) {
self.appearance.post_update(ctx); self.appearance.post_update(ctx);
let ViewState { jobs, diff_state, config_state, graphics_state, .. } = &mut self.view_state; let ViewState { jobs, diff_state, config_state, graphics_state, .. } = &mut self.view_state;
config_state.post_update(ctx, jobs, &self.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 { let Ok(mut state) = self.state.write() else {
return; return;
}; };
let state = &mut *state; let state = &mut *state;
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(info) = &state.project_config_info {
if file_modified(&info.path, info.timestamp) { if let Some(last_ts) = info.timestamp {
if file_modified(&info.path, last_ts) {
state.config_change = true; state.config_change = true;
} }
} }
}
}
if state.config_change { if state.config_change {
state.config_change = false; state.config_change = false;
match load_project_config(state) { match load_project_config(state) {
Ok(()) => config_state.load_error = None, Ok(()) => state.config_error = None,
Err(e) => { Err(e) => {
log::error!("Failed to load project config: {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 let Some(result) = &diff_state.build {
if state.last_mod_check.elapsed().as_millis() >= 500 { if mod_check {
state.last_mod_check = Instant::now();
if let Some((obj, _)) = &result.first_obj { if let Some((obj, _)) = &result.first_obj {
if let (Some(path), Some(timestamp)) = (&obj.path, obj.timestamp) { if let (Some(path), Some(timestamp)) = (&obj.path, obj.timestamp) {
if file_modified(path, timestamp) { if file_modified(path, timestamp) {
@ -457,11 +609,11 @@ impl App {
&& state.config.selected_obj.is_some() && state.config.selected_obj.is_some()
&& !jobs.is_running(Job::ObjDiff) && !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_build = false;
state.queue_reload = false; state.queue_reload = false;
} else if state.queue_reload && !jobs.is_running(Job::ObjDiff) { } 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 // Don't build, just reload the current files
diff_config.build_base = false; diff_config.build_base = false;
diff_config.build_target = false; diff_config.build_target = false;
@ -636,6 +788,11 @@ impl eframe::App for App {
{ {
state.queue_reload = true; 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(); ui.separator();
if jobs_menu_ui(ui, jobs, appearance) { 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| { egui::CentralPanel::default().show(ctx, |ui| {
let build_success = matches!(&diff_state.build, Some(b) if b.first_status.success && b.second_status.success); 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 { action = if diff_state.current_view == View::FunctionDiff && build_success {
function_diff_ui(ui, diff_state, appearance); function_diff_ui(ui, diff_state, appearance)
} else if diff_state.current_view == View::DataDiff && build_success { } 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 { } 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 { } 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); project_window(ctx, state, show_project_config, config_state, appearance);
@ -674,7 +832,7 @@ impl eframe::App for App {
graphics_window(ctx, show_graphics, frame_history, graphics_state, appearance); graphics_window(ctx, show_graphics, frame_history, graphics_state, appearance);
jobs_window(ctx, show_jobs, jobs, appearance); jobs_window(ctx, show_jobs, jobs, appearance);
self.post_update(ctx); self.post_update(ctx, action);
} }
/// Called by the framework to save state before shutdown. /// Called by the framework to save state before shutdown.

View File

@ -2,6 +2,10 @@ use std::path::PathBuf;
use eframe::Storage; use eframe::Storage;
use globset::Glob; use globset::Glob;
use objdiff_core::{
config::ScratchConfig,
diff::{ArmArchVersion, ArmR9Usage, DiffObjConfig, MipsAbi, MipsInstrCategory, X86Formatter},
};
use crate::app::{AppConfig, ObjectConfig, CONFIG_KEY}; use crate::app::{AppConfig, ObjectConfig, CONFIG_KEY};
@ -11,7 +15,7 @@ pub struct AppConfigVersion {
} }
impl Default for 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. /// Deserialize the AppConfig from storage, handling upgrades from older versions.
@ -19,7 +23,8 @@ pub fn deserialize_config(storage: &dyn Storage) -> Option<AppConfig> {
let str = storage.get_string(CONFIG_KEY)?; let str = storage.get_string(CONFIG_KEY)?;
match ron::from_str::<AppConfigVersion>(&str) { match ron::from_str::<AppConfigVersion>(&str) {
Ok(version) => match version.version { Ok(version) => match version.version {
1 => from_str::<AppConfig>(&str), 2 => from_str::<AppConfig>(&str),
1 => from_str::<AppConfigV1>(&str).map(|c| c.into_config()),
_ => { _ => {
log::warn!("Unknown config version: {}", version.version); log::warn!("Unknown config version: {}", version.version);
None None
@ -44,6 +49,180 @@ where T: serde::de::DeserializeOwned {
} }
} }
#[derive(serde::Deserialize, serde::Serialize)]
pub struct ScratchConfigV1 {
#[serde(default)]
pub platform: Option<String>,
#[serde(default)]
pub compiler: Option<String>,
#[serde(default)]
pub c_flags: Option<String>,
#[serde(default)]
pub ctx_path: Option<PathBuf>,
#[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<PathBuf>,
pub base_path: Option<PathBuf>,
pub reverse_fn_order: Option<bool>,
pub complete: Option<bool>,
pub scratch: Option<ScratchConfigV1>,
pub source_path: Option<String>,
}
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<String>,
#[serde(default)]
pub custom_args: Option<Vec<String>>,
#[serde(default)]
pub selected_wsl_distro: Option<String>,
#[serde(default)]
pub project_dir: Option<PathBuf>,
#[serde(default)]
pub target_obj_dir: Option<PathBuf>,
#[serde(default)]
pub base_obj_dir: Option<PathBuf>,
#[serde(default)]
pub selected_obj: Option<ObjectConfigV1>,
#[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<Glob>,
#[serde(default)]
pub recent_projects: Vec<PathBuf>,
#[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)] #[derive(serde::Deserialize, serde::Serialize)]
pub struct ObjectConfigV0 { pub struct ObjectConfigV0 {
pub name: String, pub name: String,
@ -59,9 +238,7 @@ impl ObjectConfigV0 {
target_path: Some(self.target_path), target_path: Some(self.target_path),
base_path: Some(self.base_path), base_path: Some(self.base_path),
reverse_fn_order: self.reverse_fn_order, reverse_fn_order: self.reverse_fn_order,
complete: None, ..Default::default()
scratch: None,
source_path: None,
} }
} }
} }

View File

@ -4,11 +4,11 @@ use anyhow::Result;
use globset::Glob; use globset::Glob;
use objdiff_core::config::{try_project_config, ProjectObject, DEFAULT_WATCH_PATTERNS}; use objdiff_core::config::{try_project_config, ProjectObject, DEFAULT_WATCH_PATTERNS};
use crate::app::AppState; use crate::app::{AppState, ObjectConfig};
#[derive(Clone)] #[derive(Clone)]
pub enum ProjectObjectNode { pub enum ProjectObjectNode {
File(String, Box<ProjectObject>), Unit(String, usize),
Dir(String, Vec<ProjectObjectNode>), Dir(String, Vec<ProjectObjectNode>),
} }
@ -33,17 +33,18 @@ fn find_dir<'a>(
} }
fn build_nodes( fn build_nodes(
objects: &[ProjectObject], units: &mut [ProjectObject],
project_dir: &Path, project_dir: &Path,
target_obj_dir: Option<&Path>, target_obj_dir: Option<&Path>,
base_obj_dir: Option<&Path>, base_obj_dir: Option<&Path>,
) -> Vec<ProjectObjectNode> { ) -> Vec<ProjectObjectNode> {
let mut nodes = 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 mut out_nodes = &mut nodes;
let path = if let Some(name) = &object.name { let path = if let Some(name) = &unit.name {
Path::new(name) Path::new(name)
} else if let Some(path) = &object.path { } else if let Some(path) = &unit.path {
path path
} else { } else {
continue; 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(); 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 nodes
} }
@ -70,24 +69,36 @@ pub fn load_project_config(state: &mut AppState) -> Result<()> {
}; };
if let Some((result, info)) = try_project_config(project_dir) { if let Some((result, info)) = try_project_config(project_dir) {
let project_config = result?; let project_config = result?;
state.config.custom_make = project_config.custom_make; state.config.custom_make = project_config.custom_make.clone();
state.config.custom_args = project_config.custom_args; state.config.custom_args = project_config.custom_args.clone();
state.config.target_obj_dir = project_config.target_dir.map(|p| project_dir.join(p)); state.config.target_obj_dir =
state.config.base_obj_dir = project_config.base_dir.map(|p| project_dir.join(p)); project_config.target_dir.as_deref().map(|p| project_dir.join(p));
state.config.build_base = project_config.build_base; state.config.base_obj_dir = project_config.base_dir.as_deref().map(|p| project_dir.join(p));
state.config.build_target = project_config.build_target; state.config.build_base = project_config.build_base.unwrap_or(true);
state.config.watch_patterns = project_config.watch_patterns.unwrap_or_else(|| { 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() DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
}); });
state.watcher_change = true; state.watcher_change = true;
state.objects = project_config.objects; state.objects = project_config.units.clone().unwrap_or_default();
state.object_nodes = build_nodes( state.object_nodes = build_nodes(
&state.objects, &mut state.objects,
project_dir, project_dir,
state.config.target_obj_dir.as_deref(), state.config.target_obj_dir.as_deref(),
state.config.base_obj_dir.as_deref(), state.config.base_obj_dir.as_deref(),
); );
state.current_project_config = Some(project_config);
state.project_config_info = Some(info); 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(()) Ok(())
} }

View File

@ -39,7 +39,7 @@ impl CreateScratchConfig {
Ok(Self { Ok(Self {
build_config: BuildConfig::from_config(config), build_config: BuildConfig::from_config(config),
context_path: scratch_config.ctx_path.clone(), 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(), compiler: scratch_config.compiler.clone().unwrap_or_default(),
platform: scratch_config.platform.clone().unwrap_or_default(), platform: scratch_config.platform.clone().unwrap_or_default(),
compiler_flags: scratch_config.c_flags.clone().unwrap_or_default(), compiler_flags: scratch_config.c_flags.clone().unwrap_or_default(),

View File

@ -53,7 +53,7 @@ impl JobQueue {
} }
/// Returns whether any job is running. /// Returns whether any job is running.
#[allow(dead_code)] #[expect(dead_code)]
pub fn any_running(&self) -> bool { pub fn any_running(&self) -> bool {
self.jobs.iter().any(|job| { self.jobs.iter().any(|job| {
if let Some(handle) = &job.handle { if let Some(handle) = &job.handle {

View File

@ -6,13 +6,13 @@ use std::{
use anyhow::{anyhow, Error, Result}; use anyhow::{anyhow, Error, Result};
use objdiff_core::{ use objdiff_core::{
diff::{diff_objs, DiffObjConfig, ObjDiff}, diff::{diff_objs, DiffObjConfig, MappingConfig, ObjDiff},
obj::{read, ObjInfo}, obj::{read, ObjInfo},
}; };
use time::OffsetDateTime; use time::OffsetDateTime;
use crate::{ use crate::{
app::{AppConfig, ObjectConfig}, app::{AppConfig, AppState, ObjectConfig},
jobs::{start_job, update_status, Job, JobContext, JobResult, JobState}, jobs::{start_job, update_status, Job, JobContext, JobResult, JobState},
}; };
@ -60,16 +60,20 @@ pub struct ObjDiffConfig {
pub build_target: bool, pub build_target: bool,
pub selected_obj: Option<ObjectConfig>, pub selected_obj: Option<ObjectConfig>,
pub diff_obj_config: DiffObjConfig, pub diff_obj_config: DiffObjConfig,
pub selecting_left: Option<String>,
pub selecting_right: Option<String>,
} }
impl ObjDiffConfig { impl ObjDiffConfig {
pub(crate) fn from_config(config: &AppConfig) -> Self { pub(crate) fn from_state(state: &AppState) -> Self {
Self { Self {
build_config: BuildConfig::from_config(config), build_config: BuildConfig::from_config(&state.config),
build_base: config.build_base, build_base: state.config.build_base,
build_target: config.build_target, build_target: state.config.build_target,
selected_obj: config.selected_obj.clone(), selected_obj: state.config.selected_obj.clone(),
diff_obj_config: config.diff_obj_config.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( fn run_build(
context: &JobContext, context: &JobContext,
cancel: Receiver<()>, cancel: Receiver<()>,
config: ObjDiffConfig, mut config: ObjDiffConfig,
) -> Result<Box<ObjDiffResult>> { ) -> Result<Box<ObjDiffResult>> {
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 let project_dir = config
.build_config .build_config
.project_dir .project_dir

View File

@ -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);
});
});
}
},
);
});
});
}

View File

@ -43,7 +43,6 @@ pub struct ConfigViewState {
pub build_running: bool, pub build_running: bool,
pub queue_build: bool, pub queue_build: bool,
pub watch_pattern_text: String, pub watch_pattern_text: String,
pub load_error: Option<String>,
pub object_search: String, pub object_search: String,
pub filter_diffable: bool, pub filter_diffable: bool,
pub filter_incomplete: bool, pub filter_incomplete: bool,
@ -93,10 +92,7 @@ impl ConfigViewState {
name: obj_path.display().to_string(), name: obj_path.display().to_string(),
target_path: Some(target_path), target_path: Some(target_path),
base_path: Some(path), base_path: Some(path),
reverse_fn_order: None, ..Default::default()
complete: None,
scratch: None,
source_path: None,
}); });
} else if let Ok(obj_path) = path.strip_prefix(target_dir) { } else if let Ok(obj_path) = path.strip_prefix(target_dir) {
let base_path = base_dir.join(obj_path); let base_path = base_dir.join(obj_path);
@ -104,10 +100,7 @@ impl ConfigViewState {
name: obj_path.display().to_string(), name: obj_path.display().to_string(),
target_path: Some(path), target_path: Some(path),
base_path: Some(base_path), base_path: Some(base_path),
reverse_fn_order: None, ..Default::default()
complete: None,
scratch: None,
source_path: None,
}); });
} }
} }
@ -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 objects.is_empty() {
if let (Some(_base_dir), Some(target_dir)) = (base_obj_dir, target_obj_dir) { if let (Some(_base_dir), Some(target_dir)) = (base_obj_dir, target_obj_dir) {
if ui.button("Select object").clicked() { if ui.button("Select object").clicked() {
@ -316,6 +312,7 @@ pub fn config_ui(
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
for node in object_nodes.iter().filter_map(|node| { for node in object_nodes.iter().filter_map(|node| {
filter_node( filter_node(
objects,
node, node,
&search, &search,
config_state.filter_diffable, config_state.filter_diffable,
@ -325,8 +322,9 @@ pub fn config_ui(
}) { }) {
display_node( display_node(
ui, ui,
&mut new_selected_obj, &mut new_selected_index,
project_dir.as_deref(), project_dir.as_deref(),
objects,
&node, &node,
appearance, appearance,
node_open, node_open,
@ -334,10 +332,11 @@ pub fn config_ui(
} }
}); });
} }
if new_selected_obj != *selected_obj { if new_selected_index != selected_index {
if let Some(obj) = new_selected_obj { if let Some(idx) = new_selected_index {
// Will set obj_changed, which will trigger a rebuild // 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() 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, ui: &mut egui::Ui,
selected_obj: &mut Option<ObjectConfig>, selected_obj: &mut Option<usize>,
project_dir: Option<&Path>, project_dir: Option<&Path>,
name: &str, name: &str,
object: &ProjectObject, units: &[ProjectObject],
index: usize,
appearance: &Appearance, appearance: &Appearance,
) { ) {
let object_name = object.name(); let object = &units[index];
let selected = matches!(selected_obj, Some(obj) if obj.name == object_name); let selected = *selected_obj == Some(index);
let color = if selected { let color = if selected {
appearance.emphasized_text_color appearance.emphasized_text_color
} else if let Some(complete) = object.complete() { } else if let Some(complete) = object.complete() {
@ -381,18 +381,8 @@ fn display_object(
if get_source_path(project_dir, object).is_some() { if get_source_path(project_dir, object).is_some() {
response.context_menu(|ui| object_context_ui(ui, object, project_dir)); response.context_menu(|ui| object_context_ui(ui, object, project_dir));
} }
// Always recreate ObjectConfig if selected, in case the project config changed. if response.clicked() {
// ObjectConfig is compared using equality, so this won't unnecessarily trigger a rebuild. *selected_obj = Some(index);
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(),
});
} }
} }
@ -427,18 +417,19 @@ enum NodeOpen {
fn display_node( fn display_node(
ui: &mut egui::Ui, ui: &mut egui::Ui,
selected_obj: &mut Option<ObjectConfig>, selected_obj: &mut Option<usize>,
project_dir: Option<&Path>, project_dir: Option<&Path>,
units: &[ProjectObject],
node: &ProjectObjectNode, node: &ProjectObjectNode,
appearance: &Appearance, appearance: &Appearance,
node_open: NodeOpen, node_open: NodeOpen,
) { ) {
match node { match node {
ProjectObjectNode::File(name, object) => { ProjectObjectNode::Unit(name, idx) => {
display_object(ui, selected_obj, project_dir, name, object, appearance); display_unit(ui, selected_obj, project_dir, name, units, *idx, appearance);
} }
ProjectObjectNode::Dir(name, children) => { 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 { let open = match node_open {
NodeOpen::Default => None, NodeOpen::Default => None,
NodeOpen::Open => Some(true), NodeOpen::Open => Some(true),
@ -461,16 +452,16 @@ fn display_node(
.open(open) .open(open)
.show(ui, |ui| { .show(ui, |ui| {
for node in children { 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 { match node {
ProjectObjectNode::File(_, object) => object.name() == selected_obj.name, ProjectObjectNode::Unit(_, idx) => *idx == selected_obj,
ProjectObjectNode::Dir(_, children) => { ProjectObjectNode::Dir(_, children) => {
children.iter().any(|node| contains_node(node, selected_obj)) 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( fn filter_node(
units: &[ProjectObject],
node: &ProjectObjectNode, node: &ProjectObjectNode,
search: &str, search: &str,
filter_diffable: bool, filter_diffable: bool,
@ -485,12 +477,12 @@ fn filter_node(
show_hidden: bool, show_hidden: bool,
) -> Option<ProjectObjectNode> { ) -> Option<ProjectObjectNode> {
match node { match node {
ProjectObjectNode::File(name, object) => { ProjectObjectNode::Unit(name, idx) => {
let unit = &units[*idx];
if (search.is_empty() || name.to_ascii_lowercase().contains(search)) if (search.is_empty() || name.to_ascii_lowercase().contains(search))
&& (!filter_diffable && (!filter_diffable || (unit.base_path.is_some() && unit.target_path.is_some()))
|| (object.base_path.is_some() && object.target_path.is_some())) && (!filter_incomplete || matches!(unit.complete(), None | Some(false)))
&& (!filter_incomplete || matches!(object.complete(), None | Some(false))) && (show_hidden || !unit.hidden())
&& (show_hidden || !object.hidden())
{ {
Some(node.clone()) Some(node.clone())
} else { } else {
@ -501,7 +493,14 @@ fn filter_node(
let new_children = children let new_children = children
.iter() .iter()
.filter_map(|child| { .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::<Vec<_>>(); .collect::<Vec<_>>();
if !new_children.is_empty() { if !new_children.is_empty() {
@ -570,14 +569,14 @@ pub fn project_window(
split_obj_config_ui(ui, &mut state_guard, config_state, appearance); 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; let mut open = true;
egui::Window::new("Error").open(&mut open).show(ctx, |ui| { egui::Window::new("Error").open(&mut open).show(ctx, |ui| {
ui.label("Failed to load project config:"); ui.label("Failed to load project config:");
ui.colored_label(appearance.delete_color, error); ui.colored_label(appearance.delete_color, error);
}); });
if !open { if !open {
config_state.load_error = None; state_guard.config_error = None;
} }
} }
} }

View File

@ -1,7 +1,6 @@
use std::{cmp::min, default::Default, mem::take}; use std::{cmp::min, default::Default, mem::take};
use egui::{text::LayoutJob, Align, Label, Layout, Sense, Vec2, Widget}; use egui::{text::LayoutJob, Id, Label, RichText, Sense, Widget};
use egui_extras::{Column, TableBuilder};
use objdiff_core::{ use objdiff_core::{
diff::{ObjDataDiff, ObjDataDiffKind, ObjDiff}, diff::{ObjDataDiff, ObjDataDiffKind, ObjDiff},
obj::ObjInfo, obj::ObjInfo,
@ -10,14 +9,15 @@ use time::format_description;
use crate::views::{ use crate::views::{
appearance::Appearance, appearance::Appearance,
symbol_diff::{DiffViewState, SymbolRefByName, View}, column_layout::{render_header, render_table},
symbol_diff::{DiffViewAction, DiffViewNavigation, DiffViewState},
write_text, write_text,
}; };
const BYTES_PER_ROW: usize = 16; const BYTES_PER_ROW: usize = 16;
fn find_section(obj: &ObjInfo, selected_symbol: &SymbolRefByName) -> Option<usize> { fn find_section(obj: &ObjInfo, section_name: &str) -> Option<usize> {
obj.sections.iter().position(|section| section.name == selected_symbol.section_name) obj.sections.iter().position(|section| section.name == section_name)
} }
fn data_row_ui(ui: &mut egui::Ui, address: usize, diffs: &[ObjDataDiff], appearance: &Appearance) { fn data_row_ui(ui: &mut egui::Ui, address: usize, diffs: &[ObjDataDiff], appearance: &Appearance) {
@ -131,20 +131,37 @@ fn split_diffs(diffs: &[ObjDataDiff]) -> Vec<Vec<ObjDataDiff>> {
split_diffs split_diffs
} }
#[derive(Clone, Copy)]
struct SectionDiffContext<'a> {
obj: &'a ObjInfo,
diff: &'a ObjDiff,
section_index: Option<usize>,
}
impl<'a> SectionDiffContext<'a> {
pub fn new(obj: Option<&'a (ObjInfo, ObjDiff)>, section_name: Option<&str>) -> Option<Self> {
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( fn data_table_ui(
table: TableBuilder<'_>, ui: &mut egui::Ui,
left_obj: Option<&(ObjInfo, ObjDiff)>, available_width: f32,
right_obj: Option<&(ObjInfo, ObjDiff)>, left_ctx: Option<SectionDiffContext<'_>>,
selected_symbol: &SymbolRefByName, right_ctx: Option<SectionDiffContext<'_>>,
config: &Appearance, config: &Appearance,
) -> Option<()> { ) -> Option<()> {
let left_section = left_obj.and_then(|(obj, diff)| { let left_section = left_ctx
find_section(obj, selected_symbol).map(|i| (&obj.sections[i], &diff.sections[i])) .and_then(|ctx| ctx.section_index.map(|i| (&ctx.obj.sections[i], &ctx.diff.sections[i])));
}); let right_section = right_ctx
let right_section = right_obj.and_then(|(obj, diff)| { .and_then(|ctx| ctx.section_index.map(|i| (&ctx.obj.sections[i], &ctx.diff.sections[i])));
find_section(obj, selected_symbol).map(|i| (&obj.sections[i], &diff.sections[i]))
});
let total_bytes = left_section let total_bytes = left_section
.or(right_section)? .or(right_section)?
.1 .1
@ -159,72 +176,78 @@ fn data_table_ui(
let left_diffs = left_section.map(|(_, section)| split_diffs(&section.data_diff)); let left_diffs = left_section.map(|(_, section)| split_diffs(&section.data_diff));
let right_diffs = right_section.map(|(_, section)| split_diffs(&section.data_diff)); let right_diffs = right_section.map(|(_, section)| split_diffs(&section.data_diff));
table.body(|body| { render_table(ui, available_width, 2, config.code_font.size, total_rows, |row, column| {
body.rows(config.code_font.size, total_rows, |mut row| { let i = row.index();
let row_index = row.index(); let address = i * BYTES_PER_ROW;
let address = row_index * BYTES_PER_ROW;
row.col(|ui| { row.col(|ui| {
if column == 0 {
if let Some(left_diffs) = &left_diffs { 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);
} }
}); } else if column == 1 {
row.col(|ui| {
if let Some(right_diffs) = &right_diffs { 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(()) Some(())
} }
pub fn data_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &Appearance) { #[must_use]
let (Some(result), Some(selected_symbol)) = (&state.build, &state.symbol_state.selected_symbol) pub fn data_diff_ui(
else { ui: &mut egui::Ui,
return; state: &DiffViewState,
appearance: &Appearance,
) -> Option<DiffViewAction> {
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 // Header
let available_width = ui.available_width(); let available_width = ui.available_width();
let column_width = available_width / 2.0; render_header(ui, available_width, 2, |ui, column| {
ui.allocate_ui_with_layout( if column == 0 {
Vec2 { x: available_width, y: 100.0 },
Layout::left_to_right(Align::Min),
|ui| {
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate);
// Left column // 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() { if ui.button("⏴ Back").clicked() {
state.current_view = View::SymbolDiff; ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
} }
ui.scope(|ui| { if let Some(section) =
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); left_ctx.and_then(|ctx| ctx.section_index.map(|i| &ctx.obj.sections[i]))
ui.colored_label(appearance.highlight_color, &selected_symbol.symbol_name);
ui.label("Diff target:");
});
},
);
// 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.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.horizontal(|ui| {
if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() {
ret = Some(DiffViewAction::Build);
} }
ui.scope(|ui| { ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
@ -232,45 +255,38 @@ pub fn data_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &A
ui.colored_label(appearance.replace_color, "Building…"); ui.colored_label(appearance.replace_color, "Building…");
} else { } else {
ui.label("Last built:"); ui.label("Last built:");
let format = let format = format_description::parse("[hour]:[minute]:[second]").unwrap();
format_description::parse("[hour]:[minute]:[second]").unwrap();
ui.label( ui.label(
result result.time.to_offset(appearance.utc_offset).format(&format).unwrap(),
.time
.to_offset(appearance.utc_offset)
.format(&format)
.unwrap(),
); );
} }
}); });
}); });
ui.scope(|ui| { if let Some(section) =
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); right_ctx.and_then(|ctx| ctx.section_index.map(|i| &ctx.obj.sections[i]))
ui.label(""); {
ui.label("Diff base:"); 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),
);
}
}
}); });
},
);
},
);
ui.separator();
// Table // Table
ui.style_mut().interaction.selectable_labels = false; let id =
let available_height = ui.available_height(); Id::new(state.symbol_state.left_symbol.as_ref().and_then(|s| s.section_name.as_deref()))
let table = TableBuilder::new(ui) .with(state.symbol_state.right_symbol.as_ref().and_then(|s| s.section_name.as_deref()));
.striped(false) ui.push_id(id, |ui| {
.cell_layout(Layout::left_to_right(Align::Min)) data_table_ui(ui, available_width, left_ctx, right_ctx, appearance);
.columns(Column::exact(column_width).clip(true), 2) });
.resizable(false) ret
.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,
);
} }

View File

@ -1,28 +1,20 @@
use egui::{Align, Layout, ScrollArea, Ui, Vec2}; use egui::{RichText, ScrollArea};
use egui_extras::{Size, StripBuilder};
use objdiff_core::{ use objdiff_core::{
arch::ppc::ExceptionInfo, arch::ppc::ExceptionInfo,
diff::ObjDiff, obj::{ObjInfo, ObjSymbol},
obj::{ObjInfo, ObjSymbol, SymbolRef},
}; };
use time::format_description; use time::format_description;
use crate::views::{ use crate::views::{
appearance::Appearance, 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<SymbolRef> {
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 { fn decode_extab(extab: &ExceptionInfo) -> String {
let mut text = String::from(""); 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( fn extab_text_ui(
ui: &mut Ui, ui: &mut egui::Ui,
obj: &(ObjInfo, ObjDiff), ctx: FunctionDiffContext<'_>,
symbol_ref: SymbolRef, symbol: &ObjSymbol,
appearance: &Appearance, appearance: &Appearance,
) -> Option<()> { ) -> Option<()> {
let (_section, symbol) = obj.0.section_symbol(symbol_ref); if let Some(extab_entry) = find_extab_entry(ctx.obj, symbol) {
if let Some(extab_entry) = find_extab_entry(&obj.0, symbol) {
let text = decode_extab(extab_entry); let text = decode_extab(extab_entry);
ui.colored_label(appearance.replace_color, &text); ui.colored_label(appearance.replace_color, &text);
return Some(()); return Some(());
@ -65,79 +55,131 @@ fn extab_text_ui(
} }
fn extab_ui( fn extab_ui(
ui: &mut Ui, ui: &mut egui::Ui,
obj: Option<&(ObjInfo, ObjDiff)>, ctx: FunctionDiffContext<'_>,
selected_symbol: &SymbolRefByName,
appearance: &Appearance, appearance: &Appearance,
_left: bool, _column: usize,
) { ) {
ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| { ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
ui.scope(|ui| { ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
let symbol = obj.and_then(|(obj, _)| find_symbol(obj, selected_symbol)); if let Some((_section, symbol)) =
ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref))
if let (Some(object), Some(symbol_ref)) = (obj, symbol) { {
extab_text_ui(ui, object, symbol_ref, appearance); extab_text_ui(ui, ctx, symbol, appearance);
} }
}); });
}); });
} }
pub fn extab_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &Appearance) { #[must_use]
let (Some(result), Some(selected_symbol)) = (&state.build, &state.symbol_state.selected_symbol) pub fn extab_diff_ui(
else { ui: &mut egui::Ui,
return; state: &DiffViewState,
appearance: &Appearance,
) -> Option<DiffViewAction> {
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 // Header
let available_width = ui.available_width(); let available_width = ui.available_width();
let column_width = available_width / 2.0; render_header(ui, available_width, 2, |ui, column| {
ui.allocate_ui_with_layout( if column == 0 {
Vec2 { x: available_width, y: 100.0 },
Layout::left_to_right(Align::Min),
|ui| {
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate);
// Left column // 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| { ui.horizontal(|ui| {
if ui.button("⏴ Back").clicked() { if ui.button("⏴ Back").clicked() {
state.current_view = View::SymbolDiff; 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()));
}
} }
}); });
let name = selected_symbol if let Some((_section, symbol)) = left_ctx
.demangled_symbol_name .and_then(|ctx| ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref)))
.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:");
});
},
);
// 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; 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.horizontal(|ui| {
if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() {
ret = Some(DiffViewAction::Build);
} }
ui.scope(|ui| { ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
@ -145,57 +187,62 @@ pub fn extab_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &
ui.colored_label(appearance.replace_color, "Building…"); ui.colored_label(appearance.replace_color, "Building…");
} else { } else {
ui.label("Last built:"); ui.label("Last built:");
let format = let format = format_description::parse("[hour]:[minute]:[second]").unwrap();
format_description::parse("[hour]:[minute]:[second]").unwrap();
ui.label( ui.label(
result result.time.to_offset(appearance.utc_offset).format(&format).unwrap(),
.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);
}
}); });
ui.scope(|ui| { if let Some(((_section, symbol), symbol_diff)) = right_ctx.and_then(|ctx| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); ctx.symbol_ref.map(|symbol_ref| {
if let Some(match_percent) = result (ctx.obj.section_symbol(symbol_ref), ctx.diff.symbol_diff(symbol_ref))
.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) let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name);
{ ui.label(
ui.colored_label( RichText::new(name)
match_color_for_symbol(match_percent, appearance), .font(appearance.code_font.clone())
format!("{:.0}%", match_percent.floor()), .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.colored_label(appearance.replace_color, "Missing");
} }
ui.label("Diff base:"); } else {
ui.label(
RichText::new("Missing")
.font(appearance.code_font.clone())
.color(appearance.replace_color),
);
}
}
}); });
},
);
},
);
ui.separator();
// Table // Table
StripBuilder::new(ui).size(Size::remainder()).vertical(|mut strip| { render_strips(ui, available_width, 2, |ui, column| {
strip.strip(|builder| { if column == 0 {
builder.sizes(Size::remainder(), 2).horizontal(|mut strip| { if let Some(ctx) = left_ctx {
strip.cell(|ui| { extab_ui(ui, ctx, appearance, column);
extab_ui(ui, result.first_obj.as_ref(), selected_symbol, appearance, true); }
}); } else if column == 1 {
strip.cell(|ui| { if let Some(ctx) = right_ctx {
extab_ui(ui, result.second_obj.as_ref(), selected_symbol, appearance, false); extab_ui(ui, ctx, appearance, column);
}); }
}); }
}); });
}); ret
} }

View File

@ -1,28 +1,29 @@
use std::default::Default; use std::default::Default;
use egui::{text::LayoutJob, Align, Label, Layout, Response, Sense, Vec2, Widget}; use egui::{text::LayoutJob, Id, Label, Response, RichText, Sense, Widget};
use egui_extras::{Column, TableBuilder, TableRow}; use egui_extras::TableRow;
use objdiff_core::{ use objdiff_core::{
arch::ObjArch, arch::ObjArch,
diff::{ diff::{
display::{display_diff, DiffText, HighlightKind}, display::{display_diff, DiffText, HighlightKind},
ObjDiff, ObjInsDiff, ObjInsDiffKind, 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 time::format_description;
use crate::views::{ use crate::views::{
appearance::Appearance, 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)] #[derive(Default)]
pub struct FunctionViewState { pub struct FunctionViewState {
left_highlight: HighlightKind, left_highlight: HighlightKind,
@ -30,16 +31,17 @@ pub struct FunctionViewState {
} }
impl FunctionViewState { impl FunctionViewState {
fn highlight(&self, column: ColumnId) -> &HighlightKind { pub fn highlight(&self, column: usize) -> &HighlightKind {
match column { match column {
ColumnId::Left => &self.left_highlight, 0 => &self.left_highlight,
ColumnId::Right => &self.right_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 { match column {
ColumnId::Left => { 0 => {
if highlight == self.left_highlight { if highlight == self.left_highlight {
if highlight == self.right_highlight { if highlight == self.right_highlight {
self.left_highlight = HighlightKind::None; self.left_highlight = HighlightKind::None;
@ -51,7 +53,7 @@ impl FunctionViewState {
self.left_highlight = highlight; self.left_highlight = highlight;
} }
} }
ColumnId::Right => { 1 => {
if highlight == self.right_highlight { if highlight == self.right_highlight {
if highlight == self.left_highlight { if highlight == self.left_highlight {
self.left_highlight = HighlightKind::None; self.left_highlight = HighlightKind::None;
@ -63,10 +65,11 @@ impl FunctionViewState {
self.right_highlight = highlight; self.right_highlight = highlight;
} }
} }
_ => {}
} }
} }
fn clear_highlight(&mut self) { pub fn clear_highlight(&mut self) {
self.left_highlight = HighlightKind::None; self.left_highlight = HighlightKind::None;
self.right_highlight = HighlightKind::None; self.right_highlight = HighlightKind::None;
} }
@ -223,17 +226,19 @@ fn find_symbol(obj: &ObjInfo, selected_symbol: &SymbolRefByName) -> Option<Symbo
None None
} }
#[allow(clippy::too_many_arguments)] #[must_use]
#[expect(clippy::too_many_arguments)]
fn diff_text_ui( fn diff_text_ui(
ui: &mut egui::Ui, ui: &mut egui::Ui,
text: DiffText<'_>, text: DiffText<'_>,
ins_diff: &ObjInsDiff, ins_diff: &ObjInsDiff,
appearance: &Appearance, appearance: &Appearance,
ins_view_state: &mut FunctionViewState, ins_view_state: &FunctionViewState,
column: ColumnId, column: usize,
space_width: f32, space_width: f32,
response_cb: impl Fn(Response) -> Response, response_cb: impl Fn(Response) -> Response,
) { ) -> Option<DiffViewAction> {
let mut ret = None;
let label_text; let label_text;
let mut base_color = match ins_diff.kind { let mut base_color = match ins_diff.kind {
ObjInsDiffKind::None | ObjInsDiffKind::OpMismatch | ObjInsDiffKind::ArgMismatch => { ObjInsDiffKind::None | ObjInsDiffKind::OpMismatch | ObjInsDiffKind::ArgMismatch => {
@ -287,7 +292,7 @@ fn diff_text_ui(
} }
DiffText::Spacing(n) => { DiffText::Spacing(n) => {
ui.add_space(n as f32 * space_width); ui.add_space(n as f32 * space_width);
return; return ret;
} }
DiffText::Eol => { DiffText::Eol => {
label_text = "\n".to_string(); label_text = "\n".to_string();
@ -304,22 +309,25 @@ fn diff_text_ui(
.ui(ui); .ui(ui);
response = response_cb(response); response = response_cb(response);
if response.clicked() { if response.clicked() {
ins_view_state.set_highlight(column, text.into()); ret = Some(DiffViewAction::SetDiffHighlight(column, text.into()));
} }
if len < pad_to { if len < pad_to {
ui.add_space((pad_to - len) as f32 * space_width); ui.add_space((pad_to - len) as f32 * space_width);
} }
ret
} }
#[must_use]
fn asm_row_ui( fn asm_row_ui(
ui: &mut egui::Ui, ui: &mut egui::Ui,
ins_diff: &ObjInsDiff, ins_diff: &ObjInsDiff,
symbol: &ObjSymbol, symbol: &ObjSymbol,
appearance: &Appearance, appearance: &Appearance,
ins_view_state: &mut FunctionViewState, ins_view_state: &FunctionViewState,
column: ColumnId, column: usize,
response_cb: impl Fn(Response) -> Response, response_cb: impl Fn(Response) -> Response,
) { ) -> Option<DiffViewAction> {
let mut ret = None;
ui.spacing_mut().item_spacing.x = 0.0; ui.spacing_mut().item_spacing.x = 0.0;
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
if ins_diff.kind != ObjInsDiffKind::None { 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, ' ')); let space_width = ui.fonts(|f| f.glyph_width(&appearance.code_font, ' '));
display_diff(ins_diff, symbol.address, |text| { display_diff(ins_diff, symbol.address, |text| {
diff_text_ui( if let Some(action) = diff_text_ui(
ui, ui,
text, text,
ins_diff, ins_diff,
@ -336,170 +344,385 @@ fn asm_row_ui(
column, column,
space_width, space_width,
&response_cb, &response_cb,
); ) {
ret = Some(action);
}
Ok::<_, ()>(()) Ok::<_, ()>(())
}) })
.unwrap(); .unwrap();
ret
} }
#[must_use]
fn asm_col_ui( fn asm_col_ui(
row: &mut TableRow<'_, '_>, row: &mut TableRow<'_, '_>,
obj: &(ObjInfo, ObjDiff), ctx: FunctionDiffContext<'_>,
symbol_ref: SymbolRef,
appearance: &Appearance, appearance: &Appearance,
ins_view_state: &mut FunctionViewState, ins_view_state: &FunctionViewState,
column: ColumnId, column: usize,
) { ) -> Option<DiffViewAction> {
let (section, symbol) = obj.0.section_symbol(symbol_ref); let mut ret = None;
let section = section.unwrap(); let symbol_ref = ctx.symbol_ref?;
let ins_diff = &obj.1.symbol_diff(symbol_ref).instructions[row.index()]; 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| { let response_cb = |response: Response| {
if let Some(ins) = &ins_diff.ins { if let Some(ins) = &ins_diff.ins {
response.context_menu(|ui| ins_context_menu(ui, section, ins, symbol)); response.context_menu(|ui| ins_context_menu(ui, section, ins, symbol));
response.on_hover_ui_at_pointer(|ui| { 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 { } else {
response response
} }
}; };
let (_, response) = row.col(|ui| { 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); response_cb(response);
ret
} }
fn empty_col_ui(row: &mut TableRow<'_, '_>) { #[must_use]
row.col(|ui| {
ui.label("");
});
}
fn asm_table_ui( fn asm_table_ui(
table: TableBuilder<'_>, ui: &mut egui::Ui,
left_obj: Option<&(ObjInfo, ObjDiff)>, available_width: f32,
right_obj: Option<&(ObjInfo, ObjDiff)>, left_ctx: Option<FunctionDiffContext<'_>>,
selected_symbol: &SymbolRefByName, right_ctx: Option<FunctionDiffContext<'_>>,
appearance: &Appearance, appearance: &Appearance,
ins_view_state: &mut FunctionViewState, ins_view_state: &FunctionViewState,
) -> Option<()> { symbol_state: &SymbolViewState,
let left_symbol = left_obj.and_then(|(obj, _)| find_symbol(obj, selected_symbol)); ) -> Option<DiffViewAction> {
let right_symbol = right_obj.and_then(|(obj, _)| find_symbol(obj, selected_symbol)); let mut ret = None;
let instructions_len = match (left_symbol, right_symbol) { let left_len = left_ctx.and_then(|ctx| {
(Some(left_symbol_ref), Some(right_symbol_ref)) => { ctx.symbol_ref.map(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).instructions.len())
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(); let right_len = right_ctx.and_then(|ctx| {
debug_assert_eq!(left_len, right_len); 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 left_len
} }
(Some(left_symbol_ref), None) => { (Some(left_len), None) => left_len,
left_obj.unwrap().1.symbol_diff(left_symbol_ref).instructions.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| { if left_len.is_some() && right_len.is_some() {
body.rows(appearance.code_font.size, instructions_len, |mut row| { // Joint view
row.set_hovered(false); // Disable row hover effect render_table(
if let (Some(left_obj), Some(left_symbol_ref)) = (left_obj, left_symbol) { ui,
asm_col_ui( available_width,
&mut row, 2,
left_obj, appearance.code_font.size,
left_symbol_ref, instructions_len,
appearance, |row, column| {
ins_view_state, if column == 0 {
ColumnId::Left, if let Some(ctx) = left_ctx {
); if let Some(action) =
} else { asm_col_ui(row, ctx, appearance, ins_view_state, column)
empty_col_ui(&mut row); {
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 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() { if row.response().clicked() {
ins_view_state.clear_highlight(); 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
} }
pub fn function_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &Appearance) { #[derive(Clone, Copy)]
let (Some(result), Some(selected_symbol)) = (&state.build, &state.symbol_state.selected_symbol) pub struct FunctionDiffContext<'a> {
else { pub obj: &'a ObjInfo,
return; pub diff: &'a ObjDiff,
pub symbol_ref: Option<SymbolRef>,
}
impl<'a> FunctionDiffContext<'a> {
pub fn new(
obj: Option<&'a (ObjInfo, ObjDiff)>,
selected_symbol: Option<&SymbolRefByName>,
) -> Option<Self> {
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() }
}
#[must_use]
pub fn function_diff_ui(
ui: &mut egui::Ui,
state: &DiffViewState,
appearance: &Appearance,
) -> Option<DiffViewAction> {
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 // Header
let available_width = ui.available_width(); let available_width = ui.available_width();
let column_width = available_width / 2.0; render_header(ui, available_width, 2, |ui, column| {
ui.allocate_ui_with_layout( if column == 0 {
Vec2 { x: available_width, y: 100.0 },
Layout::left_to_right(Align::Min),
|ui| {
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate);
// Left column // 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| { ui.horizontal(|ui| {
if ui.button("⏴ Back").clicked() { if ui.button("⏴ Back").clicked() {
state.current_view = View::SymbolDiff; ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
} }
ui.separator(); ui.separator();
if ui if ui
.add_enabled( .add_enabled(
!state.scratch_running && state.scratch_available, !state.scratch_running
&& state.scratch_available
&& left_ctx.map_or(false, |ctx| ctx.has_symbol()),
egui::Button::new("📲 decomp.me"), egui::Button::new("📲 decomp.me"),
) )
.on_hover_text_at_pointer("Create a new scratch on decomp.me (beta)") .on_hover_text_at_pointer("Create a new scratch on decomp.me (beta)")
.on_disabled_hover_text("Scratch configuration missing") .on_disabled_hover_text("Scratch configuration missing")
.clicked() .clicked()
{ {
state.queue_scratch = true; 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()));
}
} }
}); });
let name = selected_symbol if let Some((_section, symbol)) = left_ctx
.demangled_symbol_name .and_then(|ctx| ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref)))
.as_deref() {
.unwrap_or(&selected_symbol.symbol_name); let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name);
ui.scope(|ui| { ui.label(
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); RichText::new(name)
ui.colored_label(appearance.highlight_color, name); .font(appearance.code_font.clone())
ui.label("Diff target:"); .color(appearance.highlight_color),
});
},
); );
if right_ctx.map_or(false, |m| m.has_symbol())
// Right column && ui
ui.allocate_ui_with_layout( .button("Change target")
Vec2 { x: column_width, y: 100.0 }, .on_hover_text_at_pointer("Choose a different symbol to use as the target")
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() .clicked()
{ {
state.queue_build = true; 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.horizontal(|ui| {
if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() {
ret = Some(DiffViewAction::Build);
} }
ui.scope(|ui| { ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
@ -507,75 +730,90 @@ pub fn function_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance
ui.colored_label(appearance.replace_color, "Building…"); ui.colored_label(appearance.replace_color, "Building…");
} else { } else {
ui.label("Last built:"); ui.label("Last built:");
let format = let format = format_description::parse("[hour]:[minute]:[second]").unwrap();
format_description::parse("[hour]:[minute]:[second]").unwrap();
ui.label( ui.label(
result result.time.to_offset(appearance.utc_offset).format(&format).unwrap(),
.time
.to_offset(appearance.utc_offset)
.format(&format)
.unwrap(),
); );
} }
}); });
ui.separator(); ui.separator();
if ui if ui
.add_enabled( .add_enabled(state.source_path_available, egui::Button::new("🖹 Source file"))
state.source_path_available,
egui::Button::new("🖹 Source file"),
)
.on_hover_text_at_pointer("Open the source file in the default editor") .on_hover_text_at_pointer("Open the source file in the default editor")
.on_disabled_hover_text("Source file metadata missing") .on_disabled_hover_text("Source file metadata missing")
.clicked() .clicked()
{ {
state.queue_open_source_path = true; ret = Some(DiffViewAction::OpenSourcePath);
} }
}); });
ui.scope(|ui| { if let Some(((_section, symbol), symbol_diff)) = right_ctx.and_then(|ctx| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); ctx.symbol_ref.map(|symbol_ref| {
if let Some(match_percent) = result (ctx.obj.section_symbol(symbol_ref), ctx.diff.symbol_diff(symbol_ref))
.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) let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name);
{ ui.label(
ui.colored_label( RichText::new(name)
match_color_for_symbol(match_percent, appearance), .font(appearance.code_font.clone())
format!("{:.0}%", match_percent.floor()), .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)),
); );
} else {
ui.colored_label(appearance.replace_color, "Missing");
} }
ui.label("Diff base:"); if left_ctx.map_or(false, |m| m.has_symbol()) {
});
},
);
},
);
ui.separator(); ui.separator();
if ui
.button("Change base")
.on_hover_text_at_pointer(
"Choose a different symbol to use as the base",
)
.clicked()
{
if let Some(symbol_ref) = state.symbol_state.left_symbol.as_ref() {
ret = Some(DiffViewAction::SelectingRight(symbol_ref.clone()));
}
}
}
});
} 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 // Table
ui.style_mut().interaction.selectable_labels = false; let id = Id::new(state.symbol_state.left_symbol.as_ref().map(|s| s.symbol_name.as_str()))
let available_height = ui.available_height(); .with(state.symbol_state.right_symbol.as_ref().map(|s| s.symbol_name.as_str()));
let table = TableBuilder::new(ui) if let Some(action) = ui
.striped(false) .push_id(id, |ui| {
.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( asm_table_ui(
table, ui,
result.first_obj.as_ref(), available_width,
result.second_obj.as_ref(), left_ctx,
selected_symbol, right_ctx,
appearance, appearance,
&mut state.function_state, &state.function_state,
); &state.symbol_state,
)
})
.inner
{
ret = Some(action);
}
ret
} }

View File

@ -1,6 +1,7 @@
use egui::{text::LayoutJob, Color32, FontId, TextFormat}; use egui::{text::LayoutJob, Color32, FontId, TextFormat};
pub(crate) mod appearance; pub(crate) mod appearance;
pub(crate) mod column_layout;
pub(crate) mod config; pub(crate) mod config;
pub(crate) mod data_diff; pub(crate) mod data_diff;
pub(crate) mod debug; pub(crate) mod debug;

View File

@ -1,13 +1,12 @@
use std::mem::take; use std::{collections::BTreeMap, mem::take};
use egui::{ use egui::{
text::LayoutJob, Align, CollapsingHeader, Color32, Id, Layout, OpenUrl, ScrollArea, text::LayoutJob, CollapsingHeader, Color32, Id, OpenUrl, ScrollArea, SelectableLabel, TextEdit,
SelectableLabel, TextEdit, Ui, Vec2, Widget, Ui, Widget,
}; };
use egui_extras::{Size, StripBuilder};
use objdiff_core::{ use objdiff_core::{
arch::ObjArch, arch::ObjArch,
diff::{ObjDiff, ObjSymbolDiff}, diff::{display::HighlightKind, ObjDiff, ObjSymbolDiff},
obj::{ObjInfo, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlags, SymbolRef}, obj::{ObjInfo, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlags, SymbolRef},
}; };
use regex::{Regex, RegexBuilder}; use regex::{Regex, RegexBuilder};
@ -19,17 +18,28 @@ use crate::{
objdiff::{BuildStatus, ObjDiffResult}, objdiff::{BuildStatus, ObjDiffResult},
Job, JobQueue, JobResult, 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 struct SymbolRefByName {
pub symbol_name: String, pub symbol_name: String,
pub demangled_symbol_name: Option<String>, pub section_name: Option<String>,
pub section_name: String,
} }
#[allow(clippy::enum_variant_names)] impl SymbolRefByName {
#[derive(Default, Eq, PartialEq, Copy, Clone)] pub fn new(symbol: &ObjSymbol, section: Option<&ObjSection>) -> Self {
Self { symbol_name: symbol.name.clone(), section_name: section.map(|s| s.name.clone()) }
}
}
#[expect(clippy::enum_variant_names)]
#[derive(Debug, Default, Eq, PartialEq, Copy, Clone, Hash)]
pub enum View { pub enum View {
#[default] #[default]
SymbolDiff, SymbolDiff,
@ -38,6 +48,71 @@ pub enum View {
ExtabDiff, 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<SymbolRef>, Option<SymbolRef>),
/// 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<View>,
pub left_symbol: Option<SymbolRefByName>,
pub right_symbol: Option<SymbolRefByName>,
}
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<SymbolDiffContext<'_>>,
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)] #[derive(Default)]
pub struct DiffViewState { pub struct DiffViewState {
pub build: Option<Box<ObjDiffResult>>, pub build: Option<Box<ObjDiffResult>>,
@ -47,22 +122,23 @@ pub struct DiffViewState {
pub function_state: FunctionViewState, pub function_state: FunctionViewState,
pub search: String, pub search: String,
pub search_regex: Option<Regex>, pub search_regex: Option<Regex>,
pub queue_build: bool,
pub build_running: bool, pub build_running: bool,
pub scratch_available: bool, pub scratch_available: bool,
pub queue_scratch: bool,
pub scratch_running: bool, pub scratch_running: bool,
pub source_path_available: bool, pub source_path_available: bool,
pub queue_open_source_path: bool, pub post_build_nav: Option<DiffViewNavigation>,
pub object_name: String,
} }
#[derive(Default)] #[derive(Default)]
pub struct SymbolViewState { pub struct SymbolViewState {
pub highlighted_symbol: (Option<SymbolRef>, Option<SymbolRef>), pub highlighted_symbol: (Option<SymbolRef>, Option<SymbolRef>),
pub selected_symbol: Option<SymbolRefByName>, pub left_symbol: Option<SymbolRefByName>,
pub right_symbol: Option<SymbolRefByName>,
pub reverse_fn_order: bool, pub reverse_fn_order: bool,
pub disable_reverse_fn_order: bool, pub disable_reverse_fn_order: bool,
pub show_hidden_symbols: bool, pub show_hidden_symbols: bool,
pub show_mapped_symbols: bool,
} }
impl DiffViewState { impl DiffViewState {
@ -70,6 +146,16 @@ impl DiffViewState {
jobs.results.retain_mut(|result| match result { jobs.results.retain_mut(|result| match result {
JobResult::ObjDiff(result) => { JobResult::ObjDiff(result) => {
self.build = take(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 false
} }
JobResult::CreateScratch(result) => { JobResult::CreateScratch(result) => {
@ -93,44 +179,103 @@ impl DiffViewState {
self.source_path_available = false; self.source_path_available = false;
} }
self.scratch_available = CreateScratchConfig::is_available(&state.config); 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<DiffViewAction>,
ctx: &egui::Context,
jobs: &mut JobQueue,
state: &AppStateRef,
) {
if let Some(result) = take(&mut self.scratch) { if let Some(result) = take(&mut self.scratch) {
ctx.output_mut(|o| o.open_url = Some(OpenUrl::new_tab(result.scratch_url))); ctx.output_mut(|o| o.open_url = Some(OpenUrl::new_tab(result.scratch_url)));
} }
if self.queue_build { let Some(action) = action else {
self.queue_build = false; return;
};
match action {
DiffViewAction::Build => {
if let Ok(mut state) = state.write() { if let Ok(mut state) = state.write() {
state.queue_build = true; state.queue_build = true;
} }
} }
DiffViewAction::Navigate(nav) => {
if self.queue_scratch { if self.post_build_nav.is_some() {
self.queue_scratch = false; // Ignore action if we're already navigating
if let Some(function_name) = return;
self.symbol_state.selected_symbol.as_ref().map(|sym| sym.symbol_name.clone()) }
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)
{ {
if let Ok(state) = state.read() { // 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);
}
(None, Some(right_ref)) => {
state.set_selecting_left(&right_ref.symbol_name);
}
(Some(_), Some(_)) => unreachable!(),
(None, None) => unreachable!(),
}
self.post_build_nav = Some(nav);
}
}
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) { match CreateScratchConfig::from_config(&state.config, function_name) {
Ok(config) => { Ok(config) => {
jobs.push_once(Job::CreateScratch, || { jobs.push_once(Job::CreateScratch, || start_create_scratch(ctx, config));
start_create_scratch(ctx, config)
});
} }
Err(err) => { Err(err) => {
log::error!("Failed to create scratch config: {err}"); log::error!("Failed to create scratch config: {err}");
} }
} }
} }
} DiffViewAction::OpenSourcePath => {
} let Ok(state) = state.read() else {
return;
if self.queue_open_source_path { };
self.queue_open_source_path = false;
if let Ok(state) = state.read() {
if let (Some(project_dir), Some(source_path)) = ( if let (Some(project_dir), Some(source_path)) = (
&state.config.project_dir, &state.config.project_dir,
state.config.selected_obj.as_ref().and_then(|obj| obj.source_path.as_ref()), 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( fn symbol_context_menu_ui(
ui: &mut Ui, ui: &mut Ui,
state: &mut SymbolViewState, ctx: SymbolDiffContext<'_>,
arch: &dyn ObjArch, other_ctx: Option<SymbolDiffContext<'_>>,
symbol: &ObjSymbol, symbol: &ObjSymbol,
symbol_diff: &ObjSymbolDiff,
section: Option<&ObjSection>, section: Option<&ObjSection>,
) -> Option<View> { column: usize,
) -> Option<DiffViewNavigation> {
let mut ret = None; let mut ret = None;
ui.scope(|ui| { ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
@ -185,14 +393,35 @@ fn symbol_context_menu_ui(
} }
} }
if let Some(section) = section { 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() { if has_extab && ui.button("Decode exception table").clicked() {
state.selected_symbol = Some(SymbolRefByName { ret = Some(DiffViewNavigation::with_symbols(
symbol_name: symbol.name.clone(), View::ExtabDiff,
demangled_symbol_name: symbol.demangled_name.clone(), other_ctx,
section_name: section.name.clone(), 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,
}); });
ret = Some(View::ExtabDiff); } else {
ret = Some(DiffViewNavigation {
view: Some(View::FunctionDiff),
left_symbol: None,
right_symbol: Some(symbol_ref),
});
}
ui.close_menu(); ui.close_menu();
} }
} }
@ -232,27 +461,28 @@ fn symbol_hover_ui(ui: &mut Ui, arch: &dyn ObjArch, symbol: &ObjSymbol, appearan
} }
#[must_use] #[must_use]
#[allow(clippy::too_many_arguments)] #[expect(clippy::too_many_arguments)]
fn symbol_ui( fn symbol_ui(
ui: &mut Ui, ui: &mut Ui,
arch: &dyn ObjArch, ctx: SymbolDiffContext<'_>,
other_ctx: Option<SymbolDiffContext<'_>>,
symbol: &ObjSymbol, symbol: &ObjSymbol,
symbol_diff: &ObjSymbolDiff, symbol_diff: &ObjSymbolDiff,
section: Option<&ObjSection>, section: Option<&ObjSection>,
state: &mut SymbolViewState, state: &SymbolViewState,
appearance: &Appearance, appearance: &Appearance,
left: bool, column: usize,
) -> Option<View> { ) -> Option<DiffViewAction> {
if symbol.flags.0.contains(ObjSymbolFlags::Hidden) && !state.show_hidden_symbols {
return None;
}
let mut ret = None; let mut ret = None;
if symbol.flags.0.contains(ObjSymbolFlags::Hidden) && !state.show_hidden_symbols {
return ret;
}
let mut job = LayoutJob::default(); let mut job = LayoutJob::default();
let name: &str = let name: &str =
if let Some(demangled) = &symbol.demangled_name { demangled } else { &symbol.name }; if let Some(demangled) = &symbol.demangled_name { demangled } else { &symbol.name };
let mut selected = false; let mut selected = false;
if let Some(sym_ref) = 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; 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(") ", appearance.text_color, &mut job, appearance.code_font.clone());
} }
write_text(name, appearance.highlight_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) let response = SelectableLabel::new(selected, job).ui(ui).on_hover_ui_at_pointer(|ui| {
.ui(ui) symbol_hover_ui(ui, ctx.obj.arch.as_ref(), symbol, appearance)
.on_hover_ui_at_pointer(|ui| symbol_hover_ui(ui, arch, symbol, appearance)); });
response.context_menu(|ui| { 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 response.clicked() {
if let Some(section) = section { if let Some(section) = section {
if section.kind == ObjSectionKind::Code { match section.kind {
state.selected_symbol = Some(SymbolRefByName { ObjSectionKind::Code => {
symbol_name: symbol.name.clone(), ret = Some(DiffViewAction::Navigate(DiffViewNavigation::with_symbols(
demangled_symbol_name: symbol.demangled_name.clone(), View::FunctionDiff,
section_name: section.name.clone(), other_ctx,
}); symbol,
ret = Some(View::FunctionDiff); section,
} else if section.kind == ObjSectionKind::Data { symbol_diff,
state.selected_symbol = Some(SymbolRefByName { column,
symbol_name: section.name.clone(), )));
demangled_symbol_name: None, }
section_name: section.name.clone(), ObjSectionKind::Data => {
}); ret = Some(DiffViewAction::Navigate(DiffViewNavigation::with_symbols(
ret = Some(View::DataDiff); View::DataDiff,
other_ctx,
symbol,
section,
symbol_diff,
column,
)));
}
ObjSectionKind::Bss => {}
} }
} }
} else if response.hovered() { } else if response.hovered() {
state.highlighted_symbol = if let Some(diff_symbol) = symbol_diff.diff_symbol { ret = Some(if let Some(target_symbol) = symbol_diff.target_symbol {
if left { if column == 0 {
(Some(symbol_diff.symbol_ref), Some(diff_symbol)) DiffViewAction::SetSymbolHighlight(
Some(symbol_diff.symbol_ref),
Some(target_symbol),
)
} else { } else {
(Some(diff_symbol), Some(symbol_diff.symbol_ref)) DiffViewAction::SetSymbolHighlight(
Some(target_symbol),
Some(symbol_diff.symbol_ref),
)
} }
} else { } else {
(None, None) DiffViewAction::SetSymbolHighlight(None, None)
}; });
} }
ret ret
} }
fn symbol_matches_search(symbol: &ObjSymbol, search_regex: Option<&Regex>) -> bool { fn symbol_matches_filter(
if let Some(search_regex) = search_regex { symbol: &ObjSymbol,
search_regex.is_match(&symbol.name) diff: &ObjSymbolDiff,
|| symbol.demangled_name.as_ref().map(|s| search_regex.is_match(s)).unwrap_or(false) filter: SymbolFilter<'_>,
} else { ) -> bool {
true 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] #[must_use]
fn symbol_list_ui( pub fn symbol_list_ui(
ui: &mut Ui, ui: &mut Ui,
obj: &(ObjInfo, ObjDiff), ctx: SymbolDiffContext<'_>,
state: &mut SymbolViewState, other_ctx: Option<SymbolDiffContext<'_>>,
search_regex: Option<&Regex>, state: &SymbolViewState,
filter: SymbolFilter<'_>,
appearance: &Appearance, appearance: &Appearance,
left: bool, column: usize,
) -> Option<View> { ) -> Option<DiffViewAction> {
let mut ret = None; let mut ret = None;
let arch = obj.0.arch.as_ref();
ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| { 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(&section_diff.symbols) {
if !symbol_matches_filter(symbol, symbol_diff, filter) {
continue;
}
mapping.insert(symbol_diff.symbol_ref, symbol_diff);
}
}
}
ui.scope(|ui| { ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); 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| { CollapsingHeader::new(".comm").default_open(true).show(ui, |ui| {
for (symbol, symbol_diff) in obj.0.common.iter().zip(&obj.1.common) { for (symbol_ref, symbol_diff) in mapping
if !symbol_matches_search(symbol, search_regex) { .iter()
continue; .filter(|(symbol_ref, _)| symbol_ref.section_idx == usize::MAX)
} {
ret = ret.or(symbol_ui( let symbol = ctx.obj.section_symbol(*symbol_ref).1;
if let Some(result) = symbol_ui(
ui, ui,
arch, ctx,
other_ctx,
symbol, symbol,
symbol_diff, symbol_diff,
None, None,
state, state,
appearance, 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( let mut header = LayoutJob::simple_singleline(
format!("{} ({:x})", section.name, section.size), format!("{} ({:x})", section.name, section.size),
appearance.code_font.clone(), appearance.code_font.clone(),
@ -406,40 +712,45 @@ fn symbol_list_ui(
.default_open(true) .default_open(true)
.show(ui, |ui| { .show(ui, |ui| {
if section.kind == ObjSectionKind::Code && state.reverse_fn_order { if section.kind == ObjSectionKind::Code && state.reverse_fn_order {
for (symbol, symbol_diff) in for (symbol, symbol_diff) in mapping
section.symbols.iter().zip(&section_diff.symbols).rev() .iter()
.filter(|(symbol_ref, _)| symbol_ref.section_idx == section_index)
.rev()
{ {
if !symbol_matches_search(symbol, search_regex) { let symbol = ctx.obj.section_symbol(*symbol).1;
continue; if let Some(result) = symbol_ui(
}
ret = ret.or(symbol_ui(
ui, ui,
arch, ctx,
other_ctx,
symbol, symbol,
symbol_diff, symbol_diff,
Some(section), Some(section),
state, state,
appearance, appearance,
left, column,
)); ) {
ret = Some(result);
}
} }
} else { } else {
for (symbol, symbol_diff) in for (symbol, symbol_diff) in mapping
section.symbols.iter().zip(&section_diff.symbols) .iter()
.filter(|(symbol_ref, _)| symbol_ref.section_idx == section_index)
{ {
if !symbol_matches_search(symbol, search_regex) { let symbol = ctx.obj.section_symbol(*symbol).1;
continue; if let Some(result) = symbol_ui(
}
ret = ret.or(symbol_ui(
ui, ui,
arch, ctx,
other_ctx,
symbol, symbol,
symbol_diff, symbol_diff,
Some(section), Some(section),
state, state,
appearance, appearance,
left, column,
)); ) {
ret = Some(result);
}
} }
} }
}); });
@ -487,80 +798,62 @@ fn missing_obj_ui(ui: &mut Ui, appearance: &Appearance) {
}); });
} }
pub fn symbol_diff_ui(ui: &mut Ui, state: &mut DiffViewState, appearance: &Appearance) { #[derive(Copy, Clone)]
let DiffViewState { build, current_view, symbol_state, search, search_regex, .. } = state; pub struct SymbolDiffContext<'a> {
let Some(result) = build else { pub obj: &'a ObjInfo,
return; pub diff: &'a ObjDiff,
}
#[must_use]
pub fn symbol_diff_ui(
ui: &mut Ui,
state: &mut DiffViewState,
appearance: &Appearance,
) -> Option<DiffViewAction> {
let mut ret = None;
let Some(result) = &state.build else {
return ret;
}; };
// Header // Header
let available_width = ui.available_width(); let available_width = ui.available_width();
let column_width = available_width / 2.0; render_header(ui, available_width, 2, |ui, column| {
ui.allocate_ui_with_layout( if column == 0 {
Vec2 { x: available_width, y: 100.0 },
Layout::left_to_right(Align::Min),
|ui| {
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate);
// Left column // 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.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.label("Build target:"); ui.label("Target object");
if result.first_status.success { if result.first_status.success {
if result.first_obj.is_none() { if result.first_obj.is_none() {
ui.colored_label(appearance.replace_color, "Missing"); ui.colored_label(appearance.replace_color, "Missing");
} else { } else {
ui.label("OK"); ui.colored_label(appearance.highlight_color, state.object_name.clone());
} }
} else { } else {
ui.colored_label(appearance.delete_color, "Fail"); ui.colored_label(appearance.delete_color, "Fail");
} }
}); });
if TextEdit::singleline(search).hint_text("Filter symbols").ui(ui).changed() { let mut search = state.search.clone();
if search.is_empty() { if TextEdit::singleline(&mut search).hint_text("Filter symbols").ui(ui).changed() {
*search_regex = None; ret = Some(DiffViewAction::SetSearch(search));
} else if let Ok(regex) =
RegexBuilder::new(search).case_insensitive(true).build()
{
*search_regex = Some(regex);
} else {
*search_regex = None;
} }
} } else if column == 1 {
},
);
// Right column // 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.horizontal(|ui| {
ui.scope(|ui| { ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.label("Build base:"); ui.label("Base object");
}); });
ui.separator(); ui.separator();
if ui if ui
.add_enabled( .add_enabled(state.source_path_available, egui::Button::new("🖹 Source file"))
state.source_path_available,
egui::Button::new("🖹 Source file"),
)
.on_hover_text_at_pointer("Open the source file in the default editor") .on_hover_text_at_pointer("Open the source file in the default editor")
.on_disabled_hover_text("Source file metadata missing") .on_disabled_hover_text("Source file metadata missing")
.clicked() .clicked()
{ {
state.queue_open_source_path = true; ret = Some(DiffViewAction::OpenSourcePath);
} }
}); });
@ -570,7 +863,7 @@ pub fn symbol_diff_ui(ui: &mut Ui, state: &mut DiffViewState, appearance: &Appea
if result.second_obj.is_none() { if result.second_obj.is_none() {
ui.colored_label(appearance.replace_color, "Missing"); ui.colored_label(appearance.replace_color, "Missing");
} else { } else {
ui.label("OK"); ui.colored_label(appearance.highlight_color, "OK");
} }
} else { } else {
ui.colored_label(appearance.delete_color, "Fail"); ui.colored_label(appearance.delete_color, "Fail");
@ -578,64 +871,66 @@ pub fn symbol_diff_ui(ui: &mut Ui, state: &mut DiffViewState, appearance: &Appea
}); });
if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() { if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() {
state.queue_build = true; ret = Some(DiffViewAction::Build);
} }
}, }
); });
},
);
ui.separator();
// Table // Table
let mut ret = None; let filter = match &state.search_regex {
StripBuilder::new(ui).size(Size::remainder()).vertical(|mut strip| { Some(regex) => SymbolFilter::Search(regex),
strip.strip(|builder| { _ => SymbolFilter::None,
builder.sizes(Size::remainder(), 2).horizontal(|mut strip| { };
strip.cell(|ui| { render_strips(ui, available_width, 2, |ui, column| {
ui.push_id("left", |ui| { if column == 0 {
// Left column
if result.first_status.success { if result.first_status.success {
if let Some(obj) = &result.first_obj { if let Some((obj, diff)) = &result.first_obj {
ret = ret.or(symbol_list_ui( if let Some(result) = symbol_list_ui(
ui, ui,
obj, SymbolDiffContext { obj, diff },
symbol_state, result
search_regex.as_ref(), .second_obj
.as_ref()
.map(|(obj, diff)| SymbolDiffContext { obj, diff }),
&state.symbol_state,
filter,
appearance, appearance,
true, column,
)); ) {
ret = Some(result);
}
} else { } else {
missing_obj_ui(ui, appearance); missing_obj_ui(ui, appearance);
} }
} else { } else {
build_log_ui(ui, &result.first_status, appearance); build_log_ui(ui, &result.first_status, appearance);
} }
}); } else if column == 1 {
}); // Right column
strip.cell(|ui| {
ui.push_id("right", |ui| {
if result.second_status.success { if result.second_status.success {
if let Some(obj) = &result.second_obj { if let Some((obj, diff)) = &result.second_obj {
ret = ret.or(symbol_list_ui( if let Some(result) = symbol_list_ui(
ui, ui,
obj, SymbolDiffContext { obj, diff },
symbol_state, result
search_regex.as_ref(), .first_obj
.as_ref()
.map(|(obj, diff)| SymbolDiffContext { obj, diff }),
&state.symbol_state,
filter,
appearance, appearance,
false, column,
)); ) {
ret = Some(result);
}
} else { } else {
missing_obj_ui(ui, appearance); missing_obj_ui(ui, appearance);
} }
} else { } else {
build_log_ui(ui, &result.second_status, appearance); build_log_ui(ui, &result.second_status, appearance);
} }
});
});
});
});
});
if let Some(view) = ret {
*current_view = view;
} }
});
ret
} }