xenia-vfs/xex: cache full disc tree; instrument XEX load
DiscImageDevice now walks the GDFX tree at open() and caches every
file/dir entry by full relative path; the previous root-only scan
returned ENOENT for any path under a subdirectory (dat/tables.pak,
media/x.wav). Lookups become O(n) over the cached vec.
xex::load_image gains a tracing span plus per-load metrics
(xex.load_image_ms histogram, xex.bytes_{in,out} counters) so the
observability subscriber the app installs can see decompression cost.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -229,7 +229,7 @@ fn parse_import_libraries(data: &[u8], headers: &[Xex2OptionalHeader]) -> Vec<Im
|
||||
let name = std::str::from_utf8(&data[start..end]).unwrap_or("???").to_string();
|
||||
spos += name.len() + 1;
|
||||
// 4-byte alignment
|
||||
if spos % 4 != 0 { spos += 4 - (spos % 4); }
|
||||
if !spos.is_multiple_of(4) { spos += 4 - (spos % 4); }
|
||||
strings.push(name);
|
||||
}
|
||||
|
||||
@@ -359,21 +359,31 @@ pub fn get_stack_size(header: &Xex2Header) -> u32 {
|
||||
|
||||
/// Load the XEX image data into a flat buffer (decompressing if needed).
|
||||
/// Returns the decompressed image bytes ready to map into guest memory.
|
||||
#[tracing::instrument(skip_all, fields(bytes = data.len()))]
|
||||
pub fn load_image(data: &[u8], header: &Xex2Header) -> io::Result<Vec<u8>> {
|
||||
let started = std::time::Instant::now();
|
||||
let source = &data[header.header_size as usize..];
|
||||
let bytes_in = source.len();
|
||||
|
||||
match &header.file_format_info {
|
||||
let output = match &header.file_format_info {
|
||||
Some(info) if info.compression_type == COMPRESSION_BASIC => {
|
||||
load_basic_compressed(source, info)
|
||||
tracing::debug!(compression = "basic", "decompressing");
|
||||
load_basic_compressed(source, info)?
|
||||
}
|
||||
Some(info) if info.compression_type == COMPRESSION_NORMAL => {
|
||||
load_normal_compressed(source, info, header)
|
||||
tracing::debug!(compression = "normal/LZX", "decompressing");
|
||||
load_normal_compressed(source, info, header)?
|
||||
}
|
||||
_ => {
|
||||
// Uncompressed (or no format info = treat as uncompressed)
|
||||
Ok(source.to_vec())
|
||||
}
|
||||
}
|
||||
_ => source.to_vec(),
|
||||
};
|
||||
|
||||
let elapsed_ms = started.elapsed().as_millis() as f64;
|
||||
metrics::histogram!("xex.load_image_ms").record(elapsed_ms);
|
||||
metrics::counter!("xex.bytes_in").increment(bytes_in as u64);
|
||||
metrics::counter!("xex.bytes_out").increment(output.len() as u64);
|
||||
let ratio = if bytes_in == 0 { 0.0 } else { output.len() as f64 / bytes_in as f64 };
|
||||
tracing::info!(bytes_in, bytes_out = output.len(), ratio, elapsed_ms, "image loaded");
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
/// Load basic compressed image data.
|
||||
@@ -425,6 +435,7 @@ const XEX2_RETAIL_KEY: [u8; 16] = [
|
||||
const XEX2_DEVKIT_KEY: [u8; 16] = [0u8; 16];
|
||||
|
||||
/// AES-128-CBC decryption with zero IV (matching Xbox 360 XEX decryption).
|
||||
#[tracing::instrument(skip_all, fields(bytes = input.len()))]
|
||||
fn aes_decrypt_cbc(key: &[u8; 16], input: &[u8]) -> Vec<u8> {
|
||||
let cipher = Aes128::new(key.into());
|
||||
let mut output = vec![0u8; input.len()];
|
||||
@@ -523,6 +534,7 @@ fn deblock(input: &[u8], first_block_size: u32) -> io::Result<Vec<u8>> {
|
||||
|
||||
/// Load normal (LZX) compressed image data.
|
||||
/// Pipeline: decrypt → de-block → LZX decompress (pure Rust)
|
||||
#[tracing::instrument(skip_all, fields(bytes_in = source.len()))]
|
||||
fn load_normal_compressed(source: &[u8], info: &FileFormatInfo, header: &Xex2Header) -> io::Result<Vec<u8>> {
|
||||
let uncompressed_size = header.security_info.as_ref()
|
||||
.map(|s| s.image_size as usize)
|
||||
|
||||
Reference in New Issue
Block a user