1086 lines
33 KiB
Markdown
1086 lines
33 KiB
Markdown
# Xenia → Rust Port: Technical Feasibility Report v2.0
|
||
|
||
This report consolidates the full analysis from both the JIT-based and interpreter-based approaches, grounded in the actual codebase structure.
|
||
|
||
---
|
||
|
||
## 1. Project Architecture
|
||
|
||
Xenia is organized into ~12 major subsystems, all owned by the central `Emulator` class:
|
||
|
||
```
|
||
src/xenia/
|
||
├── app/ # Entry point, premake build
|
||
├── apu/ # Audio (XAudio2, SDL2 backends)
|
||
├── base/ # Platform abstractions, threading, memory
|
||
├── cpu/ # PPC frontend, HIR, compiler passes, x64 JIT backend
|
||
│ ├── ppc/ # Decoder, HIR emitters, scanner
|
||
│ ├── hir/ # HIR opcodes, builder, values
|
||
│ ├── compiler/ # Optimization passes
|
||
│ └── backend/x64/ # Xbyak-based x64 emitter
|
||
├── gpu/ # Xenos GPU, DXBC/SPIR-V shader translators, shader interpreter
|
||
├── hid/ # Input (SDL2, XInput, etc.)
|
||
├── kernel/ # HLE kernel (xboxkrnl/, xam/)
|
||
├── vfs/ # Virtual filesystem (disc image, STFS, ZArchive)
|
||
├── ui/ # ImGui, Vulkan/D3D12 window
|
||
├── memory.cc/.h # 4GB+ guest address space
|
||
└── emulator.cc/.h
|
||
``` [1](#2-0)
|
||
|
||
---
|
||
|
||
## 2. The CPU Pipeline in Detail
|
||
|
||
The current execution path for guest code is:
|
||
|
||
```
|
||
PPC binary → PPCScanner (basic blocks) → PPCHIRBuilder (HIR)
|
||
→ Compiler passes (CFG, simplification, const-prop, DCE, regalloc)
|
||
→ X64Emitter (Xbyak) → native x64 machine code
|
||
``` [2](#2-1)
|
||
|
||
There is **no existing PPC interpreter**. The `ppc_emit_*.cc` files emit HIR, not execute instructions directly. The `PPCScanner` does basic block analysis for the JIT: [3](#2-2)
|
||
|
||
The register file is `PPCContext`: 32 GPRs, 32 FPRs, 128 VMX registers, plus LR/CTR/XER/CR: [4](#2-3)
|
||
|
||
Each guest thread owns a `ThreadState` which holds a `PPCContext*`: [5](#2-4)
|
||
|
||
The full `PPCOpcode` enum covers ~250 opcodes including many Xbox 360-specific VMX128 extensions: [6](#2-5)
|
||
|
||
---
|
||
|
||
## 3. The Two Approaches Compared
|
||
|
||
### Approach A: JIT Port (keep the JIT, rewrite everything else in Rust)
|
||
|
||
The JIT backend (`X64Emitter`) inherits from `Xbyak::CodeGenerator` — a C++ header-only library with no Rust equivalent: [7](#2-6)
|
||
|
||
The MMIO system relies on hardware exceptions (`SIGSEGV`/`VEH`) to intercept guest memory accesses made by JIT-compiled code, then decodes the faulting x64 instruction at runtime: [8](#2-7)
|
||
|
||
This is fundamentally incompatible with safe Rust. Signal handlers in Rust are `unsafe` and severely restricted in what they can do.
|
||
|
||
The host↔guest thunks emit raw x64 at runtime and depend on `PPCContext` being at hardcoded `offsetof` positions: [9](#2-8)
|
||
|
||
**Verdict:** The JIT approach requires keeping the entire `cpu/backend/x64/` subtree in C++ and bridging via FFI. The `cxx` crate is the most ergonomic tool for this, but the FFI boundary would be complex and the safety gains minimal in the hottest code path.
|
||
|
||
---
|
||
|
||
### Approach B: Interpreter Port (execute PPC instructions directly, no JIT)
|
||
|
||
This eliminates the three hardest blockers:
|
||
|
||
| Blocker | JIT | Interpreter |
|
||
|---|---|---|
|
||
| Xbyak / x64 codegen | Blocker | Eliminated |
|
||
| MMIO exception handler | Blocker | Eliminated |
|
||
| Host↔guest ABI thunks | Blocker | Eliminated |
|
||
| `PPCContext` layout coupling to JIT | Hard | Eliminated |
|
||
|
||
In an interpreter, every memory access is a function call. The codebase already has the explicit MMIO-check pattern in `MMIOAwareLoad`: [10](#2-9)
|
||
|
||
And `MMIOHandler` exposes `CheckLoad`/`CheckStore` for exactly this: [11](#2-10)
|
||
|
||
The opcode dispatch infrastructure (`LookupOpcode`, `PPCDecodeData`, `PPCOpcode` enum) is pure data-manipulation code with no JIT dependency — directly portable to Rust: [12](#2-11)
|
||
|
||
The `ppc_emit_*.cc` files serve as a complete semantic reference for every instruction, even though they emit HIR rather than executing directly. The VMX128 instruction encoding macros are documented there: [13](#2-12)
|
||
|
||
**The dominant cost of Approach B is performance.** The Xbox 360 has 3 PPC cores × 2 hardware threads. A naive interpreter runs ~20–100× slower than JIT. Most 3D games would be unplayable.
|
||
|
||
---
|
||
|
||
## 4. Subsystem-by-Subsystem Feasibility
|
||
|
||
### 4.1 Memory System — `unsafe`, but contained
|
||
|
||
The 4GB+ virtual address space reservation via OS file mappings is required regardless of approach: [14](#2-13)
|
||
|
||
This is `unsafe` Rust via `libc`/`windows-sys`, but it is a well-defined, bounded `unsafe` block. The `xe::be<T>` big-endian wrapper maps cleanly to a Rust newtype: [15](#2-14)
|
||
|
||
### 4.2 Kernel HLE — Medium difficulty
|
||
|
||
The kernel has a massive HLE surface: `xboxkrnl_table.inc` alone is 96KB, covering threading, memory, I/O, RTL, video, audio, crypto, and more. The `X_KTHREAD` struct is a packed big-endian layout that lives in guest memory: [16](#2-15)
|
||
|
||
In Rust, this becomes a `#[repr(C, packed)]` struct with `be<T>` newtypes. The `unsafe` is explicit and auditable. The threading HLE (`xboxkrnl_threading.cc` at 66KB) is the most complex piece.
|
||
|
||
### 4.3 GPU — Hard
|
||
|
||
The GPU has two shader translation pipelines (DXBC and SPIR-V) totaling tens of thousands of lines, translating Xenos microcode to host shader IR. The SPIR-V translator uses the glslang `SpirvBuilder` C++ API directly: [17](#2-16)
|
||
|
||
There is no Rust equivalent of `SpirvBuilder`. The `rspirv` crate provides partial SPIR-V construction but lacks the full feature set.
|
||
|
||
**Notable:** The GPU subsystem already has a `ShaderInterpreter` for Xenos microcode — used for internal rectangles and clears where texture fetches are not needed: [18](#2-17)
|
||
|
||
This demonstrates the pattern: interpreter as a correctness baseline, translator for performance.
|
||
|
||
### 4.4 VFS — High feasibility
|
||
|
||
The VFS devices (disc image, ZArchive, STFS/XContent) are pure data-parsing code with no platform dependencies beyond file I/O:
|
||
|
||
|
||
These are ideal candidates for safe Rust. The `stfs_xbox.h` format definitions (15KB of packed structs) translate directly to `#[repr(C)]` Rust structs.
|
||
|
||
### 4.5 Concurrency — Improved by Rust
|
||
|
||
The codebase uses custom spinlocks (`xe_fast_mutex`, `xe_global_mutex`, `xe_unlikely_mutex`) alongside `std::atomic`. Rust's `Send`/`Sync` traits would catch data races at compile time that are currently only caught at runtime: [19](#2-18)
|
||
|
||
### 4.6 Third-party C++ dependencies
|
||
|
||
| Library | Purpose | Rust status |
|
||
|---|---|---|
|
||
| Xbyak | x64 JIT codegen | No equivalent; `dynasm-rs` is limited |
|
||
| glslang/SPIRV-Tools | Shader compilation | `rspirv` (partial) |
|
||
| capstone | x86 disassembly | `capstone` crate (binding) |
|
||
| Dear ImGui | UI | `imgui-rs` (binding) |
|
||
| FFmpeg | Audio/video decode | `ffmpeg-next` (binding) |
|
||
| D3D12/XAudio2 | Windows GPU/audio | `windows-rs` (binding) |
|
||
| Vulkan | GPU | `ash` (good) |
|
||
| snappy/xxhash | Compression/hashing | Native Rust crates exist | [20](#2-19)
|
||
|
||
---
|
||
|
||
## 5. The Interpreter + Basic Block Cache Path
|
||
|
||
The most realistic path to a working Rust port that is both architecturally feasible and eventually performant:
|
||
|
||
```
|
||
Phase 1: Pure interpreter
|
||
- PPCContext as #[repr(C)] Rust struct
|
||
- match on PPCOpcode enum → one fn per opcode
|
||
- Explicit MMIO check on every load/store (no signal handlers)
|
||
- Correct but slow (~20-100x slower than JIT)
|
||
|
||
Phase 2: Basic block cache
|
||
- PPCScanner already identifies basic block boundaries
|
||
- Cache translated basic blocks as Vec<fn(&mut PPCContext)>
|
||
- Invalidate on self-modifying code
|
||
- Recovers significant performance without a full JIT
|
||
|
||
Phase 3: Optional: LLVM via inkwell
|
||
- Replace the basic block cache with LLVM IR generation
|
||
- Eliminates Xbyak entirely; LLVM handles x64/ARM64
|
||
- Major architectural change but Rust-native
|
||
```
|
||
|
||
The `PPCScanner` already provides the basic block boundary detection needed for Phase 2: [3](#2-2)
|
||
|
||
---
|
||
|
||
## 6. Summary Matrix
|
||
|
||
```
|
||
Subsystem | Approach A (JIT) | Approach B (Interpreter)
|
||
---------------------------|------------------|-------------------------
|
||
VFS / file formats | High | High
|
||
Config / logging | High | High
|
||
Kernel HLE (non-threading) | Medium | Medium
|
||
Kernel threading HLE | Low-Medium | Low-Medium
|
||
Memory system | Low (unsafe) | Low (unsafe, same)
|
||
GPU shader translation | Very Low | Very Low (same)
|
||
VMX128 instructions | Hard (HIR emit) | Hard (fn per opcode)
|
||
lwarx/stwcx atomics | Hard (x64 asm) | Medium (AtomicU32 CAS)
|
||
MMIO handler | Blocker | Eliminated
|
||
x64 JIT / Xbyak | Blocker | Eliminated
|
||
Host↔guest thunks | Blocker | Eliminated
|
||
Performance | Good | Poor (Phase 1)
|
||
```
|
||
|
||
**Bottom line:** A full JIT port is not practical without either keeping the JIT in C++ (hybrid) or replacing Xbyak with LLVM. An interpreter port is architecturally feasible in Rust and eliminates the three hardest blockers, but requires a basic block cache layer to approach playable performance. The GPU shader translation pipeline is the second major obstacle regardless of CPU approach, with no clean Rust-native equivalent of the glslang `SpirvBuilder` API.
|
||
|
||
### Citations
|
||
|
||
**File:** src/xenia/emulator.h (L336-349)
|
||
```text
|
||
std::unique_ptr<Memory> memory_;
|
||
|
||
std::unique_ptr<cpu::Processor> processor_;
|
||
std::unique_ptr<apu::AudioSystem> audio_system_;
|
||
std::unique_ptr<apu::AudioMediaPlayer> audio_media_player_;
|
||
std::unique_ptr<gpu::GraphicsSystem> graphics_system_;
|
||
std::unique_ptr<hid::InputSystem> input_system_;
|
||
|
||
std::unique_ptr<cpu::ExportResolver> export_resolver_;
|
||
std::unique_ptr<vfs::VirtualFileSystem> file_system_;
|
||
std::unique_ptr<patcher::Patcher> patcher_;
|
||
std::unique_ptr<patcher::PluginLoader> plugin_loader_;
|
||
|
||
std::unique_ptr<kernel::KernelState> kernel_state_;
|
||
```
|
||
|
||
**File:** src/xenia/cpu/ppc/ppc_translator.cc (L44-112)
|
||
```text
|
||
PPCTranslator::PPCTranslator(PPCFrontend* frontend) : frontend_(frontend) {
|
||
Backend* backend = frontend->processor()->backend();
|
||
|
||
scanner_.reset(new PPCScanner(frontend));
|
||
builder_.reset(new PPCHIRBuilder(frontend));
|
||
compiler_.reset(new Compiler(frontend->processor()));
|
||
assembler_ = backend->CreateAssembler();
|
||
assembler_->Initialize();
|
||
|
||
bool validate = cvars::validate_hir;
|
||
|
||
// Merge blocks early. This will let us use more context in other passes.
|
||
// The CFG is required for simplification and dirtied by it.
|
||
compiler_->AddPass(std::make_unique<passes::ControlFlowAnalysisPass>());
|
||
compiler_->AddPass(std::make_unique<passes::ControlFlowSimplificationPass>());
|
||
|
||
// Passes are executed in the order they are added. Multiple of the same
|
||
// pass type may be used.
|
||
if (!cvars::disable_context_promotion) {
|
||
if (validate) {
|
||
compiler_->AddPass(std::make_unique<passes::ValidationPass>());
|
||
}
|
||
|
||
compiler_->AddPass(std::make_unique<passes::ContextPromotionPass>());
|
||
|
||
if (validate) {
|
||
compiler_->AddPass(std::make_unique<passes::ValidationPass>());
|
||
}
|
||
}
|
||
// Grouped simplification + constant propagation.
|
||
// Loops until no changes are made.
|
||
auto sap = std::make_unique<passes::ConditionalGroupPass>();
|
||
sap->AddPass(std::make_unique<passes::SimplificationPass>());
|
||
if (validate) sap->AddPass(std::make_unique<passes::ValidationPass>());
|
||
sap->AddPass(std::make_unique<passes::ConstantPropagationPass>());
|
||
if (validate) sap->AddPass(std::make_unique<passes::ValidationPass>());
|
||
compiler_->AddPass(std::move(sap));
|
||
|
||
if (backend->machine_info()->supports_extended_load_store) {
|
||
// Backend supports the advanced LOAD/STORE instructions.
|
||
// These will save us a lot of HIR opcodes.
|
||
compiler_->AddPass(
|
||
std::make_unique<passes::MemorySequenceCombinationPass>());
|
||
if (validate)
|
||
compiler_->AddPass(std::make_unique<passes::ValidationPass>());
|
||
}
|
||
compiler_->AddPass(std::make_unique<passes::SimplificationPass>());
|
||
if (validate) compiler_->AddPass(std::make_unique<passes::ValidationPass>());
|
||
// compiler_->AddPass(std::make_unique<passes::DeadStoreEliminationPass>());
|
||
// if (validate)
|
||
// compiler_->AddPass(std::make_unique<passes::ValidationPass>());
|
||
compiler_->AddPass(std::make_unique<passes::DeadCodeEliminationPass>());
|
||
if (validate) compiler_->AddPass(std::make_unique<passes::ValidationPass>());
|
||
|
||
//// Removes all unneeded variables. Try not to add new ones after this.
|
||
// compiler_->AddPass(new passes::ValueReductionPass());
|
||
// if (validate) compiler_->AddPass(new passes::ValidationPass());
|
||
|
||
// Register allocation for the target backend.
|
||
// Will modify the HIR to add loads/stores.
|
||
// This should be the last pass before finalization, as after this all
|
||
// registers are assigned and ready to be emitted.
|
||
compiler_->AddPass(std::make_unique<passes::RegisterAllocationPass>(
|
||
backend->machine_info()));
|
||
if (validate) compiler_->AddPass(std::make_unique<passes::ValidationPass>());
|
||
|
||
// Must come last. The HIR is not really HIR after this.
|
||
compiler_->AddPass(std::make_unique<passes::FinalizationPass>());
|
||
}
|
||
```
|
||
|
||
**File:** src/xenia/cpu/ppc/ppc_scanner.h (L29-42)
|
||
```text
|
||
class PPCScanner {
|
||
public:
|
||
explicit PPCScanner(PPCFrontend* frontend);
|
||
~PPCScanner();
|
||
|
||
bool Scan(GuestFunction* function, FunctionDebugInfo* debug_info);
|
||
|
||
std::vector<BlockInfo> FindBlocks(GuestFunction* function);
|
||
|
||
private:
|
||
bool IsRestGprLr(uint32_t address);
|
||
|
||
PPCFrontend* frontend_ = nullptr;
|
||
};
|
||
```
|
||
|
||
**File:** src/xenia/cpu/ppc/ppc_context.h (L376-400)
|
||
```text
|
||
// Most frequently used registers first.
|
||
|
||
uint64_t r[32]; // 0x20 General purpose registers
|
||
uint64_t ctr; // 0x18 Count register
|
||
uint64_t lr; // 0x10 Link register
|
||
|
||
uint64_t msr; // machine state register
|
||
|
||
double f[32]; // 0x120 Floating-point registers
|
||
vec128_t v[128]; // 0x220 VMX128 vector registers
|
||
vec128_t vscr_vec;
|
||
// XER register:
|
||
// Split to make it easier to do individual updates.
|
||
uint8_t xer_ca;
|
||
uint8_t xer_ov;
|
||
uint8_t xer_so;
|
||
|
||
// Condition registers:
|
||
// These are split to make it easier to do DCE on unused stores.
|
||
uint64_t cr() const;
|
||
void set_cr(uint64_t value);
|
||
// todo: remove, saturation should be represented by a vector
|
||
uint8_t vscr_sat;
|
||
|
||
uint32_t vrsave;
|
||
```
|
||
|
||
**File:** src/xenia/cpu/thread_state.h (L24-50)
|
||
```text
|
||
class ThreadState {
|
||
public:
|
||
ThreadState(Processor* processor, uint32_t thread_id, uint32_t stack_base = 0,
|
||
uint32_t pcr_address = 0);
|
||
~ThreadState();
|
||
|
||
Processor* processor() const { return processor_; }
|
||
Memory* memory() const { return memory_; }
|
||
void* backend_data() const { return backend_data_; }
|
||
ppc::PPCContext* context() const { return context_; }
|
||
uint32_t thread_id() const { return thread_id_; }
|
||
|
||
static void Bind(ThreadState* thread_state);
|
||
static ThreadState* Get();
|
||
static uint32_t GetThreadID();
|
||
|
||
private:
|
||
Processor* processor_;
|
||
Memory* memory_;
|
||
void* backend_data_;
|
||
|
||
uint32_t pcr_address_ = 0;
|
||
uint32_t thread_id_ = 0;
|
||
|
||
// NOTE: must be 64b aligned for SSE ops.
|
||
ppc::PPCContext* context_;
|
||
};
|
||
```
|
||
|
||
**File:** src/xenia/cpu/ppc/ppc_opcode.h (L14-200)
|
||
```text
|
||
enum class PPCOpcode : uint32_t {
|
||
addcx,
|
||
addex,
|
||
addi,
|
||
addic,
|
||
addicx,
|
||
addis,
|
||
addmex,
|
||
addx,
|
||
addzex,
|
||
andcx,
|
||
andisx,
|
||
andix,
|
||
andx,
|
||
bcctrx,
|
||
bclrx,
|
||
bcx,
|
||
bx,
|
||
cmp,
|
||
cmpi,
|
||
cmpl,
|
||
cmpli,
|
||
cntlzdx,
|
||
cntlzwx,
|
||
crand,
|
||
crandc,
|
||
creqv,
|
||
crnand,
|
||
crnor,
|
||
cror,
|
||
crorc,
|
||
crxor,
|
||
dcbf,
|
||
dcbi,
|
||
dcbst,
|
||
dcbt,
|
||
dcbtst,
|
||
dcbz,
|
||
dcbz128,
|
||
divdux,
|
||
divdx,
|
||
divwux,
|
||
divwx,
|
||
eieio,
|
||
eqvx,
|
||
extsbx,
|
||
extshx,
|
||
extswx,
|
||
fabsx,
|
||
faddsx,
|
||
faddx,
|
||
fcfidx,
|
||
fcmpo,
|
||
fcmpu,
|
||
fctidx,
|
||
fctidzx,
|
||
fctiwx,
|
||
fctiwzx,
|
||
fdivsx,
|
||
fdivx,
|
||
fmaddsx,
|
||
fmaddx,
|
||
fmrx,
|
||
fmsubsx,
|
||
fmsubx,
|
||
fmulsx,
|
||
fmulx,
|
||
fnabsx,
|
||
fnegx,
|
||
fnmaddsx,
|
||
fnmaddx,
|
||
fnmsubsx,
|
||
fnmsubx,
|
||
fresx,
|
||
frspx,
|
||
frsqrtex,
|
||
fselx,
|
||
fsqrtsx,
|
||
fsqrtx,
|
||
fsubsx,
|
||
fsubx,
|
||
icbi,
|
||
isync,
|
||
lbz,
|
||
lbzu,
|
||
lbzux,
|
||
lbzx,
|
||
ld,
|
||
ldarx,
|
||
ldbrx,
|
||
ldu,
|
||
ldux,
|
||
ldx,
|
||
lfd,
|
||
lfdu,
|
||
lfdux,
|
||
lfdx,
|
||
lfs,
|
||
lfsu,
|
||
lfsux,
|
||
lfsx,
|
||
lha,
|
||
lhau,
|
||
lhaux,
|
||
lhax,
|
||
lhbrx,
|
||
lhz,
|
||
lhzu,
|
||
lhzux,
|
||
lhzx,
|
||
lmw,
|
||
lswi,
|
||
lswx,
|
||
lvebx,
|
||
lvehx,
|
||
lvewx,
|
||
lvewx128,
|
||
lvlx,
|
||
lvlx128,
|
||
lvlxl,
|
||
lvlxl128,
|
||
lvrx,
|
||
lvrx128,
|
||
lvrxl,
|
||
lvrxl128,
|
||
lvsl,
|
||
lvsl128,
|
||
lvsr,
|
||
lvsr128,
|
||
lvx,
|
||
lvx128,
|
||
lvxl,
|
||
lvxl128,
|
||
lwa,
|
||
lwarx,
|
||
lwaux,
|
||
lwax,
|
||
lwbrx,
|
||
lwz,
|
||
lwzu,
|
||
lwzux,
|
||
lwzx,
|
||
mcrf,
|
||
mcrfs,
|
||
mcrxr,
|
||
mfcr,
|
||
mffsx,
|
||
mfmsr,
|
||
mfspr,
|
||
mftb,
|
||
mfvscr,
|
||
mtcrf,
|
||
mtfsb0x,
|
||
mtfsb1x,
|
||
mtfsfix,
|
||
mtfsfx,
|
||
mtmsr,
|
||
mtmsrd,
|
||
mtspr,
|
||
mtvscr,
|
||
mulhdux,
|
||
mulhdx,
|
||
mulhwux,
|
||
mulhwx,
|
||
mulldx,
|
||
mulli,
|
||
mullwx,
|
||
nandx,
|
||
negx,
|
||
norx,
|
||
orcx,
|
||
ori,
|
||
oris,
|
||
orx,
|
||
rldclx,
|
||
rldcrx,
|
||
rldiclx,
|
||
rldicrx,
|
||
rldicx,
|
||
rldimix,
|
||
rlwimix,
|
||
rlwinmx,
|
||
rlwnmx,
|
||
sc,
|
||
sldx,
|
||
slwx,
|
||
sradix,
|
||
```
|
||
|
||
**File:** src/xenia/cpu/backend/x64/x64_emitter.h (L208-212)
|
||
```text
|
||
class X64Emitter : public Xbyak::CodeGenerator {
|
||
public:
|
||
X64Emitter(X64Backend* backend, XbyakAllocator* allocator);
|
||
virtual ~X64Emitter();
|
||
|
||
```
|
||
|
||
**File:** src/xenia/cpu/mmio_handler.cc (L402-470)
|
||
```text
|
||
bool MMIOHandler::ExceptionCallback(Exception* ex) {
|
||
if (ex->code() != Exception::Code::kAccessViolation) {
|
||
return false;
|
||
}
|
||
Exception::AccessViolationOperation operation =
|
||
ex->access_violation_operation();
|
||
if (operation != Exception::AccessViolationOperation::kRead &&
|
||
operation != Exception::AccessViolationOperation::kWrite) {
|
||
// Data Execution Prevention or something else uninteresting.
|
||
return false;
|
||
}
|
||
bool is_write = operation == Exception::AccessViolationOperation::kWrite;
|
||
if (ex->fault_address() < uint64_t(virtual_membase_) ||
|
||
ex->fault_address() > uint64_t(memory_end_)) {
|
||
// Quick kill anything outside our mapping.
|
||
return false;
|
||
}
|
||
|
||
void* fault_host_address = reinterpret_cast<void*>(ex->fault_address());
|
||
|
||
// Access violations are pretty rare, so we can do a linear search here.
|
||
// Only check if in the virtual range, as we only support virtual ranges.
|
||
const MMIORange* range = nullptr;
|
||
uint32_t fault_guest_virtual_address = 0;
|
||
if (ex->fault_address() < uint64_t(physical_membase_)) {
|
||
fault_guest_virtual_address = host_to_guest_virtual_(
|
||
host_to_guest_virtual_context_, fault_host_address);
|
||
for (const auto& test_range : mapped_ranges_) {
|
||
if ((fault_guest_virtual_address & test_range.mask) ==
|
||
test_range.address) {
|
||
// Address is within the range of this mapping.
|
||
range = &test_range;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
if (!range) {
|
||
// Recheck if the pages are still protected (race condition - another thread
|
||
// clears the watch we just hit).
|
||
// Do this under the lock so we don't introduce another race condition.
|
||
auto lock = global_critical_region_.Acquire();
|
||
memory::PageAccess cur_access;
|
||
size_t page_length = memory::page_size();
|
||
memory::QueryProtect(fault_host_address, page_length, cur_access);
|
||
if (cur_access != memory::PageAccess::kNoAccess &&
|
||
(!is_write || cur_access != memory::PageAccess::kReadOnly)) {
|
||
// Another thread has cleared this watch. Abort.
|
||
XELOGD("Race condition on watch, was already cleared by another thread!");
|
||
return true;
|
||
}
|
||
// The address is not found within any range, so either a write watch or an
|
||
// actual access violation.
|
||
if (access_violation_callback_) {
|
||
return access_violation_callback_(std::move(lock),
|
||
access_violation_callback_context_,
|
||
fault_host_address, is_write);
|
||
}
|
||
return false;
|
||
}
|
||
|
||
auto rip = ex->pc();
|
||
auto p = reinterpret_cast<const uint8_t*>(rip);
|
||
DecodedLoadStore decoded_load_store;
|
||
if (!TryDecodeLoadStore(p, decoded_load_store)) {
|
||
XELOGE("Unable to decode MMIO load or store instruction at {}",
|
||
static_cast<const void*>(p));
|
||
assert_always("Unknown MMIO instruction type");
|
||
return false;
|
||
}
|
||
```
|
||
|
||
**File:** src/xenia/cpu/backend/x64/x64_backend.cc (L630-726)
|
||
```text
|
||
HostToGuestThunk X64HelperEmitter::EmitHostToGuestThunk() {
|
||
#ifdef XE_PLATFORM_WIN32
|
||
// rcx = target
|
||
// rdx = arg0 (context)
|
||
// r8 = arg1 (guest return address)
|
||
|
||
_code_offsets code_offsets = {};
|
||
|
||
constexpr size_t stack_size = StackLayout::THUNK_STACK_SIZE;
|
||
|
||
code_offsets.prolog = getSize();
|
||
|
||
// rsp + 0 = return address
|
||
mov(qword[rsp + 8 * 3], r8);
|
||
mov(qword[rsp + 8 * 2], rdx);
|
||
mov(qword[rsp + 8 * 1], rcx);
|
||
sub(rsp, stack_size);
|
||
|
||
code_offsets.prolog_stack_alloc = getSize();
|
||
code_offsets.body = getSize();
|
||
|
||
// Save nonvolatile registers.
|
||
EmitSaveNonvolatileRegs();
|
||
|
||
mov(rax, rcx);
|
||
mov(rsi, rdx); // context
|
||
mov(rdi, ptr[rdx + offsetof(ppc::PPCContext, virtual_membase)]); // membase
|
||
mov(rcx, r8); // return address
|
||
call(rax);
|
||
vzeroupper();
|
||
EmitLoadNonvolatileRegs();
|
||
|
||
code_offsets.epilog = getSize();
|
||
|
||
add(rsp, stack_size);
|
||
mov(rcx, qword[rsp + 8 * 1]);
|
||
mov(rdx, qword[rsp + 8 * 2]);
|
||
mov(r8, qword[rsp + 8 * 3]);
|
||
ret();
|
||
#elif XE_PLATFORM_LINUX || XE_PLATFORM_MAC
|
||
// System-V ABI args:
|
||
// rdi = target
|
||
// rsi = arg0 (context)
|
||
// rdx = arg1 (guest return address)
|
||
|
||
struct _code_offsets {
|
||
size_t prolog;
|
||
size_t prolog_stack_alloc;
|
||
size_t body;
|
||
size_t epilog;
|
||
size_t tail;
|
||
} code_offsets = {};
|
||
|
||
constexpr size_t stack_size = StackLayout::THUNK_STACK_SIZE;
|
||
|
||
code_offsets.prolog = getSize();
|
||
// rsp + 0 = return address
|
||
sub(rsp, stack_size);
|
||
|
||
code_offsets.prolog_stack_alloc = getSize();
|
||
code_offsets.body = getSize();
|
||
|
||
// Save nonvolatile registers.
|
||
EmitSaveNonvolatileRegs();
|
||
|
||
mov(rax, rdi);
|
||
// mov(rsi, rsi); // context
|
||
mov(rdi, ptr[rsi + offsetof(ppc::PPCContext, virtual_membase)]); // membase
|
||
mov(rcx, rdx); // return address
|
||
call(rax);
|
||
|
||
EmitLoadNonvolatileRegs();
|
||
|
||
code_offsets.epilog = getSize();
|
||
|
||
add(rsp, stack_size);
|
||
ret();
|
||
#else
|
||
assert_always("Unknown platform ABI in host to guest thunk!");
|
||
#endif
|
||
|
||
code_offsets.tail = getSize();
|
||
|
||
assert_zero(code_offsets.prolog);
|
||
EmitFunctionInfo func_info = {};
|
||
func_info.code_size.total = getSize();
|
||
func_info.code_size.prolog = code_offsets.body - code_offsets.prolog;
|
||
func_info.code_size.body = code_offsets.epilog - code_offsets.body;
|
||
func_info.code_size.epilog = code_offsets.tail - code_offsets.epilog;
|
||
func_info.code_size.tail = getSize() - code_offsets.tail;
|
||
func_info.prolog_stack_alloc_offset =
|
||
code_offsets.prolog_stack_alloc - code_offsets.prolog;
|
||
func_info.stack_size = stack_size;
|
||
|
||
void* fn = Emplace(func_info);
|
||
return (HostToGuestThunk)fn;
|
||
}
|
||
```
|
||
|
||
**File:** src/xenia/cpu/backend/x64/x64_seq_memory.cc (L1215-1237)
|
||
```text
|
||
template <typename T, bool swap>
|
||
static T MMIOAwareLoad(void* _ctx, unsigned int guestaddr) {
|
||
T value;
|
||
|
||
if (guestaddr >= 0xE0000000) {
|
||
guestaddr += 0x1000;
|
||
}
|
||
|
||
auto ctx = reinterpret_cast<ppc::PPCContext*>(_ctx);
|
||
|
||
auto gaddr = ctx->processor->memory()->LookupVirtualMappedRange(guestaddr);
|
||
if (!gaddr) {
|
||
value = *reinterpret_cast<T*>(ctx->virtual_membase + guestaddr);
|
||
if (swap) {
|
||
value = xe::byte_swap(value);
|
||
}
|
||
} else {
|
||
/*
|
||
was having issues, found by comparing the values used with exceptions
|
||
to these that we were reversed...
|
||
*/
|
||
value = gaddr->read(nullptr, gaddr->callback_context, guestaddr);
|
||
}
|
||
```
|
||
|
||
**File:** src/xenia/cpu/mmio_handler.h (L73-74)
|
||
```text
|
||
bool CheckLoad(uint32_t virtual_address, uint32_t* out_value);
|
||
bool CheckStore(uint32_t virtual_address, uint32_t value);
|
||
```
|
||
|
||
**File:** src/xenia/cpu/ppc/ppc_opcode_lookup_gen.cc (L261-285)
|
||
```text
|
||
switch ((ExtractBits(code, 22, 25) << 2)|(ExtractBits(code, 27, 27) << 0)) {
|
||
case 0b000101: PPC_DECODER_HIT(vrlw128);
|
||
case 0b001101: PPC_DECODER_HIT(vslw128);
|
||
case 0b010101: PPC_DECODER_HIT(vsraw128);
|
||
case 0b011101: PPC_DECODER_HIT(vsrw128);
|
||
case 0b101000: PPC_DECODER_HIT(vmaxfp128);
|
||
case 0b101100: PPC_DECODER_HIT(vminfp128);
|
||
case 0b110000: PPC_DECODER_HIT(vmrghw128);
|
||
case 0b110100: PPC_DECODER_HIT(vmrglw128);
|
||
case 0b111000: PPC_DECODER_HIT(vupkhsb128);
|
||
case 0b111100: PPC_DECODER_HIT(vupklsb128);
|
||
}
|
||
PPC_DECODER_MISS;
|
||
case 7: PPC_DECODER_HIT(mulli);
|
||
case 8: PPC_DECODER_HIT(subficx);
|
||
case 10: PPC_DECODER_HIT(cmpli);
|
||
case 11: PPC_DECODER_HIT(cmpi);
|
||
case 12: PPC_DECODER_HIT(addic);
|
||
case 13: PPC_DECODER_HIT(addicx);
|
||
case 14: PPC_DECODER_HIT(addi);
|
||
case 15: PPC_DECODER_HIT(addis);
|
||
case 16: PPC_DECODER_HIT(bcx);
|
||
case 17: PPC_DECODER_HIT(sc);
|
||
case 18: PPC_DECODER_HIT(bx);
|
||
case 19:
|
||
```
|
||
|
||
**File:** src/xenia/cpu/ppc/ppc_emit_altivec.cc (L36-50)
|
||
```text
|
||
#define OP(x) ((((uint32_t)(x)) & 0x3f) << 26)
|
||
#define VX128(op, xop) (OP(op) | (((uint32_t)(xop)) & 0x3d0))
|
||
#define VX128_1(op, xop) (OP(op) | (((uint32_t)(xop)) & 0x7f3))
|
||
#define VX128_2(op, xop) (OP(op) | (((uint32_t)(xop)) & 0x210))
|
||
#define VX128_3(op, xop) (OP(op) | (((uint32_t)(xop)) & 0x7f0))
|
||
#define VX128_4(op, xop) (OP(op) | (((uint32_t)(xop)) & 0x730))
|
||
#define VX128_5(op, xop) (OP(op) | (((uint32_t)(xop)) & 0x10))
|
||
#define VX128_P(op, xop) (OP(op) | (((uint32_t)(xop)) & 0x630))
|
||
|
||
#define VX128_VD128 (i.VX128.VD128l | (i.VX128.VD128h << 5))
|
||
#define VX128_VA128 \
|
||
(i.VX128.VA128l | (i.VX128.VA128h << 5) | (i.VX128.VA128H << 6))
|
||
#define VX128_VB128 (i.VX128.VB128l | (i.VX128.VB128h << 5))
|
||
#define VX128_1_VD128 (i.VX128_1.VD128l | (i.VX128_1.VD128h << 5))
|
||
#define VX128_2_VD128 (i.VX128_2.VD128l | (i.VX128_2.VD128h << 5))
|
||
```
|
||
|
||
**File:** src/xenia/memory.cc (L137-168)
|
||
```text
|
||
bool Memory::Initialize() {
|
||
file_name_ = fmt::format("xenia_memory_{}", Clock::QueryHostTickCount());
|
||
|
||
// Create main page file-backed mapping. This is all reserved but
|
||
// uncommitted (so it shouldn't expand page file).
|
||
mapping_ = xe::memory::CreateFileMappingHandle(
|
||
file_name_,
|
||
// entire 4gb space + 512mb physical:
|
||
0x11FFFFFFF, xe::memory::PageAccess::kReadWrite, false);
|
||
if (mapping_ == xe::memory::kFileMappingHandleInvalid) {
|
||
XELOGE("Unable to reserve the 4gb guest address space.");
|
||
assert_always();
|
||
return false;
|
||
}
|
||
|
||
// Attempt to create our views. This may fail at the first address
|
||
// we pick, so try a few times.
|
||
mapping_base_ = 0;
|
||
for (size_t n = 32; n < 64; n++) {
|
||
auto mapping_base = reinterpret_cast<uint8_t*>(1ull << n);
|
||
if (!MapViews(mapping_base)) {
|
||
mapping_base_ = mapping_base;
|
||
break;
|
||
}
|
||
}
|
||
if (!mapping_base_) {
|
||
XELOGE("Unable to find a continuous block in the 64bit address space.");
|
||
assert_always();
|
||
return false;
|
||
}
|
||
virtual_membase_ = mapping_base_;
|
||
physical_membase_ = mapping_base_ + 0x100000000ull;
|
||
```
|
||
|
||
**File:** src/xenia/base/byte_order.h (L83-136)
|
||
```text
|
||
template <typename T, std::endian E>
|
||
struct endian_store {
|
||
endian_store() = default;
|
||
endian_store(const T& src) { set(src); }
|
||
endian_store(const endian_store& other) { set(other); }
|
||
operator T() const { return get(); }
|
||
|
||
void set(const T& src) {
|
||
if constexpr (std::endian::native == E) {
|
||
value = src;
|
||
} else {
|
||
value = xe::byte_swap(src);
|
||
}
|
||
}
|
||
void set(const endian_store& other) { value = other.value; }
|
||
T get() const {
|
||
if constexpr (std::endian::native == E) {
|
||
return value;
|
||
}
|
||
return xe::byte_swap(value);
|
||
}
|
||
|
||
endian_store<T, E>& operator+=(int a) {
|
||
*this = *this + a;
|
||
return *this;
|
||
}
|
||
endian_store<T, E>& operator-=(int a) {
|
||
*this = *this - a;
|
||
return *this;
|
||
}
|
||
endian_store<T, E>& operator++() {
|
||
*this += 1;
|
||
return *this;
|
||
} // ++a
|
||
endian_store<T, E> operator++(int) {
|
||
*this += 1;
|
||
return (*this - 1);
|
||
} // a++
|
||
endian_store<T, E>& operator--() {
|
||
*this -= 1;
|
||
return *this;
|
||
} // --a
|
||
endian_store<T, E> operator--(int) {
|
||
*this -= 1;
|
||
return (*this + 1);
|
||
} // a--
|
||
|
||
T value;
|
||
};
|
||
|
||
template <typename T>
|
||
using be = endian_store<T, std::endian::big>;
|
||
template <typename T>
|
||
using le = endian_store<T, std::endian::little>;
|
||
```
|
||
|
||
**File:** src/xenia/kernel/xthread.h (L249-280)
|
||
```text
|
||
struct X_KTHREAD {
|
||
X_DISPATCH_HEADER header; // 0x0
|
||
xe::be<uint32_t> unk_10; // 0x10
|
||
xe::be<uint32_t> unk_14; // 0x14
|
||
X_KTIMER wait_timeout_timer; // 0x18
|
||
X_KWAIT_BLOCK wait_timeout_block; // 0x40
|
||
uint8_t unk_58[0x4]; // 0x58
|
||
xe::be<uint32_t> stack_base; // 0x5C
|
||
xe::be<uint32_t> stack_limit; // 0x60
|
||
xe::be<uint32_t> stack_kernel; // 0x64
|
||
xe::be<uint32_t> tls_address; // 0x68
|
||
// state = is thread running, suspended, etc
|
||
uint8_t thread_state; // 0x6C
|
||
// 0x70 = priority?
|
||
uint8_t alerted[2]; // 0x6D
|
||
uint8_t alertable; // 0x6F
|
||
uint8_t priority; // 0x70
|
||
uint8_t fpu_exceptions_on; // 0x71
|
||
// these two process types both get set to the same thing, process_type is
|
||
// referenced most frequently, however process_type_dup gets referenced a few
|
||
// times while the process is being created
|
||
uint8_t process_type_dup;
|
||
uint8_t process_type;
|
||
// apc_mode determines which list an apc goes into
|
||
util::X_TYPED_LIST<XAPC, offsetof(XAPC, list_entry)> apc_lists[2];
|
||
TypedGuestPointer<X_KPROCESS> process; // 0x84
|
||
uint8_t executing_kernel_apc; // 0x88
|
||
// when context switch happens, this is copied into
|
||
// apc_software_interrupt_state for kpcr
|
||
uint8_t deferred_apc_software_interrupt_state; // 0x89
|
||
uint8_t user_apc_pending; // 0x8A
|
||
uint8_t may_queue_apcs; // 0x8B
|
||
```
|
||
|
||
**File:** src/xenia/gpu/spirv_shader_translator.cc (L153-170)
|
||
```text
|
||
void SpirvShaderTranslator::StartTranslation() {
|
||
// TODO(Triang3l): Logger.
|
||
builder_ = std::make_unique<SpirvBuilder>(
|
||
features_.spirv_version, (kSpirvMagicToolId << 16) | 1, nullptr);
|
||
|
||
builder_->addCapability(IsSpirvTessEvalShader() ? spv::CapabilityTessellation
|
||
: spv::CapabilityShader);
|
||
if (features_.spirv_version < spv::Spv_1_4) {
|
||
if (features_.signed_zero_inf_nan_preserve_float32 ||
|
||
features_.denorm_flush_to_zero_float32 ||
|
||
features_.rounding_mode_rte_float32) {
|
||
builder_->addExtension("SPV_KHR_float_controls");
|
||
}
|
||
}
|
||
ext_inst_glsl_std_450_ = builder_->import("GLSL.std.450");
|
||
builder_->setMemoryModel(spv::AddressingModelLogical,
|
||
spv::MemoryModelGLSL450);
|
||
builder_->setSource(spv::SourceLanguageUnknown, 0);
|
||
```
|
||
|
||
**File:** src/xenia/gpu/shader_interpreter.h (L26-60)
|
||
```text
|
||
class ShaderInterpreter {
|
||
public:
|
||
ShaderInterpreter(const RegisterFile& register_file, const Memory& memory)
|
||
: register_file_(register_file), memory_(memory) {}
|
||
|
||
class ExportSink {
|
||
public:
|
||
virtual ~ExportSink() = default;
|
||
virtual void AllocExport(ucode::AllocType type, uint32_t size) {}
|
||
virtual void Export(ucode::ExportRegister export_register,
|
||
const float* value, uint32_t value_mask) {}
|
||
};
|
||
|
||
void SetTraceWriter(TraceWriter* new_trace_writer) {
|
||
trace_writer_ = new_trace_writer;
|
||
}
|
||
|
||
ExportSink* GetExportSink() const { return export_sink_; }
|
||
void SetExportSink(ExportSink* new_export_sink) {
|
||
export_sink_ = new_export_sink;
|
||
}
|
||
|
||
const float* temp_registers() const { return &temp_registers_[0][0]; }
|
||
float* temp_registers() { return &temp_registers_[0][0]; }
|
||
|
||
static bool CanInterpretShader(const Shader& shader) {
|
||
assert_true(shader.is_ucode_analyzed());
|
||
// Texture instructions are not very common in vertex shaders (and not used
|
||
// in Direct3D 9's internal rectangles such as clears) and are extremely
|
||
// complex, not implemented.
|
||
if (shader.uses_texture_fetch_instruction_results()) {
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
```
|
||
|
||
**File:** src/xenia/base/mutex.h (L27-87)
|
||
```text
|
||
class alignas(4096) xe_global_mutex {
|
||
XE_MAYBE_UNUSED
|
||
char detail[64];
|
||
|
||
public:
|
||
xe_global_mutex();
|
||
~xe_global_mutex();
|
||
|
||
void lock();
|
||
void unlock();
|
||
bool try_lock();
|
||
};
|
||
using global_mutex_type = xe_global_mutex;
|
||
|
||
class alignas(64) xe_fast_mutex {
|
||
XE_MAYBE_UNUSED
|
||
char detail[64];
|
||
|
||
public:
|
||
xe_fast_mutex();
|
||
~xe_fast_mutex();
|
||
|
||
void lock();
|
||
void unlock();
|
||
bool try_lock();
|
||
};
|
||
// a mutex that is extremely unlikely to ever be locked
|
||
// use for race conditions that have extremely remote odds of happening
|
||
class xe_unlikely_mutex {
|
||
std::atomic<uint32_t> mut;
|
||
bool _tryget() {
|
||
uint32_t lock_expected = 0;
|
||
return mut.compare_exchange_strong(lock_expected, 1);
|
||
}
|
||
|
||
public:
|
||
xe_unlikely_mutex() : mut(0) {}
|
||
~xe_unlikely_mutex() { mut = 0; }
|
||
|
||
void lock() {
|
||
if (XE_LIKELY(_tryget())) {
|
||
return;
|
||
} else {
|
||
do {
|
||
// chrispy: warning, if no SMT, mm_pause does nothing...
|
||
#if XE_ARCH_AMD64 == 1
|
||
_mm_pause();
|
||
#endif
|
||
|
||
} while (!_tryget());
|
||
}
|
||
}
|
||
void unlock() { mut.exchange(0); }
|
||
bool try_lock() { return _tryget(); }
|
||
};
|
||
using xe_mutex = xe_fast_mutex;
|
||
#else
|
||
using global_mutex_type = std::recursive_mutex;
|
||
using xe_mutex = std::mutex;
|
||
using xe_unlikely_mutex = std::mutex;
|
||
#endif
|
||
```
|
||
|
||
**File:** src/xenia/app/premake5.lua (L31-42)
|
||
```lua
|
||
"glslang-spirv",
|
||
"imgui",
|
||
"libavcodec",
|
||
"libavutil",
|
||
"mspack",
|
||
"snappy",
|
||
"xxhash",
|
||
})
|
||
defines({
|
||
"XBYAK_NO_OP_NAMES",
|
||
"XBYAK_ENABLE_OMITTED_OPERAND",
|
||
})
|
||
```
|