10 Commits

Author SHA1 Message Date
MechaCat02
a519c76800 [Rust] Implement FPU/VMX128 opcodes, XEX LZX decompression, XISO browsing, and memory safety
Some checks failed
Orchestrator / Commit Message Validation (push) Has been skipped
Orchestrator / Lint (push) Successful in 2m5s
Orchestrator / Windows (x86-64) (push) Failing after 6m13s
Orchestrator / Linux (x86-64) (push) Failing after 20m59s
Orchestrator / Create Release (push) Has been skipped
Major additions to the xenia-rs Rust port:

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

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

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-12 21:32:46 +02:00
MechaCat02
06a23212fb Initial xenia-rs 2026-04-12 18:25:46 +02:00
Gliniak
1da37db584 [Winkey] Passthrough: Added support for hid key codes.
Thanks Devildwarf for initial implementation
2026-04-12 09:47:12 +02:00
Herman S.
65b74819aa [Threading] Implement priority boost on wake
When a thread wakes from a kernel wait, the Xenon scheduler boosts its
effective priority by the increment passed to the signaling call
(KeSetEvent, KeReleaseSemaphore, KeReleaseMutant). The boost is clamped
to the per-thread max_dynamic_priority cap, respects the guest
boost_disabled flag, and is drained on the next quantum expiry.
Guest KTHREAD priority fields are now initialized from parent process
defaults, and the previously unknown fields involved have been renamed
to match their identified purpose.
2026-04-11 17:05:05 +09:00
Herman S.
b3d8a21b72 [Threading] Add thread priority mapping with timer-driven quantum decay
And default ignore_thread_priorities to false.

Map Xenon's 0-31 priority range across all 5 host priority levels
instead of collapsing 0-17 into kNormal.

Use timer-driven quantum decay (~20ms period) matching Xenon's
60-quantum / 3-per-tick cycle to prevent starvation by gradually lowering
effective priority for non-real-time threads (< 18), piggybacking on the
existing 1ms timestamp timer.
2026-04-10 08:21:42 +09:00
Herman S.
61c8eb0707 [Threading] Improve same-CPU spinlock contention
Spinlock acquire now checks if the lock holder shares the same guest
CPU and yields more aggressively (Sleep(0)) when contending on the
same Xenon HW thread, which should better approximate real kernel's
implicit serialization.

Child threads without an explicit affinity mask now inherit the
parent's guest CPU assignment instead of round-robining, so the
spinlock check correctly identifies parent-child co-location.
2026-04-09 21:14:39 +09:00
Adrian
e23376afcc [Kernel] Fixed NtSignalAndWaitForSingleObjectEx prototype 2026-04-07 20:05:12 +02:00
Herman S.
4acda223db [CPU] Remove ATOMIC_EXCHANGE opcode (dead code) 2026-04-08 00:05:29 +09:00
Herman S.
ade7e610bb [XMA] Fix stall detection false positive in Work loop
Stall detection was triggering during multi-pass subframe consumption
(e.g. stereo with subframe_decode_count < total subframes), breaking
audio looping in games like Tomb Raider. Now only detects a stall when
no subframes were pending, so Consume-only iterations aren't mistaken
for no-progress cycles. Fixes Halo 4 without regressing Tomb Raider.
2026-04-07 23:14:08 +09:00
goldislead
8a49c0380f [GPU] EVENT_WRITE_ZPD relaxed END detection
Fixes culling flicker in 555307D5.
2026-04-04 22:18:17 +02:00
73 changed files with 9694 additions and 296 deletions

3
.gitignore vendored
View File

@@ -116,3 +116,6 @@ node_modules/.bin/
/cache0
/devkit
recent.toml
# Rust
target/

164
RUST_PORT_CODEMAP.md Normal file
View File

@@ -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<passes::ControlFlowAnalysisPass>());
```
### 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 <typename T>
using be = endian_store<T, std::endian::big>;
```
### 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<BlockInfo> 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<uint32_t> stack_base; // 0x5C
```
### 7c. Kernel State Management (`emulator.h:349`)
Central kernel object tracking all guest kernel state
```text
std::unique_ptr<kernel::KernelState> 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<SpirvBuilder>(
```
### 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",
```

1085
RUST_PORT_REPORT.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -176,6 +176,9 @@ bool XmaContextNew::Work() {
data.output_buffer_valid, data.subframe_decode_count,
data.output_buffer_padding);
const uint32_t pre_decode_offset = data.input_buffer_read_offset;
const uint8_t pre_remaining_subframes = current_frame_remaining_subframes_;
Decode(&data);
Consume(&output_rb, &data);
@@ -185,6 +188,21 @@ bool XmaContextNew::Work() {
id(), data.IsAnyInputBufferValid(), data.error_status);
break;
}
// If Decode didn't advance the read offset and produced no new frame,
// we can't make progress. Break to avoid spinning.
// Only check when there were no pending subframes — if we entered this
// iteration with subframes remaining, Decode() intentionally skipped
// (offset unchanged) while Consume() drained the frame.
if (pre_remaining_subframes == 0 &&
data.input_buffer_read_offset == pre_decode_offset &&
current_frame_remaining_subframes_ == 0) {
XELOGAPU(
"XmaContext {}: Decode stalled at offset {} (no progress), "
"waiting for next buffer",
id(), pre_decode_offset);
break;
}
}
data.output_buffer_write_offset =

View File

@@ -511,7 +511,7 @@ class IConfigVarUpdate {
// If you're reviewing a pull request with a change here, check if 1) has been
// done by the submitter before merging.
static constexpr uint32_t kLastCommittedUpdateDate =
MakeConfigVarUpdateDate(2025, 12, 4, 21);
MakeConfigVarUpdateDate(2026, 4, 9, 12);
virtual ~IConfigVarUpdate() = default;

View File

@@ -867,101 +867,6 @@ struct MEMSET_I64
EMITTER_OPCODE_TABLE(OPCODE_MEMSET, MEMSET_I64);
// ============================================================================
// OPCODE_ATOMIC_EXCHANGE
// ============================================================================
// Note: src1 is a HOST address (not guest), matching the x64 backend.
struct ATOMIC_EXCHANGE_I8
: Sequence<ATOMIC_EXCHANGE_I8,
I<OPCODE_ATOMIC_EXCHANGE, I8Op, I64Op, I8Op>> {
static void Emit(A64Emitter& e, const EmitArgType& i) {
// src1 is already a host address.
if (i.src1.is_constant) {
e.mov(e.x4, i.src1.constant());
} else {
e.mov(e.x4, i.src1);
}
if (i.src2.is_constant) {
e.mov(e.w0, static_cast<uint64_t>(
static_cast<uint32_t>(i.src2.constant()) & 0xFF));
} else {
e.and_(e.w0, i.src2, 0xFF);
}
if (e.IsFeatureEnabled(kA64EmitLSE)) {
e.swpalb(e.w0, i.dest, ptr(e.x4));
return;
}
auto& retry = e.NewCachedLabel();
e.L(retry);
e.ldaxrb(e.w1, ptr(e.x4));
e.stlxrb(e.w2, e.w0, ptr(e.x4));
e.cbnz(e.w2, retry);
e.mov(i.dest, e.w1);
}
};
struct ATOMIC_EXCHANGE_I16
: Sequence<ATOMIC_EXCHANGE_I16,
I<OPCODE_ATOMIC_EXCHANGE, I16Op, I64Op, I16Op>> {
static void Emit(A64Emitter& e, const EmitArgType& i) {
if (i.src1.is_constant) {
e.mov(e.x4, i.src1.constant());
} else {
e.mov(e.x4, i.src1);
}
if (i.src2.is_constant) {
e.mov(e.w0, static_cast<uint64_t>(
static_cast<uint32_t>(i.src2.constant()) & 0xFFFF));
} else {
e.and_(e.w0, i.src2, 0xFFFF);
}
if (e.IsFeatureEnabled(kA64EmitLSE)) {
e.swpalh(e.w0, i.dest, ptr(e.x4));
return;
}
auto& retry = e.NewCachedLabel();
e.L(retry);
e.ldaxrh(e.w1, ptr(e.x4));
e.stlxrh(e.w2, e.w0, ptr(e.x4));
e.cbnz(e.w2, retry);
e.mov(i.dest, e.w1);
}
};
struct ATOMIC_EXCHANGE_I32
: Sequence<ATOMIC_EXCHANGE_I32,
I<OPCODE_ATOMIC_EXCHANGE, I32Op, I64Op, I32Op>> {
static void Emit(A64Emitter& e, const EmitArgType& i) {
// src1 is a host address (not guest).
if (i.src1.is_constant) {
e.mov(e.x4, i.src1.constant());
} else {
e.mov(e.x4, i.src1);
}
if (i.src2.is_constant) {
e.mov(e.w0,
static_cast<uint64_t>(static_cast<uint32_t>(i.src2.constant())));
} else {
e.mov(e.w0, i.src2);
}
if (e.IsFeatureEnabled(kA64EmitLSE)) {
e.swpal(e.w0, i.dest, ptr(e.x4));
return;
}
auto& retry = e.NewCachedLabel();
e.L(retry);
e.ldaxr(e.w1, ptr(e.x4));
e.stlxr(e.w2, e.w0, ptr(e.x4));
e.cbnz(e.w2, retry);
e.mov(i.dest, e.w1);
}
};
EMITTER_OPCODE_TABLE(OPCODE_ATOMIC_EXCHANGE, ATOMIC_EXCHANGE_I8,
ATOMIC_EXCHANGE_I16, ATOMIC_EXCHANGE_I32);
// ============================================================================
// OPCODE_ATOMIC_COMPARE_EXCHANGE
// ============================================================================

View File

@@ -295,68 +295,6 @@ RegExp ComputeMemoryAddressOffset(X64Emitter& e, const T& guest,
}
}
// ============================================================================
// OPCODE_ATOMIC_EXCHANGE
// ============================================================================
// Note that the address we use here is a real, host address!
// This is weird, and should be fixed.
template <typename SEQ, typename REG, typename ARGS>
void EmitAtomicExchangeXX(X64Emitter& e, const ARGS& i) {
if (i.dest == i.src1) {
e.mov(e.rax, i.src1);
if (i.dest != i.src2) {
if (i.src2.is_constant) {
e.mov(i.dest, i.src2.constant());
} else {
e.mov(i.dest, i.src2);
}
}
e.lock();
e.xchg(e.dword[e.rax], i.dest);
} else {
if (i.dest != i.src2) {
if (i.src2.is_constant) {
e.mov(i.dest, i.src2.constant());
} else {
e.mov(i.dest, i.src2);
}
}
e.lock();
e.xchg(e.dword[i.src1.reg()], i.dest);
}
}
struct ATOMIC_EXCHANGE_I8
: Sequence<ATOMIC_EXCHANGE_I8,
I<OPCODE_ATOMIC_EXCHANGE, I8Op, I64Op, I8Op>> {
static void Emit(X64Emitter& e, const EmitArgType& i) {
EmitAtomicExchangeXX<ATOMIC_EXCHANGE_I8, Reg8>(e, i);
}
};
struct ATOMIC_EXCHANGE_I16
: Sequence<ATOMIC_EXCHANGE_I16,
I<OPCODE_ATOMIC_EXCHANGE, I16Op, I64Op, I16Op>> {
static void Emit(X64Emitter& e, const EmitArgType& i) {
EmitAtomicExchangeXX<ATOMIC_EXCHANGE_I16, Reg16>(e, i);
}
};
struct ATOMIC_EXCHANGE_I32
: Sequence<ATOMIC_EXCHANGE_I32,
I<OPCODE_ATOMIC_EXCHANGE, I32Op, I64Op, I32Op>> {
static void Emit(X64Emitter& e, const EmitArgType& i) {
EmitAtomicExchangeXX<ATOMIC_EXCHANGE_I32, Reg32>(e, i);
}
};
struct ATOMIC_EXCHANGE_I64
: Sequence<ATOMIC_EXCHANGE_I64,
I<OPCODE_ATOMIC_EXCHANGE, I64Op, I64Op, I64Op>> {
static void Emit(X64Emitter& e, const EmitArgType& i) {
EmitAtomicExchangeXX<ATOMIC_EXCHANGE_I64, Reg64>(e, i);
}
};
EMITTER_OPCODE_TABLE(OPCODE_ATOMIC_EXCHANGE, ATOMIC_EXCHANGE_I8,
ATOMIC_EXCHANGE_I16, ATOMIC_EXCHANGE_I32,
ATOMIC_EXCHANGE_I64);
struct LVL_V128 : Sequence<LVL_V128, I<OPCODE_LVL, V128Op, I64Op>> {
static void Emit(X64Emitter& e, const EmitArgType& i) {
e.mov(e.edx, 0xf);

View File

@@ -2225,17 +2225,6 @@ Value* HIRBuilder::Unpack(Value* value, uint32_t pack_flags) {
return i->dest;
}
Value* HIRBuilder::AtomicExchange(Value* address, Value* new_value) {
ASSERT_ADDRESS_TYPE(address);
ASSERT_INTEGER_TYPE(new_value);
Instr* i =
AppendInstr(OPCODE_ATOMIC_EXCHANGE_info, 0, AllocValue(new_value->type));
i->set_src1(address);
i->set_src2(new_value);
i->src3.value = NULL;
return i->dest;
}
Value* HIRBuilder::AtomicCompareExchange(Value* address, Value* old_value,
Value* new_value) {
ASSERT_ADDRESS_TYPE(address);

View File

@@ -297,7 +297,6 @@ class HIRBuilder {
Value* Pack(Value* value1, Value* value2, uint32_t pack_flags = 0);
Value* Unpack(Value* value, uint32_t pack_flags = 0);
Value* AtomicExchange(Value* address, Value* new_value);
Value* AtomicCompareExchange(Value* address, Value* old_value,
Value* new_value);
Value* AtomicAdd(Value* address, Value* value);

View File

@@ -282,7 +282,6 @@ enum Opcode {
OPCODE_PACK, // break up into smaller operations and add a float16 convert
// opcode
OPCODE_UNPACK,
OPCODE_ATOMIC_EXCHANGE,
OPCODE_ATOMIC_COMPARE_EXCHANGE,
OPCODE_SET_ROUNDING_MODE,
OPCODE_VECTOR_DENORMFLUSH, // converts denormals to signed zeros in a vector

View File

@@ -650,12 +650,6 @@ DEFINE_OPCODE(
OPCODE_SIG_V_V,
0)
DEFINE_OPCODE(
OPCODE_ATOMIC_EXCHANGE,
"atomic_exchange",
OPCODE_SIG_V_V_V,
OPCODE_FLAG_VOLATILE)
DEFINE_OPCODE(
OPCODE_ATOMIC_COMPARE_EXCHANGE,
"atomic_compare_exchange",

View File

@@ -646,45 +646,6 @@ TEST_CASE("SET_NJM_OFF", "[backend]") {
#endif
}
// =============================================================================
// Atomic Exchange I32
// =============================================================================
// Tests that AtomicExchange correctly swaps a value in memory and returns
// the old value.
// NOTE: OPCODE_ATOMIC_EXCHANGE uses a HOST address (not guest), per the
// x64 backend comment: "the address we use here is a real, host address!"
TEST_CASE("ATOMIC_EXCHANGE_I32", "[backend]") {
TestFunction test([](HIRBuilder& b) {
// r[4] holds the host address directly.
auto addr = LoadGPR(b, 4);
auto new_val = b.Truncate(LoadGPR(b, 5), hir::INT32_TYPE);
auto old_val = b.AtomicExchange(addr, new_val);
StoreGPR(b, 3, b.ZeroExtend(old_val, hir::INT64_TYPE));
b.Return();
});
// Allocate guest memory and compute the host pointer.
uint32_t guest_addr = test.memory->SystemHeapAlloc(4);
REQUIRE(guest_addr != 0);
auto* host_ptr = test.memory->TranslateVirtual(guest_addr);
test.Run(
[&](PPCContext* ctx) {
*reinterpret_cast<uint32_t*>(host_ptr) = 0xAABBCCDD;
// Pass the HOST address in r[4].
ctx->r[4] = reinterpret_cast<uint64_t>(host_ptr);
ctx->r[5] = 0x11223344;
},
[&](PPCContext* ctx) {
// r[3] should have the old value.
REQUIRE(static_cast<uint32_t>(ctx->r[3]) == 0xAABBCCDD);
// Memory should now have the new value.
REQUIRE(*reinterpret_cast<uint32_t*>(host_ptr) == 0x11223344);
});
test.memory->SystemHeapFree(guest_addr);
}
// =============================================================================
// DOT_PRODUCT_3 — inline NEON dot product of first 3 vector elements
// =============================================================================

View File

@@ -977,10 +977,10 @@ bool COMMAND_PROCESSOR::ExecutePacketType3_EVENT_WRITE_ZPD(
register_file_->values[XE_GPU_REG_RB_SAMPLE_COUNT_ADDR]);
// 0xFFFFFEED is written to this two locations by D3D only on D3DISSUE_END
// and used to detect a finished query.
bool is_end_via_z_pass = pSampleCounts->ZPass_A == kQueryFinished &&
bool is_end_via_z_pass = pSampleCounts->ZPass_A == kQueryFinished ||
pSampleCounts->ZPass_B == kQueryFinished;
// Older versions of D3D also checks for ZFail (4D5307D5).
bool is_end_via_z_fail = pSampleCounts->ZFail_A == kQueryFinished &&
bool is_end_via_z_fail = pSampleCounts->ZFail_A == kQueryFinished ||
pSampleCounts->ZFail_B == kQueryFinished;
std::memset(pSampleCounts, 0, sizeof(xe_gpu_depth_sample_counts));

View File

@@ -41,6 +41,125 @@ namespace xe {
namespace hid {
namespace winkey {
static uint8_t VirtualKeyToHIDUsage(UINT vk) {
// Letters: contiguous in both VK and HID space
if (vk >= 'A' && vk <= 'Z') {
return vk - 'A' + 0x04;
}
// Digits 1-9 (0 is irregular: 0x27)
if (vk >= '1' && vk <= '9') {
return vk - '1' + 0x1E;
}
// F1-F12
if (vk >= VK_F1 && vk <= VK_F12) {
return vk - VK_F1 + 0x3A;
}
// F13-F24
if (vk >= VK_F13 && vk <= VK_F24) {
return vk - VK_F13 + 0x68;
}
// Numpad 1-9 (0 is irregular: 0x62)
if (vk >= VK_NUMPAD1 && vk <= VK_NUMPAD9) {
return vk - VK_NUMPAD1 + 0x59;
}
// Modifiers (Left side starts at 0xE0, Right at 0xE4)
if (vk >= VK_LCONTROL && vk <= VK_LWIN) {
return vk - VK_LCONTROL + 0xE0;
}
if (vk >= VK_RCONTROL && vk <= VK_RWIN) {
return vk - VK_RCONTROL + 0xE4;
}
switch (vk) {
case '0':
return 0x27;
case VK_RETURN:
return 0x28;
case VK_ESCAPE:
return 0x29;
case VK_BACK:
return 0x2A;
case VK_TAB:
return 0x2B;
case VK_SPACE:
return 0x2C;
case VK_OEM_MINUS:
return 0x2D;
case VK_OEM_PLUS:
return 0x2E;
case VK_OEM_4:
return 0x2F;
case VK_OEM_6:
return 0x30;
case VK_OEM_5:
return 0x31;
case VK_OEM_1:
return 0x33;
case VK_OEM_7:
return 0x34;
case VK_OEM_3:
return 0x35;
case VK_OEM_COMMA:
return 0x36;
case VK_OEM_PERIOD:
return 0x37;
case VK_OEM_2:
return 0x38;
case VK_CAPITAL:
return 0x39;
case VK_SNAPSHOT:
return 0x46;
case VK_SCROLL:
return 0x47;
case VK_PAUSE:
return 0x48;
case VK_INSERT:
return 0x49;
case VK_HOME:
return 0x4A;
case VK_PRIOR:
return 0x4B;
case VK_DELETE:
return 0x4C;
case VK_END:
return 0x4D;
case VK_NEXT:
return 0x4E;
case VK_RIGHT:
return 0x4F;
case VK_LEFT:
return 0x50;
case VK_DOWN:
return 0x51;
case VK_UP:
return 0x52;
case VK_NUMLOCK:
return 0x53;
case VK_DIVIDE:
return 0x54;
case VK_MULTIPLY:
return 0x55;
case VK_SUBTRACT:
return 0x56;
case VK_ADD:
return 0x57;
case VK_NUMPAD0:
return 0x62;
case VK_DECIMAL:
return 0x63;
case VK_APPS:
return 0x65;
default:
break;
}
return 0x00;
}
bool static IsPassthroughEnabled() {
return static_cast<KeyboardMode>(cvars::keyboard_mode) ==
KeyboardMode::Passthrough;
@@ -344,10 +463,12 @@ X_RESULT WinKeyInputDriver::GetKeystroke(uint32_t user_index, uint32_t flags,
}
if (IsPassthroughEnabled()) {
const UINT vk = static_cast<UINT>(xinput_virtual_key);
hid_code = VirtualKeyToHIDUsage(vk);
if (GetKeyboardState(key_map_)) {
const UINT sc = MapVirtualKey(vk, MAPVK_VK_TO_VSC);
WCHAR buf;
if (ToUnicode(uint8_t(xinput_virtual_key), 0, key_map_, &buf, 1, 0) ==
1) {
if (ToUnicode(vk, sc, key_map_, &buf, 1, 0) == 1) {
keystroke_flags |= 0x1000; // XINPUT_KEYSTROKE_VALIDUNICODE
unicode = buf;
}

View File

@@ -1151,6 +1151,18 @@ void KernelState::UpdateKeTimestampBundle() {
xe::store_and_swap<uint64_t>(&lpKeTimeStampBundle->system_time,
Clock::QueryGuestSystemTime());
xe::store_and_swap<uint32_t>(&lpKeTimeStampBundle->tick_count, uptime_ms);
// Every 20 ticks (~20ms), decay priority on running guest threads.
// This simulates the Xenon decrementer-driven quantum expiration.
if (++quantum_timer_counter_ >= 20) {
quantum_timer_counter_ = 0;
auto global_lock = global_critical_region_.Acquire();
for (auto& [id, thread] : threads_by_id_) {
if (thread->is_running()) {
thread->CheckQuantumAndDecay();
}
}
}
}
uint32_t KernelState::GetKeTimestampBundle() {
@@ -1300,15 +1312,16 @@ void KernelState::EmulateCPInterruptDPC(uint32_t interrupt_callback,
}
void KernelState::InitializeProcess(X_KPROCESS* process, uint32_t type,
char unk_18, char unk_19, char unk_1A) {
char priority_class, char default_priority,
char max_dynamic_priority) {
uint32_t guest_kprocess = memory()->HostToGuestVirtual(process);
uint32_t thread_list_guest_ptr =
guest_kprocess + offsetof(X_KPROCESS, thread_list);
process->unk_18 = unk_18;
process->unk_19 = unk_19;
process->unk_1A = unk_1A;
process->process_priority_class = priority_class;
process->default_thread_priority = default_priority;
process->max_dynamic_priority = max_dynamic_priority;
util::XeInitializeListHead(&process->thread_list, thread_list_guest_ptr);
process->quantum = 60;
// doubt any guest code uses this ptr, which i think probably has something to
@@ -1316,7 +1329,7 @@ void KernelState::InitializeProcess(X_KPROCESS* process, uint32_t type,
process->clrdataa_masked_ptr = 0;
// clrdataa_ & ~(1U << 31);
process->thread_count = 0;
process->unk_1B = 0x06;
process->disable_quantum_decay = 0x06;
process->kernel_stack_size = 16 * 1024;
process->tls_slot_size = 0x80;

View File

@@ -66,10 +66,10 @@ struct X_KPROCESS {
// so it sets this ptr to 0x1C0000
xe::be<uint32_t> clrdataa_masked_ptr;
xe::be<uint32_t> thread_count;
uint8_t unk_18;
uint8_t unk_19;
uint8_t unk_1A;
uint8_t unk_1B;
uint8_t process_priority_class;
uint8_t default_thread_priority;
uint8_t max_dynamic_priority;
uint8_t disable_quantum_decay;
xe::be<uint32_t> kernel_stack_size;
xe::be<uint32_t> tls_static_data_address;
xe::be<uint32_t> tls_data_size;
@@ -332,8 +332,9 @@ class KernelState {
private:
void LoadKernelModule(object_ref<KernelModule> kernel_module);
void InitializeProcess(X_KPROCESS* process, uint32_t type, char unk_18,
char unk_19, char unk_1A);
void InitializeProcess(X_KPROCESS* process, uint32_t type,
char priority_class, char default_priority,
char max_dynamic_priority);
void SetProcessTLSVars(X_KPROCESS* process, int num_slots, int tls_data_size,
int tls_static_data_address);
void InitializeKernelGuestGlobals();
@@ -384,6 +385,7 @@ class KernelState {
BitMap tls_bitmap_;
uint32_t ke_timestamp_bundle_ptr_ = 0;
std::unique_ptr<xe::threading::HighResolutionTimer> timestamp_timer_;
uint32_t quantum_timer_counter_ = 0;
cpu::backend::GuestTrampolineGroup kernel_trampoline_group_;
// fixed address referenced by dashboards. Data is currently unknown
uint32_t strange_hardcoded_page_ = 0x8E038634 & (~0xFFFF);

View File

@@ -707,8 +707,7 @@ uint32_t xeKeReleaseSemaphore(X_KSEMAPHORE* semaphore_ptr, uint32_t increment,
return 0;
}
// TODO(benvanik): increment thread priority?
// TODO(benvanik): wait?
sem->set_priority_increment(increment);
int32_t previous_count = 0;
[[maybe_unused]] bool success =
@@ -1099,8 +1098,8 @@ DECLARE_XBOXKRNL_EXPORT3(NtWaitForMultipleObjectsEx, kThreading, kImplemented,
dword_result_t NtSignalAndWaitForSingleObjectEx_entry(dword_t signal_handle,
dword_t wait_handle,
dword_t wait_mode,
dword_t alertable,
dword_t r6,
lpqword_t timeout_ptr) {
X_STATUS result = X_STATUS_SUCCESS;
// pre-lock for these two handle lookups
@@ -1113,9 +1112,9 @@ dword_result_t NtSignalAndWaitForSingleObjectEx_entry(dword_t signal_handle,
global_critical_region::mutex().unlock();
if (signal_object && wait_object) {
uint64_t timeout = timeout_ptr ? static_cast<uint64_t>(*timeout_ptr) : 0u;
result =
XObject::SignalAndWait(signal_object.get(), wait_object.get(), 3, 1,
alertable, timeout_ptr ? &timeout : nullptr);
result = XObject::SignalAndWait(signal_object.get(), wait_object.get(), 3,
wait_mode, alertable,
timeout_ptr ? &timeout : nullptr);
} else {
result = X_STATUS_INVALID_HANDLE;
}
@@ -1138,11 +1137,34 @@ uint32_t xeKeKfAcquireSpinLock(PPCContext* ctx, X_KSPINLOCK* lock,
PrefetchForCAS(lock);
assert_true(lock->prcb_of_owner != static_cast<uint32_t>(ctx->r[13]));
uint32_t our_pcr = static_cast<uint32_t>(ctx->r[13]);
uint8_t our_cpu =
ctx->TranslateVirtualGPR<X_KPCR*>(our_pcr)->prcb_data.current_cpu;
// Lock.
while (!xe::atomic_cas(0, xe::byte_swap(static_cast<uint32_t>(ctx->r[13])),
&lock->prcb_of_owner.value)) {
// Spin!
// TODO(benvanik): error on deadlock?
while (
!xe::atomic_cas(0, xe::byte_swap(our_pcr), &lock->prcb_of_owner.value)) {
// On real hardware, threads sharing a Xenon HW thread are serialized by
// the kernel scheduler — the spinner would be preempted within one
// timeslice (~1ms) so the holder can make progress. In the naive
// host-thread model both threads run truly in parallel, so the spinner
// can burn its entire host quantum without giving the holder a chance.
//
// Check whether the lock holder is assigned to the same guest CPU as us.
// If so, yield the host thread aggressively (Sleep(0)) to force a host
// context switch and give the holder a chance to run and release.
// The relationship is stable — affinity doesn't change while a thread
// holds a spinlock — so one check per contention episode is sufficient.
uint32_t owner_pcr_be = lock->prcb_of_owner.value;
if (owner_pcr_be) {
uint32_t owner_pcr = xe::byte_swap(owner_pcr_be);
auto* owner_kpcr = ctx->TranslateVirtual<X_KPCR*>(owner_pcr);
if (owner_kpcr->prcb_data.current_cpu == our_cpu) {
xe::threading::Sleep(std::chrono::milliseconds(0));
continue;
}
}
xe::threading::MaybeYield();
}
@@ -1560,8 +1582,6 @@ DECLARE_XBOXKRNL_EXPORT2(KeInitializeDpc, kThreading, kImplemented, kSketchy);
dword_result_t KeInsertQueueDpc_entry(pointer_t<XDPC> dpc, dword_t arg1,
dword_t arg2) {
assert_always("DPC does not dispatch yet; going to hang!");
uint32_t list_entry_ptr = dpc.guest_address() + 4;
// Lock dispatcher.
@@ -1579,9 +1599,43 @@ dword_result_t KeInsertQueueDpc_entry(pointer_t<XDPC> dpc, dword_t arg1,
dpc_list->Insert(list_entry_ptr);
// Dispatch the DPC inline on the calling thread. On real hardware DPCs
// are deferred to DISPATCH_IRQL on the target processor, but DPC routines
// access per-CPU state via r13 (KPCR) so they must run on a thread whose
// KPCR is valid for the target CPU. The calling thread's KPCR satisfies
// this for the common case (desired_cpu_number == 0, meaning current CPU).
// Inline dispatch also avoids latency issues with shared work queues.
uint32_t routine = dpc->routine;
if (routine) {
auto thread = XThread::GetCurrentThread();
if (thread) {
auto thread_state = thread->thread_state();
auto ppc_context = thread_state->context();
auto kpcr = ppc_context->TranslateVirtualGPR<X_KPCR*>(ppc_context->r[13]);
// If we're already inside a DPC (reentrant KeInsertQueueDpc from a DPC
// routine), skip the impersonation — we're already at DISPATCH_IRQL.
bool already_in_dpc = kpcr->prcb_data.dpc_active != 0;
DPCImpersonationScope dpc_scope{};
if (!already_in_dpc) {
kernel_state()->BeginDPCImpersonation(ppc_context, dpc_scope);
}
uint64_t args[] = {dpc.guest_address(), (uint64_t)dpc->context,
(uint64_t)arg1, (uint64_t)arg2};
kernel_state()->processor()->Execute(thread_state, routine, args,
xe::countof(args));
if (!already_in_dpc) {
kernel_state()->EndDPCImpersonation(ppc_context, dpc_scope);
}
}
}
return 1;
}
DECLARE_XBOXKRNL_EXPORT2(KeInsertQueueDpc, kThreading, kStub, kSketchy);
DECLARE_XBOXKRNL_EXPORT2(KeInsertQueueDpc, kThreading, kImplemented, kSketchy);
dword_result_t KeRemoveQueueDpc_entry(pointer_t<XDPC> dpc) {
bool result = false;

View File

@@ -58,11 +58,13 @@ void XEvent::InitializeNative(void* native_ptr, X_DISPATCH_HEADER* header) {
}
int32_t XEvent::Set(uint32_t priority_increment, bool wait) {
set_priority_increment(priority_increment);
event_->Set();
return 1;
}
int32_t XEvent::Pulse(uint32_t priority_increment, bool wait) {
set_priority_increment(priority_increment);
event_->Pulse();
return 1;
}

View File

@@ -45,6 +45,8 @@ X_STATUS XMutant::ReleaseMutant(uint32_t priority_increment, bool abandon,
owning_thread_ = nullptr;
}
set_priority_increment(priority_increment);
// TODO(benvanik): abandoning.
assert_false(abandon);
if (mutant_->Release()) {

View File

@@ -203,13 +203,20 @@ X_STATUS XObject::Wait(uint32_t wait_reason, uint32_t processor_mode,
auto result =
xe::threading::Wait(wait_handle, alertable ? true : false, timeout_ms);
switch (result) {
case xe::threading::WaitResult::kSuccess:
WaitCallback();
return X_STATUS_SUCCESS;
case xe::threading::WaitResult::kUserCallback:
// Or X_STATUS_ALERTED?
case xe::threading::WaitResult::kUserCallback: {
auto current_thread = XThread::GetCurrentThread();
if (current_thread) {
current_thread->BoostOnWake(priority_increment());
}
if (result == xe::threading::WaitResult::kSuccess) {
WaitCallback();
return X_STATUS_SUCCESS;
}
return X_STATUS_USER_APC;
}
case xe::threading::WaitResult::kTimeout:
xe::threading::MaybeYield();
return X_STATUS_TIMEOUT;
@@ -231,13 +238,20 @@ X_STATUS XObject::SignalAndWait(XObject* signal_object, XObject* wait_object,
auto result = xe::threading::SignalAndWait(
signal_object->GetWaitHandle(), wait_object->GetWaitHandle(),
alertable ? true : false, timeout_ms);
switch (result) {
case xe::threading::WaitResult::kSuccess:
wait_object->WaitCallback();
return X_STATUS_SUCCESS;
case xe::threading::WaitResult::kUserCallback:
// Or X_STATUS_ALERTED?
case xe::threading::WaitResult::kUserCallback: {
auto current_thread = XThread::GetCurrentThread();
if (current_thread) {
current_thread->BoostOnWake(wait_object->priority_increment());
}
if (result == xe::threading::WaitResult::kSuccess) {
wait_object->WaitCallback();
return X_STATUS_SUCCESS;
}
return X_STATUS_USER_APC;
}
case xe::threading::WaitResult::kTimeout:
xe::threading::MaybeYield();
return X_STATUS_TIMEOUT;
@@ -264,25 +278,31 @@ X_STATUS XObject::WaitMultiple(uint32_t count, XObject** objects,
TimeoutTicksToMs(*opt_timeout)))
: std::chrono::milliseconds::max();
X_STATUS status;
uint32_t boost_increment = 0;
if (wait_type) {
auto result = xe::threading::WaitAny(wait_handles, count,
alertable ? true : false, timeout_ms);
switch (result.first) {
case xe::threading::WaitResult::kSuccess:
objects[result.second]->WaitCallback();
return X_STATUS(result.second);
boost_increment = objects[result.second]->priority_increment();
status = X_STATUS(result.second);
break;
case xe::threading::WaitResult::kUserCallback:
// Or X_STATUS_ALERTED?
return X_STATUS_USER_APC;
status = X_STATUS_USER_APC;
break;
case xe::threading::WaitResult::kTimeout:
xe::threading::MaybeYield();
return X_STATUS_TIMEOUT;
default:
status = X_STATUS_TIMEOUT;
break;
case xe::threading::WaitResult::kAbandoned:
return X_STATUS(X_STATUS_ABANDONED_WAIT_0 + result.second);
status = X_STATUS(X_STATUS_ABANDONED_WAIT_0 + result.second);
break;
default:
case xe::threading::WaitResult::kFailed:
return X_STATUS_UNSUCCESSFUL;
status = X_STATUS_UNSUCCESSFUL;
break;
}
} else {
auto result = xe::threading::WaitAll(wait_handles, count,
@@ -291,21 +311,38 @@ X_STATUS XObject::WaitMultiple(uint32_t count, XObject** objects,
case xe::threading::WaitResult::kSuccess:
for (uint32_t i = 0; i < count; i++) {
objects[i]->WaitCallback();
// Use the largest increment among the signaled objects.
if (objects[i]->priority_increment() > boost_increment) {
boost_increment = objects[i]->priority_increment();
}
}
return X_STATUS_SUCCESS;
status = X_STATUS_SUCCESS;
break;
case xe::threading::WaitResult::kUserCallback:
// Or X_STATUS_ALERTED?
return X_STATUS_USER_APC;
status = X_STATUS_USER_APC;
break;
case xe::threading::WaitResult::kTimeout:
xe::threading::MaybeYield();
return X_STATUS_TIMEOUT;
status = X_STATUS_TIMEOUT;
break;
default:
case xe::threading::WaitResult::kAbandoned:
case xe::threading::WaitResult::kFailed:
return X_STATUS_ABANDONED_WAIT_0;
status = X_STATUS_ABANDONED_WAIT_0;
break;
}
}
// Apply priority boost if the thread actually blocked (not on
// timeout/failure).
if (status != X_STATUS_TIMEOUT && status != X_STATUS_UNSUCCESSFUL &&
status != X_STATUS_ABANDONED_WAIT_0) {
auto current_thread = XThread::GetCurrentThread();
if (current_thread) {
current_thread->BoostOnWake(boost_increment);
}
}
return status;
}
uint8_t* XObject::CreateNative(uint32_t size) {

View File

@@ -226,6 +226,12 @@ class XObject {
void* native_ptr, int32_t as_type = -1,
bool already_locked = false);
// Priority increment stored by the most recent signal operation
// (KeSetEvent, KeReleaseSemaphore, etc.). Read by the waiter on wake
// to apply a priority boost matching real Xenon scheduler behavior.
uint32_t priority_increment() const { return priority_increment_; }
void set_priority_increment(uint32_t inc) { priority_increment_ = inc; }
protected:
bool SaveObject(ByteStream* stream);
bool RestoreObject(ByteStream* stream);
@@ -253,6 +259,8 @@ class XObject {
KernelState* kernel_state_;
uint32_t priority_increment_ = 0;
// Host objects are persisted through resets/etc.
bool host_object_ = false;

View File

@@ -14,6 +14,7 @@
#endif
#include "xenia/base/byte_stream.h"
#include "xenia/base/clock.h"
#include "xenia/base/logging.h"
#include "xenia/base/platform.h"
#include "xenia/base/profiling.h"
@@ -24,8 +25,9 @@
#include "xenia/kernel/user_module.h"
#include "xenia/kernel/xboxkrnl/xboxkrnl_threading.h"
DEFINE_bool(ignore_thread_priorities, true,
DEFINE_bool(ignore_thread_priorities, false,
"Ignores game-specified thread priorities.", "Kernel");
UPDATE_from_bool(ignore_thread_priorities, 2026, 4, 9, 12, true);
DEFINE_bool(ignore_thread_affinities, true,
"Ignores game-specified thread affinities.", "Kernel");
@@ -155,13 +157,17 @@ static uint8_t next_cpu = 0;
static uint8_t GetFakeCpuNumber(uint8_t proc_mask) {
// NOTE: proc_mask is logical processors, not physical processors or cores.
if (!proc_mask) {
next_cpu = (next_cpu + 1) % 6;
return next_cpu; // is this reasonable?
// TODO(Triang3l): Does the following apply here?
// On Xbox 360, threads without an explicit processor assignment stay on
// the same hardware thread as the parent. Preserve this so that the
// guest CPU assignment reflects the game's intent — parent-child thread
// pairs that share a HW thread may rely on implicit serialization.
// https://docs.microsoft.com/en-us/windows/win32/dxtecharts/coding-for-multiple-cores
// "On Xbox 360, you must explicitly assign software threads to a particular
// hardware thread by using XSetThreadProcessor. Otherwise, all child
// threads will stay on the same hardware thread as the parent."
XThread* parent = current_xthread_tls_;
if (parent) {
return parent->active_cpu();
}
next_cpu = (next_cpu + 1) % 6;
return next_cpu;
}
assert_false(proc_mask & 0xC0);
@@ -208,6 +214,19 @@ void XThread::InitializeGuestObject() {
guest_thread->apc_lists[0].Initialize(memory());
guest_thread->apc_lists[1].Initialize(memory());
guest_thread->process_priority_class = process->process_priority_class;
auto base_prio = process->default_thread_priority;
guest_thread->base_priority_copy = base_prio;
guest_thread->base_priority = base_prio;
guest_thread->priority = base_prio;
guest_thread->max_dynamic_priority = process->max_dynamic_priority;
guest_thread->quantum = process->quantum;
// Sync the host-side priority tracking to match the guest defaults.
// Games may later override these via KeSetPriorityThread.
priority_ = base_prio;
base_priority_ = base_prio;
guest_thread->a_prcb_ptr = kpcrb;
guest_thread->another_prcb_ptr = kpcrb;
@@ -666,28 +685,129 @@ void XThread::RundownAPCs() {
int32_t XThread::QueryPriority() { return thread_->priority(); }
void XThread::SetPriority(int32_t increment) {
if (is_guest_thread()) {
guest_object<X_KTHREAD>()->priority = static_cast<uint8_t>(increment);
}
priority_ = increment;
int32_t target_priority = 0;
if (increment > 0x22) {
target_priority = xe::threading::ThreadPriority::kHighest;
} else if (increment > 0x11) {
target_priority = xe::threading::ThreadPriority::kAboveNormal;
} else if (increment < -0x22) {
target_priority = xe::threading::ThreadPriority::kLowest;
} else if (increment < -0x11) {
target_priority = xe::threading::ThreadPriority::kBelowNormal;
// Map Xenon's 0-31 priority range across the available host priority levels.
// Priority 18 (0x12) is the Xenon real-time threshold — threads at or above
// it don't get quantum decay on real hardware.
static int32_t GuestPriorityToHost(int32_t guest_priority) {
if (guest_priority >= 24) {
return xe::threading::ThreadPriority::kHighest;
} else if (guest_priority >= 17) {
return xe::threading::ThreadPriority::kAboveNormal;
} else if (guest_priority >= 10) {
return xe::threading::ThreadPriority::kNormal;
} else if (guest_priority >= 5) {
return xe::threading::ThreadPriority::kBelowNormal;
} else {
target_priority = xe::threading::ThreadPriority::kNormal;
return xe::threading::ThreadPriority::kLowest;
}
}
void XThread::SetPriority(int32_t increment) {
// Clamp to valid Xenon priority range. Negative values can arrive via
// KeSetBasePriorityThread (signed offset from process base).
int32_t clamped = std::max(increment, 0);
if (is_guest_thread()) {
guest_object<X_KTHREAD>()->priority = static_cast<uint8_t>(clamped);
}
priority_ = clamped;
base_priority_ = clamped;
quantum_start_ms_ = Clock::QueryHostUptimeMillis();
if (!cvars::ignore_thread_priorities) {
thread_->set_priority(target_priority);
thread_->set_priority(GuestPriorityToHost(clamped));
}
}
void XThread::CheckQuantumAndDecay() {
if (cvars::ignore_thread_priorities) return;
// Real-time threads (current priority >= 0x12) don't decay on Xenon.
if (priority_ >= 18) return;
uint64_t now = Clock::QueryHostUptimeMillis();
uint64_t elapsed = now - quantum_start_ms_;
// On Xenon, the clock interrupt fires every ~1ms and decrements the
// thread's quantum by 3. The process quantum is 60, so it takes ~20ms
// for quantum to expire. When it does, the scheduler decays the
// effective priority by exactly 1 and resets quantum. We approximate
// this by decaying 1 priority level per 20ms of elapsed wall-clock time.
constexpr uint64_t kQuantumPeriodMs = 20;
if (elapsed < kQuantumPeriodMs) return;
int32_t decay_steps = static_cast<int32_t>(elapsed / kQuantumPeriodMs);
// On the first decay step, drain the accumulated priority boost as well.
// The real kernel computes: new_prio = priority - boost_accumulator - 1
// then zeroes the accumulator. Additional decay steps (if the timer
// callback was late) each subtract 1 more.
int32_t total_decay = boost_amount_ + decay_steps;
boost_amount_ = 0;
int32_t new_priority = priority_ - total_decay;
if (new_priority < base_priority_) {
new_priority = base_priority_;
}
if (new_priority != priority_) {
priority_ = new_priority;
if (is_guest_thread()) {
guest_object<X_KTHREAD>()->priority = static_cast<uint8_t>(new_priority);
}
thread_->set_priority(GuestPriorityToHost(new_priority));
}
quantum_start_ms_ = now;
}
void XThread::BoostOnWake(int32_t increment) {
if (cvars::ignore_thread_priorities) return;
// Real-time threads (priority >= 0x12) just get their quantum reset.
if (priority_ >= 18) {
boost_amount_ = 0;
quantum_start_ms_ = Clock::QueryHostUptimeMillis();
return;
}
// Match the real kernel (xeEnqueueThreadPostWait):
// - Only apply boost if there is no pending decay (priority_decrement == 0)
// AND boost is not disabled on this thread.
// - Boosted priority = base + increment, clamped to max_priority_cap.
// - Only boost UP — never lower priority below its current value.
bool apply_boost = false;
if (increment > 0 && is_guest_thread()) {
auto* kthread = guest_object<X_KTHREAD>();
if (kthread->priority_decrement == 0 && !kthread->boost_disabled) {
apply_boost = true;
}
} else if (increment > 0) {
// Host threads (non-guest): apply boost unconditionally.
apply_boost = true;
}
if (apply_boost) {
int32_t boosted = base_priority_ + increment;
// Clamp to the per-thread max dynamic priority cap.
// For title threads this is 17 (just below real-time threshold).
int32_t max_cap = 17;
if (is_guest_thread()) {
uint8_t guest_cap = guest_object<X_KTHREAD>()->max_dynamic_priority;
if (guest_cap > 0) {
max_cap = guest_cap;
}
}
if (boosted > max_cap) {
boosted = max_cap;
}
// Only boost UP, never lower.
if (boosted > priority_) {
priority_ = boosted;
boost_amount_ = priority_ - base_priority_;
if (is_guest_thread()) {
guest_object<X_KTHREAD>()->priority = static_cast<uint8_t>(priority_);
}
thread_->set_priority(GuestPriorityToHost(priority_));
}
}
quantum_start_ms_ = Clock::QueryHostUptimeMillis();
}
void XThread::SetAffinity(uint32_t affinity) {
SetActiveCpu(GetFakeCpuNumber(affinity));
}

View File

@@ -298,9 +298,9 @@ struct X_KTHREAD {
uint8_t unk_A5[0xB]; // 0xA5
int32_t apc_disable_count; // 0xB0
xe::be<int32_t> quantum; // 0xB4
uint8_t unk_B8; // 0xB8
uint8_t unk_B9; // 0xB9
uint8_t unk_BA; // 0xBA
uint8_t saturation_increment; // 0xB8
uint8_t base_priority; // 0xB9
uint8_t priority_decrement; // 0xBA
uint8_t boost_disabled; // 0xBB
uint8_t suspend_count; // 0xBC
uint8_t was_preempted; // 0xBD
@@ -310,9 +310,9 @@ struct X_KTHREAD {
// all
TypedGuestPointer<X_KPRCB> a_prcb_ptr; // 0xC0
TypedGuestPointer<X_KPRCB> another_prcb_ptr; // 0xC4
uint8_t unk_C8; // 0xC8
uint8_t unk_C9; // 0xC9
uint8_t unk_CA; // 0xCA
uint8_t process_priority_class; // 0xC8
uint8_t base_priority_copy; // 0xC9
uint8_t max_dynamic_priority; // 0xCA
uint8_t unk_CB; // 0xCB
X_KSPINLOCK timer_list_lock; // 0xCC
xe::be<uint32_t> stack_alloc_base; // 0xD0
@@ -422,6 +422,22 @@ class XThread : public XObject, public cpu::Thread {
int32_t QueryPriority();
void SetPriority(int32_t increment);
// Called periodically (~20ms) by KernelState's timestamp timer to simulate
// the Xenon scheduler's quantum-based priority decay for non-real-time
// threads (base_priority < 18). Threads that run for longer than one
// quantum (~20ms) have their effective priority decayed toward the base,
// which causes them to drop into lower host priority buckets and prevents
// starvation. On the first decay step the accumulated priority boost is
// also drained.
void CheckQuantumAndDecay();
// Called when a thread wakes from a kernel wait. Applies a priority
// boost of |increment| above base_priority (matching the Xenon kernel's
// unwait-boost behavior) and restarts the quantum timer. The boost is
// drained on the next quantum expiry via CheckQuantumAndDecay().
// If increment is 0 or the thread has boost disabled, the priority is
// simply restored to base_priority.
void BoostOnWake(int32_t increment);
// Xbox thread IDs:
// 0 - core 0, thread 0 - user
// 1 - core 0, thread 1 - user
@@ -491,7 +507,10 @@ class XThread : public XObject, public cpu::Thread {
bool main_thread_ = false; // Entry-point thread
bool running_ = false;
int32_t priority_ = 0;
int32_t priority_ = 0; // current effective priority (may be decayed)
int32_t base_priority_ = 0; // priority floor — decay never goes below this
int32_t boost_amount_ = 0; // accumulated priority boost above base
uint64_t quantum_start_ms_ = 0; // host uptime (ms) when quantum last reset
#if !XE_PLATFORM_WIN32
// Condition variable for thread self-suspension.

3
xenia-rs/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
/target/
*.iso
*.xiso

721
xenia-rs/Cargo.lock generated Normal file
View File

@@ -0,0 +1,721 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "aes"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
]
[[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 = "cc"
version = "1.2.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "cipher"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common",
"inout",
]
[[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 = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "inout"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
dependencies = [
"generic-array",
]
[[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 = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[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 = "typenum"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
[[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 = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[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 = [
"aes",
"anyhow",
"byteorder",
"cc",
"thiserror",
"tracing",
"xenia-memory",
"xenia-types",
]

43
xenia-rs/Cargo.toml Normal file
View File

@@ -0,0 +1,43 @@
[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"] }
aes = "0.8"

View File

@@ -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"] }

View File

@@ -0,0 +1,308 @@
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),
}
}
/// Load XEX data from a path. If the path is an ISO, extract default.xex from it.
fn load_xex_data(path: &str) -> Result<Vec<u8>> {
let lower = path.to_lowercase();
if lower.ends_with(".iso") || lower.ends_with(".xiso") {
use xenia_vfs::VfsDevice;
println!("Detected disc image, extracting default.xex...");
let disc = xenia_vfs::disc_image::DiscImageDevice::open("disc", std::path::Path::new(path))
.map_err(|e| anyhow::anyhow!("Failed to open disc image: {}", e))?;
disc.read_file("default.xex")
.map_err(|e| anyhow::anyhow!("Failed to extract default.xex from disc image: {}", e))
} else {
Ok(std::fs::read(path)?)
}
}
fn cmd_info(path: &str) -> Result<()> {
let data = load_xex_data(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());
}
if let Some(ref ffi) = header.file_format_info {
println!("\n=== File Format ===");
println!("Encryption: {}", match ffi.encryption_type {
0 => "None", 1 => "Normal (AES)", _ => "Unknown"
});
println!("Compression: {}", match ffi.compression_type {
0 => "None", 1 => "Basic", 2 => "Normal (LZX)", _ => "Unknown"
});
if !ffi.basic_blocks.is_empty() {
println!("Basic blocks: {}", ffi.basic_blocks.len());
}
if ffi.normal_window_size != 0 {
println!("LZX Window: {:#x}", ffi.normal_window_size);
}
}
if !header.import_libraries.is_empty() {
println!("\n=== Import Libraries ===");
for lib in &header.import_libraries {
println!(" {} (v{:#010x}, {} ordinals)", lib.name, lib.version_cur, lib.ordinals.len());
}
}
Ok(())
}
fn cmd_disasm(path: &str, count: usize) -> Result<()> {
let data = load_xex_data(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);
// Load and decompress the image
let image_data = xenia_xex::loader::load_image(&data, &header)?;
println!("Image loaded: {} bytes decompressed", image_data.len());
println!("Disassembly from entry point ({} instructions):\n", count);
let entry_offset = (entry - base) as usize;
if entry_offset + count * 4 <= image_data.len() {
let block = xenia_cpu::disasm::disassemble_block(&image_data[entry_offset..], entry, count);
for (addr, text) in block {
println!(" {:#010x}: {}", addr, text);
}
} else {
println!(" (entry point offset {:#x} is outside image bounds, image is {:#x} bytes)", entry_offset, image_data.len());
}
Ok(())
}
fn cmd_exec(path: &str, max_instructions: u64) -> Result<()> {
let data = load_xex_data(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"))?;
// Print compression info
if let Some(ref ffi) = header.file_format_info {
println!("Compression: {} (encryption: {})",
match ffi.compression_type {
0 => "none", 1 => "basic", 2 => "normal (LZX)", _ => "unknown"
},
match ffi.encryption_type {
0 => "none", 1 => "normal (AES)", _ => "unknown"
});
}
if !header.import_libraries.is_empty() {
println!("Import libraries:");
for lib in &header.import_libraries {
println!(" {} ({} ordinals)", lib.name, lib.ordinals.len());
}
}
println!("Loading XEX: entry={:#010x} base={:#010x}", entry, base);
// Allocate guest memory
let mut mem = xenia_memory::GuestMemory::new()
.map_err(|e| anyhow::anyhow!("Failed to allocate guest memory: {}", e))?;
// Load and decompress the XEX image
let image_data = xenia_xex::loader::load_image(&data, &header)?;
let alloc_size = ((image_data.len() + 4095) & !4095) as u32;
mem.alloc(
base,
alloc_size,
xenia_memory::page_table::MemoryProtect::READ | xenia_memory::page_table::MemoryProtect::WRITE,
).map_err(|e| anyhow::anyhow!("Failed to allocate guest memory region: {}", e))?;
mem.write_bulk(base, &image_data);
// 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;
}
// Check if PC is in mapped memory before trying to execute
if !mem.is_mapped(ctx.pc) {
println!("[{:>8}] FAULT: PC {:#010x} is in unmapped memory", instruction_count, ctx.pc);
break;
}
// Pre-step debugger
debugger.pre_step(&ctx, &mem);
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(())
}

View File

@@ -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 }

View File

@@ -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()
}
}

View File

@@ -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 }

View File

@@ -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()
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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;

View File

@@ -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)
}
}

View File

@@ -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 }

View File

@@ -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<String>,
}

View File

@@ -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<u32, Breakpoint>,
pub trace_log: Vec<TraceEntry>,
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()
}
}

View File

@@ -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,
}

View File

@@ -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 }

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

@@ -0,0 +1,28 @@
/// Xenos GPU register file. 0x6000 32-bit registers.
pub struct RegisterFile {
pub regs: Vec<u32>,
}
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()
}
}

View File

@@ -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 }

View File

@@ -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()
}
}

View File

@@ -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 }

View File

@@ -0,0 +1,343 @@
//! 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);
state.register_export(Xboxkrnl, 0xB9, "MmAllocatePhysicalMemory", mm_allocate_physical_memory);
state.register_export(Xboxkrnl, 0xBA, "MmAllocatePhysicalMemoryEx", mm_allocate_physical_memory_ex);
// Threading
state.register_export(Xboxkrnl, 0x0C, "ExCreateThread", ex_create_thread);
state.register_export(Xboxkrnl, 0x5F, "KeDelayExecutionThread", ke_delay_execution_thread);
state.register_export(Xboxkrnl, 0x97, "KeSetAffinityThread", ke_set_affinity_thread);
state.register_export(Xboxkrnl, 0x154, "KeTlsGetValue", ke_tls_get_value);
state.register_export(Xboxkrnl, 0x155, "KeTlsSetValue", ke_tls_set_value);
// 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);
// Spinlocks/IRQL
state.register_export(Xboxkrnl, 0xB1, "KfAcquireSpinLock", kf_acquire_spin_lock);
state.register_export(Xboxkrnl, 0xB4, "KfReleaseSpinLock", kf_release_spin_lock);
state.register_export(Xboxkrnl, 0x85, "KeRaiseIrqlToDpcLevel", ke_raise_irql_to_dpc_level);
state.register_export(Xboxkrnl, 0xB3, "KfLowerIrql", kf_lower_irql);
// Module
state.register_export(Xboxkrnl, 0x195, "XexGetModuleHandle", xex_get_module_handle);
state.register_export(Xboxkrnl, 0x197, "XexGetProcedureAddress", xex_get_procedure_address);
// Object
state.register_export(Xboxkrnl, 0x110, "ObReferenceObjectByHandle", ob_reference_object_by_handle);
// Process/System
state.register_export(Xboxkrnl, 0x66, "KeGetCurrentProcessType", ke_get_current_process_type);
state.register_export(Xboxkrnl, 0x83, "KeQueryPerformanceFrequency", ke_query_performance_frequency);
state.register_export(Xboxkrnl, 0x84, "KeQuerySystemTime", ke_query_system_time);
state.register_export(Xboxkrnl, 0x10, "ExGetXConfigSetting", ex_get_xconfig_setting);
// RTL
state.register_export(Xboxkrnl, 0x11A, "RtlInitAnsiString", rtl_init_ansi_string);
state.register_export(Xboxkrnl, 0x12D, "RtlInitUnicodeString", rtl_init_unicode_string);
state.register_export(Xboxkrnl, 0x127, "RtlFreeAnsiString", rtl_free_ansi_string);
state.register_export(Xboxkrnl, 0x13B, "sprintf", stub_sprintf);
// I/O
state.register_export(Xboxkrnl, 0xD2, "NtCreateFile", nt_create_file);
state.register_export(Xboxkrnl, 0xF0, "NtReadFile", nt_read_file);
state.register_export(Xboxkrnl, 0xE8, "NtQueryInformationFile", nt_query_information_file);
state.register_export(Xboxkrnl, 0xE7, "NtQueryFullAttributesFile", nt_query_full_attributes_file);
// Video
state.register_export(Xboxkrnl, 0x142, "VdGetCurrentDisplayGamma", vd_get_current_display_gamma);
state.register_export(Xboxkrnl, 0x14B, "VdQueryVideoMode", vd_query_video_mode);
state.register_export(Xboxkrnl, 0x1C2, "VdInitializeEngines", vd_initialize_engines);
// Debug
state.register_export(Xboxkrnl, 0x166, "DbgPrint", dbg_print);
}
// ===== Memory =====
fn nt_allocate_virtual_memory(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
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 mm_allocate_physical_memory(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
// r3 = region, r4 = size, r5 = protect
// Return a fake address in physical memory range
ctx.gpr[3] = 0xA000_0000; // Fake physical allocation
}
fn mm_allocate_physical_memory_ex(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
// r3 = size, r4 = protect, r5 = min_addr, r6 = max_addr, r7 = alignment
ctx.gpr[3] = 0xA000_0000; // Fake physical allocation
}
// ===== Threading =====
fn ex_create_thread(ctx: &mut PpcContext, _mem: &mut GuestMemory, state: &mut KernelState) {
let handle = state.alloc_handle();
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) {
ctx.gpr[3] = 0;
}
fn ke_set_affinity_thread(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
// r3 = thread handle, r4 = affinity mask
ctx.gpr[3] = 0; // Return previous affinity
}
fn ke_tls_get_value(ctx: &mut PpcContext, _mem: &mut GuestMemory, state: &mut KernelState) {
let index = ctx.gpr[3] as u32;
ctx.gpr[3] = state.tls_get(index);
}
fn ke_tls_set_value(ctx: &mut PpcContext, _mem: &mut GuestMemory, state: &mut KernelState) {
let index = ctx.gpr[3] as u32;
let value = ctx.gpr[4];
state.tls_set(index, value);
ctx.gpr[3] = 1; // TRUE = success
}
// ===== Sync =====
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) {
ctx.gpr[3] = 0; // STATUS_SUCCESS (immediately signaled)
}
fn nt_close(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
ctx.gpr[3] = 0;
}
// ===== Spinlocks/IRQL =====
fn kf_acquire_spin_lock(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
// Return old IRQL (simulate DISPATCH_LEVEL = 2)
ctx.gpr[3] = 0; // Previous IRQL (PASSIVE_LEVEL)
}
fn kf_release_spin_lock(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
// r3 = spin lock, r4 = old IRQL
ctx.gpr[3] = 0;
}
fn ke_raise_irql_to_dpc_level(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
ctx.gpr[3] = 0; // Return old IRQL
}
fn kf_lower_irql(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
ctx.gpr[3] = 0;
}
// ===== Module =====
fn xex_get_module_handle(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
ctx.gpr[3] = 0; // Return NULL
}
fn xex_get_procedure_address(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
// r3 = module_handle, r4 = ordinal, r5 = address_ptr
let ordinal = ctx.gpr[4] as u32;
tracing::warn!("XexGetProcedureAddress: ordinal {:#x} not found", ordinal);
ctx.gpr[3] = 0xC000_0034; // STATUS_OBJECT_NAME_NOT_FOUND
}
// ===== Object =====
fn ob_reference_object_by_handle(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
// r3 = handle, r4 = object_type, r5 = out_object_ptr
ctx.gpr[3] = 0; // STATUS_SUCCESS
}
// ===== Process/System =====
fn ke_get_current_process_type(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
ctx.gpr[3] = 1; // PROC_USER (user mode process)
}
fn ke_query_performance_frequency(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
ctx.gpr[3] = 50_000_000; // 50 MHz (Xbox 360 timebase frequency)
}
fn ke_query_system_time(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) {
use xenia_memory::MemoryAccess;
let time_ptr = ctx.gpr[3] as u32;
if time_ptr != 0 {
// Write a fake system time (Windows FILETIME format, 100ns intervals since 1601)
// Use a fixed value so execution is deterministic
let fake_time: u64 = 132_500_000_000_000_000; // ~2021
mem.write_u32(time_ptr, (fake_time >> 32) as u32);
mem.write_u32(time_ptr + 4, fake_time as u32);
}
}
fn ex_get_xconfig_setting(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
// r3 = category, r4 = setting, r5 = buffer, r6 = buffer_size_ptr
ctx.gpr[3] = 0; // STATUS_SUCCESS (but writes nothing)
}
// ===== RTL =====
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 {
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 rtl_init_unicode_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 {
// Count wide chars (2 bytes each, null-terminated)
let mut len: u16 = 0;
let mut addr = src_ptr;
while mem.read_u16(addr) != 0 {
len += 2;
addr += 2;
}
// UNICODE_STRING: {Length, MaxLength, Buffer}
mem.write_u16(dest_ptr, len);
mem.write_u16(dest_ptr + 2, len + 2);
mem.write_u32(dest_ptr + 4, src_ptr);
} else {
mem.write_u16(dest_ptr, 0);
mem.write_u16(dest_ptr + 2, 0);
mem.write_u32(dest_ptr + 4, 0);
}
}
fn rtl_free_ansi_string(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
// Stub: no-op (we don't track allocations yet)
ctx.gpr[3] = 0;
}
fn stub_sprintf(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) {
use xenia_memory::MemoryAccess;
// r3 = dest buffer, r4 = format string
// Stub: just copy the format string as-is
let dest = ctx.gpr[3] as u32;
let fmt = ctx.gpr[4] as u32;
if fmt != 0 && dest != 0 {
let mut addr = fmt;
let mut daddr = dest;
loop {
let c = mem.read_u8(addr);
mem.write_u8(daddr, c);
if c == 0 { break; }
addr += 1;
daddr += 1;
}
}
ctx.gpr[3] = 0; // Return length (stub)
}
// ===== I/O =====
fn nt_create_file(ctx: &mut PpcContext, _mem: &mut GuestMemory, state: &mut KernelState) {
let handle = state.alloc_handle();
tracing::info!("NtCreateFile: allocated handle {:#x}", handle);
ctx.gpr[3] = 0; // STATUS_SUCCESS
}
fn nt_read_file(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
// Stub: return end of file
ctx.gpr[3] = 0xC000_0011; // STATUS_END_OF_FILE
}
fn nt_query_information_file(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
ctx.gpr[3] = 0; // STATUS_SUCCESS
}
fn nt_query_full_attributes_file(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
ctx.gpr[3] = 0xC000_0034; // STATUS_OBJECT_NAME_NOT_FOUND
}
// ===== Video =====
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 {
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 vd_initialize_engines(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
tracing::info!("VdInitializeEngines called");
ctx.gpr[3] = 0;
}
// ===== Debug =====
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;
}

View File

@@ -0,0 +1,4 @@
pub mod exports;
pub mod state;
pub use state::KernelState;

View File

@@ -0,0 +1,90 @@
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,
tls_slots: HashMap<u32, u64>,
}
impl KernelState {
pub fn new() -> Self {
let mut state = Self {
exports: HashMap::new(),
next_handle: 0x1000,
tls_slots: HashMap::new(),
};
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
}
pub fn tls_get(&self, index: u32) -> u64 {
self.tls_slots.get(&index).copied().unwrap_or(0)
}
pub fn tls_set(&mut self, index: u32, value: u64) {
self.tls_slots.insert(index, value);
}
}
impl Default for KernelState {
fn default() -> Self {
Self::new()
}
}

View File

@@ -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"] }

View File

@@ -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>;
}

View File

@@ -0,0 +1,265 @@
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<PageEntry>,
/// Registered MMIO regions (sorted by base address for binary search).
mmio_regions: Vec<MmioRegion>,
/// 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<Self, MemoryError> {
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<u32, MemoryError> {
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());
}
}
/// Check if a guest address has been allocated/committed.
pub fn is_mapped(&self, addr: u32) -> bool {
let page = (addr / PAGE_SIZE) as usize;
if page >= self.page_table.len() {
return false;
}
self.page_table[page].state().contains(AllocationState::COMMIT)
}
/// 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 {
if !self.is_mapped(addr) { return 0; }
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 if !self.is_mapped(addr) {
0
} 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 if !self.is_mapped(addr) {
0
} 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 if !self.is_mapped(addr) {
0
} 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) {
if !self.is_mapped(addr) { return; }
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 if !self.is_mapped(addr) {
return;
} 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 if !self.is_mapped(addr) {
return;
} 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 if !self.is_mapped(addr) {
return;
} 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() || !self.is_mapped(addr) {
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);
}
}
}
}

View File

@@ -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),
}

View File

@@ -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<dyn Fn(u32) -> u32 + Send + Sync>,
pub write_callback: Box<dyn Fn(u32, u32) + Send + Sync>,
}
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()
}
}

View File

@@ -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());
}
}

View File

@@ -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,
);
}

View File

@@ -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 }

View File

@@ -0,0 +1,122 @@
use serde::{Deserialize, Serialize};
use std::fmt;
use std::marker::PhantomData;
/// Big-endian value wrapper matching `xe::be<T>` 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: BeSwap>(T::Bytes, PhantomData<T>);
impl<T: BeSwap> Be<T> {
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<T: BeSwap + Default> Default for Be<T> {
fn default() -> Self {
Self::new(T::default())
}
}
impl<T: BeSwap + fmt::Debug> fmt::Debug for Be<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.get().fmt(f)
}
}
impl<T: BeSwap + fmt::Display> fmt::Display for Be<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.get().fmt(f)
}
}
impl<T: BeSwap + PartialEq> PartialEq for Be<T> {
fn eq(&self, other: &Self) -> bool {
// Compare raw bytes for efficiency (same byte order)
self.0.as_ref() == other.0.as_ref()
}
}
impl<T: BeSwap + Eq> Eq for Be<T> {}
/// 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::<u32>::new(0x12345678);
assert_eq!(v.get(), 0x12345678);
assert_eq!(v.raw_bytes(), &[0x12, 0x34, 0x56, 0x78]);
}
#[test]
fn test_be_u16() {
let v = Be::<u16>::new(0xABCD);
assert_eq!(v.get(), 0xABCD);
assert_eq!(v.raw_bytes(), &[0xAB, 0xCD]);
}
#[test]
fn test_be_mutate() {
let mut v = Be::<u32>::new(1);
assert_eq!(v.get(), 1);
v.set(42);
assert_eq!(v.get(), 42);
}
#[test]
fn test_be_f32() {
let v = Be::<f32>::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]);
}
}

View File

@@ -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<T> = Result<T, XeniaError>;

View File

@@ -0,0 +1,6 @@
pub mod endian;
pub mod error;
pub mod vec128;
pub use endian::Be;
pub use vec128::Vec128;

View File

@@ -0,0 +1,206 @@
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());
}
/// Get all 4 u32 elements as an array.
pub fn as_u32x4(&self) -> [u32; 4] {
[self.u32x4(0), self.u32x4(1), self.u32x4(2), self.u32x4(3)]
}
/// Get all 4 f32 elements as an array.
pub fn as_f32x4(&self) -> [f32; 4] {
[self.f32x4(0), self.f32x4(1), self.f32x4(2), self.f32x4(3)]
}
/// Get all 8 u16 elements as an array.
pub fn as_u16x8(&self) -> [u16; 8] {
[
self.u16x8(0), self.u16x8(1), self.u16x8(2), self.u16x8(3),
self.u16x8(4), self.u16x8(5), self.u16x8(6), self.u16x8(7),
]
}
/// Get all 16 bytes as an array.
pub fn as_bytes(&self) -> [u8; 16] {
self.bytes
}
/// Create from a byte array.
pub fn from_bytes(bytes: [u8; 16]) -> Self {
Self { bytes }
}
/// Create from a u32 array (big-endian elements).
pub fn from_u32x4_array(arr: [u32; 4]) -> Self {
Self::from_u32x4(arr[0], arr[1], arr[2], arr[3])
}
/// Create from an f32 array (big-endian elements).
pub fn from_f32x4_array(arr: [f32; 4]) -> Self {
Self::from_f32x4(arr[0], arr[1], arr[2], arr[3])
}
/// Create from a u16 array (big-endian elements).
pub fn from_u16x8_array(arr: [u16; 8]) -> Self {
let mut v = Self::ZERO;
for i in 0..8 { v.set_u16x8(i, arr[i]); }
v
}
}
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);
}
}
}

View File

@@ -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 }

View File

@@ -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<String>, root: impl AsRef<Path>) -> 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<Vec<VfsEntry>, 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<Vec<u8>, VfsError> {
let full_path = self.root.join(path);
std::fs::read(&full_path).map_err(VfsError::from)
}
fn stat(&self, path: &str) -> Result<VfsEntry, VfsError> {
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,
})
}
}

View File

@@ -0,0 +1,185 @@
use crate::{VfsDevice, VfsEntry, VfsError};
use std::io::{Read, Seek, SeekFrom};
/// XISO disc image device. Parses Xbox 360 disc images (GDFX/XISO format).
pub struct DiscImageDevice {
name: String,
path: std::path::PathBuf,
game_offset: u64,
/// Cached root directory buffer (typically small, a few KB).
root_buffer: Vec<u8>,
}
/// XISO sector size
pub const SECTOR_SIZE: u64 = 0x800;
/// GDFX magic string
const GDFX_MAGIC: &[u8; 20] = b"MICROSOFT*XBOX*MEDIA";
/// File attribute: directory
const FILE_ATTRIBUTE_DIRECTORY: u8 = 0x10;
/// Known game partition offsets to try
const LIKELY_OFFSETS: &[u64] = &[
0x0000_0000,
0x0000_FB20,
0x0002_0600,
0x0208_0000,
0x0FD9_0000,
];
impl DiscImageDevice {
pub fn open(name: impl Into<String>, path: &std::path::Path) -> Result<Self, VfsError> {
let mut file = std::fs::File::open(path)?;
// Find the game partition by locating the GDFX magic at sector 32
let mut game_offset = 0u64;
let mut magic_found = false;
let mut magic_buf = [0u8; 20];
for &offset in LIKELY_OFFSETS {
let magic_pos = offset + 32 * SECTOR_SIZE;
if file.seek(SeekFrom::Start(magic_pos)).is_ok()
&& file.read_exact(&mut magic_buf).is_ok()
&& magic_buf == *GDFX_MAGIC
{
game_offset = offset;
magic_found = true;
break;
}
}
if !magic_found {
return Err(VfsError::InvalidFormat(
"GDFX magic not found - not a valid XISO disc image".into(),
));
}
// Read root directory info from sector 32 header
let fs_ptr = game_offset + 32 * SECTOR_SIZE;
file.seek(SeekFrom::Start(fs_ptr + 20))?;
let mut buf4 = [0u8; 4];
file.read_exact(&mut buf4)?;
let root_sector = u32::from_le_bytes(buf4) as u64;
file.read_exact(&mut buf4)?;
let root_size = u32::from_le_bytes(buf4) as u64;
let root_byte_offset = game_offset + root_sector * SECTOR_SIZE;
// Read the root directory buffer into memory (typically small)
file.seek(SeekFrom::Start(root_byte_offset))?;
let mut root_buffer = vec![0u8; root_size as usize];
file.read_exact(&mut root_buffer)?;
Ok(Self {
name: name.into(),
path: path.to_path_buf(),
game_offset,
root_buffer,
})
}
/// Read all directory entries from the root directory tree.
fn read_entries(&self) -> Vec<VfsEntry> {
let mut entries = Vec::new();
self.read_entry(&self.root_buffer, 0, &mut entries);
entries
}
/// Recursively read a directory entry from the binary tree structure.
fn read_entry(&self, buffer: &[u8], ordinal: u16, entries: &mut Vec<VfsEntry>) {
let p = ordinal as usize * 4;
if p + 14 > buffer.len() {
return;
}
let node_l = u16::from_le_bytes([buffer[p], buffer[p + 1]]);
let node_r = u16::from_le_bytes([buffer[p + 2], buffer[p + 3]]);
let sector = u32::from_le_bytes([buffer[p + 4], buffer[p + 5], buffer[p + 6], buffer[p + 7]]) as u64;
let length = u32::from_le_bytes([buffer[p + 8], buffer[p + 9], buffer[p + 10], buffer[p + 11]]) as u64;
let attributes = buffer[p + 12];
let name_length = buffer[p + 13] as usize;
if p + 14 + name_length > buffer.len() {
return;
}
// Traverse left subtree first (smaller names)
if node_l != 0 && node_l != 0xFFFF {
self.read_entry(buffer, node_l, entries);
}
// Read this entry's name
let name = String::from_utf8_lossy(&buffer[p + 14..p + 14 + name_length]).to_string();
let is_directory = (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
let file_offset = self.game_offset + sector * SECTOR_SIZE;
entries.push(VfsEntry {
name,
is_directory,
size: length,
offset: file_offset,
});
// Traverse right subtree (larger names)
if node_r != 0 && node_r != 0xFFFF {
self.read_entry(buffer, node_r, entries);
}
}
}
impl VfsDevice for DiscImageDevice {
fn name(&self) -> &str {
&self.name
}
fn list_root(&self) -> Result<Vec<VfsEntry>, VfsError> {
Ok(self.read_entries())
}
fn read_file(&self, path: &str) -> Result<Vec<u8>, VfsError> {
let entries = self.read_entries();
let entry = entries.iter()
.find(|e| e.name.eq_ignore_ascii_case(path) && !e.is_directory)
.ok_or_else(|| VfsError::NotFound(path.to_string()))?;
let offset = entry.offset;
let size = entry.size as usize;
// Read from file using seek
let mut file = std::fs::File::open(&self.path)?;
let file_len = file.seek(SeekFrom::End(0))?;
if offset + size as u64 > file_len {
return Err(VfsError::NotFound(format!(
"File data extends past end of image: {} (offset={:#x}, size={:#x}, image_len={:#x})",
path, offset, size, file_len
)));
}
file.seek(SeekFrom::Start(offset))?;
let mut buf = vec![0u8; size];
let bytes_read = file.read(&mut buf)?;
if bytes_read < size {
// Try reading the rest
let mut total = bytes_read;
while total < size {
let n = file.read(&mut buf[total..])?;
if n == 0 {
return Err(VfsError::NotFound(format!(
"Short read: got {} of {} bytes for {}",
total, size, path
)));
}
total += n;
}
}
Ok(buf)
}
fn stat(&self, path: &str) -> Result<VfsEntry, VfsError> {
let entries = self.read_entries();
entries.into_iter()
.find(|e| e.name.eq_ignore_ascii_case(path))
.ok_or_else(|| VfsError::NotFound(path.to_string()))
}
}

View File

@@ -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<Vec<VfsEntry>, VfsError>;
fn read_file(&self, path: &str) -> Result<Vec<u8>, VfsError>;
fn stat(&self, path: &str) -> Result<VfsEntry, VfsError>;
}

View File

@@ -0,0 +1,17 @@
[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 }
aes = { workspace = true }
[build-dependencies]
cc = "1"

View File

@@ -0,0 +1,18 @@
fn main() {
let mspack_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("..")
.join("..")
.join("third_party")
.join("mspack");
cc::Build::new()
.file("lzx_wrapper.c")
.file(mspack_dir.join("lzxd.c"))
.file(mspack_dir.join("system.c"))
.include(&mspack_dir)
.define("HAVE_CONFIG_H", None)
.define("SIZEOF_OFF_T", "8")
.warnings(false)
.compile("mspack_lzx");
}

View File

@@ -0,0 +1,143 @@
/*
* Thin C wrapper around mspack's LZX decompressor for use from Rust FFI.
* This provides a simple buffer-to-buffer decompression function.
*/
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <stdio.h>
/* Stub for xenia_log (referenced by lzxd.c debug macros) */
void xenia_log(const char *fmt, ...) {
(void)fmt;
}
/* Pull in mspack headers from xenia's third_party */
#define HAVE_CONFIG_H
#include "config.h"
#include "mspack.h"
#include "system.h"
#include "lzx.h"
/* Memory-backed file for mspack I/O */
typedef struct {
struct mspack_system sys;
void *buffer;
off_t buffer_size;
off_t offset;
} mspack_memory_file;
static struct mspack_file *mem_open(struct mspack_system *self, const char *fn, int mode) {
(void)self; (void)fn; (void)mode;
return NULL;
}
static void mem_close(struct mspack_file *file) { (void)file; }
static int mem_read(struct mspack_file *file, void *buffer, int chars) {
mspack_memory_file *memfile = (mspack_memory_file *)file;
off_t remaining = memfile->buffer_size - memfile->offset;
off_t total = (off_t)chars < remaining ? (off_t)chars : remaining;
memcpy(buffer, (uint8_t *)memfile->buffer + memfile->offset, total);
memfile->offset += total;
return (int)total;
}
static int mem_write(struct mspack_file *file, void *buffer, int chars) {
mspack_memory_file *memfile = (mspack_memory_file *)file;
off_t remaining = memfile->buffer_size - memfile->offset;
off_t total = (off_t)chars < remaining ? (off_t)chars : remaining;
memcpy((uint8_t *)memfile->buffer + memfile->offset, buffer, total);
memfile->offset += total;
return (int)total;
}
static int mem_seek(struct mspack_file *file, off_t offset, int mode) {
(void)file; (void)offset; (void)mode;
return -1;
}
static off_t mem_tell(struct mspack_file *file) {
(void)file;
return 0;
}
static void mem_msg(struct mspack_file *file, const char *format, ...) {
(void)file; (void)format;
}
static void *mem_alloc(struct mspack_system *self, size_t bytes) {
(void)self;
return calloc(bytes, 1);
}
static void mem_free(void *ptr) { free(ptr); }
static void mem_copy(void *src, void *dest, size_t bytes) {
memcpy(dest, src, bytes);
}
/*
* Decompress LZX data from a memory buffer.
* Returns 0 on success, non-zero on error.
*/
int xenia_lzx_decompress(
const void *lzx_data, uint32_t lzx_len,
void *dest, uint32_t dest_len,
uint32_t window_size)
{
/* Calculate window_bits from window_size (find the bit position) */
uint32_t window_bits = 0;
uint32_t tmp = window_size;
while (tmp > 1) {
tmp >>= 1;
window_bits++;
}
if ((1u << window_bits) != window_size || window_bits < 15 || window_bits > 21) {
return 1;
}
/* Set up mspack memory system */
struct mspack_system sys;
memset(&sys, 0, sizeof(sys));
sys.open = mem_open;
sys.close = mem_close;
sys.read = mem_read;
sys.write = mem_write;
sys.seek = mem_seek;
sys.tell = mem_tell;
sys.message = mem_msg;
sys.alloc = mem_alloc;
sys.free = mem_free;
sys.copy = mem_copy;
mspack_memory_file src_file;
memset(&src_file, 0, sizeof(src_file));
src_file.buffer = (void *)lzx_data;
src_file.buffer_size = (off_t)lzx_len;
src_file.offset = 0;
mspack_memory_file dst_file;
memset(&dst_file, 0, sizeof(dst_file));
dst_file.buffer = dest;
dst_file.buffer_size = (off_t)dest_len;
dst_file.offset = 0;
struct lzxd_stream *lzxd = lzxd_init(
&sys,
(struct mspack_file *)&src_file,
(struct mspack_file *)&dst_file,
(int)window_bits,
0, /* reset_interval: 0 = never reset */
0x8000, /* input_buffer_size */
(off_t)dest_len,
0 /* is_delta */
);
if (!lzxd) {
return 2;
}
int result = lzxd_decompress(lzxd, (off_t)dest_len);
lzxd_free(lzxd);
return result;
}

View File

@@ -0,0 +1,102 @@
/// 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<Xex2OptionalHeader>,
pub security_info: Option<Xex2SecurityInfo>,
/// Parsed file format info (if present).
pub file_format_info: Option<FileFormatInfo>,
/// Parsed import libraries.
pub import_libraries: Vec<ImportLibrary>,
}
#[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,
/// Encrypted session key (decrypted with retail/devkit key to get actual session key).
pub aes_key: [u8; 16],
pub page_descriptors: Vec<Xex2PageDescriptor>,
}
#[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
}
}
/// File format info (compression and encryption types).
#[derive(Debug, Clone)]
pub struct FileFormatInfo {
pub info_size: u32,
pub encryption_type: u16,
pub compression_type: u16,
/// For basic compression: list of (data_size, zero_size) block pairs.
pub basic_blocks: Vec<BasicCompressionBlock>,
/// For normal (LZX) compression: window size.
pub normal_window_size: u32,
/// For normal (LZX) compression: first block size (from header).
pub normal_first_block_size: u32,
/// For normal (LZX) compression: first block hash (from header).
pub normal_first_block_hash: [u8; 20],
}
#[derive(Debug, Clone, Copy)]
pub struct BasicCompressionBlock {
pub data_size: u32,
pub zero_size: u32,
}
/// An imported library with its ordinals.
#[derive(Debug, Clone)]
pub struct ImportLibrary {
pub name: String,
pub version_min: u32,
pub version_cur: u32,
pub ordinals: Vec<u32>,
}
/// XEX2 magic: "XEX2"
pub const XEX2_MAGIC: u32 = 0x58455832;
/// Compression types
pub const COMPRESSION_NONE: u16 = 0;
pub const COMPRESSION_BASIC: u16 = 1;
pub const COMPRESSION_NORMAL: u16 = 2;
/// Encryption types
pub const ENCRYPTION_NONE: u16 = 0;
pub const ENCRYPTION_NORMAL: u16 = 1;
/// 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 = 0x00020104;
pub const ORIGINAL_PE_NAME: u32 = 0x000183FF;
pub const FILE_FORMAT_INFO: u32 = 0x000003FF;
}

View File

@@ -0,0 +1,4 @@
pub mod header;
pub mod loader;
pub use header::Xex2Header;

View File

@@ -0,0 +1,521 @@
use crate::header::*;
use aes::cipher::{BlockDecrypt, KeyInit};
use aes::Aes128;
use byteorder::{BigEndian, ReadBytesExt};
use std::io::{self, Cursor, Read, Seek, SeekFrom};
unsafe extern "C" {
fn xenia_lzx_decompress(
lzx_data: *const std::ffi::c_void,
lzx_len: u32,
dest: *mut std::ffi::c_void,
dest_len: u32,
window_size: u32,
) -> i32;
}
/// Parse a XEX2 header from raw file data.
pub fn parse_xex2_header(data: &[u8]) -> io::Result<Xex2Header> {
let mut cursor = Cursor::new(data);
let magic = cursor.read_u32::<BigEndian>()?;
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::<BigEndian>()?;
let header_size = cursor.read_u32::<BigEndian>()?;
let _reserved = cursor.read_u32::<BigEndian>()?;
let security_offset = cursor.read_u32::<BigEndian>()?;
let header_count = cursor.read_u32::<BigEndian>()?;
let mut optional_headers = Vec::new();
for _ in 0..header_count {
let key = cursor.read_u32::<BigEndian>()?;
let value = cursor.read_u32::<BigEndian>()?;
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
};
// Parse file format info
let file_format_info = parse_file_format_info(data, &optional_headers);
// Parse import libraries
let import_libraries = parse_import_libraries(data, &optional_headers);
Ok(Xex2Header {
magic,
module_flags,
header_size,
security_offset,
header_count,
optional_headers,
security_info,
file_format_info,
import_libraries,
})
}
fn parse_security_info(cursor: &mut Cursor<&[u8]>) -> io::Result<Xex2SecurityInfo> {
// xex2_security_info layout (from xex2_info.h):
// 0x000: header_size (u32)
// 0x004: image_size (u32)
// 0x008: rsa_signature (0x100 bytes)
// 0x108: unk_108 (u32)
// 0x10C: image_flags (u32)
// 0x110: load_address (u32)
// 0x114: section_digest (0x14 bytes)
// 0x128: import_table_count (u32)
// 0x12C: import_table_digest (0x14 bytes)
// 0x140: xgd2_media_id (0x10 bytes)
// 0x150: aes_key (0x10 bytes)
// 0x160: export_table (u32)
// 0x164: header_digest (0x14 bytes)
// 0x178: region (u32)
// 0x17C: allowed_media_types (u32)
// 0x180: page_descriptor_count (u32)
// 0x184: page_descriptors[] (each is 0x18 bytes: u32 value + 0x14 digest)
let _header_size = cursor.read_u32::<BigEndian>()?; // 0x000
let image_size = cursor.read_u32::<BigEndian>()?; // 0x004
// Skip RSA signature (0x100 bytes)
let mut rsa_sig = [0u8; 0x100];
cursor.read_exact(&mut rsa_sig)?; // 0x008
let _unk_108 = cursor.read_u32::<BigEndian>()?; // 0x108
let image_flags = cursor.read_u32::<BigEndian>()?; // 0x10C
let load_address = cursor.read_u32::<BigEndian>()?; // 0x110
// Skip section_digest (0x14 bytes)
let mut digest = [0u8; 0x14];
cursor.read_exact(&mut digest)?; // 0x114
let _import_table_count = cursor.read_u32::<BigEndian>()?; // 0x128
// Skip import_table_digest (0x14 bytes)
cursor.read_exact(&mut digest)?; // 0x12C
// Skip xgd2_media_id (0x10 bytes)
let mut media_id = [0u8; 0x10];
cursor.read_exact(&mut media_id)?; // 0x140
// Read aes_key (0x10 bytes)
let mut aes_key = [0u8; 0x10];
cursor.read_exact(&mut aes_key)?; // 0x150
let export_table_address = cursor.read_u32::<BigEndian>()?; // 0x160
// Skip header_digest (0x14 bytes)
cursor.read_exact(&mut digest)?; // 0x164
let _region = cursor.read_u32::<BigEndian>()?; // 0x178
let _allowed_media = cursor.read_u32::<BigEndian>()?; // 0x17C
let page_descriptor_count = cursor.read_u32::<BigEndian>()?; // 0x180
let mut page_descriptors = Vec::new();
for _ in 0..page_descriptor_count {
let size_and_info = cursor.read_u32::<BigEndian>()?;
// Skip data_digest (0x14 bytes per descriptor)
cursor.read_exact(&mut digest)?;
page_descriptors.push(Xex2PageDescriptor { size_and_info });
}
Ok(Xex2SecurityInfo {
image_size,
load_address,
export_table_address,
image_flags,
aes_key,
page_descriptors,
})
}
/// Parse file format info from the optional header data.
fn parse_file_format_info(data: &[u8], headers: &[Xex2OptionalHeader]) -> Option<FileFormatInfo> {
// The key format: low 8 bits indicate the data size category
// 0xFF = data offset is a pointer to variable-size data in the header area
let header = headers.iter().find(|h| h.key == header_keys::FILE_FORMAT_INFO)?;
let offset = header.value as usize;
if offset + 8 > data.len() {
return None;
}
let mut cursor = Cursor::new(data);
cursor.seek(SeekFrom::Start(offset as u64)).ok()?;
let info_size = cursor.read_u32::<BigEndian>().ok()?;
let encryption_type = cursor.read_u16::<BigEndian>().ok()?;
let compression_type = cursor.read_u16::<BigEndian>().ok()?;
let mut basic_blocks = Vec::new();
let mut normal_window_size = 0u32;
let mut normal_first_block_size = 0u32;
let mut normal_first_block_hash = [0u8; 20];
match compression_type {
COMPRESSION_BASIC => {
// Basic compression blocks: (data_size, zero_size) pairs
// Number of blocks = (info_size - 8) / 8
let block_count = if info_size > 8 { (info_size - 8) / 8 } else { 0 };
for _ in 0..block_count {
let data_size = cursor.read_u32::<BigEndian>().ok()?;
let zero_size = cursor.read_u32::<BigEndian>().ok()?;
basic_blocks.push(BasicCompressionBlock { data_size, zero_size });
}
}
COMPRESSION_NORMAL => {
normal_window_size = cursor.read_u32::<BigEndian>().ok()?;
// Read first_block: block_size (4) + block_hash (20)
normal_first_block_size = cursor.read_u32::<BigEndian>().ok()?;
cursor.read_exact(&mut normal_first_block_hash).ok()?;
}
_ => {}
}
Some(FileFormatInfo {
info_size,
encryption_type,
compression_type,
basic_blocks,
normal_window_size,
normal_first_block_size,
normal_first_block_hash,
})
}
/// Parse import libraries from the optional header data.
fn parse_import_libraries(data: &[u8], headers: &[Xex2OptionalHeader]) -> Vec<ImportLibrary> {
let header = match headers.iter().find(|h| h.key == header_keys::IMPORT_LIBRARIES) {
Some(h) => h,
None => return Vec::new(),
};
let offset = header.value as usize;
if offset + 4 > data.len() {
return Vec::new();
}
let mut cursor = Cursor::new(data);
if cursor.seek(SeekFrom::Start(offset as u64)).is_err() {
return Vec::new();
}
let mut libraries = Vec::new();
// Import libraries header: total_size (4), string_table_size (4), string_count (4)
let _total_size = match cursor.read_u32::<BigEndian>() { Ok(v) => v, Err(_) => return libraries };
let string_table_size = match cursor.read_u32::<BigEndian>() { Ok(v) => v, Err(_) => return libraries };
let string_count = match cursor.read_u32::<BigEndian>() { Ok(v) => v, Err(_) => return libraries };
// Read string table
let string_table_start = cursor.position() as usize;
let mut names = Vec::new();
for _ in 0..string_count {
let mut name = String::new();
loop {
let b = match cursor.read_u8() { Ok(v) => v, Err(_) => break };
if b == 0 { break; }
name.push(b as char);
}
names.push(name);
}
// Align to end of string table
let string_table_end = string_table_start + string_table_size as usize;
if string_table_end > data.len() {
return libraries;
}
let _ = cursor.seek(SeekFrom::Start(string_table_end as u64));
// Read library records
// Each record: size(4), next_import_digest(20 bytes), id(4), version(4), version_min(4),
// name_index(2), record_count(2), ordinals(record_count * 4)
for _ in 0..names.len() {
let lib_size = match cursor.read_u32::<BigEndian>() { Ok(v) => v, Err(_) => break };
if lib_size < 40 { break; }
// Skip digest (20 bytes)
let mut digest = [0u8; 20];
if cursor.read_exact(&mut digest).is_err() { break; }
let _id = cursor.read_u32::<BigEndian>().unwrap_or(0);
let version_cur = cursor.read_u32::<BigEndian>().unwrap_or(0);
let version_min = cursor.read_u32::<BigEndian>().unwrap_or(0);
let name_index = cursor.read_u16::<BigEndian>().unwrap_or(0);
let record_count = cursor.read_u16::<BigEndian>().unwrap_or(0);
let name = names.get(name_index as usize).cloned().unwrap_or_default();
let mut ordinals = Vec::new();
for _ in 0..record_count {
let ordinal = cursor.read_u32::<BigEndian>().unwrap_or(0);
ordinals.push(ordinal);
}
libraries.push(ImportLibrary {
name,
version_min,
version_cur,
ordinals,
});
}
libraries
}
/// Get an optional header value by key.
pub fn get_opt_header(header: &Xex2Header, key: u32) -> Option<u32> {
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<u32> {
get_opt_header(header, header_keys::ENTRY_POINT)
}
/// Get the image base address.
pub fn get_image_base(header: &Xex2Header) -> Option<u32> {
get_opt_header(header, header_keys::IMAGE_BASE_ADDRESS)
}
/// Get the default stack size.
pub fn get_stack_size(header: &Xex2Header) -> u32 {
get_opt_header(header, header_keys::DEFAULT_STACK_SIZE).unwrap_or(0x10_0000) // Default 1MB
}
/// Load the XEX image data into a flat buffer (decompressing if needed).
/// Returns the decompressed image bytes ready to map into guest memory.
pub fn load_image(data: &[u8], header: &Xex2Header) -> io::Result<Vec<u8>> {
let source = &data[header.header_size as usize..];
match &header.file_format_info {
Some(info) if info.compression_type == COMPRESSION_BASIC => {
load_basic_compressed(source, info)
}
Some(info) if info.compression_type == COMPRESSION_NORMAL => {
load_normal_compressed(source, info, header)
}
_ => {
// Uncompressed (or no format info = treat as uncompressed)
Ok(source.to_vec())
}
}
}
/// Load basic compressed image data.
fn load_basic_compressed(source: &[u8], info: &FileFormatInfo) -> io::Result<Vec<u8>> {
// Calculate total uncompressed size
let total_size: u64 = info.basic_blocks.iter()
.map(|b| b.data_size as u64 + b.zero_size as u64)
.sum();
let mut output = vec![0u8; total_size as usize];
let mut src_offset = 0usize;
let mut dst_offset = 0usize;
for block in &info.basic_blocks {
let data_size = block.data_size as usize;
let zero_size = block.zero_size as usize;
if src_offset + data_size > source.len() {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
format!("Basic compression block data extends past end of file (src_offset={:#x}, data_size={:#x}, source_len={:#x})",
src_offset, data_size, source.len()),
));
}
// Copy data block
if dst_offset + data_size <= output.len() {
output[dst_offset..dst_offset + data_size]
.copy_from_slice(&source[src_offset..src_offset + data_size]);
}
src_offset += data_size;
dst_offset += data_size;
// Zero-filled gap (already zeroed from vec initialization)
dst_offset += zero_size;
}
Ok(output)
}
/// Xbox 360 retail AES key for XEX2 session key decryption.
const XEX2_RETAIL_KEY: [u8; 16] = [
0x20, 0xB1, 0x85, 0xA5, 0x9D, 0x28, 0xFD, 0xC3,
0x40, 0x58, 0x3F, 0xBB, 0x08, 0x96, 0xBF, 0x91,
];
/// Xbox 360 devkit AES key (all zeros).
#[allow(dead_code)]
const XEX2_DEVKIT_KEY: [u8; 16] = [0u8; 16];
/// AES-128-CBC decryption with zero IV (matching Xbox 360 XEX decryption).
fn aes_decrypt_cbc(key: &[u8; 16], input: &[u8]) -> Vec<u8> {
let cipher = Aes128::new(key.into());
let mut output = vec![0u8; input.len()];
let mut iv = [0u8; 16];
for (i, chunk) in input.chunks(16).enumerate() {
if chunk.len() < 16 {
// Partial block at end - copy as-is
output[i * 16..i * 16 + chunk.len()].copy_from_slice(chunk);
break;
}
let mut block = aes::Block::clone_from_slice(chunk);
cipher.decrypt_block(&mut block);
// XOR with IV (previous ciphertext block)
for j in 0..16 {
block[j] ^= iv[j];
}
iv.copy_from_slice(chunk);
output[i * 16..(i + 1) * 16].copy_from_slice(&block);
}
output
}
/// Derive the session key by decrypting the XEX's aes_key field with the retail key.
/// Falls back to devkit key if retail produces invalid results.
fn derive_session_key(header: &Xex2Header) -> [u8; 16] {
let sec = match &header.security_info {
Some(s) => s,
None => return [0u8; 16],
};
let decrypted = aes_decrypt_cbc(&XEX2_RETAIL_KEY, &sec.aes_key);
let mut session_key = [0u8; 16];
session_key.copy_from_slice(&decrypted[..16]);
session_key
}
/// De-block compressed data: strip block headers and extract chunk payloads.
///
/// The first block's size comes from the file format header (first_block_size).
/// Each block in the data starts with a block_info struct for the NEXT block:
/// - block_size: u32 BE (size of the next block)
/// - block_hash: [u8; 20] (SHA1 of the next block)
/// Followed by chunks: { chunk_size: u16 BE, data: [u8; chunk_size] }, terminated by chunk_size=0
fn deblock(input: &[u8], first_block_size: u32) -> io::Result<Vec<u8>> {
let mut output = Vec::new();
let mut pos = 0usize;
let mut cur_block_size = first_block_size as usize;
while cur_block_size > 0 && pos < input.len() {
let next_block_pos = pos + cur_block_size;
// Read next block's info from start of current block data
let next_block_size = if pos + 4 <= input.len() {
u32::from_be_bytes([
input[pos], input[pos + 1], input[pos + 2], input[pos + 3],
]) as usize
} else {
0
};
// Skip block_info header (4 bytes size + 20 bytes hash)
let mut p = pos + 4 + 20;
// Read chunks within this block
loop {
if p + 2 > input.len() {
break;
}
let chunk_size = ((input[p] as usize) << 8) | (input[p + 1] as usize);
p += 2;
if chunk_size == 0 {
break;
}
if p + chunk_size > input.len() {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
format!("De-block chunk extends past input (pos={:#x}, chunk_size={:#x}, input_len={:#x})",
p, chunk_size, input.len()),
));
}
output.extend_from_slice(&input[p..p + chunk_size]);
p += chunk_size;
}
if next_block_pos <= pos {
break; // Prevent infinite loop
}
pos = next_block_pos;
cur_block_size = next_block_size;
}
Ok(output)
}
/// Load normal (LZX) compressed image data.
/// Pipeline: decrypt → de-block → LZX decompress
fn load_normal_compressed(source: &[u8], info: &FileFormatInfo, header: &Xex2Header) -> io::Result<Vec<u8>> {
let uncompressed_size = header.security_info.as_ref()
.map(|s| s.image_size as usize)
.unwrap_or(0);
if uncompressed_size == 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Cannot decompress: image_size is 0",
));
}
// Step 1: Decrypt if needed
let decrypted;
let input = if info.encryption_type == ENCRYPTION_NORMAL {
let session_key = derive_session_key(header);
decrypted = aes_decrypt_cbc(&session_key, source);
&decrypted
} else {
source
};
// Step 2: De-block (strip block headers, extract chunk payloads)
let deblocked = deblock(input, info.normal_first_block_size)?;
if deblocked.is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"De-blocking produced no data",
));
}
// Step 3: LZX decompress using mspack C library
let mut output = vec![0u8; uncompressed_size];
let result = unsafe {
xenia_lzx_decompress(
deblocked.as_ptr() as *const std::ffi::c_void,
deblocked.len() as u32,
output.as_mut_ptr() as *mut std::ffi::c_void,
uncompressed_size as u32,
info.normal_window_size,
)
};
if result != 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("LZX decompression failed (mspack error code {})", result),
));
}
tracing::info!("LZX decompressed: {} -> {} bytes", deblocked.len(), uncompressed_size);
Ok(output)
}