From 06a23212fb162916704c63a00ac623e662115d0b Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Sun, 12 Apr 2026 18:25:46 +0200 Subject: [PATCH] Initial xenia-rs --- .gitignore | 3 + RUST_PORT_CODEMAP.md | 164 +++ RUST_PORT_REPORT.md | 1085 ++++++++++++++ xenia-rs/Cargo.lock | 626 ++++++++ xenia-rs/Cargo.toml | 42 + xenia-rs/crates/xenia-app/Cargo.toml | 25 + xenia-rs/crates/xenia-app/src/main.rs | 244 ++++ xenia-rs/crates/xenia-apu/Cargo.toml | 10 + xenia-rs/crates/xenia-apu/src/lib.rs | 16 + xenia-rs/crates/xenia-cpu/Cargo.toml | 12 + xenia-rs/crates/xenia-cpu/src/context.rs | 191 +++ xenia-rs/crates/xenia-cpu/src/decoder.rs | 819 +++++++++++ xenia-rs/crates/xenia-cpu/src/disasm.rs | 276 ++++ xenia-rs/crates/xenia-cpu/src/interpreter.rs | 1293 +++++++++++++++++ xenia-rs/crates/xenia-cpu/src/lib.rs | 9 + xenia-rs/crates/xenia-cpu/src/opcode.rs | 196 +++ xenia-rs/crates/xenia-debugger/Cargo.toml | 12 + .../crates/xenia-debugger/src/breakpoint.rs | 7 + xenia-rs/crates/xenia-debugger/src/lib.rs | 125 ++ xenia-rs/crates/xenia-debugger/src/trace.rs | 9 + xenia-rs/crates/xenia-gpu/Cargo.toml | 13 + .../crates/xenia-gpu/src/command_processor.rs | 17 + xenia-rs/crates/xenia-gpu/src/lib.rs | 21 + .../crates/xenia-gpu/src/register_file.rs | 28 + xenia-rs/crates/xenia-hid/Cargo.toml | 10 + xenia-rs/crates/xenia-hid/src/lib.rs | 47 + xenia-rs/crates/xenia-kernel/Cargo.toml | 13 + xenia-rs/crates/xenia-kernel/src/exports.rs | 142 ++ xenia-rs/crates/xenia-kernel/src/lib.rs | 4 + xenia-rs/crates/xenia-kernel/src/state.rs | 80 + xenia-rs/crates/xenia-memory/Cargo.toml | 17 + xenia-rs/crates/xenia-memory/src/access.rs | 47 + xenia-rs/crates/xenia-memory/src/heap.rs | 242 +++ xenia-rs/crates/xenia-memory/src/lib.rs | 31 + xenia-rs/crates/xenia-memory/src/mmio.rs | 27 + .../crates/xenia-memory/src/page_table.rs | 122 ++ xenia-rs/crates/xenia-memory/src/platform.rs | 98 ++ xenia-rs/crates/xenia-types/Cargo.toml | 11 + xenia-rs/crates/xenia-types/src/endian.rs | 122 ++ xenia-rs/crates/xenia-types/src/error.rs | 27 + xenia-rs/crates/xenia-types/src/lib.rs | 6 + xenia-rs/crates/xenia-types/src/vec128.rs | 161 ++ xenia-rs/crates/xenia-vfs/Cargo.toml | 12 + xenia-rs/crates/xenia-vfs/src/device.rs | 54 + xenia-rs/crates/xenia-vfs/src/disc_image.rs | 40 + xenia-rs/crates/xenia-vfs/src/lib.rs | 33 + xenia-rs/crates/xenia-xex/Cargo.toml | 13 + xenia-rs/crates/xenia-xex/src/header.rs | 55 + xenia-rs/crates/xenia-xex/src/lib.rs | 4 + xenia-rs/crates/xenia-xex/src/loader.rs | 98 ++ 50 files changed, 6759 insertions(+) create mode 100644 RUST_PORT_CODEMAP.md create mode 100644 RUST_PORT_REPORT.md create mode 100644 xenia-rs/Cargo.lock create mode 100644 xenia-rs/Cargo.toml create mode 100644 xenia-rs/crates/xenia-app/Cargo.toml create mode 100644 xenia-rs/crates/xenia-app/src/main.rs create mode 100644 xenia-rs/crates/xenia-apu/Cargo.toml create mode 100644 xenia-rs/crates/xenia-apu/src/lib.rs create mode 100644 xenia-rs/crates/xenia-cpu/Cargo.toml create mode 100644 xenia-rs/crates/xenia-cpu/src/context.rs create mode 100644 xenia-rs/crates/xenia-cpu/src/decoder.rs create mode 100644 xenia-rs/crates/xenia-cpu/src/disasm.rs create mode 100644 xenia-rs/crates/xenia-cpu/src/interpreter.rs create mode 100644 xenia-rs/crates/xenia-cpu/src/lib.rs create mode 100644 xenia-rs/crates/xenia-cpu/src/opcode.rs create mode 100644 xenia-rs/crates/xenia-debugger/Cargo.toml create mode 100644 xenia-rs/crates/xenia-debugger/src/breakpoint.rs create mode 100644 xenia-rs/crates/xenia-debugger/src/lib.rs create mode 100644 xenia-rs/crates/xenia-debugger/src/trace.rs create mode 100644 xenia-rs/crates/xenia-gpu/Cargo.toml create mode 100644 xenia-rs/crates/xenia-gpu/src/command_processor.rs create mode 100644 xenia-rs/crates/xenia-gpu/src/lib.rs create mode 100644 xenia-rs/crates/xenia-gpu/src/register_file.rs create mode 100644 xenia-rs/crates/xenia-hid/Cargo.toml create mode 100644 xenia-rs/crates/xenia-hid/src/lib.rs create mode 100644 xenia-rs/crates/xenia-kernel/Cargo.toml create mode 100644 xenia-rs/crates/xenia-kernel/src/exports.rs create mode 100644 xenia-rs/crates/xenia-kernel/src/lib.rs create mode 100644 xenia-rs/crates/xenia-kernel/src/state.rs create mode 100644 xenia-rs/crates/xenia-memory/Cargo.toml create mode 100644 xenia-rs/crates/xenia-memory/src/access.rs create mode 100644 xenia-rs/crates/xenia-memory/src/heap.rs create mode 100644 xenia-rs/crates/xenia-memory/src/lib.rs create mode 100644 xenia-rs/crates/xenia-memory/src/mmio.rs create mode 100644 xenia-rs/crates/xenia-memory/src/page_table.rs create mode 100644 xenia-rs/crates/xenia-memory/src/platform.rs create mode 100644 xenia-rs/crates/xenia-types/Cargo.toml create mode 100644 xenia-rs/crates/xenia-types/src/endian.rs create mode 100644 xenia-rs/crates/xenia-types/src/error.rs create mode 100644 xenia-rs/crates/xenia-types/src/lib.rs create mode 100644 xenia-rs/crates/xenia-types/src/vec128.rs create mode 100644 xenia-rs/crates/xenia-vfs/Cargo.toml create mode 100644 xenia-rs/crates/xenia-vfs/src/device.rs create mode 100644 xenia-rs/crates/xenia-vfs/src/disc_image.rs create mode 100644 xenia-rs/crates/xenia-vfs/src/lib.rs create mode 100644 xenia-rs/crates/xenia-xex/Cargo.toml create mode 100644 xenia-rs/crates/xenia-xex/src/header.rs create mode 100644 xenia-rs/crates/xenia-xex/src/lib.rs create mode 100644 xenia-rs/crates/xenia-xex/src/loader.rs diff --git a/.gitignore b/.gitignore index 4b0c33cb9..516786fab 100644 --- a/.gitignore +++ b/.gitignore @@ -116,3 +116,6 @@ node_modules/.bin/ /cache0 /devkit recent.toml + +# Rust +target/ \ No newline at end of file diff --git a/RUST_PORT_CODEMAP.md b/RUST_PORT_CODEMAP.md new file mode 100644 index 000000000..dae905f7b --- /dev/null +++ b/RUST_PORT_CODEMAP.md @@ -0,0 +1,164 @@ +## Xenia Xbox 360 Emulator: JIT vs Interpreter Architecture Analysis +Analysis of Xenia's architecture comparing JIT vs interpreter approaches for Rust porting. Key findings: JIT pipeline [1a-1d] uses Xbyak with no Rust equivalent, MMIO relies on hardware exceptions [2a-2d], while interpreter approach uses explicit MMIO checking [3a-3d]. The PPC context [4a-4d] and opcode infrastructure [5a-5d] support both approaches. Memory system [6a-6c] requires unsafe Rust regardless. Kernel HLE [7a-7c] and GPU shader translation [8a-8c] present additional challenges. +### 1. JIT Code Generation Pipeline +The current PPC-to-x64 JIT compilation pipeline using HIR and Xbyak +### 1a. JIT Pipeline Initialization (`ppc_translator.cc:44`) +Sets up scanner, HIR builder, compiler, and Xbyak assembler +```text +PPCTranslator::PPCTranslator(PPCFrontend* frontend) : frontend_(frontend) { +``` +### 1b. HIR Optimization Passes (`ppc_translator.cc:57`) +Adds multiple optimization passes to the compiler pipeline +```text +compiler_->AddPass(std::make_unique()); +``` +### 1c. Xbyak-based Code Emitter (`x64_emitter.h:208`) +X64Emitter inherits from Xbyak for runtime x64 code generation +```text +class X64Emitter : public Xbyak::CodeGenerator { +``` +### 1d. Host-to-Guest Thunk (`x64_backend.cc:656`) +Raw x64 assembly emitted for host↔guest ABI transitions +```text +mov(rdi, ptr[rsi + offsetof(ppc::PPCContext, virtual_membase)]); // membase +``` +### 2. MMIO Exception Handling +Hardware exception-based MMIO interception used by the JIT +### 2a. Exception Handler Entry (`mmio_handler.cc:402`) +Catches access violations to handle MMIO operations +```text +bool MMIOHandler::ExceptionCallback(Exception* ex) { +``` +### 2b. Filter Access Violations (`mmio_handler.cc:403`) +Only processes memory access violations +```text +if (ex->code() != Exception::Code::kAccessViolation) { +``` +### 2c. Address Translation (`mmio_handler.cc:427`) +Translates host fault address back to guest virtual address +```text +fault_guest_virtual_address = host_to_guest_virtual_( +``` +### 2d. Instruction Decoding (`mmio_handler.cc:449`) +Decodes the faulting x64 instruction to determine the operation +```text +if (!TryDecodeLoadStore(p, decoded_load_store)) { +``` +### 3. Interpreter Alternative Approach +Explicit MMIO checking pattern that enables interpreter implementation +### 3a. Explicit MMIO Check Function (`x64_seq_memory.cc:1216`) +Pattern for checking MMIO ranges without exceptions +```text +static T MMIOAwareLoad(void* _ctx, unsigned int guestaddr) { +``` +### 3b. Range Lookup (`x64_seq_memory.cc:1225`) +Explicitly checks if address is in mapped MMIO range +```text +auto gaddr = ctx->processor->memory()->LookupVirtualMappedRange(guestaddr); +``` +### 3c. MMIO Callback Invocation (`x64_seq_memory.cc:1236`) +Calls MMIO read callback instead of accessing memory directly +```text +value = gaddr->read(nullptr, gaddr->callback_context, guestaddr); +``` +### 3d. MMIO Handler Interface (`mmio_handler.h:73`) +Public API for explicit MMIO checking in interpreter mode +```text +bool CheckLoad(uint32_t virtual_address, uint32_t* out_value); +``` +### 4. PPC Context and State Management +Register file and thread state structures that would need Rust porting +### 4a. PPC Register File (`ppc_context.h:378`) +32 GPRs, CTR, LR, MSR in the context structure +```text +uint64_t r[32]; // 0x20 General purpose registers +``` +### 4b. Floating-Point and Vector Registers (`ppc_context.h:384`) +32 FPRs and 128 VMX128 vector registers +```text +double f[32]; // 0x120 Floating-point registers +``` +### 4c. Thread State Context (`thread_state.h:49`) +Each guest thread owns a PPCContext pointer +```text +ppc::PPCContext* context_; +``` +### 4d. Big-Endian Wrapper (`byte_order.h:134`) +Type that handles big-endian conversion for guest structures +```text +template +using be = endian_store; +``` +### 5. Opcode Dispatch Infrastructure +PPC opcode enumeration and lookup that enables interpreter implementation +### 5a. PPC Opcode Enumeration (`ppc_opcode.h:14`) +Complete enum of all PPC opcodes including VMX128 extensions +```text +enum class PPCOpcode : uint32_t { +``` +### 5b. Opcode Dispatch Table (`ppc_opcode_lookup_gen.cc:262`) +Fast lookup table for opcode decoding +```text +case 0b000101: PPC_DECODER_HIT(vrlw128); +``` +### 5c. VMX128 Instruction Encoding (`ppc_emit_altivec.cc:37`) +Macros for decoding Xbox 360-specific VMX128 instructions +```text +#define VX128(op, xop) (OP(op) | (((uint32_t)(xop)) & 0x3d0)) +``` +### 5d. Basic Block Discovery (`ppc_scanner.h:36`) +Finds basic block boundaries for potential interpreter caching +```text +std::vector FindBlocks(GuestFunction* function); +``` +### 6. Memory System Architecture +4GB virtual address space management required for both approaches +### 6a. Virtual Memory Allocation (`memory.cc:142`) +Creates 4GB+ file-backed mapping for guest address space +```text +mapping_ = xe::memory::CreateFileMappingHandle( +``` +### 6b. Virtual Memory Base (`memory.cc:167`) +Sets up virtual and physical memory base addresses +```text +virtual_membase_ = mapping_base_; +``` +### 6c. Guest Address Translation (`ppc_context.h:433`) +Fast inline function for translating guest to host addresses +```text +inline T TranslateVirtual(uint32_t guest_address) const { +``` +### 7. Kernel HLE System +Massive kernel export table that would need Rust porting +### 7a. Kernel Export Table (`xboxkrnl_table.inc:15`) +96KB+ table of all xboxkrnl exports +```text +XE_EXPORT(xboxkrnl, 0x00000001, DbgBreakPoint, kFunction), +``` +### 7b. Guest Thread Structure (`xthread.h:256`) +Packed big-endian X_KTHREAD structure living in guest memory +```text +xe::be stack_base; // 0x5C +``` +### 7c. Kernel State Management (`emulator.h:349`) +Central kernel object tracking all guest kernel state +```text +std::unique_ptr kernel_state_; +``` +### 8. GPU Shader Translation +Complex shader translation pipeline that poses challenges for Rust porting +### 8a. SPIR-V Builder Initialization (`spirv_shader_translator.cc:155`) +Uses glslang C++ API for SPIR-V generation +```text +builder_ = std::make_unique( +``` +### 8b. Shader Interpreter (`shader_interpreter.h:51`) +Existing interpreter for simple shaders without texture fetches +```text +static bool CanInterpretShader(const Shader& shader) { +``` +### 8c. C++ Dependency (`premake5.lua:31`) +glslang dependency with no direct Rust equivalent +```text +"glslang-spirv", +``` \ No newline at end of file diff --git a/RUST_PORT_REPORT.md b/RUST_PORT_REPORT.md new file mode 100644 index 000000000..593b21f77 --- /dev/null +++ b/RUST_PORT_REPORT.md @@ -0,0 +1,1085 @@ +# Xenia → Rust Port: Technical Feasibility Report v2.0 + +This report consolidates the full analysis from both the JIT-based and interpreter-based approaches, grounded in the actual codebase structure. + +--- + +## 1. Project Architecture + +Xenia is organized into ~12 major subsystems, all owned by the central `Emulator` class: + +``` +src/xenia/ +├── app/ # Entry point, premake build +├── apu/ # Audio (XAudio2, SDL2 backends) +├── base/ # Platform abstractions, threading, memory +├── cpu/ # PPC frontend, HIR, compiler passes, x64 JIT backend +│ ├── ppc/ # Decoder, HIR emitters, scanner +│ ├── hir/ # HIR opcodes, builder, values +│ ├── compiler/ # Optimization passes +│ └── backend/x64/ # Xbyak-based x64 emitter +├── gpu/ # Xenos GPU, DXBC/SPIR-V shader translators, shader interpreter +├── hid/ # Input (SDL2, XInput, etc.) +├── kernel/ # HLE kernel (xboxkrnl/, xam/) +├── vfs/ # Virtual filesystem (disc image, STFS, ZArchive) +├── ui/ # ImGui, Vulkan/D3D12 window +├── memory.cc/.h # 4GB+ guest address space +└── emulator.cc/.h +``` [1](#2-0) + +--- + +## 2. The CPU Pipeline in Detail + +The current execution path for guest code is: + +``` +PPC binary → PPCScanner (basic blocks) → PPCHIRBuilder (HIR) + → Compiler passes (CFG, simplification, const-prop, DCE, regalloc) + → X64Emitter (Xbyak) → native x64 machine code +``` [2](#2-1) + +There is **no existing PPC interpreter**. The `ppc_emit_*.cc` files emit HIR, not execute instructions directly. The `PPCScanner` does basic block analysis for the JIT: [3](#2-2) + +The register file is `PPCContext`: 32 GPRs, 32 FPRs, 128 VMX registers, plus LR/CTR/XER/CR: [4](#2-3) + +Each guest thread owns a `ThreadState` which holds a `PPCContext*`: [5](#2-4) + +The full `PPCOpcode` enum covers ~250 opcodes including many Xbox 360-specific VMX128 extensions: [6](#2-5) + +--- + +## 3. The Two Approaches Compared + +### Approach A: JIT Port (keep the JIT, rewrite everything else in Rust) + +The JIT backend (`X64Emitter`) inherits from `Xbyak::CodeGenerator` — a C++ header-only library with no Rust equivalent: [7](#2-6) + +The MMIO system relies on hardware exceptions (`SIGSEGV`/`VEH`) to intercept guest memory accesses made by JIT-compiled code, then decodes the faulting x64 instruction at runtime: [8](#2-7) + +This is fundamentally incompatible with safe Rust. Signal handlers in Rust are `unsafe` and severely restricted in what they can do. + +The host↔guest thunks emit raw x64 at runtime and depend on `PPCContext` being at hardcoded `offsetof` positions: [9](#2-8) + +**Verdict:** The JIT approach requires keeping the entire `cpu/backend/x64/` subtree in C++ and bridging via FFI. The `cxx` crate is the most ergonomic tool for this, but the FFI boundary would be complex and the safety gains minimal in the hottest code path. + +--- + +### Approach B: Interpreter Port (execute PPC instructions directly, no JIT) + +This eliminates the three hardest blockers: + +| Blocker | JIT | Interpreter | +|---|---|---| +| Xbyak / x64 codegen | Blocker | Eliminated | +| MMIO exception handler | Blocker | Eliminated | +| Host↔guest ABI thunks | Blocker | Eliminated | +| `PPCContext` layout coupling to JIT | Hard | Eliminated | + +In an interpreter, every memory access is a function call. The codebase already has the explicit MMIO-check pattern in `MMIOAwareLoad`: [10](#2-9) + +And `MMIOHandler` exposes `CheckLoad`/`CheckStore` for exactly this: [11](#2-10) + +The opcode dispatch infrastructure (`LookupOpcode`, `PPCDecodeData`, `PPCOpcode` enum) is pure data-manipulation code with no JIT dependency — directly portable to Rust: [12](#2-11) + +The `ppc_emit_*.cc` files serve as a complete semantic reference for every instruction, even though they emit HIR rather than executing directly. The VMX128 instruction encoding macros are documented there: [13](#2-12) + +**The dominant cost of Approach B is performance.** The Xbox 360 has 3 PPC cores × 2 hardware threads. A naive interpreter runs ~20–100× slower than JIT. Most 3D games would be unplayable. + +--- + +## 4. Subsystem-by-Subsystem Feasibility + +### 4.1 Memory System — `unsafe`, but contained + +The 4GB+ virtual address space reservation via OS file mappings is required regardless of approach: [14](#2-13) + +This is `unsafe` Rust via `libc`/`windows-sys`, but it is a well-defined, bounded `unsafe` block. The `xe::be` big-endian wrapper maps cleanly to a Rust newtype: [15](#2-14) + +### 4.2 Kernel HLE — Medium difficulty + +The kernel has a massive HLE surface: `xboxkrnl_table.inc` alone is 96KB, covering threading, memory, I/O, RTL, video, audio, crypto, and more. The `X_KTHREAD` struct is a packed big-endian layout that lives in guest memory: [16](#2-15) + +In Rust, this becomes a `#[repr(C, packed)]` struct with `be` newtypes. The `unsafe` is explicit and auditable. The threading HLE (`xboxkrnl_threading.cc` at 66KB) is the most complex piece. + +### 4.3 GPU — Hard + +The GPU has two shader translation pipelines (DXBC and SPIR-V) totaling tens of thousands of lines, translating Xenos microcode to host shader IR. The SPIR-V translator uses the glslang `SpirvBuilder` C++ API directly: [17](#2-16) + +There is no Rust equivalent of `SpirvBuilder`. The `rspirv` crate provides partial SPIR-V construction but lacks the full feature set. + +**Notable:** The GPU subsystem already has a `ShaderInterpreter` for Xenos microcode — used for internal rectangles and clears where texture fetches are not needed: [18](#2-17) + +This demonstrates the pattern: interpreter as a correctness baseline, translator for performance. + +### 4.4 VFS — High feasibility + +The VFS devices (disc image, ZArchive, STFS/XContent) are pure data-parsing code with no platform dependencies beyond file I/O: + + +These are ideal candidates for safe Rust. The `stfs_xbox.h` format definitions (15KB of packed structs) translate directly to `#[repr(C)]` Rust structs. + +### 4.5 Concurrency — Improved by Rust + +The codebase uses custom spinlocks (`xe_fast_mutex`, `xe_global_mutex`, `xe_unlikely_mutex`) alongside `std::atomic`. Rust's `Send`/`Sync` traits would catch data races at compile time that are currently only caught at runtime: [19](#2-18) + +### 4.6 Third-party C++ dependencies + +| Library | Purpose | Rust status | +|---|---|---| +| Xbyak | x64 JIT codegen | No equivalent; `dynasm-rs` is limited | +| glslang/SPIRV-Tools | Shader compilation | `rspirv` (partial) | +| capstone | x86 disassembly | `capstone` crate (binding) | +| Dear ImGui | UI | `imgui-rs` (binding) | +| FFmpeg | Audio/video decode | `ffmpeg-next` (binding) | +| D3D12/XAudio2 | Windows GPU/audio | `windows-rs` (binding) | +| Vulkan | GPU | `ash` (good) | +| snappy/xxhash | Compression/hashing | Native Rust crates exist | [20](#2-19) + +--- + +## 5. The Interpreter + Basic Block Cache Path + +The most realistic path to a working Rust port that is both architecturally feasible and eventually performant: + +``` +Phase 1: Pure interpreter + - PPCContext as #[repr(C)] Rust struct + - match on PPCOpcode enum → one fn per opcode + - Explicit MMIO check on every load/store (no signal handlers) + - Correct but slow (~20-100x slower than JIT) + +Phase 2: Basic block cache + - PPCScanner already identifies basic block boundaries + - Cache translated basic blocks as Vec + - Invalidate on self-modifying code + - Recovers significant performance without a full JIT + +Phase 3: Optional: LLVM via inkwell + - Replace the basic block cache with LLVM IR generation + - Eliminates Xbyak entirely; LLVM handles x64/ARM64 + - Major architectural change but Rust-native +``` + +The `PPCScanner` already provides the basic block boundary detection needed for Phase 2: [3](#2-2) + +--- + +## 6. Summary Matrix + +``` +Subsystem | Approach A (JIT) | Approach B (Interpreter) +---------------------------|------------------|------------------------- +VFS / file formats | High | High +Config / logging | High | High +Kernel HLE (non-threading) | Medium | Medium +Kernel threading HLE | Low-Medium | Low-Medium +Memory system | Low (unsafe) | Low (unsafe, same) +GPU shader translation | Very Low | Very Low (same) +VMX128 instructions | Hard (HIR emit) | Hard (fn per opcode) +lwarx/stwcx atomics | Hard (x64 asm) | Medium (AtomicU32 CAS) +MMIO handler | Blocker | Eliminated +x64 JIT / Xbyak | Blocker | Eliminated +Host↔guest thunks | Blocker | Eliminated +Performance | Good | Poor (Phase 1) +``` + +**Bottom line:** A full JIT port is not practical without either keeping the JIT in C++ (hybrid) or replacing Xbyak with LLVM. An interpreter port is architecturally feasible in Rust and eliminates the three hardest blockers, but requires a basic block cache layer to approach playable performance. The GPU shader translation pipeline is the second major obstacle regardless of CPU approach, with no clean Rust-native equivalent of the glslang `SpirvBuilder` API. + +### Citations + +**File:** src/xenia/emulator.h (L336-349) +```text + std::unique_ptr memory_; + + std::unique_ptr processor_; + std::unique_ptr audio_system_; + std::unique_ptr audio_media_player_; + std::unique_ptr graphics_system_; + std::unique_ptr input_system_; + + std::unique_ptr export_resolver_; + std::unique_ptr file_system_; + std::unique_ptr patcher_; + std::unique_ptr plugin_loader_; + + std::unique_ptr kernel_state_; +``` + +**File:** src/xenia/cpu/ppc/ppc_translator.cc (L44-112) +```text +PPCTranslator::PPCTranslator(PPCFrontend* frontend) : frontend_(frontend) { + Backend* backend = frontend->processor()->backend(); + + scanner_.reset(new PPCScanner(frontend)); + builder_.reset(new PPCHIRBuilder(frontend)); + compiler_.reset(new Compiler(frontend->processor())); + assembler_ = backend->CreateAssembler(); + assembler_->Initialize(); + + bool validate = cvars::validate_hir; + + // Merge blocks early. This will let us use more context in other passes. + // The CFG is required for simplification and dirtied by it. + compiler_->AddPass(std::make_unique()); + compiler_->AddPass(std::make_unique()); + + // Passes are executed in the order they are added. Multiple of the same + // pass type may be used. + if (!cvars::disable_context_promotion) { + if (validate) { + compiler_->AddPass(std::make_unique()); + } + + compiler_->AddPass(std::make_unique()); + + if (validate) { + compiler_->AddPass(std::make_unique()); + } + } + // Grouped simplification + constant propagation. + // Loops until no changes are made. + auto sap = std::make_unique(); + sap->AddPass(std::make_unique()); + if (validate) sap->AddPass(std::make_unique()); + sap->AddPass(std::make_unique()); + if (validate) sap->AddPass(std::make_unique()); + compiler_->AddPass(std::move(sap)); + + if (backend->machine_info()->supports_extended_load_store) { + // Backend supports the advanced LOAD/STORE instructions. + // These will save us a lot of HIR opcodes. + compiler_->AddPass( + std::make_unique()); + if (validate) + compiler_->AddPass(std::make_unique()); + } + compiler_->AddPass(std::make_unique()); + if (validate) compiler_->AddPass(std::make_unique()); + // compiler_->AddPass(std::make_unique()); + // if (validate) + // compiler_->AddPass(std::make_unique()); + compiler_->AddPass(std::make_unique()); + if (validate) compiler_->AddPass(std::make_unique()); + + //// Removes all unneeded variables. Try not to add new ones after this. + // compiler_->AddPass(new passes::ValueReductionPass()); + // if (validate) compiler_->AddPass(new passes::ValidationPass()); + + // Register allocation for the target backend. + // Will modify the HIR to add loads/stores. + // This should be the last pass before finalization, as after this all + // registers are assigned and ready to be emitted. + compiler_->AddPass(std::make_unique( + backend->machine_info())); + if (validate) compiler_->AddPass(std::make_unique()); + + // Must come last. The HIR is not really HIR after this. + compiler_->AddPass(std::make_unique()); +} +``` + +**File:** src/xenia/cpu/ppc/ppc_scanner.h (L29-42) +```text +class PPCScanner { + public: + explicit PPCScanner(PPCFrontend* frontend); + ~PPCScanner(); + + bool Scan(GuestFunction* function, FunctionDebugInfo* debug_info); + + std::vector FindBlocks(GuestFunction* function); + + private: + bool IsRestGprLr(uint32_t address); + + PPCFrontend* frontend_ = nullptr; +}; +``` + +**File:** src/xenia/cpu/ppc/ppc_context.h (L376-400) +```text + // Most frequently used registers first. + + uint64_t r[32]; // 0x20 General purpose registers + uint64_t ctr; // 0x18 Count register + uint64_t lr; // 0x10 Link register + + uint64_t msr; // machine state register + + double f[32]; // 0x120 Floating-point registers + vec128_t v[128]; // 0x220 VMX128 vector registers + vec128_t vscr_vec; + // XER register: + // Split to make it easier to do individual updates. + uint8_t xer_ca; + uint8_t xer_ov; + uint8_t xer_so; + + // Condition registers: + // These are split to make it easier to do DCE on unused stores. + uint64_t cr() const; + void set_cr(uint64_t value); + // todo: remove, saturation should be represented by a vector + uint8_t vscr_sat; + + uint32_t vrsave; +``` + +**File:** src/xenia/cpu/thread_state.h (L24-50) +```text +class ThreadState { + public: + ThreadState(Processor* processor, uint32_t thread_id, uint32_t stack_base = 0, + uint32_t pcr_address = 0); + ~ThreadState(); + + Processor* processor() const { return processor_; } + Memory* memory() const { return memory_; } + void* backend_data() const { return backend_data_; } + ppc::PPCContext* context() const { return context_; } + uint32_t thread_id() const { return thread_id_; } + + static void Bind(ThreadState* thread_state); + static ThreadState* Get(); + static uint32_t GetThreadID(); + + private: + Processor* processor_; + Memory* memory_; + void* backend_data_; + + uint32_t pcr_address_ = 0; + uint32_t thread_id_ = 0; + + // NOTE: must be 64b aligned for SSE ops. + ppc::PPCContext* context_; +}; +``` + +**File:** src/xenia/cpu/ppc/ppc_opcode.h (L14-200) +```text +enum class PPCOpcode : uint32_t { + addcx, + addex, + addi, + addic, + addicx, + addis, + addmex, + addx, + addzex, + andcx, + andisx, + andix, + andx, + bcctrx, + bclrx, + bcx, + bx, + cmp, + cmpi, + cmpl, + cmpli, + cntlzdx, + cntlzwx, + crand, + crandc, + creqv, + crnand, + crnor, + cror, + crorc, + crxor, + dcbf, + dcbi, + dcbst, + dcbt, + dcbtst, + dcbz, + dcbz128, + divdux, + divdx, + divwux, + divwx, + eieio, + eqvx, + extsbx, + extshx, + extswx, + fabsx, + faddsx, + faddx, + fcfidx, + fcmpo, + fcmpu, + fctidx, + fctidzx, + fctiwx, + fctiwzx, + fdivsx, + fdivx, + fmaddsx, + fmaddx, + fmrx, + fmsubsx, + fmsubx, + fmulsx, + fmulx, + fnabsx, + fnegx, + fnmaddsx, + fnmaddx, + fnmsubsx, + fnmsubx, + fresx, + frspx, + frsqrtex, + fselx, + fsqrtsx, + fsqrtx, + fsubsx, + fsubx, + icbi, + isync, + lbz, + lbzu, + lbzux, + lbzx, + ld, + ldarx, + ldbrx, + ldu, + ldux, + ldx, + lfd, + lfdu, + lfdux, + lfdx, + lfs, + lfsu, + lfsux, + lfsx, + lha, + lhau, + lhaux, + lhax, + lhbrx, + lhz, + lhzu, + lhzux, + lhzx, + lmw, + lswi, + lswx, + lvebx, + lvehx, + lvewx, + lvewx128, + lvlx, + lvlx128, + lvlxl, + lvlxl128, + lvrx, + lvrx128, + lvrxl, + lvrxl128, + lvsl, + lvsl128, + lvsr, + lvsr128, + lvx, + lvx128, + lvxl, + lvxl128, + lwa, + lwarx, + lwaux, + lwax, + lwbrx, + lwz, + lwzu, + lwzux, + lwzx, + mcrf, + mcrfs, + mcrxr, + mfcr, + mffsx, + mfmsr, + mfspr, + mftb, + mfvscr, + mtcrf, + mtfsb0x, + mtfsb1x, + mtfsfix, + mtfsfx, + mtmsr, + mtmsrd, + mtspr, + mtvscr, + mulhdux, + mulhdx, + mulhwux, + mulhwx, + mulldx, + mulli, + mullwx, + nandx, + negx, + norx, + orcx, + ori, + oris, + orx, + rldclx, + rldcrx, + rldiclx, + rldicrx, + rldicx, + rldimix, + rlwimix, + rlwinmx, + rlwnmx, + sc, + sldx, + slwx, + sradix, +``` + +**File:** src/xenia/cpu/backend/x64/x64_emitter.h (L208-212) +```text +class X64Emitter : public Xbyak::CodeGenerator { + public: + X64Emitter(X64Backend* backend, XbyakAllocator* allocator); + virtual ~X64Emitter(); + +``` + +**File:** src/xenia/cpu/mmio_handler.cc (L402-470) +```text +bool MMIOHandler::ExceptionCallback(Exception* ex) { + if (ex->code() != Exception::Code::kAccessViolation) { + return false; + } + Exception::AccessViolationOperation operation = + ex->access_violation_operation(); + if (operation != Exception::AccessViolationOperation::kRead && + operation != Exception::AccessViolationOperation::kWrite) { + // Data Execution Prevention or something else uninteresting. + return false; + } + bool is_write = operation == Exception::AccessViolationOperation::kWrite; + if (ex->fault_address() < uint64_t(virtual_membase_) || + ex->fault_address() > uint64_t(memory_end_)) { + // Quick kill anything outside our mapping. + return false; + } + + void* fault_host_address = reinterpret_cast(ex->fault_address()); + + // Access violations are pretty rare, so we can do a linear search here. + // Only check if in the virtual range, as we only support virtual ranges. + const MMIORange* range = nullptr; + uint32_t fault_guest_virtual_address = 0; + if (ex->fault_address() < uint64_t(physical_membase_)) { + fault_guest_virtual_address = host_to_guest_virtual_( + host_to_guest_virtual_context_, fault_host_address); + for (const auto& test_range : mapped_ranges_) { + if ((fault_guest_virtual_address & test_range.mask) == + test_range.address) { + // Address is within the range of this mapping. + range = &test_range; + break; + } + } + } + if (!range) { + // Recheck if the pages are still protected (race condition - another thread + // clears the watch we just hit). + // Do this under the lock so we don't introduce another race condition. + auto lock = global_critical_region_.Acquire(); + memory::PageAccess cur_access; + size_t page_length = memory::page_size(); + memory::QueryProtect(fault_host_address, page_length, cur_access); + if (cur_access != memory::PageAccess::kNoAccess && + (!is_write || cur_access != memory::PageAccess::kReadOnly)) { + // Another thread has cleared this watch. Abort. + XELOGD("Race condition on watch, was already cleared by another thread!"); + return true; + } + // The address is not found within any range, so either a write watch or an + // actual access violation. + if (access_violation_callback_) { + return access_violation_callback_(std::move(lock), + access_violation_callback_context_, + fault_host_address, is_write); + } + return false; + } + + auto rip = ex->pc(); + auto p = reinterpret_cast(rip); + DecodedLoadStore decoded_load_store; + if (!TryDecodeLoadStore(p, decoded_load_store)) { + XELOGE("Unable to decode MMIO load or store instruction at {}", + static_cast(p)); + assert_always("Unknown MMIO instruction type"); + return false; + } +``` + +**File:** src/xenia/cpu/backend/x64/x64_backend.cc (L630-726) +```text +HostToGuestThunk X64HelperEmitter::EmitHostToGuestThunk() { +#ifdef XE_PLATFORM_WIN32 + // rcx = target + // rdx = arg0 (context) + // r8 = arg1 (guest return address) + + _code_offsets code_offsets = {}; + + constexpr size_t stack_size = StackLayout::THUNK_STACK_SIZE; + + code_offsets.prolog = getSize(); + + // rsp + 0 = return address + mov(qword[rsp + 8 * 3], r8); + mov(qword[rsp + 8 * 2], rdx); + mov(qword[rsp + 8 * 1], rcx); + sub(rsp, stack_size); + + code_offsets.prolog_stack_alloc = getSize(); + code_offsets.body = getSize(); + + // Save nonvolatile registers. + EmitSaveNonvolatileRegs(); + + mov(rax, rcx); + mov(rsi, rdx); // context + mov(rdi, ptr[rdx + offsetof(ppc::PPCContext, virtual_membase)]); // membase + mov(rcx, r8); // return address + call(rax); + vzeroupper(); + EmitLoadNonvolatileRegs(); + + code_offsets.epilog = getSize(); + + add(rsp, stack_size); + mov(rcx, qword[rsp + 8 * 1]); + mov(rdx, qword[rsp + 8 * 2]); + mov(r8, qword[rsp + 8 * 3]); + ret(); +#elif XE_PLATFORM_LINUX || XE_PLATFORM_MAC + // System-V ABI args: + // rdi = target + // rsi = arg0 (context) + // rdx = arg1 (guest return address) + + struct _code_offsets { + size_t prolog; + size_t prolog_stack_alloc; + size_t body; + size_t epilog; + size_t tail; + } code_offsets = {}; + + constexpr size_t stack_size = StackLayout::THUNK_STACK_SIZE; + + code_offsets.prolog = getSize(); + // rsp + 0 = return address + sub(rsp, stack_size); + + code_offsets.prolog_stack_alloc = getSize(); + code_offsets.body = getSize(); + + // Save nonvolatile registers. + EmitSaveNonvolatileRegs(); + + mov(rax, rdi); + // mov(rsi, rsi); // context + mov(rdi, ptr[rsi + offsetof(ppc::PPCContext, virtual_membase)]); // membase + mov(rcx, rdx); // return address + call(rax); + + EmitLoadNonvolatileRegs(); + + code_offsets.epilog = getSize(); + + add(rsp, stack_size); + ret(); +#else + assert_always("Unknown platform ABI in host to guest thunk!"); +#endif + + code_offsets.tail = getSize(); + + assert_zero(code_offsets.prolog); + EmitFunctionInfo func_info = {}; + func_info.code_size.total = getSize(); + func_info.code_size.prolog = code_offsets.body - code_offsets.prolog; + func_info.code_size.body = code_offsets.epilog - code_offsets.body; + func_info.code_size.epilog = code_offsets.tail - code_offsets.epilog; + func_info.code_size.tail = getSize() - code_offsets.tail; + func_info.prolog_stack_alloc_offset = + code_offsets.prolog_stack_alloc - code_offsets.prolog; + func_info.stack_size = stack_size; + + void* fn = Emplace(func_info); + return (HostToGuestThunk)fn; +} +``` + +**File:** src/xenia/cpu/backend/x64/x64_seq_memory.cc (L1215-1237) +```text +template +static T MMIOAwareLoad(void* _ctx, unsigned int guestaddr) { + T value; + + if (guestaddr >= 0xE0000000) { + guestaddr += 0x1000; + } + + auto ctx = reinterpret_cast(_ctx); + + auto gaddr = ctx->processor->memory()->LookupVirtualMappedRange(guestaddr); + if (!gaddr) { + value = *reinterpret_cast(ctx->virtual_membase + guestaddr); + if (swap) { + value = xe::byte_swap(value); + } + } else { + /* + was having issues, found by comparing the values used with exceptions + to these that we were reversed... + */ + value = gaddr->read(nullptr, gaddr->callback_context, guestaddr); + } +``` + +**File:** src/xenia/cpu/mmio_handler.h (L73-74) +```text + bool CheckLoad(uint32_t virtual_address, uint32_t* out_value); + bool CheckStore(uint32_t virtual_address, uint32_t value); +``` + +**File:** src/xenia/cpu/ppc/ppc_opcode_lookup_gen.cc (L261-285) +```text + switch ((ExtractBits(code, 22, 25) << 2)|(ExtractBits(code, 27, 27) << 0)) { + case 0b000101: PPC_DECODER_HIT(vrlw128); + case 0b001101: PPC_DECODER_HIT(vslw128); + case 0b010101: PPC_DECODER_HIT(vsraw128); + case 0b011101: PPC_DECODER_HIT(vsrw128); + case 0b101000: PPC_DECODER_HIT(vmaxfp128); + case 0b101100: PPC_DECODER_HIT(vminfp128); + case 0b110000: PPC_DECODER_HIT(vmrghw128); + case 0b110100: PPC_DECODER_HIT(vmrglw128); + case 0b111000: PPC_DECODER_HIT(vupkhsb128); + case 0b111100: PPC_DECODER_HIT(vupklsb128); + } + PPC_DECODER_MISS; + case 7: PPC_DECODER_HIT(mulli); + case 8: PPC_DECODER_HIT(subficx); + case 10: PPC_DECODER_HIT(cmpli); + case 11: PPC_DECODER_HIT(cmpi); + case 12: PPC_DECODER_HIT(addic); + case 13: PPC_DECODER_HIT(addicx); + case 14: PPC_DECODER_HIT(addi); + case 15: PPC_DECODER_HIT(addis); + case 16: PPC_DECODER_HIT(bcx); + case 17: PPC_DECODER_HIT(sc); + case 18: PPC_DECODER_HIT(bx); + case 19: +``` + +**File:** src/xenia/cpu/ppc/ppc_emit_altivec.cc (L36-50) +```text +#define OP(x) ((((uint32_t)(x)) & 0x3f) << 26) +#define VX128(op, xop) (OP(op) | (((uint32_t)(xop)) & 0x3d0)) +#define VX128_1(op, xop) (OP(op) | (((uint32_t)(xop)) & 0x7f3)) +#define VX128_2(op, xop) (OP(op) | (((uint32_t)(xop)) & 0x210)) +#define VX128_3(op, xop) (OP(op) | (((uint32_t)(xop)) & 0x7f0)) +#define VX128_4(op, xop) (OP(op) | (((uint32_t)(xop)) & 0x730)) +#define VX128_5(op, xop) (OP(op) | (((uint32_t)(xop)) & 0x10)) +#define VX128_P(op, xop) (OP(op) | (((uint32_t)(xop)) & 0x630)) + +#define VX128_VD128 (i.VX128.VD128l | (i.VX128.VD128h << 5)) +#define VX128_VA128 \ + (i.VX128.VA128l | (i.VX128.VA128h << 5) | (i.VX128.VA128H << 6)) +#define VX128_VB128 (i.VX128.VB128l | (i.VX128.VB128h << 5)) +#define VX128_1_VD128 (i.VX128_1.VD128l | (i.VX128_1.VD128h << 5)) +#define VX128_2_VD128 (i.VX128_2.VD128l | (i.VX128_2.VD128h << 5)) +``` + +**File:** src/xenia/memory.cc (L137-168) +```text +bool Memory::Initialize() { + file_name_ = fmt::format("xenia_memory_{}", Clock::QueryHostTickCount()); + + // Create main page file-backed mapping. This is all reserved but + // uncommitted (so it shouldn't expand page file). + mapping_ = xe::memory::CreateFileMappingHandle( + file_name_, + // entire 4gb space + 512mb physical: + 0x11FFFFFFF, xe::memory::PageAccess::kReadWrite, false); + if (mapping_ == xe::memory::kFileMappingHandleInvalid) { + XELOGE("Unable to reserve the 4gb guest address space."); + assert_always(); + return false; + } + + // Attempt to create our views. This may fail at the first address + // we pick, so try a few times. + mapping_base_ = 0; + for (size_t n = 32; n < 64; n++) { + auto mapping_base = reinterpret_cast(1ull << n); + if (!MapViews(mapping_base)) { + mapping_base_ = mapping_base; + break; + } + } + if (!mapping_base_) { + XELOGE("Unable to find a continuous block in the 64bit address space."); + assert_always(); + return false; + } + virtual_membase_ = mapping_base_; + physical_membase_ = mapping_base_ + 0x100000000ull; +``` + +**File:** src/xenia/base/byte_order.h (L83-136) +```text +template +struct endian_store { + endian_store() = default; + endian_store(const T& src) { set(src); } + endian_store(const endian_store& other) { set(other); } + operator T() const { return get(); } + + void set(const T& src) { + if constexpr (std::endian::native == E) { + value = src; + } else { + value = xe::byte_swap(src); + } + } + void set(const endian_store& other) { value = other.value; } + T get() const { + if constexpr (std::endian::native == E) { + return value; + } + return xe::byte_swap(value); + } + + endian_store& operator+=(int a) { + *this = *this + a; + return *this; + } + endian_store& operator-=(int a) { + *this = *this - a; + return *this; + } + endian_store& operator++() { + *this += 1; + return *this; + } // ++a + endian_store operator++(int) { + *this += 1; + return (*this - 1); + } // a++ + endian_store& operator--() { + *this -= 1; + return *this; + } // --a + endian_store operator--(int) { + *this -= 1; + return (*this + 1); + } // a-- + + T value; +}; + +template +using be = endian_store; +template +using le = endian_store; +``` + +**File:** src/xenia/kernel/xthread.h (L249-280) +```text +struct X_KTHREAD { + X_DISPATCH_HEADER header; // 0x0 + xe::be unk_10; // 0x10 + xe::be unk_14; // 0x14 + X_KTIMER wait_timeout_timer; // 0x18 + X_KWAIT_BLOCK wait_timeout_block; // 0x40 + uint8_t unk_58[0x4]; // 0x58 + xe::be stack_base; // 0x5C + xe::be stack_limit; // 0x60 + xe::be stack_kernel; // 0x64 + xe::be tls_address; // 0x68 + // state = is thread running, suspended, etc + uint8_t thread_state; // 0x6C + // 0x70 = priority? + uint8_t alerted[2]; // 0x6D + uint8_t alertable; // 0x6F + uint8_t priority; // 0x70 + uint8_t fpu_exceptions_on; // 0x71 + // these two process types both get set to the same thing, process_type is + // referenced most frequently, however process_type_dup gets referenced a few + // times while the process is being created + uint8_t process_type_dup; + uint8_t process_type; + // apc_mode determines which list an apc goes into + util::X_TYPED_LIST apc_lists[2]; + TypedGuestPointer process; // 0x84 + uint8_t executing_kernel_apc; // 0x88 + // when context switch happens, this is copied into + // apc_software_interrupt_state for kpcr + uint8_t deferred_apc_software_interrupt_state; // 0x89 + uint8_t user_apc_pending; // 0x8A + uint8_t may_queue_apcs; // 0x8B +``` + +**File:** src/xenia/gpu/spirv_shader_translator.cc (L153-170) +```text +void SpirvShaderTranslator::StartTranslation() { + // TODO(Triang3l): Logger. + builder_ = std::make_unique( + features_.spirv_version, (kSpirvMagicToolId << 16) | 1, nullptr); + + builder_->addCapability(IsSpirvTessEvalShader() ? spv::CapabilityTessellation + : spv::CapabilityShader); + if (features_.spirv_version < spv::Spv_1_4) { + if (features_.signed_zero_inf_nan_preserve_float32 || + features_.denorm_flush_to_zero_float32 || + features_.rounding_mode_rte_float32) { + builder_->addExtension("SPV_KHR_float_controls"); + } + } + ext_inst_glsl_std_450_ = builder_->import("GLSL.std.450"); + builder_->setMemoryModel(spv::AddressingModelLogical, + spv::MemoryModelGLSL450); + builder_->setSource(spv::SourceLanguageUnknown, 0); +``` + +**File:** src/xenia/gpu/shader_interpreter.h (L26-60) +```text +class ShaderInterpreter { + public: + ShaderInterpreter(const RegisterFile& register_file, const Memory& memory) + : register_file_(register_file), memory_(memory) {} + + class ExportSink { + public: + virtual ~ExportSink() = default; + virtual void AllocExport(ucode::AllocType type, uint32_t size) {} + virtual void Export(ucode::ExportRegister export_register, + const float* value, uint32_t value_mask) {} + }; + + void SetTraceWriter(TraceWriter* new_trace_writer) { + trace_writer_ = new_trace_writer; + } + + ExportSink* GetExportSink() const { return export_sink_; } + void SetExportSink(ExportSink* new_export_sink) { + export_sink_ = new_export_sink; + } + + const float* temp_registers() const { return &temp_registers_[0][0]; } + float* temp_registers() { return &temp_registers_[0][0]; } + + static bool CanInterpretShader(const Shader& shader) { + assert_true(shader.is_ucode_analyzed()); + // Texture instructions are not very common in vertex shaders (and not used + // in Direct3D 9's internal rectangles such as clears) and are extremely + // complex, not implemented. + if (shader.uses_texture_fetch_instruction_results()) { + return false; + } + return true; + } +``` + +**File:** src/xenia/base/mutex.h (L27-87) +```text +class alignas(4096) xe_global_mutex { + XE_MAYBE_UNUSED + char detail[64]; + + public: + xe_global_mutex(); + ~xe_global_mutex(); + + void lock(); + void unlock(); + bool try_lock(); +}; +using global_mutex_type = xe_global_mutex; + +class alignas(64) xe_fast_mutex { + XE_MAYBE_UNUSED + char detail[64]; + + public: + xe_fast_mutex(); + ~xe_fast_mutex(); + + void lock(); + void unlock(); + bool try_lock(); +}; +// a mutex that is extremely unlikely to ever be locked +// use for race conditions that have extremely remote odds of happening +class xe_unlikely_mutex { + std::atomic mut; + bool _tryget() { + uint32_t lock_expected = 0; + return mut.compare_exchange_strong(lock_expected, 1); + } + + public: + xe_unlikely_mutex() : mut(0) {} + ~xe_unlikely_mutex() { mut = 0; } + + void lock() { + if (XE_LIKELY(_tryget())) { + return; + } else { + do { + // chrispy: warning, if no SMT, mm_pause does nothing... +#if XE_ARCH_AMD64 == 1 + _mm_pause(); +#endif + + } while (!_tryget()); + } + } + void unlock() { mut.exchange(0); } + bool try_lock() { return _tryget(); } +}; +using xe_mutex = xe_fast_mutex; +#else +using global_mutex_type = std::recursive_mutex; +using xe_mutex = std::mutex; +using xe_unlikely_mutex = std::mutex; +#endif +``` + +**File:** src/xenia/app/premake5.lua (L31-42) +```lua + "glslang-spirv", + "imgui", + "libavcodec", + "libavutil", + "mspack", + "snappy", + "xxhash", + }) + defines({ + "XBYAK_NO_OP_NAMES", + "XBYAK_ENABLE_OMITTED_OPERAND", + }) +``` diff --git a/xenia-rs/Cargo.lock b/xenia-rs/Cargo.lock new file mode 100644 index 000000000..74d521c85 --- /dev/null +++ b/xenia-rs/Cargo.lock @@ -0,0 +1,626 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clap" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.184" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "xenia-app" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "tracing", + "tracing-subscriber", + "xenia-apu", + "xenia-cpu", + "xenia-debugger", + "xenia-gpu", + "xenia-hid", + "xenia-kernel", + "xenia-memory", + "xenia-types", + "xenia-vfs", + "xenia-xex", +] + +[[package]] +name = "xenia-apu" +version = "0.1.0" +dependencies = [ + "thiserror", + "tracing", + "xenia-types", +] + +[[package]] +name = "xenia-cpu" +version = "0.1.0" +dependencies = [ + "bitflags", + "thiserror", + "tracing", + "xenia-memory", + "xenia-types", +] + +[[package]] +name = "xenia-debugger" +version = "0.1.0" +dependencies = [ + "thiserror", + "tracing", + "xenia-cpu", + "xenia-memory", + "xenia-types", +] + +[[package]] +name = "xenia-gpu" +version = "0.1.0" +dependencies = [ + "anyhow", + "byteorder", + "thiserror", + "tracing", + "xenia-memory", + "xenia-types", +] + +[[package]] +name = "xenia-hid" +version = "0.1.0" +dependencies = [ + "thiserror", + "tracing", + "xenia-types", +] + +[[package]] +name = "xenia-kernel" +version = "0.1.0" +dependencies = [ + "anyhow", + "thiserror", + "tracing", + "xenia-cpu", + "xenia-memory", + "xenia-types", +] + +[[package]] +name = "xenia-memory" +version = "0.1.0" +dependencies = [ + "bitflags", + "libc", + "thiserror", + "tracing", + "windows-sys 0.59.0", + "xenia-types", +] + +[[package]] +name = "xenia-types" +version = "0.1.0" +dependencies = [ + "bitflags", + "byteorder", + "serde", + "thiserror", +] + +[[package]] +name = "xenia-vfs" +version = "0.1.0" +dependencies = [ + "anyhow", + "byteorder", + "thiserror", + "tracing", + "xenia-types", +] + +[[package]] +name = "xenia-xex" +version = "0.1.0" +dependencies = [ + "anyhow", + "byteorder", + "thiserror", + "tracing", + "xenia-memory", + "xenia-types", +] diff --git a/xenia-rs/Cargo.toml b/xenia-rs/Cargo.toml new file mode 100644 index 000000000..7b3f033a5 --- /dev/null +++ b/xenia-rs/Cargo.toml @@ -0,0 +1,42 @@ +[workspace] +resolver = "2" +members = [ + "crates/xenia-types", + "crates/xenia-memory", + "crates/xenia-cpu", + "crates/xenia-xex", + "crates/xenia-vfs", + "crates/xenia-kernel", + "crates/xenia-gpu", + "crates/xenia-apu", + "crates/xenia-hid", + "crates/xenia-debugger", + "crates/xenia-app", +] + +[workspace.package] +version = "0.1.0" +edition = "2024" +license = "BSD-3-Clause" + +[workspace.dependencies] +# Shared types +xenia-types = { path = "crates/xenia-types" } +xenia-memory = { path = "crates/xenia-memory" } +xenia-cpu = { path = "crates/xenia-cpu" } +xenia-xex = { path = "crates/xenia-xex" } +xenia-vfs = { path = "crates/xenia-vfs" } +xenia-kernel = { path = "crates/xenia-kernel" } +xenia-gpu = { path = "crates/xenia-gpu" } +xenia-apu = { path = "crates/xenia-apu" } +xenia-hid = { path = "crates/xenia-hid" } +xenia-debugger = { path = "crates/xenia-debugger" } + +# External dependencies +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +bitflags = "2" +byteorder = "1" +thiserror = "2" +anyhow = "1" +serde = { version = "1", features = ["derive"] } diff --git a/xenia-rs/crates/xenia-app/Cargo.toml b/xenia-rs/crates/xenia-app/Cargo.toml new file mode 100644 index 000000000..14628bfd3 --- /dev/null +++ b/xenia-rs/crates/xenia-app/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "xenia-app" +version.workspace = true +edition.workspace = true +license.workspace = true + +[[bin]] +name = "xenia-rs" +path = "src/main.rs" + +[dependencies] +xenia-types = { workspace = true } +xenia-memory = { workspace = true } +xenia-cpu = { workspace = true } +xenia-xex = { workspace = true } +xenia-vfs = { workspace = true } +xenia-kernel = { workspace = true } +xenia-gpu = { workspace = true } +xenia-apu = { workspace = true } +xenia-hid = { workspace = true } +xenia-debugger = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +anyhow = { workspace = true } +clap = { version = "4", features = ["derive"] } diff --git a/xenia-rs/crates/xenia-app/src/main.rs b/xenia-rs/crates/xenia-app/src/main.rs new file mode 100644 index 000000000..019a90c52 --- /dev/null +++ b/xenia-rs/crates/xenia-app/src/main.rs @@ -0,0 +1,244 @@ +use anyhow::Result; +use clap::{Parser, Subcommand}; +use tracing_subscriber::EnvFilter; + +#[derive(Parser)] +#[command(name = "xenia-rs")] +#[command(about = "Xbox 360 emulator for reverse engineering and preservation")] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + /// Disassemble a XEX file from its entry point + Disasm { + /// Path to XEX file + path: String, + /// Number of instructions to disassemble + #[arg(short = 'n', default_value = "64")] + count: usize, + }, + /// Load and execute a XEX file with tracing + Exec { + /// Path to XEX file + path: String, + /// Maximum instructions to execute before stopping + #[arg(short = 'n', default_value = "1000")] + max_instructions: u64, + }, + /// Browse XISO disc image contents + Browse { + /// Path to XISO file + path: String, + }, + /// Display XEX header information + Info { + /// Path to XEX file + path: String, + }, +} + +fn main() -> Result<()> { + tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env().add_directive("info".parse()?)) + .init(); + + let cli = Cli::parse(); + + match cli.command { + Commands::Disasm { path, count } => cmd_disasm(&path, count), + Commands::Exec { path, max_instructions } => cmd_exec(&path, max_instructions), + Commands::Browse { path } => cmd_browse(&path), + Commands::Info { path } => cmd_info(&path), + } +} + +fn cmd_info(path: &str) -> Result<()> { + let data = std::fs::read(path)?; + let header = xenia_xex::loader::parse_xex2_header(&data)?; + + println!("=== XEX2 Header ==="); + println!("Magic: {:#010x}", header.magic); + println!("Module Flags: {:#010x}", header.module_flags); + println!("Header Size: {:#x}", header.header_size); + println!("Headers: {}", header.header_count); + + if let Some(entry) = xenia_xex::loader::get_entry_point(&header) { + println!("Entry Point: {:#010x}", entry); + } + if let Some(base) = xenia_xex::loader::get_image_base(&header) { + println!("Image Base: {:#010x}", base); + } + + println!("\n=== Optional Headers ==="); + for h in &header.optional_headers { + println!(" Key: {:#010x} Value: {:#010x}", h.key, h.value); + } + + if let Some(ref sec) = header.security_info { + println!("\n=== Security Info ==="); + println!("Image Size: {:#x}", sec.image_size); + println!("Load Address: {:#010x}", sec.load_address); + println!("Image Flags: {:#010x}", sec.image_flags); + println!("Page Descs: {}", sec.page_descriptors.len()); + } + + Ok(()) +} + +fn cmd_disasm(path: &str, count: usize) -> Result<()> { + let data = std::fs::read(path)?; + let header = xenia_xex::loader::parse_xex2_header(&data)?; + + let entry = xenia_xex::loader::get_entry_point(&header) + .ok_or_else(|| anyhow::anyhow!("No entry point found in XEX2 header"))?; + let base = xenia_xex::loader::get_image_base(&header) + .ok_or_else(|| anyhow::anyhow!("No image base found in XEX2 header"))?; + + println!("Entry point: {:#010x}, Image base: {:#010x}", entry, base); + 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); + for (addr, text) in block { + println!(" {:#010x}: {}", addr, text); + } + } else { + println!(" (entry point offset {:#x} is outside file bounds)", entry_offset); + } + + Ok(()) +} + +fn cmd_exec(path: &str, max_instructions: u64) -> Result<()> { + let data = std::fs::read(path)?; + let header = xenia_xex::loader::parse_xex2_header(&data)?; + + let entry = xenia_xex::loader::get_entry_point(&header) + .ok_or_else(|| anyhow::anyhow!("No entry point found"))?; + let base = xenia_xex::loader::get_image_base(&header) + .ok_or_else(|| anyhow::anyhow!("No image base found"))?; + + 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..]; + 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); + + // Allocate stack (1MB at 0x70000000) + let stack_base = 0x7000_0000u32; + let stack_size = 0x10_0000u32; + mem.alloc( + stack_base, + stack_size, + xenia_memory::page_table::MemoryProtect::READ | xenia_memory::page_table::MemoryProtect::WRITE, + ).map_err(|e| anyhow::anyhow!("Failed to allocate stack: {}", e))?; + + // Set up CPU context + let mut ctx = xenia_cpu::PpcContext::new(); + ctx.pc = entry; + ctx.gpr[1] = (stack_base + stack_size - 0x80) as u64; // Stack pointer (with red zone) + ctx.gpr[13] = 0; // Small data area (TLS) + + // Set up kernel + let mut _kernel = xenia_kernel::KernelState::new(); + + // Set up debugger + let mut debugger = xenia_debugger::Debugger::new(); + debugger.paused = false; + debugger.step_mode = xenia_debugger::StepMode::Run; + debugger.trace_enabled = true; + + println!("Starting execution (max {} instructions)...\n", max_instructions); + + use xenia_cpu::interpreter::{step, StepResult}; + + let mut instruction_count: u64 = 0; + loop { + if instruction_count >= max_instructions { + println!("\nReached max instruction count ({})", max_instructions); + break; + } + + // Pre-step debugger + debugger.pre_step(&ctx, &mem); + + let result = step(&mut ctx, &mut mem); + instruction_count += 1; + + // Post-step debugger + debugger.post_step(&ctx, &mem); + + match result { + StepResult::Continue => {} + StepResult::SystemCall => { + println!("[{:>8}] SYSCALL at {:#010x}", instruction_count, ctx.pc.wrapping_sub(4)); + } + StepResult::Unimplemented(op) => { + println!("[{:>8}] UNIMPL: {:?} at {:#010x}", instruction_count, op, ctx.pc.wrapping_sub(4)); + } + StepResult::Trap => { + println!("[{:>8}] TRAP at {:#010x}", instruction_count, ctx.pc.wrapping_sub(4)); + } + StepResult::Halted => { + println!("[{:>8}] HALTED", instruction_count); + break; + } + } + + if debugger.should_break() { + println!("[{:>8}] BREAK at {:#010x}", instruction_count, ctx.pc); + break; + } + } + + println!("\n=== Final State ==="); + println!("PC: {:#010x}", ctx.pc); + println!("LR: {:#010x}", ctx.lr as u32); + println!("CTR: {:#010x}", ctx.ctr as u32); + println!("CR: {:#010x}", ctx.cr()); + println!("XER: CA={} OV={} SO={}", ctx.xer_ca, ctx.xer_ov, ctx.xer_so); + for i in 0..32 { + if ctx.gpr[i] != 0 { + println!("r{:<2}: {:#018x}", i, ctx.gpr[i]); + } + } + println!("\nExecuted {} instructions", instruction_count); + println!("Trace log: {} entries", debugger.trace_log.len()); + + Ok(()) +} + +fn cmd_browse(path: &str) -> Result<()> { + use xenia_vfs::VfsDevice; + + 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))?; + + println!("=== XISO Contents: {} ===", path); + match disc.list_root() { + Ok(entries) => { + for entry in entries { + let kind = if entry.is_directory { "DIR " } else { "FILE" }; + println!(" {} {:>10} {}", kind, entry.size, entry.name); + } + } + Err(e) => println!(" Error listing contents: {}", e), + } + + Ok(()) +} diff --git a/xenia-rs/crates/xenia-apu/Cargo.toml b/xenia-rs/crates/xenia-apu/Cargo.toml new file mode 100644 index 000000000..7da119cf9 --- /dev/null +++ b/xenia-rs/crates/xenia-apu/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "xenia-apu" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +xenia-types = { workspace = true } +tracing = { workspace = true } +thiserror = { workspace = true } diff --git a/xenia-rs/crates/xenia-apu/src/lib.rs b/xenia-rs/crates/xenia-apu/src/lib.rs new file mode 100644 index 000000000..885083c21 --- /dev/null +++ b/xenia-rs/crates/xenia-apu/src/lib.rs @@ -0,0 +1,16 @@ +/// Audio processing unit stub. Logging only for now. +pub struct AudioSystem { + pub enabled: bool, +} + +impl AudioSystem { + pub fn new() -> Self { + Self { enabled: false } + } +} + +impl Default for AudioSystem { + fn default() -> Self { + Self::new() + } +} diff --git a/xenia-rs/crates/xenia-cpu/Cargo.toml b/xenia-rs/crates/xenia-cpu/Cargo.toml new file mode 100644 index 000000000..3ca488b6e --- /dev/null +++ b/xenia-rs/crates/xenia-cpu/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "xenia-cpu" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +xenia-types = { workspace = true } +xenia-memory = { workspace = true } +tracing = { workspace = true } +bitflags = { workspace = true } +thiserror = { workspace = true } diff --git a/xenia-rs/crates/xenia-cpu/src/context.rs b/xenia-rs/crates/xenia-cpu/src/context.rs new file mode 100644 index 000000000..b500c5000 --- /dev/null +++ b/xenia-rs/crates/xenia-cpu/src/context.rs @@ -0,0 +1,191 @@ +use xenia_types::Vec128; + +/// Condition register field (one of CR0-CR7). +#[derive(Debug, Clone, Copy, Default)] +pub struct CrField { + pub lt: bool, + pub gt: bool, + pub eq: bool, + pub so: bool, +} + +impl CrField { + pub fn as_u8(&self) -> u8 { + ((self.lt as u8) << 3) | ((self.gt as u8) << 2) | ((self.eq as u8) << 1) | (self.so as u8) + } + + pub fn from_u8(val: u8) -> Self { + Self { + lt: val & 8 != 0, + gt: val & 4 != 0, + eq: val & 2 != 0, + so: val & 1 != 0, + } + } +} + +/// SPR (Special Purpose Register) numbers used by mfspr/mtspr. +pub mod spr { + pub const XER: u32 = 1; + pub const LR: u32 = 8; + pub const CTR: u32 = 9; + pub const TBL: u32 = 268; + pub const TBU: u32 = 269; + pub const SPRG0: u32 = 272; + pub const SPRG1: u32 = 273; + pub const SPRG2: u32 = 274; + pub const SPRG3: u32 = 275; + pub const PVR: u32 = 287; + pub const PIR: u32 = 1023; +} + +/// PowerPC processor context. Holds all register state for one guest thread. +/// Mirrors PPCContext from ppc_context.h, minus JIT-specific fields. +#[repr(C, align(64))] +pub struct PpcContext { + // General purpose registers (R0-R31) + pub gpr: [u64; 32], + // Count register + pub ctr: u64, + // Link register + pub lr: u64, + // Machine state register + pub msr: u64, + // Floating-point registers (F0-F31) + pub fpr: [f64; 32], + // VMX128 vector registers (V0-V127, Xbox 360 extended set) + pub vr: [Vec128; 128], + + // Condition register fields (CR0-CR7) + pub cr: [CrField; 8], + // Floating-point status and control register + pub fpscr: u32, + // XER register (split for easy individual updates) + pub xer_ca: u8, + pub xer_ov: u8, + pub xer_so: u8, + // Altivec VSCR saturation bit + pub vscr_sat: u8, + + // Program counter + pub pc: u32, + // Reservation address/value for lwarx/stwcx + pub reserved_addr: u32, + pub reserved_val: u64, + pub has_reservation: bool, + + // Thread ID (for kernel use) + pub thread_id: u32, + + // Cycle counter for timing + pub cycle_count: u64, + + // Time base (incremented each instruction for debugging) + pub timebase: u64, +} + +impl PpcContext { + pub fn new() -> Self { + Self { + gpr: [0; 32], + ctr: 0, + lr: 0, + msr: 0, + fpr: [0.0; 32], + vr: [Vec128::ZERO; 128], + cr: [CrField::default(); 8], + fpscr: 0, + xer_ca: 0, + xer_ov: 0, + xer_so: 0, + vscr_sat: 0, + pc: 0, + reserved_addr: 0, + reserved_val: 0, + has_reservation: false, + thread_id: 0, + cycle_count: 0, + timebase: 0, + } + } + + /// Get the full 32-bit condition register. + pub fn cr(&self) -> u32 { + let mut val = 0u32; + for (i, field) in self.cr.iter().enumerate() { + val |= (field.as_u8() as u32) << (28 - i * 4); + } + val + } + + /// Set the full 32-bit condition register. + pub fn set_cr(&mut self, val: u32) { + for i in 0..8 { + self.cr[i] = CrField::from_u8(((val >> (28 - i * 4)) & 0xF) as u8); + } + } + + /// Get a single CR bit by absolute bit number (0-31). + pub fn get_cr_bit(&self, bit: u32) -> bool { + let field = (bit / 4) as usize; + let sub = bit % 4; + match sub { + 0 => self.cr[field].lt, + 1 => self.cr[field].gt, + 2 => self.cr[field].eq, + 3 => self.cr[field].so, + _ => unreachable!(), + } + } + + /// Set a single CR bit by absolute bit number (0-31). + pub fn set_cr_bit(&mut self, bit: u32, val: bool) { + let field = (bit / 4) as usize; + let sub = bit % 4; + match sub { + 0 => self.cr[field].lt = val, + 1 => self.cr[field].gt = val, + 2 => self.cr[field].eq = val, + 3 => self.cr[field].so = val, + _ => unreachable!(), + } + } + + /// Update a condition register field based on a comparison result (signed). + pub fn update_cr_signed(&mut self, field: usize, val: i64) { + self.cr[field] = CrField { + lt: val < 0, + gt: val > 0, + eq: val == 0, + so: self.xer_so != 0, + }; + } + + /// Update a condition register field based on a comparison result (unsigned). + pub fn update_cr_unsigned(&mut self, field: usize, a: u64, b: u64) { + self.cr[field] = CrField { + lt: a < b, + gt: a > b, + eq: a == b, + so: self.xer_so != 0, + }; + } + + /// Get the full XER register value. + pub fn xer(&self) -> u32 { + ((self.xer_so as u32) << 31) | ((self.xer_ov as u32) << 30) | ((self.xer_ca as u32) << 29) + } + + /// Set XER from a full 32-bit value. + pub fn set_xer(&mut self, val: u32) { + self.xer_so = ((val >> 31) & 1) as u8; + self.xer_ov = ((val >> 30) & 1) as u8; + self.xer_ca = ((val >> 29) & 1) as u8; + } +} + +impl Default for PpcContext { + fn default() -> Self { + Self::new() + } +} diff --git a/xenia-rs/crates/xenia-cpu/src/decoder.rs b/xenia-rs/crates/xenia-cpu/src/decoder.rs new file mode 100644 index 000000000..c690226b1 --- /dev/null +++ b/xenia-rs/crates/xenia-cpu/src/decoder.rs @@ -0,0 +1,819 @@ +use crate::opcode::PpcOpcode; + +/// Extract bits [a..=b] from a 32-bit value (PPC bit numbering: 0 = MSB). +#[inline(always)] +const fn extract_bits(v: u32, a: u32, b: u32) -> u32 { + (v >> (32 - 1 - b)) & ((1 << (b - a + 1)) - 1) +} + +/// Decoded PPC instruction with extracted operand fields. +#[derive(Debug, Clone, Copy)] +pub struct DecodedInstr { + pub opcode: PpcOpcode, + pub raw: u32, + pub addr: u32, +} + +impl DecodedInstr { + // Common field extractors (PPC bit numbering) + + /// Primary opcode (bits 0-5) + #[inline] pub fn op(&self) -> u32 { extract_bits(self.raw, 0, 5) } + + /// rD/rS/rT (bits 6-10) - destination/source register + #[inline] pub fn rd(&self) -> usize { extract_bits(self.raw, 6, 10) as usize } + #[inline] pub fn rs(&self) -> usize { self.rd() } + #[inline] pub fn rt(&self) -> usize { self.rd() } + + /// rA (bits 11-15) + #[inline] pub fn ra(&self) -> usize { extract_bits(self.raw, 11, 15) as usize } + + /// rB (bits 16-20) + #[inline] pub fn rb(&self) -> usize { extract_bits(self.raw, 16, 20) as usize } + + /// rC (bits 21-25) - for 4-operand instructions + #[inline] pub fn rc(&self) -> usize { extract_bits(self.raw, 21, 25) as usize } + + /// SIMM/UIMM (bits 16-31) - signed/unsigned immediate + #[inline] pub fn simm16(&self) -> i16 { (self.raw & 0xFFFF) as i16 } + #[inline] pub fn uimm16(&self) -> u16 { (self.raw & 0xFFFF) as u16 } + + /// D-form displacement (signed, bits 16-31) + #[inline] pub fn d(&self) -> i32 { self.simm16() as i32 } + + /// DS-form displacement (signed, bits 16-29, shifted left 2) + #[inline] pub fn ds(&self) -> i32 { (self.raw & 0xFFFC) as i16 as i32 } + + /// LI field for branch (bits 6-29, sign-extended, shifted left 2) + #[inline] pub fn li(&self) -> i32 { + let li = extract_bits(self.raw, 6, 29); + // Sign-extend from 24 bits, then shift left 2 + let sign_extended = ((li as i32) << 8) >> 8; + sign_extended << 2 + } + + /// BD field for conditional branch (bits 16-29, sign-extended, shifted left 2) + #[inline] pub fn bd(&self) -> i32 { + let bd = extract_bits(self.raw, 16, 29); + let sign_extended = ((bd as i32) << 18) >> 18; + sign_extended << 2 + } + + /// BO field (bits 6-10) - branch options + #[inline] pub fn bo(&self) -> u32 { extract_bits(self.raw, 6, 10) } + + /// BI field (bits 11-15) - branch condition + #[inline] pub fn bi(&self) -> u32 { extract_bits(self.raw, 11, 15) } + + /// AA bit (bit 30) - absolute address + #[inline] pub fn aa(&self) -> bool { (self.raw >> 1) & 1 != 0 } + + /// LK bit (bit 31) - link (update LR) + #[inline] pub fn lk(&self) -> bool { self.raw & 1 != 0 } + + /// Rc bit (bit 31) - record CR0 + #[inline] pub fn rc_bit(&self) -> bool { self.raw & 1 != 0 } + + /// OE bit (bit 21) - overflow enable + #[inline] pub fn oe(&self) -> bool { extract_bits(self.raw, 21, 21) != 0 } + + /// MB, ME fields for rotate instructions + #[inline] pub fn mb(&self) -> u32 { extract_bits(self.raw, 21, 25) } + #[inline] pub fn me(&self) -> u32 { extract_bits(self.raw, 26, 30) } + + /// SH field (bits 16-20) for shift instructions + #[inline] pub fn sh(&self) -> u32 { extract_bits(self.raw, 16, 20) } + + /// SH field for 64-bit shifts (bits 16-20 + bit 30) + #[inline] pub fn sh64(&self) -> u32 { + (extract_bits(self.raw, 16, 20) << 1) | extract_bits(self.raw, 30, 30) + } + + /// SPR field (bits 11-20, swapped halves) + #[inline] pub fn spr(&self) -> u32 { + let spr_raw = extract_bits(self.raw, 11, 20); + ((spr_raw & 0x1F) << 5) | ((spr_raw >> 5) & 0x1F) + } + + /// CRM field (bits 12-19) for mtcrf + #[inline] pub fn crm(&self) -> u32 { extract_bits(self.raw, 12, 19) } + + /// crfD (bits 6-8) - condition register field destination + #[inline] pub fn crfd(&self) -> usize { extract_bits(self.raw, 6, 8) as usize } + + /// crfS (bits 11-13) + #[inline] pub fn crfs(&self) -> usize { extract_bits(self.raw, 11, 13) as usize } + + /// L bit (bit 10) - 64-bit compare + #[inline] pub fn l(&self) -> bool { extract_bits(self.raw, 10, 10) != 0 } + + /// crbD (bits 6-10) + #[inline] pub fn crbd(&self) -> u32 { extract_bits(self.raw, 6, 10) } + /// crbA (bits 11-15) + #[inline] pub fn crba(&self) -> u32 { extract_bits(self.raw, 11, 15) } + /// crbB (bits 16-20) + #[inline] pub fn crbb(&self) -> u32 { extract_bits(self.raw, 16, 20) } + + // VMX128 field extractors + + /// VA128 (bits 6-10, plus bit from 29) + #[inline] pub fn va128(&self) -> usize { + (extract_bits(self.raw, 6, 10) | (extract_bits(self.raw, 29, 29) << 5)) as usize + } + + /// VB128 (bits 16-20, plus bits from 28, 30) + #[inline] pub fn vb128(&self) -> usize { + (extract_bits(self.raw, 16, 20) + | (extract_bits(self.raw, 28, 28) << 5) + | (extract_bits(self.raw, 30, 30) << 6)) as usize + } + + /// VD128 (bits 6-10, plus bits from 21, 22) + #[inline] pub fn vd128(&self) -> usize { + (extract_bits(self.raw, 6, 10) + | (extract_bits(self.raw, 21, 21) << 5) + | (extract_bits(self.raw, 22, 22) << 6)) as usize + } + + /// VS128 - same encoding as VD128 + #[inline] pub fn vs128(&self) -> usize { self.vd128() } + + /// NB field (bits 16-20) for lswi/stswi + #[inline] pub fn nb(&self) -> u32 { extract_bits(self.raw, 16, 20) } +} + +/// Decode a 32-bit PPC instruction into its opcode. +/// Direct translation of the C++ LookupOpcode from ppc_opcode_lookup_gen.cc. +pub fn decode(raw: u32, addr: u32) -> DecodedInstr { + let opcode = lookup_opcode(raw); + DecodedInstr { opcode, raw, addr } +} + +fn lookup_opcode(code: u32) -> PpcOpcode { + match extract_bits(code, 0, 5) { + 2 => PpcOpcode::tdi, + 3 => PpcOpcode::twi, + 4 => decode_op4(code), + 5 => decode_op5(code), + 6 => decode_op6(code), + 7 => PpcOpcode::mulli, + 8 => PpcOpcode::subficx, + 10 => PpcOpcode::cmpli, + 11 => PpcOpcode::cmpi, + 12 => PpcOpcode::addic, + 13 => PpcOpcode::addicx, + 14 => PpcOpcode::addi, + 15 => PpcOpcode::addis, + 16 => PpcOpcode::bcx, + 17 => PpcOpcode::sc, + 18 => PpcOpcode::bx, + 19 => decode_op19(code), + 20 => PpcOpcode::rlwimix, + 21 => PpcOpcode::rlwinmx, + 23 => PpcOpcode::rlwnmx, + 24 => PpcOpcode::ori, + 25 => PpcOpcode::oris, + 26 => PpcOpcode::xori, + 27 => PpcOpcode::xoris, + 28 => PpcOpcode::andix, + 29 => PpcOpcode::andisx, + 30 => decode_op30(code), + 31 => decode_op31(code), + 32 => PpcOpcode::lwz, + 33 => PpcOpcode::lwzu, + 34 => PpcOpcode::lbz, + 35 => PpcOpcode::lbzu, + 36 => PpcOpcode::stw, + 37 => PpcOpcode::stwu, + 38 => PpcOpcode::stb, + 39 => PpcOpcode::stbu, + 40 => PpcOpcode::lhz, + 41 => PpcOpcode::lhzu, + 42 => PpcOpcode::lha, + 43 => PpcOpcode::lhau, + 44 => PpcOpcode::sth, + 45 => PpcOpcode::sthu, + 46 => PpcOpcode::lmw, + 47 => PpcOpcode::stmw, + 48 => PpcOpcode::lfs, + 49 => PpcOpcode::lfsu, + 50 => PpcOpcode::lfd, + 51 => PpcOpcode::lfdu, + 52 => PpcOpcode::stfs, + 53 => PpcOpcode::stfsu, + 54 => PpcOpcode::stfd, + 55 => PpcOpcode::stfdu, + 58 => match extract_bits(code, 30, 31) { + 0b00 => PpcOpcode::ld, + 0b01 => PpcOpcode::ldu, + 0b10 => PpcOpcode::lwa, + _ => PpcOpcode::Invalid, + }, + 59 => match extract_bits(code, 26, 30) { + 0b10010 => PpcOpcode::fdivsx, + 0b10100 => PpcOpcode::fsubsx, + 0b10101 => PpcOpcode::faddsx, + 0b10110 => PpcOpcode::fsqrtsx, + 0b11000 => PpcOpcode::fresx, + 0b11001 => PpcOpcode::fmulsx, + 0b11100 => PpcOpcode::fmsubsx, + 0b11101 => PpcOpcode::fmaddsx, + 0b11110 => PpcOpcode::fnmsubsx, + 0b11111 => PpcOpcode::fnmaddsx, + _ => PpcOpcode::Invalid, + }, + 62 => match extract_bits(code, 30, 31) { + 0b00 => PpcOpcode::std, + 0b01 => PpcOpcode::stdu, + _ => PpcOpcode::Invalid, + }, + 63 => decode_op63(code), + _ => PpcOpcode::Invalid, + } +} + +fn decode_op4(code: u32) -> PpcOpcode { + // VMX128 load/store (op=4, bits 21-27 << 4 | bits 30-31) + let key1 = (extract_bits(code, 21, 27) << 4) | extract_bits(code, 30, 31); + match key1 { + 0b00000000011 => return PpcOpcode::lvsl128, + 0b00001000011 => return PpcOpcode::lvsr128, + 0b00010000011 => return PpcOpcode::lvewx128, + 0b00011000011 => return PpcOpcode::lvx128, + 0b00110000011 => return PpcOpcode::stvewx128, + 0b00111000011 => return PpcOpcode::stvx128, + 0b01011000011 => return PpcOpcode::lvxl128, + 0b01111000011 => return PpcOpcode::stvxl128, + 0b10000000011 => return PpcOpcode::lvlx128, + 0b10001000011 => return PpcOpcode::lvrx128, + 0b10100000011 => return PpcOpcode::stvlx128, + 0b10101000011 => return PpcOpcode::stvrx128, + 0b11000000011 => return PpcOpcode::lvlxl128, + 0b11001000011 => return PpcOpcode::lvrxl128, + 0b11100000011 => return PpcOpcode::stvlxl128, + 0b11101000011 => return PpcOpcode::stvrxl128, + _ => {} + } + + // Standard VMX (op=4, bits 21-31) + let key2 = extract_bits(code, 21, 31); + match key2 { + 0b00000000000 => return PpcOpcode::vaddubm, + 0b00000000010 => return PpcOpcode::vmaxub, + 0b00000000100 => return PpcOpcode::vrlb, + 0b00000001000 => return PpcOpcode::vmuloub, + 0b00000001010 => return PpcOpcode::vaddfp, + 0b00000001100 => return PpcOpcode::vmrghb, + 0b00000001110 => return PpcOpcode::vpkuhum, + 0b00001000000 => return PpcOpcode::vadduhm, + 0b00001000010 => return PpcOpcode::vmaxuh, + 0b00001000100 => return PpcOpcode::vrlh, + 0b00001001000 => return PpcOpcode::vmulouh, + 0b00001001010 => return PpcOpcode::vsubfp, + 0b00001001100 => return PpcOpcode::vmrghh, + 0b00001001110 => return PpcOpcode::vpkuwum, + 0b00010000000 => return PpcOpcode::vadduwm, + 0b00010000010 => return PpcOpcode::vmaxuw, + 0b00010000100 => return PpcOpcode::vrlw, + 0b00010001100 => return PpcOpcode::vmrghw, + 0b00010001110 => return PpcOpcode::vpkuhus, + 0b00011001110 => return PpcOpcode::vpkuwus, + 0b00100000010 => return PpcOpcode::vmaxsb, + 0b00100000100 => return PpcOpcode::vslb, + 0b00100001000 => return PpcOpcode::vmulosb, + 0b00100001010 => return PpcOpcode::vrefp, + 0b00100001100 => return PpcOpcode::vmrglb, + 0b00100001110 => return PpcOpcode::vpkshus, + 0b00101000010 => return PpcOpcode::vmaxsh, + 0b00101000100 => return PpcOpcode::vslh, + 0b00101001000 => return PpcOpcode::vmulosh, + 0b00101001010 => return PpcOpcode::vrsqrtefp, + 0b00101001100 => return PpcOpcode::vmrglh, + 0b00101001110 => return PpcOpcode::vpkswus, + 0b00110000000 => return PpcOpcode::vaddcuw, + 0b00110000010 => return PpcOpcode::vmaxsw, + 0b00110000100 => return PpcOpcode::vslw, + 0b00110001010 => return PpcOpcode::vexptefp, + 0b00110001100 => return PpcOpcode::vmrglw, + 0b00110001110 => return PpcOpcode::vpkshss, + 0b00111000100 => return PpcOpcode::vsl, + 0b00111001010 => return PpcOpcode::vlogefp, + 0b00111001110 => return PpcOpcode::vpkswss, + 0b01000000000 => return PpcOpcode::vaddubs, + 0b01000000010 => return PpcOpcode::vminub, + 0b01000000100 => return PpcOpcode::vsrb, + 0b01000001000 => return PpcOpcode::vmuleub, + 0b01000001010 => return PpcOpcode::vrfin, + 0b01000001100 => return PpcOpcode::vspltb, + 0b01000001110 => return PpcOpcode::vupkhsb, + 0b01001000000 => return PpcOpcode::vadduhs, + 0b01001000010 => return PpcOpcode::vminuh, + 0b01001000100 => return PpcOpcode::vsrh, + 0b01001001000 => return PpcOpcode::vmuleuh, + 0b01001001010 => return PpcOpcode::vrfiz, + 0b01001001100 => return PpcOpcode::vsplth, + 0b01001001110 => return PpcOpcode::vupkhsh, + 0b01010000000 => return PpcOpcode::vadduws, + 0b01010000010 => return PpcOpcode::vminuw, + 0b01010000100 => return PpcOpcode::vsrw, + 0b01010001010 => return PpcOpcode::vrfip, + 0b01010001100 => return PpcOpcode::vspltw, + 0b01010001110 => return PpcOpcode::vupklsb, + 0b01011000100 => return PpcOpcode::vsr, + 0b01011001010 => return PpcOpcode::vrfim, + 0b01011001110 => return PpcOpcode::vupklsh, + 0b01100000000 => return PpcOpcode::vaddsbs, + 0b01100000010 => return PpcOpcode::vminsb, + 0b01100000100 => return PpcOpcode::vsrab, + 0b01100001000 => return PpcOpcode::vmulesb, + 0b01100001010 => return PpcOpcode::vcfux, + 0b01100001100 => return PpcOpcode::vspltisb, + 0b01100001110 => return PpcOpcode::vpkpx, + 0b01101000000 => return PpcOpcode::vaddshs, + 0b01101000010 => return PpcOpcode::vminsh, + 0b01101000100 => return PpcOpcode::vsrah, + 0b01101001000 => return PpcOpcode::vmulesh, + 0b01101001010 => return PpcOpcode::vcfsx, + 0b01101001100 => return PpcOpcode::vspltish, + 0b01101001110 => return PpcOpcode::vupkhpx, + 0b01110000000 => return PpcOpcode::vaddsws, + 0b01110000010 => return PpcOpcode::vminsw, + 0b01110000100 => return PpcOpcode::vsraw, + 0b01110001010 => return PpcOpcode::vctuxs, + 0b01110001100 => return PpcOpcode::vspltisw, + 0b01111001010 => return PpcOpcode::vctsxs, + 0b01111001110 => return PpcOpcode::vupklpx, + 0b10000000000 => return PpcOpcode::vsububm, + 0b10000000010 => return PpcOpcode::vavgub, + 0b10000000100 => return PpcOpcode::vand, + 0b10000001010 => return PpcOpcode::vmaxfp, + 0b10000001100 => return PpcOpcode::vslo, + 0b10001000000 => return PpcOpcode::vsubuhm, + 0b10001000010 => return PpcOpcode::vavguh, + 0b10001000100 => return PpcOpcode::vandc, + 0b10001001010 => return PpcOpcode::vminfp, + 0b10001001100 => return PpcOpcode::vsro, + 0b10010000000 => return PpcOpcode::vsubuwm, + 0b10010000010 => return PpcOpcode::vavguw, + 0b10010000100 => return PpcOpcode::vor, + 0b10011000100 => return PpcOpcode::vxor, + 0b10100000010 => return PpcOpcode::vavgsb, + 0b10100000100 => return PpcOpcode::vnor, + 0b10101000010 => return PpcOpcode::vavgsh, + 0b10110000000 => return PpcOpcode::vsubcuw, + 0b10110000010 => return PpcOpcode::vavgsw, + 0b11000000000 => return PpcOpcode::vsububs, + 0b11000000100 => return PpcOpcode::mfvscr, + 0b11000001000 => return PpcOpcode::vsum4ubs, + 0b11001000000 => return PpcOpcode::vsubuhs, + 0b11001000100 => return PpcOpcode::mtvscr, + 0b11001001000 => return PpcOpcode::vsum4shs, + 0b11010000000 => return PpcOpcode::vsubuws, + 0b11010001000 => return PpcOpcode::vsum2sws, + 0b11100000000 => return PpcOpcode::vsubsbs, + 0b11100001000 => return PpcOpcode::vsum4sbs, + 0b11101000000 => return PpcOpcode::vsubshs, + 0b11110000000 => return PpcOpcode::vsubsws, + 0b11110001000 => return PpcOpcode::vsumsws, + _ => {} + } + + // VMX compare (op=4, bits 22-31) + let key3 = extract_bits(code, 22, 31); + match key3 { + 0b0000000110 => return PpcOpcode::vcmpequb, + 0b0001000110 => return PpcOpcode::vcmpequh, + 0b0010000110 => return PpcOpcode::vcmpequw, + 0b0011000110 => return PpcOpcode::vcmpeqfp, + 0b0111000110 => return PpcOpcode::vcmpgefp, + 0b1000000110 => return PpcOpcode::vcmpgtub, + 0b1001000110 => return PpcOpcode::vcmpgtuh, + 0b1010000110 => return PpcOpcode::vcmpgtuw, + 0b1011000110 => return PpcOpcode::vcmpgtfp, + 0b1100000110 => return PpcOpcode::vcmpgtsb, + 0b1101000110 => return PpcOpcode::vcmpgtsh, + 0b1110000110 => return PpcOpcode::vcmpgtsw, + 0b1111000110 => return PpcOpcode::vcmpbfp, + _ => {} + } + + // VMX 4-operand (op=4, bits 26-31) + let key4 = extract_bits(code, 26, 31); + match key4 { + 0b100000 => return PpcOpcode::vmhaddshs, + 0b100001 => return PpcOpcode::vmhraddshs, + 0b100010 => return PpcOpcode::vmladduhm, + 0b100100 => return PpcOpcode::vmsumubm, + 0b100101 => return PpcOpcode::vmsummbm, + 0b100110 => return PpcOpcode::vmsumuhm, + 0b100111 => return PpcOpcode::vmsumuhs, + 0b101000 => return PpcOpcode::vmsumshm, + 0b101001 => return PpcOpcode::vmsumshs, + 0b101010 => return PpcOpcode::vsel, + 0b101011 => return PpcOpcode::vperm, + 0b101100 => return PpcOpcode::vsldoi, + 0b101110 => return PpcOpcode::vmaddfp, + 0b101111 => return PpcOpcode::vnmsubfp, + _ => {} + } + + // vsldoi128 (op=4, bit 27) + if extract_bits(code, 27, 27) == 1 { + return PpcOpcode::vsldoi128; + } + + PpcOpcode::Invalid +} + +fn decode_op5(code: u32) -> PpcOpcode { + // vperm128 (op=5, bits 22,27) + let key1 = (extract_bits(code, 22, 22) << 5) | extract_bits(code, 27, 27); + if key1 == 0b000000 { + return PpcOpcode::vperm128; + } + + let key2 = (extract_bits(code, 22, 25) << 2) | extract_bits(code, 27, 27); + match key2 { + 0b000001 => PpcOpcode::vaddfp128, + 0b000101 => PpcOpcode::vsubfp128, + 0b001001 => PpcOpcode::vmulfp128, + 0b001101 => PpcOpcode::vmaddfp128, + 0b010001 => PpcOpcode::vmaddcfp128, + 0b010101 => PpcOpcode::vnmsubfp128, + 0b011001 => PpcOpcode::vmsum3fp128, + 0b011101 => PpcOpcode::vmsum4fp128, + 0b100000 => PpcOpcode::vpkshss128, + 0b100001 => PpcOpcode::vand128, + 0b100100 => PpcOpcode::vpkshus128, + 0b100101 => PpcOpcode::vandc128, + 0b101000 => PpcOpcode::vpkswss128, + 0b101001 => PpcOpcode::vnor128, + 0b101100 => PpcOpcode::vpkswus128, + 0b101101 => PpcOpcode::vor128, + 0b110000 => PpcOpcode::vpkuhum128, + 0b110001 => PpcOpcode::vxor128, + 0b110100 => PpcOpcode::vpkuhus128, + 0b110101 => PpcOpcode::vsel128, + 0b111000 => PpcOpcode::vpkuwum128, + 0b111001 => PpcOpcode::vslo128, + 0b111100 => PpcOpcode::vpkuwus128, + 0b111101 => PpcOpcode::vsro128, + _ => PpcOpcode::Invalid, + } +} + +fn decode_op6(code: u32) -> PpcOpcode { + // vpermwi128 + let key1 = (extract_bits(code, 21, 22) << 5) | extract_bits(code, 26, 27); + if key1 == 0b0100001 { + return PpcOpcode::vpermwi128; + } + + // vpkd3d128, vrlimi128 + let key2 = (extract_bits(code, 21, 23) << 4) | extract_bits(code, 26, 27); + match key2 { + 0b1100001 => return PpcOpcode::vpkd3d128, + 0b1110001 => return PpcOpcode::vrlimi128, + _ => {} + } + + // Unary VMX128 ops + let key3 = extract_bits(code, 21, 27); + match key3 { + 0b0100011 => return PpcOpcode::vcfpsxws128, + 0b0100111 => return PpcOpcode::vcfpuxws128, + 0b0101011 => return PpcOpcode::vcsxwfp128, + 0b0101111 => return PpcOpcode::vcuxwfp128, + 0b0110011 => return PpcOpcode::vrfim128, + 0b0110111 => return PpcOpcode::vrfin128, + 0b0111011 => return PpcOpcode::vrfip128, + 0b0111111 => return PpcOpcode::vrfiz128, + 0b1100011 => return PpcOpcode::vrefp128, + 0b1100111 => return PpcOpcode::vrsqrtefp128, + 0b1101011 => return PpcOpcode::vexptefp128, + 0b1101111 => return PpcOpcode::vlogefp128, + 0b1110011 => return PpcOpcode::vspltw128, + 0b1110111 => return PpcOpcode::vspltisw128, + 0b1111111 => return PpcOpcode::vupkd3d128, + _ => {} + } + + // VMX128 compare + let key4 = (extract_bits(code, 22, 24) << 3) | extract_bits(code, 27, 27); + match key4 { + 0b000000 => return PpcOpcode::vcmpeqfp128, + 0b001000 => return PpcOpcode::vcmpgefp128, + 0b010000 => return PpcOpcode::vcmpgtfp128, + 0b011000 => return PpcOpcode::vcmpbfp128, + 0b100000 => return PpcOpcode::vcmpequw128, + _ => {} + } + + // VMX128 shift/merge + let key5 = (extract_bits(code, 22, 25) << 2) | extract_bits(code, 27, 27); + match key5 { + 0b000101 => return PpcOpcode::vrlw128, + 0b001101 => return PpcOpcode::vslw128, + 0b010101 => return PpcOpcode::vsraw128, + 0b011101 => return PpcOpcode::vsrw128, + 0b101000 => return PpcOpcode::vmaxfp128, + 0b101100 => return PpcOpcode::vminfp128, + 0b110000 => return PpcOpcode::vmrghw128, + 0b110100 => return PpcOpcode::vmrglw128, + 0b111000 => return PpcOpcode::vupkhsb128, + 0b111100 => return PpcOpcode::vupklsb128, + _ => {} + } + + PpcOpcode::Invalid +} + +fn decode_op19(code: u32) -> PpcOpcode { + match extract_bits(code, 21, 30) { + 0b0000000000 => PpcOpcode::mcrf, + 0b0000010000 => PpcOpcode::bclrx, + 0b0000100001 => PpcOpcode::crnor, + 0b0010000001 => PpcOpcode::crandc, + 0b0010010110 => PpcOpcode::isync, + 0b0011000001 => PpcOpcode::crxor, + 0b0011100001 => PpcOpcode::crnand, + 0b0100000001 => PpcOpcode::crand, + 0b0100100001 => PpcOpcode::creqv, + 0b0110100001 => PpcOpcode::crorc, + 0b0111000001 => PpcOpcode::cror, + 0b1000010000 => PpcOpcode::bcctrx, + _ => PpcOpcode::Invalid, + } +} + +fn decode_op30(code: u32) -> PpcOpcode { + match extract_bits(code, 27, 29) { + 0b000 => PpcOpcode::rldiclx, + 0b001 => PpcOpcode::rldicrx, + 0b010 => PpcOpcode::rldicx, + 0b011 => PpcOpcode::rldimix, + _ => match extract_bits(code, 27, 30) { + 0b1000 => PpcOpcode::rldclx, + 0b1001 => PpcOpcode::rldcrx, + _ => PpcOpcode::Invalid, + }, + } +} + +fn decode_op31(code: u32) -> PpcOpcode { + // sradix has a unique 10-bit key (bits 21-29) + if extract_bits(code, 21, 29) == 0b110011101 { + return PpcOpcode::sradix; + } + + // Main op31 table (bits 21-30) + let key = extract_bits(code, 21, 30); + match key { + 0b0000000000 => return PpcOpcode::cmp, + 0b0000000100 => return PpcOpcode::tw, + 0b0000000110 => return PpcOpcode::lvsl, + 0b0000000111 => return PpcOpcode::lvebx, + 0b0000010011 => return PpcOpcode::mfcr, + 0b0000010100 => return PpcOpcode::lwarx, + 0b0000010101 => return PpcOpcode::ldx, + 0b0000010111 => return PpcOpcode::lwzx, + 0b0000011000 => return PpcOpcode::slwx, + 0b0000011010 => return PpcOpcode::cntlzwx, + 0b0000011011 => return PpcOpcode::sldx, + 0b0000011100 => return PpcOpcode::andx, + 0b0000100000 => return PpcOpcode::cmpl, + 0b0000100110 => return PpcOpcode::lvsr, + 0b0000100111 => return PpcOpcode::lvehx, + 0b0000110101 => return PpcOpcode::ldux, + 0b0000110110 => return PpcOpcode::dcbst, + 0b0000110111 => return PpcOpcode::lwzux, + 0b0000111010 => return PpcOpcode::cntlzdx, + 0b0000111100 => return PpcOpcode::andcx, + 0b0001000100 => return PpcOpcode::td, + 0b0001000111 => return PpcOpcode::lvewx, + 0b0001010011 => return PpcOpcode::mfmsr, + 0b0001010100 => return PpcOpcode::ldarx, + 0b0001010110 => return PpcOpcode::dcbf, + 0b0001010111 => return PpcOpcode::lbzx, + 0b0001100111 => return PpcOpcode::lvx, + 0b0001110111 => return PpcOpcode::lbzux, + 0b0001111100 => return PpcOpcode::norx, + 0b0010000111 => return PpcOpcode::stvebx, + 0b0010010000 => return PpcOpcode::mtcrf, + 0b0010010010 => return PpcOpcode::mtmsr, + 0b0010010101 => return PpcOpcode::stdx, + 0b0010010110 => return PpcOpcode::stwcx, + 0b0010010111 => return PpcOpcode::stwx, + 0b0010100111 => return PpcOpcode::stvehx, + 0b0010110010 => return PpcOpcode::mtmsrd, + 0b0010110101 => return PpcOpcode::stdux, + 0b0010110111 => return PpcOpcode::stwux, + 0b0011000111 => return PpcOpcode::stvewx, + 0b0011010110 => return PpcOpcode::stdcx, + 0b0011010111 => return PpcOpcode::stbx, + 0b0011100111 => return PpcOpcode::stvx, + 0b0011110110 => return PpcOpcode::dcbtst, + 0b0011110111 => return PpcOpcode::stbux, + 0b0100010110 => return PpcOpcode::dcbt, + 0b0100010111 => return PpcOpcode::lhzx, + 0b0100011100 => return PpcOpcode::eqvx, + 0b0100110111 => return PpcOpcode::lhzux, + 0b0100111100 => return PpcOpcode::xorx, + 0b0101010011 => return PpcOpcode::mfspr, + 0b0101010101 => return PpcOpcode::lwax, + 0b0101010111 => return PpcOpcode::lhax, + 0b0101100111 => return PpcOpcode::lvxl, + 0b0101110011 => return PpcOpcode::mftb, + 0b0101110101 => return PpcOpcode::lwaux, + 0b0101110111 => return PpcOpcode::lhaux, + 0b0110010111 => return PpcOpcode::sthx, + 0b0110011100 => return PpcOpcode::orcx, + 0b0110110111 => return PpcOpcode::sthux, + 0b0110111100 => return PpcOpcode::orx, + 0b0111010011 => return PpcOpcode::mtspr, + 0b0111010110 => return PpcOpcode::dcbi, + 0b0111011100 => return PpcOpcode::nandx, + 0b0111100111 => return PpcOpcode::stvxl, + 0b1000000000 => return PpcOpcode::mcrxr, + 0b1000000111 => return PpcOpcode::lvlx, + 0b1000010100 => return PpcOpcode::ldbrx, + 0b1000010101 => return PpcOpcode::lswx, + 0b1000010110 => return PpcOpcode::lwbrx, + 0b1000010111 => return PpcOpcode::lfsx, + 0b1000011000 => return PpcOpcode::srwx, + 0b1000011011 => return PpcOpcode::srdx, + 0b1000100111 => return PpcOpcode::lvrx, + 0b1000110111 => return PpcOpcode::lfsux, + 0b1001010101 => return PpcOpcode::lswi, + 0b1001010110 => return PpcOpcode::sync, + 0b1001010111 => return PpcOpcode::lfdx, + 0b1001110111 => return PpcOpcode::lfdux, + 0b1010000111 => return PpcOpcode::stvlx, + 0b1010010100 => return PpcOpcode::stdbrx, + 0b1010010101 => return PpcOpcode::stswx, + 0b1010010110 => return PpcOpcode::stwbrx, + 0b1010010111 => return PpcOpcode::stfsx, + 0b1010100111 => return PpcOpcode::stvrx, + 0b1010110111 => return PpcOpcode::stfsux, + 0b1011010101 => return PpcOpcode::stswi, + 0b1011010111 => return PpcOpcode::stfdx, + 0b1011110111 => return PpcOpcode::stfdux, + 0b1100000111 => return PpcOpcode::lvlxl, + 0b1100010110 => return PpcOpcode::lhbrx, + 0b1100011000 => return PpcOpcode::srawx, + 0b1100011010 => return PpcOpcode::sradx, + 0b1100100111 => return PpcOpcode::lvrxl, + 0b1100111000 => return PpcOpcode::srawix, + 0b1101010110 => return PpcOpcode::eieio, + 0b1110000111 => return PpcOpcode::stvlxl, + 0b1110010110 => return PpcOpcode::sthbrx, + 0b1110011010 => return PpcOpcode::extshx, + 0b1110100111 => return PpcOpcode::stvrxl, + 0b1110111010 => return PpcOpcode::extsbx, + 0b1111010110 => return PpcOpcode::icbi, + 0b1111010111 => return PpcOpcode::stfiwx, + 0b1111011010 => return PpcOpcode::extswx, + _ => {} + } + + // Arithmetic op31 (bits 22-30) + let key2 = extract_bits(code, 22, 30); + match key2 { + 0b000001000 => return PpcOpcode::subfcx, + 0b000001001 => return PpcOpcode::mulhdux, + 0b000001010 => return PpcOpcode::addcx, + 0b000001011 => return PpcOpcode::mulhwux, + 0b000101000 => return PpcOpcode::subfx, + 0b001001001 => return PpcOpcode::mulhdx, + 0b001001011 => return PpcOpcode::mulhwx, + 0b001101000 => return PpcOpcode::negx, + 0b010001000 => return PpcOpcode::subfex, + 0b010001010 => return PpcOpcode::addex, + 0b011001000 => return PpcOpcode::subfzex, + 0b011001010 => return PpcOpcode::addzex, + 0b011101000 => return PpcOpcode::subfmex, + 0b011101001 => return PpcOpcode::mulldx, + 0b011101010 => return PpcOpcode::addmex, + 0b011101011 => return PpcOpcode::mullwx, + 0b100001010 => return PpcOpcode::addx, + 0b111001001 => return PpcOpcode::divdux, + 0b111001011 => return PpcOpcode::divwux, + 0b111101001 => return PpcOpcode::divdx, + 0b111101011 => return PpcOpcode::divwx, + _ => {} + } + + // dcbz/dcbz128 special case + let key3 = (extract_bits(code, 6, 10) << 20) | (extract_bits(code, 21, 30)); + match key3 { + 0b0000000000000001111110110 => return PpcOpcode::dcbz, + 0b0000100000000001111110110 => return PpcOpcode::dcbz128, + _ => {} + } + + PpcOpcode::Invalid +} + +fn decode_op63(code: u32) -> PpcOpcode { + // Primary op63 table (bits 21-30) + match extract_bits(code, 21, 30) { + 0b0000000000 => return PpcOpcode::fcmpu, + 0b0000001100 => return PpcOpcode::frspx, + 0b0000001110 => return PpcOpcode::fctiwx, + 0b0000001111 => return PpcOpcode::fctiwzx, + 0b0000100000 => return PpcOpcode::fcmpo, + 0b0000100110 => return PpcOpcode::mtfsb1x, + 0b0000101000 => return PpcOpcode::fnegx, + 0b0001000000 => return PpcOpcode::mcrfs, + 0b0001000110 => return PpcOpcode::mtfsb0x, + 0b0001001000 => return PpcOpcode::fmrx, + 0b0010000110 => return PpcOpcode::mtfsfix, + 0b0010001000 => return PpcOpcode::fnabsx, + 0b0100001000 => return PpcOpcode::fabsx, + 0b1001000111 => return PpcOpcode::mffsx, + 0b1011000111 => return PpcOpcode::mtfsfx, + 0b1100101110 => return PpcOpcode::fctidx, + 0b1100101111 => return PpcOpcode::fctidzx, + 0b1101001110 => return PpcOpcode::fcfidx, + _ => {} + } + + // FPU arithmetic (bits 26-30) + match extract_bits(code, 26, 30) { + 0b10010 => PpcOpcode::fdivx, + 0b10100 => PpcOpcode::fsubx, + 0b10101 => PpcOpcode::faddx, + 0b10110 => PpcOpcode::fsqrtx, + 0b10111 => PpcOpcode::fselx, + 0b11001 => PpcOpcode::fmulx, + 0b11010 => PpcOpcode::frsqrtex, + 0b11100 => PpcOpcode::fmsubx, + 0b11101 => PpcOpcode::fmaddx, + 0b11110 => PpcOpcode::fnmsubx, + 0b11111 => PpcOpcode::fnmaddx, + _ => PpcOpcode::Invalid, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_decode_addi() { + // addi r3, r1, 0x10 => opcode 14, rD=3, rA=1, SIMM=0x10 + let raw: u32 = (14 << 26) | (3 << 21) | (1 << 16) | 0x10; + let instr = decode(raw, 0); + assert_eq!(instr.opcode, PpcOpcode::addi); + assert_eq!(instr.rd(), 3); + assert_eq!(instr.ra(), 1); + assert_eq!(instr.simm16(), 0x10); + } + + #[test] + fn test_decode_lwz() { + // lwz r5, 0x20(r1) => opcode 32 + let raw: u32 = (32 << 26) | (5 << 21) | (1 << 16) | 0x20; + let instr = decode(raw, 0); + assert_eq!(instr.opcode, PpcOpcode::lwz); + assert_eq!(instr.rd(), 5); + assert_eq!(instr.ra(), 1); + assert_eq!(instr.d(), 0x20); + } + + #[test] + fn test_decode_branch() { + // b +0x100 => opcode 18, LI=0x40 (shifted left 2 = 0x100), AA=0, LK=0 + let raw: u32 = (18 << 26) | (0x40 << 2); + let instr = decode(raw, 0); + assert_eq!(instr.opcode, PpcOpcode::bx); + assert_eq!(instr.li(), 0x100); + assert!(!instr.aa()); + assert!(!instr.lk()); + } + + #[test] + fn test_decode_stw() { + // stw r7, 0x8(r2) + let raw: u32 = (36 << 26) | (7 << 21) | (2 << 16) | 0x8; + let instr = decode(raw, 0); + assert_eq!(instr.opcode, PpcOpcode::stw); + assert_eq!(instr.rs(), 7); + assert_eq!(instr.ra(), 2); + } + + #[test] + fn test_decode_ori_nop() { + // ori r0, r0, 0 = NOP + let raw: u32 = (24 << 26); + let instr = decode(raw, 0); + assert_eq!(instr.opcode, PpcOpcode::ori); + } + + #[test] + fn test_extract_bits() { + assert_eq!(extract_bits(0xFFFF_FFFF, 0, 5), 0x3F); + assert_eq!(extract_bits(0x8000_0000, 0, 0), 1); + assert_eq!(extract_bits(0x0000_0001, 31, 31), 1); + } +} diff --git a/xenia-rs/crates/xenia-cpu/src/disasm.rs b/xenia-rs/crates/xenia-cpu/src/disasm.rs new file mode 100644 index 000000000..e4ee2ca1e --- /dev/null +++ b/xenia-rs/crates/xenia-cpu/src/disasm.rs @@ -0,0 +1,276 @@ +use crate::decoder::DecodedInstr; +use crate::opcode::PpcOpcode; +use std::fmt::Write; + +/// Disassemble a decoded instruction into PPC assembly text. +pub fn disassemble(instr: &DecodedInstr) -> String { + let mut out = String::new(); + match instr.opcode { + // Branch instructions + PpcOpcode::bx => { + let target = if instr.aa() { + instr.li() as u32 + } else { + instr.addr.wrapping_add(instr.li() as u32) + }; + let mnemonic = if instr.lk() { "bl" } else { "b" }; + write!(out, "{} 0x{:08X}", mnemonic, target).unwrap(); + } + PpcOpcode::bcx => { + let bo = instr.bo(); + let bi = instr.bi(); + let target = if instr.aa() { + instr.bd() as u32 + } else { + instr.addr.wrapping_add(instr.bd() as u32) + }; + let mnemonic = if instr.lk() { "bcl" } else { "bc" }; + write!(out, "{} {},{},0x{:08X}", mnemonic, bo, bi, target).unwrap(); + } + PpcOpcode::bclrx => { + let mnemonic = if instr.lk() { "bclrl" } else { "bclr" }; + write!(out, "{} {},{}", mnemonic, instr.bo(), instr.bi()).unwrap(); + } + PpcOpcode::bcctrx => { + let mnemonic = if instr.lk() { "bcctrl" } else { "bcctr" }; + write!(out, "{} {},{}", mnemonic, instr.bo(), instr.bi()).unwrap(); + } + + // System call + PpcOpcode::sc => { + write!(out, "sc").unwrap(); + } + + // D-form load/store + PpcOpcode::lwz | PpcOpcode::lwzu | PpcOpcode::lbz | PpcOpcode::lbzu | + PpcOpcode::lhz | PpcOpcode::lhzu | PpcOpcode::lha | PpcOpcode::lhau | + PpcOpcode::lfs | PpcOpcode::lfsu | PpcOpcode::lfd | PpcOpcode::lfdu => { + write!(out, "{:?} r{},{}(r{})", instr.opcode, instr.rd(), instr.d(), instr.ra()).unwrap(); + } + PpcOpcode::stw | PpcOpcode::stwu | PpcOpcode::stb | PpcOpcode::stbu | + PpcOpcode::sth | PpcOpcode::sthu | + PpcOpcode::stfs | PpcOpcode::stfsu | PpcOpcode::stfd | PpcOpcode::stfdu => { + write!(out, "{:?} r{},{}(r{})", instr.opcode, instr.rs(), instr.d(), instr.ra()).unwrap(); + } + + // D-form immediate ALU + PpcOpcode::addi | PpcOpcode::addis | PpcOpcode::addic | PpcOpcode::addicx | + PpcOpcode::subficx | PpcOpcode::mulli => { + write!(out, "{:?} r{},r{},{}", instr.opcode, instr.rd(), instr.ra(), instr.simm16()).unwrap(); + } + + // D-form immediate logical + PpcOpcode::ori | PpcOpcode::oris | PpcOpcode::xori | PpcOpcode::xoris | + PpcOpcode::andix | PpcOpcode::andisx => { + write!(out, "{:?} r{},r{},0x{:04X}", instr.opcode, instr.ra(), instr.rs(), instr.uimm16()).unwrap(); + } + + // Compare + PpcOpcode::cmpi => { + write!(out, "cmp{}i cr{},r{},{}", if instr.l() { "d" } else { "w" }, + instr.crfd(), instr.ra(), instr.simm16()).unwrap(); + } + PpcOpcode::cmpli => { + write!(out, "cmpl{}i cr{},r{},0x{:04X}", if instr.l() { "d" } else { "w" }, + instr.crfd(), instr.ra(), instr.uimm16()).unwrap(); + } + PpcOpcode::cmp => { + write!(out, "cmp{} cr{},r{},r{}", if instr.l() { "d" } else { "w" }, + instr.crfd(), instr.ra(), instr.rb()).unwrap(); + } + PpcOpcode::cmpl => { + write!(out, "cmpl{} cr{},r{},r{}", if instr.l() { "d" } else { "w" }, + instr.crfd(), instr.ra(), instr.rb()).unwrap(); + } + + // X-form ALU (3-register) + PpcOpcode::addx | PpcOpcode::addcx | PpcOpcode::addex | PpcOpcode::addzex | + PpcOpcode::addmex | PpcOpcode::subfx | PpcOpcode::subfcx | PpcOpcode::subfex | + PpcOpcode::subfzex | PpcOpcode::subfmex | PpcOpcode::negx | + PpcOpcode::mullwx | PpcOpcode::mulhwx | PpcOpcode::mulhwux | + PpcOpcode::divwx | PpcOpcode::divwux | + PpcOpcode::mulldx | PpcOpcode::mulhdx | PpcOpcode::mulhdux | + PpcOpcode::divdx | PpcOpcode::divdux => { + write!(out, "{:?} r{},r{},r{}", instr.opcode, instr.rd(), instr.ra(), instr.rb()).unwrap(); + } + + // X-form logical + PpcOpcode::andx | PpcOpcode::andcx | PpcOpcode::orx | PpcOpcode::orcx | + PpcOpcode::xorx | PpcOpcode::norx | PpcOpcode::nandx | PpcOpcode::eqvx => { + write!(out, "{:?} r{},r{},r{}", instr.opcode, instr.ra(), instr.rs(), instr.rb()).unwrap(); + } + + // Shift/rotate + PpcOpcode::slwx | PpcOpcode::srwx | PpcOpcode::srawx | PpcOpcode::sldx | + PpcOpcode::srdx | PpcOpcode::sradx => { + write!(out, "{:?} r{},r{},r{}", instr.opcode, instr.ra(), instr.rs(), instr.rb()).unwrap(); + } + PpcOpcode::srawix => { + write!(out, "srawi r{},r{},{}", instr.ra(), instr.rs(), instr.sh()).unwrap(); + } + PpcOpcode::sradix => { + write!(out, "sradi r{},r{},{}", instr.ra(), instr.rs(), instr.sh64()).unwrap(); + } + + // Rotate + PpcOpcode::rlwinmx => { + write!(out, "rlwinm r{},r{},{},{},{}", instr.ra(), instr.rs(), instr.sh(), instr.mb(), instr.me()).unwrap(); + } + PpcOpcode::rlwimix => { + write!(out, "rlwimi r{},r{},{},{},{}", instr.ra(), instr.rs(), instr.sh(), instr.mb(), instr.me()).unwrap(); + } + PpcOpcode::rlwnmx => { + write!(out, "rlwnm r{},r{},r{},{},{}", instr.ra(), instr.rs(), instr.rb(), instr.mb(), instr.me()).unwrap(); + } + + // Special register moves + PpcOpcode::mfspr => { + let spr_name = match instr.spr() { + 1 => "xer", + 8 => "lr", + 9 => "ctr", + 268 => "tbl", + 269 => "tbu", + _ => "", + }; + if spr_name.is_empty() { + write!(out, "mfspr r{},{}", instr.rd(), instr.spr()).unwrap(); + } else { + write!(out, "mf{} r{}", spr_name, instr.rd()).unwrap(); + } + } + PpcOpcode::mtspr => { + let spr_name = match instr.spr() { + 1 => "xer", + 8 => "lr", + 9 => "ctr", + _ => "", + }; + if spr_name.is_empty() { + write!(out, "mtspr {},r{}", instr.spr(), instr.rs()).unwrap(); + } else { + write!(out, "mt{} r{}", spr_name, instr.rs()).unwrap(); + } + } + PpcOpcode::mfcr => { + write!(out, "mfcr r{}", instr.rd()).unwrap(); + } + PpcOpcode::mtcrf => { + write!(out, "mtcrf 0x{:02X},r{}", instr.crm(), instr.rs()).unwrap(); + } + + // Extend + PpcOpcode::extsbx => write!(out, "extsb r{},r{}", instr.ra(), instr.rs()).unwrap(), + PpcOpcode::extshx => write!(out, "extsh r{},r{}", instr.ra(), instr.rs()).unwrap(), + PpcOpcode::extswx => write!(out, "extsw r{},r{}", instr.ra(), instr.rs()).unwrap(), + PpcOpcode::cntlzwx => write!(out, "cntlzw r{},r{}", instr.ra(), instr.rs()).unwrap(), + PpcOpcode::cntlzdx => write!(out, "cntlzd r{},r{}", instr.ra(), instr.rs()).unwrap(), + + // X-form load/store + PpcOpcode::lwzx | PpcOpcode::lwzux | PpcOpcode::lbzx | PpcOpcode::lbzux | + PpcOpcode::lhzx | PpcOpcode::lhzux | PpcOpcode::lhax | PpcOpcode::lhaux | + PpcOpcode::lwax | PpcOpcode::lwaux | PpcOpcode::ldx | PpcOpcode::ldux | + PpcOpcode::lfsx | PpcOpcode::lfsux | PpcOpcode::lfdx | PpcOpcode::lfdux | + PpcOpcode::lwbrx | PpcOpcode::lhbrx | PpcOpcode::ldbrx | + PpcOpcode::lwarx | PpcOpcode::ldarx => { + write!(out, "{:?} r{},r{},r{}", instr.opcode, instr.rd(), instr.ra(), instr.rb()).unwrap(); + } + PpcOpcode::stwx | PpcOpcode::stwux | PpcOpcode::stbx | PpcOpcode::stbux | + PpcOpcode::sthx | PpcOpcode::sthux | PpcOpcode::stdx | PpcOpcode::stdux | + PpcOpcode::stfsx | PpcOpcode::stfsux | PpcOpcode::stfdx | PpcOpcode::stfdux | + PpcOpcode::stwbrx | PpcOpcode::sthbrx | PpcOpcode::stdbrx | + PpcOpcode::stwcx | PpcOpcode::stdcx | PpcOpcode::stfiwx => { + write!(out, "{:?} r{},r{},r{}", instr.opcode, instr.rs(), instr.ra(), instr.rb()).unwrap(); + } + + // Cache/sync ops (no-ops for interpreter) + PpcOpcode::dcbf | PpcOpcode::dcbi | PpcOpcode::dcbst | + PpcOpcode::dcbt | PpcOpcode::dcbtst | PpcOpcode::icbi => { + write!(out, "{:?} r{},r{}", instr.opcode, instr.ra(), instr.rb()).unwrap(); + } + PpcOpcode::dcbz | PpcOpcode::dcbz128 => { + write!(out, "{:?} r{},r{}", instr.opcode, instr.ra(), instr.rb()).unwrap(); + } + PpcOpcode::sync | PpcOpcode::eieio | PpcOpcode::isync => { + write!(out, "{:?}", instr.opcode).unwrap(); + } + + // Load/store multiple + PpcOpcode::lmw => write!(out, "lmw r{},{}(r{})", instr.rd(), instr.d(), instr.ra()).unwrap(), + PpcOpcode::stmw => write!(out, "stmw r{},{}(r{})", instr.rs(), instr.d(), instr.ra()).unwrap(), + + // DS-form loads/stores + PpcOpcode::ld | PpcOpcode::ldu | PpcOpcode::lwa => { + write!(out, "{:?} r{},{}(r{})", instr.opcode, instr.rd(), instr.ds(), instr.ra()).unwrap(); + } + PpcOpcode::std | PpcOpcode::stdu => { + write!(out, "{:?} r{},{}(r{})", instr.opcode, instr.rs(), instr.ds(), instr.ra()).unwrap(); + } + + // CR logical ops + PpcOpcode::crand | PpcOpcode::crandc | PpcOpcode::creqv | PpcOpcode::crnand | + PpcOpcode::crnor | PpcOpcode::cror | PpcOpcode::crorc | PpcOpcode::crxor => { + write!(out, "{:?} {},{},{}", instr.opcode, instr.crbd(), instr.crba(), instr.crbb()).unwrap(); + } + PpcOpcode::mcrf => { + write!(out, "mcrf cr{},cr{}", instr.crfd(), instr.crfs()).unwrap(); + } + + // Trap + PpcOpcode::tdi => write!(out, "tdi {},r{},{}", instr.rd(), instr.ra(), instr.simm16()).unwrap(), + PpcOpcode::twi => write!(out, "twi {},r{},{}", instr.rd(), instr.ra(), instr.simm16()).unwrap(), + PpcOpcode::td => write!(out, "td {},r{},r{}", instr.rd(), instr.ra(), instr.rb()).unwrap(), + PpcOpcode::tw => write!(out, "tw {},r{},r{}", instr.rd(), instr.ra(), instr.rb()).unwrap(), + + // Default: just print opcode and raw hex + _ => { + write!(out, "{:?} [{:08X}]", instr.opcode, instr.raw).unwrap(); + } + } + out +} + +/// Disassemble a range of instructions from a byte slice. +pub fn disassemble_block(data: &[u8], base_addr: u32, count: usize) -> Vec<(u32, String)> { + let mut result = Vec::new(); + for i in 0..count { + let offset = i * 4; + if offset + 4 > data.len() { + break; + } + let raw = u32::from_be_bytes([ + data[offset], + data[offset + 1], + data[offset + 2], + data[offset + 3], + ]); + let addr = base_addr + offset as u32; + let instr = crate::decode(raw, addr); + let text = disassemble(&instr); + result.push((addr, text)); + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::decoder::decode; + + #[test] + fn test_disasm_nop() { + // ori r0, r0, 0 = NOP + let instr = decode(0x60000000, 0); + let text = disassemble(&instr); + assert!(text.contains("ori"), "Expected 'ori', got: {}", text); + } + + #[test] + fn test_disasm_addi() { + let raw = (14u32 << 26) | (3 << 21) | (1 << 16) | 16; + let instr = decode(raw, 0); + let text = disassemble(&instr); + assert!(text.contains("addi"), "Got: {}", text); + assert!(text.contains("r3"), "Got: {}", text); + } +} diff --git a/xenia-rs/crates/xenia-cpu/src/interpreter.rs b/xenia-rs/crates/xenia-cpu/src/interpreter.rs new file mode 100644 index 000000000..9a8a43fe5 --- /dev/null +++ b/xenia-rs/crates/xenia-cpu/src/interpreter.rs @@ -0,0 +1,1293 @@ +//! PPC interpreter - executes instructions one at a time. +//! This is the core execution engine. Every instruction is observable +//! by the debugger (pre_step/post_step hooks on every cycle). + +use crate::context::PpcContext; +use crate::decoder::{decode, DecodedInstr}; +use crate::opcode::PpcOpcode; +use xenia_memory::MemoryAccess; + +/// Result of executing a single instruction. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum StepResult { + /// Normal execution, advance to next instruction. + Continue, + /// Hit a system call (sc instruction). Kernel should handle. + SystemCall, + /// Hit an unimplemented opcode. + Unimplemented(PpcOpcode), + /// Hit a trap instruction. + Trap, + /// Execution halted (by debugger or error). + Halted, +} + +/// Execute a single PPC instruction. +pub fn step(ctx: &mut PpcContext, mem: &mut dyn MemoryAccess) -> StepResult { + let raw = mem.read_u32(ctx.pc); + let instr = decode(raw, ctx.pc); + + let result = execute(ctx, mem, &instr); + + ctx.cycle_count += 1; + ctx.timebase += 1; + + result +} + +/// Execute a decoded instruction, updating context and memory. +fn execute(ctx: &mut PpcContext, mem: &mut dyn MemoryAccess, instr: &DecodedInstr) -> StepResult { + match instr.opcode { + // ===== ALU: Immediate ===== + PpcOpcode::addi => { + let ra_val = if instr.ra() == 0 { 0 } else { ctx.gpr[instr.ra()] }; + ctx.gpr[instr.rd()] = ra_val.wrapping_add(instr.simm16() as i64 as u64); + ctx.pc += 4; + } + PpcOpcode::addis => { + let ra_val = if instr.ra() == 0 { 0 } else { ctx.gpr[instr.ra()] }; + ctx.gpr[instr.rd()] = ra_val.wrapping_add((instr.simm16() as i64 as u64) << 16); + ctx.pc += 4; + } + PpcOpcode::addic => { + let ra = ctx.gpr[instr.ra()]; + let imm = instr.simm16() as i64 as u64; + let result = ra.wrapping_add(imm); + ctx.xer_ca = if result < ra { 1 } else { 0 }; + ctx.gpr[instr.rd()] = result; + ctx.pc += 4; + } + PpcOpcode::addicx => { + let ra = ctx.gpr[instr.ra()]; + let imm = instr.simm16() as i64 as u64; + let result = ra.wrapping_add(imm); + ctx.xer_ca = if result < ra { 1 } else { 0 }; + ctx.gpr[instr.rd()] = result; + // Update CR0 + ctx.update_cr_signed(0, result as i32 as i64); + ctx.pc += 4; + } + PpcOpcode::subficx => { + let ra = ctx.gpr[instr.ra()]; + let imm = instr.simm16() as i64 as u64; + let result = imm.wrapping_sub(ra); + ctx.xer_ca = if imm >= ra { 1 } else { 0 }; + ctx.gpr[instr.rd()] = result; + ctx.pc += 4; + } + PpcOpcode::mulli => { + let ra = ctx.gpr[instr.ra()] as i64; + let imm = instr.simm16() as i64; + ctx.gpr[instr.rd()] = ra.wrapping_mul(imm) as u64; + ctx.pc += 4; + } + + // ===== ALU: Register ===== + PpcOpcode::addx => { + let ra = ctx.gpr[instr.ra()]; + let rb = ctx.gpr[instr.rb()]; + let result = ra.wrapping_add(rb); + ctx.gpr[instr.rd()] = result; + if instr.oe() { + // TODO: overflow detection + } + if instr.rc_bit() { + ctx.update_cr_signed(0, result as i32 as i64); + } + ctx.pc += 4; + } + PpcOpcode::addcx => { + let ra = ctx.gpr[instr.ra()]; + let rb = ctx.gpr[instr.rb()]; + let result = ra.wrapping_add(rb); + ctx.xer_ca = if result < ra { 1 } else { 0 }; + ctx.gpr[instr.rd()] = result; + if instr.rc_bit() { + ctx.update_cr_signed(0, result as i32 as i64); + } + ctx.pc += 4; + } + PpcOpcode::addex => { + let ra = ctx.gpr[instr.ra()]; + let rb = ctx.gpr[instr.rb()]; + let ca = ctx.xer_ca as u64; + let result = ra.wrapping_add(rb).wrapping_add(ca); + ctx.xer_ca = if result < ra || (ca != 0 && result == ra) { 1 } else { 0 }; + ctx.gpr[instr.rd()] = result; + if instr.rc_bit() { + ctx.update_cr_signed(0, result as i32 as i64); + } + ctx.pc += 4; + } + PpcOpcode::addzex => { + let ra = ctx.gpr[instr.ra()]; + let ca = ctx.xer_ca as u64; + let result = ra.wrapping_add(ca); + ctx.xer_ca = if result < ra { 1 } else { 0 }; + ctx.gpr[instr.rd()] = result; + if instr.rc_bit() { + ctx.update_cr_signed(0, result as i32 as i64); + } + ctx.pc += 4; + } + PpcOpcode::addmex => { + let ra = ctx.gpr[instr.ra()]; + let ca = ctx.xer_ca as u64; + let result = ra.wrapping_add(ca).wrapping_sub(1); + ctx.xer_ca = if ra != 0 || ca != 0 { 1 } else { 0 }; + ctx.gpr[instr.rd()] = result; + if instr.rc_bit() { + ctx.update_cr_signed(0, result as i32 as i64); + } + ctx.pc += 4; + } + PpcOpcode::subfx => { + let ra = ctx.gpr[instr.ra()]; + let rb = ctx.gpr[instr.rb()]; + let result = rb.wrapping_sub(ra); + ctx.gpr[instr.rd()] = result; + if instr.rc_bit() { + ctx.update_cr_signed(0, result as i32 as i64); + } + ctx.pc += 4; + } + PpcOpcode::subfcx => { + let ra = ctx.gpr[instr.ra()]; + let rb = ctx.gpr[instr.rb()]; + let result = rb.wrapping_sub(ra); + ctx.xer_ca = if rb >= ra { 1 } else { 0 }; + ctx.gpr[instr.rd()] = result; + if instr.rc_bit() { + ctx.update_cr_signed(0, result as i32 as i64); + } + ctx.pc += 4; + } + PpcOpcode::subfex => { + let ra = ctx.gpr[instr.ra()]; + let rb = ctx.gpr[instr.rb()]; + let ca = ctx.xer_ca as u64; + let result = (!ra).wrapping_add(rb).wrapping_add(ca); + ctx.xer_ca = if rb > ra || (rb == ra && ca != 0) { 1 } else { 0 }; + ctx.gpr[instr.rd()] = result; + if instr.rc_bit() { + ctx.update_cr_signed(0, result as i32 as i64); + } + ctx.pc += 4; + } + PpcOpcode::subfzex => { + let ra = ctx.gpr[instr.ra()]; + let ca = ctx.xer_ca as u64; + let result = (!ra).wrapping_add(ca); + ctx.xer_ca = if !ra != 0 || ca != 0 { 1 } else { 0 }; + ctx.gpr[instr.rd()] = result; + if instr.rc_bit() { + ctx.update_cr_signed(0, result as i32 as i64); + } + ctx.pc += 4; + } + PpcOpcode::subfmex => { + let ra = ctx.gpr[instr.ra()]; + let ca = ctx.xer_ca as u64; + let result = (!ra).wrapping_add(ca).wrapping_sub(1); + ctx.xer_ca = if (!ra) != 0 || ca != 0 { 1 } else { 0 }; + ctx.gpr[instr.rd()] = result; + if instr.rc_bit() { + ctx.update_cr_signed(0, result as i32 as i64); + } + ctx.pc += 4; + } + PpcOpcode::negx => { + let ra = ctx.gpr[instr.ra()]; + ctx.gpr[instr.rd()] = (!ra).wrapping_add(1); + if instr.rc_bit() { + ctx.update_cr_signed(0, ctx.gpr[instr.rd()] as i32 as i64); + } + ctx.pc += 4; + } + PpcOpcode::mullwx => { + let ra = ctx.gpr[instr.ra()] as i32 as i64; + let rb = ctx.gpr[instr.rb()] as i32 as i64; + ctx.gpr[instr.rd()] = ra.wrapping_mul(rb) as u64; + if instr.rc_bit() { + ctx.update_cr_signed(0, ctx.gpr[instr.rd()] as i32 as i64); + } + ctx.pc += 4; + } + PpcOpcode::mulhwx => { + let ra = ctx.gpr[instr.ra()] as i32 as i64; + let rb = ctx.gpr[instr.rb()] as i32 as i64; + let result = ra.wrapping_mul(rb); + ctx.gpr[instr.rd()] = ((result >> 32) as i32 as i64 as u64) & 0xFFFF_FFFF; + if instr.rc_bit() { + ctx.update_cr_signed(0, ctx.gpr[instr.rd()] as i32 as i64); + } + ctx.pc += 4; + } + PpcOpcode::mulhwux => { + let ra = ctx.gpr[instr.ra()] as u32 as u64; + let rb = ctx.gpr[instr.rb()] as u32 as u64; + let result = ra.wrapping_mul(rb); + ctx.gpr[instr.rd()] = (result >> 32) & 0xFFFF_FFFF; + if instr.rc_bit() { + ctx.update_cr_signed(0, ctx.gpr[instr.rd()] as i32 as i64); + } + ctx.pc += 4; + } + PpcOpcode::divwx => { + let ra = ctx.gpr[instr.ra()] as i32; + let rb = ctx.gpr[instr.rb()] as i32; + if rb == 0 || (ra == i32::MIN && rb == -1) { + ctx.gpr[instr.rd()] = 0; + } else { + ctx.gpr[instr.rd()] = (ra / rb) as i64 as u64; + } + if instr.rc_bit() { + ctx.update_cr_signed(0, ctx.gpr[instr.rd()] as i32 as i64); + } + ctx.pc += 4; + } + PpcOpcode::divwux => { + let ra = ctx.gpr[instr.ra()] as u32; + let rb = ctx.gpr[instr.rb()] as u32; + if rb == 0 { + ctx.gpr[instr.rd()] = 0; + } else { + ctx.gpr[instr.rd()] = (ra / rb) as u64; + } + if instr.rc_bit() { + ctx.update_cr_signed(0, ctx.gpr[instr.rd()] as i32 as i64); + } + ctx.pc += 4; + } + + // ===== Logical ===== + PpcOpcode::andix => { + ctx.gpr[instr.ra()] = ctx.gpr[instr.rs()] & (instr.uimm16() as u64); + ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i32 as i64); + ctx.pc += 4; + } + PpcOpcode::andisx => { + ctx.gpr[instr.ra()] = ctx.gpr[instr.rs()] & ((instr.uimm16() as u64) << 16); + ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i32 as i64); + ctx.pc += 4; + } + PpcOpcode::ori => { + ctx.gpr[instr.ra()] = ctx.gpr[instr.rs()] | (instr.uimm16() as u64); + ctx.pc += 4; + } + PpcOpcode::oris => { + ctx.gpr[instr.ra()] = ctx.gpr[instr.rs()] | ((instr.uimm16() as u64) << 16); + ctx.pc += 4; + } + PpcOpcode::xori => { + ctx.gpr[instr.ra()] = ctx.gpr[instr.rs()] ^ (instr.uimm16() as u64); + ctx.pc += 4; + } + PpcOpcode::xoris => { + ctx.gpr[instr.ra()] = ctx.gpr[instr.rs()] ^ ((instr.uimm16() as u64) << 16); + ctx.pc += 4; + } + PpcOpcode::andx => { + ctx.gpr[instr.ra()] = ctx.gpr[instr.rs()] & ctx.gpr[instr.rb()]; + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i32 as i64); } + ctx.pc += 4; + } + PpcOpcode::andcx => { + ctx.gpr[instr.ra()] = ctx.gpr[instr.rs()] & !ctx.gpr[instr.rb()]; + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i32 as i64); } + ctx.pc += 4; + } + PpcOpcode::orx => { + ctx.gpr[instr.ra()] = ctx.gpr[instr.rs()] | ctx.gpr[instr.rb()]; + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i32 as i64); } + ctx.pc += 4; + } + PpcOpcode::orcx => { + ctx.gpr[instr.ra()] = ctx.gpr[instr.rs()] | !ctx.gpr[instr.rb()]; + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i32 as i64); } + ctx.pc += 4; + } + PpcOpcode::xorx => { + ctx.gpr[instr.ra()] = ctx.gpr[instr.rs()] ^ ctx.gpr[instr.rb()]; + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i32 as i64); } + ctx.pc += 4; + } + PpcOpcode::norx => { + ctx.gpr[instr.ra()] = !(ctx.gpr[instr.rs()] | ctx.gpr[instr.rb()]); + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i32 as i64); } + ctx.pc += 4; + } + PpcOpcode::nandx => { + ctx.gpr[instr.ra()] = !(ctx.gpr[instr.rs()] & ctx.gpr[instr.rb()]); + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i32 as i64); } + ctx.pc += 4; + } + PpcOpcode::eqvx => { + ctx.gpr[instr.ra()] = !(ctx.gpr[instr.rs()] ^ ctx.gpr[instr.rb()]); + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i32 as i64); } + ctx.pc += 4; + } + + // ===== Extend/Count ===== + PpcOpcode::extsbx => { + ctx.gpr[instr.ra()] = ctx.gpr[instr.rs()] as i8 as i64 as u64; + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i32 as i64); } + ctx.pc += 4; + } + PpcOpcode::extshx => { + ctx.gpr[instr.ra()] = ctx.gpr[instr.rs()] as i16 as i64 as u64; + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i32 as i64); } + ctx.pc += 4; + } + PpcOpcode::extswx => { + ctx.gpr[instr.ra()] = ctx.gpr[instr.rs()] as i32 as i64 as u64; + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i64); } + ctx.pc += 4; + } + PpcOpcode::cntlzwx => { + ctx.gpr[instr.ra()] = (ctx.gpr[instr.rs()] as u32).leading_zeros() as u64; + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i32 as i64); } + ctx.pc += 4; + } + PpcOpcode::cntlzdx => { + ctx.gpr[instr.ra()] = ctx.gpr[instr.rs()].leading_zeros() as u64; + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i64); } + ctx.pc += 4; + } + + // ===== Shift ===== + PpcOpcode::slwx => { + let sh = ctx.gpr[instr.rb()] as u32; + ctx.gpr[instr.ra()] = if sh < 32 { + ((ctx.gpr[instr.rs()] as u32) << sh) as u64 + } else { 0 }; + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i32 as i64); } + ctx.pc += 4; + } + PpcOpcode::srwx => { + let sh = ctx.gpr[instr.rb()] as u32; + ctx.gpr[instr.ra()] = if sh < 32 { + ((ctx.gpr[instr.rs()] as u32) >> sh) as u64 + } else { 0 }; + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i32 as i64); } + ctx.pc += 4; + } + PpcOpcode::srawx => { + let rs = ctx.gpr[instr.rs()] as i32; + let sh = ctx.gpr[instr.rb()] as u32 & 0x3F; + if sh == 0 { + ctx.gpr[instr.ra()] = rs as i64 as u64; + ctx.xer_ca = 0; + } else if sh < 32 { + let result = rs >> sh; + ctx.xer_ca = if rs < 0 && (rs as u32) << (32 - sh) != 0 { 1 } else { 0 }; + ctx.gpr[instr.ra()] = result as i64 as u64; + } else { + ctx.gpr[instr.ra()] = if rs < 0 { u64::MAX } else { 0 }; + ctx.xer_ca = if rs < 0 { 1 } else { 0 }; + } + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i32 as i64); } + ctx.pc += 4; + } + PpcOpcode::srawix => { + let rs = ctx.gpr[instr.rs()] as i32; + let sh = instr.sh(); + if sh == 0 { + ctx.gpr[instr.ra()] = rs as i64 as u64; + ctx.xer_ca = 0; + } else { + let result = rs >> sh; + ctx.xer_ca = if rs < 0 && (rs as u32) << (32 - sh) != 0 { 1 } else { 0 }; + ctx.gpr[instr.ra()] = result as i64 as u64; + } + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i32 as i64); } + ctx.pc += 4; + } + PpcOpcode::sldx => { + let sh = ctx.gpr[instr.rb()] & 0x7F; + ctx.gpr[instr.ra()] = if sh < 64 { + ctx.gpr[instr.rs()] << sh + } else { 0 }; + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i64); } + ctx.pc += 4; + } + PpcOpcode::srdx => { + let sh = ctx.gpr[instr.rb()] & 0x7F; + ctx.gpr[instr.ra()] = if sh < 64 { + ctx.gpr[instr.rs()] >> sh + } else { 0 }; + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i64); } + ctx.pc += 4; + } + PpcOpcode::sradx => { + let rs = ctx.gpr[instr.rs()] as i64; + let sh = ctx.gpr[instr.rb()] & 0x7F; + if sh == 0 { + ctx.gpr[instr.ra()] = rs as u64; + ctx.xer_ca = 0; + } else if sh < 64 { + let result = rs >> sh; + ctx.xer_ca = if rs < 0 && (rs as u64) << (64 - sh) != 0 { 1 } else { 0 }; + ctx.gpr[instr.ra()] = result as u64; + } else { + ctx.gpr[instr.ra()] = if rs < 0 { u64::MAX } else { 0 }; + ctx.xer_ca = if rs < 0 { 1 } else { 0 }; + } + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i64); } + ctx.pc += 4; + } + PpcOpcode::sradix => { + let rs = ctx.gpr[instr.rs()] as i64; + let sh = instr.sh64(); + if sh == 0 { + ctx.gpr[instr.ra()] = rs as u64; + ctx.xer_ca = 0; + } else { + let result = rs >> sh; + ctx.xer_ca = if rs < 0 && (rs as u64) << (64 - sh) != 0 { 1 } else { 0 }; + ctx.gpr[instr.ra()] = result as u64; + } + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i64); } + ctx.pc += 4; + } + + // ===== Rotate ===== + PpcOpcode::rlwinmx => { + let rs = ctx.gpr[instr.rs()] as u32; + let sh = instr.sh(); + let mb = instr.mb(); + let me = instr.me(); + let rotated = rs.rotate_left(sh); + let mask = rlw_mask(mb, me); + ctx.gpr[instr.ra()] = (rotated & mask) as u64; + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i32 as i64); } + ctx.pc += 4; + } + PpcOpcode::rlwimix => { + let rs = ctx.gpr[instr.rs()] as u32; + let sh = instr.sh(); + let mb = instr.mb(); + let me = instr.me(); + let rotated = rs.rotate_left(sh); + let mask = rlw_mask(mb, me); + let ra = ctx.gpr[instr.ra()] as u32; + ctx.gpr[instr.ra()] = ((rotated & mask) | (ra & !mask)) as u64; + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i32 as i64); } + ctx.pc += 4; + } + PpcOpcode::rlwnmx => { + let rs = ctx.gpr[instr.rs()] as u32; + let sh = ctx.gpr[instr.rb()] as u32 & 0x1F; + let mb = instr.mb(); + let me = instr.me(); + let rotated = rs.rotate_left(sh); + let mask = rlw_mask(mb, me); + ctx.gpr[instr.ra()] = (rotated & mask) as u64; + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i32 as i64); } + ctx.pc += 4; + } + PpcOpcode::rldiclx => { + let rs = ctx.gpr[instr.rs()]; + let sh = instr.sh64(); + let mb = (instr.mb() << 1) | ((instr.raw >> 1) & 1); // 6-bit mb + let rotated = rs.rotate_left(sh); + let mask = rld_mask_left(mb); + ctx.gpr[instr.ra()] = rotated & mask; + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i64); } + ctx.pc += 4; + } + PpcOpcode::rldicrx => { + let rs = ctx.gpr[instr.rs()]; + let sh = instr.sh64(); + let me = (instr.mb() << 1) | ((instr.raw >> 1) & 1); // 6-bit me + let rotated = rs.rotate_left(sh); + let mask = rld_mask_right(me); + ctx.gpr[instr.ra()] = rotated & mask; + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i64); } + ctx.pc += 4; + } + PpcOpcode::rldicx => { + let rs = ctx.gpr[instr.rs()]; + let sh = instr.sh64(); + let mb = (instr.mb() << 1) | ((instr.raw >> 1) & 1); + let rotated = rs.rotate_left(sh); + let mask = rld_mask_left(mb) & rld_mask_right(63 - sh); + ctx.gpr[instr.ra()] = rotated & mask; + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i64); } + ctx.pc += 4; + } + PpcOpcode::rldimix => { + let rs = ctx.gpr[instr.rs()]; + let sh = instr.sh64(); + let mb = (instr.mb() << 1) | ((instr.raw >> 1) & 1); + let rotated = rs.rotate_left(sh); + let mask = rld_mask_left(mb) & rld_mask_right(63 - sh); + ctx.gpr[instr.ra()] = (rotated & mask) | (ctx.gpr[instr.ra()] & !mask); + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i64); } + ctx.pc += 4; + } + PpcOpcode::rldclx => { + let rs = ctx.gpr[instr.rs()]; + let sh = ctx.gpr[instr.rb()] & 0x3F; + let mb = (instr.mb() << 1) | ((instr.raw >> 1) & 1); + let rotated = rs.rotate_left(sh as u32); + let mask = rld_mask_left(mb); + ctx.gpr[instr.ra()] = rotated & mask; + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i64); } + ctx.pc += 4; + } + PpcOpcode::rldcrx => { + let rs = ctx.gpr[instr.rs()]; + let sh = ctx.gpr[instr.rb()] & 0x3F; + let me = (instr.mb() << 1) | ((instr.raw >> 1) & 1); + let rotated = rs.rotate_left(sh as u32); + let mask = rld_mask_right(me); + ctx.gpr[instr.ra()] = rotated & mask; + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i64); } + ctx.pc += 4; + } + + // ===== Compare ===== + PpcOpcode::cmpi => { + let bf = instr.crfd(); + if instr.l() { + // 64-bit compare + let ra = ctx.gpr[instr.ra()] as i64; + let imm = instr.simm16() as i64; + ctx.update_cr_signed(bf, ra - imm); + if ra == imm { ctx.cr[bf].eq = true; } + } else { + let ra = ctx.gpr[instr.ra()] as i32; + let imm = instr.simm16() as i32; + ctx.update_cr_signed(bf, (ra as i64) - (imm as i64)); + if ra == imm { ctx.cr[bf].eq = true; } + } + ctx.pc += 4; + } + PpcOpcode::cmpli => { + let bf = instr.crfd(); + if instr.l() { + let ra = ctx.gpr[instr.ra()]; + let imm = instr.uimm16() as u64; + ctx.update_cr_unsigned(bf, ra, imm); + } else { + let ra = ctx.gpr[instr.ra()] as u32 as u64; + let imm = instr.uimm16() as u64; + ctx.update_cr_unsigned(bf, ra, imm); + } + ctx.pc += 4; + } + PpcOpcode::cmp => { + let bf = instr.crfd(); + if instr.l() { + let ra = ctx.gpr[instr.ra()] as i64; + let rb = ctx.gpr[instr.rb()] as i64; + ctx.update_cr_signed(bf, ra.wrapping_sub(rb)); + if ra == rb { ctx.cr[bf].eq = true; } + } else { + let ra = ctx.gpr[instr.ra()] as i32; + let rb = ctx.gpr[instr.rb()] as i32; + ctx.update_cr_signed(bf, (ra as i64).wrapping_sub(rb as i64)); + if ra == rb { ctx.cr[bf].eq = true; } + } + ctx.pc += 4; + } + PpcOpcode::cmpl => { + let bf = instr.crfd(); + if instr.l() { + ctx.update_cr_unsigned(bf, ctx.gpr[instr.ra()], ctx.gpr[instr.rb()]); + } else { + ctx.update_cr_unsigned(bf, ctx.gpr[instr.ra()] as u32 as u64, ctx.gpr[instr.rb()] as u32 as u64); + } + ctx.pc += 4; + } + + // ===== Branch ===== + PpcOpcode::bx => { + let target = if instr.aa() { + instr.li() as u32 + } else { + ctx.pc.wrapping_add(instr.li() as u32) + }; + if instr.lk() { + ctx.lr = (ctx.pc + 4) as u64; + } + ctx.pc = target; + } + PpcOpcode::bcx => { + let bo = instr.bo(); + let bi = instr.bi(); + + // Decrement CTR if needed + if bo & 0b00100 == 0 { + ctx.ctr = ctx.ctr.wrapping_sub(1); + } + + let ctr_ok = (bo & 0b00100) != 0 + || ((ctx.ctr != 0) ^ ((bo & 0b00010) != 0)); + let cond_ok = (bo & 0b10000) != 0 + || (ctx.get_cr_bit(bi) == ((bo & 0b01000) != 0)); + + if ctr_ok && cond_ok { + let target = if instr.aa() { + instr.bd() as u32 + } else { + ctx.pc.wrapping_add(instr.bd() as u32) + }; + if instr.lk() { + ctx.lr = (ctx.pc + 4) as u64; + } + ctx.pc = target; + } else { + if instr.lk() { + ctx.lr = (ctx.pc + 4) as u64; + } + ctx.pc += 4; + } + } + PpcOpcode::bclrx => { + let bo = instr.bo(); + let bi = instr.bi(); + + if bo & 0b00100 == 0 { + ctx.ctr = ctx.ctr.wrapping_sub(1); + } + + let ctr_ok = (bo & 0b00100) != 0 + || ((ctx.ctr != 0) ^ ((bo & 0b00010) != 0)); + let cond_ok = (bo & 0b10000) != 0 + || (ctx.get_cr_bit(bi) == ((bo & 0b01000) != 0)); + + let next_pc = ctx.pc + 4; + if ctr_ok && cond_ok { + ctx.pc = (ctx.lr as u32) & !3; + } else { + ctx.pc = next_pc; + } + if instr.lk() { + ctx.lr = next_pc as u64; + } + } + PpcOpcode::bcctrx => { + let bo = instr.bo(); + let bi = instr.bi(); + + let cond_ok = (bo & 0b10000) != 0 + || (ctx.get_cr_bit(bi) == ((bo & 0b01000) != 0)); + + if cond_ok { + let next_pc = ctx.pc + 4; + ctx.pc = (ctx.ctr as u32) & !3; + if instr.lk() { + ctx.lr = next_pc as u64; + } + } else { + if instr.lk() { + ctx.lr = (ctx.pc + 4) as u64; + } + ctx.pc += 4; + } + } + + // ===== System call ===== + PpcOpcode::sc => { + ctx.pc += 4; + return StepResult::SystemCall; + } + + // ===== Load instructions ===== + PpcOpcode::lwz => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(instr.d() as i64 as u64) as u32; + ctx.gpr[instr.rd()] = mem.read_u32(ea) as u64; + ctx.pc += 4; + } + PpcOpcode::lwzu => { + let ea = ctx.gpr[instr.ra()].wrapping_add(instr.d() as i64 as u64) as u32; + ctx.gpr[instr.rd()] = mem.read_u32(ea) as u64; + ctx.gpr[instr.ra()] = ea as u64; + ctx.pc += 4; + } + PpcOpcode::lwzx => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(ctx.gpr[instr.rb()]) as u32; + ctx.gpr[instr.rd()] = mem.read_u32(ea) as u64; + ctx.pc += 4; + } + PpcOpcode::lwzux => { + let ea = ctx.gpr[instr.ra()].wrapping_add(ctx.gpr[instr.rb()]) as u32; + ctx.gpr[instr.rd()] = mem.read_u32(ea) as u64; + ctx.gpr[instr.ra()] = ea as u64; + ctx.pc += 4; + } + PpcOpcode::lbz => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(instr.d() as i64 as u64) as u32; + ctx.gpr[instr.rd()] = mem.read_u8(ea) as u64; + ctx.pc += 4; + } + PpcOpcode::lbzu => { + let ea = ctx.gpr[instr.ra()].wrapping_add(instr.d() as i64 as u64) as u32; + ctx.gpr[instr.rd()] = mem.read_u8(ea) as u64; + ctx.gpr[instr.ra()] = ea as u64; + ctx.pc += 4; + } + PpcOpcode::lbzx => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(ctx.gpr[instr.rb()]) as u32; + ctx.gpr[instr.rd()] = mem.read_u8(ea) as u64; + ctx.pc += 4; + } + PpcOpcode::lbzux => { + let ea = ctx.gpr[instr.ra()].wrapping_add(ctx.gpr[instr.rb()]) as u32; + ctx.gpr[instr.rd()] = mem.read_u8(ea) as u64; + ctx.gpr[instr.ra()] = ea as u64; + ctx.pc += 4; + } + PpcOpcode::lhz => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(instr.d() as i64 as u64) as u32; + ctx.gpr[instr.rd()] = mem.read_u16(ea) as u64; + ctx.pc += 4; + } + PpcOpcode::lhzu => { + let ea = ctx.gpr[instr.ra()].wrapping_add(instr.d() as i64 as u64) as u32; + ctx.gpr[instr.rd()] = mem.read_u16(ea) as u64; + ctx.gpr[instr.ra()] = ea as u64; + ctx.pc += 4; + } + PpcOpcode::lhzx => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(ctx.gpr[instr.rb()]) as u32; + ctx.gpr[instr.rd()] = mem.read_u16(ea) as u64; + ctx.pc += 4; + } + PpcOpcode::lha => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(instr.d() as i64 as u64) as u32; + ctx.gpr[instr.rd()] = mem.read_u16(ea) as i16 as i64 as u64; + ctx.pc += 4; + } + PpcOpcode::lhax => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(ctx.gpr[instr.rb()]) as u32; + ctx.gpr[instr.rd()] = mem.read_u16(ea) as i16 as i64 as u64; + ctx.pc += 4; + } + PpcOpcode::ld => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(instr.ds() as i64 as u64) as u32; + ctx.gpr[instr.rd()] = mem.read_u64(ea); + ctx.pc += 4; + } + PpcOpcode::ldx => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(ctx.gpr[instr.rb()]) as u32; + ctx.gpr[instr.rd()] = mem.read_u64(ea); + ctx.pc += 4; + } + PpcOpcode::lwa => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(instr.ds() as i64 as u64) as u32; + ctx.gpr[instr.rd()] = mem.read_u32(ea) as i32 as i64 as u64; + ctx.pc += 4; + } + PpcOpcode::lwax => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(ctx.gpr[instr.rb()]) as u32; + ctx.gpr[instr.rd()] = mem.read_u32(ea) as i32 as i64 as u64; + ctx.pc += 4; + } + + // FP loads + PpcOpcode::lfs => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(instr.d() as i64 as u64) as u32; + ctx.fpr[instr.rd()] = mem.read_f32(ea) as f64; + ctx.pc += 4; + } + PpcOpcode::lfsx => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(ctx.gpr[instr.rb()]) as u32; + ctx.fpr[instr.rd()] = mem.read_f32(ea) as f64; + ctx.pc += 4; + } + PpcOpcode::lfd => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(instr.d() as i64 as u64) as u32; + ctx.fpr[instr.rd()] = mem.read_f64(ea); + ctx.pc += 4; + } + PpcOpcode::lfdx => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(ctx.gpr[instr.rb()]) as u32; + ctx.fpr[instr.rd()] = mem.read_f64(ea); + ctx.pc += 4; + } + + // Reservation (lwarx/stwcx) + PpcOpcode::lwarx => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(ctx.gpr[instr.rb()]) as u32; + let val = mem.read_u32(ea); + ctx.gpr[instr.rd()] = val as u64; + ctx.reserved_addr = ea; + ctx.reserved_val = val as u64; + ctx.has_reservation = true; + ctx.pc += 4; + } + PpcOpcode::stwcx => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(ctx.gpr[instr.rb()]) as u32; + if ctx.has_reservation && ctx.reserved_addr == ea { + mem.write_u32(ea, ctx.gpr[instr.rs()] as u32); + ctx.cr[0] = crate::context::CrField { lt: false, gt: false, eq: true, so: ctx.xer_so != 0 }; + } else { + ctx.cr[0] = crate::context::CrField { lt: false, gt: false, eq: false, so: ctx.xer_so != 0 }; + } + ctx.has_reservation = false; + ctx.pc += 4; + } + + // ===== Store instructions ===== + PpcOpcode::stw => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(instr.d() as i64 as u64) as u32; + mem.write_u32(ea, ctx.gpr[instr.rs()] as u32); + ctx.pc += 4; + } + PpcOpcode::stwu => { + let ea = ctx.gpr[instr.ra()].wrapping_add(instr.d() as i64 as u64) as u32; + mem.write_u32(ea, ctx.gpr[instr.rs()] as u32); + ctx.gpr[instr.ra()] = ea as u64; + ctx.pc += 4; + } + PpcOpcode::stwx => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(ctx.gpr[instr.rb()]) as u32; + mem.write_u32(ea, ctx.gpr[instr.rs()] as u32); + ctx.pc += 4; + } + PpcOpcode::stwux => { + let ea = ctx.gpr[instr.ra()].wrapping_add(ctx.gpr[instr.rb()]) as u32; + mem.write_u32(ea, ctx.gpr[instr.rs()] as u32); + ctx.gpr[instr.ra()] = ea as u64; + ctx.pc += 4; + } + PpcOpcode::stb => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(instr.d() as i64 as u64) as u32; + mem.write_u8(ea, ctx.gpr[instr.rs()] as u8); + ctx.pc += 4; + } + PpcOpcode::stbu => { + let ea = ctx.gpr[instr.ra()].wrapping_add(instr.d() as i64 as u64) as u32; + mem.write_u8(ea, ctx.gpr[instr.rs()] as u8); + ctx.gpr[instr.ra()] = ea as u64; + ctx.pc += 4; + } + PpcOpcode::stbx => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(ctx.gpr[instr.rb()]) as u32; + mem.write_u8(ea, ctx.gpr[instr.rs()] as u8); + ctx.pc += 4; + } + PpcOpcode::sth => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(instr.d() as i64 as u64) as u32; + mem.write_u16(ea, ctx.gpr[instr.rs()] as u16); + ctx.pc += 4; + } + PpcOpcode::sthu => { + let ea = ctx.gpr[instr.ra()].wrapping_add(instr.d() as i64 as u64) as u32; + mem.write_u16(ea, ctx.gpr[instr.rs()] as u16); + ctx.gpr[instr.ra()] = ea as u64; + ctx.pc += 4; + } + PpcOpcode::sthx => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(ctx.gpr[instr.rb()]) as u32; + mem.write_u16(ea, ctx.gpr[instr.rs()] as u16); + ctx.pc += 4; + } + PpcOpcode::std => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(instr.ds() as i64 as u64) as u32; + mem.write_u64(ea, ctx.gpr[instr.rs()]); + ctx.pc += 4; + } + PpcOpcode::stdx => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(ctx.gpr[instr.rb()]) as u32; + mem.write_u64(ea, ctx.gpr[instr.rs()]); + ctx.pc += 4; + } + + // FP stores + PpcOpcode::stfs => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(instr.d() as i64 as u64) as u32; + mem.write_f32(ea, ctx.fpr[instr.rs()] as f32); + ctx.pc += 4; + } + PpcOpcode::stfd => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(instr.d() as i64 as u64) as u32; + mem.write_f64(ea, ctx.fpr[instr.rs()]); + ctx.pc += 4; + } + + // ===== Special register moves ===== + PpcOpcode::mfspr => { + let spr = instr.spr(); + ctx.gpr[instr.rd()] = match spr { + crate::context::spr::XER => ctx.xer() as u64, + crate::context::spr::LR => ctx.lr, + crate::context::spr::CTR => ctx.ctr, + crate::context::spr::TBL => ctx.timebase & 0xFFFF_FFFF, + crate::context::spr::TBU => ctx.timebase >> 32, + _ => { + tracing::warn!("mfspr: unimplemented SPR {}", spr); + 0 + } + }; + ctx.pc += 4; + } + PpcOpcode::mtspr => { + let spr = instr.spr(); + let val = ctx.gpr[instr.rs()]; + match spr { + crate::context::spr::XER => ctx.set_xer(val as u32), + crate::context::spr::LR => ctx.lr = val, + crate::context::spr::CTR => ctx.ctr = val, + _ => { + tracing::warn!("mtspr: unimplemented SPR {}", spr); + } + } + ctx.pc += 4; + } + PpcOpcode::mfcr => { + ctx.gpr[instr.rd()] = ctx.cr() as u64; + ctx.pc += 4; + } + PpcOpcode::mtcrf => { + let crm = instr.crm(); + let val = ctx.gpr[instr.rs()] as u32; + let old = ctx.cr(); + let mut new = old; + for i in 0..8u32 { + if crm & (1 << (7 - i)) != 0 { + let mask = 0xF << (28 - i * 4); + new = (new & !mask) | (val & mask); + } + } + ctx.set_cr(new); + ctx.pc += 4; + } + PpcOpcode::mfmsr => { + ctx.gpr[instr.rd()] = ctx.msr; + ctx.pc += 4; + } + PpcOpcode::mtmsr | PpcOpcode::mtmsrd => { + ctx.msr = ctx.gpr[instr.rs()]; + ctx.pc += 4; + } + PpcOpcode::mftb => { + let tbr = instr.spr(); + ctx.gpr[instr.rd()] = match tbr { + 268 => ctx.timebase & 0xFFFF_FFFF, + 269 => ctx.timebase >> 32, + _ => 0, + }; + ctx.pc += 4; + } + + // CR logical + PpcOpcode::crand => { cr_logical(ctx, instr, |a, b| a & b); ctx.pc += 4; } + PpcOpcode::crandc => { cr_logical(ctx, instr, |a, b| a & !b); ctx.pc += 4; } + PpcOpcode::creqv => { cr_logical(ctx, instr, |a, b| !(a ^ b)); ctx.pc += 4; } + PpcOpcode::crnand => { cr_logical(ctx, instr, |a, b| !(a & b)); ctx.pc += 4; } + PpcOpcode::crnor => { cr_logical(ctx, instr, |a, b| !(a | b)); ctx.pc += 4; } + PpcOpcode::cror => { cr_logical(ctx, instr, |a, b| a | b); ctx.pc += 4; } + PpcOpcode::crorc => { cr_logical(ctx, instr, |a, b| a | !b); ctx.pc += 4; } + PpcOpcode::crxor => { cr_logical(ctx, instr, |a, b| a ^ b); ctx.pc += 4; } + PpcOpcode::mcrf => { + ctx.cr[instr.crfd()] = ctx.cr[instr.crfs()]; + ctx.pc += 4; + } + + // ===== Cache/sync (no-ops in interpreter) ===== + PpcOpcode::dcbf | PpcOpcode::dcbi | PpcOpcode::dcbst | + PpcOpcode::dcbt | PpcOpcode::dcbtst | PpcOpcode::icbi | + PpcOpcode::sync | PpcOpcode::eieio | PpcOpcode::isync => { + ctx.pc += 4; + } + PpcOpcode::dcbz => { + // Zero 32 bytes at effective address + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = (ea.wrapping_add(ctx.gpr[instr.rb()]) as u32) & !31; + for i in 0..8 { + mem.write_u32(ea + i * 4, 0); + } + ctx.pc += 4; + } + PpcOpcode::dcbz128 => { + // Zero 128 bytes + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = (ea.wrapping_add(ctx.gpr[instr.rb()]) as u32) & !127; + for i in 0..32 { + mem.write_u32(ea + i * 4, 0); + } + ctx.pc += 4; + } + + // ===== Load multiple ===== + PpcOpcode::lmw => { + let mut ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + ea = ea.wrapping_add(instr.d() as i64 as u64); + for r in instr.rd()..32 { + ctx.gpr[r] = mem.read_u32(ea as u32) as u64; + ea = ea.wrapping_add(4); + } + ctx.pc += 4; + } + PpcOpcode::stmw => { + let mut ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + ea = ea.wrapping_add(instr.d() as i64 as u64); + for r in instr.rs()..32 { + mem.write_u32(ea as u32, ctx.gpr[r] as u32); + ea = ea.wrapping_add(4); + } + ctx.pc += 4; + } + + // ===== Trap ===== + PpcOpcode::tdi | PpcOpcode::twi | PpcOpcode::td | PpcOpcode::tw => { + // For now, just trace and continue + tracing::warn!("Trap instruction at {:#010x}: {:?}", ctx.pc, instr.opcode); + ctx.pc += 4; + return StepResult::Trap; + } + + // ===== Byte-reverse loads ===== + PpcOpcode::lwbrx => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(ctx.gpr[instr.rb()]) as u32; + let val = mem.read_u32(ea); + ctx.gpr[instr.rd()] = val.swap_bytes() as u64; + ctx.pc += 4; + } + PpcOpcode::lhbrx => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(ctx.gpr[instr.rb()]) as u32; + let val = mem.read_u16(ea); + ctx.gpr[instr.rd()] = val.swap_bytes() as u64; + ctx.pc += 4; + } + PpcOpcode::stwbrx => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(ctx.gpr[instr.rb()]) as u32; + mem.write_u32(ea, (ctx.gpr[instr.rs()] as u32).swap_bytes()); + ctx.pc += 4; + } + PpcOpcode::sthbrx => { + let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; + let ea = ea.wrapping_add(ctx.gpr[instr.rb()]) as u32; + mem.write_u16(ea, (ctx.gpr[instr.rs()] as u16).swap_bytes()); + ctx.pc += 4; + } + + // Anything not yet implemented + _ => { + tracing::warn!("Unimplemented opcode at {:#010x}: {:?} [{:08X}]", ctx.pc, instr.opcode, instr.raw); + ctx.pc += 4; + return StepResult::Unimplemented(instr.opcode); + } + } + StepResult::Continue +} + +/// Helper for CR logical operations. +fn cr_logical(ctx: &mut PpcContext, instr: &DecodedInstr, op: fn(bool, bool) -> bool) { + let a = ctx.get_cr_bit(instr.crba()); + let b = ctx.get_cr_bit(instr.crbb()); + ctx.set_cr_bit(instr.crbd(), op(a, b)); +} + +/// Generate 32-bit rotate mask for rlwinm/rlwimi/rlwnm. +fn rlw_mask(mb: u32, me: u32) -> u32 { + if mb <= me { + (u32::MAX >> mb) & (u32::MAX << (31 - me)) + } else { + (u32::MAX >> mb) | (u32::MAX << (31 - me)) + } +} + +/// Generate 64-bit mask clearing bits 0..mb-1 (left mask for rldicl). +fn rld_mask_left(mb: u32) -> u64 { + if mb == 0 { u64::MAX } else { u64::MAX >> mb } +} + +/// Generate 64-bit mask clearing bits me+1..63 (right mask for rldicr). +fn rld_mask_right(me: u32) -> u64 { + if me >= 63 { u64::MAX } else { u64::MAX << (63 - me) } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Simple test memory (64KB) + struct TestMem { + data: Vec, + } + + impl TestMem { + fn new() -> Self { + Self { data: vec![0; 65536] } + } + } + + impl MemoryAccess for TestMem { + fn read_u8(&self, addr: u32) -> u8 { self.data[addr as usize] } + fn read_u16(&self, addr: u32) -> u16 { + let a = addr as usize; + u16::from_be_bytes([self.data[a], self.data[a+1]]) + } + fn read_u32(&self, addr: u32) -> u32 { + let a = addr as usize; + u32::from_be_bytes([self.data[a], self.data[a+1], self.data[a+2], self.data[a+3]]) + } + fn read_u64(&self, addr: u32) -> u64 { + let a = addr as usize; + u64::from_be_bytes([ + self.data[a], self.data[a+1], self.data[a+2], self.data[a+3], + self.data[a+4], self.data[a+5], self.data[a+6], self.data[a+7], + ]) + } + fn write_u8(&mut self, addr: u32, val: u8) { self.data[addr as usize] = val; } + fn write_u16(&mut self, addr: u32, val: u16) { + let a = addr as usize; + self.data[a..a+2].copy_from_slice(&val.to_be_bytes()); + } + fn write_u32(&mut self, addr: u32, val: u32) { + let a = addr as usize; + self.data[a..a+4].copy_from_slice(&val.to_be_bytes()); + } + fn write_u64(&mut self, addr: u32, val: u64) { + let a = addr as usize; + self.data[a..a+8].copy_from_slice(&val.to_be_bytes()); + } + fn translate(&self, _addr: u32) -> Option<*const u8> { None } + fn translate_mut(&mut self, _addr: u32) -> Option<*mut u8> { None } + } + + fn write_instr(mem: &mut TestMem, addr: u32, raw: u32) { + mem.write_u32(addr, raw); + } + + #[test] + fn test_addi() { + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + // addi r3, r0, 42 + write_instr(&mut mem, 0, (14 << 26) | (3 << 21) | (0 << 16) | 42); + ctx.pc = 0; + let result = step(&mut ctx, &mut mem); + assert_eq!(result, StepResult::Continue); + assert_eq!(ctx.gpr[3], 42); + assert_eq!(ctx.pc, 4); + } + + #[test] + fn test_addis() { + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + // addis r3, r0, 1 => r3 = 0x10000 + write_instr(&mut mem, 0, (15 << 26) | (3 << 21) | (0 << 16) | 1); + ctx.pc = 0; + step(&mut ctx, &mut mem); + assert_eq!(ctx.gpr[3], 0x10000); + } + + #[test] + fn test_lwz_stw() { + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + // Store 0xDEADBEEF at address 0x100 + mem.write_u32(0x100, 0xDEADBEEF); + // addi r1, r0, 0x100 + write_instr(&mut mem, 0, (14 << 26) | (1 << 21) | (0 << 16) | 0x100); + // lwz r3, 0(r1) + write_instr(&mut mem, 4, (32 << 26) | (3 << 21) | (1 << 16) | 0); + ctx.pc = 0; + step(&mut ctx, &mut mem); + step(&mut ctx, &mut mem); + assert_eq!(ctx.gpr[3], 0xDEADBEEF); + } + + #[test] + fn test_branch() { + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + // b +0x10 (from addr 0x100) + write_instr(&mut mem, 0x100, (18 << 26) | (4 << 2)); // LI=4, shifted=0x10 + ctx.pc = 0x100; + step(&mut ctx, &mut mem); + assert_eq!(ctx.pc, 0x110); + } + + #[test] + fn test_bl_updates_lr() { + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + // bl +0x10 (from addr 0x200) + write_instr(&mut mem, 0x200, (18 << 26) | (4 << 2) | 1); // LK=1 + ctx.pc = 0x200; + step(&mut ctx, &mut mem); + assert_eq!(ctx.pc, 0x210); + assert_eq!(ctx.lr, 0x204); + } + + #[test] + fn test_cmp_and_bc() { + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + ctx.gpr[3] = 10; + // cmpi cr0, 0, r3, 10 (32-bit compare) + write_instr(&mut mem, 0, (11 << 26) | (0 << 23) | (0 << 21) | (3 << 16) | (10u32 & 0xFFFF)); + // bc 12,2,+8 (branch if CR0.EQ, bo=12, bi=2) + write_instr(&mut mem, 4, (16 << 26) | (12 << 21) | (2 << 16) | (2 << 2)); + ctx.pc = 0; + step(&mut ctx, &mut mem); // cmpi + assert!(ctx.cr[0].eq); + step(&mut ctx, &mut mem); // bc - should branch + assert_eq!(ctx.pc, 12); // 4 + 8 + } + + #[test] + fn test_rlwinm() { + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + ctx.gpr[3] = 0xFF00_FF00; + // rlwinm r4, r3, 8, 0, 31 (rotate left 8, full mask = shift left 8) + let raw = (21 << 26) | (3 << 21) | (4 << 16) | (8 << 11) | (0 << 6) | (31 << 1); + write_instr(&mut mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mut mem); + assert_eq!(ctx.gpr[4], 0x00FF_00FF); + } + + #[test] + fn test_ori_nop() { + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + // ori r0, r0, 0 (NOP) + write_instr(&mut mem, 0, 0x60000000); + ctx.pc = 0; + ctx.gpr[0] = 0xDEAD; + step(&mut ctx, &mut mem); + assert_eq!(ctx.gpr[0], 0xDEAD); + assert_eq!(ctx.pc, 4); + } +} diff --git a/xenia-rs/crates/xenia-cpu/src/lib.rs b/xenia-rs/crates/xenia-cpu/src/lib.rs new file mode 100644 index 000000000..b84cb730e --- /dev/null +++ b/xenia-rs/crates/xenia-cpu/src/lib.rs @@ -0,0 +1,9 @@ +pub mod context; +pub mod decoder; +pub mod disasm; +pub mod interpreter; +pub mod opcode; + +pub use context::PpcContext; +pub use decoder::decode; +pub use opcode::PpcOpcode; diff --git a/xenia-rs/crates/xenia-cpu/src/opcode.rs b/xenia-rs/crates/xenia-cpu/src/opcode.rs new file mode 100644 index 000000000..01fb77cb0 --- /dev/null +++ b/xenia-rs/crates/xenia-cpu/src/opcode.rs @@ -0,0 +1,196 @@ +/// All PPC opcodes supported by the Xbox 360, including VMX128 extensions. +/// Directly mirrors the C++ PPCOpcode enum from ppc_opcode.h. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(u32)] +#[allow(non_camel_case_types)] +pub enum PpcOpcode { + // ALU + addcx, addex, addi, addic, addicx, addis, addmex, addx, addzex, + andcx, andisx, andix, andx, + // Branch + bcctrx, bclrx, bcx, bx, + // Compare + cmp, cmpi, cmpl, cmpli, + // Count leading zeros + cntlzdx, cntlzwx, + // Condition register + crand, crandc, creqv, crnand, crnor, cror, crorc, crxor, + // Data cache + dcbf, dcbi, dcbst, dcbt, dcbtst, dcbz, dcbz128, + // Division + divdux, divdx, divwux, divwx, + // Sync/barrier + eieio, + // Logical + eqvx, extsbx, extshx, extswx, + // FPU + fabsx, faddsx, faddx, fcfidx, fcmpo, fcmpu, fctidx, fctidzx, fctiwx, fctiwzx, + fdivsx, fdivx, fmaddsx, fmaddx, fmrx, fmsubsx, fmsubx, fmulsx, fmulx, + fnabsx, fnegx, fnmaddsx, fnmaddx, fnmsubsx, fnmsubx, fresx, frspx, frsqrtex, + fselx, fsqrtsx, fsqrtx, fsubsx, fsubx, + // Instruction cache + icbi, isync, + // Load byte + lbz, lbzu, lbzux, lbzx, + // Load doubleword + ld, ldarx, ldbrx, ldu, ldux, ldx, + // Load float + lfd, lfdu, lfdux, lfdx, lfs, lfsu, lfsux, lfsx, + // Load halfword + lha, lhau, lhaux, lhax, lhbrx, lhz, lhzu, lhzux, lhzx, + // Load multiple/string + lmw, lswi, lswx, + // Load vector + lvebx, lvehx, lvewx, lvewx128, lvlx, lvlx128, lvlxl, lvlxl128, + lvrx, lvrx128, lvrxl, lvrxl128, + lvsl, lvsl128, lvsr, lvsr128, + lvx, lvx128, lvxl, lvxl128, + // Load word + lwa, lwarx, lwaux, lwax, lwbrx, lwz, lwzu, lwzux, lwzx, + // Move CR + mcrf, mcrfs, mcrxr, + // Move from special + mfcr, mffsx, mfmsr, mfspr, mftb, mfvscr, + // Move to special + mtcrf, mtfsb0x, mtfsb1x, mtfsfix, mtfsfx, mtmsr, mtmsrd, mtspr, mtvscr, + // Multiply + mulhdux, mulhdx, mulhwux, mulhwx, mulldx, mulli, mullwx, + // Logical + nandx, negx, norx, orcx, ori, oris, orx, + // Rotate + rldclx, rldcrx, rldiclx, rldicrx, rldicx, rldimix, rlwimix, rlwinmx, rlwnmx, + // System call + sc, + // Shift + sldx, slwx, sradix, sradx, srawix, srawx, srdx, srwx, + // Store byte + stb, stbu, stbux, stbx, + // Store doubleword + std, stdbrx, stdcx, stdu, stdux, stdx, + // Store float + stfd, stfdu, stfdux, stfdx, stfiwx, stfs, stfsu, stfsux, stfsx, + // Store halfword + sth, sthbrx, sthu, sthux, sthx, + // Store multiple/string + stmw, stswi, stswx, + // Store vector + stvebx, stvehx, stvewx, stvewx128, stvlx, stvlx128, stvlxl, stvlxl128, + stvrx, stvrx128, stvrxl, stvrxl128, + stvx, stvx128, stvxl, stvxl128, + // Store word + stw, stwbrx, stwcx, stwu, stwux, stwx, + // Subtract + subfcx, subfex, subficx, subfmex, subfx, subfzex, + // Sync + sync, + // Trap + td, tdi, tw, twi, + // VMX integer + vaddcuw, vaddfp, vaddfp128, vaddsbs, vaddshs, vaddsws, + vaddubm, vaddubs, vadduhm, vadduhs, vadduwm, vadduws, + vand, vand128, vandc, vandc128, + vavgsb, vavgsh, vavgsw, vavgub, vavguh, vavguw, + vcfpsxws128, vcfpuxws128, vcfsx, vcfux, + vcmpbfp, vcmpbfp128, vcmpeqfp, vcmpeqfp128, + vcmpequb, vcmpequh, vcmpequw, vcmpequw128, + vcmpgefp, vcmpgefp128, vcmpgtfp, vcmpgtfp128, + vcmpgtsb, vcmpgtsh, vcmpgtsw, vcmpgtub, vcmpgtuh, vcmpgtuw, + vcsxwfp128, vctsxs, vctuxs, vcuxwfp128, + vexptefp, vexptefp128, vlogefp, vlogefp128, + vmaddcfp128, vmaddfp, vmaddfp128, + vmaxfp, vmaxfp128, vmaxsb, vmaxsh, vmaxsw, vmaxub, vmaxuh, vmaxuw, + vmhaddshs, vmhraddshs, + vminfp, vminfp128, vminsb, vminsh, vminsw, vminub, vminuh, vminuw, + vmladduhm, + vmrghb, vmrghh, vmrghw, vmrghw128, vmrglb, vmrglh, vmrglw, vmrglw128, + vmsum3fp128, vmsum4fp128, + vmsummbm, vmsumshm, vmsumshs, vmsumubm, vmsumuhm, vmsumuhs, + vmulesb, vmulesh, vmuleub, vmuleuh, vmulfp128, + vmulosb, vmulosh, vmuloub, vmulouh, + vnmsubfp, vnmsubfp128, vnor, vnor128, + vor, vor128, + vperm, vperm128, vpermwi128, vpkd3d128, + vpkpx, vpkshss, vpkshss128, vpkshus, vpkshus128, + vpkswss, vpkswss128, vpkswus, vpkswus128, + vpkuhum, vpkuhum128, vpkuhus, vpkuhus128, + vpkuwum, vpkuwum128, vpkuwus, vpkuwus128, + vrefp, vrefp128, + vrfim, vrfim128, vrfin, vrfin128, vrfip, vrfip128, vrfiz, vrfiz128, + vrlb, vrlh, vrlimi128, vrlw, vrlw128, + vrsqrtefp, vrsqrtefp128, + vsel, vsel128, + vsl, vslb, vsldoi, vsldoi128, vslh, vslo, vslo128, vslw, vslw128, + vspltb, vsplth, vspltisb, vspltish, vspltisw, vspltisw128, vspltw, vspltw128, + vsr, vsrab, vsrah, vsraw, vsraw128, vsrb, vsrh, vsro, vsro128, vsrw, vsrw128, + vsubcuw, vsubfp, vsubfp128, vsubsbs, vsubshs, vsubsws, + vsububm, vsububs, vsubuhm, vsubuhs, vsubuwm, vsubuws, + vsum2sws, vsum4sbs, vsum4shs, vsum4ubs, vsumsws, + vupkd3d128, vupkhpx, vupkhsb, vupkhsb128, vupkhsh, + vupklpx, vupklsb, vupklsb128, vupklsh, + vxor, vxor128, + // XOR immediate + xori, xoris, xorx, + // Invalid + Invalid, +} + +impl PpcOpcode { + /// Returns true if this opcode is a branch instruction. + pub fn is_branch(&self) -> bool { + matches!(self, Self::bx | Self::bcx | Self::bclrx | Self::bcctrx) + } + + /// Returns true if this opcode is a system call. + pub fn is_syscall(&self) -> bool { + matches!(self, Self::sc) + } + + /// Returns true if this is a load instruction. + pub fn is_load(&self) -> bool { + matches!(self, + Self::lbz | Self::lbzu | Self::lbzux | Self::lbzx | + Self::lhz | Self::lhzu | Self::lhzux | Self::lhzx | + Self::lha | Self::lhau | Self::lhaux | Self::lhax | + Self::lwz | Self::lwzu | Self::lwzux | Self::lwzx | + Self::lwa | Self::lwax | Self::lwaux | + Self::ld | Self::ldu | Self::ldux | Self::ldx | + Self::lfs | Self::lfsu | Self::lfsux | Self::lfsx | + Self::lfd | Self::lfdu | Self::lfdux | Self::lfdx | + Self::lhbrx | Self::lwbrx | Self::ldbrx | + Self::lmw | Self::lswi | Self::lswx | + Self::lwarx | Self::ldarx + ) + } + + /// Returns true if this is a store instruction. + pub fn is_store(&self) -> bool { + matches!(self, + Self::stb | Self::stbu | Self::stbux | Self::stbx | + Self::sth | Self::sthu | Self::sthux | Self::sthx | + Self::stw | Self::stwu | Self::stwux | Self::stwx | + Self::std | Self::stdu | Self::stdux | Self::stdx | + Self::stfs | Self::stfsu | Self::stfsux | Self::stfsx | + Self::stfd | Self::stfdu | Self::stfdux | Self::stfdx | + Self::sthbrx | Self::stwbrx | Self::stdbrx | + Self::stmw | Self::stswi | Self::stswx | + Self::stwcx | Self::stdcx | Self::stfiwx + ) + } + + pub fn name(&self) -> &'static str { + match self { + Self::Invalid => "invalid", + _ => { + // Use debug formatting to get the variant name + // This is a placeholder - in practice we'd have a lookup table + "?" + } + } + } +} + +impl std::fmt::Display for PpcOpcode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Debug::fmt(self, f) + } +} diff --git a/xenia-rs/crates/xenia-debugger/Cargo.toml b/xenia-rs/crates/xenia-debugger/Cargo.toml new file mode 100644 index 000000000..61b7402af --- /dev/null +++ b/xenia-rs/crates/xenia-debugger/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "xenia-debugger" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +xenia-types = { workspace = true } +xenia-memory = { workspace = true } +xenia-cpu = { workspace = true } +tracing = { workspace = true } +thiserror = { workspace = true } diff --git a/xenia-rs/crates/xenia-debugger/src/breakpoint.rs b/xenia-rs/crates/xenia-debugger/src/breakpoint.rs new file mode 100644 index 000000000..7559035f4 --- /dev/null +++ b/xenia-rs/crates/xenia-debugger/src/breakpoint.rs @@ -0,0 +1,7 @@ +/// A code breakpoint at a specific guest address. +#[derive(Debug, Clone)] +pub struct Breakpoint { + pub addr: u32, + pub enabled: bool, + pub condition: Option, +} diff --git a/xenia-rs/crates/xenia-debugger/src/lib.rs b/xenia-rs/crates/xenia-debugger/src/lib.rs new file mode 100644 index 000000000..0cf6d2baa --- /dev/null +++ b/xenia-rs/crates/xenia-debugger/src/lib.rs @@ -0,0 +1,125 @@ +pub mod breakpoint; +pub mod trace; + +use std::collections::HashMap; +use xenia_cpu::context::PpcContext; +use xenia_memory::MemoryAccess; + +pub use breakpoint::Breakpoint; +pub use trace::TraceEntry; + +/// The debugger. Hooks into every instruction step for observation. +pub struct Debugger { + pub breakpoints: HashMap, + pub trace_log: Vec, + pub trace_enabled: bool, + pub max_trace_entries: usize, + pub paused: bool, + pub step_mode: StepMode, + break_pending: bool, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum StepMode { + /// Run freely until breakpoint or pause + Run, + /// Execute one instruction then pause + StepInto, + /// Run but break after current function returns (when LR changes) + StepOver { return_addr: u32 }, +} + +impl Debugger { + pub fn new() -> Self { + Self { + breakpoints: HashMap::new(), + trace_log: Vec::new(), + trace_enabled: true, + max_trace_entries: 100_000, + paused: true, // Start paused for debugging + step_mode: StepMode::StepInto, + break_pending: false, + } + } + + /// Called before each instruction executes. + pub fn pre_step(&mut self, ctx: &PpcContext, _mem: &dyn MemoryAccess) { + // Check breakpoints + if let Some(bp) = self.breakpoints.get(&ctx.pc) { + if bp.enabled { + self.break_pending = true; + tracing::info!("Breakpoint hit at {:#010x}", ctx.pc); + } + } + } + + /// Called after each instruction executes. + pub fn post_step(&mut self, ctx: &PpcContext, _mem: &dyn MemoryAccess) { + // Log to trace + if self.trace_enabled { + if self.trace_log.len() >= self.max_trace_entries { + self.trace_log.remove(0); + } + self.trace_log.push(TraceEntry { + pc: ctx.pc, + cycle: ctx.cycle_count, + gpr_snapshot: [ctx.gpr[0], ctx.gpr[1], ctx.gpr[3], ctx.gpr[4]], + lr: ctx.lr, + }); + } + + // Handle step mode + match self.step_mode { + StepMode::StepInto => { + self.break_pending = true; + } + StepMode::StepOver { return_addr } => { + if ctx.pc == return_addr { + self.break_pending = true; + } + } + StepMode::Run => {} + } + } + + /// Should we break execution? + pub fn should_break(&self) -> bool { + self.break_pending || self.paused + } + + /// Add a breakpoint at the given address. + pub fn add_breakpoint(&mut self, addr: u32) { + self.breakpoints.insert(addr, Breakpoint { addr, enabled: true, condition: None }); + } + + /// Remove a breakpoint. + pub fn remove_breakpoint(&mut self, addr: u32) { + self.breakpoints.remove(&addr); + } + + /// Continue execution. + pub fn continue_execution(&mut self) { + self.paused = false; + self.break_pending = false; + self.step_mode = StepMode::Run; + } + + /// Step one instruction. + pub fn step_into(&mut self) { + self.paused = false; + self.break_pending = false; + self.step_mode = StepMode::StepInto; + } + + /// Clear break state after handling. + pub fn acknowledge_break(&mut self) { + self.break_pending = false; + self.paused = true; + } +} + +impl Default for Debugger { + fn default() -> Self { + Self::new() + } +} diff --git a/xenia-rs/crates/xenia-debugger/src/trace.rs b/xenia-rs/crates/xenia-debugger/src/trace.rs new file mode 100644 index 000000000..091c6c41b --- /dev/null +++ b/xenia-rs/crates/xenia-debugger/src/trace.rs @@ -0,0 +1,9 @@ +/// A single entry in the instruction trace log. +#[derive(Debug, Clone)] +pub struct TraceEntry { + pub pc: u32, + pub cycle: u64, + /// Snapshot of key GPRs: [r0, r1(sp), r3(arg0/retval), r4(arg1)] + pub gpr_snapshot: [u64; 4], + pub lr: u64, +} diff --git a/xenia-rs/crates/xenia-gpu/Cargo.toml b/xenia-rs/crates/xenia-gpu/Cargo.toml new file mode 100644 index 000000000..fe02e00d8 --- /dev/null +++ b/xenia-rs/crates/xenia-gpu/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "xenia-gpu" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +xenia-types = { workspace = true } +xenia-memory = { workspace = true } +tracing = { workspace = true } +thiserror = { workspace = true } +anyhow = { workspace = true } +byteorder = { workspace = true } diff --git a/xenia-rs/crates/xenia-gpu/src/command_processor.rs b/xenia-rs/crates/xenia-gpu/src/command_processor.rs new file mode 100644 index 000000000..ee854a3a1 --- /dev/null +++ b/xenia-rs/crates/xenia-gpu/src/command_processor.rs @@ -0,0 +1,17 @@ +/// PM4 command processor stub. +/// Will parse the GPU command ring buffer and dispatch to render operations. +pub struct CommandProcessor { + pub enabled: bool, +} + +impl CommandProcessor { + pub fn new() -> Self { + Self { enabled: false } + } +} + +impl Default for CommandProcessor { + fn default() -> Self { + Self::new() + } +} diff --git a/xenia-rs/crates/xenia-gpu/src/lib.rs b/xenia-rs/crates/xenia-gpu/src/lib.rs new file mode 100644 index 000000000..8adce9cd8 --- /dev/null +++ b/xenia-rs/crates/xenia-gpu/src/lib.rs @@ -0,0 +1,21 @@ +pub mod command_processor; +pub mod register_file; + +/// Stub GPU system for initial implementation. +pub struct GpuSystem { + pub register_file: register_file::RegisterFile, +} + +impl GpuSystem { + pub fn new() -> Self { + Self { + register_file: register_file::RegisterFile::new(), + } + } +} + +impl Default for GpuSystem { + fn default() -> Self { + Self::new() + } +} diff --git a/xenia-rs/crates/xenia-gpu/src/register_file.rs b/xenia-rs/crates/xenia-gpu/src/register_file.rs new file mode 100644 index 000000000..24fc52e94 --- /dev/null +++ b/xenia-rs/crates/xenia-gpu/src/register_file.rs @@ -0,0 +1,28 @@ +/// Xenos GPU register file. 0x6000 32-bit registers. +pub struct RegisterFile { + pub regs: Vec, +} + +impl RegisterFile { + pub fn new() -> Self { + Self { + regs: vec![0u32; 0x6000], + } + } + + pub fn read(&self, index: u32) -> u32 { + self.regs.get(index as usize).copied().unwrap_or(0) + } + + pub fn write(&mut self, index: u32, value: u32) { + if let Some(r) = self.regs.get_mut(index as usize) { + *r = value; + } + } +} + +impl Default for RegisterFile { + fn default() -> Self { + Self::new() + } +} diff --git a/xenia-rs/crates/xenia-hid/Cargo.toml b/xenia-rs/crates/xenia-hid/Cargo.toml new file mode 100644 index 000000000..b47a0a57d --- /dev/null +++ b/xenia-rs/crates/xenia-hid/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "xenia-hid" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +xenia-types = { workspace = true } +tracing = { workspace = true } +thiserror = { workspace = true } diff --git a/xenia-rs/crates/xenia-hid/src/lib.rs b/xenia-rs/crates/xenia-hid/src/lib.rs new file mode 100644 index 000000000..576fc5f54 --- /dev/null +++ b/xenia-rs/crates/xenia-hid/src/lib.rs @@ -0,0 +1,47 @@ +/// Human input device system stub. +pub struct InputSystem { + pub gamepad: GamepadState, +} + +#[derive(Default, Clone, Copy)] +pub struct GamepadState { + pub buttons: u16, + pub left_trigger: u8, + pub right_trigger: u8, + pub left_stick_x: i16, + pub left_stick_y: i16, + pub right_stick_x: i16, + pub right_stick_y: i16, +} + +/// Xbox 360 button flags +pub mod buttons { + pub const DPAD_UP: u16 = 0x0001; + pub const DPAD_DOWN: u16 = 0x0002; + pub const DPAD_LEFT: u16 = 0x0004; + pub const DPAD_RIGHT: u16 = 0x0008; + pub const START: u16 = 0x0010; + pub const BACK: u16 = 0x0020; + pub const LEFT_THUMB: u16 = 0x0040; + pub const RIGHT_THUMB: u16 = 0x0080; + pub const LEFT_SHOULDER: u16 = 0x0100; + pub const RIGHT_SHOULDER: u16 = 0x0200; + pub const A: u16 = 0x1000; + pub const B: u16 = 0x2000; + pub const X: u16 = 0x4000; + pub const Y: u16 = 0x8000; +} + +impl InputSystem { + pub fn new() -> Self { + Self { + gamepad: GamepadState::default(), + } + } +} + +impl Default for InputSystem { + fn default() -> Self { + Self::new() + } +} diff --git a/xenia-rs/crates/xenia-kernel/Cargo.toml b/xenia-rs/crates/xenia-kernel/Cargo.toml new file mode 100644 index 000000000..50de041d5 --- /dev/null +++ b/xenia-rs/crates/xenia-kernel/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "xenia-kernel" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +xenia-types = { workspace = true } +xenia-memory = { workspace = true } +xenia-cpu = { workspace = true } +tracing = { workspace = true } +thiserror = { workspace = true } +anyhow = { workspace = true } diff --git a/xenia-rs/crates/xenia-kernel/src/exports.rs b/xenia-rs/crates/xenia-kernel/src/exports.rs new file mode 100644 index 000000000..72c565a31 --- /dev/null +++ b/xenia-rs/crates/xenia-kernel/src/exports.rs @@ -0,0 +1,142 @@ +//! HLE kernel export implementations. +//! Each export mirrors a function from xboxkrnl_table.inc. + +use crate::state::{KernelState, ModuleId}; +use xenia_cpu::PpcContext; +use xenia_memory::GuestMemory; + +pub fn register_exports(state: &mut KernelState) { + use ModuleId::Xboxkrnl; + + // Memory + state.register_export(Xboxkrnl, 0xBB, "NtAllocateVirtualMemory", nt_allocate_virtual_memory); + state.register_export(Xboxkrnl, 0xBC, "NtFreeVirtualMemory", nt_free_virtual_memory); + state.register_export(Xboxkrnl, 0xC4, "NtQueryVirtualMemory", nt_query_virtual_memory); + + // Threading + state.register_export(Xboxkrnl, 0x0C, "ExCreateThread", ex_create_thread); + state.register_export(Xboxkrnl, 0x5F, "KeDelayExecutionThread", ke_delay_execution_thread); + + // Sync + state.register_export(Xboxkrnl, 0xC0, "NtCreateEvent", nt_create_event); + state.register_export(Xboxkrnl, 0x63, "KeSetEvent", ke_set_event); + state.register_export(Xboxkrnl, 0x6B, "KeWaitForSingleObject", ke_wait_for_single_object); + state.register_export(Xboxkrnl, 0x53, "NtClose", nt_close); + + // Module + state.register_export(Xboxkrnl, 0x195, "XexGetModuleHandle", xex_get_module_handle); + + // RTL + state.register_export(Xboxkrnl, 0x11A, "RtlInitAnsiString", rtl_init_ansi_string); + + // Video + state.register_export(Xboxkrnl, 0x142, "VdGetCurrentDisplayGamma", vd_get_current_display_gamma); + state.register_export(Xboxkrnl, 0x14B, "VdQueryVideoMode", vd_query_video_mode); + + // Debug + state.register_export(Xboxkrnl, 0x166, "DbgPrint", dbg_print); +} + +fn nt_allocate_virtual_memory(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) { + let _base_addr_ptr = ctx.gpr[3] as u32; + let _size_ptr = ctx.gpr[4] as u32; + // Stub: return success + ctx.gpr[3] = 0; // STATUS_SUCCESS +} + +fn nt_free_virtual_memory(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) { + ctx.gpr[3] = 0; +} + +fn nt_query_virtual_memory(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) { + ctx.gpr[3] = 0; +} + +fn ex_create_thread(ctx: &mut PpcContext, _mem: &mut GuestMemory, state: &mut KernelState) { + let handle = state.alloc_handle(); + // Write handle to output parameter (r3 = handle_ptr) + tracing::info!("ExCreateThread: allocated handle {:#x}", handle); + ctx.gpr[3] = 0; // STATUS_SUCCESS +} + +fn ke_delay_execution_thread(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) { + // In cooperative mode, this is where we'd yield to another thread + ctx.gpr[3] = 0; +} + +fn nt_create_event(ctx: &mut PpcContext, _mem: &mut GuestMemory, state: &mut KernelState) { + let _handle = state.alloc_handle(); + ctx.gpr[3] = 0; +} + +fn ke_set_event(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) { + ctx.gpr[3] = 0; +} + +fn ke_wait_for_single_object(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) { + // Stub: return immediately as if signaled + ctx.gpr[3] = 0; // STATUS_SUCCESS +} + +fn nt_close(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) { + ctx.gpr[3] = 0; +} + +fn xex_get_module_handle(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) { + ctx.gpr[3] = 0; // Return NULL for now +} + +fn rtl_init_ansi_string(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) { + use xenia_memory::MemoryAccess; + let dest_ptr = ctx.gpr[3] as u32; + let src_ptr = ctx.gpr[4] as u32; + + if src_ptr != 0 { + // Read string length + let mut len: u16 = 0; + let mut addr = src_ptr; + while mem.read_u8(addr) != 0 { + len += 1; + addr += 1; + } + // Write ANSI_STRING struct: {Length, MaxLength, Buffer} + mem.write_u16(dest_ptr, len); + mem.write_u16(dest_ptr + 2, len + 1); + mem.write_u32(dest_ptr + 4, src_ptr); + } +} + +fn vd_get_current_display_gamma(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) { + ctx.gpr[3] = 0; +} + +fn vd_query_video_mode(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) { + use xenia_memory::MemoryAccess; + let mode_ptr = ctx.gpr[3] as u32; + if mode_ptr != 0 { + // Write a basic video mode (1280x720) + mem.write_u32(mode_ptr, 1280); // width + mem.write_u32(mode_ptr + 4, 720); // height + mem.write_u32(mode_ptr + 8, 0); // is_interlaced + mem.write_u32(mode_ptr + 12, 0); // is_widescreen + mem.write_u32(mode_ptr + 16, 60); // refresh_rate + } + ctx.gpr[3] = 0; +} + +fn dbg_print(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) { + use xenia_memory::MemoryAccess; + let str_ptr = ctx.gpr[3] as u32; + if str_ptr != 0 { + let mut s = String::new(); + let mut addr = str_ptr; + loop { + let c = mem.read_u8(addr); + if c == 0 { break; } + s.push(c as char); + addr += 1; + } + tracing::info!("DbgPrint: {}", s); + } + ctx.gpr[3] = 0; +} diff --git a/xenia-rs/crates/xenia-kernel/src/lib.rs b/xenia-rs/crates/xenia-kernel/src/lib.rs new file mode 100644 index 000000000..617772466 --- /dev/null +++ b/xenia-rs/crates/xenia-kernel/src/lib.rs @@ -0,0 +1,4 @@ +pub mod exports; +pub mod state; + +pub use state::KernelState; diff --git a/xenia-rs/crates/xenia-kernel/src/state.rs b/xenia-rs/crates/xenia-kernel/src/state.rs new file mode 100644 index 000000000..01c97bf64 --- /dev/null +++ b/xenia-rs/crates/xenia-kernel/src/state.rs @@ -0,0 +1,80 @@ +use std::collections::HashMap; +use xenia_cpu::PpcContext; +use xenia_memory::GuestMemory; + +/// Function signature for HLE kernel exports. +pub type KernelExportFn = fn(&mut PpcContext, &mut GuestMemory, &mut KernelState); + +/// Module identifier for kernel exports. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ModuleId { + Xboxkrnl, + Xam, + Xbdm, +} + +/// Central kernel state tracking all guest OS state. +pub struct KernelState { + exports: HashMap<(ModuleId, u32), (&'static str, KernelExportFn)>, + next_handle: u32, +} + +impl KernelState { + pub fn new() -> Self { + let mut state = Self { + exports: HashMap::new(), + next_handle: 0x1000, + }; + crate::exports::register_exports(&mut state); + state + } + + pub fn register_export( + &mut self, + module: ModuleId, + ordinal: u32, + name: &'static str, + func: KernelExportFn, + ) { + self.exports.insert((module, ordinal), (name, func)); + } + + pub fn call_export( + &mut self, + module: ModuleId, + ordinal: u32, + ctx: &mut PpcContext, + mem: &mut GuestMemory, + ) -> bool { + if let Some(&(name, func)) = self.exports.get(&(module, ordinal)) { + tracing::info!( + "Kernel call: {:?}:{:#x} ({}) args=[{:#x}, {:#x}, {:#x}, {:#x}]", + module, ordinal, name, + ctx.gpr[3], ctx.gpr[4], ctx.gpr[5], ctx.gpr[6] + ); + func(ctx, mem, self); + tracing::info!(" -> returned {:#x}", ctx.gpr[3]); + true + } else { + tracing::warn!( + "Unimplemented kernel export: {:?}:{:#x}", + module, ordinal + ); + // Return 0 (STATUS_SUCCESS) by default for unimplemented calls + ctx.gpr[3] = 0; + false + } + } + + pub fn alloc_handle(&mut self) -> u32 { + let h = self.next_handle; + self.next_handle += 4; + h + } +} + +impl Default for KernelState { + fn default() -> Self { + Self::new() + } +} diff --git a/xenia-rs/crates/xenia-memory/Cargo.toml b/xenia-rs/crates/xenia-memory/Cargo.toml new file mode 100644 index 000000000..9b515b502 --- /dev/null +++ b/xenia-rs/crates/xenia-memory/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "xenia-memory" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +xenia-types = { workspace = true } +tracing = { workspace = true } +bitflags = { workspace = true } +thiserror = { workspace = true } + +[target.'cfg(unix)'.dependencies] +libc = "0.2" + +[target.'cfg(windows)'.dependencies] +windows-sys = { version = "0.59", features = ["Win32_System_Memory", "Win32_Foundation"] } diff --git a/xenia-rs/crates/xenia-memory/src/access.rs b/xenia-rs/crates/xenia-memory/src/access.rs new file mode 100644 index 000000000..7ee385510 --- /dev/null +++ b/xenia-rs/crates/xenia-memory/src/access.rs @@ -0,0 +1,47 @@ +/// Trait for all guest memory access. Every load/store goes through this, +/// enabling MMIO checking and debugger observation on every access. +/// This is the key abstraction that eliminates the need for MMIO exception handlers. +pub trait MemoryAccess { + fn read_u8(&self, addr: u32) -> u8; + fn read_u16(&self, addr: u32) -> u16; + fn read_u32(&self, addr: u32) -> u32; + fn read_u64(&self, addr: u32) -> u64; + fn read_f32(&self, addr: u32) -> f32 { + f32::from_bits(self.read_u32(addr)) + } + fn read_f64(&self, addr: u32) -> f64 { + f64::from_bits(self.read_u64(addr)) + } + + fn write_u8(&mut self, addr: u32, val: u8); + fn write_u16(&mut self, addr: u32, val: u16); + fn write_u32(&mut self, addr: u32, val: u32); + fn write_u64(&mut self, addr: u32, val: u64); + fn write_f32(&mut self, addr: u32, val: f32) { + self.write_u32(addr, val.to_bits()); + } + fn write_f64(&mut self, addr: u32, val: f64) { + self.write_u64(addr, val.to_bits()); + } + + /// Read a block of bytes from guest memory. + fn read_bytes(&self, addr: u32, buf: &mut [u8]) { + for (i, byte) in buf.iter_mut().enumerate() { + *byte = self.read_u8(addr.wrapping_add(i as u32)); + } + } + + /// Write a block of bytes to guest memory. + fn write_bytes(&mut self, addr: u32, buf: &[u8]) { + for (i, &byte) in buf.iter().enumerate() { + self.write_u8(addr.wrapping_add(i as u32), byte); + } + } + + /// Get a direct host pointer for the given guest address. + /// Returns None if the address is invalid or in an MMIO region. + fn translate(&self, addr: u32) -> Option<*const u8>; + + /// Get a mutable direct host pointer for the given guest address. + fn translate_mut(&mut self, addr: u32) -> Option<*mut u8>; +} diff --git a/xenia-rs/crates/xenia-memory/src/heap.rs b/xenia-rs/crates/xenia-memory/src/heap.rs new file mode 100644 index 000000000..07f034953 --- /dev/null +++ b/xenia-rs/crates/xenia-memory/src/heap.rs @@ -0,0 +1,242 @@ +use crate::access::MemoryAccess; +use crate::mmio::MmioRegion; +use crate::page_table::{AllocationState, MemoryProtect, PageEntry}; +use crate::MemoryError; + +const PAGE_SIZE: u32 = 4096; +/// Total guest address space: 4GB. +const GUEST_ADDRESS_SPACE: usize = 0x1_0000_0000; +/// Number of 4K pages in the 4GB address space. +const PAGE_COUNT: usize = GUEST_ADDRESS_SPACE / PAGE_SIZE as usize; +/// Physical memory mask (512MB physical address space). +const PHYSICAL_ADDR_MASK: u32 = 0x1FFF_FFFF; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HeapType { + GuestVirtual, + GuestXex, + GuestPhysical, +} + +/// The core guest memory system. Manages a 4GB virtual address space +/// via mmap/VirtualAlloc, with page-level tracking and MMIO dispatch. +pub struct GuestMemory { + /// Host pointer to the base of the 4GB guest address space. + membase: *mut u8, + /// Page table tracking allocation state for each 4K page. + page_table: Vec, + /// Registered MMIO regions (sorted by base address for binary search). + mmio_regions: Vec, + /// Whether the memory mapping is owned (should be unmapped on drop). + owned: bool, +} + +unsafe impl Send for GuestMemory {} +unsafe impl Sync for GuestMemory {} + +impl GuestMemory { + /// Create a new guest memory space by reserving a 4GB virtual address region. + pub fn new() -> Result { + let membase = crate::platform::reserve_address_space(GUEST_ADDRESS_SPACE)?; + Ok(Self { + membase, + page_table: vec![PageEntry::default(); PAGE_COUNT], + mmio_regions: Vec::new(), + owned: true, + }) + } + + /// Get the host base pointer for the guest address space. + pub fn membase(&self) -> *const u8 { + self.membase + } + + /// Get a mutable host base pointer. + pub fn membase_mut(&mut self) -> *mut u8 { + self.membase + } + + /// Translate a guest virtual address to a host pointer. + pub fn translate_virtual(&self, guest_addr: u32) -> *const u8 { + unsafe { self.membase.add(guest_addr as usize) } + } + + /// Translate a guest virtual address to a mutable host pointer. + pub fn translate_virtual_mut(&mut self, guest_addr: u32) -> *mut u8 { + unsafe { self.membase.add(guest_addr as usize) } + } + + /// Translate a guest physical address to a host pointer. + pub fn translate_physical(&self, guest_addr: u32) -> *const u8 { + let phys = guest_addr & PHYSICAL_ADDR_MASK; + unsafe { self.membase.add(phys as usize) } + } + + /// Register an MMIO region. + pub fn add_mmio_region(&mut self, region: MmioRegion) { + let base = region.base_address; + let idx = self + .mmio_regions + .binary_search_by_key(&base, |r| r.base_address) + .unwrap_or_else(|i| i); + self.mmio_regions.insert(idx, region); + } + + /// Check if an address is in a registered MMIO region. + fn find_mmio(&self, addr: u32) -> Option<&MmioRegion> { + self.mmio_regions.iter().find(|r| r.contains(addr)) + } + + /// Allocate a region in the guest address space. + pub fn alloc( + &mut self, + base: u32, + size: u32, + protect: MemoryProtect, + ) -> Result { + let page_start = (base / PAGE_SIZE) as usize; + let page_count = ((size + PAGE_SIZE - 1) / PAGE_SIZE) as usize; + + // Commit pages via platform + let host_ptr = unsafe { self.membase.add(base as usize) }; + crate::platform::commit_memory(host_ptr, (page_count * PAGE_SIZE as usize) as usize)?; + + // Update page table + for i in 0..page_count { + let idx = page_start + i; + if idx < self.page_table.len() { + let entry = &mut self.page_table[idx]; + entry.set_base_address(page_start as u32); + entry.set_region_page_count(page_count as u32); + entry.set_allocation_protect(protect); + entry.set_current_protect(protect); + entry.set_state(AllocationState::RESERVE | AllocationState::COMMIT); + } + } + + Ok(base) + } + + /// Read a slice of bytes from guest memory (bypassing MMIO for bulk reads). + pub fn read_bulk(&self, addr: u32, buf: &mut [u8]) { + let ptr = self.translate_virtual(addr); + unsafe { + std::ptr::copy_nonoverlapping(ptr, buf.as_mut_ptr(), buf.len()); + } + } + + /// Write a slice of bytes to guest memory (bypassing MMIO for bulk writes). + pub fn write_bulk(&mut self, addr: u32, buf: &[u8]) { + let ptr = self.translate_virtual_mut(addr); + unsafe { + std::ptr::copy_nonoverlapping(buf.as_ptr(), ptr, buf.len()); + } + } + + /// Get a page table entry for a given address. + pub fn page_entry(&self, addr: u32) -> &PageEntry { + let page = (addr / PAGE_SIZE) as usize; + &self.page_table[page] + } +} + +impl MemoryAccess for GuestMemory { + fn read_u8(&self, addr: u32) -> u8 { + let ptr = self.translate_virtual(addr); + unsafe { *ptr } + } + + fn read_u16(&self, addr: u32) -> u16 { + if let Some(mmio) = self.find_mmio(addr) { + (mmio.read_callback)(addr) as u16 + } else { + let ptr = self.translate_virtual(addr) as *const [u8; 2]; + u16::from_be_bytes(unsafe { *ptr }) + } + } + + fn read_u32(&self, addr: u32) -> u32 { + if let Some(mmio) = self.find_mmio(addr) { + (mmio.read_callback)(addr) + } else { + let ptr = self.translate_virtual(addr) as *const [u8; 4]; + u32::from_be_bytes(unsafe { *ptr }) + } + } + + fn read_u64(&self, addr: u32) -> u64 { + if let Some(mmio) = self.find_mmio(addr) { + let hi = (mmio.read_callback)(addr) as u64; + let lo = (mmio.read_callback)(addr.wrapping_add(4)) as u64; + (hi << 32) | lo + } else { + let ptr = self.translate_virtual(addr) as *const [u8; 8]; + u64::from_be_bytes(unsafe { *ptr }) + } + } + + fn write_u8(&mut self, addr: u32, val: u8) { + let ptr = self.translate_virtual_mut(addr); + unsafe { *ptr = val }; + } + + fn write_u16(&mut self, addr: u32, val: u16) { + if let Some(mmio) = self.find_mmio(addr) { + (mmio.write_callback)(addr, val as u32); + } else { + let ptr = self.translate_virtual_mut(addr); + unsafe { + std::ptr::copy_nonoverlapping(val.to_be_bytes().as_ptr(), ptr, 2); + } + } + } + + fn write_u32(&mut self, addr: u32, val: u32) { + if let Some(mmio) = self.find_mmio(addr) { + (mmio.write_callback)(addr, val); + } else { + let ptr = self.translate_virtual_mut(addr); + unsafe { + std::ptr::copy_nonoverlapping(val.to_be_bytes().as_ptr(), ptr, 4); + } + } + } + + fn write_u64(&mut self, addr: u32, val: u64) { + if let Some(mmio) = self.find_mmio(addr) { + (mmio.write_callback)(addr, (val >> 32) as u32); + (mmio.write_callback)(addr.wrapping_add(4), val as u32); + } else { + let ptr = self.translate_virtual_mut(addr); + unsafe { + std::ptr::copy_nonoverlapping(val.to_be_bytes().as_ptr(), ptr, 8); + } + } + } + + fn translate(&self, addr: u32) -> Option<*const u8> { + if self.find_mmio(addr).is_some() { + None + } else { + Some(self.translate_virtual(addr)) + } + } + + fn translate_mut(&mut self, addr: u32) -> Option<*mut u8> { + if self.find_mmio(addr).is_some() { + None + } else { + Some(self.translate_virtual_mut(addr)) + } + } +} + +impl Drop for GuestMemory { + fn drop(&mut self) { + if self.owned && !self.membase.is_null() { + unsafe { + crate::platform::release_address_space(self.membase, GUEST_ADDRESS_SPACE); + } + } + } +} diff --git a/xenia-rs/crates/xenia-memory/src/lib.rs b/xenia-rs/crates/xenia-memory/src/lib.rs new file mode 100644 index 000000000..f7d991a63 --- /dev/null +++ b/xenia-rs/crates/xenia-memory/src/lib.rs @@ -0,0 +1,31 @@ +pub mod access; +pub mod heap; +pub mod mmio; +pub mod page_table; + +mod platform; + +use thiserror::Error; + +pub use access::MemoryAccess; +pub use heap::{GuestMemory, HeapType}; +pub use mmio::MmioRegion; +pub use page_table::PageEntry; + +#[derive(Debug, Error)] +pub enum MemoryError { + #[error("Failed to allocate guest address space: {0}")] + AllocationFailed(String), + + #[error("Invalid guest address: {0:#010x}")] + InvalidAddress(u32), + + #[error("MMIO access at {0:#010x}")] + MmioAccess(u32), + + #[error("Protection violation at {0:#010x}")] + ProtectionViolation(u32), + + #[error("Out of memory in heap {0:?}")] + OutOfMemory(HeapType), +} diff --git a/xenia-rs/crates/xenia-memory/src/mmio.rs b/xenia-rs/crates/xenia-memory/src/mmio.rs new file mode 100644 index 000000000..fed319826 --- /dev/null +++ b/xenia-rs/crates/xenia-memory/src/mmio.rs @@ -0,0 +1,27 @@ +/// Represents a mapped MMIO region with read/write callbacks. +/// Instead of trapping access violations (as the C++ JIT does), the interpreter +/// explicitly checks each memory access against registered MMIO regions. +pub struct MmioRegion { + pub base_address: u32, + pub mask: u32, + pub size: u32, + pub read_callback: Box u32 + Send + Sync>, + pub write_callback: Box, +} + +impl MmioRegion { + pub fn contains(&self, addr: u32) -> bool { + let masked = addr & self.mask; + masked >= self.base_address && masked < self.base_address + self.size + } +} + +impl std::fmt::Debug for MmioRegion { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("MmioRegion") + .field("base_address", &format_args!("{:#010x}", self.base_address)) + .field("mask", &format_args!("{:#010x}", self.mask)) + .field("size", &format_args!("{:#x}", self.size)) + .finish() + } +} diff --git a/xenia-rs/crates/xenia-memory/src/page_table.rs b/xenia-rs/crates/xenia-memory/src/page_table.rs new file mode 100644 index 000000000..8d116eb7f --- /dev/null +++ b/xenia-rs/crates/xenia-memory/src/page_table.rs @@ -0,0 +1,122 @@ +use bitflags::bitflags; + +/// Describes a single page in the page table. +/// Mirrors the C++ `PageEntry` union from memory.h:82-99. +#[derive(Clone, Copy, Default)] +pub struct PageEntry(u64); + +impl PageEntry { + /// Base address of the allocated region in 4K pages (20 bits). + pub fn base_address(&self) -> u32 { + (self.0 & 0xFFFFF) as u32 + } + + pub fn set_base_address(&mut self, val: u32) { + self.0 = (self.0 & !0xFFFFF) | (val as u64 & 0xFFFFF); + } + + /// Total number of pages in the allocated region (20 bits). + pub fn region_page_count(&self) -> u32 { + ((self.0 >> 20) & 0xFFFFF) as u32 + } + + pub fn set_region_page_count(&mut self, val: u32) { + self.0 = (self.0 & !(0xFFFFF << 20)) | ((val as u64 & 0xFFFFF) << 20); + } + + /// Protection bits specified during region allocation (4 bits). + pub fn allocation_protect(&self) -> MemoryProtect { + MemoryProtect::from_bits_truncate(((self.0 >> 40) & 0xF) as u32) + } + + pub fn set_allocation_protect(&mut self, val: MemoryProtect) { + self.0 = (self.0 & !(0xF << 40)) | ((val.bits() as u64 & 0xF) << 40); + } + + /// Current protection bits (4 bits). + pub fn current_protect(&self) -> MemoryProtect { + MemoryProtect::from_bits_truncate(((self.0 >> 44) & 0xF) as u32) + } + + pub fn set_current_protect(&mut self, val: MemoryProtect) { + self.0 = (self.0 & !(0xF << 44)) | ((val.bits() as u64 & 0xF) << 44); + } + + /// Allocation state (2 bits). + pub fn state(&self) -> AllocationState { + AllocationState::from_bits_truncate(((self.0 >> 48) & 0x3) as u32) + } + + pub fn set_state(&mut self, val: AllocationState) { + self.0 = (self.0 & !(0x3 << 48)) | ((val.bits() as u64 & 0x3) << 48); + } + + pub fn is_committed(&self) -> bool { + self.state().contains(AllocationState::COMMIT) + } + + pub fn is_reserved(&self) -> bool { + self.state().contains(AllocationState::RESERVE) + } + + pub fn is_free(&self) -> bool { + self.state().is_empty() + } +} + +bitflags! { + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub struct MemoryProtect: u32 { + const READ = 1 << 0; + const WRITE = 1 << 1; + const NO_CACHE = 1 << 2; + const WRITE_COMBINE = 1 << 3; + } +} + +bitflags! { + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub struct AllocationState: u32 { + const RESERVE = 1 << 0; + const COMMIT = 1 << 1; + } +} + +impl std::fmt::Debug for PageEntry { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PageEntry") + .field("base_address", &format_args!("{:#x}", self.base_address())) + .field("region_page_count", &self.region_page_count()) + .field("allocation_protect", &self.allocation_protect()) + .field("current_protect", &self.current_protect()) + .field("state", &self.state()) + .finish() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_page_entry_bitfields() { + let mut entry = PageEntry::default(); + assert!(entry.is_free()); + + entry.set_base_address(0x100); + entry.set_region_page_count(0x10); + entry.set_allocation_protect(MemoryProtect::READ | MemoryProtect::WRITE); + entry.set_current_protect(MemoryProtect::READ); + entry.set_state(AllocationState::RESERVE | AllocationState::COMMIT); + + assert_eq!(entry.base_address(), 0x100); + assert_eq!(entry.region_page_count(), 0x10); + assert_eq!( + entry.allocation_protect(), + MemoryProtect::READ | MemoryProtect::WRITE + ); + assert_eq!(entry.current_protect(), MemoryProtect::READ); + assert!(entry.is_committed()); + assert!(entry.is_reserved()); + } +} diff --git a/xenia-rs/crates/xenia-memory/src/platform.rs b/xenia-rs/crates/xenia-memory/src/platform.rs new file mode 100644 index 000000000..a584eaea7 --- /dev/null +++ b/xenia-rs/crates/xenia-memory/src/platform.rs @@ -0,0 +1,98 @@ +use crate::MemoryError; + +/// Reserve a contiguous virtual address region without committing physical pages. +#[cfg(unix)] +pub fn reserve_address_space(size: usize) -> Result<*mut u8, MemoryError> { + unsafe { + let ptr = libc::mmap( + std::ptr::null_mut(), + size, + libc::PROT_NONE, + libc::MAP_PRIVATE | libc::MAP_ANONYMOUS | libc::MAP_NORESERVE, + -1, + 0, + ); + if ptr == libc::MAP_FAILED { + Err(MemoryError::AllocationFailed(format!( + "mmap failed for {} bytes: {}", + size, + std::io::Error::last_os_error() + ))) + } else { + Ok(ptr as *mut u8) + } + } +} + +/// Commit (make accessible) a region within a previously reserved address space. +#[cfg(unix)] +pub fn commit_memory(ptr: *mut u8, size: usize) -> Result<(), MemoryError> { + unsafe { + let result = libc::mprotect(ptr as *mut libc::c_void, size, libc::PROT_READ | libc::PROT_WRITE); + if result != 0 { + Err(MemoryError::AllocationFailed(format!( + "mprotect failed for {} bytes: {}", + size, + std::io::Error::last_os_error() + ))) + } else { + Ok(()) + } + } +} + +/// Release a previously reserved address space. +#[cfg(unix)] +pub unsafe fn release_address_space(ptr: *mut u8, size: usize) { + unsafe { libc::munmap(ptr as *mut libc::c_void, size); } +} + +#[cfg(windows)] +pub fn reserve_address_space(size: usize) -> Result<*mut u8, MemoryError> { + unsafe { + let ptr = windows_sys::Win32::System::Memory::VirtualAlloc( + std::ptr::null_mut(), + size, + windows_sys::Win32::System::Memory::MEM_RESERVE, + windows_sys::Win32::System::Memory::PAGE_NOACCESS, + ); + if ptr.is_null() { + Err(MemoryError::AllocationFailed(format!( + "VirtualAlloc reserve failed for {} bytes", + size, + ))) + } else { + Ok(ptr as *mut u8) + } + } +} + +#[cfg(windows)] +pub fn commit_memory(ptr: *mut u8, size: usize) -> Result<(), MemoryError> { + unsafe { + let result = windows_sys::Win32::System::Memory::VirtualAlloc( + ptr as *mut _, + size, + windows_sys::Win32::System::Memory::MEM_COMMIT, + windows_sys::Win32::System::Memory::PAGE_READWRITE, + ); + if result.is_null() { + Err(MemoryError::AllocationFailed(format!( + "VirtualAlloc commit failed for {} bytes", + size, + ))) + } else { + Ok(()) + } + } +} + +#[cfg(windows)] +pub unsafe fn release_address_space(ptr: *mut u8, size: usize) { + let _ = size; + windows_sys::Win32::System::Memory::VirtualFree( + ptr as *mut _, + 0, + windows_sys::Win32::System::Memory::MEM_RELEASE, + ); +} diff --git a/xenia-rs/crates/xenia-types/Cargo.toml b/xenia-rs/crates/xenia-types/Cargo.toml new file mode 100644 index 000000000..e95a51edd --- /dev/null +++ b/xenia-rs/crates/xenia-types/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "xenia-types" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +bitflags = { workspace = true } +byteorder = { workspace = true } +thiserror = { workspace = true } +serde = { workspace = true } diff --git a/xenia-rs/crates/xenia-types/src/endian.rs b/xenia-rs/crates/xenia-types/src/endian.rs new file mode 100644 index 000000000..a36b0e273 --- /dev/null +++ b/xenia-rs/crates/xenia-types/src/endian.rs @@ -0,0 +1,122 @@ +use serde::{Deserialize, Serialize}; +use std::fmt; +use std::marker::PhantomData; + +/// Big-endian value wrapper matching `xe::be` from the C++ codebase. +/// Stores the value in big-endian byte order and transparently converts +/// on access. Used for guest memory structures that are natively big-endian. +#[derive(Clone, Copy, Serialize, Deserialize)] +#[repr(transparent)] +pub struct Be(T::Bytes, PhantomData); + +impl Be { + pub fn new(val: T) -> Self { + Self(val.to_be_bytes(), PhantomData) + } + + pub fn get(self) -> T { + T::from_be_bytes(self.0) + } + + pub fn set(&mut self, val: T) { + self.0 = val.to_be_bytes(); + } + + pub fn raw_bytes(&self) -> &T::Bytes { + &self.0 + } +} + +impl Default for Be { + fn default() -> Self { + Self::new(T::default()) + } +} + +impl fmt::Debug for Be { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.get().fmt(f) + } +} + +impl fmt::Display for Be { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.get().fmt(f) + } +} + +impl PartialEq for Be { + fn eq(&self, other: &Self) -> bool { + // Compare raw bytes for efficiency (same byte order) + self.0.as_ref() == other.0.as_ref() + } +} + +impl Eq for Be {} + +/// Trait for types that can be converted to/from big-endian byte representations. +pub trait BeSwap: Copy { + type Bytes: Copy + AsRef<[u8]> + serde::Serialize + for<'de> serde::Deserialize<'de>; + fn to_be_bytes(self) -> Self::Bytes; + fn from_be_bytes(bytes: Self::Bytes) -> Self; +} + +macro_rules! impl_be_swap { + ($t:ty) => { + impl BeSwap for $t { + type Bytes = [u8; std::mem::size_of::<$t>()]; + fn to_be_bytes(self) -> Self::Bytes { + <$t>::to_be_bytes(self) + } + fn from_be_bytes(bytes: Self::Bytes) -> Self { + <$t>::from_be_bytes(bytes) + } + } + }; +} + +impl_be_swap!(u8); +impl_be_swap!(u16); +impl_be_swap!(u32); +impl_be_swap!(u64); +impl_be_swap!(i8); +impl_be_swap!(i16); +impl_be_swap!(i32); +impl_be_swap!(i64); +impl_be_swap!(f32); +impl_be_swap!(f64); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_be_u32() { + let v = Be::::new(0x12345678); + assert_eq!(v.get(), 0x12345678); + assert_eq!(v.raw_bytes(), &[0x12, 0x34, 0x56, 0x78]); + } + + #[test] + fn test_be_u16() { + let v = Be::::new(0xABCD); + assert_eq!(v.get(), 0xABCD); + assert_eq!(v.raw_bytes(), &[0xAB, 0xCD]); + } + + #[test] + fn test_be_mutate() { + let mut v = Be::::new(1); + assert_eq!(v.get(), 1); + v.set(42); + assert_eq!(v.get(), 42); + } + + #[test] + fn test_be_f32() { + let v = Be::::new(1.0); + assert_eq!(v.get(), 1.0); + // IEEE 754: 1.0f = 0x3F800000 => bytes [0x3F, 0x80, 0x00, 0x00] + assert_eq!(v.raw_bytes(), &[0x3F, 0x80, 0x00, 0x00]); + } +} diff --git a/xenia-rs/crates/xenia-types/src/error.rs b/xenia-rs/crates/xenia-types/src/error.rs new file mode 100644 index 000000000..cb0806a6a --- /dev/null +++ b/xenia-rs/crates/xenia-types/src/error.rs @@ -0,0 +1,27 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum XeniaError { + #[error("Invalid XEX2 file: {0}")] + InvalidXex(String), + + #[error("Invalid XISO file: {0}")] + InvalidXiso(String), + + #[error("Memory error: {0}")] + Memory(String), + + #[error("Unimplemented opcode: {0}")] + UnimplementedOpcode(String), + + #[error("Unimplemented kernel export: module={module} ordinal={ordinal:#x}")] + UnimplementedExport { module: String, ordinal: u32 }, + + #[error("Invalid guest address: {0:#010x}")] + InvalidAddress(u32), + + #[error("I/O error: {0}")] + Io(#[from] std::io::Error), +} + +pub type XeniaResult = Result; diff --git a/xenia-rs/crates/xenia-types/src/lib.rs b/xenia-rs/crates/xenia-types/src/lib.rs new file mode 100644 index 000000000..f635422e5 --- /dev/null +++ b/xenia-rs/crates/xenia-types/src/lib.rs @@ -0,0 +1,6 @@ +pub mod endian; +pub mod error; +pub mod vec128; + +pub use endian::Be; +pub use vec128::Vec128; diff --git a/xenia-rs/crates/xenia-types/src/vec128.rs b/xenia-rs/crates/xenia-types/src/vec128.rs new file mode 100644 index 000000000..122994ddf --- /dev/null +++ b/xenia-rs/crates/xenia-types/src/vec128.rs @@ -0,0 +1,161 @@ +use serde::{Deserialize, Serialize}; +use std::fmt; + +/// 128-bit vector register type matching the Xbox 360's VMX128 registers. +/// Stored in big-endian byte order (matching guest memory layout). +#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[repr(C, align(16))] +pub struct Vec128 { + pub bytes: [u8; 16], +} + +impl Vec128 { + pub const ZERO: Self = Self { bytes: [0; 16] }; + + pub fn from_u32x4(a: u32, b: u32, c: u32, d: u32) -> Self { + let mut bytes = [0u8; 16]; + bytes[0..4].copy_from_slice(&a.to_be_bytes()); + bytes[4..8].copy_from_slice(&b.to_be_bytes()); + bytes[8..12].copy_from_slice(&c.to_be_bytes()); + bytes[12..16].copy_from_slice(&d.to_be_bytes()); + Self { bytes } + } + + pub fn from_f32x4(a: f32, b: f32, c: f32, d: f32) -> Self { + Self::from_u32x4(a.to_bits(), b.to_bits(), c.to_bits(), d.to_bits()) + } + + /// Read the i-th u32 element (big-endian, 0-indexed). + pub fn u32x4(&self, i: usize) -> u32 { + let off = i * 4; + u32::from_be_bytes([ + self.bytes[off], + self.bytes[off + 1], + self.bytes[off + 2], + self.bytes[off + 3], + ]) + } + + /// Write the i-th u32 element (big-endian, 0-indexed). + pub fn set_u32x4(&mut self, i: usize, val: u32) { + let off = i * 4; + self.bytes[off..off + 4].copy_from_slice(&val.to_be_bytes()); + } + + /// Read the i-th f32 element (big-endian, 0-indexed). + pub fn f32x4(&self, i: usize) -> f32 { + f32::from_bits(self.u32x4(i)) + } + + /// Write the i-th f32 element (big-endian, 0-indexed). + pub fn set_f32x4(&mut self, i: usize, val: f32) { + self.set_u32x4(i, val.to_bits()); + } + + /// Read the i-th u16 element (big-endian, 0-indexed). + pub fn u16x8(&self, i: usize) -> u16 { + let off = i * 2; + u16::from_be_bytes([self.bytes[off], self.bytes[off + 1]]) + } + + /// Write the i-th u16 element (big-endian, 0-indexed). + pub fn set_u16x8(&mut self, i: usize, val: u16) { + let off = i * 2; + self.bytes[off..off + 2].copy_from_slice(&val.to_be_bytes()); + } + + /// Read the i-th u8 element (0-indexed). + pub fn u8x16(&self, i: usize) -> u8 { + self.bytes[i] + } + + /// Write the i-th u8 element (0-indexed). + pub fn set_u8x16(&mut self, i: usize, val: u8) { + self.bytes[i] = val; + } + + /// Read as two u64 values (big-endian). + pub fn u64x2(&self, i: usize) -> u64 { + let off = i * 8; + u64::from_be_bytes([ + self.bytes[off], + self.bytes[off + 1], + self.bytes[off + 2], + self.bytes[off + 3], + self.bytes[off + 4], + self.bytes[off + 5], + self.bytes[off + 6], + self.bytes[off + 7], + ]) + } + + pub fn set_u64x2(&mut self, i: usize, val: u64) { + let off = i * 8; + self.bytes[off..off + 8].copy_from_slice(&val.to_be_bytes()); + } +} + +impl Default for Vec128 { + fn default() -> Self { + Self::ZERO + } +} + +impl fmt::Debug for Vec128 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "Vec128({:08X}_{:08X}_{:08X}_{:08X})", + self.u32x4(0), + self.u32x4(1), + self.u32x4(2), + self.u32x4(3), + ) + } +} + +impl fmt::Display for Vec128 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(self, f) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_u32x4_roundtrip() { + let v = Vec128::from_u32x4(0xDEADBEEF, 0xCAFEBABE, 0x12345678, 0x9ABCDEF0); + assert_eq!(v.u32x4(0), 0xDEADBEEF); + assert_eq!(v.u32x4(1), 0xCAFEBABE); + assert_eq!(v.u32x4(2), 0x12345678); + assert_eq!(v.u32x4(3), 0x9ABCDEF0); + } + + #[test] + fn test_f32x4_roundtrip() { + let v = Vec128::from_f32x4(1.0, -2.5, 3.14, 0.0); + assert_eq!(v.f32x4(0), 1.0); + assert_eq!(v.f32x4(1), -2.5); + assert!((v.f32x4(2) - 3.14).abs() < f32::EPSILON); + assert_eq!(v.f32x4(3), 0.0); + } + + #[test] + fn test_u16x8() { + let v = Vec128::from_u32x4(0x00010002, 0x00030004, 0x00050006, 0x00070008); + assert_eq!(v.u16x8(0), 0x0001); + assert_eq!(v.u16x8(1), 0x0002); + assert_eq!(v.u16x8(6), 0x0007); + assert_eq!(v.u16x8(7), 0x0008); + } + + #[test] + fn test_zero() { + let v = Vec128::ZERO; + for i in 0..4 { + assert_eq!(v.u32x4(i), 0); + } + } +} diff --git a/xenia-rs/crates/xenia-vfs/Cargo.toml b/xenia-rs/crates/xenia-vfs/Cargo.toml new file mode 100644 index 000000000..e30e2c349 --- /dev/null +++ b/xenia-rs/crates/xenia-vfs/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "xenia-vfs" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +xenia-types = { workspace = true } +tracing = { workspace = true } +byteorder = { workspace = true } +thiserror = { workspace = true } +anyhow = { workspace = true } diff --git a/xenia-rs/crates/xenia-vfs/src/device.rs b/xenia-rs/crates/xenia-vfs/src/device.rs new file mode 100644 index 000000000..3a83bc8b9 --- /dev/null +++ b/xenia-rs/crates/xenia-vfs/src/device.rs @@ -0,0 +1,54 @@ +use crate::{VfsDevice, VfsEntry, VfsError}; +use std::path::{Path, PathBuf}; + +/// Host filesystem pass-through device. +pub struct HostPathDevice { + name: String, + root: PathBuf, +} + +impl HostPathDevice { + pub fn new(name: impl Into, root: impl AsRef) -> Self { + Self { + name: name.into(), + root: root.as_ref().to_path_buf(), + } + } +} + +impl VfsDevice for HostPathDevice { + fn name(&self) -> &str { + &self.name + } + + fn list_root(&self) -> Result, VfsError> { + let mut entries = Vec::new(); + for entry in std::fs::read_dir(&self.root)? { + let entry = entry?; + let metadata = entry.metadata()?; + entries.push(VfsEntry { + name: entry.file_name().to_string_lossy().into_owned(), + is_directory: metadata.is_dir(), + size: metadata.len(), + offset: 0, + }); + } + Ok(entries) + } + + fn read_file(&self, path: &str) -> Result, VfsError> { + let full_path = self.root.join(path); + std::fs::read(&full_path).map_err(VfsError::from) + } + + fn stat(&self, path: &str) -> Result { + let full_path = self.root.join(path); + let metadata = std::fs::metadata(&full_path)?; + Ok(VfsEntry { + name: path.to_string(), + is_directory: metadata.is_dir(), + size: metadata.len(), + offset: 0, + }) + } +} diff --git a/xenia-rs/crates/xenia-vfs/src/disc_image.rs b/xenia-rs/crates/xenia-vfs/src/disc_image.rs new file mode 100644 index 000000000..03fad7f9c --- /dev/null +++ b/xenia-rs/crates/xenia-vfs/src/disc_image.rs @@ -0,0 +1,40 @@ +use crate::{VfsDevice, VfsEntry, VfsError}; + +/// XISO disc image device. Parses Xbox 360 disc images. +pub struct DiscImageDevice { + name: String, + _data: Vec, +} + +/// XISO sector size +pub const SECTOR_SIZE: usize = 0x800; + +impl DiscImageDevice { + pub fn open(name: impl Into, path: &std::path::Path) -> Result { + let data = std::fs::read(path)?; + // TODO: validate XISO header + Ok(Self { + name: name.into(), + _data: data, + }) + } +} + +impl VfsDevice for DiscImageDevice { + fn name(&self) -> &str { + &self.name + } + + fn list_root(&self) -> Result, VfsError> { + // TODO: Parse XISO directory tree + Ok(Vec::new()) + } + + fn read_file(&self, _path: &str) -> Result, VfsError> { + Err(VfsError::NotFound("Not yet implemented".into())) + } + + fn stat(&self, _path: &str) -> Result { + Err(VfsError::NotFound("Not yet implemented".into())) + } +} diff --git a/xenia-rs/crates/xenia-vfs/src/lib.rs b/xenia-rs/crates/xenia-vfs/src/lib.rs new file mode 100644 index 000000000..86cedc252 --- /dev/null +++ b/xenia-rs/crates/xenia-vfs/src/lib.rs @@ -0,0 +1,33 @@ +pub mod device; +pub mod disc_image; + +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum VfsError { + #[error("I/O error: {0}")] + Io(#[from] std::io::Error), + + #[error("Invalid format: {0}")] + InvalidFormat(String), + + #[error("File not found: {0}")] + NotFound(String), +} + +/// A virtual filesystem entry (file or directory). +#[derive(Debug)] +pub struct VfsEntry { + pub name: String, + pub is_directory: bool, + pub size: u64, + pub offset: u64, +} + +/// Trait for VFS device implementations (XISO, STFS, host path, etc.) +pub trait VfsDevice: Send + Sync { + fn name(&self) -> &str; + fn list_root(&self) -> Result, VfsError>; + fn read_file(&self, path: &str) -> Result, VfsError>; + fn stat(&self, path: &str) -> Result; +} diff --git a/xenia-rs/crates/xenia-xex/Cargo.toml b/xenia-rs/crates/xenia-xex/Cargo.toml new file mode 100644 index 000000000..9b70e2eef --- /dev/null +++ b/xenia-rs/crates/xenia-xex/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "xenia-xex" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +xenia-types = { workspace = true } +xenia-memory = { workspace = true } +tracing = { workspace = true } +byteorder = { workspace = true } +thiserror = { workspace = true } +anyhow = { workspace = true } diff --git a/xenia-rs/crates/xenia-xex/src/header.rs b/xenia-rs/crates/xenia-xex/src/header.rs new file mode 100644 index 000000000..eadf977a3 --- /dev/null +++ b/xenia-rs/crates/xenia-xex/src/header.rs @@ -0,0 +1,55 @@ +/// XEX2 file header. Parsed from the beginning of an Xbox 360 executable. +#[derive(Debug)] +pub struct Xex2Header { + pub magic: u32, + pub module_flags: u32, + pub header_size: u32, + pub security_offset: u32, + pub header_count: u32, + pub optional_headers: Vec, + pub security_info: Option, +} + +#[derive(Debug)] +pub struct Xex2OptionalHeader { + pub key: u32, + pub value: u32, +} + +#[derive(Debug)] +pub struct Xex2SecurityInfo { + pub image_size: u32, + pub load_address: u32, + pub export_table_address: u32, + pub image_flags: u32, + pub page_descriptors: Vec, +} + +#[derive(Debug, Clone, Copy)] +pub struct Xex2PageDescriptor { + pub size_and_info: u32, +} + +impl Xex2PageDescriptor { + pub fn page_count(&self) -> u32 { + self.size_and_info >> 4 + } + + pub fn info(&self) -> u32 { + self.size_and_info & 0xF + } +} + +/// XEX2 magic: "XEX2" +pub const XEX2_MAGIC: u32 = 0x58455832; + +/// Optional header keys +pub mod header_keys { + pub const ENTRY_POINT: u32 = 0x00010100; + pub const IMAGE_BASE_ADDRESS: u32 = 0x00010201; + pub const IMPORT_LIBRARIES: u32 = 0x000103FF; + pub const TLS_INFO: u32 = 0x00020200; + pub const EXECUTION_INFO: u32 = 0x00040006; + pub const DEFAULT_STACK_SIZE: u32 = 0x00020200; + pub const ORIGINAL_PE_NAME: u32 = 0x000183FF; +} diff --git a/xenia-rs/crates/xenia-xex/src/lib.rs b/xenia-rs/crates/xenia-xex/src/lib.rs new file mode 100644 index 000000000..d910942dc --- /dev/null +++ b/xenia-rs/crates/xenia-xex/src/lib.rs @@ -0,0 +1,4 @@ +pub mod header; +pub mod loader; + +pub use header::Xex2Header; diff --git a/xenia-rs/crates/xenia-xex/src/loader.rs b/xenia-rs/crates/xenia-xex/src/loader.rs new file mode 100644 index 000000000..56aef4950 --- /dev/null +++ b/xenia-rs/crates/xenia-xex/src/loader.rs @@ -0,0 +1,98 @@ +use crate::header::*; +use byteorder::{BigEndian, ReadBytesExt}; +use std::io::{self, Cursor, Read, Seek, SeekFrom}; + +/// Parse a XEX2 header from raw file data. +pub fn parse_xex2_header(data: &[u8]) -> io::Result { + let mut cursor = Cursor::new(data); + + let magic = cursor.read_u32::()?; + if magic != XEX2_MAGIC { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("Invalid XEX2 magic: {:#010x} (expected {:#010x})", magic, XEX2_MAGIC), + )); + } + + let module_flags = cursor.read_u32::()?; + let header_size = cursor.read_u32::()?; + let _reserved = cursor.read_u32::()?; + let security_offset = cursor.read_u32::()?; + let header_count = cursor.read_u32::()?; + + let mut optional_headers = Vec::new(); + for _ in 0..header_count { + let key = cursor.read_u32::()?; + let value = cursor.read_u32::()?; + optional_headers.push(Xex2OptionalHeader { key, value }); + } + + // Parse security info + let security_info = if (security_offset as usize) < data.len() { + cursor.seek(SeekFrom::Start(security_offset as u64))?; + Some(parse_security_info(&mut cursor)?) + } else { + None + }; + + Ok(Xex2Header { + magic, + module_flags, + header_size, + security_offset, + header_count, + optional_headers, + security_info, + }) +} + +fn parse_security_info(cursor: &mut Cursor<&[u8]>) -> io::Result { + let _header_size = cursor.read_u32::()?; + let image_size = cursor.read_u32::()?; + + // Skip RSA signature (256 bytes) and other security fields + let mut skip_buf = [0u8; 256]; + cursor.read_exact(&mut skip_buf)?; + + // Skip image info hash (20 bytes) and import table hash (20 bytes) + cursor.read_exact(&mut [0u8; 20])?; + cursor.read_exact(&mut [0u8; 20])?; + + let load_address = cursor.read_u32::()?; + let _load_size = cursor.read_u32::()?; + let export_table_address = cursor.read_u32::()?; + let image_flags = cursor.read_u32::()?; + + // Read page descriptor count + let page_descriptor_count = cursor.read_u32::()?; + let mut page_descriptors = Vec::new(); + for _ in 0..page_descriptor_count { + let size_and_info = cursor.read_u32::()?; + page_descriptors.push(Xex2PageDescriptor { size_and_info }); + } + + Ok(Xex2SecurityInfo { + image_size, + load_address, + export_table_address, + image_flags, + page_descriptors, + }) +} + +/// Get an optional header value by key. +pub fn get_opt_header(header: &Xex2Header, key: u32) -> Option { + header.optional_headers.iter() + .find(|h| h.key == key) + .map(|h| h.value) +} + +/// Get the entry point address from the XEX2 header. +pub fn get_entry_point(header: &Xex2Header) -> Option { + get_opt_header(header, header_keys::ENTRY_POINT) +} + +/// Get the image base address. +pub fn get_image_base(header: &Xex2Header) -> Option { + get_opt_header(header, header_keys::IMAGE_BASE_ADDRESS) +}