Compare commits

...

21 Commits

Author SHA1 Message Date
Luke Street 8acaaf528c Version v2.2.0 2024-09-29 12:26:41 -06:00
Luke Street 6e881a74e1 Remove armv7-unknown-linux-musleabi build 2024-09-29 11:57:13 -06:00
Luke Street cc1bc44e69 Use mimalloc when targeting musl 2024-09-29 11:52:04 -06:00
Luke Street c7b85518ab Rework jobs view & error handling improvements
Job status is now shown in the top menu bar,
with a new Jobs window that can be toggled.

Build and diff errors are now handled more
gracefully.

Fixes #40
2024-09-28 12:14:20 -06:00
Luke Street bb039a1445 Add "Open source file" option
Available when right-clicking an object in
the object list or when viewing an object

Resolves #99
2024-09-28 11:50:56 -06:00
Luke Street 8fc142d316 Debounce loaded object modification check
Before, this was running 2 fs::metadata
calls every frame. We don't need to do it
nearly that often, so now it only checks
once every 500ms.

This required refactoring AppConfig into
a separate AppState that holds transient
runtime state along with the loaded
AppConfig.
2024-09-28 10:55:22 -06:00
Luke Street b0123b3f83 Improve build log message when command doesn't exist
Before, it didn't include the actual command
that was attempted to run.
2024-09-28 10:55:09 -06:00
Luke Street 2ec17aee9b Improve config read/write performance
We were accidentally using unbuffered readers
and writers before, leading to long pauses on
the main thread on slow filesystems. (e.g.
FUSE or WSL)
2024-09-28 10:54:54 -06:00
Luke Street ec9731e1e5 Set app_id in eframe NativeOptions
Fixes missing WM_CLASS on Wayland
2024-09-28 10:53:58 -06:00
OndrikB a06382c27e Disambiguate dummy symbols (#107)
* Disambiguate dummy symbols

* Small formatting improvement

* Put HashMap logic into symbol creation
2024-09-27 00:33:36 -06:00
Luke Street e013638c5a clippy fixes 2024-09-27 00:30:30 -06:00
Luke Street 70ab82f1f7 gui: Highlight registers in columns separately
This matches the behavior of decomp.me and the
CLI.

Resolves #71
2024-09-27 00:27:36 -06:00
Luke Street c5896689cf Use ppc750cl Opcode::from 2024-09-27 00:12:21 -06:00
Luke Street 67719dd93e report: Exclude "hidden" functions
Fixes #111
2024-09-27 00:12:21 -06:00
Luke Street 258e141017 Upgrade all dependencies 2024-09-27 00:12:16 -06:00
Luke Street dbdda55065 Add Report::split
A hack for supporting games that build
all versions at once.
2024-09-26 23:47:03 -06:00
Steven Casper a43320af1f PPC: Guess reloc data type based on the instruction. (#108)
* Guess reloc data type based on the instruction.

Adds an entry to the reloc tooltip to show the inferred data type
and value.

* Fix clippy warning

* Match on Opcode rather than mnemonic string
2024-09-25 23:45:37 -06:00
Amber Brault 35bbd40f5d Actually update extab stuff (#110)
* Update cwextab

* Update

* Update ppc.rs

* Make fmt shut up
2024-09-24 09:16:14 -06:00
Amber Brault c1cb4b0b19 Update cwextab (#109) 2024-09-23 21:24:33 -06:00
Luke Street 2379853faa Remove unused imports 2024-09-10 23:29:22 -06:00
Luke Street 5e1aff180f Remove vergen / GIT_COMMIT_SHA handling 2024-09-10 23:22:40 -06:00
29 changed files with 1537 additions and 1075 deletions
-5
View File
@@ -110,11 +110,6 @@ jobs:
name: linux-aarch64
build: zigbuild
features: default
- platform: ubuntu-latest
target: armv7-unknown-linux-musleabi
name: linux-armv7l
build: zigbuild
features: default
- platform: windows-latest
target: i686-pc-windows-msvc
name: windows-x86
Generated
+521 -629
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -13,7 +13,7 @@ strip = "debuginfo"
codegen-units = 1
[workspace.package]
version = "2.0.0"
version = "2.2.0"
authors = ["Luke Street <luke@street.dev>"]
edition = "2021"
license = "MIT OR Apache-2.0"
+3 -1
View File
@@ -11,7 +11,6 @@ description = """
A local diffing tool for decompilation projects.
"""
publish = false
build = "build.rs"
[dependencies]
anyhow = "1.0"
@@ -29,3 +28,6 @@ supports-color = "3.0"
time = { version = "0.3", features = ["formatting", "local-offset"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
[target.'cfg(target_env = "musl")'.dependencies]
mimalloc = "0.1"
-9
View File
@@ -1,9 +0,0 @@
fn main() {
let output = std::process::Command::new("git")
.args(["rev-parse", "HEAD"])
.output()
.expect("Failed to execute git");
let rev = String::from_utf8(output.stdout).expect("Failed to parse git output");
println!("cargo:rustc-env=GIT_COMMIT_SHA={rev}");
println!("cargo:rustc-rerun-if-changed=.git/HEAD");
}
+1 -2
View File
@@ -31,10 +31,9 @@ where T: FromArgs
Ok(v) => {
if v.version {
println!(
"{} {} {}",
"{} {}",
command_name.first().unwrap_or(&""),
env!("CARGO_PKG_VERSION"),
env!("GIT_COMMIT_SHA"),
);
std::process::exit(0);
} else {
+1 -1
View File
@@ -237,7 +237,7 @@ fn report_object(
}
for (symbol, symbol_diff) in section.symbols.iter().zip(&section_diff.symbols) {
if symbol.size == 0 {
if symbol.size == 0 || symbol.flags.0.contains(ObjSymbolFlags::Hidden) {
continue;
}
if let Some(existing_functions) = &mut existing_functions {
+6
View File
@@ -2,6 +2,12 @@ mod argp_version;
mod cmd;
mod util;
// musl's allocator is very slow, so use mimalloc when targeting musl.
// Otherwise, use the system allocator to avoid extra code size.
#[cfg(target_env = "musl")]
#[global_allocator]
static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc;
use std::{env, ffi::OsStr, fmt::Display, path::PathBuf, str::FromStr};
use anyhow::{Error, Result};
+4 -1
View File
@@ -27,6 +27,9 @@ arm = ["any-arch", "cpp_demangle", "unarm", "arm-attr"]
bindings = ["serde_json", "prost", "pbjson"]
wasm = ["bindings", "console_error_panic_hook", "console_log"]
[package.metadata.docs.rs]
features = ["all"]
[dependencies]
anyhow = "1.0"
byteorder = "1.5"
@@ -57,7 +60,7 @@ gimli = { version = "0.31", default-features = false, features = ["read-all"], o
# ppc
cwdemangle = { version = "1.0", optional = true }
cwextab = { version = "0.2", optional = true }
cwextab = { version = "0.3", optional = true }
ppc750cl = { version = "0.3", optional = true }
# mips
+100 -1
View File
@@ -1,11 +1,13 @@
use std::{borrow::Cow, collections::BTreeMap};
use std::{borrow::Cow, collections::BTreeMap, ffi::CStr};
use anyhow::{bail, Result};
use byteorder::ByteOrder;
use object::{Architecture, File, Object, ObjectSymbol, Relocation, RelocationFlags, Symbol};
use crate::{
diff::DiffObjConfig,
obj::{ObjIns, ObjReloc, ObjSection},
util::ReallySigned,
};
#[cfg(feature = "arm")]
@@ -17,6 +19,97 @@ pub mod ppc;
#[cfg(feature = "x86")]
pub mod x86;
/// Represents the type of data associated with an instruction
pub enum DataType {
Int8,
Int16,
Int32,
Int64,
Int128,
Float,
Double,
Bytes,
String,
}
impl DataType {
pub fn display_bytes<Endian: ByteOrder>(&self, bytes: &[u8]) -> Option<String> {
if self.required_len().is_some_and(|l| bytes.len() < l) {
return None;
}
match self {
DataType::Int8 => {
let i = i8::from_ne_bytes(bytes.try_into().unwrap());
if i < 0 {
format!("Int8: {:#x} ({:#x})", i, ReallySigned(i))
} else {
format!("Int8: {:#x}", i)
}
}
DataType::Int16 => {
let i = Endian::read_i16(bytes);
if i < 0 {
format!("Int16: {:#x} ({:#x})", i, ReallySigned(i))
} else {
format!("Int16: {:#x}", i)
}
}
DataType::Int32 => {
let i = Endian::read_i32(bytes);
if i < 0 {
format!("Int32: {:#x} ({:#x})", i, ReallySigned(i))
} else {
format!("Int32: {:#x}", i)
}
}
DataType::Int64 => {
let i = Endian::read_i64(bytes);
if i < 0 {
format!("Int64: {:#x} ({:#x})", i, ReallySigned(i))
} else {
format!("Int64: {:#x}", i)
}
}
DataType::Int128 => {
let i = Endian::read_i128(bytes);
if i < 0 {
format!("Int128: {:#x} ({:#x})", i, ReallySigned(i))
} else {
format!("Int128: {:#x}", i)
}
}
DataType::Float => {
format!("Float: {}", Endian::read_f32(bytes))
}
DataType::Double => {
format!("Double: {}", Endian::read_f64(bytes))
}
DataType::Bytes => {
format!("Bytes: {:#?}", bytes)
}
DataType::String => {
format!("String: {:?}", CStr::from_bytes_until_nul(bytes).ok()?)
}
}
.into()
}
fn required_len(&self) -> Option<usize> {
match self {
DataType::Int8 => Some(1),
DataType::Int16 => Some(2),
DataType::Int32 => Some(4),
DataType::Int64 => Some(8),
DataType::Int128 => Some(16),
DataType::Float => Some(4),
DataType::Double => Some(8),
DataType::Bytes => None,
DataType::String => None,
}
}
}
pub trait ObjArch: Send + Sync {
fn process_code(
&self,
@@ -42,6 +135,12 @@ pub trait ObjArch: Send + Sync {
fn symbol_address(&self, symbol: &Symbol) -> u64 { symbol.address() }
fn guess_data_type(&self, _instruction: &ObjIns) -> Option<DataType> { None }
fn display_data_type(&self, _ty: DataType, bytes: &[u8]) -> Option<String> {
Some(format!("Bytes: {:#x?}", bytes))
}
// Downcast methods
#[cfg(feature = "ppc")]
fn ppc(&self) -> Option<&ppc::ObjArchPpc> { None }
+38 -5
View File
@@ -1,15 +1,16 @@
use std::{borrow::Cow, collections::BTreeMap};
use anyhow::{bail, ensure, Result};
use byteorder::BigEndian;
use cwextab::{decode_extab, ExceptionTableData};
use object::{
elf, File, Object, ObjectSection, ObjectSymbol, Relocation, RelocationFlags, RelocationTarget,
Symbol, SymbolKind,
};
use ppc750cl::{Argument, InsIter, GPR};
use ppc750cl::{Argument, InsIter, Opcode, GPR};
use crate::{
arch::{ObjArch, ProcessCodeResult},
arch::{DataType, ObjArch, ProcessCodeResult},
diff::DiffObjConfig,
obj::{ObjIns, ObjInsArg, ObjInsArgValue, ObjReloc, ObjSection, ObjSymbol},
};
@@ -186,6 +187,34 @@ impl ObjArch for ObjArchPpc {
}
}
fn guess_data_type(&self, instruction: &ObjIns) -> Option<super::DataType> {
// Always shows the first string of the table. Not ideal, but it's really hard to find
// the actual string being referenced.
if instruction.reloc.as_ref().is_some_and(|r| r.target.name.starts_with("@stringBase")) {
return Some(DataType::String);
}
match Opcode::from(instruction.op as u8) {
Opcode::Lbz | Opcode::Lbzu | Opcode::Lbzux | Opcode::Lbzx => Some(DataType::Int8),
Opcode::Lhz | Opcode::Lhzu | Opcode::Lhzux | Opcode::Lhzx => Some(DataType::Int16),
Opcode::Lha | Opcode::Lhau | Opcode::Lhaux | Opcode::Lhax => Some(DataType::Int16),
Opcode::Lwz | Opcode::Lwzu | Opcode::Lwzux | Opcode::Lwzx => Some(DataType::Int32),
Opcode::Lfs | Opcode::Lfsu | Opcode::Lfsux | Opcode::Lfsx => Some(DataType::Float),
Opcode::Lfd | Opcode::Lfdu | Opcode::Lfdux | Opcode::Lfdx => Some(DataType::Double),
Opcode::Stb | Opcode::Stbu | Opcode::Stbux | Opcode::Stbx => Some(DataType::Int8),
Opcode::Sth | Opcode::Sthu | Opcode::Sthux | Opcode::Sthx => Some(DataType::Int16),
Opcode::Stw | Opcode::Stwu | Opcode::Stwux | Opcode::Stwx => Some(DataType::Int32),
Opcode::Stfs | Opcode::Stfsu | Opcode::Stfsux | Opcode::Stfsx => Some(DataType::Float),
Opcode::Stfd | Opcode::Stfdu | Opcode::Stfdux | Opcode::Stfdx => Some(DataType::Double),
_ => None,
}
}
fn display_data_type(&self, ty: DataType, bytes: &[u8]) -> Option<String> {
ty.display_bytes::<BigEndian>(bytes)
}
fn ppc(&self) -> Option<&ObjArchPpc> { Some(self) }
}
@@ -303,9 +332,13 @@ fn decode_exception_info(file: &File<'_>) -> Result<Option<BTreeMap<usize, Excep
continue;
};
let data = match decode_extab(extab_data) {
Some(decoded_data) => decoded_data,
None => {
log::warn!("Exception table decoding failed for function {}", extab_func_name);
Ok(decoded_data) => decoded_data,
Err(e) => {
log::warn!(
"Exception table decoding failed for function {}, reason: {}",
extab_func_name,
e.to_string()
);
return Ok(None);
}
};
+66
View File
@@ -117,6 +117,72 @@ impl Report {
measures.calc_matched_percent();
}
}
/// Split the report into multiple reports based on progress categories.
/// Assumes progress categories are in the format `version`, `version.category`.
/// This is a hack for projects that generate all versions in a single report.
pub fn split(self) -> Vec<(String, Report)> {
let mut reports = Vec::new();
// Map units to Option to allow taking ownership
let mut units = self.units.into_iter().map(Some).collect::<Vec<_>>();
for category in &self.categories {
if category.id.contains(".") {
// Skip subcategories
continue;
}
fn is_sub_category(id: &str, parent: &str, sep: char) -> bool {
id.starts_with(parent)
&& id.get(parent.len()..).map_or(false, |s| s.starts_with(sep))
}
let mut sub_categories = self
.categories
.iter()
.filter(|c| is_sub_category(&c.id, &category.id, '.'))
.cloned()
.collect::<Vec<_>>();
// Remove category prefix
for sub_category in &mut sub_categories {
sub_category.id = sub_category.id[category.id.len() + 1..].to_string();
}
let mut sub_units = units
.iter_mut()
.filter_map(|opt| {
let unit = opt.as_mut()?;
let metadata = unit.metadata.as_ref()?;
if metadata.progress_categories.contains(&category.id) {
opt.take()
} else {
None
}
})
.collect::<Vec<_>>();
for sub_unit in &mut sub_units {
// Remove leading version/ from unit name
if let Some(name) =
sub_unit.name.strip_prefix(&category.id).and_then(|s| s.strip_prefix('/'))
{
sub_unit.name = name.to_string();
}
// Filter progress categories
let Some(metadata) = sub_unit.metadata.as_mut() else {
continue;
};
metadata.progress_categories = metadata
.progress_categories
.iter()
.filter(|c| is_sub_category(c, &category.id, '.'))
.map(|c| c[category.id.len() + 1..].to_string())
.collect();
}
reports.push((category.id.clone(), Report {
measures: category.measures,
units: sub_units,
version: self.version,
categories: sub_categories,
}));
}
reports
}
}
impl Measures {
+9 -4
View File
@@ -1,6 +1,6 @@
use std::{
fs::File,
io::Read,
io::{BufReader, Read},
path::{Path, PathBuf},
};
@@ -124,6 +124,10 @@ impl ProjectObject {
pub fn hidden(&self) -> bool {
self.metadata.as_ref().and_then(|m| m.auto_generated).unwrap_or(false)
}
pub fn source_path(&self) -> Option<&String> {
self.metadata.as_ref().and_then(|m| m.source_path.as_ref())
}
}
#[derive(Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
@@ -156,7 +160,7 @@ pub struct ProjectConfigInfo {
pub fn try_project_config(dir: &Path) -> Option<(Result<ProjectConfig>, ProjectConfigInfo)> {
for filename in CONFIG_FILENAMES.iter() {
let config_path = dir.join(filename);
let Ok(mut file) = File::open(&config_path) else {
let Ok(file) = File::open(&config_path) else {
continue;
};
let metadata = file.metadata();
@@ -165,9 +169,10 @@ pub fn try_project_config(dir: &Path) -> Option<(Result<ProjectConfig>, ProjectC
continue;
}
let ts = FileTime::from_last_modification_time(&metadata);
let mut reader = BufReader::new(file);
let mut result = match filename.contains("json") {
true => read_json_config(&mut file),
false => read_yml_config(&mut file),
true => read_json_config(&mut reader),
false => read_yml_config(&mut reader),
};
if let Ok(config) = &result {
// Validate min_version if present
+1
View File
@@ -126,6 +126,7 @@ pub struct ObjSymbol {
pub virtual_address: Option<u64>,
/// Original index in object symbol table
pub original_index: Option<usize>,
pub bytes: Vec<u8>,
}
pub struct ObjInfo {
+37 -4
View File
@@ -1,4 +1,10 @@
use std::{collections::HashSet, fs, io::Cursor, mem::size_of, path::Path};
use std::{
collections::{HashMap, HashSet},
fs,
io::Cursor,
mem::size_of,
path::Path,
};
use anyhow::{anyhow, bail, ensure, Context, Result};
use filetime::FileTime;
@@ -78,6 +84,16 @@ fn to_obj_symbol(
let virtual_address = split_meta
.and_then(|m| m.virtual_addresses.as_ref())
.and_then(|v| v.get(symbol.index().0).cloned());
let bytes = symbol
.section_index()
.and_then(|idx| obj_file.section_by_index(idx).ok())
.and_then(|section| section.data().ok())
.and_then(|data| {
data.get(section_address as usize..(section_address + symbol.size()) as usize)
})
.unwrap_or(&[]);
Ok(ObjSymbol {
name: name.to_string(),
demangled_name,
@@ -89,6 +105,7 @@ fn to_obj_symbol(
addend,
virtual_address,
original_index: Some(symbol.index().0),
bytes: bytes.to_vec(),
})
}
@@ -136,6 +153,7 @@ fn symbols_by_section(
obj_file: &File<'_>,
section: &ObjSection,
split_meta: Option<&SplitMeta>,
name_counts: &mut HashMap<String, u32>,
) -> Result<Vec<ObjSymbol>> {
let mut result = Vec::<ObjSymbol>::new();
for symbol in obj_file.symbols() {
@@ -168,8 +186,14 @@ fn symbols_by_section(
}
if result.is_empty() {
// Dummy symbol for empty sections
*name_counts.entry(section.name.clone()).or_insert(0) += 1;
let current_count: u32 = *name_counts.get(&section.name).unwrap();
result.push(ObjSymbol {
name: format!("[{}]", section.name),
name: if current_count > 1 {
format!("[{} ({})]", section.name, current_count)
} else {
format!("[{}]", section.name)
},
demangled_name: None,
address: 0,
section_address: 0,
@@ -179,6 +203,7 @@ fn symbols_by_section(
addend: 0,
virtual_address: None,
original_index: None,
bytes: Vec::new(),
});
}
Ok(result)
@@ -239,6 +264,7 @@ fn find_section_symbol(
addend: offset_addr as i64,
virtual_address: None,
original_index: None,
bytes: Vec::new(),
})
}
@@ -521,6 +547,7 @@ fn update_combined_symbol(symbol: ObjSymbol, address_change: i64) -> Result<ObjS
None
},
original_index: symbol.original_index,
bytes: symbol.bytes,
})
}
@@ -621,9 +648,15 @@ pub fn parse(data: &[u8], config: &DiffObjConfig) -> Result<ObjInfo> {
let arch = new_arch(&obj_file)?;
let split_meta = split_meta(&obj_file)?;
let mut sections = filter_sections(&obj_file, split_meta.as_ref())?;
let mut name_counts: HashMap<String, u32> = HashMap::new();
for section in &mut sections {
section.symbols =
symbols_by_section(arch.as_ref(), &obj_file, section, split_meta.as_ref())?;
section.symbols = symbols_by_section(
arch.as_ref(),
&obj_file,
section,
split_meta.as_ref(),
&mut name_counts,
)?;
section.relocations =
relocations_by_section(arch.as_ref(), &obj_file, section, split_meta.as_ref())?;
}
+10 -10
View File
@@ -29,10 +29,10 @@ bytes = "1.7"
cfg-if = "1.0"
const_format = "0.2"
cwdemangle = "1.0"
cwextab = "0.2"
cwextab = "0.3.1"
dirs = "5.0"
egui = "0.28"
egui_extras = "0.28"
egui = "0.29"
egui_extras = "0.29"
filetime = "0.2"
float-ord = "0.3"
font-kit = "0.14"
@@ -40,10 +40,11 @@ globset = { version = "0.4", features = ["serde1"] }
log = "0.4"
notify = { git = "https://github.com/notify-rs/notify", rev = "128bf6230c03d39dbb7f301ff7b20e594e34c3a2" }
objdiff-core = { path = "../objdiff-core", features = ["all"] }
open = "5.3"
png = "0.17"
pollster = "0.3"
regex = "1.10"
rfd = { version = "0.14" } #, default-features = false, features = ['xdg-portal']
rfd = { version = "0.15" } #, default-features = false, features = ['xdg-portal']
rlwinmdec = "1.0"
ron = "0.8"
serde = { version = "1.0", features = ["derive"] }
@@ -55,7 +56,7 @@ time = { version = "0.3", features = ["formatting", "local-offset"] }
# Keep version in sync with egui
[dependencies.eframe]
version = "0.28"
version = "0.29"
features = [
"default_fonts",
"persistence",
@@ -66,7 +67,7 @@ default-features = false
# Keep version in sync with eframe
[dependencies.wgpu]
version = "0.20"
version = "22.1"
features = [
"dx12",
"metal",
@@ -89,9 +90,6 @@ self_update = "0.41"
path-slash = "0.2"
winapi = "0.3"
[target.'cfg(windows)'.build-dependencies]
winres = "0.1"
[target.'cfg(unix)'.dependencies]
exec = "0.3"
@@ -106,4 +104,6 @@ tracing-wasm = "0.2"
[build-dependencies]
anyhow = "1.0"
vergen-gitcl = { version = "1.0", features = ["build", "cargo"] }
[target.'cfg(windows)'.build-dependencies]
tauri-winres = "0.1"
+5 -7
View File
@@ -1,14 +1,12 @@
use anyhow::Result;
use vergen_gitcl::{BuildBuilder, CargoBuilder, Emitter, GitclBuilder};
fn main() -> Result<()> {
#[cfg(windows)]
{
winres::WindowsResource::new().set_icon("assets/icon.ico").compile()?;
let mut res = tauri_winres::WindowsResource::new();
res.set_icon("assets/icon.ico");
res.set_language(0x0409); // US English
res.compile()?;
}
Emitter::default()
.add_instructions(&BuildBuilder::all_build()?)?
.add_instructions(&CargoBuilder::all_cargo()?)?
.add_instructions(&GitclBuilder::all_git()?)?
.emit()
Ok(())
}
+131 -111
View File
@@ -7,6 +7,7 @@ use std::{
atomic::{AtomicBool, Ordering},
Arc, Mutex, RwLock,
},
time::Instant,
};
use filetime::FileTime;
@@ -39,7 +40,7 @@ use crate::{
frame_history::FrameHistory,
function_diff::function_diff_ui,
graphics::{graphics_window, GraphicsConfig, GraphicsViewState},
jobs::jobs_ui,
jobs::{jobs_menu_ui, jobs_window},
rlwinm::{rlwinm_decode_window, RlwinmDecodeViewState},
symbol_diff::{symbol_diff_ui, DiffViewState, View},
},
@@ -61,6 +62,7 @@ pub struct ViewState {
pub show_arch_config: bool,
pub show_debug: bool,
pub show_graphics: bool,
pub show_jobs: bool,
}
/// The configuration for a single object file.
@@ -72,6 +74,7 @@ pub struct ObjectConfig {
pub reverse_fn_order: Option<bool>,
pub complete: Option<bool>,
pub scratch: Option<ScratchConfig>,
pub source_path: Option<String>,
}
#[inline]
@@ -82,6 +85,36 @@ fn default_watch_patterns() -> Vec<Glob> {
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
}
pub struct AppState {
pub config: AppConfig,
pub objects: Vec<ProjectObject>,
pub object_nodes: Vec<ProjectObjectNode>,
pub watcher_change: bool,
pub config_change: bool,
pub obj_change: bool,
pub queue_build: bool,
pub queue_reload: bool,
pub project_config_info: Option<ProjectConfigInfo>,
pub last_mod_check: Instant,
}
impl Default for AppState {
fn default() -> Self {
Self {
config: Default::default(),
objects: vec![],
object_nodes: vec![],
watcher_change: false,
config_change: false,
obj_change: false,
queue_build: false,
queue_reload: false,
project_config_info: None,
last_mod_check: Instant::now(),
}
}
}
#[derive(Clone, serde::Deserialize, serde::Serialize)]
pub struct AppConfig {
// TODO: https://github.com/ron-rs/ron/pull/455
@@ -116,23 +149,6 @@ pub struct AppConfig {
pub recent_projects: Vec<PathBuf>,
#[serde(default)]
pub diff_obj_config: DiffObjConfig,
#[serde(skip)]
pub objects: Vec<ProjectObject>,
#[serde(skip)]
pub object_nodes: Vec<ProjectObjectNode>,
#[serde(skip)]
pub watcher_change: bool,
#[serde(skip)]
pub config_change: bool,
#[serde(skip)]
pub obj_change: bool,
#[serde(skip)]
pub queue_build: bool,
#[serde(skip)]
pub queue_reload: bool,
#[serde(skip)]
pub project_config_info: Option<ProjectConfigInfo>,
}
impl Default for AppConfig {
@@ -153,30 +169,22 @@ impl Default for AppConfig {
watch_patterns: DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect(),
recent_projects: vec![],
diff_obj_config: Default::default(),
objects: vec![],
object_nodes: vec![],
watcher_change: false,
config_change: false,
obj_change: false,
queue_build: false,
queue_reload: false,
project_config_info: None,
}
}
}
impl AppConfig {
impl AppState {
pub fn set_project_dir(&mut self, path: PathBuf) {
self.recent_projects.retain(|p| p != &path);
if self.recent_projects.len() > 9 {
self.recent_projects.truncate(9);
self.config.recent_projects.retain(|p| p != &path);
if self.config.recent_projects.len() > 9 {
self.config.recent_projects.truncate(9);
}
self.recent_projects.insert(0, path.clone());
self.project_dir = Some(path);
self.target_obj_dir = None;
self.base_obj_dir = None;
self.selected_obj = None;
self.build_target = false;
self.config.recent_projects.insert(0, path.clone());
self.config.project_dir = Some(path);
self.config.target_obj_dir = None;
self.config.base_obj_dir = None;
self.config.selected_obj = None;
self.config.build_target = false;
self.objects.clear();
self.object_nodes.clear();
self.watcher_change = true;
@@ -187,33 +195,33 @@ impl AppConfig {
}
pub fn set_target_obj_dir(&mut self, path: PathBuf) {
self.target_obj_dir = Some(path);
self.selected_obj = None;
self.config.target_obj_dir = Some(path);
self.config.selected_obj = None;
self.obj_change = true;
self.queue_build = false;
}
pub fn set_base_obj_dir(&mut self, path: PathBuf) {
self.base_obj_dir = Some(path);
self.selected_obj = None;
self.config.base_obj_dir = Some(path);
self.config.selected_obj = None;
self.obj_change = true;
self.queue_build = false;
}
pub fn set_selected_obj(&mut self, object: ObjectConfig) {
self.selected_obj = Some(object);
self.config.selected_obj = Some(object);
self.obj_change = true;
self.queue_build = false;
}
}
pub type AppConfigRef = Arc<RwLock<AppConfig>>;
pub type AppStateRef = Arc<RwLock<AppState>>;
#[derive(Default)]
pub struct App {
appearance: Appearance,
view_state: ViewState,
config: AppConfigRef,
state: AppStateRef,
modified: Arc<AtomicBool>,
watcher: Option<notify::RecommendedWatcher>,
app_path: Option<PathBuf>,
@@ -241,16 +249,17 @@ impl App {
if let Some(appearance) = eframe::get_value::<Appearance>(storage, APPEARANCE_KEY) {
app.appearance = appearance;
}
if let Some(mut config) = deserialize_config(storage) {
if config.project_dir.is_some() {
config.config_change = true;
config.watcher_change = true;
if let Some(config) = deserialize_config(storage) {
let mut state = AppState { config, ..Default::default() };
if state.config.project_dir.is_some() {
state.config_change = true;
state.watcher_change = true;
}
if config.selected_obj.is_some() {
config.queue_build = true;
if state.config.selected_obj.is_some() {
state.queue_build = true;
}
app.view_state.config_state.queue_check_update = config.auto_update_check;
app.config = Arc::new(RwLock::new(config));
app.view_state.config_state.queue_check_update = state.config.auto_update_check;
app.state = Arc::new(RwLock::new(state));
}
}
app.appearance.init_fonts(&cc.egui_ctx);
@@ -336,8 +345,8 @@ impl App {
jobs.results.append(&mut results);
jobs.clear_finished();
diff_state.pre_update(jobs, &self.config);
config_state.pre_update(jobs, &self.config);
diff_state.pre_update(jobs, &self.state);
config_state.pre_update(jobs, &self.state);
debug_assert!(jobs.results.is_empty());
}
@@ -345,23 +354,23 @@ impl App {
self.appearance.post_update(ctx);
let ViewState { jobs, diff_state, config_state, graphics_state, .. } = &mut self.view_state;
config_state.post_update(ctx, jobs, &self.config);
diff_state.post_update(ctx, jobs, &self.config);
config_state.post_update(ctx, jobs, &self.state);
diff_state.post_update(ctx, jobs, &self.state);
let Ok(mut config) = self.config.write() else {
let Ok(mut state) = self.state.write() else {
return;
};
let config = &mut *config;
let state = &mut *state;
if let Some(info) = &config.project_config_info {
if let Some(info) = &state.project_config_info {
if file_modified(&info.path, info.timestamp) {
config.config_change = true;
state.config_change = true;
}
}
if config.config_change {
config.config_change = false;
match load_project_config(config) {
if state.config_change {
state.config_change = false;
match load_project_config(state) {
Ok(()) => config_state.load_error = None,
Err(e) => {
log::error!("Failed to load project config: {e}");
@@ -370,47 +379,50 @@ impl App {
}
}
if config.watcher_change {
if state.watcher_change {
drop(self.watcher.take());
if let Some(project_dir) = &config.project_dir {
match build_globset(&config.watch_patterns).map_err(anyhow::Error::new).and_then(
|globset| {
if let Some(project_dir) = &state.config.project_dir {
match build_globset(&state.config.watch_patterns)
.map_err(anyhow::Error::new)
.and_then(|globset| {
create_watcher(ctx.clone(), self.modified.clone(), project_dir, globset)
.map_err(anyhow::Error::new)
},
) {
}) {
Ok(watcher) => self.watcher = Some(watcher),
Err(e) => log::error!("Failed to create watcher: {e}"),
}
config.watcher_change = false;
state.watcher_change = false;
}
}
if config.obj_change {
if state.obj_change {
*diff_state = Default::default();
if config.selected_obj.is_some() {
config.queue_build = true;
if state.config.selected_obj.is_some() {
state.queue_build = true;
}
config.obj_change = false;
state.obj_change = false;
}
if self.modified.swap(false, Ordering::Relaxed) && config.rebuild_on_changes {
config.queue_build = true;
if self.modified.swap(false, Ordering::Relaxed) && state.config.rebuild_on_changes {
state.queue_build = true;
}
if let Some(result) = &diff_state.build {
if let Some((obj, _)) = &result.first_obj {
if let (Some(path), Some(timestamp)) = (&obj.path, obj.timestamp) {
if file_modified(path, timestamp) {
config.queue_reload = true;
if state.last_mod_check.elapsed().as_millis() >= 500 {
state.last_mod_check = Instant::now();
if let Some((obj, _)) = &result.first_obj {
if let (Some(path), Some(timestamp)) = (&obj.path, obj.timestamp) {
if file_modified(path, timestamp) {
state.queue_reload = true;
}
}
}
}
if let Some((obj, _)) = &result.second_obj {
if let (Some(path), Some(timestamp)) = (&obj.path, obj.timestamp) {
if file_modified(path, timestamp) {
config.queue_reload = true;
if let Some((obj, _)) = &result.second_obj {
if let (Some(path), Some(timestamp)) = (&obj.path, obj.timestamp) {
if file_modified(path, timestamp) {
state.queue_reload = true;
}
}
}
}
@@ -418,17 +430,20 @@ impl App {
// Don't clear `queue_build` if a build is running. A file may have been modified during
// the build, so we'll start another build after the current one finishes.
if config.queue_build && config.selected_obj.is_some() && !jobs.is_running(Job::ObjDiff) {
jobs.push(start_build(ctx, ObjDiffConfig::from_config(config)));
config.queue_build = false;
config.queue_reload = false;
} else if config.queue_reload && !jobs.is_running(Job::ObjDiff) {
let mut diff_config = ObjDiffConfig::from_config(config);
if state.queue_build
&& state.config.selected_obj.is_some()
&& !jobs.is_running(Job::ObjDiff)
{
jobs.push(start_build(ctx, ObjDiffConfig::from_config(&state.config)));
state.queue_build = false;
state.queue_reload = false;
} else if state.queue_reload && !jobs.is_running(Job::ObjDiff) {
let mut diff_config = ObjDiffConfig::from_config(&state.config);
// Don't build, just reload the current files
diff_config.build_base = false;
diff_config.build_target = false;
jobs.push(start_build(ctx, diff_config));
config.queue_reload = false;
state.queue_reload = false;
}
if graphics_state.should_relaunch {
@@ -453,7 +468,7 @@ impl eframe::App for App {
self.pre_update(ctx);
let Self { config, appearance, view_state, .. } = self;
let Self { state, appearance, view_state, .. } = self;
let ViewState {
jobs,
config_state,
@@ -469,6 +484,7 @@ impl eframe::App for App {
show_arch_config,
show_debug,
show_graphics,
show_jobs,
} = view_state;
frame_history.on_new_frame(ctx.input(|i| i.time), frame.info().cpu_usage);
@@ -485,8 +501,8 @@ impl eframe::App for App {
*show_project_config = !*show_project_config;
ui.close_menu();
}
let recent_projects = if let Ok(guard) = config.read() {
guard.recent_projects.clone()
let recent_projects = if let Ok(guard) = state.read() {
guard.config.recent_projects.clone()
} else {
vec![]
};
@@ -495,12 +511,12 @@ impl eframe::App for App {
} else {
ui.menu_button("Recent Projects…", |ui| {
if ui.button("Clear").clicked() {
config.write().unwrap().recent_projects.clear();
state.write().unwrap().config.recent_projects.clear();
};
ui.separator();
for path in recent_projects {
if ui.button(format!("{}", path.display())).clicked() {
config.write().unwrap().set_project_dir(path);
state.write().unwrap().set_project_dir(path);
ui.close_menu();
}
}
@@ -533,12 +549,12 @@ impl eframe::App for App {
*show_arch_config = !*show_arch_config;
ui.close_menu();
}
let mut config = config.write().unwrap();
let mut state = state.write().unwrap();
let response = ui
.checkbox(&mut config.rebuild_on_changes, "Rebuild on changes")
.checkbox(&mut state.config.rebuild_on_changes, "Rebuild on changes")
.on_hover_text("Automatically re-run the build & diff when files change.");
if response.changed() {
config.watcher_change = true;
state.watcher_change = true;
};
ui.add_enabled(
!diff_state.symbol_state.disable_reverse_fn_order,
@@ -554,7 +570,7 @@ impl eframe::App for App {
);
if ui
.checkbox(
&mut config.diff_obj_config.relax_reloc_diffs,
&mut state.config.diff_obj_config.relax_reloc_diffs,
"Relax relocation diffs",
)
.on_hover_text(
@@ -562,28 +578,32 @@ impl eframe::App for App {
)
.changed()
{
config.queue_reload = true;
state.queue_reload = true;
}
if ui
.checkbox(
&mut config.diff_obj_config.space_between_args,
&mut state.config.diff_obj_config.space_between_args,
"Space between args",
)
.changed()
{
config.queue_reload = true;
state.queue_reload = true;
}
if ui
.checkbox(
&mut config.diff_obj_config.combine_data_sections,
&mut state.config.diff_obj_config.combine_data_sections,
"Combine data sections",
)
.on_hover_text("Combines data sections with equal names.")
.changed()
{
config.queue_reload = true;
state.queue_reload = true;
}
});
ui.separator();
if jobs_menu_ui(ui, jobs, appearance) {
*show_jobs = !*show_jobs;
}
});
});
@@ -603,8 +623,7 @@ impl eframe::App for App {
} else {
egui::SidePanel::left("side_panel").show(ctx, |ui| {
egui::ScrollArea::both().show(ui, |ui| {
config_ui(ui, config, show_project_config, config_state, appearance);
jobs_ui(ui, jobs, appearance);
config_ui(ui, state, show_project_config, config_state, appearance);
});
});
@@ -613,21 +632,22 @@ impl eframe::App for App {
});
}
project_window(ctx, config, show_project_config, config_state, appearance);
project_window(ctx, state, show_project_config, config_state, appearance);
appearance_window(ctx, show_appearance_config, appearance);
demangle_window(ctx, show_demangle, demangle_state, appearance);
rlwinm_decode_window(ctx, show_rlwinm_decode, rlwinm_decode_state, appearance);
arch_config_window(ctx, config, show_arch_config, appearance);
arch_config_window(ctx, state, show_arch_config, appearance);
debug_window(ctx, show_debug, frame_history, appearance);
graphics_window(ctx, show_graphics, frame_history, graphics_state, appearance);
jobs_window(ctx, show_jobs, jobs, appearance);
self.post_update(ctx);
}
/// Called by the frame work to save state before shutdown.
fn save(&mut self, storage: &mut dyn eframe::Storage) {
if let Ok(config) = self.config.read() {
eframe::set_value(storage, CONFIG_KEY, &*config);
if let Ok(state) = self.state.read() {
eframe::set_value(storage, CONFIG_KEY, &state.config);
}
eframe::set_value(storage, APPEARANCE_KEY, &self.appearance);
}
+1
View File
@@ -61,6 +61,7 @@ impl ObjectConfigV0 {
reverse_fn_order: self.reverse_fn_order,
complete: None,
scratch: None,
source_path: None,
}
}
}
+17 -17
View File
@@ -4,7 +4,7 @@ use anyhow::Result;
use globset::Glob;
use objdiff_core::config::{try_project_config, ProjectObject, DEFAULT_WATCH_PATTERNS};
use crate::app::AppConfig;
use crate::app::AppState;
#[derive(Clone)]
pub enum ProjectObjectNode {
@@ -64,30 +64,30 @@ fn build_nodes(
nodes
}
pub fn load_project_config(config: &mut AppConfig) -> Result<()> {
let Some(project_dir) = &config.project_dir else {
pub fn load_project_config(state: &mut AppState) -> Result<()> {
let Some(project_dir) = &state.config.project_dir else {
return Ok(());
};
if let Some((result, info)) = try_project_config(project_dir) {
let project_config = result?;
config.custom_make = project_config.custom_make;
config.custom_args = project_config.custom_args;
config.target_obj_dir = project_config.target_dir.map(|p| project_dir.join(p));
config.base_obj_dir = project_config.base_dir.map(|p| project_dir.join(p));
config.build_base = project_config.build_base;
config.build_target = project_config.build_target;
config.watch_patterns = project_config.watch_patterns.unwrap_or_else(|| {
state.config.custom_make = project_config.custom_make;
state.config.custom_args = project_config.custom_args;
state.config.target_obj_dir = project_config.target_dir.map(|p| project_dir.join(p));
state.config.base_obj_dir = project_config.base_dir.map(|p| project_dir.join(p));
state.config.build_base = project_config.build_base;
state.config.build_target = project_config.build_target;
state.config.watch_patterns = project_config.watch_patterns.unwrap_or_else(|| {
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
});
config.watcher_change = true;
config.objects = project_config.objects;
config.object_nodes = build_nodes(
&config.objects,
state.watcher_change = true;
state.objects = project_config.objects;
state.object_nodes = build_nodes(
&state.objects,
project_dir,
config.target_obj_dir.as_deref(),
config.base_obj_dir.as_deref(),
state.config.target_obj_dir.as_deref(),
state.config.base_obj_dir.as_deref(),
);
config.project_config_info = Some(info);
state.project_config_info = Some(info);
}
Ok(())
}

Some files were not shown because too many files have changed in this diff Show More