[Rust] Implement FPU/VMX128 opcodes, XEX LZX decompression, XISO browsing, and memory safety
Some checks failed
Orchestrator / Commit Message Validation (push) Has been skipped
Orchestrator / Lint (push) Successful in 2m5s
Orchestrator / Windows (x86-64) (push) Failing after 6m13s
Orchestrator / Linux (x86-64) (push) Failing after 20m59s
Orchestrator / Create Release (push) Has been skipped

Major additions to the xenia-rs Rust port:

- CPU: ~170 new PPC opcode implementations (FPU, VMX128, 64-bit ALU, load/store variants)
- XEX: Full LZX (normal) decompression pipeline with AES-128-CBC decryption via mspack FFI
- XEX: Parse file format info, import libraries, and security info AES key from headers
- VFS: Rewrite XISO disc image to use seek-based I/O (handles 7GB+ images without loading into memory)
- App: Auto-detect ISO files and extract default.xex for all CLI commands
- App: Add `info` and `browse` CLI subcommands
- Kernel: Expand HLE exports from 14 to 40 stubs (memory, threading, TLS, I/O, video)
- Memory: Add bounds checking on all guest memory accesses to prevent segfaults
- Types: Add Vec128 array-based accessors (from_u32x4_array, from_f32x4_array, etc.)

Tested against Project Sylpheed (USA) disc image - all four CLI commands
(browse, info, disasm, exec) work correctly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-04-12 21:32:46 +02:00
parent 06a23212fb
commit a519c76800
16 changed files with 2509 additions and 51 deletions

View File

@@ -55,8 +55,23 @@ fn main() -> Result<()> {
}
}
/// Load XEX data from a path. If the path is an ISO, extract default.xex from it.
fn load_xex_data(path: &str) -> Result<Vec<u8>> {
let lower = path.to_lowercase();
if lower.ends_with(".iso") || lower.ends_with(".xiso") {
use xenia_vfs::VfsDevice;
println!("Detected disc image, extracting default.xex...");
let disc = xenia_vfs::disc_image::DiscImageDevice::open("disc", std::path::Path::new(path))
.map_err(|e| anyhow::anyhow!("Failed to open disc image: {}", e))?;
disc.read_file("default.xex")
.map_err(|e| anyhow::anyhow!("Failed to extract default.xex from disc image: {}", e))
} else {
Ok(std::fs::read(path)?)
}
}
fn cmd_info(path: &str) -> Result<()> {
let data = std::fs::read(path)?;
let data = load_xex_data(path)?;
let header = xenia_xex::loader::parse_xex2_header(&data)?;
println!("=== XEX2 Header ===");
@@ -85,11 +100,34 @@ fn cmd_info(path: &str) -> Result<()> {
println!("Page Descs: {}", sec.page_descriptors.len());
}
if let Some(ref ffi) = header.file_format_info {
println!("\n=== File Format ===");
println!("Encryption: {}", match ffi.encryption_type {
0 => "None", 1 => "Normal (AES)", _ => "Unknown"
});
println!("Compression: {}", match ffi.compression_type {
0 => "None", 1 => "Basic", 2 => "Normal (LZX)", _ => "Unknown"
});
if !ffi.basic_blocks.is_empty() {
println!("Basic blocks: {}", ffi.basic_blocks.len());
}
if ffi.normal_window_size != 0 {
println!("LZX Window: {:#x}", ffi.normal_window_size);
}
}
if !header.import_libraries.is_empty() {
println!("\n=== Import Libraries ===");
for lib in &header.import_libraries {
println!(" {} (v{:#010x}, {} ordinals)", lib.name, lib.version_cur, lib.ordinals.len());
}
}
Ok(())
}
fn cmd_disasm(path: &str, count: usize) -> Result<()> {
let data = std::fs::read(path)?;
let data = load_xex_data(path)?;
let header = xenia_xex::loader::parse_xex2_header(&data)?;
let entry = xenia_xex::loader::get_entry_point(&header)
@@ -98,24 +136,27 @@ fn cmd_disasm(path: &str, count: usize) -> Result<()> {
.ok_or_else(|| anyhow::anyhow!("No image base found in XEX2 header"))?;
println!("Entry point: {:#010x}, Image base: {:#010x}", entry, base);
// Load and decompress the image
let image_data = xenia_xex::loader::load_image(&data, &header)?;
println!("Image loaded: {} bytes decompressed", image_data.len());
println!("Disassembly from entry point ({} instructions):\n", count);
// For now, disassemble from the raw file data at the entry offset
let entry_offset = (entry - base) as usize + header.header_size as usize;
if entry_offset + count * 4 <= data.len() {
let block = xenia_cpu::disasm::disassemble_block(&data[entry_offset..], entry, count);
let entry_offset = (entry - base) as usize;
if entry_offset + count * 4 <= image_data.len() {
let block = xenia_cpu::disasm::disassemble_block(&image_data[entry_offset..], entry, count);
for (addr, text) in block {
println!(" {:#010x}: {}", addr, text);
}
} else {
println!(" (entry point offset {:#x} is outside file bounds)", entry_offset);
println!(" (entry point offset {:#x} is outside image bounds, image is {:#x} bytes)", entry_offset, image_data.len());
}
Ok(())
}
fn cmd_exec(path: &str, max_instructions: u64) -> Result<()> {
let data = std::fs::read(path)?;
let data = load_xex_data(path)?;
let header = xenia_xex::loader::parse_xex2_header(&data)?;
let entry = xenia_xex::loader::get_entry_point(&header)
@@ -123,21 +164,38 @@ fn cmd_exec(path: &str, max_instructions: u64) -> Result<()> {
let base = xenia_xex::loader::get_image_base(&header)
.ok_or_else(|| anyhow::anyhow!("No image base found"))?;
// Print compression info
if let Some(ref ffi) = header.file_format_info {
println!("Compression: {} (encryption: {})",
match ffi.compression_type {
0 => "none", 1 => "basic", 2 => "normal (LZX)", _ => "unknown"
},
match ffi.encryption_type {
0 => "none", 1 => "normal (AES)", _ => "unknown"
});
}
if !header.import_libraries.is_empty() {
println!("Import libraries:");
for lib in &header.import_libraries {
println!(" {} ({} ordinals)", lib.name, lib.ordinals.len());
}
}
println!("Loading XEX: entry={:#010x} base={:#010x}", entry, base);
// Allocate guest memory
let mut mem = xenia_memory::GuestMemory::new()
.map_err(|e| anyhow::anyhow!("Failed to allocate guest memory: {}", e))?;
// Map the XEX image into guest memory
let image_data = &data[header.header_size as usize..];
// Load and decompress the XEX image
let image_data = xenia_xex::loader::load_image(&data, &header)?;
let alloc_size = ((image_data.len() + 4095) & !4095) as u32;
mem.alloc(
base,
alloc_size,
xenia_memory::page_table::MemoryProtect::READ | xenia_memory::page_table::MemoryProtect::WRITE,
).map_err(|e| anyhow::anyhow!("Failed to allocate guest memory region: {}", e))?;
mem.write_bulk(base, image_data);
mem.write_bulk(base, &image_data);
// Allocate stack (1MB at 0x70000000)
let stack_base = 0x7000_0000u32;
@@ -174,6 +232,12 @@ fn cmd_exec(path: &str, max_instructions: u64) -> Result<()> {
break;
}
// Check if PC is in mapped memory before trying to execute
if !mem.is_mapped(ctx.pc) {
println!("[{:>8}] FAULT: PC {:#010x} is in unmapped memory", instruction_count, ctx.pc);
break;
}
// Pre-step debugger
debugger.pre_step(&ctx, &mem);