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,26 +102,32 @@ 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
if obj.name.as_deref() == Some(u) { .units
.as_deref_mut()
.unwrap_or_default()
.iter_mut()
.find_map(|obj| {
if obj.name.as_deref() == Some(u) {
resolve_paths(obj);
return Some(obj);
}
let up = unit_path.as_deref()?;
resolve_paths(obj); resolve_paths(obj);
return Some(obj);
}
let up = unit_path.as_deref()?; if [&obj.base_path, &obj.target_path]
.into_iter()
.filter_map(|p| p.as_ref().and_then(|p| p.canonicalize().ok()))
.any(|p| p == up)
{
return Some(obj);
}
resolve_paths(obj); None
})
if [&obj.base_path, &obj.target_path] else {
.into_iter()
.filter_map(|p| p.as_ref().and_then(|p| p.canonicalize().ok()))
.any(|p| p == up)
{
return Some(obj);
}
None
}) 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;
if let Some(info) = &state.project_config_info { let mut mod_check = false;
if file_modified(&info.path, info.timestamp) { if state.last_mod_check.elapsed().as_millis() >= 500 {
state.config_change = true; state.last_mod_check = Instant::now();
mod_check = true;
}
if mod_check {
if let Some(info) = &state.project_config_info {
if let Some(last_ts) = info.timestamp {
if file_modified(&info.path, last_ts) {
state.config_change = true;
}
}
} }
} }
if state.config_change { 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,10 +832,10 @@ 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 frame work to save state before shutdown. /// Called by the framework to save state before shutdown.
fn save(&mut self, storage: &mut dyn eframe::Storage) { fn save(&mut self, storage: &mut dyn eframe::Storage) {
if let Ok(state) = self.state.read() { if let Ok(state) = self.state.read() {
eframe::set_value(storage, CONFIG_KEY, &state.config); eframe::set_value(storage, CONFIG_KEY, &state.config);

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,118 +176,117 @@ 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( if ui.button("⏴ Back").clicked() {
Vec2 { x: column_width, y: 100.0 }, ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
Layout::top_down(Align::Min), }
|ui| {
ui.set_width(column_width);
if ui.button("⏴ Back").clicked() {
state.current_view = View::SymbolDiff;
}
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.colored_label(appearance.highlight_color, &selected_symbol.symbol_name);
ui.label("Diff target:");
});
},
);
if let Some(section) =
left_ctx.and_then(|ctx| ctx.section_index.map(|i| &ctx.obj.sections[i]))
{
ui.label(
RichText::new(section.name.clone())
.font(appearance.code_font.clone())
.color(appearance.highlight_color),
);
} else {
ui.label(
RichText::new("Missing")
.font(appearance.code_font.clone())
.color(appearance.replace_color),
);
}
} else if column == 1 {
// Right column // Right column
ui.allocate_ui_with_layout( ui.horizontal(|ui| {
Vec2 { x: column_width, y: 100.0 }, if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() {
Layout::top_down(Align::Min), ret = Some(DiffViewAction::Build);
|ui| { }
ui.set_width(column_width); ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
if state.build_running {
ui.colored_label(appearance.replace_color, "Building…");
} else {
ui.label("Last built:");
let format = format_description::parse("[hour]:[minute]:[second]").unwrap();
ui.label(
result.time.to_offset(appearance.utc_offset).format(&format).unwrap(),
);
}
});
});
ui.horizontal(|ui| { if let Some(section) =
if ui right_ctx.and_then(|ctx| ctx.section_index.map(|i| &ctx.obj.sections[i]))
.add_enabled(!state.build_running, egui::Button::new("Build")) {
.clicked() ui.label(
{ RichText::new(section.name.clone())
state.queue_build = true; .font(appearance.code_font.clone())
} .color(appearance.highlight_color),
ui.scope(|ui| { );
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); } else {
if state.build_running { ui.label(
ui.colored_label(appearance.replace_color, "Building…"); RichText::new("Missing")
} else { .font(appearance.code_font.clone())
ui.label("Last built:"); .color(appearance.replace_color),
let format = );
format_description::parse("[hour]:[minute]:[second]").unwrap(); }
ui.label( }
result });
.time
.to_offset(appearance.utc_offset)
.format(&format)
.unwrap(),
);
}
});
});
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.label("");
ui.label("Diff base:");
});
},
);
},
);
ui.separator();
// 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,137 +55,194 @@ 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( ui.horizontal(|ui| {
Vec2 { x: column_width, y: 100.0 }, if ui.button("⏴ Back").clicked() {
Layout::top_down(Align::Min), ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
|ui| { }
ui.set_width(column_width); ui.separator();
if ui
ui.horizontal(|ui| { .add_enabled(
if ui.button("⏴ Back").clicked() { !state.scratch_running
state.current_view = View::SymbolDiff; && state.scratch_available
} && left_ctx.map_or(false, |ctx| ctx.has_symbol()),
}); egui::Button::new("📲 decomp.me"),
)
let name = selected_symbol .on_hover_text_at_pointer("Create a new scratch on decomp.me (beta)")
.demangled_symbol_name .on_disabled_hover_text("Scratch configuration missing")
.as_deref() .clicked()
.unwrap_or(&selected_symbol.symbol_name); {
ui.scope(|ui| { if let Some((_section, symbol)) = left_ctx.and_then(|ctx| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref))
ui.colored_label(appearance.highlight_color, name); }) {
ui.label("Diff target:"); ret = Some(DiffViewAction::CreateScratch(symbol.name.clone()));
}); }
}, }
); });
if let Some((_section, symbol)) = left_ctx
.and_then(|ctx| ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref)))
{
let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name);
ui.label(
RichText::new(name)
.font(appearance.code_font.clone())
.color(appearance.highlight_color),
);
} else {
ui.label(
RichText::new("Missing")
.font(appearance.code_font.clone())
.color(appearance.replace_color),
);
}
} else if column == 1 {
// Right column // Right column
ui.allocate_ui_with_layout( ui.horizontal(|ui| {
Vec2 { x: column_width, y: 100.0 }, if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() {
Layout::top_down(Align::Min), ret = Some(DiffViewAction::Build);
|ui| { }
ui.set_width(column_width); ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
if state.build_running {
ui.colored_label(appearance.replace_color, "Building…");
} else {
ui.label("Last built:");
let format = format_description::parse("[hour]:[minute]:[second]").unwrap();
ui.label(
result.time.to_offset(appearance.utc_offset).format(&format).unwrap(),
);
}
});
ui.separator();
if ui
.add_enabled(state.source_path_available, egui::Button::new("🖹 Source file"))
.on_hover_text_at_pointer("Open the source file in the default editor")
.on_disabled_hover_text("Source file metadata missing")
.clicked()
{
ret = Some(DiffViewAction::OpenSourcePath);
}
});
ui.horizontal(|ui| { if let Some(((_section, symbol), symbol_diff)) = right_ctx.and_then(|ctx| {
if ui ctx.symbol_ref.map(|symbol_ref| {
.add_enabled(!state.build_running, egui::Button::new("Build")) (ctx.obj.section_symbol(symbol_ref), ctx.diff.symbol_diff(symbol_ref))
.clicked() })
{ }) {
state.queue_build = true; let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name);
} ui.label(
ui.scope(|ui| { RichText::new(name)
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); .font(appearance.code_font.clone())
if state.build_running { .color(appearance.highlight_color),
ui.colored_label(appearance.replace_color, "Building…"); );
} else { if let Some(match_percent) = symbol_diff.match_percent {
ui.label("Last built:"); ui.label(
let format = RichText::new(format!("{:.0}%", match_percent.floor()))
format_description::parse("[hour]:[minute]:[second]").unwrap(); .font(appearance.code_font.clone())
ui.label( .color(match_color_for_symbol(match_percent, appearance)),
result );
.time }
.to_offset(appearance.utc_offset) } else {
.format(&format) ui.label(
.unwrap(), RichText::new("Missing")
); .font(appearance.code_font.clone())
} .color(appearance.replace_color),
}); );
}); }
}
ui.scope(|ui| { });
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
if let Some(match_percent) = result
.second_obj
.as_ref()
.and_then(|(obj, diff)| {
find_symbol(obj, selected_symbol).map(|sref| {
&diff.sections[sref.section_idx].symbols[sref.symbol_idx]
})
})
.and_then(|symbol| symbol.match_percent)
{
ui.colored_label(
match_color_for_symbol(match_percent, appearance),
format!("{:.0}%", match_percent.floor()),
);
} else {
ui.colored_label(appearance.replace_color, "Missing");
}
ui.label("Diff base:");
});
},
);
},
);
ui.separator();
// Table // 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,246 +344,476 @@ 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);
if let (Some(right_obj), Some(right_symbol_ref)) = (right_obj, right_symbol) { }
asm_col_ui( }
&mut row, } else if column == 1 {
right_obj, if let Some(ctx) = right_ctx {
right_symbol_ref, if let Some(action) =
appearance, asm_col_ui(row, ctx, appearance, ins_view_state, column)
ins_view_state, {
ColumnId::Right, ret = Some(action);
); }
} else { }
empty_col_ui(&mut row); if row.response().clicked() {
} ret = Some(DiffViewAction::ClearDiffHighlight);
if row.response().clicked() { }
ins_view_state.clear_highlight(); }
},
);
} 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( ui.horizontal(|ui| {
Vec2 { x: column_width, y: 100.0 }, if ui.button("⏴ Back").clicked() {
Layout::top_down(Align::Min), ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
|ui| { }
ui.set_width(column_width); ui.separator();
if ui
ui.horizontal(|ui| { .add_enabled(
if ui.button("⏴ Back").clicked() { !state.scratch_running
state.current_view = View::SymbolDiff; && state.scratch_available
} && left_ctx.map_or(false, |ctx| ctx.has_symbol()),
ui.separator(); egui::Button::new("📲 decomp.me"),
if ui )
.add_enabled( .on_hover_text_at_pointer("Create a new scratch on decomp.me (beta)")
!state.scratch_running && state.scratch_available, .on_disabled_hover_text("Scratch configuration missing")
egui::Button::new("📲 decomp.me"), .clicked()
) {
.on_hover_text_at_pointer("Create a new scratch on decomp.me (beta)") if let Some((_section, symbol)) = left_ctx.and_then(|ctx| {
.on_disabled_hover_text("Scratch configuration missing") ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref))
.clicked() }) {
{ ret = Some(DiffViewAction::CreateScratch(symbol.name.clone()));
state.queue_scratch = true; }
} }
}); });
let name = selected_symbol
.demangled_symbol_name
.as_deref()
.unwrap_or(&selected_symbol.symbol_name);
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.colored_label(appearance.highlight_color, name);
ui.label("Diff target:");
});
},
);
if let Some((_section, symbol)) = left_ctx
.and_then(|ctx| ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref)))
{
let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name);
ui.label(
RichText::new(name)
.font(appearance.code_font.clone())
.color(appearance.highlight_color),
);
if right_ctx.map_or(false, |m| m.has_symbol())
&& ui
.button("Change target")
.on_hover_text_at_pointer("Choose a different symbol to use as the target")
.clicked()
{
if let Some(symbol_ref) = state.symbol_state.right_symbol.as_ref() {
ret = Some(DiffViewAction::SelectingLeft(symbol_ref.clone()));
}
}
} else {
ui.label(
RichText::new("Missing")
.font(appearance.code_font.clone())
.color(appearance.replace_color),
);
ui.label(
RichText::new("Choose target symbol")
.font(appearance.code_font.clone())
.color(appearance.highlight_color),
);
}
} else if column == 1 {
// Right column // Right column
ui.allocate_ui_with_layout( ui.horizontal(|ui| {
Vec2 { x: column_width, y: 100.0 }, if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() {
Layout::top_down(Align::Min), ret = Some(DiffViewAction::Build);
|ui| { }
ui.set_width(column_width); ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
if state.build_running {
ui.colored_label(appearance.replace_color, "Building…");
} else {
ui.label("Last built:");
let format = format_description::parse("[hour]:[minute]:[second]").unwrap();
ui.label(
result.time.to_offset(appearance.utc_offset).format(&format).unwrap(),
);
}
});
ui.separator();
if ui
.add_enabled(state.source_path_available, egui::Button::new("🖹 Source file"))
.on_hover_text_at_pointer("Open the source file in the default editor")
.on_disabled_hover_text("Source file metadata missing")
.clicked()
{
ret = Some(DiffViewAction::OpenSourcePath);
}
});
ui.horizontal(|ui| { if let Some(((_section, symbol), symbol_diff)) = right_ctx.and_then(|ctx| {
if ui ctx.symbol_ref.map(|symbol_ref| {
.add_enabled(!state.build_running, egui::Button::new("Build")) (ctx.obj.section_symbol(symbol_ref), ctx.diff.symbol_diff(symbol_ref))
.clicked() })
{ }) {
state.queue_build = true; let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name);
} ui.label(
ui.scope(|ui| { RichText::new(name)
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); .font(appearance.code_font.clone())
if state.build_running { .color(appearance.highlight_color),
ui.colored_label(appearance.replace_color, "Building…"); );
} else { ui.horizontal(|ui| {
ui.label("Last built:"); if let Some(match_percent) = symbol_diff.match_percent {
let format = ui.label(
format_description::parse("[hour]:[minute]:[second]").unwrap(); RichText::new(format!("{:.0}%", match_percent.floor()))
ui.label( .font(appearance.code_font.clone())
result .color(match_color_for_symbol(match_percent, appearance)),
.time );
.to_offset(appearance.utc_offset) }
.format(&format) if left_ctx.map_or(false, |m| m.has_symbol()) {
.unwrap(),
);
}
});
ui.separator(); ui.separator();
if ui if ui
.add_enabled( .button("Change base")
state.source_path_available, .on_hover_text_at_pointer(
egui::Button::new("🖹 Source file"), "Choose a different symbol to use as the base",
) )
.on_hover_text_at_pointer("Open the source file in the default editor")
.on_disabled_hover_text("Source file metadata missing")
.clicked() .clicked()
{ {
state.queue_open_source_path = true; if let Some(symbol_ref) = state.symbol_state.left_symbol.as_ref() {
ret = Some(DiffViewAction::SelectingRight(symbol_ref.clone()));
}
} }
}); }
});
ui.scope(|ui| { } else {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); ui.label(
if let Some(match_percent) = result RichText::new("Missing")
.second_obj .font(appearance.code_font.clone())
.as_ref() .color(appearance.replace_color),
.and_then(|(obj, diff)| { );
find_symbol(obj, selected_symbol).map(|sref| { ui.label(
&diff.sections[sref.section_idx].symbols[sref.symbol_idx] RichText::new("Choose base symbol")
}) .font(appearance.code_font.clone())
}) .color(appearance.highlight_color),
.and_then(|symbol| symbol.match_percent) );
{ }
ui.colored_label( }
match_color_for_symbol(match_percent, appearance), });
format!("{:.0}%", match_percent.floor()),
);
} else {
ui.colored_label(appearance.replace_color, "Missing");
}
ui.label("Diff base:");
});
},
);
},
);
ui.separator();
// Table // 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)) asm_table_ui(
.columns(Column::exact(column_width).clip(true), 2) ui,
.resizable(false) available_width,
.auto_shrink([false, false]) left_ctx,
.min_scrolled_height(available_height) right_ctx,
.sense(Sense::click()); appearance,
asm_table_ui( &state.function_state,
table, &state.symbol_state,
result.first_obj.as_ref(), )
result.second_obj.as_ref(), })
selected_symbol, .inner
appearance, {
&mut state.function_state, 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;

File diff suppressed because it is too large Load Diff