Compare commits
2 Commits
phase-a-tr
...
archive/xe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a519c76800 | ||
|
|
06a23212fb |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -116,3 +116,6 @@ node_modules/.bin/
|
||||
/cache0
|
||||
/devkit
|
||||
recent.toml
|
||||
|
||||
# Rust
|
||||
target/
|
||||
@@ -96,12 +96,10 @@ include_directories(SYSTEM
|
||||
add_compile_definitions(
|
||||
VULKAN_HPP_NO_TO_STRING
|
||||
IMGUI_DISABLE_DEFAULT_FONT
|
||||
IMGUI_USE_STB_SPRINTF
|
||||
USE_CPP17 # Tabulate
|
||||
)
|
||||
|
||||
# Prevent ASan error from ImGUI
|
||||
add_compile_definitions($<$<NOT:$<CONFIG:Checked>>:IMGUI_USE_STB_SPRINTF>)
|
||||
|
||||
# Static library define
|
||||
add_compile_definitions(_LIB)
|
||||
|
||||
|
||||
164
RUST_PORT_CODEMAP.md
Normal file
164
RUST_PORT_CODEMAP.md
Normal 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
1085
RUST_PORT_REPORT.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -61,9 +61,6 @@ void NoProfileDialog::OnDraw(ImGuiIO& io) {
|
||||
const auto content_files = xe::filesystem::ListDirectories(
|
||||
emulator_window_->emulator()->content_root());
|
||||
|
||||
if (ImGui::IsWindowAppearing()) {
|
||||
ImGui::SetKeyboardFocusHere();
|
||||
}
|
||||
if (content_files.empty()) {
|
||||
if (ImGui::Button("Create Profile")) {
|
||||
new kernel::xam::ui::CreateProfileUI(emulator_window_->imgui_drawer(),
|
||||
@@ -216,21 +213,7 @@ void ProfileConfigDialog::OnDraw(ImGuiIO& io) {
|
||||
if (ImGui::BeginMenu("Login to slot:")) {
|
||||
for (uint8_t i = 1; i <= XUserMaxUserCount; i++) {
|
||||
if (ImGui::MenuItem(fmt::format("slot {}", i).c_str())) {
|
||||
uint64_t current_slot_xuid = 0;
|
||||
|
||||
if (const auto current_profile = profile_manager->GetProfile(
|
||||
static_cast<uint8_t>(i - 1));
|
||||
current_profile) {
|
||||
current_slot_xuid = current_profile->xuid();
|
||||
}
|
||||
|
||||
profile_manager->Login(xuid, i - 1);
|
||||
LoadProfileIcon(xuid);
|
||||
|
||||
// Release resources
|
||||
if (current_slot_xuid) {
|
||||
LoadProfileIcon(current_slot_xuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::EndMenu();
|
||||
|
||||
@@ -68,28 +68,20 @@ class AudioMediaPlayer {
|
||||
}
|
||||
XmpApp::PlaybackFlags GetPlaybackFlags() const { return playback_flags_; }
|
||||
|
||||
void SetXMPOverride(bool xmp_override) { xmp_override_ = xmp_override; }
|
||||
bool IsXMPOverrideEnabled() const { return xmp_override_; }
|
||||
void SetPlaybackClient(XmpApp::PlaybackClient playback_client) {
|
||||
if (playback_client == XmpApp::PlaybackClient::kSystem) {
|
||||
return;
|
||||
}
|
||||
|
||||
playback_client_ = playback_client;
|
||||
}
|
||||
|
||||
XmpApp::PlaybackClient GetPlaybackClient() const { return playback_client_; }
|
||||
uint32_t GetDashInItState() const { return dash_init_state; }
|
||||
|
||||
void SetXMPClient(XMP_CLIENT xmp_client) { xmp_client_ = xmp_client; }
|
||||
|
||||
void SetPlaybackController(PlaybackController playback_controller) {
|
||||
playback_controller_ = playback_controller;
|
||||
}
|
||||
|
||||
PlaybackController GetPlaybackController() const {
|
||||
return playback_controller_;
|
||||
}
|
||||
|
||||
XMP_CLIENT GetXMPClient() const { return xmp_client_; }
|
||||
|
||||
bool IsTitleInPlaybackControl() const {
|
||||
const bool game_control = xmp_client_ == XMP_CLIENT::Game &&
|
||||
playback_controller_ == PlaybackController::Game;
|
||||
|
||||
return game_control || xmp_override_ || is_title_rendering_enabled_;
|
||||
return playback_client_ == XmpApp::PlaybackClient::kTitle ||
|
||||
is_title_rendering_enabled_;
|
||||
}
|
||||
|
||||
void SetCaptureCallback(uint32_t callback, uint32_t context,
|
||||
@@ -110,12 +102,10 @@ class AudioMediaPlayer {
|
||||
std::span<uint8_t> LoadSongToMemory();
|
||||
|
||||
XmpApp::State state_ = XmpApp::State::kIdle;
|
||||
bool xmp_override_ = false;
|
||||
XmpApp::PlaybackClient playback_client_ = XmpApp::PlaybackClient::kSystem;
|
||||
XmpApp::PlaybackMode playback_mode_ = XmpApp::PlaybackMode::kInOrder;
|
||||
XmpApp::RepeatMode repeat_mode_ = XmpApp::RepeatMode::kPlaylist;
|
||||
XmpApp::PlaybackFlags playback_flags_ = XmpApp::PlaybackFlags::kDefault;
|
||||
PlaybackController playback_controller_ = PlaybackController::Game;
|
||||
XMP_CLIENT xmp_client_ = XMP_CLIENT::Game;
|
||||
std::atomic<float> volume_ = 0.0f;
|
||||
uint32_t dash_init_state = 0;
|
||||
|
||||
|
||||
@@ -248,14 +248,6 @@ void AudioSystem::SubmitFrame(size_t index, float* samples) {
|
||||
"(in_use={}, driver={:p})",
|
||||
index, index < kMaximumClientCount ? clients_[index].in_use : false,
|
||||
index < kMaximumClientCount ? (void*)clients_[index].driver : nullptr);
|
||||
|
||||
// Submit silence instead of dropping the frame to maintain the callback
|
||||
// chain. If we don't submit anything, the audio driver's OnBufferEnd
|
||||
// callback will never fire, causing the semaphore to leak.
|
||||
if (index < kMaximumClientCount && clients_[index].driver) {
|
||||
static float silence[apu::AudioDriver::kFrameSamplesMax] = {0};
|
||||
(clients_[index].driver)->SubmitFrame(silence);
|
||||
}
|
||||
return;
|
||||
}
|
||||
(clients_[index].driver)->SubmitFrame(samples);
|
||||
|
||||
@@ -1,391 +0,0 @@
|
||||
/**
|
||||
******************************************************************************
|
||||
* Xenia : Xbox 360 Emulator Research Project *
|
||||
******************************************************************************
|
||||
* Copyright 2026 Ben Vanik. All rights reserved. *
|
||||
* Released under the BSD license - see LICENSE in the root for more details. *
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#include "third_party/catch/include/catch.hpp"
|
||||
|
||||
#include "xenia/memory.h"
|
||||
|
||||
namespace xe {
|
||||
namespace test {
|
||||
|
||||
// Helper to create a VirtualHeap for testing without a full Memory instance.
|
||||
// Uses reserve-only allocations to avoid needing real host memory mappings.
|
||||
class TestHeap {
|
||||
public:
|
||||
TestHeap(uint32_t heap_base, uint32_t heap_size, uint32_t page_size) {
|
||||
heap_.Initialize(nullptr, nullptr, HeapType::kGuestXex, heap_base,
|
||||
heap_size, page_size);
|
||||
}
|
||||
|
||||
~TestHeap() {
|
||||
// Don't call Dispose — it tries to DeallocFixed on nullptr membase.
|
||||
}
|
||||
|
||||
VirtualHeap& heap() { return heap_; }
|
||||
|
||||
// Reserve-only allocation (skips host memory commit).
|
||||
bool Alloc(uint32_t size, uint32_t alignment, bool top_down,
|
||||
uint32_t* out_address) {
|
||||
return heap_.AllocRange(heap_.heap_base(),
|
||||
heap_.heap_base() + heap_.heap_size() - 1, size,
|
||||
alignment, kMemoryAllocationReserve,
|
||||
kMemoryProtectRead, top_down, out_address);
|
||||
}
|
||||
|
||||
bool AllocRange(uint32_t low, uint32_t high, uint32_t size,
|
||||
uint32_t alignment, bool top_down, uint32_t* out_address) {
|
||||
return heap_.AllocRange(low, high, size, alignment,
|
||||
kMemoryAllocationReserve, kMemoryProtectRead,
|
||||
top_down, out_address);
|
||||
}
|
||||
|
||||
bool AllocFixed(uint32_t base_address, uint32_t size) {
|
||||
return heap_.AllocFixed(base_address, size, heap_.page_size(),
|
||||
kMemoryAllocationReserve, kMemoryProtectRead);
|
||||
}
|
||||
|
||||
bool Release(uint32_t address) { return heap_.Release(address); }
|
||||
|
||||
uint32_t unreserved_page_count() const {
|
||||
return heap_.unreserved_page_count();
|
||||
}
|
||||
|
||||
uint32_t total_page_count() const { return heap_.total_page_count(); }
|
||||
|
||||
private:
|
||||
VirtualHeap heap_;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Basic allocation and release
|
||||
// ============================================================================
|
||||
|
||||
TEST_CASE("heap_alloc_basic", "[heap]") {
|
||||
// 1MB heap, 4KB pages = 256 pages
|
||||
TestHeap h(0x80000000, 0x100000, 0x1000);
|
||||
REQUIRE(h.total_page_count() == 256);
|
||||
REQUIRE(h.unreserved_page_count() == 256);
|
||||
|
||||
uint32_t addr = 0;
|
||||
REQUIRE(h.Alloc(0x1000, 0x1000, false, &addr));
|
||||
REQUIRE(addr == 0x80000000);
|
||||
REQUIRE(h.unreserved_page_count() == 255);
|
||||
|
||||
REQUIRE(h.Alloc(0x2000, 0x1000, false, &addr));
|
||||
REQUIRE(addr == 0x80001000);
|
||||
REQUIRE(h.unreserved_page_count() == 253);
|
||||
}
|
||||
|
||||
TEST_CASE("heap_alloc_top_down", "[heap]") {
|
||||
TestHeap h(0x80000000, 0x100000, 0x1000);
|
||||
|
||||
// Top-down treats high_page_number as exclusive, so the top page is
|
||||
// never handed out.
|
||||
uint32_t addr = 0;
|
||||
REQUIRE(h.Alloc(0x1000, 0x1000, true, &addr));
|
||||
REQUIRE(addr == 0x800FE000);
|
||||
REQUIRE(h.unreserved_page_count() == 255);
|
||||
|
||||
REQUIRE(h.Alloc(0x2000, 0x1000, true, &addr));
|
||||
REQUIRE(addr == 0x800FC000);
|
||||
REQUIRE(h.unreserved_page_count() == 253);
|
||||
}
|
||||
|
||||
TEST_CASE("heap_alloc_release", "[heap]") {
|
||||
TestHeap h(0x80000000, 0x100000, 0x1000);
|
||||
|
||||
uint32_t addr1 = 0, addr2 = 0;
|
||||
REQUIRE(h.Alloc(0x4000, 0x1000, false, &addr1));
|
||||
REQUIRE(h.Alloc(0x4000, 0x1000, false, &addr2));
|
||||
REQUIRE(addr1 == 0x80000000);
|
||||
REQUIRE(addr2 == 0x80004000);
|
||||
REQUIRE(h.unreserved_page_count() == 248);
|
||||
|
||||
REQUIRE(h.Release(addr1));
|
||||
REQUIRE(h.unreserved_page_count() == 252);
|
||||
|
||||
REQUIRE(h.Release(addr2));
|
||||
REQUIRE(h.unreserved_page_count() == 256);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Coalescing
|
||||
// ============================================================================
|
||||
|
||||
TEST_CASE("heap_coalesce_adjacent_releases", "[heap]") {
|
||||
TestHeap h(0x80000000, 0x100000, 0x1000);
|
||||
|
||||
// Allocate 3 adjacent 4-page blocks.
|
||||
uint32_t a1 = 0, a2 = 0, a3 = 0;
|
||||
REQUIRE(h.Alloc(0x4000, 0x1000, false, &a1));
|
||||
REQUIRE(h.Alloc(0x4000, 0x1000, false, &a2));
|
||||
REQUIRE(h.Alloc(0x4000, 0x1000, false, &a3));
|
||||
REQUIRE(a1 == 0x80000000);
|
||||
REQUIRE(a2 == 0x80004000);
|
||||
REQUIRE(a3 == 0x80008000);
|
||||
|
||||
// Release middle block, then adjacent blocks — should coalesce.
|
||||
REQUIRE(h.Release(a2));
|
||||
REQUIRE(h.Release(a1));
|
||||
REQUIRE(h.Release(a3));
|
||||
|
||||
// All freed. Now allocate a 12-page block — should succeed in the
|
||||
// coalesced free region.
|
||||
uint32_t big = 0;
|
||||
REQUIRE(h.Alloc(0xC000, 0x1000, false, &big));
|
||||
REQUIRE(big == 0x80000000);
|
||||
}
|
||||
|
||||
TEST_CASE("heap_coalesce_merge_before", "[heap]") {
|
||||
TestHeap h(0x80000000, 0x100000, 0x1000);
|
||||
|
||||
uint32_t a1 = 0, a2 = 0;
|
||||
REQUIRE(h.Alloc(0x4000, 0x1000, false, &a1));
|
||||
REQUIRE(h.Alloc(0x4000, 0x1000, false, &a2));
|
||||
|
||||
// Release first, then second — second should merge with first.
|
||||
REQUIRE(h.Release(a1));
|
||||
REQUIRE(h.Release(a2));
|
||||
|
||||
uint32_t big = 0;
|
||||
REQUIRE(h.Alloc(0x8000, 0x1000, false, &big));
|
||||
REQUIRE(big == 0x80000000);
|
||||
}
|
||||
|
||||
TEST_CASE("heap_coalesce_merge_after", "[heap]") {
|
||||
TestHeap h(0x80000000, 0x100000, 0x1000);
|
||||
|
||||
uint32_t a1 = 0, a2 = 0;
|
||||
REQUIRE(h.Alloc(0x4000, 0x1000, false, &a1));
|
||||
REQUIRE(h.Alloc(0x4000, 0x1000, false, &a2));
|
||||
|
||||
// Release second, then first — first should merge with second.
|
||||
REQUIRE(h.Release(a2));
|
||||
REQUIRE(h.Release(a1));
|
||||
|
||||
uint32_t big = 0;
|
||||
REQUIRE(h.Alloc(0x8000, 0x1000, false, &big));
|
||||
REQUIRE(big == 0x80000000);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Fragmentation resistance
|
||||
// ============================================================================
|
||||
|
||||
TEST_CASE("heap_fragmentation_reuse", "[heap]") {
|
||||
// 80KB heap, 4KB pages = 20 pages
|
||||
TestHeap h(0x80000000, 0x14000, 0x1000);
|
||||
|
||||
// Allocate 4 x 4-page blocks (uses 16 of 20 pages).
|
||||
uint32_t a[4];
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
REQUIRE(h.Alloc(0x4000, 0x1000, false, &a[i]));
|
||||
}
|
||||
REQUIRE(h.unreserved_page_count() == 4);
|
||||
|
||||
// Release alternating blocks to fragment.
|
||||
REQUIRE(h.Release(a[0])); // free pages 0-3
|
||||
REQUIRE(h.Release(a[2])); // free pages 8-11
|
||||
|
||||
// Can't allocate 5 pages (no single contiguous block of 5 in gaps).
|
||||
uint32_t fail_addr = 0;
|
||||
REQUIRE_FALSE(h.Alloc(0x5000, 0x1000, false, &fail_addr));
|
||||
|
||||
// Can allocate 4 pages (fits in either free gap).
|
||||
uint32_t ok_addr = 0;
|
||||
REQUIRE(h.Alloc(0x4000, 0x1000, false, &ok_addr));
|
||||
REQUIRE(ok_addr == 0x80000000); // bottom-up, first fit.
|
||||
|
||||
// Release remaining to defragment.
|
||||
REQUIRE(h.Release(a[1]));
|
||||
REQUIRE(h.Release(a[3]));
|
||||
REQUIRE(h.Release(ok_addr));
|
||||
|
||||
// Now 20 pages should be available as one contiguous block.
|
||||
REQUIRE(h.unreserved_page_count() == 20);
|
||||
uint32_t big = 0;
|
||||
REQUIRE(h.Alloc(0xC000, 0x1000, false, &big));
|
||||
REQUIRE(big == 0x80000000);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Alignment
|
||||
// ============================================================================
|
||||
|
||||
TEST_CASE("heap_alloc_alignment", "[heap]") {
|
||||
// 1MB heap, 4KB pages
|
||||
TestHeap h(0x80000000, 0x100000, 0x1000);
|
||||
|
||||
// Allocate 1 page to offset the next allocation.
|
||||
uint32_t first = 0;
|
||||
REQUIRE(h.Alloc(0x1000, 0x1000, false, &first));
|
||||
REQUIRE(first == 0x80000000);
|
||||
|
||||
// Allocate with 64KB alignment — should skip to 0x80010000.
|
||||
uint32_t aligned = 0;
|
||||
REQUIRE(h.Alloc(0x1000, 0x10000, false, &aligned));
|
||||
REQUIRE((aligned % 0x10000) == 0);
|
||||
REQUIRE(aligned == 0x80010000);
|
||||
}
|
||||
|
||||
TEST_CASE("heap_alloc_alignment_top_down", "[heap]") {
|
||||
// 1MB heap, 4KB pages
|
||||
TestHeap h(0x80000000, 0x100000, 0x1000);
|
||||
|
||||
// Top-down skips the top page (0x800FF000), so a 1-page allocation
|
||||
// lands on page 0xFE.
|
||||
uint32_t first = 0;
|
||||
REQUIRE(h.Alloc(0x1000, 0x1000, true, &first));
|
||||
REQUIRE(first == 0x800FE000);
|
||||
|
||||
// 64KB-aligned top-down: stride 16, exclusive high at page 0xFF, so
|
||||
// the highest aligned base is page 0xE0.
|
||||
uint32_t aligned = 0;
|
||||
REQUIRE(h.Alloc(0x1000, 0x10000, true, &aligned));
|
||||
REQUIRE((aligned % 0x10000) == 0);
|
||||
REQUIRE(aligned == 0x800E0000);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// AllocFixed
|
||||
// ============================================================================
|
||||
|
||||
TEST_CASE("heap_alloc_fixed", "[heap]") {
|
||||
TestHeap h(0x80000000, 0x100000, 0x1000);
|
||||
|
||||
REQUIRE(h.AllocFixed(0x80010000, 0x4000));
|
||||
REQUIRE(h.unreserved_page_count() == 252);
|
||||
|
||||
// Allocate bottom-up — should get 0x80000000 (before the fixed alloc).
|
||||
uint32_t addr = 0;
|
||||
REQUIRE(h.Alloc(0x1000, 0x1000, false, &addr));
|
||||
REQUIRE(addr == 0x80000000);
|
||||
|
||||
// Allocating at the same fixed address should fail (already reserved).
|
||||
REQUIRE_FALSE(h.AllocFixed(0x80010000, 0x1000));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Range allocation
|
||||
// ============================================================================
|
||||
|
||||
TEST_CASE("heap_alloc_range", "[heap]") {
|
||||
TestHeap h(0x80000000, 0x100000, 0x1000);
|
||||
|
||||
// Allocate in a specific sub-range.
|
||||
uint32_t addr = 0;
|
||||
REQUIRE(h.AllocRange(0x80080000, 0x800FFFFF, 0x4000, 0x1000, false, &addr));
|
||||
REQUIRE(addr >= 0x80080000);
|
||||
REQUIRE(addr + 0x4000 <= 0x80100000);
|
||||
}
|
||||
|
||||
TEST_CASE("heap_alloc_range_exhaustion", "[heap]") {
|
||||
// 64KB heap, 4KB pages = 16 pages
|
||||
TestHeap h(0x80000000, 0x10000, 0x1000);
|
||||
|
||||
// Fill the lower half.
|
||||
REQUIRE(h.AllocFixed(0x80000000, 0x8000));
|
||||
|
||||
// Try to allocate in the lower half — should fail.
|
||||
// Use page-aligned high address so xe::align doesn't extend the range.
|
||||
uint32_t addr = 0;
|
||||
REQUIRE_FALSE(
|
||||
h.AllocRange(0x80000000, 0x80007000, 0x1000, 0x1000, false, &addr));
|
||||
|
||||
// Allocate in the upper half — should succeed.
|
||||
REQUIRE(h.AllocRange(0x80008000, 0x8000F000, 0x1000, 0x1000, false, &addr));
|
||||
REQUIRE(addr >= 0x80008000);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Reset
|
||||
// ============================================================================
|
||||
|
||||
TEST_CASE("heap_reset", "[heap]") {
|
||||
TestHeap h(0x80000000, 0x100000, 0x1000);
|
||||
|
||||
// Fill most of the heap.
|
||||
uint32_t addr = 0;
|
||||
while (h.Alloc(0x1000, 0x1000, false, &addr)) {
|
||||
}
|
||||
|
||||
// Reset should restore all pages.
|
||||
h.heap().Reset();
|
||||
REQUIRE(h.unreserved_page_count() == 256);
|
||||
|
||||
// Should be able to allocate a large block again.
|
||||
REQUIRE(h.Alloc(0xF0000, 0x1000, false, &addr));
|
||||
REQUIRE(addr == 0x80000000);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Stress: many alloc/release cycles
|
||||
// ============================================================================
|
||||
|
||||
TEST_CASE("heap_stress_alloc_release", "[heap]") {
|
||||
// 272KB heap, 4KB pages = 68 pages (extra pages avoid off-by-one in range
|
||||
// check for full-heap-sized allocations).
|
||||
TestHeap h(0x80000000, 0x44000, 0x1000);
|
||||
|
||||
// Allocate 16 x 4-page blocks (uses 64 of 68 pages).
|
||||
uint32_t addrs[16];
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
REQUIRE(h.Alloc(0x4000, 0x1000, false, &addrs[i]));
|
||||
}
|
||||
REQUIRE(h.unreserved_page_count() == 4);
|
||||
|
||||
// Release all odd-indexed blocks.
|
||||
for (int i = 1; i < 16; i += 2) {
|
||||
REQUIRE(h.Release(addrs[i]));
|
||||
}
|
||||
REQUIRE(h.unreserved_page_count() == 36);
|
||||
|
||||
// Re-allocate 4-page blocks into the gaps.
|
||||
for (int i = 1; i < 16; i += 2) {
|
||||
REQUIRE(h.Alloc(0x4000, 0x1000, false, &addrs[i]));
|
||||
}
|
||||
REQUIRE(h.unreserved_page_count() == 4);
|
||||
|
||||
// Release everything.
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
REQUIRE(h.Release(addrs[i]));
|
||||
}
|
||||
REQUIRE(h.unreserved_page_count() == 68);
|
||||
|
||||
// 64-page allocation should succeed after full release (coalesced).
|
||||
uint32_t full = 0;
|
||||
REQUIRE(h.Alloc(0x40000, 0x1000, false, &full));
|
||||
REQUIRE(full == 0x80000000);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 64KB page heap (like v40000000)
|
||||
// ============================================================================
|
||||
|
||||
TEST_CASE("heap_64k_pages", "[heap]") {
|
||||
// 4MB heap, 64KB pages = 64 pages
|
||||
TestHeap h(0x40000000, 0x400000, 0x10000);
|
||||
REQUIRE(h.total_page_count() == 64);
|
||||
|
||||
uint32_t addr = 0;
|
||||
REQUIRE(h.Alloc(0x10000, 0x10000, false, &addr));
|
||||
REQUIRE(addr == 0x40000000);
|
||||
REQUIRE(h.unreserved_page_count() == 63);
|
||||
|
||||
REQUIRE(h.Alloc(0x20000, 0x10000, false, &addr));
|
||||
REQUIRE(addr == 0x40010000);
|
||||
REQUIRE(h.unreserved_page_count() == 61);
|
||||
|
||||
REQUIRE(h.Release(0x40000000));
|
||||
REQUIRE(h.Release(0x40010000));
|
||||
REQUIRE(h.unreserved_page_count() == 64);
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
} // namespace xe
|
||||
@@ -162,11 +162,13 @@ TEST_CASE("PhysicalHeap vE0000000 alignment", "[memory]") {
|
||||
}
|
||||
|
||||
SECTION("alloc with alignment larger than page_size is rejected") {
|
||||
// vE0000000 has a 0x1000 physical translation offset, so a 64KB
|
||||
// alignment request can't produce a 64KB-aligned guest address.
|
||||
// PhysicalHeap::Alloc forces top-down, which here lands one stride
|
||||
// past the end of the child heap and BaseHeap::AllocFixed rejects
|
||||
// it as out of range.
|
||||
// The translation offset (0xDFFFF000) is only 4KB-aligned, so a
|
||||
// 64KB alignment request produces a misaligned translated address.
|
||||
//
|
||||
// Without the fix: BaseHeap::AllocFixed receives a misaligned address,
|
||||
// hitting assert_true in debug or silently corrupting in release.
|
||||
// With the fix: PhysicalHeap::Alloc detects the misalignment and
|
||||
// returns false cleanly.
|
||||
uint32_t alignment = 0x10000; // 64KB
|
||||
uint32_t addr = 0;
|
||||
bool ok = heap.Alloc(0x10000, alignment, kMemoryAllocationReserve,
|
||||
@@ -193,19 +195,15 @@ TEST_CASE("PhysicalHeap vE0000000 AllocRange alignment", "[memory]") {
|
||||
REQUIRE(addr % 0x1000 == 0);
|
||||
}
|
||||
|
||||
SECTION("AllocRange with large alignment succeeds via bottom-up") {
|
||||
// Bottom-up search picks a low parent address that translates to a
|
||||
// guest address inside the child heap, so BaseHeap::AllocFixed accepts
|
||||
// it. The PhysicalHeap alignment check is host-based
|
||||
// ((addr + host_address_offset_) % alignment), so the misalignment of
|
||||
// the guest address itself is not rejected here.
|
||||
SECTION("AllocRange rejects misaligned translation") {
|
||||
// Same scenario as Alloc: 64KB alignment on a heap whose translation
|
||||
// offset (0xDFFFF000) is not 64KB-aligned.
|
||||
uint32_t alignment = 0x10000;
|
||||
uint32_t addr = 0;
|
||||
bool ok = heap.AllocRange(0xE0000000, 0xFFFCFFFF, 0x10000, alignment,
|
||||
kMemoryAllocationReserve, kMemoryProtectRead,
|
||||
false, &addr);
|
||||
REQUIRE(ok);
|
||||
REQUIRE(addr >= 0xE0000000);
|
||||
REQUIRE_FALSE(ok);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
#include <sched.h>
|
||||
#include <semaphore.h>
|
||||
#include <signal.h>
|
||||
#include <sys/resource.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <unistd.h>
|
||||
#include <array>
|
||||
@@ -612,7 +611,6 @@ class PosixCondition<Thread> final : public PosixConditionBase {
|
||||
/// Thread::GetCurrentThread() on the main thread
|
||||
explicit PosixCondition(pthread_t thread)
|
||||
: thread_(thread),
|
||||
tid_(static_cast<pid_t>(syscall(SYS_gettid))),
|
||||
signaled_(false),
|
||||
exit_code_(0),
|
||||
state_(State::kRunning),
|
||||
@@ -744,48 +742,31 @@ class PosixCondition<Thread> final : public PosixConditionBase {
|
||||
|
||||
int priority() const {
|
||||
WaitStarted();
|
||||
if (!fifo_failed_) {
|
||||
int policy;
|
||||
sched_param param{};
|
||||
int ret = pthread_getschedparam(thread_, &policy, ¶m);
|
||||
if (ret != 0) {
|
||||
return -1;
|
||||
}
|
||||
return param.sched_priority;
|
||||
int policy;
|
||||
sched_param param{};
|
||||
int ret = pthread_getschedparam(thread_, &policy, ¶m);
|
||||
if (ret != 0) {
|
||||
return -1;
|
||||
}
|
||||
// When using nice values, map back to the SCHED_FIFO range (1-32)
|
||||
// so callers see a consistent priority space.
|
||||
int nice_val = getpriority(PRIO_PROCESS, tid_);
|
||||
// nice -19..19 → fifo 32..1
|
||||
return 16 - nice_val;
|
||||
|
||||
return param.sched_priority;
|
||||
}
|
||||
|
||||
void set_priority(int new_priority) const {
|
||||
WaitStarted();
|
||||
if (!fifo_failed_) {
|
||||
// Try real-time SCHED_FIFO for best priority control.
|
||||
sched_param param{};
|
||||
param.sched_priority = new_priority;
|
||||
int res = pthread_setschedparam(thread_, SCHED_FIFO, ¶m);
|
||||
if (res == 0) {
|
||||
return;
|
||||
sched_param param{};
|
||||
param.sched_priority = new_priority;
|
||||
int res = pthread_setschedparam(thread_, SCHED_FIFO, ¶m);
|
||||
if (res != 0) {
|
||||
switch (res) {
|
||||
case EPERM:
|
||||
XELOGW("Permission denied while setting priority");
|
||||
break;
|
||||
case EINVAL:
|
||||
assert_always();
|
||||
default:
|
||||
XELOGW("Unknown error while setting priority");
|
||||
}
|
||||
if (res == EPERM) {
|
||||
fifo_failed_ = true;
|
||||
} else {
|
||||
XELOGW("Unexpected error {} while setting SCHED_FIFO priority", res);
|
||||
fifo_failed_ = true;
|
||||
}
|
||||
}
|
||||
// Fall back to nice values under SCHED_OTHER.
|
||||
// Map SCHED_FIFO range (1-32) to nice range (19 to -19).
|
||||
// Center: fifo 16 → nice 0.
|
||||
int nice_val = 16 - new_priority;
|
||||
// Clamp to valid nice range.
|
||||
if (nice_val < -20) nice_val = -20;
|
||||
if (nice_val > 19) nice_val = 19;
|
||||
if (tid_ > 0) {
|
||||
setpriority(PRIO_PROCESS, tid_, nice_val);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -949,8 +930,6 @@ class PosixCondition<Thread> final : public PosixConditionBase {
|
||||
sem_destroy(&suspend_sem_);
|
||||
}
|
||||
pthread_t thread_;
|
||||
pid_t tid_ = 0; // Kernel TID for setpriority() fallback
|
||||
mutable bool fifo_failed_ = false; // True after SCHED_FIFO was rejected
|
||||
bool signaled_;
|
||||
int exit_code_;
|
||||
State state_; // Protected by state_mutex_
|
||||
@@ -1264,7 +1243,6 @@ void* PosixCondition<Thread>::ThreadStartRoutine(void* parameter) {
|
||||
delete start_data;
|
||||
|
||||
current_thread_ = thread;
|
||||
thread->handle_.tid_ = static_cast<pid_t>(syscall(SYS_gettid));
|
||||
{
|
||||
std::unique_lock lock(thread->handle_.state_mutex_);
|
||||
thread->handle_.state_ =
|
||||
|
||||
@@ -13,8 +13,6 @@
|
||||
|
||||
#include <climits>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "third_party/fmt/include/fmt/format.h"
|
||||
#include "xenia/base/assert.h"
|
||||
@@ -65,111 +63,6 @@ DEFINE_bool(instrument_call_times, false,
|
||||
"Compute time taken for functions, for profiling guest code",
|
||||
"x64");
|
||||
#endif
|
||||
|
||||
// AUDIT-061/067: forward decls of probe/watch tables (defined in
|
||||
// ppc_hir_builder.cc).
|
||||
namespace xe {
|
||||
namespace cpu {
|
||||
namespace audit61 {
|
||||
const std::vector<uint32_t>& pcs();
|
||||
} // namespace audit61
|
||||
namespace audit67 {
|
||||
const std::vector<uint32_t>& vals();
|
||||
} // namespace audit67
|
||||
namespace hashprobe {
|
||||
const std::vector<uint32_t>& pcs();
|
||||
} // namespace hashprobe
|
||||
} // namespace cpu
|
||||
} // namespace xe
|
||||
|
||||
// AUDIT-061: handler for trap codes [200, 232). arg0 carries trap idx
|
||||
// (trap_code - 200), mapping to ::xe::cpu::audit61::pcs()[idx]. Emits one
|
||||
// log line per fire with cr0/cr6 LGE flags + key GPRs + LR + tid.
|
||||
static uint64_t TrapAudit61Branch(void* raw_context, uint64_t idx) {
|
||||
auto* ctx = reinterpret_cast<xe::cpu::ppc::PPCContext_s*>(raw_context);
|
||||
const auto& pcs = ::xe::cpu::audit61::pcs();
|
||||
uint32_t pc = (idx < pcs.size()) ? pcs[static_cast<size_t>(idx)] : 0u;
|
||||
uint32_t tid = 0;
|
||||
if (ctx->thread_state) {
|
||||
tid = ctx->thread_state->thread_id();
|
||||
}
|
||||
auto enc = [](uint8_t lt, uint8_t gt, uint8_t eq) {
|
||||
char buf[4];
|
||||
buf[0] = lt ? 'L' : '.';
|
||||
buf[1] = gt ? 'G' : '.';
|
||||
buf[2] = eq ? 'E' : '.';
|
||||
buf[3] = '\0';
|
||||
return std::string(buf);
|
||||
};
|
||||
XELOGI(
|
||||
"AUDIT-061-BR pc={:08X} lr={:08X} cr0={} cr6={} r3={:08X} r4={:08X} "
|
||||
"r5={:08X} r6={:08X} r31={:08X} tid={}",
|
||||
pc, static_cast<uint32_t>(ctx->lr),
|
||||
enc(ctx->cr0.cr0_lt, ctx->cr0.cr0_gt, ctx->cr0.cr0_eq),
|
||||
enc(ctx->cr6.cr6_all_equal, ctx->cr6.cr6_1, ctx->cr6.cr6_none_equal),
|
||||
static_cast<uint32_t>(ctx->r[3]), static_cast<uint32_t>(ctx->r[4]),
|
||||
static_cast<uint32_t>(ctx->r[5]), static_cast<uint32_t>(ctx->r[6]),
|
||||
static_cast<uint32_t>(ctx->r[31]), tid);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// AUDIT-067: handler for trap codes [250, 254). arg0 carries trap idx
|
||||
// (trap_code - 250), mapping to ::xe::cpu::audit67::vals()[idx]. Fired when
|
||||
// a 4-byte guest store sees the configured value. The store-emit site stashed
|
||||
// (pc << 32) | (ea & 0xFFFFFFFF) into ctx->scratch right before the trap.
|
||||
static uint64_t TrapAudit67ValueWatch(void* raw_context, uint64_t idx) {
|
||||
auto* ctx = reinterpret_cast<xe::cpu::ppc::PPCContext_s*>(raw_context);
|
||||
const auto& vals = ::xe::cpu::audit67::vals();
|
||||
uint32_t val =
|
||||
(idx < vals.size()) ? vals[static_cast<size_t>(idx)] : 0u;
|
||||
uint32_t pc = static_cast<uint32_t>(ctx->scratch >> 32);
|
||||
uint32_t dst = static_cast<uint32_t>(ctx->scratch & 0xFFFFFFFFu);
|
||||
uint32_t tid = 0;
|
||||
if (ctx->thread_state) {
|
||||
tid = ctx->thread_state->thread_id();
|
||||
}
|
||||
XELOGI(
|
||||
"AUDIT-067-VAL pc={:08X} lr={:08X} val={:08X} dst={:08X} "
|
||||
"r3={:08X} r4={:08X} r5={:08X} r6={:08X} r31={:08X} tid={}",
|
||||
pc, static_cast<uint32_t>(ctx->lr), val, dst,
|
||||
static_cast<uint32_t>(ctx->r[3]), static_cast<uint32_t>(ctx->r[4]),
|
||||
static_cast<uint32_t>(ctx->r[5]), static_cast<uint32_t>(ctx->r[6]),
|
||||
static_cast<uint32_t>(ctx->r[31]), tid);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// PHASE-A hash probe: trap codes [300, 332). arg0 carries trap idx
|
||||
// (trap_code - 300) -> ::xe::cpu::hashprobe::pcs()[idx]. Derefs r3 as a guest
|
||||
// C-string (the archive path being hashed) and logs it with r3..r6 + LR + tid.
|
||||
// Log-only; observes the same code path the retail binary runs.
|
||||
static uint64_t TrapPhaseAHashProbe(void* raw_context, uint64_t idx) {
|
||||
auto* ctx = reinterpret_cast<xe::cpu::ppc::PPCContext_s*>(raw_context);
|
||||
const auto& pcs = ::xe::cpu::hashprobe::pcs();
|
||||
uint32_t pc = (idx < pcs.size()) ? pcs[static_cast<size_t>(idx)] : 0u;
|
||||
uint32_t r3 = static_cast<uint32_t>(ctx->r[3]);
|
||||
// Deref r3 as a guest C-string via the flat virtual mapping (bounded).
|
||||
std::string arg;
|
||||
if (r3 && ctx->virtual_membase) {
|
||||
const char* p =
|
||||
reinterpret_cast<const char*>(ctx->virtual_membase) + r3;
|
||||
for (size_t i = 0; i < 128 && p[i]; ++i) {
|
||||
char c = p[i];
|
||||
arg.push_back((c >= 0x20 && c < 0x7f) ? c : '.');
|
||||
}
|
||||
}
|
||||
uint32_t tid = 0;
|
||||
if (ctx->thread_state) {
|
||||
tid = ctx->thread_state->thread_id();
|
||||
}
|
||||
XELOGI(
|
||||
"PHASE-A-HASHPROBE pc={:08X} lr={:08X} arg=\"{}\" r3={:08X} r4={:08X} "
|
||||
"r5={:08X} r6={:08X} tid={}",
|
||||
pc, static_cast<uint32_t>(ctx->lr), arg, r3,
|
||||
static_cast<uint32_t>(ctx->r[4]), static_cast<uint32_t>(ctx->r[5]),
|
||||
static_cast<uint32_t>(ctx->r[6]), tid);
|
||||
return 0;
|
||||
}
|
||||
|
||||
namespace xe {
|
||||
namespace cpu {
|
||||
namespace backend {
|
||||
@@ -562,27 +455,6 @@ void X64Emitter::Trap(uint16_t trap_type) {
|
||||
// ?
|
||||
break;
|
||||
default:
|
||||
// AUDIT-067: trap codes [250, 254) dispatch the value-watch handler.
|
||||
// arg0 = idx into ::xe::cpu::audit67::vals().
|
||||
if (trap_type >= 250 && trap_type < 254) {
|
||||
CallNative(::TrapAudit67ValueWatch,
|
||||
static_cast<uint64_t>(trap_type - 250));
|
||||
break;
|
||||
}
|
||||
// AUDIT-061: trap codes [200, 232) dispatch the branch-probe handler.
|
||||
// arg0 = idx into ::xe::cpu::audit61::pcs().
|
||||
if (trap_type >= 200 && trap_type < 232) {
|
||||
CallNative(::TrapAudit61Branch,
|
||||
static_cast<uint64_t>(trap_type - 200));
|
||||
break;
|
||||
}
|
||||
// PHASE-A hash probe: trap codes [300, 332).
|
||||
// arg0 = idx into ::xe::cpu::hashprobe::pcs().
|
||||
if (trap_type >= 300 && trap_type < 332) {
|
||||
CallNative(::TrapPhaseAHashProbe,
|
||||
static_cast<uint64_t>(trap_type - 300));
|
||||
break;
|
||||
}
|
||||
XELOGW("Unknown trap type {}", trap_type);
|
||||
db(0xCC);
|
||||
break;
|
||||
|
||||
@@ -934,47 +934,8 @@ struct VECTOR_SHL_V128
|
||||
static void EmitInt8(X64Emitter& e, const EmitArgType& i) {
|
||||
// TODO(benvanik): native version (with shift magic).
|
||||
|
||||
// gf2p8mulb's "x8 + x4 + x3 + x + 1"-polynomial-reduction only
|
||||
// applies when the multiplication overflows. Masking away any bits
|
||||
// that would have overflowed turns the polynomial-multiplication into
|
||||
// regular modulo-multiplication
|
||||
const uint64_t gfni_shift_mask = UINT64_C(0x01'03'07'0f'1f'3f'7f'ff);
|
||||
// n << 0 == n * 1 | n << 1 == n * 2 | n << 2 == n * 4 | etc
|
||||
const uint64_t gfni_multiply_table = UINT64_C(0x80'40'20'10'08'04'02'01);
|
||||
|
||||
if (e.IsFeatureEnabled(kX64EmitAVX2)) {
|
||||
if (!i.src2.is_constant) {
|
||||
if (e.IsFeatureEnabled(kX64EmitGFNI | kX64EmitAVX512Ortho |
|
||||
kX64EmitAVX512VBMI)) {
|
||||
e.LoadConstantXmm(e.xmm0, vec128q(gfni_shift_mask, gfni_shift_mask));
|
||||
e.vpermb(e.xmm0, i.src2, e.xmm0);
|
||||
e.vpand(e.xmm0, i.src1, e.xmm0);
|
||||
|
||||
e.LoadConstantXmm(e.xmm1,
|
||||
vec128q(gfni_multiply_table, gfni_multiply_table));
|
||||
e.vpermb(e.xmm1, i.src2, e.xmm1);
|
||||
|
||||
e.vgf2p8mulb(i.dest, e.xmm0, e.xmm1);
|
||||
return;
|
||||
} else if (e.IsFeatureEnabled(kX64EmitGFNI)) {
|
||||
// Only use the lower 4 bits
|
||||
// This also protects from vpshufb from writing zero when the MSB is
|
||||
// set
|
||||
e.LoadConstantXmm(e.xmm0, vec128b(0x0F));
|
||||
e.vpand(e.xmm2, i.src2, e.xmm0);
|
||||
|
||||
e.LoadConstantXmm(e.xmm0, vec128q(gfni_shift_mask, gfni_shift_mask));
|
||||
e.vpshufb(e.xmm0, e.xmm0, e.xmm2);
|
||||
e.vpand(e.xmm0, i.src1, e.xmm0);
|
||||
|
||||
e.LoadConstantXmm(e.xmm1,
|
||||
vec128q(gfni_multiply_table, gfni_multiply_table));
|
||||
e.vpshufb(e.xmm1, e.xmm1, e.xmm2);
|
||||
|
||||
e.vgf2p8mulb(i.dest, e.xmm0, e.xmm1);
|
||||
return;
|
||||
}
|
||||
|
||||
// get high 8 bytes
|
||||
e.vpunpckhqdq(e.xmm1, i.src1, i.src1);
|
||||
e.vpunpckhqdq(e.xmm3, i.src2, i.src2);
|
||||
@@ -1019,15 +980,6 @@ struct VECTOR_SHL_V128
|
||||
}
|
||||
}
|
||||
if (all_same) {
|
||||
if (e.IsFeatureEnabled(kX64EmitGFNI)) {
|
||||
// Every count is the same, so we can use gf2p8affineqb.
|
||||
const uint8_t shift_amount = seenvalue & 0b111;
|
||||
const uint64_t shift_matrix =
|
||||
UINT64_C(0x0102040810204080) >> (shift_amount * 8);
|
||||
e.vgf2p8affineqb(i.dest, i.src1,
|
||||
e.StashConstantXmm(0, vec128q(shift_matrix)), 0);
|
||||
return;
|
||||
}
|
||||
e.vpmovzxbw(e.ymm0, i.src1);
|
||||
e.vpsllw(e.ymm0, e.ymm0, seenvalue);
|
||||
e.vextracti128(e.xmm1, e.ymm0, 1);
|
||||
@@ -1040,39 +992,6 @@ struct VECTOR_SHL_V128
|
||||
} else {
|
||||
e.LoadConstantXmm(e.xmm2, constmask);
|
||||
|
||||
if (e.IsFeatureEnabled(kX64EmitGFNI | kX64EmitAVX512Ortho |
|
||||
kX64EmitAVX512VBMI)) {
|
||||
e.LoadConstantXmm(e.xmm0,
|
||||
vec128q(gfni_shift_mask, gfni_shift_mask));
|
||||
e.vpermb(e.xmm0, e.xmm2, e.xmm0);
|
||||
e.vpand(e.xmm0, i.src1, e.xmm0);
|
||||
|
||||
e.LoadConstantXmm(
|
||||
e.xmm1, vec128q(gfni_multiply_table, gfni_multiply_table));
|
||||
e.vpermb(e.xmm1, e.xmm2, e.xmm1);
|
||||
|
||||
e.vgf2p8mulb(i.dest, e.xmm0, e.xmm1);
|
||||
return;
|
||||
} else if (e.IsFeatureEnabled(kX64EmitGFNI)) {
|
||||
// Only use the lower 4 bits
|
||||
// This also protects from vpshufb from writing zero when the MSB is
|
||||
// set
|
||||
e.LoadConstantXmm(e.xmm0, vec128b(0x0F));
|
||||
e.vpand(e.xmm2, e.xmm2, e.xmm0);
|
||||
|
||||
e.LoadConstantXmm(e.xmm0,
|
||||
vec128q(gfni_shift_mask, gfni_shift_mask));
|
||||
e.vpshufb(e.xmm0, e.xmm0, e.xmm2);
|
||||
e.vpand(e.xmm0, i.src1, e.xmm0);
|
||||
|
||||
e.LoadConstantXmm(
|
||||
e.xmm1, vec128q(gfni_multiply_table, gfni_multiply_table));
|
||||
e.vpshufb(e.xmm1, e.xmm1, e.xmm2);
|
||||
|
||||
e.vgf2p8mulb(i.dest, e.xmm0, e.xmm1);
|
||||
return;
|
||||
}
|
||||
|
||||
e.vpunpckhqdq(e.xmm1, i.src1, i.src1);
|
||||
e.vpunpckhqdq(e.xmm3, e.xmm2, e.xmm2);
|
||||
|
||||
|
||||
@@ -57,124 +57,3 @@ DEFINE_bool(break_condition_truncate, true, "truncate value to 32-bits", "CPU");
|
||||
|
||||
DEFINE_bool(break_on_debugbreak, true, "int3 on JITed __debugbreak requests.",
|
||||
"CPU");
|
||||
|
||||
// AUDIT-DEMO: smoke marker (memory entry: emulator.cc:225,283). Always-on bool.
|
||||
DEFINE_bool(audit_demo_setup_trace, true,
|
||||
"Audit smoke marker: log AUDIT-DEMO-SETUP-BEGIN at emulator setup.",
|
||||
"Audit");
|
||||
|
||||
// AUDIT-061: comma-separated list of guest PCs to log on each fire.
|
||||
// Format: "0xPC1,0xPC2,..." (max 32 PCs). Each fire emits
|
||||
// AUDIT-061-BR pc=X lr=X cr0=LGE cr6=LGE r3=X r4=X r5=X r6=X r31=X tid=N.
|
||||
// Default empty (off); no perf cost when empty.
|
||||
DEFINE_string(audit_61_branch_probe_pcs, "",
|
||||
"AUDIT-061: CSV of guest PCs to trace (cr0/cr6 + regs/tid).",
|
||||
"Audit");
|
||||
|
||||
// AUDIT-067: comma-separated list of u32 values to watch. When non-empty,
|
||||
// every 4-byte guest store (stw/stwu/stwx/stwux/stmw) emits a runtime
|
||||
// equality check; matches log AUDIT-067-VAL pc=X lr=X val=X dst=X r3..r6 r31 tid=N.
|
||||
// Max 4 values. Default empty (off); zero overhead when empty.
|
||||
DEFINE_string(audit_67_value_watch, "",
|
||||
"AUDIT-067: CSV of u32 values (max 4) — log every guest "
|
||||
"store whose value matches.",
|
||||
"Audit");
|
||||
|
||||
// AUDIT-068: host-side memory-write watch. See cpu_flags.h header for format.
|
||||
// Mirrors AUDIT-067 but covers host-side writes (xe::store_and_swap<T>,
|
||||
// Memory::Zero/Fill/Copy). Empty default = zero cost.
|
||||
DEFINE_string(audit_68_host_mem_watch_values, "",
|
||||
"AUDIT-068: CSV of u32 values (max 8) — log every host-side "
|
||||
"guest-memory write whose value matches.",
|
||||
"Audit");
|
||||
DEFINE_string(audit_68_host_mem_watch_addrs, "",
|
||||
"AUDIT-068: CSV of guest VAs or VA ranges 'START-END' (max 8) "
|
||||
"— log every host-side guest-memory write whose guest VA falls "
|
||||
"within the configured set.",
|
||||
"Audit");
|
||||
|
||||
// AUDIT-068 Session 3: read-mode probe. See cpu_flags.h for format.
|
||||
DEFINE_string(audit_68_host_mem_read_probe, "",
|
||||
"AUDIT-068 Session 3: CSV of 'VA:SIZE:PERIOD_NS' tuples (max 8) "
|
||||
"— a dedicated poll thread reads the value at each VA every "
|
||||
"PERIOD_NS and emits AUDIT-068-READ-CHANGE on transition.",
|
||||
"Audit");
|
||||
|
||||
// AUDIT-069: see cpu_flags.h header. Empty default = zero cost.
|
||||
DEFINE_string(audit_69_event_signal_watch, "",
|
||||
"AUDIT-069: CSV of guest event-handle IDs (max 4) — log each "
|
||||
"XEvent::Set / Ke*Event / Nt*Event fire whose target matches.",
|
||||
"Audit");
|
||||
DEFINE_string(audit_69_event_signal_native_ptr, "",
|
||||
"AUDIT-069: CSV of guest event native VAs (X_KEVENT*) (max 4) "
|
||||
"— log each set fire whose native pointer matches.",
|
||||
"Audit");
|
||||
DEFINE_bool(audit_69_log_all_sets, false,
|
||||
"AUDIT-069: when true, log EVERY XEvent::Set/Pulse fire (used "
|
||||
"for one-run wait→signal correlation across handle drift). "
|
||||
"Default false; use only with --mute=true.",
|
||||
"Audit");
|
||||
|
||||
// AUDIT-070 (S5 of AUDIT-069 family): semaphore-release watch. See header.
|
||||
DEFINE_string(audit_70_semaphore_release_watch, "",
|
||||
"AUDIT-070: CSV of guest semaphore handle IDs (max 4) — log "
|
||||
"each NtReleaseSemaphore / xeKeReleaseSemaphore fire whose "
|
||||
"target matches.",
|
||||
"Audit");
|
||||
DEFINE_bool(audit_70_log_all_releases, false,
|
||||
"AUDIT-070: when true, log EVERY NtReleaseSemaphore / "
|
||||
"xeKeReleaseSemaphore fire (used to identify the work-semaphore "
|
||||
"handle on first run). Default false; use only with --mute=true.",
|
||||
"Audit");
|
||||
|
||||
// Phase A — see kernel/event_log.h.
|
||||
DEFINE_string(phase_a_event_log_path, "",
|
||||
"Phase A: write schema-v1 JSONL event log to this path. "
|
||||
"Empty (default) = disabled.",
|
||||
"Audit");
|
||||
DEFINE_bool(phase_a_event_log_mem_writes, false,
|
||||
"Phase A: include mem.write events in the JSONL log. RESERVED — "
|
||||
"not wired in this phase. Default false.",
|
||||
"Audit");
|
||||
|
||||
// Phase A — expansive extraction tracing (game-data RE). All default-off so the
|
||||
// diff-oriented schema-v1 output stays byte-identical when unused.
|
||||
DEFINE_bool(phase_a_trace_args, false,
|
||||
"Phase A extraction: populate the kernel.call args (raw r3..r10) and "
|
||||
"args_resolved (file I/O path/offset/length/buffer, alloc size, "
|
||||
"handle names) fields, and emit file.read events. Default false.",
|
||||
"Audit");
|
||||
DEFINE_string(phase_a_hash_probe, "",
|
||||
"Phase A extraction: CSV of guest PCs (max 32). At each, deref r3 "
|
||||
"as a guest C-string and emit a guest.call event {pc,arg_str,"
|
||||
"r3..r6,tid} — used to recover the IPFB/IDXD name-hash. Default "
|
||||
"empty (off).",
|
||||
"Audit");
|
||||
|
||||
// Phase D Stage 1 — see kernel/event_log.h `EmitContentionObserved`.
|
||||
DEFINE_bool(kernel_emit_contention, false,
|
||||
"Phase D Stage 1: emit `contention.observed` events when "
|
||||
"RtlEnterCriticalSection's spin loop is exhausted and the call "
|
||||
"falls through to xeKeWaitForSingleObject. Default false (zero "
|
||||
"cost when disabled). Requires --phase_a_event_log_path to be "
|
||||
"set as well.",
|
||||
"Audit");
|
||||
|
||||
// Phase B — see kernel/phase_b_snapshot.h.
|
||||
DEFINE_string(phase_b_snapshot_dir, "",
|
||||
"Phase B: write 5-file structured state snapshot to "
|
||||
"<dir>/canary/ at the moment immediately before the first "
|
||||
"guest PPC instruction of entry_point. Empty (default) = "
|
||||
"disabled, zero overhead.",
|
||||
"Audit");
|
||||
DEFINE_bool(phase_b_snapshot_and_exit, false,
|
||||
"Phase B: after writing the snapshot, exit the process "
|
||||
"immediately (std::_Exit(0)) so re-runs are byte-deterministic.",
|
||||
"Audit");
|
||||
DEFINE_bool(phase_b_dump_section_content, false,
|
||||
"Phase B: in memory.json, populate section_contents[].content_b64 "
|
||||
"with raw bytes of every committed XEX-image region. Default "
|
||||
"false — per-region SHA-256 is enough for the routine diff; "
|
||||
"this is the escape hatch for the STOP-and-report condition "
|
||||
"(image_loaded_sha256 mismatch).",
|
||||
"Audit");
|
||||
|
||||
@@ -35,78 +35,4 @@ DECLARE_bool(break_condition_truncate);
|
||||
|
||||
DECLARE_bool(break_on_debugbreak);
|
||||
|
||||
// AUDIT-DEMO smoke marker.
|
||||
DECLARE_bool(audit_demo_setup_trace);
|
||||
|
||||
// AUDIT-061: multi-PC branch probe — emits one log line per fire with
|
||||
// (pc, lr, cr0 LGE, cr6 LGE, r3, r4, r5, r6, r31, tid). CSV of guest PCs.
|
||||
DECLARE_string(audit_61_branch_probe_pcs);
|
||||
|
||||
// AUDIT-067: value-watch — emit a log line for each 32-bit guest store whose
|
||||
// value-to-be-stored matches any configured value. CSV of u32 values
|
||||
// ("0xDEADBEEF,..."), max 4 entries. Default empty (off); zero cost when empty.
|
||||
DECLARE_string(audit_67_value_watch);
|
||||
|
||||
// AUDIT-068: host-side memory-write watch — emit a log line for each host-side
|
||||
// write to guest memory whose VALUE matches any configured u32 value, or whose
|
||||
// guest VA falls within any configured ADDR or ADDR-range. Mirrors AUDIT-067
|
||||
// but covers the host-side write paths (xe::store_and_swap<T>, Memory::Zero/
|
||||
// Fill/Copy) that AUDIT-067's JIT store-opcode hooks cannot see.
|
||||
//
|
||||
// VALUES: CSV of u32 values, max 8 entries; e.g. "0x8200A208,0x8200A928".
|
||||
// ADDRS: CSV of guest VAs or VA ranges, max 8 entries; range form is
|
||||
// "0xSTART-0xEND" (inclusive). e.g. "0x42500000-0x42600000,0xBCE25340".
|
||||
// Default empty (off); zero cost on the hot path when both are empty.
|
||||
DECLARE_string(audit_68_host_mem_watch_values);
|
||||
DECLARE_string(audit_68_host_mem_watch_addrs);
|
||||
|
||||
// AUDIT-068 Session 3: read-mode probe. CSV of "VA:SIZE:PERIOD_NS" tuples
|
||||
// (max 8). A dedicated low-priority thread polls each VA every PERIOD_NS and
|
||||
// emits AUDIT-068-READ-CHANGE when the value transitions. SIZE in {1,2,4,8}.
|
||||
// Example: "0xBCE25340:4:1000000" = poll u32 at 0xBCE25340 every 1 ms.
|
||||
// Default empty (off); the poll thread is not spawned when empty.
|
||||
DECLARE_string(audit_68_host_mem_read_probe);
|
||||
|
||||
// AUDIT-069: event-signal watch. CSV of guest handle IDs (e.g. "0xF8000098")
|
||||
// to log on every XEvent::Set / KeSetEvent / NtSetEvent / KePulseEvent /
|
||||
// NtPulseEvent fire whose target matches. Max 4 entries. Default empty (off);
|
||||
// zero cost on the hot path when empty.
|
||||
DECLARE_string(audit_69_event_signal_watch);
|
||||
// AUDIT-069: event-signal watch by native guest VA (X_KEVENT*). CSV of guest
|
||||
// VAs (max 4). Default empty (off). Use when the handle id varies across
|
||||
// boots but the native dispatcher pointer is stable.
|
||||
DECLARE_string(audit_69_event_signal_native_ptr);
|
||||
// AUDIT-069: when true, log EVERY XEvent::Set / XEvent::Pulse fire (subject
|
||||
// to the slowpath gate). Use only with --mute=true and short windows — high
|
||||
// volume. Default false (off).
|
||||
DECLARE_bool(audit_69_log_all_sets);
|
||||
|
||||
// AUDIT-070 (S5 of AUDIT-069 family): semaphore-release watch. CSV of guest
|
||||
// handle IDs (e.g. "0xF8000098") to log on every NtReleaseSemaphore /
|
||||
// xeKeReleaseSemaphore fire whose target matches. Max 4 entries. Default
|
||||
// empty (off); zero cost on the hot path when empty.
|
||||
DECLARE_string(audit_70_semaphore_release_watch);
|
||||
// AUDIT-070: when true, log EVERY NtReleaseSemaphore / xeKeReleaseSemaphore
|
||||
// fire. Use only with --mute=true and short windows — used to identify the
|
||||
// canary work-semaphore handle on first run. Default false (off).
|
||||
DECLARE_bool(audit_70_log_all_releases);
|
||||
|
||||
// Phase A: JSONL event-log emitter path. When non-empty, the engine writes
|
||||
// schema-v1 JSONL events to this file. Empty (default) = no overhead, no
|
||||
// behavior change. Schema: xenia-rs/audit-runs/phase-a-diff-harness/schema-v1.md
|
||||
DECLARE_string(phase_a_event_log_path);
|
||||
DECLARE_bool(phase_a_event_log_mem_writes);
|
||||
DECLARE_bool(phase_a_trace_args);
|
||||
DECLARE_string(phase_a_hash_probe);
|
||||
|
||||
// Phase B: initial-state snapshot. When the dir cvar is non-empty, the
|
||||
// engine writes a five-file structured state snapshot (cpu_state.json,
|
||||
// memory.json, kernel.json, vfs.json, config.json, plus manifest.json) to
|
||||
// `<dir>/canary/` at the moment immediately before the first guest PPC
|
||||
// instruction of the XEX entry_point executes. See
|
||||
// `xenia-rs/audit-runs/phase-b-state-equivalence/`.
|
||||
DECLARE_string(phase_b_snapshot_dir);
|
||||
DECLARE_bool(phase_b_snapshot_and_exit);
|
||||
DECLARE_bool(phase_b_dump_section_content);
|
||||
|
||||
#endif // XENIA_CPU_CPU_FLAGS_H_
|
||||
|
||||
@@ -34,141 +34,6 @@ DEFINE_bool(
|
||||
"unimplemented PowerPC instruction is encountered.",
|
||||
"CPU");
|
||||
|
||||
// AUDIT-061 — multi-PC branch probe. Parses cvars::audit_61_branch_probe_pcs
|
||||
// once and exposes a (pc -> trap_id) lookup table. trap_id range [200, 65535].
|
||||
// PCs outside the table are not probed. Native side reads g_audit61_pcs[idx].
|
||||
#include <vector>
|
||||
#include <string>
|
||||
namespace xe {
|
||||
namespace cpu {
|
||||
namespace audit61 {
|
||||
constexpr uint16_t kTrapBase = 200;
|
||||
constexpr size_t kMaxPcs = 32;
|
||||
static std::vector<uint32_t> g_pcs;
|
||||
static bool g_parsed = false;
|
||||
|
||||
const std::vector<uint32_t>& pcs() {
|
||||
if (!g_parsed) {
|
||||
g_parsed = true;
|
||||
const std::string& csv = cvars::audit_61_branch_probe_pcs;
|
||||
size_t pos = 0;
|
||||
while (pos < csv.size() && g_pcs.size() < kMaxPcs) {
|
||||
size_t end = csv.find(',', pos);
|
||||
std::string tok = csv.substr(pos, end - pos);
|
||||
// strip whitespace
|
||||
while (!tok.empty() && (tok.front() == ' ' || tok.front() == '\t'))
|
||||
tok.erase(tok.begin());
|
||||
while (!tok.empty() && (tok.back() == ' ' || tok.back() == '\t'))
|
||||
tok.pop_back();
|
||||
if (!tok.empty()) {
|
||||
try {
|
||||
uint32_t v = static_cast<uint32_t>(std::stoul(tok, nullptr, 0));
|
||||
g_pcs.push_back(v);
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
if (end == std::string::npos) break;
|
||||
pos = end + 1;
|
||||
}
|
||||
}
|
||||
return g_pcs;
|
||||
}
|
||||
|
||||
// Returns trap id for pc, or 0 if pc not in probe set.
|
||||
uint16_t trap_id_for(uint32_t pc) {
|
||||
const auto& v = pcs();
|
||||
for (size_t i = 0; i < v.size(); ++i) {
|
||||
if (v[i] == pc) return static_cast<uint16_t>(kTrapBase + i);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
} // namespace audit61
|
||||
|
||||
// AUDIT-067 — value-watch. Parses cvars::audit_67_value_watch once, exposes
|
||||
// values via vals(). Trap codes for matches start at kTrapBase = 250.
|
||||
namespace audit67 {
|
||||
constexpr uint16_t kTrapBase = 250;
|
||||
constexpr size_t kMaxVals = 4;
|
||||
static std::vector<uint32_t> g_vals;
|
||||
static bool g_parsed = false;
|
||||
|
||||
const std::vector<uint32_t>& vals() {
|
||||
if (!g_parsed) {
|
||||
g_parsed = true;
|
||||
const std::string& csv = cvars::audit_67_value_watch;
|
||||
size_t pos = 0;
|
||||
while (pos < csv.size() && g_vals.size() < kMaxVals) {
|
||||
size_t end = csv.find(',', pos);
|
||||
std::string tok = csv.substr(pos, end - pos);
|
||||
while (!tok.empty() && (tok.front() == ' ' || tok.front() == '\t'))
|
||||
tok.erase(tok.begin());
|
||||
while (!tok.empty() && (tok.back() == ' ' || tok.back() == '\t'))
|
||||
tok.pop_back();
|
||||
if (!tok.empty()) {
|
||||
try {
|
||||
uint32_t v = static_cast<uint32_t>(std::stoul(tok, nullptr, 0));
|
||||
g_vals.push_back(v);
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
if (end == std::string::npos) break;
|
||||
pos = end + 1;
|
||||
}
|
||||
XELOGI("AUDIT-067-INIT csv=\"{}\" parsed_count={}", csv, g_vals.size());
|
||||
for (size_t i = 0; i < g_vals.size(); ++i) {
|
||||
XELOGI("AUDIT-067-INIT vals[{}] = 0x{:08X}", i, g_vals[i]);
|
||||
}
|
||||
}
|
||||
return g_vals;
|
||||
}
|
||||
} // namespace audit67
|
||||
|
||||
// PHASE-A hash probe — multi-PC guest probe for recovering the IPFB/IDXD
|
||||
// name-hash. Parses cvars::phase_a_hash_probe once; trap ids start at 300.
|
||||
// At each fire the native handler derefs r3 as a guest C-string (the archive
|
||||
// path being hashed) and logs it alongside r3..r6. Mirrors audit61.
|
||||
namespace hashprobe {
|
||||
constexpr uint16_t kTrapBase = 300;
|
||||
constexpr size_t kMaxPcs = 32;
|
||||
static std::vector<uint32_t> g_pcs;
|
||||
static bool g_parsed = false;
|
||||
|
||||
const std::vector<uint32_t>& pcs() {
|
||||
if (!g_parsed) {
|
||||
g_parsed = true;
|
||||
const std::string& csv = cvars::phase_a_hash_probe;
|
||||
size_t pos = 0;
|
||||
while (pos < csv.size() && g_pcs.size() < kMaxPcs) {
|
||||
size_t end = csv.find(',', pos);
|
||||
std::string tok = csv.substr(pos, end - pos);
|
||||
while (!tok.empty() && (tok.front() == ' ' || tok.front() == '\t'))
|
||||
tok.erase(tok.begin());
|
||||
while (!tok.empty() && (tok.back() == ' ' || tok.back() == '\t'))
|
||||
tok.pop_back();
|
||||
if (!tok.empty()) {
|
||||
try {
|
||||
g_pcs.push_back(static_cast<uint32_t>(std::stoul(tok, nullptr, 0)));
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
if (end == std::string::npos) break;
|
||||
pos = end + 1;
|
||||
}
|
||||
}
|
||||
return g_pcs;
|
||||
}
|
||||
|
||||
uint16_t trap_id_for(uint32_t pc) {
|
||||
const auto& v = pcs();
|
||||
for (size_t i = 0; i < v.size(); ++i) {
|
||||
if (v[i] == pc) return static_cast<uint16_t>(kTrapBase + i);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
} // namespace hashprobe
|
||||
} // namespace cpu
|
||||
} // namespace xe
|
||||
|
||||
namespace xe {
|
||||
namespace cpu {
|
||||
namespace ppc {
|
||||
@@ -309,32 +174,6 @@ bool PPCHIRBuilder::Emit(GuestFunction* function, uint32_t flags) {
|
||||
|
||||
MaybeBreakOnInstruction(address);
|
||||
|
||||
// AUDIT-061: emit a trap before this instruction if it's on the probe
|
||||
// list. The trap fires BEFORE the cmp/branch HIR emit so the native
|
||||
// handler observes cr0/cr6 set by the *previous* instruction (the cmp
|
||||
// that controls this conditional branch). ContextBarrier flushes
|
||||
// HIR temporaries to PPCContext so the handler reads consistent state.
|
||||
if (!::xe::cpu::audit61::pcs().empty()) {
|
||||
uint16_t tid = ::xe::cpu::audit61::trap_id_for(address);
|
||||
if (tid != 0) {
|
||||
Comment("--audit_61_branch_probe target");
|
||||
ContextBarrier();
|
||||
Trap(tid);
|
||||
}
|
||||
}
|
||||
|
||||
// PHASE-A hash probe: trap before this instruction so the native handler
|
||||
// observes r3 (arg string ptr) as set by the caller. ContextBarrier
|
||||
// flushes HIR temporaries so the handler reads consistent GPRs.
|
||||
if (!::xe::cpu::hashprobe::pcs().empty()) {
|
||||
uint16_t tid = ::xe::cpu::hashprobe::trap_id_for(address);
|
||||
if (tid != 0) {
|
||||
Comment("--phase_a_hash_probe target");
|
||||
ContextBarrier();
|
||||
Trap(tid);
|
||||
}
|
||||
}
|
||||
|
||||
InstrData i;
|
||||
i.address = address;
|
||||
i.code = code;
|
||||
|
||||
@@ -49,8 +49,6 @@ DEFINE_bool(
|
||||
|
||||
DECLARE_bool(allow_plugins);
|
||||
|
||||
DECLARE_bool(disable_context_promotion);
|
||||
|
||||
static constexpr uint8_t xe_xex1_retail_key[16] = {
|
||||
0xA2, 0x6C, 0x10, 0xF7, 0x1F, 0xD9, 0x35, 0xE9,
|
||||
0x8B, 0x99, 0x92, 0x2C, 0xE9, 0x32, 0x15, 0x72};
|
||||
@@ -1354,10 +1352,7 @@ std::unique_ptr<Function> XexModule::CreateFunction(uint32_t address) {
|
||||
processor_->backend()->CreateGuestFunction(this, address));
|
||||
}
|
||||
void XexInfoCache::Init(XexModule* xexmod) {
|
||||
// If context promotion is disabled then disable instruction info cache as
|
||||
// well, otherwise XMA will write to unknown registers.
|
||||
if (cvars::disable_instruction_infocache ||
|
||||
cvars::disable_context_promotion) {
|
||||
if (cvars::disable_instruction_infocache) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1377,20 +1372,20 @@ void XexInfoCache::Init(XexModule* xexmod) {
|
||||
|
||||
auto try_open = [this, &infocache_path, num_codebytes]() {
|
||||
bool did_exist = true;
|
||||
const size_t file_size = sizeof(InfoCacheFlagsHeader) +
|
||||
(sizeof(InfoCacheFlags) * (num_codebytes / 4));
|
||||
|
||||
if (!std::filesystem::exists(infocache_path)) {
|
||||
xe::filesystem::CreateEmptyFile(infocache_path);
|
||||
std::filesystem::resize_file(infocache_path, file_size);
|
||||
did_exist = false;
|
||||
}
|
||||
|
||||
// todo: prepopulate with stuff from pdata, dll exports
|
||||
this->executable_addr_flags_ = MappedMemory::Open(
|
||||
infocache_path, xe::MappedMemory::Mode::kReadWrite, 0,
|
||||
file_size); // one infocacheflags entry for each PPC instr-sized addr
|
||||
|
||||
this->executable_addr_flags_ = std::move(xe::MappedMemory::Open(
|
||||
infocache_path, xe::MappedMemory::Mode::kReadWrite, 0,
|
||||
sizeof(InfoCacheFlagsHeader) +
|
||||
(sizeof(InfoCacheFlags) *
|
||||
(num_codebytes /
|
||||
4)))); // one infocacheflags entry for each PPC instr-sized addr
|
||||
return did_exist;
|
||||
};
|
||||
|
||||
|
||||
@@ -1445,15 +1445,7 @@ X_STATUS Emulator::CompleteLaunch(const std::filesystem::path& path,
|
||||
const std::string_view module_path) {
|
||||
// Making changes to the UI (setting the icon) and executing game config
|
||||
// load callbacks which expect to be called from the UI thread.
|
||||
// If not on UI thread, dispatch to it synchronously.
|
||||
if (!display_window_->app_context().IsInUIThread()) {
|
||||
X_STATUS result = X_STATUS_UNSUCCESSFUL;
|
||||
display_window_->app_context().CallInUIThreadSynchronous(
|
||||
[this, &path, &module_path, &result]() {
|
||||
result = CompleteLaunch(path, module_path);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
assert_true(display_window_->app_context().IsInUIThread());
|
||||
|
||||
// Setup NullDevices for raw HDD partition accesses
|
||||
// Cache/STFC code baked into games tries reading/writing to these
|
||||
@@ -1707,11 +1699,6 @@ X_STATUS Emulator::CompleteLaunch(const std::filesystem::path& path,
|
||||
}
|
||||
}
|
||||
|
||||
// Resume the main thread now.
|
||||
// If the debugger has requested a suspend this will just decrement the
|
||||
// suspend count without resuming it until the debugger wants.
|
||||
main_thread_->Resume();
|
||||
|
||||
return X_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,31 +26,53 @@ HardwarePortal::~HardwarePortal() {
|
||||
libusb_exit(context_);
|
||||
}
|
||||
|
||||
bool HardwarePortal::IsConnected() { return handle_ != nullptr; }
|
||||
|
||||
void HardwarePortal::OpenDevice() {
|
||||
if (!context_ || handle_) {
|
||||
if (!context_ || connected_) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Allow only one portal device at the time.
|
||||
for (const auto& entry : kPortalVendorProductIdList) {
|
||||
handle_ =
|
||||
libusb_open_device_with_vid_pid(context_, entry.first, entry.second);
|
||||
if (handle_) {
|
||||
libusb_claim_interface(handle_, 0);
|
||||
libusb_device** devs;
|
||||
const ssize_t cnt = libusb_get_device_list(context_, &devs);
|
||||
if (cnt < 0) {
|
||||
// No device available... It might appear later.
|
||||
return;
|
||||
}
|
||||
|
||||
for (ssize_t i = 0; i < cnt; ++i) {
|
||||
libusb_device* dev = devs[i];
|
||||
|
||||
libusb_device_descriptor desc;
|
||||
if (libusb_get_device_descriptor(dev, &desc) == 0) {
|
||||
// Check if this device matches the target Vendor ID and Product ID.
|
||||
// Small limitation. We're only supporting one device being connected at
|
||||
// the time.
|
||||
for (const auto& entry : kPortalVendorProductIdList) {
|
||||
if (desc.idVendor == entry.first && desc.idProduct == entry.second) {
|
||||
if (libusb_open(dev, &handle_) == 0) {
|
||||
libusb_claim_interface(handle_, 0);
|
||||
connected_ = true;
|
||||
// We found device. No need to go thorugh remaining IDs.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// We found device. No need to go thorugh remaining devices.
|
||||
if (connected_) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
libusb_free_device_list(devs, 0);
|
||||
}
|
||||
|
||||
void HardwarePortal::CloseDevice() {
|
||||
if (!handle_) {
|
||||
if (!connected_) {
|
||||
return;
|
||||
}
|
||||
|
||||
libusb_release_interface(handle_, 0);
|
||||
libusb_close(handle_);
|
||||
connected_ = false;
|
||||
handle_ = nullptr;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,8 +28,6 @@ class HardwarePortal final : public Portal {
|
||||
HardwarePortal();
|
||||
~HardwarePortal() override;
|
||||
|
||||
virtual bool IsConnected() override;
|
||||
|
||||
virtual void OnDeviceArrival() override;
|
||||
virtual void OnDeviceRemoval() override;
|
||||
|
||||
|
||||
@@ -15,11 +15,16 @@ namespace hid {
|
||||
Portal::Portal() {}
|
||||
Portal::~Portal() {}
|
||||
|
||||
bool Portal::IsConnected() {
|
||||
std::lock_guard<xe_mutex> guard(lock_);
|
||||
return connected_;
|
||||
}
|
||||
|
||||
X_STATUS Portal::Read(std::span<uint8_t> data, uint32_t& bytes_read,
|
||||
uint16_t& state) {
|
||||
std::lock_guard<xe_mutex> guard(lock_);
|
||||
|
||||
if (!IsConnected()) {
|
||||
if (!connected_) {
|
||||
return X_ERROR_DEVICE_NOT_CONNECTED;
|
||||
}
|
||||
|
||||
@@ -50,7 +55,7 @@ X_STATUS Portal::Read(std::span<uint8_t> data, uint32_t& bytes_read,
|
||||
X_STATUS Portal::Write(std::span<uint8_t> data) {
|
||||
std::lock_guard<xe_mutex> guard(lock_);
|
||||
|
||||
if (!IsConnected()) {
|
||||
if (!connected_) {
|
||||
return X_ERROR_DEVICE_NOT_CONNECTED;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ class Portal {
|
||||
Portal();
|
||||
virtual ~Portal();
|
||||
|
||||
virtual bool IsConnected() = 0;
|
||||
bool IsConnected();
|
||||
|
||||
virtual X_STATUS Read(std::span<uint8_t> data, uint32_t& bytes_read,
|
||||
uint16_t& state);
|
||||
@@ -34,6 +34,9 @@ class Portal {
|
||||
virtual void OnDeviceArrival() = 0;
|
||||
virtual void OnDeviceRemoval() = 0;
|
||||
|
||||
protected:
|
||||
bool connected_ = false;
|
||||
|
||||
private:
|
||||
virtual void OpenDevice() = 0;
|
||||
virtual void CloseDevice() = 0;
|
||||
|
||||
@@ -1,709 +0,0 @@
|
||||
/**
|
||||
******************************************************************************
|
||||
* Xenia : Xbox 360 Emulator Research Project *
|
||||
******************************************************************************
|
||||
* Phase A event-log emitter — see event_log.h and schema-v1.md.
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#include "xenia/kernel/event_log.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "third_party/fmt/include/fmt/format.h"
|
||||
|
||||
#include "xenia/base/cvar.h"
|
||||
#include "xenia/cpu/ppc/ppc_context.h"
|
||||
#include "xenia/kernel/kernel.h"
|
||||
#include "xenia/kernel/kernel_state.h"
|
||||
#include "xenia/kernel/util/object_table.h"
|
||||
#include "xenia/kernel/xfile.h"
|
||||
#include "xenia/kernel/xthread.h"
|
||||
#include "xenia/memory.h"
|
||||
|
||||
DECLARE_string(phase_a_event_log_path);
|
||||
DECLARE_bool(kernel_emit_contention);
|
||||
DECLARE_bool(phase_a_trace_args);
|
||||
DECLARE_string(phase_a_hash_probe);
|
||||
|
||||
namespace xe {
|
||||
namespace kernel {
|
||||
namespace phase_a {
|
||||
|
||||
namespace {
|
||||
|
||||
// Cached enabled state, computed lazily from cvar (cheap fast-path).
|
||||
std::atomic<int> g_state{0}; // 0=untouched, 1=enabled, 2=disabled
|
||||
std::FILE* g_file = nullptr;
|
||||
std::mutex g_file_mu;
|
||||
std::once_flag g_init_once;
|
||||
|
||||
// Per-thread monotonic event index (key for the diff tool).
|
||||
// Phase C+15-α: per-tid (not per-host-thread). Multiple host threads
|
||||
// can emit with tid=0 (boot + XThread bootstrap before guest tid is
|
||||
// assigned), and a thread_local counter aliased across host threads
|
||||
// produces duplicate `tid_event_idx` values, breaking the diff tool's
|
||||
// monotonicity invariant. Use a tid-keyed map guarded by a mutex.
|
||||
std::unordered_map<uint32_t, uint64_t> g_tid_counters;
|
||||
std::mutex g_tid_counters_mu;
|
||||
|
||||
uint64_t NextTidEventIdx(uint32_t tid) {
|
||||
std::lock_guard<std::mutex> lock(g_tid_counters_mu);
|
||||
auto& c = g_tid_counters[tid];
|
||||
uint64_t v = c;
|
||||
c = v + 1;
|
||||
return v;
|
||||
}
|
||||
|
||||
uint64_t PeekTidEventIdxLocked(uint32_t tid) {
|
||||
std::lock_guard<std::mutex> lock(g_tid_counters_mu);
|
||||
auto it = g_tid_counters.find(tid);
|
||||
return it == g_tid_counters.end() ? 0 : it->second;
|
||||
}
|
||||
|
||||
// Process-start ns for the host_ns field. Captured on first use; debug only.
|
||||
std::chrono::steady_clock::time_point g_t0;
|
||||
std::once_flag g_t0_once;
|
||||
|
||||
void EnsureT0() {
|
||||
std::call_once(g_t0_once,
|
||||
[]() { g_t0 = std::chrono::steady_clock::now(); });
|
||||
}
|
||||
|
||||
int64_t HostNsSinceStart() {
|
||||
EnsureT0();
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
return std::chrono::duration_cast<std::chrono::nanoseconds>(now - g_t0)
|
||||
.count();
|
||||
}
|
||||
|
||||
void OpenIfNeeded() {
|
||||
std::call_once(g_init_once, []() {
|
||||
const std::string& path = cvars::phase_a_event_log_path;
|
||||
if (path.empty()) {
|
||||
g_state.store(2, std::memory_order_release);
|
||||
return;
|
||||
}
|
||||
g_file = std::fopen(path.c_str(), "wb");
|
||||
if (!g_file) {
|
||||
g_state.store(2, std::memory_order_release);
|
||||
return;
|
||||
}
|
||||
g_state.store(1, std::memory_order_release);
|
||||
// Write the schema header as the first line — synthetic tid=0.
|
||||
auto header = fmt::format(
|
||||
"{{\"schema_version\":1,\"engine\":\"canary\",\"kind\":\"schema_version"
|
||||
"\",\"tid\":0,\"tid_event_idx\":0,\"guest_cycle\":0,\"host_ns\":{},\""
|
||||
"deterministic\":true,\"payload\":{{\"version\":1,\"emitter_build\":\""
|
||||
"canary-phaseA\"}}}}\n",
|
||||
HostNsSinceStart());
|
||||
std::fwrite(header.data(), 1, header.size(), g_file);
|
||||
std::fflush(g_file);
|
||||
});
|
||||
}
|
||||
|
||||
uint32_t CurrentTid() {
|
||||
// Phase C+15-α: AddHandle / RemoveHandle hooks can fire from boot
|
||||
// code (XEX loader, kernel-state init) BEFORE any XThread exists.
|
||||
// `XThread::GetCurrentThreadId` asserts in that case. Use the
|
||||
// safe `TryGetCurrentThread` accessor (returns null instead of
|
||||
// asserting). Return 0 (synthetic "no-thread" tid) when not in a
|
||||
// guest thread, matching ours's `scheduler.current == None`
|
||||
// semantics during boot init.
|
||||
XThread* t = XThread::TryGetCurrentThread();
|
||||
if (!t) {
|
||||
return 0;
|
||||
}
|
||||
return t->guest_object<X_KTHREAD>()->thread_id;
|
||||
}
|
||||
|
||||
void WriteLine(const std::string& line) {
|
||||
std::lock_guard<std::mutex> lock(g_file_mu);
|
||||
if (!g_file) return;
|
||||
std::fwrite(line.data(), 1, line.size(), g_file);
|
||||
std::fputc('\n', g_file);
|
||||
// Flush every line so a crash mid-boot still produces a useful prefix.
|
||||
std::fflush(g_file);
|
||||
}
|
||||
|
||||
// Common-fields prefix. Caller appends `,\"payload\":{...}}`.
|
||||
// kind, tid, tid_event_idx, guest_cycle=0 (canary has no kernel-layer cycle),
|
||||
// host_ns, deterministic, engine.
|
||||
std::string CommonPrefix(const char* kind, uint32_t tid, uint64_t idx,
|
||||
bool deterministic) {
|
||||
return fmt::format(
|
||||
"{{\"schema_version\":1,\"engine\":\"canary\",\"kind\":\"{}\",\"tid\":{},"
|
||||
"\"tid_event_idx\":{},\"guest_cycle\":0,\"host_ns\":{},\"deterministic\":"
|
||||
"{}",
|
||||
kind, tid, idx, HostNsSinceStart(), deterministic ? "true" : "false");
|
||||
}
|
||||
|
||||
// Escape a JSON string. Keep it minimal — kernel names are ASCII.
|
||||
std::string EscapeJson(const char* s) {
|
||||
if (!s) return "null";
|
||||
std::string out;
|
||||
out.reserve(std::strlen(s) + 2);
|
||||
for (const char* p = s; *p; ++p) {
|
||||
unsigned char c = static_cast<unsigned char>(*p);
|
||||
if (c == '\\' || c == '"') {
|
||||
out.push_back('\\');
|
||||
out.push_back(static_cast<char>(c));
|
||||
} else if (c == '\n') {
|
||||
out += "\\n";
|
||||
} else if (c == '\r') {
|
||||
out += "\\r";
|
||||
} else if (c == '\t') {
|
||||
out += "\\t";
|
||||
} else if (c < 0x20) {
|
||||
out += fmt::format("\\u{:04x}", c);
|
||||
} else {
|
||||
out.push_back(static_cast<char>(c));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool IsEnabled() {
|
||||
int s = g_state.load(std::memory_order_acquire);
|
||||
if (s == 0) {
|
||||
OpenIfNeeded();
|
||||
s = g_state.load(std::memory_order_acquire);
|
||||
}
|
||||
return s == 1;
|
||||
}
|
||||
|
||||
uint64_t PeekTidEventIdx() { return PeekTidEventIdxLocked(CurrentTid()); }
|
||||
|
||||
uint64_t ComputeSemanticId(uint32_t create_site_pc, uint32_t creating_tid,
|
||||
uint64_t tid_event_idx_at_creation,
|
||||
uint32_t object_type) {
|
||||
uint8_t bytes[4 + 4 + 8 + 4];
|
||||
auto put_u32 = [&](size_t off, uint32_t v) {
|
||||
bytes[off + 0] = static_cast<uint8_t>(v & 0xFF);
|
||||
bytes[off + 1] = static_cast<uint8_t>((v >> 8) & 0xFF);
|
||||
bytes[off + 2] = static_cast<uint8_t>((v >> 16) & 0xFF);
|
||||
bytes[off + 3] = static_cast<uint8_t>((v >> 24) & 0xFF);
|
||||
};
|
||||
auto put_u64 = [&](size_t off, uint64_t v) {
|
||||
for (int i = 0; i < 8; ++i)
|
||||
bytes[off + i] = static_cast<uint8_t>((v >> (i * 8)) & 0xFF);
|
||||
};
|
||||
put_u32(0, create_site_pc);
|
||||
put_u32(4, creating_tid);
|
||||
put_u64(8, tid_event_idx_at_creation);
|
||||
put_u32(16, object_type);
|
||||
uint64_t h = 0xCBF29CE484222325ULL;
|
||||
for (size_t i = 0; i < sizeof(bytes); ++i) {
|
||||
h ^= bytes[i];
|
||||
h *= 0x100000001B3ULL;
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
void EmitSchemaHeader() {
|
||||
if (!IsEnabled()) return;
|
||||
// tid=0, tid_event_idx=0, deterministic=true. NOT consuming the per-tid
|
||||
// counter (the header is on a synthetic tid 0).
|
||||
std::string line = fmt::format(
|
||||
"{{\"schema_version\":1,\"engine\":\"canary\",\"kind\":\"schema_version"
|
||||
"\",\"tid\":0,\"tid_event_idx\":0,\"guest_cycle\":0,\"host_ns\":{},\""
|
||||
"deterministic\":true,\"payload\":{{\"version\":1,\"emitter_build\":\""
|
||||
"canary-phaseA\"}}}}",
|
||||
HostNsSinceStart());
|
||||
WriteLine(line);
|
||||
}
|
||||
|
||||
void EmitImportCall(const char* module_name, uint16_t ordinal,
|
||||
const char* fn_name) {
|
||||
if (!IsEnabled()) return;
|
||||
uint32_t tid = CurrentTid();
|
||||
uint64_t idx = NextTidEventIdx(tid);
|
||||
std::string line = CommonPrefix("import.call", tid, idx, true);
|
||||
line += fmt::format(
|
||||
",\"payload\":{{\"module\":\"{}\",\"ord\":{},\"name\":\"{}\"}}}}",
|
||||
EscapeJson(module_name), ordinal, EscapeJson(fn_name));
|
||||
WriteLine(line);
|
||||
}
|
||||
|
||||
void EmitKernelCall(const char* name) {
|
||||
if (!IsEnabled()) return;
|
||||
uint32_t tid = CurrentTid();
|
||||
uint64_t idx = NextTidEventIdx(tid);
|
||||
std::string line = CommonPrefix("kernel.call", tid, idx, true);
|
||||
line += fmt::format(",\"payload\":{{\"name\":\"{}\",\"args\":{{}},\"args_"
|
||||
"resolved\":{{}}}}}}",
|
||||
EscapeJson(name));
|
||||
WriteLine(line);
|
||||
}
|
||||
|
||||
// Phase C+10 schema-v1 extension: emit a `kernel.call` event whose
|
||||
// `args_resolved` field includes a `"path"` entry. The path string is
|
||||
// the canonical post-prefix-strip form (forward slashes, leading
|
||||
// device prefix removed). When `path` is null/empty, degrades to the
|
||||
// existing empty-args_resolved form so output is byte-identical to
|
||||
// the pre-extension behavior for unknown export names.
|
||||
void EmitKernelCallWithPath(const char* name, const char* path) {
|
||||
if (!IsEnabled()) return;
|
||||
uint32_t tid = CurrentTid();
|
||||
uint64_t idx = NextTidEventIdx(tid);
|
||||
std::string line = CommonPrefix("kernel.call", tid, idx, true);
|
||||
if (path && *path) {
|
||||
line += fmt::format(
|
||||
",\"payload\":{{\"name\":\"{}\",\"args\":{{}},\"args_resolved\":"
|
||||
"{{\"path\":\"{}\"}}}}}}",
|
||||
EscapeJson(name), EscapeJson(path));
|
||||
} else {
|
||||
line += fmt::format(",\"payload\":{{\"name\":\"{}\",\"args\":{{}},\"args_"
|
||||
"resolved\":{{}}}}}}",
|
||||
EscapeJson(name));
|
||||
}
|
||||
WriteLine(line);
|
||||
}
|
||||
|
||||
void EmitKernelReturn(const char* name, uint64_t return_value) {
|
||||
if (!IsEnabled()) return;
|
||||
uint32_t tid = CurrentTid();
|
||||
uint64_t idx = NextTidEventIdx(tid);
|
||||
std::string line = CommonPrefix("kernel.return", tid, idx, true);
|
||||
line += fmt::format(
|
||||
",\"payload\":{{\"name\":\"{}\",\"return_value\":{},\"status\":\"0x{:08x}"
|
||||
"\",\"side_effects\":[]}}}}",
|
||||
EscapeJson(name), return_value, static_cast<uint32_t>(return_value));
|
||||
WriteLine(line);
|
||||
}
|
||||
|
||||
void EmitHandleCreate(uint64_t semantic_id, uint32_t object_type,
|
||||
uint32_t raw_handle_id, const char* object_name) {
|
||||
if (!IsEnabled()) return;
|
||||
uint32_t tid = CurrentTid();
|
||||
uint64_t idx = NextTidEventIdx(tid);
|
||||
std::string line = CommonPrefix("handle.create", tid, idx, true);
|
||||
if (object_name && *object_name) {
|
||||
line += fmt::format(
|
||||
",\"payload\":{{\"handle_semantic_id\":\"{:016x}\",\"object_type\":{},"
|
||||
"\"object_name\":\"{}\",\"raw_handle_id\":\"0x{:08x}\"}}}}",
|
||||
semantic_id, object_type, EscapeJson(object_name), raw_handle_id);
|
||||
} else {
|
||||
line += fmt::format(
|
||||
",\"payload\":{{\"handle_semantic_id\":\"{:016x}\",\"object_type\":{},"
|
||||
"\"object_name\":null,\"raw_handle_id\":\"0x{:08x}\"}}}}",
|
||||
semantic_id, object_type, raw_handle_id);
|
||||
}
|
||||
WriteLine(line);
|
||||
}
|
||||
|
||||
void EmitHandleDestroy(uint64_t semantic_id, uint32_t raw_handle_id,
|
||||
uint32_t prior_refcount) {
|
||||
if (!IsEnabled()) return;
|
||||
uint32_t tid = CurrentTid();
|
||||
uint64_t idx = NextTidEventIdx(tid);
|
||||
std::string line = CommonPrefix("handle.destroy", tid, idx, true);
|
||||
line += fmt::format(
|
||||
",\"payload\":{{\"handle_semantic_id\":\"{:016x}\",\"raw_handle_id\":\""
|
||||
"0x{:08x}\",\"prior_refcount\":{}}}}}",
|
||||
semantic_id, raw_handle_id, prior_refcount);
|
||||
WriteLine(line);
|
||||
}
|
||||
|
||||
void EmitThreadCreate(uint64_t semantic_id, uint32_t parent_tid,
|
||||
uint32_t entry_pc, uint32_t ctx_ptr, uint32_t priority,
|
||||
uint32_t affinity, uint32_t stack_size, bool suspended) {
|
||||
if (!IsEnabled()) return;
|
||||
uint32_t tid = CurrentTid();
|
||||
uint64_t idx = NextTidEventIdx(tid);
|
||||
std::string line = CommonPrefix("thread.create", tid, idx, true);
|
||||
line += fmt::format(
|
||||
",\"payload\":{{\"handle_semantic_id\":\"{:016x}\",\"parent_tid\":{},"
|
||||
"\"entry_pc\":\"0x{:08x}\",\"ctx_ptr\":\"0x{:08x}\",\"priority\":{},"
|
||||
"\"affinity\":{},\"stack_size\":{},\"suspended\":{}}}}}",
|
||||
semantic_id, parent_tid, entry_pc, ctx_ptr, priority, affinity,
|
||||
stack_size, suspended ? "true" : "false");
|
||||
WriteLine(line);
|
||||
}
|
||||
|
||||
void EmitThreadExit(uint32_t exit_code) {
|
||||
if (!IsEnabled()) return;
|
||||
uint32_t tid = CurrentTid();
|
||||
uint64_t idx = NextTidEventIdx(tid);
|
||||
std::string line = CommonPrefix("thread.exit", tid, idx, true);
|
||||
line += fmt::format(",\"payload\":{{\"exit_code\":{}}}}}", exit_code);
|
||||
WriteLine(line);
|
||||
}
|
||||
|
||||
void EmitWaitBegin(const uint64_t* handles_semantic_ids, uint32_t count,
|
||||
int64_t timeout_ns, bool alertable, bool wait_all) {
|
||||
if (!IsEnabled()) return;
|
||||
uint32_t tid = CurrentTid();
|
||||
uint64_t idx = NextTidEventIdx(tid);
|
||||
std::string line = CommonPrefix("wait.begin", tid, idx, true);
|
||||
std::string ids = "[";
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
if (i) ids += ",";
|
||||
ids += fmt::format("\"{:016x}\"", handles_semantic_ids[i]);
|
||||
}
|
||||
ids += "]";
|
||||
line += fmt::format(
|
||||
",\"payload\":{{\"handles_semantic_ids\":{},\"timeout_ns\":{},"
|
||||
"\"alertable\":{},\"wait_type\":\"{}\"}}}}",
|
||||
ids, timeout_ns, alertable ? "true" : "false",
|
||||
wait_all ? "all" : "any");
|
||||
WriteLine(line);
|
||||
}
|
||||
|
||||
void EmitWaitEnd(uint32_t status, uint64_t woken_by_semantic_id_or_zero) {
|
||||
if (!IsEnabled()) return;
|
||||
uint32_t tid = CurrentTid();
|
||||
uint64_t idx = NextTidEventIdx(tid);
|
||||
std::string line = CommonPrefix("wait.end", tid, idx, false);
|
||||
if (woken_by_semantic_id_or_zero) {
|
||||
line += fmt::format(
|
||||
",\"payload\":{{\"status\":\"0x{:08x}\",\"woken_by_semantic_id\":\""
|
||||
"{:016x}\",\"wait_duration_cycles\":0}}}}",
|
||||
status, woken_by_semantic_id_or_zero);
|
||||
} else {
|
||||
line += fmt::format(
|
||||
",\"payload\":{{\"status\":\"0x{:08x}\",\"woken_by_semantic_id\":null,"
|
||||
"\"wait_duration_cycles\":0}}}}",
|
||||
status);
|
||||
}
|
||||
WriteLine(line);
|
||||
}
|
||||
|
||||
// ===== Phase C+15-α — Handle semantic ID registry =====
|
||||
namespace {
|
||||
std::unordered_map<uint32_t, uint64_t> g_handle_sids;
|
||||
std::mutex g_handle_sids_mu;
|
||||
} // namespace
|
||||
|
||||
void RegisterHandleSemanticId(uint32_t raw_handle_id, uint64_t sid) {
|
||||
if (!IsEnabled()) return;
|
||||
std::lock_guard<std::mutex> lock(g_handle_sids_mu);
|
||||
g_handle_sids[raw_handle_id] = sid;
|
||||
}
|
||||
|
||||
uint64_t LookupHandleSemanticId(uint32_t raw_handle_id) {
|
||||
std::lock_guard<std::mutex> lock(g_handle_sids_mu);
|
||||
auto it = g_handle_sids.find(raw_handle_id);
|
||||
return it == g_handle_sids.end() ? 0 : it->second;
|
||||
}
|
||||
|
||||
uint64_t ForgetHandleSemanticId(uint32_t raw_handle_id) {
|
||||
std::lock_guard<std::mutex> lock(g_handle_sids_mu);
|
||||
auto it = g_handle_sids.find(raw_handle_id);
|
||||
if (it == g_handle_sids.end()) return 0;
|
||||
uint64_t sid = it->second;
|
||||
g_handle_sids.erase(it);
|
||||
return sid;
|
||||
}
|
||||
|
||||
uint64_t EmitHandleCreateAuto(uint32_t create_site_pc, uint32_t object_type,
|
||||
uint32_t raw_handle_id,
|
||||
const char* object_name) {
|
||||
if (!IsEnabled()) return 0;
|
||||
uint32_t tid = CurrentTid();
|
||||
uint64_t idx_at_creation = PeekTidEventIdxLocked(tid);
|
||||
uint64_t sid =
|
||||
ComputeSemanticId(create_site_pc, tid, idx_at_creation, object_type);
|
||||
RegisterHandleSemanticId(raw_handle_id, sid);
|
||||
EmitHandleCreate(sid, object_type, raw_handle_id, object_name);
|
||||
return sid;
|
||||
}
|
||||
|
||||
void EmitHandleDestroyAuto(uint32_t raw_handle_id, uint32_t prior_refcount) {
|
||||
if (!IsEnabled()) return;
|
||||
uint64_t sid = ForgetHandleSemanticId(raw_handle_id);
|
||||
EmitHandleDestroy(sid, raw_handle_id, prior_refcount);
|
||||
}
|
||||
|
||||
// ===== Phase C+18 — Shared-global SIDs =====
|
||||
|
||||
uint64_t ComputeSharedGlobalSemanticId(uint32_t pointer, uint32_t object_type) {
|
||||
// Reuse the same FNV-1a recipe as `ComputeSemanticId` but with inputs
|
||||
// chosen to be scheduling-invariant:
|
||||
// create_site_pc = kSharedGlobalSidMarker (distinguishes from regular SIDs)
|
||||
// creating_tid = 0
|
||||
// tid_event_idx_at_creation = pointer (as u64)
|
||||
// object_type = object_type
|
||||
// Matches `event_log.rs::semantic_id_shared_global` byte-for-byte.
|
||||
return ComputeSemanticId(kSharedGlobalSidMarker, 0,
|
||||
static_cast<uint64_t>(pointer), object_type);
|
||||
}
|
||||
|
||||
void EmitHandleCreateSharedGlobal(uint32_t pointer, uint32_t object_type,
|
||||
uint32_t raw_handle_id,
|
||||
const char* object_name) {
|
||||
if (!IsEnabled()) return;
|
||||
uint64_t sid = ComputeSharedGlobalSemanticId(pointer, object_type);
|
||||
RegisterHandleSemanticId(raw_handle_id, sid);
|
||||
EmitHandleCreate(sid, object_type, raw_handle_id, object_name);
|
||||
}
|
||||
|
||||
// Phase C+18: per-host-thread flag that the AddHandle hook checks to
|
||||
// skip its `EmitHandleCreateAuto` call for XObjects synthesized by
|
||||
// `XObject::GetNativeObject` (a single boolean — the global critical
|
||||
// region is held across the GetNativeObject body so re-entrancy isn't
|
||||
// expected).
|
||||
namespace {
|
||||
thread_local bool t_in_get_native_object = false;
|
||||
} // namespace
|
||||
|
||||
void SetInGetNativeObject(bool active) { t_in_get_native_object = active; }
|
||||
bool IsInGetNativeObject() { return t_in_get_native_object; }
|
||||
|
||||
// ===== Phase D Stage 1 — contention.observed =====
|
||||
|
||||
void EmitContentionObserved(uint32_t cs_guest_ptr, bool contended) {
|
||||
// Two-gate fast path: cvar OFF is the default and must be byte-identical
|
||||
// to pre-Stage-1 canary. The cvar is checked first to short-circuit even
|
||||
// when the phase A event log itself is enabled (Stage 0/baseline cold
|
||||
// runs may have event log on but contention emit off).
|
||||
if (!cvars::kernel_emit_contention) return;
|
||||
if (!IsEnabled()) return;
|
||||
uint32_t tid = CurrentTid();
|
||||
uint64_t idx = NextTidEventIdx(tid);
|
||||
uint64_t site_sid =
|
||||
ComputeSharedGlobalSemanticId(cs_guest_ptr, kObjCriticalSection);
|
||||
std::string line = CommonPrefix("contention.observed", tid, idx, true);
|
||||
line += fmt::format(
|
||||
",\"payload\":{{\"cs_ptr\":\"0x{:08x}\",\"site_sid\":\"{:016x}\","
|
||||
"\"contended\":{}}}}}",
|
||||
cs_guest_ptr, site_sid, contended ? "true" : "false");
|
||||
WriteLine(line);
|
||||
}
|
||||
|
||||
// ── Expansive extraction tracing (game-data RE) ────────────────────────────
|
||||
// All gated by `phase_a_trace_args` / `phase_a_hash_probe` (default off), so
|
||||
// with those unset the JSONL output is byte-identical to the diff-schema form.
|
||||
|
||||
// kernel.call with populated `args` (raw r3..r10) and `args_resolved`
|
||||
// (currently the resolved file path, when known). `ppc_context` is a
|
||||
// PPCContext* passed as void* to keep the header free of the PPC include.
|
||||
void EmitKernelCallArgs(const char* name, void* ppc_context,
|
||||
const char* resolved_path) {
|
||||
if (!IsEnabled()) return;
|
||||
auto* ctx = reinterpret_cast<::xe::cpu::ppc::PPCContext*>(ppc_context);
|
||||
uint32_t tid = CurrentTid();
|
||||
uint64_t idx = NextTidEventIdx(tid);
|
||||
std::string line = CommonPrefix("kernel.call", tid, idx, true);
|
||||
std::string args;
|
||||
if (ctx) {
|
||||
args = fmt::format(
|
||||
"\"r3\":{},\"r4\":{},\"r5\":{},\"r6\":{},\"r7\":{},\"r8\":{},\"r9\":{},"
|
||||
"\"r10\":{}",
|
||||
ctx->r[3], ctx->r[4], ctx->r[5], ctx->r[6], ctx->r[7], ctx->r[8],
|
||||
ctx->r[9], ctx->r[10]);
|
||||
}
|
||||
std::string resolved;
|
||||
if (resolved_path && *resolved_path) {
|
||||
resolved = fmt::format("\"path\":\"{}\"", EscapeJson(resolved_path));
|
||||
}
|
||||
line += fmt::format(
|
||||
",\"payload\":{{\"name\":\"{}\",\"args\":{{{}}},\"args_resolved\":{{{}}}}}}}",
|
||||
EscapeJson(name), args, resolved);
|
||||
WriteLine(line);
|
||||
}
|
||||
|
||||
// file.read — a guest read from an open file handle. `path` is resolved from
|
||||
// the handle via the object table; `offset` is the requested byte offset.
|
||||
void EmitFileRead(uint32_t handle, const char* path, uint64_t offset,
|
||||
uint32_t length, uint32_t buffer_va) {
|
||||
if (!IsEnabled()) return;
|
||||
uint32_t tid = CurrentTid();
|
||||
uint64_t idx = NextTidEventIdx(tid);
|
||||
std::string line = CommonPrefix("file.read", tid, idx, true);
|
||||
line += fmt::format(
|
||||
",\"payload\":{{\"handle\":\"0x{:08x}\",\"path\":\"{}\",\"offset\":{},"
|
||||
"\"length\":{},\"buffer_va\":\"0x{:08x}\"}}}}",
|
||||
handle, EscapeJson(path), offset, length, buffer_va);
|
||||
WriteLine(line);
|
||||
}
|
||||
|
||||
// guest.call — fired by the guest-PC hash probe (Tier 3). `arg_str` is r3
|
||||
// dereferenced as a guest C-string (the archive path being hashed).
|
||||
void EmitGuestCall(uint32_t pc, const char* arg_str, uint32_t r3, uint32_t r4,
|
||||
uint32_t r5, uint32_t r6) {
|
||||
if (!IsEnabled()) return;
|
||||
uint32_t tid = CurrentTid();
|
||||
uint64_t idx = NextTidEventIdx(tid);
|
||||
std::string line = CommonPrefix("guest.call", tid, idx, true);
|
||||
line += fmt::format(
|
||||
",\"payload\":{{\"pc\":\"0x{:08x}\",\"arg_str\":\"{}\",\"r3\":\"0x{:08x}\","
|
||||
"\"r4\":\"0x{:08x}\",\"r5\":\"0x{:08x}\",\"r6\":\"0x{:08x}\"}}}}",
|
||||
pc, EscapeJson(arg_str), r3, r4, r5, r6);
|
||||
WriteLine(line);
|
||||
}
|
||||
|
||||
} // namespace phase_a
|
||||
|
||||
// Bridge entry points referenced from shim_utils.h. Defined here so the
|
||||
// template-heavy header does not need to include event_log.h directly.
|
||||
namespace shim {
|
||||
namespace phase_a_bridge {
|
||||
|
||||
namespace {
|
||||
|
||||
// Phase C+10: read an OBJECT_ATTRIBUTES* guest address and return the
|
||||
// raw ANSI_STRING path (trimmed). Empty/null returns std::string().
|
||||
// Mirrors ours's `path::object_attributes_raw_name`.
|
||||
std::string ReadObjectAttributesRawName(uint32_t obj_attrs_ptr) {
|
||||
if (!obj_attrs_ptr) return std::string();
|
||||
auto* ks = ::xe::kernel::KernelState::shared();
|
||||
if (!ks) return std::string();
|
||||
auto* memory = ks->memory();
|
||||
if (!memory) return std::string();
|
||||
auto* obj_attrs =
|
||||
memory->TranslateVirtual<::xe::kernel::X_OBJECT_ATTRIBUTES*>(
|
||||
obj_attrs_ptr);
|
||||
if (!obj_attrs) return std::string();
|
||||
uint32_t name_ptr = obj_attrs->name_ptr;
|
||||
if (!name_ptr) return std::string();
|
||||
auto* ansi =
|
||||
memory->TranslateVirtual<::xe::kernel::X_ANSI_STRING*>(name_ptr);
|
||||
if (!ansi) return std::string();
|
||||
uint16_t length = ansi->length;
|
||||
uint32_t buffer = ansi->pointer;
|
||||
if (!length || !buffer) return std::string();
|
||||
auto* bytes = memory->TranslateVirtual<const char*>(buffer);
|
||||
if (!bytes) return std::string();
|
||||
std::string raw(bytes, length);
|
||||
// Strip trailing NULs that some callers include in the byte count.
|
||||
while (!raw.empty() && raw.back() == '\0') raw.pop_back();
|
||||
// Trim whitespace (mirror canary's `string_util::trim`).
|
||||
auto first = raw.find_first_not_of(" \t\r\n");
|
||||
auto last = raw.find_last_not_of(" \t\r\n");
|
||||
if (first == std::string::npos) return std::string();
|
||||
return raw.substr(first, last - first + 1);
|
||||
}
|
||||
|
||||
// Phase C+11 — read an `X_FILE_RENAME_INFORMATION` buffer's rename
|
||||
// target ANSI_STRING. Layout per `info/file.h:79-83` (16 bytes):
|
||||
// offset 0 be<u32> replace_existing
|
||||
// offset 4 be<u32> root_dir_handle
|
||||
// offset 8 X_ANSI_STRING { u16 Length, u16 MaxLength, u32 Buffer }
|
||||
// Caller is expected to check `info_length >= 16` before invoking.
|
||||
// Mirrors ours's `path::file_rename_information_raw_target`.
|
||||
std::string ReadFileRenameInformationRawTarget(uint32_t info_ptr,
|
||||
uint32_t info_length) {
|
||||
if (!info_ptr || info_length < 16) return std::string();
|
||||
auto* ks = ::xe::kernel::KernelState::shared();
|
||||
if (!ks) return std::string();
|
||||
auto* memory = ks->memory();
|
||||
if (!memory) return std::string();
|
||||
auto* ansi = memory->TranslateVirtual<::xe::kernel::X_ANSI_STRING*>(
|
||||
info_ptr + 8);
|
||||
if (!ansi) return std::string();
|
||||
uint16_t length = ansi->length;
|
||||
uint32_t buffer = ansi->pointer;
|
||||
if (!length || !buffer) return std::string();
|
||||
auto* bytes = memory->TranslateVirtual<const char*>(buffer);
|
||||
if (!bytes) return std::string();
|
||||
std::string raw(bytes, length);
|
||||
while (!raw.empty() && raw.back() == '\0') raw.pop_back();
|
||||
auto first = raw.find_first_not_of(" \t\r\n");
|
||||
auto last = raw.find_last_not_of(" \t\r\n");
|
||||
if (first == std::string::npos) return std::string();
|
||||
return raw.substr(first, last - first + 1);
|
||||
}
|
||||
|
||||
// Phase C+10/C+11: known path-bearing exports. Returns the empty string
|
||||
// for any export whose name is not in the set OR whose OBJECT_ATTRIBUTES*
|
||||
// arg is null. Argument positions verified against canary's
|
||||
// `xboxkrnl_io.cc` / `xboxkrnl_io_info.cc` signatures.
|
||||
std::string ResolvePathArg(const char* name,
|
||||
::xe::cpu::ppc::PPCContext* ctx) {
|
||||
if (!name || !ctx) return std::string();
|
||||
// r3..r7 only — narrow the dispatch by first char to keep the
|
||||
// hot-path branch table tight.
|
||||
if (name[0] != 'N') return std::string();
|
||||
// NtQueryFullAttributesFile: r3 = obj_attrs
|
||||
if (std::strcmp(name, "NtQueryFullAttributesFile") == 0) {
|
||||
return ReadObjectAttributesRawName(static_cast<uint32_t>(ctx->r[3]));
|
||||
}
|
||||
// NtOpenSymbolicLinkObject: r4 = obj_attrs
|
||||
if (std::strcmp(name, "NtOpenSymbolicLinkObject") == 0) {
|
||||
return ReadObjectAttributesRawName(static_cast<uint32_t>(ctx->r[4]));
|
||||
}
|
||||
// NtCreateFile, NtOpenFile: r5 = obj_attrs
|
||||
if (std::strcmp(name, "NtCreateFile") == 0 ||
|
||||
std::strcmp(name, "NtOpenFile") == 0) {
|
||||
return ReadObjectAttributesRawName(static_cast<uint32_t>(ctx->r[5]));
|
||||
}
|
||||
// NtSetInformationFile: r5 = info_ptr, r6 = info_length, r7 = info_class.
|
||||
// Surface the rename target path when info_class==10
|
||||
// (XFileRenameInformation). Mirrors ours's C+11 dispatch in
|
||||
// `crates/xenia-kernel/src/state.rs` call_export.
|
||||
if (std::strcmp(name, "NtSetInformationFile") == 0 &&
|
||||
static_cast<uint32_t>(ctx->r[7]) == 10) {
|
||||
return ReadFileRenameInformationRawTarget(
|
||||
static_cast<uint32_t>(ctx->r[5]),
|
||||
static_cast<uint32_t>(ctx->r[6]));
|
||||
}
|
||||
return std::string();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool Enabled() { return ::xe::kernel::phase_a::IsEnabled(); }
|
||||
void EmitImportAndCall(const char* module_name, uint16_t ord,
|
||||
const char* name) {
|
||||
::xe::kernel::phase_a::EmitImportCall(module_name, ord, name);
|
||||
::xe::kernel::phase_a::EmitKernelCall(name);
|
||||
}
|
||||
// Tier 2: for NtReadFile, resolve the handle to its file path (object table)
|
||||
// and emit a file.read event with the requested byte offset / length / buffer.
|
||||
// NtReadFile(r3=FileHandle, .., r8=Buffer, r9=Length, r10=ByteOffset*).
|
||||
void MaybeEmitFileRead(const char* name, ::xe::cpu::ppc::PPCContext* ctx) {
|
||||
if (!ctx || std::strcmp(name, "NtReadFile") != 0) return;
|
||||
auto* ks = ::xe::kernel::KernelState::shared();
|
||||
if (!ks) return;
|
||||
auto* memory = ks->memory();
|
||||
uint32_t handle = static_cast<uint32_t>(ctx->r[3]);
|
||||
uint32_t buffer = static_cast<uint32_t>(ctx->r[8]);
|
||||
uint32_t length = static_cast<uint32_t>(ctx->r[9]);
|
||||
uint32_t byteoff_ptr = static_cast<uint32_t>(ctx->r[10]);
|
||||
uint64_t offset = 0;
|
||||
if (byteoff_ptr && memory) {
|
||||
auto* p = memory->TranslateVirtual<::xe::be<uint64_t>*>(byteoff_ptr);
|
||||
if (p) offset = static_cast<uint64_t>(*p);
|
||||
}
|
||||
std::string path;
|
||||
auto file = ks->object_table()->LookupObject<::xe::kernel::XFile>(handle);
|
||||
if (file) path = file->path();
|
||||
::xe::kernel::phase_a::EmitFileRead(handle, path.c_str(), offset, length,
|
||||
buffer);
|
||||
}
|
||||
|
||||
void EmitImportAndCallWithCtx(const char* module_name, uint16_t ord,
|
||||
const char* name, void* ppc_context) {
|
||||
::xe::kernel::phase_a::EmitImportCall(module_name, ord, name);
|
||||
auto* ctx = reinterpret_cast<::xe::cpu::ppc::PPCContext*>(ppc_context);
|
||||
std::string path = ResolvePathArg(name, ctx);
|
||||
if (cvars::phase_a_trace_args) {
|
||||
// Extraction mode: raw GPR args + resolved path, plus file.read detail.
|
||||
::xe::kernel::phase_a::EmitKernelCallArgs(
|
||||
name, ppc_context, path.empty() ? nullptr : path.c_str());
|
||||
MaybeEmitFileRead(name, ctx);
|
||||
return;
|
||||
}
|
||||
if (!path.empty()) {
|
||||
::xe::kernel::phase_a::EmitKernelCallWithPath(name, path.c_str());
|
||||
} else {
|
||||
::xe::kernel::phase_a::EmitKernelCall(name);
|
||||
}
|
||||
}
|
||||
void EmitReturn(const char* name, uint64_t return_value) {
|
||||
::xe::kernel::phase_a::EmitKernelReturn(name, return_value);
|
||||
}
|
||||
} // namespace phase_a_bridge
|
||||
} // namespace shim
|
||||
|
||||
} // namespace kernel
|
||||
} // namespace xe
|
||||
@@ -1,191 +0,0 @@
|
||||
/**
|
||||
******************************************************************************
|
||||
* Xenia : Xbox 360 Emulator Research Project *
|
||||
******************************************************************************
|
||||
* Phase A event-log emitter. Cvar-gated (default off). Schema v1.
|
||||
* Companion: xenia-rs/audit-runs/phase-a-diff-harness/schema-v1.md
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#ifndef XENIA_KERNEL_EVENT_LOG_H_
|
||||
#define XENIA_KERNEL_EVENT_LOG_H_
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace xe {
|
||||
namespace kernel {
|
||||
namespace phase_a {
|
||||
|
||||
// Object-type codes (must match ours's enum exactly — see schema-v1.md).
|
||||
enum ObjectType : uint32_t {
|
||||
kObjUnknown = 0x00,
|
||||
kObjEvent = 0x01,
|
||||
kObjMutant = 0x02,
|
||||
kObjSemaphore = 0x03,
|
||||
kObjTimer = 0x04,
|
||||
kObjThread = 0x05,
|
||||
kObjFile = 0x06,
|
||||
kObjIoCompletion = 0x07,
|
||||
kObjModule = 0x08,
|
||||
kObjEnumState = 0x09,
|
||||
kObjSection = 0x0A,
|
||||
kObjNotification = 0x0B,
|
||||
// Phase D Stage 1: pseudo-type used as the `object_type` input to
|
||||
// `ComputeSharedGlobalSemanticId` for RTL_CRITICAL_SECTION pointers. CS
|
||||
// is not a regular `XObject` (it lives as a guest-memory struct, not a
|
||||
// handle-tabled kernel object), but the `site_sid` field of
|
||||
// `contention.observed` reuses the shared-global SID recipe so the
|
||||
// Stage-3 manifest loader can compute the same SID in both engines.
|
||||
kObjCriticalSection = 0x0C,
|
||||
};
|
||||
|
||||
// Fast bool check (default off). Inlinable so we can guard hot paths cheaply.
|
||||
bool IsEnabled();
|
||||
|
||||
// Emitted once at startup if enabled (first line of the JSONL).
|
||||
void EmitSchemaHeader();
|
||||
|
||||
// FNV-1a 64-bit identity (see schema-v1.md). Both engines compute identically.
|
||||
uint64_t ComputeSemanticId(uint32_t create_site_pc, uint32_t creating_tid,
|
||||
uint64_t tid_event_idx_at_creation,
|
||||
uint32_t object_type);
|
||||
|
||||
// One emit per imported kernel function invocation. Emitted by the export
|
||||
// trampoline before the kernel.call event.
|
||||
void EmitImportCall(const char* module_name, uint16_t ordinal,
|
||||
const char* fn_name);
|
||||
|
||||
// Kernel call entry / return. args/args_resolved are deferred to a later
|
||||
// phase; v1 emits the name + return value only (sufficient for the diff
|
||||
// tool to align by sequence).
|
||||
void EmitKernelCall(const char* name);
|
||||
// Phase C+10: schema-v1 extension — emit kernel.call whose
|
||||
// `args_resolved` field carries a resolved `"path"` value (see
|
||||
// schema-v1.md kernel.call payload). `path == nullptr || *path == '\0'`
|
||||
// degrades to the existing empty-args_resolved form.
|
||||
void EmitKernelCallWithPath(const char* name, const char* path);
|
||||
void EmitKernelReturn(const char* name, uint64_t return_value);
|
||||
|
||||
// Handle lifecycle. raw_handle_id is engine-local; the diff key is the
|
||||
// FNV-1a semantic id.
|
||||
void EmitHandleCreate(uint64_t semantic_id, uint32_t object_type,
|
||||
uint32_t raw_handle_id, const char* object_name);
|
||||
void EmitHandleDestroy(uint64_t semantic_id, uint32_t raw_handle_id,
|
||||
uint32_t prior_refcount);
|
||||
|
||||
// Thread create/exit. parent_tid is the caller; entry_pc is the spawned
|
||||
// thread's first instruction.
|
||||
void EmitThreadCreate(uint64_t semantic_id, uint32_t parent_tid,
|
||||
uint32_t entry_pc, uint32_t ctx_ptr, uint32_t priority,
|
||||
uint32_t affinity, uint32_t stack_size, bool suspended);
|
||||
void EmitThreadExit(uint32_t exit_code);
|
||||
|
||||
// Wait begin/end. handles_count + handles_semantic_ids array.
|
||||
void EmitWaitBegin(const uint64_t* handles_semantic_ids, uint32_t count,
|
||||
int64_t timeout_ns, bool alertable, bool wait_all);
|
||||
void EmitWaitEnd(uint32_t status, uint64_t woken_by_semantic_id_or_zero);
|
||||
|
||||
// Returns the next per-tid event index (post-increment). Useful for
|
||||
// `tid_event_idx_at_creation` capture before calling ComputeSemanticId.
|
||||
uint64_t PeekTidEventIdx();
|
||||
|
||||
// Phase C+15-α: handle-semantic-ID registry. Maps raw handle id ->
|
||||
// FNV-1a 64-bit SID assigned at handle creation, so subsequent
|
||||
// `handle.destroy` / `wait.begin` / etc. events can emit a stable
|
||||
// cross-engine identity. No-op when event_log is disabled.
|
||||
void RegisterHandleSemanticId(uint32_t raw_handle_id, uint64_t sid);
|
||||
uint64_t LookupHandleSemanticId(uint32_t raw_handle_id);
|
||||
uint64_t ForgetHandleSemanticId(uint32_t raw_handle_id);
|
||||
|
||||
// Convenience: at handle creation time, peek tid_event_idx, compute
|
||||
// the FNV-1a SID, register it for `raw_handle_id`, and emit a
|
||||
// `handle.create` event. Returns the SID. `create_site_pc` is set to
|
||||
// 0 by callers that cannot resolve a guest LR — both engines agree
|
||||
// on `0` for canonicalization at v1.1.
|
||||
uint64_t EmitHandleCreateAuto(uint32_t create_site_pc, uint32_t object_type,
|
||||
uint32_t raw_handle_id,
|
||||
const char* object_name);
|
||||
|
||||
// Convenience: forget mapping + emit `handle.destroy` with the
|
||||
// previously registered SID.
|
||||
void EmitHandleDestroyAuto(uint32_t raw_handle_id, uint32_t prior_refcount);
|
||||
|
||||
// Phase C+18: marker constant used as `create_site_pc` in shared-global
|
||||
// SID computation. Both engines must use exactly this value. See
|
||||
// schema-v1.md §"Shared-global SIDs".
|
||||
constexpr uint32_t kSharedGlobalSidMarker = 0xC01AB005;
|
||||
|
||||
// Phase C+18: compute the scheduling-invariant SID for a
|
||||
// **process-global** kernel dispatcher (canary's
|
||||
// `XObject::GetNativeObject` lazy-wrap on first guest-thread touch).
|
||||
// Keyed on `(SHARED_GLOBAL_SID_MARKER, 0, pointer, object_type)` so the
|
||||
// SID depends only on the object's identity, not on the scheduling order.
|
||||
uint64_t ComputeSharedGlobalSemanticId(uint32_t pointer, uint32_t object_type);
|
||||
|
||||
// Phase C+18: emit `handle.create` with a scheduling-invariant SID for
|
||||
// a process-global kernel dispatcher synthesized by
|
||||
// `XObject::GetNativeObject`. The SID is computed via
|
||||
// `ComputeSharedGlobalSemanticId(pointer, object_type)` so the same
|
||||
// dispatcher yields the same SID in both engines regardless of which
|
||||
// guest thread happens to be the first toucher. `raw_handle_id` is the
|
||||
// XObject's allocated handle; `pointer` is the guest dispatcher
|
||||
// (`X_DISPATCH_HEADER*`) pointer (used only to derive the SID).
|
||||
//
|
||||
// Caller MUST avoid the regular AddHandle emit for this XObject (set
|
||||
// the in-flight thread-local marker around the `new XEvent` /
|
||||
// `new XSemaphore` ctor and have the AddHandle hook short-circuit).
|
||||
void EmitHandleCreateSharedGlobal(uint32_t pointer, uint32_t object_type,
|
||||
uint32_t raw_handle_id,
|
||||
const char* object_name);
|
||||
|
||||
// Phase C+18: thread-local flag indicating the current host thread is
|
||||
// inside `XObject::GetNativeObject` lazy-wrap, which constructs a new
|
||||
// XEvent/XSemaphore wrapper for a process-global guest dispatcher.
|
||||
// `AddHandle` consults this to skip its regular per-thread
|
||||
// `handle.create` emit; the explicit shared-global emit happens in
|
||||
// `GetNativeObject` after `StashHandle`. The flag is a single
|
||||
// host-thread-local bool — re-entrancy is not expected (the global
|
||||
// critical region is held throughout).
|
||||
void SetInGetNativeObject(bool active);
|
||||
bool IsInGetNativeObject();
|
||||
|
||||
// Phase D Stage 1: emit a `contention.observed` event from
|
||||
// `RtlEnterCriticalSection_entry` when the spin-loop is exhausted and
|
||||
// control falls through to `xeKeWaitForSingleObject`. The Stage-2 manifest
|
||||
// builder filters on `contended=true` and keys on `(tid, tid_event_idx)`
|
||||
// so the Stage-3 replay loop in ours can park its own `rtl_enter_critical_section`
|
||||
// at exactly the same per-tid ordinal.
|
||||
//
|
||||
// `cs_guest_ptr` is the guest VA of the `X_RTL_CRITICAL_SECTION` (the
|
||||
// argument to RtlEnterCS). `site_sid` is computed via
|
||||
// `ComputeSharedGlobalSemanticId(cs_guest_ptr, kObjCriticalSection)` so
|
||||
// both engines agree on the SID for the same CS pointer.
|
||||
//
|
||||
// Gated on `cvars::kernel_emit_contention && IsEnabled()`. Default
|
||||
// behavior (cvar off) is byte-identical to pre-Stage-1 canary.
|
||||
void EmitContentionObserved(uint32_t cs_guest_ptr, bool contended);
|
||||
|
||||
// ── Expansive extraction tracing (game-data RE; gated by phase_a_trace_args /
|
||||
// phase_a_hash_probe, default off) ─────────────────────────────────────────
|
||||
|
||||
// kernel.call with populated `args` (raw r3..r10 from the PPCContext, passed
|
||||
// as void*) and `args_resolved` (the resolved file path when known).
|
||||
void EmitKernelCallArgs(const char* name, void* ppc_context,
|
||||
const char* resolved_path);
|
||||
|
||||
// file.read — a guest read from an open file handle (path resolved from the
|
||||
// handle via the object table). Enables correlating .pak reads to TOC entries.
|
||||
void EmitFileRead(uint32_t handle, const char* path, uint64_t offset,
|
||||
uint32_t length, uint32_t buffer_va);
|
||||
|
||||
// guest.call — emitted by the guest-PC hash probe: `arg_str` is r3 dereferenced
|
||||
// as a guest C-string (the archive path being hashed). Used to recover the
|
||||
// custom IPFB/IDXD name-hash by observing (string -> hash) pairs.
|
||||
void EmitGuestCall(uint32_t pc, const char* arg_str, uint32_t r3, uint32_t r4,
|
||||
uint32_t r5, uint32_t r6);
|
||||
|
||||
} // namespace phase_a
|
||||
} // namespace kernel
|
||||
} // namespace xe
|
||||
|
||||
#endif // XENIA_KERNEL_EVENT_LOG_H_
|
||||
@@ -68,6 +68,9 @@ KernelState::KernelState(Emulator* emulator)
|
||||
InitializeKernelGuestGlobals();
|
||||
kernel_version_ = KernelVersion(cvars::kernel_build_version);
|
||||
|
||||
// Hardcoded maximum of 2048 TLS slots.
|
||||
tls_bitmap_.Resize(2048);
|
||||
|
||||
auto hc_loc_heap = memory_->LookupHeap(strange_hardcoded_page_);
|
||||
bool fixed_alloc_worked = hc_loc_heap->AllocFixed(
|
||||
strange_hardcoded_page_, 65536, 0,
|
||||
@@ -138,132 +141,18 @@ const std::unique_ptr<xam::SpaInfo> KernelState::module_xdbf(
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
uint32_t KernelState::AllocateTLS(cpu::ppc::PPCContext* context) {
|
||||
auto globals =
|
||||
memory()->TranslateVirtual<KernelGuestGlobals*>(GetKernelGuestGlobals());
|
||||
auto tls_lock = &globals->tls_lock;
|
||||
auto old_irql = xboxkrnl::xeKeKfAcquireSpinLock(context, tls_lock);
|
||||
uint32_t KernelState::AllocateTLS() { return uint32_t(tls_bitmap_.Acquire()); }
|
||||
|
||||
int result = -1;
|
||||
|
||||
auto current_thread = XThread::GetCurrentThread();
|
||||
if (!current_thread) {
|
||||
XELOGE("AllocateTLS: No current thread");
|
||||
xboxkrnl::xeKeKfReleaseSpinLock(context, tls_lock, old_irql);
|
||||
return X_TLS_OUT_OF_INDEXES;
|
||||
}
|
||||
|
||||
auto process_ptr = memory()->TranslateVirtual(
|
||||
current_thread->guest_object<X_KTHREAD>()->process);
|
||||
if (!process_ptr) {
|
||||
XELOGE("AllocateTLS: Failed to translate process pointer");
|
||||
xboxkrnl::xeKeKfReleaseSpinLock(context, tls_lock, old_irql);
|
||||
return X_TLS_OUT_OF_INDEXES;
|
||||
}
|
||||
|
||||
// Search for a free TLS slot in the process bitmap
|
||||
// Bitmap format: 1 = free, 0 = allocated
|
||||
// 8 x 32-bit words = 256 total TLS slots
|
||||
for (xe::be<uint32_t>* i = &process_ptr->tls_slot_bitmap[0];
|
||||
i < &process_ptr->tls_slot_bitmap[8]; ++i) {
|
||||
// Read bitmap value (handles big-endian conversion)
|
||||
uint32_t bitmap_value = static_cast<uint32_t>(*i);
|
||||
|
||||
// Find highest free slot using lzcnt (leading zero count)
|
||||
// Returns 0-31 if a bit is set, 32 if no bits are set
|
||||
uint32_t leading_zeros = xe::lzcnt(bitmap_value);
|
||||
|
||||
if (leading_zeros != 32) {
|
||||
// Calculate absolute slot index from bitmap position and bit offset
|
||||
// Each bitmap word represents 32 slots
|
||||
size_t bitmap_index = i - &process_ptr->tls_slot_bitmap[0];
|
||||
uint32_t base_slot = static_cast<uint32_t>(bitmap_index) * 32;
|
||||
int calculated_slot = base_slot + leading_zeros;
|
||||
|
||||
// Validate slot is within Xbox 360 TLS range
|
||||
if (calculated_slot >= 0 && calculated_slot < 256) {
|
||||
result = calculated_slot;
|
||||
|
||||
// Clear the bit to mark as allocated
|
||||
// lzcnt returns 0 for bit 31, 31 for bit 0
|
||||
uint32_t bit_index = 31 - leading_zeros;
|
||||
*i = bitmap_value & ~(1U << bit_index);
|
||||
break;
|
||||
} else {
|
||||
XELOGE("AllocateTLS: Invalid slot calculation: {}", calculated_slot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result == -1) {
|
||||
XELOGW("AllocateTLS: All TLS slots exhausted for current process");
|
||||
}
|
||||
|
||||
xboxkrnl::xeKeKfReleaseSpinLock(context, tls_lock, old_irql);
|
||||
return static_cast<uint32_t>(result);
|
||||
}
|
||||
|
||||
void KernelState::FreeTLS(cpu::ppc::PPCContext* context, uint32_t slot) {
|
||||
if (slot >= 256) {
|
||||
XELOGE("FreeTLS: Invalid slot index {}", slot);
|
||||
return;
|
||||
}
|
||||
|
||||
auto current_thread = XThread::GetCurrentThread();
|
||||
if (!current_thread) {
|
||||
XELOGE("FreeTLS: No current thread");
|
||||
return;
|
||||
}
|
||||
|
||||
auto current_kthread = current_thread->guest_object<X_KTHREAD>();
|
||||
if (!current_kthread) {
|
||||
XELOGE("FreeTLS: Failed to get guest thread object");
|
||||
return;
|
||||
}
|
||||
|
||||
auto process_ptr = memory()->TranslateVirtual(current_kthread->process);
|
||||
if (!process_ptr) {
|
||||
XELOGE("FreeTLS: Failed to translate process pointer");
|
||||
return;
|
||||
}
|
||||
|
||||
auto globals =
|
||||
memory()->TranslateVirtual<KernelGuestGlobals*>(GetKernelGuestGlobals());
|
||||
auto tls_lock = &globals->tls_lock;
|
||||
auto old_irql = xboxkrnl::xeKeKfAcquireSpinLock(context, tls_lock);
|
||||
|
||||
uint32_t bitmap_index = slot / 32;
|
||||
uint32_t bit_mask = 1U << (31 - (slot % 32));
|
||||
uint32_t bitmap_value =
|
||||
static_cast<uint32_t>(process_ptr->tls_slot_bitmap[bitmap_index]);
|
||||
|
||||
if (bitmap_value & bit_mask) {
|
||||
XELOGW("FreeTLS: Slot {} is already free", slot);
|
||||
xboxkrnl::xeKeKfReleaseSpinLock(context, tls_lock, old_irql);
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear TLS values in all threads of this process
|
||||
void KernelState::FreeTLS(uint32_t slot) {
|
||||
const std::vector<object_ref<XThread>> threads =
|
||||
object_table()->GetObjectsByType<XThread>();
|
||||
|
||||
uint32_t current_process_ptr = current_kthread->process.m_ptr;
|
||||
for (const object_ref<XThread>& thread : threads) {
|
||||
if (!thread || !thread->is_guest_thread()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto thread_kthread = thread->guest_object<X_KTHREAD>();
|
||||
if (thread_kthread &&
|
||||
thread_kthread->process.m_ptr == current_process_ptr) {
|
||||
if (thread->is_guest_thread()) {
|
||||
thread->SetTLSValue(slot, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Mark slot as free in bitmap
|
||||
process_ptr->tls_slot_bitmap[bitmap_index] = bitmap_value | bit_mask;
|
||||
|
||||
xboxkrnl::xeKeKfReleaseSpinLock(context, tls_lock, old_irql);
|
||||
tls_bitmap_.Release(slot);
|
||||
}
|
||||
|
||||
void KernelState::RegisterTitleTerminateNotification(uint32_t routine,
|
||||
@@ -426,6 +315,11 @@ object_ref<XThread> KernelState::LaunchModule(object_ref<UserModule> module) {
|
||||
// Waits for a debugger client, if desired.
|
||||
emulator()->processor()->PreLaunch();
|
||||
|
||||
// Resume the thread now.
|
||||
// If the debugger has requested a suspend this will just decrement the
|
||||
// suspend count without resuming it until the debugger wants.
|
||||
thread->Resume();
|
||||
|
||||
return thread;
|
||||
}
|
||||
|
||||
@@ -898,7 +792,10 @@ void KernelState::TerminateTitle() {
|
||||
|
||||
// Third: Unload all user modules (including the executable).
|
||||
for (size_t i = 0; i < user_modules_.size(); i++) {
|
||||
user_modules_[i]->ReleaseHandle();
|
||||
X_STATUS status = user_modules_[i]->Unload();
|
||||
assert_true(XSUCCEEDED(status));
|
||||
|
||||
object_table_.RemoveHandle(user_modules_[i]->handle());
|
||||
}
|
||||
user_modules_.clear();
|
||||
|
||||
@@ -908,6 +805,9 @@ void KernelState::TerminateTitle() {
|
||||
// Unregister all notify listeners.
|
||||
notify_listeners_.clear();
|
||||
|
||||
// Clear the TLS map.
|
||||
tls_bitmap_.Reset();
|
||||
|
||||
// Unset the executable module.
|
||||
executable_module_ = nullptr;
|
||||
|
||||
@@ -1177,6 +1077,12 @@ bool KernelState::Save(ByteStream* stream) {
|
||||
object_table_.Save(stream);
|
||||
|
||||
// Write the TLS allocation bitmap
|
||||
auto tls_bitmap = tls_bitmap_.data();
|
||||
stream->Write(uint32_t(tls_bitmap.size()));
|
||||
for (size_t i = 0; i < tls_bitmap.size(); i++) {
|
||||
stream->Write<uint64_t>(tls_bitmap[i]);
|
||||
}
|
||||
|
||||
// We save XThreads absolutely first, as they will execute code upon save
|
||||
// (which could modify the kernel state)
|
||||
auto threads = object_table_.GetObjectsByType<XThread>();
|
||||
@@ -1304,11 +1210,12 @@ bool KernelState::Restore(ByteStream* stream) {
|
||||
// Restore the object table
|
||||
object_table_.Restore(stream);
|
||||
|
||||
// TLS bitmap is now stored per-process in X_KPROCESS structures (in guest
|
||||
// memory) Skip reading old global TLS bitmap if present in old save files
|
||||
// Read the TLS allocation bitmap
|
||||
auto num_bitmap_entries = stream->Read<uint32_t>();
|
||||
auto& tls_bitmap = tls_bitmap_.data();
|
||||
tls_bitmap.resize(num_bitmap_entries);
|
||||
for (uint32_t i = 0; i < num_bitmap_entries; i++) {
|
||||
stream->Read<uint64_t>(); // Discard old data
|
||||
tls_bitmap[i] = stream->Read<uint64_t>();
|
||||
}
|
||||
|
||||
uint32_t num_threads = stream->Read<uint32_t>();
|
||||
@@ -1442,13 +1349,12 @@ void KernelState::SetProcessTLSVars(X_KPROCESS* process, int num_slots,
|
||||
process->tls_slot_size = 4 * slots_padded;
|
||||
uint32_t count_div32 = slots_padded / 32;
|
||||
for (unsigned word_index = 0; word_index < count_div32; ++word_index) {
|
||||
process->tls_slot_bitmap[word_index] = -1;
|
||||
process->bitmap[word_index] = -1;
|
||||
}
|
||||
|
||||
// set remainder of bitset
|
||||
if (((num_slots + 3) & 0x1C) != 0)
|
||||
process->tls_slot_bitmap[count_div32] = -1
|
||||
<< (32 - ((num_slots + 3) & 0x1C));
|
||||
process->bitmap[count_div32] = -1 << (32 - ((num_slots + 3) & 0x1C));
|
||||
}
|
||||
void AllocateThread(PPCContext* context) {
|
||||
uint32_t thread_mem_size = static_cast<uint32_t>(context->r[3]);
|
||||
|
||||
@@ -80,7 +80,7 @@ struct X_KPROCESS {
|
||||
uint8_t is_terminating;
|
||||
// one of X_PROCTYPE_
|
||||
uint8_t process_type;
|
||||
xe::be<uint32_t> tls_slot_bitmap[8];
|
||||
xe::be<uint32_t> bitmap[8];
|
||||
xe::be<uint32_t> unk_50;
|
||||
X_LIST_ENTRY unk_54;
|
||||
xe::be<uint32_t> unk_5C;
|
||||
@@ -140,7 +140,6 @@ struct KernelGuestGlobals {
|
||||
// this lock is only used in some Ob functions. It's odd that it is used at
|
||||
// all, as each table already has its own spinlock.
|
||||
X_KSPINLOCK ob_lock;
|
||||
X_KSPINLOCK tls_lock; // protects per-process TLS bitmap allocations
|
||||
|
||||
// if LLE emulating Xam, this is needed or you get an immediate freeze
|
||||
X_KEVENT UsbdBootEnumerationDoneEvent;
|
||||
@@ -220,8 +219,8 @@ class KernelState {
|
||||
return kernel_guest_globals_ + offsetof(KernelGuestGlobals, idle_process);
|
||||
}
|
||||
|
||||
uint32_t AllocateTLS(cpu::ppc::PPCContext* context);
|
||||
void FreeTLS(cpu::ppc::PPCContext* context, uint32_t slot);
|
||||
uint32_t AllocateTLS();
|
||||
void FreeTLS(uint32_t slot);
|
||||
|
||||
void RegisterTitleTerminateNotification(uint32_t routine, uint32_t priority);
|
||||
void RemoveTitleTerminateNotification(uint32_t routine);
|
||||
@@ -383,6 +382,7 @@ class KernelState {
|
||||
std::condition_variable_any dispatch_cond_;
|
||||
std::list<std::function<void()>> dispatch_queue_;
|
||||
|
||||
BitMap tls_bitmap_;
|
||||
uint32_t ke_timestamp_bundle_ptr_ = 0;
|
||||
std::unique_ptr<xe::threading::HighResolutionTimer> timestamp_timer_;
|
||||
uint32_t quantum_timer_counter_ = 0;
|
||||
|
||||
@@ -276,7 +276,7 @@ void ObjectTable::PurgeAllObjects() {
|
||||
auto& entry = table_[slot];
|
||||
if (entry.object) {
|
||||
entry.handle_ref_count = 0;
|
||||
entry.object->ReleaseHandle();
|
||||
entry.object->Release();
|
||||
|
||||
entry.object = nullptr;
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ X_HRESULT XmpApp::XMPCreateTitlePlaylist(
|
||||
memory_->TranslateVirtual(song_descriptor[i].genre_ptr));
|
||||
song->track_number = song_descriptor[i].track_number;
|
||||
song->duration_ms = song_descriptor[i].duration;
|
||||
song->format = static_cast<SongFormat>(
|
||||
song->format = static_cast<Song::Format>(
|
||||
xe::byte_swap<uint32_t>(song_descriptor[i].song_format));
|
||||
|
||||
if (out_song_handles) {
|
||||
@@ -148,24 +148,25 @@ X_HRESULT XmpApp::XMPPrevious() {
|
||||
return X_E_SUCCESS;
|
||||
}
|
||||
|
||||
X_HRESULT XmpApp::XMPGetTitlePlaylistBufferSize(apu::XMP_CLIENT xmp_client,
|
||||
X_HRESULT XmpApp::XMPGetTitlePlaylistBufferSize(uint32_t xmp_client,
|
||||
uint32_t song_count,
|
||||
uint32_t size_ptr) {
|
||||
/* Note:
|
||||
- Query of size for XamAlloc - the result of the alloc is passed to
|
||||
0x0007000D.
|
||||
- xmp_client can range from 0 - 6 but will fail on 1 and set size to zero
|
||||
if its anything other than 0 or 2.
|
||||
*/
|
||||
XELOGD(
|
||||
"XMPGetTitlePlaylistBufferSize(XMP client: 0x{:08X}, Song count: "
|
||||
"0x{:08X}, Size ptr: 0x{:08X})",
|
||||
uint32_t(xmp_client), song_count, size_ptr);
|
||||
xmp_client, song_count, size_ptr);
|
||||
|
||||
if (xmp_client == apu::XMP_CLIENT::HUD || !size_ptr || !song_count) {
|
||||
if (xmp_client == 1 || !size_ptr || !song_count) {
|
||||
return X_E_INVALIDARG;
|
||||
}
|
||||
uint32_t size = 0;
|
||||
if (xmp_client == apu::XMP_CLIENT::Dash ||
|
||||
xmp_client == apu::XMP_CLIENT::Game) {
|
||||
if (xmp_client == 0 || xmp_client == 2) {
|
||||
size = song_count * 0x3E8 + 0x88;
|
||||
}
|
||||
// We don't use the storage, so just fudge the number.
|
||||
@@ -185,41 +186,37 @@ X_HRESULT XmpApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr,
|
||||
reinterpret_cast<XMP_PLAY_TITLE_PLAYLIST*>(buffer);
|
||||
uint32_t playlist_handle = xe::load_and_swap<uint32_t>(
|
||||
memory_->TranslateVirtual(args->storage_ptr));
|
||||
assert_true(args->xmp_client == apu::XMP_CLIENT::Game);
|
||||
assert_true(args->xmp_client == 0x00000002);
|
||||
return XMPPlayTitlePlaylist(playlist_handle, args->song_handle);
|
||||
}
|
||||
case 0x00070003: {
|
||||
assert_true(!buffer_length || buffer_length == 4);
|
||||
apu::XMP_CLIENT xmp_client =
|
||||
static_cast<apu::XMP_CLIENT>(xe::load_and_swap<uint32_t>(buffer));
|
||||
assert_true(xmp_client == apu::XMP_CLIENT::Game);
|
||||
uint32_t xmp_client = xe::load_and_swap<uint32_t>(buffer + 0);
|
||||
assert_true(xmp_client == 0x00000002);
|
||||
return XMPContinue();
|
||||
}
|
||||
case 0x00070004: {
|
||||
assert_true(!buffer_length || buffer_length == sizeof(XMP_STOP));
|
||||
XMP_STOP* args = reinterpret_cast<XMP_STOP*>(buffer);
|
||||
assert_true(args->xmp_client == apu::XMP_CLIENT::Game);
|
||||
return XMPStop(args->allow_restart);
|
||||
assert_true(args->xmp_client == 0x00000002);
|
||||
return XMPStop(args->unk);
|
||||
}
|
||||
case 0x00070005: {
|
||||
assert_true(!buffer_length || buffer_length == 4);
|
||||
apu::XMP_CLIENT xmp_client =
|
||||
static_cast<apu::XMP_CLIENT>(xe::load_and_swap<uint32_t>(buffer));
|
||||
assert_true(xmp_client == apu::XMP_CLIENT::Game);
|
||||
uint32_t xmp_client = xe::load_and_swap<uint32_t>(buffer + 0);
|
||||
assert_true(xmp_client == 0x00000002);
|
||||
return XMPPause();
|
||||
}
|
||||
case 0x00070006: {
|
||||
assert_true(!buffer_length || buffer_length == 4);
|
||||
apu::XMP_CLIENT xmp_client =
|
||||
static_cast<apu::XMP_CLIENT>(xe::load_and_swap<uint32_t>(buffer));
|
||||
assert_true(xmp_client == apu::XMP_CLIENT::Game);
|
||||
uint32_t xmp_client = xe::load_and_swap<uint32_t>(buffer + 0);
|
||||
assert_true(xmp_client == 0x00000002);
|
||||
return XMPNext();
|
||||
}
|
||||
case 0x00070007: {
|
||||
assert_true(!buffer_length || buffer_length == 4);
|
||||
apu::XMP_CLIENT xmp_client =
|
||||
static_cast<apu::XMP_CLIENT>(xe::load_and_swap<uint32_t>(buffer));
|
||||
assert_true(xmp_client == apu::XMP_CLIENT::Game);
|
||||
uint32_t xmp_client = xe::load_and_swap<uint32_t>(buffer + 0);
|
||||
assert_true(xmp_client == 0x00000002);
|
||||
return XMPPrevious();
|
||||
}
|
||||
case 0x00070008: {
|
||||
@@ -233,10 +230,10 @@ X_HRESULT XmpApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr,
|
||||
XMP_SET_PLAYBACK_BEHAVIOR* args =
|
||||
reinterpret_cast<XMP_SET_PLAYBACK_BEHAVIOR*>(buffer);
|
||||
|
||||
assert_true(args->xmp_client == apu::XMP_CLIENT::Game ||
|
||||
args->xmp_client == apu::XMP_CLIENT::Dash);
|
||||
assert_true(args->xmp_client == 0x00000002 ||
|
||||
args->xmp_client == 0x00000000);
|
||||
XELOGD("XMPSetPlaybackBehavior({:08X}, {:08X}, {:08X}, {:08X})",
|
||||
uint32_t(args->xmp_client.get()), uint32_t(args->playback_mode),
|
||||
uint32_t(args->xmp_client), uint32_t(args->playback_mode),
|
||||
uint32_t(args->repeat_mode), uint32_t(args->flags));
|
||||
|
||||
kernel_state_->emulator()->audio_media_player()->SetPlaybackMode(
|
||||
@@ -253,14 +250,14 @@ X_HRESULT XmpApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr,
|
||||
case 0x00070009: {
|
||||
assert_true(!buffer_length || buffer_length == sizeof(XMP_GET_STATUS));
|
||||
XMP_GET_STATUS* args = reinterpret_cast<XMP_GET_STATUS*>(buffer);
|
||||
assert_true(args->xmp_client == apu::XMP_CLIENT::Game);
|
||||
assert_true(args->xmp_client == 0x00000002);
|
||||
return XMPGetStatus(args->state_ptr);
|
||||
}
|
||||
case 0x0007000B: {
|
||||
assert_true(!buffer_length || buffer_length == sizeof(XMP_GET_VOLUME));
|
||||
XMP_GET_VOLUME* args = reinterpret_cast<XMP_GET_VOLUME*>(buffer);
|
||||
|
||||
assert_true(args->xmp_client == apu::XMP_CLIENT::Game);
|
||||
assert_true(args->xmp_client == 0x00000002);
|
||||
XELOGD("XMPGetVolume({:08X})", uint32_t(args->volume_ptr));
|
||||
|
||||
xe::store_and_swap<float>(
|
||||
@@ -272,8 +269,8 @@ X_HRESULT XmpApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr,
|
||||
assert_true(!buffer_length || buffer_length == sizeof(XMP_SET_VOLUME));
|
||||
XMP_SET_VOLUME* args = reinterpret_cast<XMP_SET_VOLUME*>(buffer);
|
||||
|
||||
assert_true(args->xmp_client == apu::XMP_CLIENT::Game);
|
||||
XELOGD("XMPSetVolume({:d}, {:g})", uint32_t(args->xmp_client.get()),
|
||||
assert_true(args->xmp_client == 0x00000002);
|
||||
XELOGD("XMPSetVolume({:d}, {:g})", args->xmp_client.get(),
|
||||
float(args->value));
|
||||
kernel_state_->emulator()->audio_media_player()->SetVolume(
|
||||
float(args->value));
|
||||
@@ -288,8 +285,8 @@ X_HRESULT XmpApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr,
|
||||
xe::store_and_swap<uint32_t>(
|
||||
memory_->TranslateVirtual(args->playlist_handle_ptr),
|
||||
args->storage_ptr);
|
||||
assert_true(args->xmp_client == apu::XMP_CLIENT::Game ||
|
||||
args->xmp_client == apu::XMP_CLIENT::Dash);
|
||||
assert_true(args->xmp_client == 0x00000002 ||
|
||||
args->xmp_client == 0x00000000);
|
||||
std::u16string playlist_name;
|
||||
if (!args->playlist_name_ptr) {
|
||||
playlist_name = u"";
|
||||
@@ -310,7 +307,7 @@ X_HRESULT XmpApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr,
|
||||
reinterpret_cast<XMP_GET_CURRENT_SONG*>(buffer);
|
||||
|
||||
auto info = memory_->TranslateVirtual<XMP_SONGINFO*>(args->info_ptr);
|
||||
assert_true(args->xmp_client == apu::XMP_CLIENT::Game);
|
||||
assert_true(args->xmp_client == 0x00000002);
|
||||
assert_zero(args->unk_ptr);
|
||||
XELOGD("XMPGetCurrentSong({:08X}, {:08X})", uint32_t(args->unk_ptr),
|
||||
uint32_t(args->info_ptr));
|
||||
@@ -344,8 +341,8 @@ X_HRESULT XmpApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr,
|
||||
|
||||
uint32_t playlist_handle = xe::load_and_swap<uint32_t>(
|
||||
memory_->TranslateVirtual(args->storage_ptr));
|
||||
assert_true(args->xmp_client == apu::XMP_CLIENT::Game ||
|
||||
args->xmp_client == apu::XMP_CLIENT::Dash);
|
||||
assert_true(args->xmp_client == 0x00000002 ||
|
||||
args->xmp_client == 0x00000000);
|
||||
return XMPDeleteTitlePlaylist(playlist_handle);
|
||||
}
|
||||
case 0x0007001A: {
|
||||
@@ -355,45 +352,21 @@ X_HRESULT XmpApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr,
|
||||
XMP_SET_PLAYBACK_CONTROLLER* args =
|
||||
reinterpret_cast<XMP_SET_PLAYBACK_CONTROLLER*>(buffer);
|
||||
|
||||
const bool XMP_User_Dash =
|
||||
args->xmp_client == apu::XMP_CLIENT::Dash &&
|
||||
args->playback_controller_request == apu::PlaybackController::User;
|
||||
const bool XMPOverrideBackgroundMusic =
|
||||
args->xmp_client == apu::XMP_CLIENT::Game &&
|
||||
args->playback_controller_request == apu::PlaybackController::Game;
|
||||
const bool XMPRestoreBackgroundMusic =
|
||||
args->xmp_client == apu::XMP_CLIENT::Game &&
|
||||
args->playback_controller_request == apu::PlaybackController::Restore;
|
||||
|
||||
assert_true(XMPOverrideBackgroundMusic || XMPRestoreBackgroundMusic ||
|
||||
XMP_User_Dash);
|
||||
|
||||
assert_true(
|
||||
(args->xmp_client == 0x00000002 && args->controller == 0x00000000) ||
|
||||
(args->xmp_client == 0x00000000 && args->controller == 0x00000001));
|
||||
XELOGD("XMPSetPlaybackController({:08X}, {:08X}, {:08X})",
|
||||
uint32_t(args->xmp_client.get()),
|
||||
uint32_t(args->playback_controller_request.get()),
|
||||
uint32_t(args->playback_controller_locked));
|
||||
uint32_t(args->xmp_client), uint32_t(args->controller),
|
||||
uint32_t(args->playback_client));
|
||||
|
||||
auto media_player = kernel_state_->emulator()->audio_media_player();
|
||||
kernel_state_->emulator()->audio_media_player()->SetPlaybackClient(
|
||||
PlaybackClient(uint32_t(args->playback_client)));
|
||||
|
||||
if (args->playback_controller_request ==
|
||||
apu::PlaybackController::Restore) {
|
||||
media_player->SetXMPClient(apu::XMP_CLIENT::Game);
|
||||
media_player->SetPlaybackController(apu::PlaybackController::Game);
|
||||
} else {
|
||||
media_player->SetXMPClient(args->xmp_client);
|
||||
media_player->SetPlaybackController(
|
||||
args->playback_controller_request.get());
|
||||
}
|
||||
|
||||
media_player->SetXMPOverride(args->playback_controller_locked.get());
|
||||
|
||||
// 58411446
|
||||
kernel_state_->BroadcastNotification(
|
||||
kXNotificationXmpPlaybackControllerChanged,
|
||||
media_player->IsXMPOverrideEnabled()
|
||||
? false
|
||||
: media_player->IsTitleInPlaybackControl());
|
||||
|
||||
kernel_state_->emulator()
|
||||
->audio_media_player()
|
||||
->IsTitleInPlaybackControl());
|
||||
return X_E_SUCCESS;
|
||||
}
|
||||
case 0x0007001B: {
|
||||
@@ -403,36 +376,20 @@ X_HRESULT XmpApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr,
|
||||
XMP_GET_PLAYBACK_CONTROLLER* args =
|
||||
reinterpret_cast<XMP_GET_PLAYBACK_CONTROLLER*>(buffer);
|
||||
|
||||
assert_true(args->xmp_client == apu::XMP_CLIENT::Game);
|
||||
assert_true(args->xmp_client == 0x00000002);
|
||||
XELOGD("XMPGetPlaybackController({:08X}, {:08X}, {:08X})",
|
||||
uint32_t(args->xmp_client.get()),
|
||||
uint32_t(args->playback_controller_ptr),
|
||||
uint32_t(args->playback_controller_locked_ptr));
|
||||
|
||||
const auto media_player = kernel_state_->emulator()->audio_media_player();
|
||||
|
||||
xe::be<apu::PlaybackController>* controller =
|
||||
memory_->TranslateVirtual<xe::be<apu::PlaybackController>*>(
|
||||
args->playback_controller_ptr);
|
||||
|
||||
*controller = media_player->GetPlaybackController();
|
||||
|
||||
xe::be<uint32_t>* playback_controller_locked =
|
||||
memory_->TranslateVirtual<xe::be<uint32_t>*>(
|
||||
args->playback_controller_locked_ptr);
|
||||
|
||||
*playback_controller_locked = !media_player->IsTitleInPlaybackControl();
|
||||
uint32_t(args->xmp_client), uint32_t(args->controller_ptr),
|
||||
uint32_t(args->locked_ptr));
|
||||
xe::store_and_swap<uint32_t>(
|
||||
memory_->TranslateVirtual(args->controller_ptr), 0);
|
||||
xe::store_and_swap<uint32_t>(memory_->TranslateVirtual(args->locked_ptr),
|
||||
0);
|
||||
|
||||
if (!XThread::GetCurrentThread()->main_thread()) {
|
||||
// Atrain spawns a thread 82437FD0 to call this in a tight loop forever.
|
||||
xe::threading::Sleep(std::chrono::milliseconds(10));
|
||||
}
|
||||
|
||||
// Assert if game is not in control of playback.
|
||||
assert_true(media_player->GetPlaybackController() ==
|
||||
apu::PlaybackController::Game);
|
||||
assert_zero(!media_player->IsTitleInPlaybackControl());
|
||||
|
||||
return X_E_SUCCESS;
|
||||
}
|
||||
case 0x00070025: {
|
||||
@@ -445,7 +402,7 @@ X_HRESULT XmpApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr,
|
||||
reinterpret_cast<XMP_CREATE_USER_PLAYLIST_ENUMERATOR*>(buffer);
|
||||
|
||||
XELOGD("XMPCreateUserPlaylistEnumerator({:08X}, {:08X}, {:08X})",
|
||||
uint32_t(args->xmp_client.get()), uint32_t(args->flags),
|
||||
uint32_t(args->xmp_client), uint32_t(args->flags),
|
||||
uint32_t(args->object_ptr));
|
||||
return X_E_SUCCESS;
|
||||
}
|
||||
@@ -456,11 +413,11 @@ X_HRESULT XmpApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr,
|
||||
XMP_GET_PLAYBACK_BEHAVIOR* args =
|
||||
reinterpret_cast<XMP_GET_PLAYBACK_BEHAVIOR*>(buffer);
|
||||
|
||||
assert_true(args->xmp_client == apu::XMP_CLIENT::Game ||
|
||||
args->xmp_client == apu::XMP_CLIENT::Dash);
|
||||
assert_true(args->xmp_client == 0x00000002 ||
|
||||
args->xmp_client == 0x00000000);
|
||||
XELOGD("XMPGetPlaybackBehavior({:08X}, {:08X}, {:08X}, {:08X})",
|
||||
uint32_t(args->xmp_client.get()),
|
||||
uint32_t(args->playback_mode_ptr), uint32_t(args->repeat_mode_ptr),
|
||||
uint32_t(args->xmp_client), uint32_t(args->playback_mode_ptr),
|
||||
uint32_t(args->repeat_mode_ptr),
|
||||
uint32_t(args->playback_flags_ptr));
|
||||
if (args->playback_mode_ptr) {
|
||||
xe::store_and_swap<uint32_t>(
|
||||
@@ -494,15 +451,13 @@ X_HRESULT XmpApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr,
|
||||
XMP_GET_MEDIA_SOURCES* args =
|
||||
reinterpret_cast<XMP_GET_MEDIA_SOURCES*>(buffer);
|
||||
|
||||
assert_true(args->xmp_client == apu::XMP_CLIENT::Game ||
|
||||
args->xmp_client == apu::XMP_CLIENT::Dash);
|
||||
assert_true(args->xmp_client == 0x00000002 ||
|
||||
args->xmp_client == 0x00000000);
|
||||
XELOGD(
|
||||
"XMPGetMediaSources({:08X}, {:08X}, {:08X}, {:08X}, {:08X}), "
|
||||
"unimplemented",
|
||||
uint32_t(args->xmp_client.get()),
|
||||
args->get_connected_sources_only.get(),
|
||||
args->media_resources_ptr.get(), args->max_source.get(),
|
||||
args->sources_returned_ptr.get());
|
||||
args->xmp_client.get(), args->unk1.get(), args->unk1_ptr.get(),
|
||||
args->unk2.get(), args->unk2_ptr.get());
|
||||
return X_E_INVALIDARG;
|
||||
}
|
||||
case 0x0007002E: {
|
||||
@@ -519,12 +474,12 @@ X_HRESULT XmpApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr,
|
||||
assert_true(!buffer_length || buffer_length == sizeof(XMP_DASH_INIT));
|
||||
XMP_DASH_INIT* args = reinterpret_cast<XMP_DASH_INIT*>(buffer);
|
||||
|
||||
assert_true(args->xmp_client == apu::XMP_CLIENT::Game ||
|
||||
args->xmp_client == apu::XMP_CLIENT::Dash);
|
||||
assert_true(args->xmp_client == 0x00000002 ||
|
||||
args->xmp_client == 0x00000000);
|
||||
XELOGD(
|
||||
"XMPDashInIt({:08X}, {:08X}, {:08X}, {:08X}, {:08X}, {:08X}), "
|
||||
"unimplemented",
|
||||
uint32_t(args->xmp_client.get()), args->buffer_ptr.get(),
|
||||
args->xmp_client.get(), args->buffer_ptr.get(),
|
||||
args->buffer_length.get(), args->unk1.get(), args->unk2.get(),
|
||||
args->storage_ptr.get());
|
||||
return X_E_INVALIDARG;
|
||||
@@ -536,8 +491,8 @@ X_HRESULT XmpApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr,
|
||||
XMP_CAPTURE_OUTPUT* args = reinterpret_cast<XMP_CAPTURE_OUTPUT*>(buffer);
|
||||
|
||||
XELOGD("XMPCaptureOutput({:08X}, {:08X}, {:08X}, {:08X})",
|
||||
uint32_t(args->xmp_client.get()), args->callback.get(),
|
||||
args->context.get(), args->title_render.get());
|
||||
args->xmp_client.get(), args->callback.get(), args->context.get(),
|
||||
args->title_render.get());
|
||||
kernel_state_->emulator()->audio_media_player()->SetCaptureCallback(
|
||||
args->callback, args->context, static_cast<bool>(args->title_render));
|
||||
return X_E_SUCCESS;
|
||||
@@ -552,14 +507,14 @@ X_HRESULT XmpApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr,
|
||||
XMP_SET_MEDIA_SOURCE_WORKSPACE* args =
|
||||
reinterpret_cast<XMP_SET_MEDIA_SOURCE_WORKSPACE*>(buffer);
|
||||
|
||||
assert_true(args->xmp_client == apu::XMP_CLIENT::Game ||
|
||||
args->xmp_client == apu::XMP_CLIENT::HUD ||
|
||||
args->xmp_client == apu::XMP_CLIENT::Dash);
|
||||
assert_true(args->xmp_client == 0x00000002 ||
|
||||
args->xmp_client == 0x00000001 ||
|
||||
args->xmp_client == 0x00000000);
|
||||
XELOGD(
|
||||
"XMPSetMediaSourceWorkspace({:08X}, {:08X}, {:08X}, {:08X}), "
|
||||
"unimplemented",
|
||||
uint32_t(args->xmp_client.get()), args->unk1.get(),
|
||||
args->storage_ptr.get(), args->unk2.get());
|
||||
args->xmp_client.get(), args->unk1.get(), args->storage_ptr.get(),
|
||||
args->unk2.get());
|
||||
return X_E_INVALIDARG;
|
||||
}
|
||||
case 0x00070053: {
|
||||
@@ -567,8 +522,8 @@ X_HRESULT XmpApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr,
|
||||
// picture or video library. It only receives buffer
|
||||
XMP_GET_DASH_INIT_STATE* args =
|
||||
reinterpret_cast<XMP_GET_DASH_INIT_STATE*>(buffer);
|
||||
XELOGD("XMPGetDashInItState({:08X}, {:08X})",
|
||||
uint32_t(args->xmp_client.get()), args->dash_init_state_ptr.get());
|
||||
XELOGD("XMPGetDashInItState({:08X}, {:08X})", args->xmp_client.get(),
|
||||
args->dash_init_state_ptr.get());
|
||||
|
||||
xe::store_and_swap<uint32_t>(
|
||||
memory_->TranslateVirtual(args->dash_init_state_ptr),
|
||||
|
||||
@@ -13,28 +13,6 @@
|
||||
#include "xenia/kernel/kernel_state.h"
|
||||
#include "xenia/kernel/xam/app_manager.h"
|
||||
|
||||
namespace xe {
|
||||
namespace apu {
|
||||
enum class XMP_CLIENT : uint32_t {
|
||||
Dash,
|
||||
HUD,
|
||||
Game,
|
||||
Remote,
|
||||
MusicPlayer,
|
||||
MSAL,
|
||||
MCE,
|
||||
};
|
||||
|
||||
enum class PlaybackController : uint32_t {
|
||||
Game,
|
||||
User,
|
||||
Dash,
|
||||
MCE,
|
||||
Restore,
|
||||
};
|
||||
} // namespace apu
|
||||
} // namespace xe
|
||||
|
||||
namespace xe {
|
||||
namespace kernel {
|
||||
namespace xam {
|
||||
@@ -70,25 +48,24 @@ struct XMP_SONGINFO {
|
||||
xe::be<uint32_t> track_number;
|
||||
xe::be<uint32_t> duration;
|
||||
xe::be<uint32_t> song_format;
|
||||
xe::be<uint32_t> unknown_1;
|
||||
};
|
||||
static_assert_size(XMP_SONGINFO, 0x3E0);
|
||||
static_assert_size(XMP_SONGINFO, 988);
|
||||
|
||||
struct XMP_PLAY_TITLE_PLAYLIST {
|
||||
xe::be<apu::XMP_CLIENT> xmp_client;
|
||||
xe::be<uint32_t> xmp_client;
|
||||
xe::be<uint32_t> storage_ptr;
|
||||
xe::be<uint32_t> song_handle;
|
||||
};
|
||||
static_assert_size(XMP_PLAY_TITLE_PLAYLIST, 0xC);
|
||||
|
||||
struct XMP_STOP {
|
||||
xe::be<apu::XMP_CLIENT> xmp_client;
|
||||
xe::be<uint32_t> allow_restart;
|
||||
xe::be<uint32_t> xmp_client;
|
||||
xe::be<uint32_t> unk;
|
||||
};
|
||||
static_assert_size(XMP_STOP, 0x8);
|
||||
|
||||
struct XMP_SET_PLAYBACK_BEHAVIOR {
|
||||
xe::be<apu::XMP_CLIENT> xmp_client;
|
||||
xe::be<uint32_t> xmp_client;
|
||||
xe::be<uint32_t> playback_mode;
|
||||
xe::be<uint32_t> repeat_mode;
|
||||
xe::be<uint32_t> flags;
|
||||
@@ -96,25 +73,25 @@ struct XMP_SET_PLAYBACK_BEHAVIOR {
|
||||
static_assert_size(XMP_SET_PLAYBACK_BEHAVIOR, 0x10);
|
||||
|
||||
struct XMP_GET_STATUS {
|
||||
xe::be<apu::XMP_CLIENT> xmp_client;
|
||||
xe::be<uint32_t> xmp_client;
|
||||
xe::be<uint32_t> state_ptr;
|
||||
};
|
||||
static_assert_size(XMP_GET_STATUS, 0x8);
|
||||
|
||||
struct XMP_GET_VOLUME {
|
||||
xe::be<apu::XMP_CLIENT> xmp_client;
|
||||
xe::be<uint32_t> xmp_client;
|
||||
xe::be<uint32_t> volume_ptr;
|
||||
};
|
||||
static_assert_size(XMP_GET_VOLUME, 0x8);
|
||||
|
||||
struct XMP_SET_VOLUME {
|
||||
xe::be<apu::XMP_CLIENT> xmp_client;
|
||||
xe::be<uint32_t> xmp_client;
|
||||
xe::be<float> value;
|
||||
};
|
||||
static_assert_size(XMP_SET_VOLUME, 0x8);
|
||||
|
||||
struct XMP_CREATE_TITLE_PLAYLIST {
|
||||
xe::be<apu::XMP_CLIENT> xmp_client;
|
||||
xe::be<uint32_t> xmp_client;
|
||||
xe::be<uint32_t> storage_ptr;
|
||||
xe::be<uint32_t> storage_size;
|
||||
xe::be<uint32_t> songs_ptr;
|
||||
@@ -127,41 +104,41 @@ struct XMP_CREATE_TITLE_PLAYLIST {
|
||||
static_assert_size(XMP_CREATE_TITLE_PLAYLIST, 0x24);
|
||||
|
||||
struct XMP_GET_CURRENT_SONG {
|
||||
xe::be<apu::XMP_CLIENT> xmp_client;
|
||||
xe::be<uint32_t> xmp_client;
|
||||
xe::be<uint32_t> unk_ptr;
|
||||
xe::be<uint32_t> info_ptr;
|
||||
};
|
||||
static_assert_size(XMP_GET_CURRENT_SONG, 0xC);
|
||||
|
||||
struct XMP_DELETE_TITLE_PLAYLIST {
|
||||
xe::be<apu::XMP_CLIENT> xmp_client;
|
||||
xe::be<uint32_t> xmp_client;
|
||||
xe::be<uint32_t> storage_ptr;
|
||||
};
|
||||
static_assert_size(XMP_DELETE_TITLE_PLAYLIST, 0x8);
|
||||
|
||||
struct XMP_SET_PLAYBACK_CONTROLLER {
|
||||
xe::be<apu::XMP_CLIENT> xmp_client;
|
||||
xe::be<apu::PlaybackController> playback_controller_request;
|
||||
xe::be<uint32_t> playback_controller_locked;
|
||||
xe::be<uint32_t> xmp_client;
|
||||
xe::be<uint32_t> controller;
|
||||
xe::be<uint32_t> playback_client;
|
||||
};
|
||||
static_assert_size(XMP_SET_PLAYBACK_CONTROLLER, 0xC);
|
||||
|
||||
struct XMP_GET_PLAYBACK_CONTROLLER {
|
||||
xe::be<apu::XMP_CLIENT> xmp_client;
|
||||
xe::be<uint32_t> playback_controller_ptr;
|
||||
xe::be<uint32_t> playback_controller_locked_ptr;
|
||||
xe::be<uint32_t> xmp_client;
|
||||
xe::be<uint32_t> controller_ptr;
|
||||
xe::be<uint32_t> locked_ptr;
|
||||
};
|
||||
static_assert_size(XMP_GET_PLAYBACK_CONTROLLER, 0xC);
|
||||
|
||||
struct XMP_CREATE_USER_PLAYLIST_ENUMERATOR {
|
||||
xe::be<apu::XMP_CLIENT> xmp_client;
|
||||
xe::be<uint32_t> xmp_client;
|
||||
xe::be<uint32_t> flags;
|
||||
xe::be<uint32_t> object_ptr;
|
||||
};
|
||||
static_assert_size(XMP_CREATE_USER_PLAYLIST_ENUMERATOR, 0xC);
|
||||
|
||||
struct XMP_GET_PLAYBACK_BEHAVIOR {
|
||||
xe::be<apu::XMP_CLIENT> xmp_client;
|
||||
xe::be<uint32_t> xmp_client;
|
||||
xe::be<uint32_t> playback_mode_ptr;
|
||||
xe::be<uint32_t> repeat_mode_ptr;
|
||||
xe::be<uint32_t> playback_flags_ptr;
|
||||
@@ -169,23 +146,23 @@ struct XMP_GET_PLAYBACK_BEHAVIOR {
|
||||
static_assert_size(XMP_GET_PLAYBACK_BEHAVIOR, 0x10);
|
||||
|
||||
struct XMP_GET_MEDIA_SOURCES {
|
||||
xe::be<apu::XMP_CLIENT> xmp_client;
|
||||
xe::be<uint32_t> get_connected_sources_only;
|
||||
xe::be<uint32_t> media_resources_ptr; // *MSAL_MEDIASOURCEINFO
|
||||
xe::be<uint32_t> max_source;
|
||||
xe::be<uint32_t> sources_returned_ptr;
|
||||
xe::be<uint32_t> xmp_client;
|
||||
xe::be<uint32_t> unk1;
|
||||
xe::be<uint32_t> unk1_ptr;
|
||||
xe::be<uint32_t> unk2;
|
||||
xe::be<uint32_t> unk2_ptr;
|
||||
};
|
||||
static_assert_size(XMP_GET_MEDIA_SOURCES, 0x14);
|
||||
|
||||
struct XMP_GET_TITLE_PLAYLIST_BUFFER_SIZE {
|
||||
xe::be<apu::XMP_CLIENT> xmp_client;
|
||||
xe::be<uint32_t> xmp_client;
|
||||
xe::be<uint32_t> song_count;
|
||||
xe::be<uint32_t> size_ptr;
|
||||
};
|
||||
static_assert_size(XMP_GET_TITLE_PLAYLIST_BUFFER_SIZE, 0xC);
|
||||
|
||||
struct XMP_DASH_INIT {
|
||||
xe::be<apu::XMP_CLIENT> xmp_client;
|
||||
xe::be<uint32_t> xmp_client;
|
||||
xe::be<uint32_t> buffer_ptr; // used by XamEnumerate
|
||||
xe::be<uint32_t> buffer_length; // used by XamEnumerate
|
||||
xe::be<uint32_t> unk1;
|
||||
@@ -195,7 +172,7 @@ struct XMP_DASH_INIT {
|
||||
static_assert_size(XMP_DASH_INIT, 0x18);
|
||||
|
||||
struct XMP_CAPTURE_OUTPUT {
|
||||
xe::be<apu::XMP_CLIENT> xmp_client;
|
||||
xe::be<uint32_t> xmp_client;
|
||||
xe::be<uint32_t> callback;
|
||||
xe::be<uint32_t> context;
|
||||
xe::be<uint32_t> title_render;
|
||||
@@ -203,7 +180,7 @@ struct XMP_CAPTURE_OUTPUT {
|
||||
static_assert_size(XMP_CAPTURE_OUTPUT, 0x10);
|
||||
|
||||
struct XMP_SET_MEDIA_SOURCE_WORKSPACE {
|
||||
xe::be<apu::XMP_CLIENT> xmp_client;
|
||||
xe::be<uint32_t> xmp_client;
|
||||
xe::be<uint32_t> unk1;
|
||||
xe::be<uint32_t> storage_ptr;
|
||||
xe::be<uint32_t> unk2;
|
||||
@@ -211,7 +188,7 @@ struct XMP_SET_MEDIA_SOURCE_WORKSPACE {
|
||||
static_assert_size(XMP_SET_MEDIA_SOURCE_WORKSPACE, 0x10);
|
||||
|
||||
struct XMP_GET_DASH_INIT_STATE {
|
||||
xe::be<apu::XMP_CLIENT> xmp_client;
|
||||
xe::be<uint32_t> xmp_client;
|
||||
xe::be<uint32_t> dash_init_state_ptr;
|
||||
};
|
||||
static_assert_size(XMP_GET_DASH_INIT_STATE, 0x8);
|
||||
@@ -223,6 +200,10 @@ class XmpApp : public App {
|
||||
kPlaying = 1,
|
||||
kPaused = 2,
|
||||
};
|
||||
enum class PlaybackClient : uint32_t {
|
||||
kSystem = 0,
|
||||
kTitle = 1,
|
||||
};
|
||||
enum class PlaybackMode : uint32_t {
|
||||
kInOrder = 0,
|
||||
kShuffle = 1,
|
||||
@@ -235,12 +216,12 @@ class XmpApp : public App {
|
||||
kDefault = 0,
|
||||
kAutoPause = 1,
|
||||
};
|
||||
enum class SongFormat : uint32_t {
|
||||
kWma = 0,
|
||||
kMp3 = 1,
|
||||
};
|
||||
|
||||
struct Song {
|
||||
enum class Format : uint32_t {
|
||||
kWma = 0,
|
||||
kMp3 = 1,
|
||||
};
|
||||
|
||||
uint32_t handle;
|
||||
std::u16string file_path;
|
||||
std::u16string name;
|
||||
@@ -250,7 +231,7 @@ class XmpApp : public App {
|
||||
std::u16string genre;
|
||||
uint32_t track_number;
|
||||
uint32_t duration_ms;
|
||||
SongFormat format;
|
||||
Format format;
|
||||
};
|
||||
struct Playlist {
|
||||
uint32_t handle;
|
||||
@@ -276,7 +257,7 @@ class XmpApp : public App {
|
||||
X_HRESULT XMPPause();
|
||||
X_HRESULT XMPNext();
|
||||
X_HRESULT XMPPrevious();
|
||||
X_HRESULT XMPGetTitlePlaylistBufferSize(apu::XMP_CLIENT xmp_client,
|
||||
X_HRESULT XMPGetTitlePlaylistBufferSize(uint32_t xmp_client,
|
||||
uint32_t song_count,
|
||||
uint32_t storage_ptr);
|
||||
|
||||
|
||||
@@ -38,13 +38,12 @@ void CreateProfileUI::OnDraw(ImGuiIO& io) {
|
||||
}
|
||||
|
||||
ImGui::TextUnformatted("Gamertag:");
|
||||
const bool enter_pressed =
|
||||
ImGui::InputText("##Gamertag", gamertag_, sizeof(gamertag_),
|
||||
ImGuiInputTextFlags_EnterReturnsTrue);
|
||||
valid_gamertag_ = profile_manager->IsGamertagValid(std::string(gamertag_));
|
||||
if (ImGui::InputText("##Gamertag", gamertag_, sizeof(gamertag_))) {
|
||||
valid_gamertag_ = profile_manager->IsGamertagValid(std::string(gamertag_));
|
||||
}
|
||||
|
||||
ImGui::BeginDisabled(!valid_gamertag_);
|
||||
if (ImGui::Button("Create") || (enter_pressed && valid_gamertag_)) {
|
||||
if (ImGui::Button("Create")) {
|
||||
bool autologin = (profile_manager->GetAccountCount() == 0);
|
||||
if (profile_manager->CreateProfile(std::string(gamertag_), autologin,
|
||||
migration_) &&
|
||||
|
||||
@@ -702,43 +702,69 @@ void XamLoaderGetMediaInfo_entry(lpdword_t media_type, lpdword_t unk2) {
|
||||
}
|
||||
DECLARE_XAM_EXPORT1(XamLoaderGetMediaInfo, kNone, kStub);
|
||||
|
||||
dword_result_t xeXamContentLaunchImage(dword_t user_index,
|
||||
lpstring_t image_location,
|
||||
lpvoid_t content_data_ptr,
|
||||
dword_t content_data_size,
|
||||
lpstring_t xex_path, dword_t flag) {
|
||||
/* Notes:
|
||||
- In code this subfunction is used by all XamContentLaunchImage
|
||||
functions
|
||||
- Due to the current implementation of content data we can't use
|
||||
XCONTENT_DATA_INTERNAL
|
||||
- flags used by XamLoaderLaunchTitleEx
|
||||
- user_index used by xeXamContentOpenFile
|
||||
- if image_location null use xeXamContentOpenFile else use
|
||||
exXamContentCreate
|
||||
- root_name is "XSYSLAUNCH" while XamLoaderLaunchTitleEx uses
|
||||
"XSYSLAUNCH:\\""
|
||||
- title_id is usually written into first 8 characters of filename
|
||||
*/
|
||||
vfs::Entry* entry;
|
||||
if (!image_location) {
|
||||
XCONTENT_AGGREGATE_DATA content_data =
|
||||
*content_data_ptr.as<XCONTENT_DATA*>();
|
||||
const uint32_t title_id = xe::string_util::from_string<uint32_t>(
|
||||
content_data.file_name().substr(0, 8), true);
|
||||
dword_result_t XamContentLaunchImageFromFileInternal_entry(
|
||||
lpstring_t image_location, lpstring_t xex_name, dword_t unk) {
|
||||
const std::string image_path = static_cast<std::string>(image_location);
|
||||
const std::string xex_name_ = static_cast<std::string>(xex_name);
|
||||
|
||||
// This should be done via content_manager, however as it isn't capable of
|
||||
// such action we need to improvise.
|
||||
const std::string package_path =
|
||||
fmt::format("GAME:/Content/0000000000000000/{:08X}/{:08X}/{}", title_id,
|
||||
static_cast<uint32_t>(content_data.content_type.get()),
|
||||
content_data.file_name());
|
||||
vfs::Entry* entry = kernel_state()->file_system()->ResolvePath(image_path);
|
||||
|
||||
entry = kernel_state()->file_system()->ResolvePath(package_path);
|
||||
} else {
|
||||
entry = kernel_state()->file_system()->ResolvePath(image_location.value());
|
||||
if (!entry) {
|
||||
return X_STATUS_NO_SUCH_FILE;
|
||||
}
|
||||
|
||||
const std::filesystem::path host_path =
|
||||
kernel_state()->emulator()->content_root() / entry->name();
|
||||
if (!std::filesystem::exists(host_path)) {
|
||||
uint64_t progress = 0;
|
||||
|
||||
vfs::VirtualFileSystem::ExtractContentFile(
|
||||
entry, kernel_state()->emulator()->content_root(), progress, true);
|
||||
}
|
||||
|
||||
auto xam = kernel_state()->GetKernelModule<XamModule>("xam.xex");
|
||||
|
||||
auto& loader_data = xam->loader_data();
|
||||
loader_data.host_path = xe::path_to_utf8(host_path);
|
||||
loader_data.launch_path = xex_name_;
|
||||
|
||||
xam->SaveLoaderData();
|
||||
|
||||
auto display_window = kernel_state()->emulator()->display_window();
|
||||
auto imgui_drawer = kernel_state()->emulator()->imgui_drawer();
|
||||
|
||||
if (display_window && imgui_drawer) {
|
||||
display_window->app_context().CallInUIThreadSynchronous([imgui_drawer]() {
|
||||
xe::ui::ImGuiDialog::ShowMessageBox(
|
||||
imgui_drawer, "Launching new title!",
|
||||
"Launching new title. \nPlease close Xenia and launch it again. Game "
|
||||
"should load automatically.");
|
||||
});
|
||||
}
|
||||
|
||||
kernel_state()->TerminateTitle();
|
||||
return X_ERROR_SUCCESS;
|
||||
}
|
||||
|
||||
DECLARE_XAM_EXPORT1(XamContentLaunchImageFromFileInternal, kContent, kStub);
|
||||
|
||||
dword_result_t XamContentLaunchImageInternal_entry(lpvoid_t content_data_ptr,
|
||||
lpstring_t xex_path) {
|
||||
XCONTENT_AGGREGATE_DATA content_data = *content_data_ptr.as<XCONTENT_DATA*>();
|
||||
|
||||
// title_id is written into first 8 characters of filename
|
||||
const uint32_t title_id = xe::string_util::from_string<uint32_t>(
|
||||
content_data.file_name().substr(0, 8), true);
|
||||
|
||||
// This should be done via content_manager, however as it isn't capable of
|
||||
// such action we need to improvise.
|
||||
const std::string package_path =
|
||||
fmt::format("GAME:/Content/0000000000000000/{:08X}/{:08X}/{}", title_id,
|
||||
static_cast<uint32_t>(content_data.content_type.get()),
|
||||
content_data.file_name());
|
||||
|
||||
auto entry = kernel_state()->file_system()->ResolvePath(package_path);
|
||||
|
||||
if (!entry) {
|
||||
return X_STATUS_NO_SUCH_FILE;
|
||||
}
|
||||
@@ -748,7 +774,7 @@ dword_result_t xeXamContentLaunchImage(dword_t user_index,
|
||||
|
||||
if (!std::filesystem::exists(host_path)) {
|
||||
uint64_t progress = 0;
|
||||
vfs::VirtualFileSystem::ExtractContentFile(
|
||||
kernel_state()->file_system()->ExtractContentFile(
|
||||
entry, kernel_state()->emulator()->content_root(), progress, true);
|
||||
}
|
||||
|
||||
@@ -776,38 +802,8 @@ dword_result_t xeXamContentLaunchImage(dword_t user_index,
|
||||
return X_ERROR_SUCCESS;
|
||||
}
|
||||
|
||||
dword_result_t XamContentLaunchImageFromFileInternal_entry(
|
||||
lpstring_t image_location, lpstring_t xex_name) {
|
||||
return xeXamContentLaunchImage(XUserIndexNone, image_location, nullptr, NULL,
|
||||
xex_name, NULL);
|
||||
}
|
||||
DECLARE_XAM_EXPORT1(XamContentLaunchImageFromFileInternal, kContent, kStub);
|
||||
|
||||
dword_result_t XamContentLaunchImage_entry(dword_t user_index,
|
||||
lpvoid_t content_data_ptr,
|
||||
lpstring_t xex_path) {
|
||||
return xeXamContentLaunchImage(user_index, nullptr, content_data_ptr,
|
||||
sizeof(XCONTENT_DATA), xex_path, NULL);
|
||||
}
|
||||
DECLARE_XAM_EXPORT1(XamContentLaunchImage, kContent, kStub);
|
||||
|
||||
dword_result_t XamContentLaunchImageInternal_entry(lpvoid_t content_data_ptr,
|
||||
lpstring_t xex_path) {
|
||||
return xeXamContentLaunchImage(XUserIndexNone, nullptr, content_data_ptr,
|
||||
sizeof(XCONTENT_DATA_INTERNAL), xex_path,
|
||||
NULL);
|
||||
}
|
||||
DECLARE_XAM_EXPORT1(XamContentLaunchImageInternal, kContent, kStub);
|
||||
|
||||
dword_result_t XamContentLaunchImageInternalEx_entry(lpvoid_t content_data_ptr,
|
||||
lpstring_t xex_path,
|
||||
dword_t flags) {
|
||||
return xeXamContentLaunchImage(XUserIndexNone, nullptr, content_data_ptr,
|
||||
sizeof(XCONTENT_DATA_INTERNAL), xex_path,
|
||||
flags);
|
||||
}
|
||||
DECLARE_XAM_EXPORT1(XamContentLaunchImageInternalEx, kContent, kStub);
|
||||
|
||||
void XamContentRegisterChangeCallback_entry(dword_t callback) {
|
||||
kernel_state()->xam_state()->SetContentRegisterCallback(callback);
|
||||
}
|
||||
|
||||
@@ -383,43 +383,27 @@ void XamLoaderLaunchTitle_entry(lpstring_t raw_name_ptr, dword_t flags) {
|
||||
auto& loader_data = xam->loader_data();
|
||||
loader_data.launch_flags = flags;
|
||||
|
||||
std::string title;
|
||||
std::string message;
|
||||
|
||||
// Translate the launch path to a full path.
|
||||
if (raw_name_ptr && !raw_name_ptr.value().empty()) {
|
||||
loader_data.launch_path = xe::path_to_utf8(raw_name_ptr.value());
|
||||
loader_data.launch_data_present = true;
|
||||
xam->SaveLoaderData();
|
||||
title = "Title was restarted";
|
||||
message =
|
||||
"Title closed with new launch data. \nPlease restart Xenia. "
|
||||
"Game will be loaded automatically.";
|
||||
|
||||
auto display_window = kernel_state()->emulator()->display_window();
|
||||
auto imgui_drawer = kernel_state()->emulator()->imgui_drawer();
|
||||
|
||||
if (display_window && imgui_drawer) {
|
||||
display_window->app_context().CallInUIThreadSynchronous([imgui_drawer]() {
|
||||
xe::ui::ImGuiDialog::ShowMessageBox(
|
||||
imgui_drawer, "Title was restarted",
|
||||
"Title closed with new launch data. \nPlease restart Xenia. "
|
||||
"Game will be loaded automatically.");
|
||||
});
|
||||
}
|
||||
} else {
|
||||
title = "Title terminated";
|
||||
message = "Game requested exit to dashboard.";
|
||||
assert_always("Game requested exit to dashboard via XamLoaderLaunchTitle");
|
||||
}
|
||||
|
||||
auto display_window = kernel_state()->emulator()->display_window();
|
||||
auto imgui_drawer = kernel_state()->emulator()->imgui_drawer();
|
||||
|
||||
if (display_window && imgui_drawer) {
|
||||
display_window->app_context().CallInUIThreadSynchronous(
|
||||
[imgui_drawer, title, message]() {
|
||||
auto dialog = xe::ui::ImGuiDialog::ShowMessageBox(
|
||||
imgui_drawer, title.c_str(), message.c_str());
|
||||
|
||||
std::jthread([dialog]() {
|
||||
while (!dialog->IsClosing()) {
|
||||
std::this_thread::yield();
|
||||
}
|
||||
|
||||
std::quick_exit(0);
|
||||
}).detach();
|
||||
});
|
||||
}
|
||||
|
||||
// This function does not return.
|
||||
kernel_state()->TerminateTitle();
|
||||
}
|
||||
|
||||
@@ -10,9 +10,7 @@
|
||||
#include "xenia/kernel/kernel_state.h"
|
||||
#include "xenia/kernel/util/shim_utils.h"
|
||||
#include "xenia/kernel/xam/xam_private.h"
|
||||
#include "xenia/kernel/xboxkrnl/xboxkrnl_threading.h"
|
||||
#include "xenia/kernel/xnotifylistener.h"
|
||||
#include "xenia/kernel/xthread.h"
|
||||
#include "xenia/xbox.h"
|
||||
|
||||
namespace xe {
|
||||
@@ -29,7 +27,7 @@ uint32_t xeXamNotifyCreateListener(uint64_t mask, uint32_t is_system,
|
||||
|
||||
auto listener =
|
||||
object_ref<XNotifyListener>(new XNotifyListener(kernel_state()));
|
||||
listener->Initialize(mask, is_system, max_version);
|
||||
listener->Initialize(mask, max_version);
|
||||
|
||||
// Handle ref is incremented, so return that.
|
||||
uint32_t handle = listener->handle();
|
||||
@@ -39,10 +37,7 @@ uint32_t xeXamNotifyCreateListener(uint64_t mask, uint32_t is_system,
|
||||
|
||||
dword_result_t XamNotifyCreateListener_entry(qword_t mask,
|
||||
dword_t max_version) {
|
||||
auto thread = kernel::XThread::GetCurrentThread();
|
||||
auto ctx = thread->thread_state()->context();
|
||||
auto type = xboxkrnl::xeKeGetCurrentProcessType(ctx);
|
||||
return xeXamNotifyCreateListener(mask, type == 2, max_version);
|
||||
return xeXamNotifyCreateListener(mask, 0, max_version);
|
||||
}
|
||||
DECLARE_XAM_EXPORT1(XamNotifyCreateListener, kNone, kImplemented);
|
||||
|
||||
|
||||
@@ -331,17 +331,12 @@ dword_result_t KeSetAffinityThread_entry(lpvoid_t thread_ptr, dword_t affinity,
|
||||
return X_STATUS_INVALID_PARAMETER;
|
||||
}
|
||||
auto thread = XObject::GetNativeObject<XThread>(kernel_state(), thread_ptr);
|
||||
if (!thread) {
|
||||
XELOGW(
|
||||
"KeSetAffinityThread: guest thread pointer {:08X} did not resolve to "
|
||||
"an XThread; returning STATUS_INVALID_HANDLE",
|
||||
thread_ptr.guest_address());
|
||||
return X_STATUS_INVALID_HANDLE;
|
||||
if (thread) {
|
||||
if (previous_affinity_ptr) {
|
||||
*previous_affinity_ptr = uint32_t(1) << thread->active_cpu();
|
||||
}
|
||||
thread->SetAffinity(affinity);
|
||||
}
|
||||
if (previous_affinity_ptr) {
|
||||
*previous_affinity_ptr = uint32_t(1) << thread->active_cpu();
|
||||
}
|
||||
thread->SetAffinity(affinity);
|
||||
return X_STATUS_SUCCESS;
|
||||
}
|
||||
DECLARE_XBOXKRNL_EXPORT1(KeSetAffinityThread, kThreading, kImplemented);
|
||||
@@ -474,8 +469,8 @@ void KeQuerySystemTime_entry(lpqword_t time_ptr, const ppc_context_t& ctx) {
|
||||
DECLARE_XBOXKRNL_EXPORT1(KeQuerySystemTime, kThreading, kImplemented);
|
||||
|
||||
// https://msdn.microsoft.com/en-us/library/ms686801
|
||||
dword_result_t KeTlsAlloc_entry(const ppc_context_t& context) {
|
||||
uint32_t slot = kernel_state()->AllocateTLS(context);
|
||||
dword_result_t KeTlsAlloc_entry() {
|
||||
uint32_t slot = kernel_state()->AllocateTLS();
|
||||
XThread::GetCurrentThread()->SetTLSValue(slot, 0);
|
||||
|
||||
return slot;
|
||||
@@ -483,13 +478,12 @@ dword_result_t KeTlsAlloc_entry(const ppc_context_t& context) {
|
||||
DECLARE_XBOXKRNL_EXPORT1(KeTlsAlloc, kThreading, kImplemented);
|
||||
|
||||
// https://msdn.microsoft.com/en-us/library/ms686804
|
||||
dword_result_t KeTlsFree_entry(dword_t tls_index,
|
||||
const ppc_context_t& context) {
|
||||
dword_result_t KeTlsFree_entry(dword_t tls_index) {
|
||||
if (tls_index == X_TLS_OUT_OF_INDEXES) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
kernel_state()->FreeTLS(context, tls_index);
|
||||
kernel_state()->FreeTLS(tls_index);
|
||||
return 1;
|
||||
}
|
||||
DECLARE_XBOXKRNL_EXPORT1(KeTlsFree, kThreading, kImplemented);
|
||||
@@ -780,11 +774,13 @@ dword_result_t NtReleaseSemaphore_entry(dword_t sem_handle,
|
||||
bool success =
|
||||
sem->ReleaseSemaphore((int32_t)release_count, &previous_count);
|
||||
if (!success) {
|
||||
// Releasing would exceed the semaphore's maximum count
|
||||
// Windows returns STATUS_SEMAPHORE_LIMIT_EXCEEDED (0x0000012B)
|
||||
XELOGW(
|
||||
"NtReleaseSemaphore: release_count={} would exceed maximum (current "
|
||||
"count={})",
|
||||
uint32_t(release_count), previous_count);
|
||||
result = X_STATUS_SEMAPHORE_LIMIT_EXCEEDED;
|
||||
result = 0x0000012B;
|
||||
}
|
||||
} else {
|
||||
result = X_STATUS_INVALID_HANDLE;
|
||||
|
||||
@@ -22,14 +22,12 @@ XNotifyListener::XNotifyListener(KernelState* kernel_state)
|
||||
|
||||
XNotifyListener::~XNotifyListener() {}
|
||||
|
||||
void XNotifyListener::Initialize(uint64_t mask, uint32_t is_system,
|
||||
uint32_t max_version) {
|
||||
void XNotifyListener::Initialize(uint64_t mask, uint32_t max_version) {
|
||||
assert_false(wait_handle_);
|
||||
|
||||
wait_handle_ = xe::threading::Event::CreateManualResetEvent(false);
|
||||
assert_not_null(wait_handle_);
|
||||
mask_ = mask;
|
||||
is_system_ = is_system;
|
||||
max_version_ = max_version;
|
||||
|
||||
kernel_state_->RegisterNotifyListener(this);
|
||||
@@ -111,9 +109,8 @@ object_ref<XNotifyListener> XNotifyListener::Restore(KernelState* kernel_state,
|
||||
notify->RestoreObject(stream);
|
||||
|
||||
auto mask = stream->Read<uint64_t>();
|
||||
auto is_system = stream->Read<uint32_t>();
|
||||
auto max_version = stream->Read<uint32_t>();
|
||||
notify->Initialize(mask, is_system, max_version);
|
||||
notify->Initialize(mask, max_version);
|
||||
|
||||
auto notification_count_ = stream->Read<size_t>();
|
||||
for (size_t i = 0; i < notification_count_; i++) {
|
||||
|
||||
@@ -48,10 +48,9 @@ class XNotifyListener : public XObject {
|
||||
~XNotifyListener() override;
|
||||
|
||||
uint64_t mask() const { return mask_; }
|
||||
uint32_t is_system() const { return is_system_; }
|
||||
uint32_t max_version() const { return max_version_; }
|
||||
|
||||
void Initialize(uint64_t mask, uint32_t is_system, uint32_t max_version);
|
||||
void Initialize(uint64_t mask, uint32_t max_version);
|
||||
|
||||
void EnqueueNotification(XNotificationID id, uint32_t data);
|
||||
bool DequeueNotification(XNotificationID* out_id, uint32_t* out_data);
|
||||
@@ -71,7 +70,6 @@ class XNotifyListener : public XObject {
|
||||
xe::global_critical_region global_critical_region_;
|
||||
std::vector<std::pair<XNotificationID, uint32_t>> notifications_;
|
||||
uint64_t mask_ = 0;
|
||||
uint32_t is_system_ = 0;
|
||||
uint32_t max_version_ = 0;
|
||||
};
|
||||
|
||||
|
||||
@@ -803,10 +803,6 @@ void BaseHeap::Initialize(Memory* memory, uint8_t* membase, HeapType heap_type,
|
||||
host_address_offset_ = host_address_offset;
|
||||
page_table_.resize(heap_size / page_size);
|
||||
unreserved_page_count_ = uint32_t(page_table_.size());
|
||||
|
||||
// Initialize free block tracker with a single block covering the entire heap.
|
||||
free_blocks_.clear();
|
||||
free_blocks_[0] = uint32_t(page_table_.size());
|
||||
}
|
||||
|
||||
void BaseHeap::Dispose() {
|
||||
@@ -820,7 +816,6 @@ void BaseHeap::Dispose() {
|
||||
page_number += page_entry.region_page_count;
|
||||
}
|
||||
}
|
||||
free_blocks_.clear();
|
||||
}
|
||||
|
||||
void BaseHeap::DumpMap() {
|
||||
@@ -943,98 +938,14 @@ bool BaseHeap::Restore(ByteStream* stream) {
|
||||
}
|
||||
}
|
||||
|
||||
RebuildFreeBlocks();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void BaseHeap::RebuildFreeBlocks() {
|
||||
free_blocks_.clear();
|
||||
uint32_t run_start = UINT32_MAX;
|
||||
for (uint32_t i = 0; i < uint32_t(page_table_.size()); ++i) {
|
||||
if (page_table_[i].state == 0) {
|
||||
if (run_start == UINT32_MAX) {
|
||||
run_start = i;
|
||||
}
|
||||
} else {
|
||||
if (run_start != UINT32_MAX) {
|
||||
free_blocks_[run_start] = i - run_start;
|
||||
run_start = UINT32_MAX;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (run_start != UINT32_MAX) {
|
||||
free_blocks_[run_start] = uint32_t(page_table_.size()) - run_start;
|
||||
}
|
||||
}
|
||||
|
||||
void BaseHeap::RemoveFreeBlock(uint32_t start_page, uint32_t page_count) {
|
||||
if (free_blocks_.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the free block that contains the allocated range.
|
||||
auto it = free_blocks_.upper_bound(start_page);
|
||||
if (it != free_blocks_.begin()) {
|
||||
--it;
|
||||
}
|
||||
|
||||
// Verify the block actually contains our range.
|
||||
uint32_t block_start = it->first;
|
||||
uint32_t block_count = it->second;
|
||||
uint32_t block_end = block_start + block_count;
|
||||
assert_true(start_page >= block_start &&
|
||||
start_page + page_count <= block_end);
|
||||
|
||||
free_blocks_.erase(it);
|
||||
|
||||
// Insert remnant before the allocated range.
|
||||
if (block_start < start_page) {
|
||||
free_blocks_[block_start] = start_page - block_start;
|
||||
}
|
||||
|
||||
// Insert remnant after the allocated range.
|
||||
uint32_t alloc_end = start_page + page_count;
|
||||
if (alloc_end < block_end) {
|
||||
free_blocks_[alloc_end] = block_end - alloc_end;
|
||||
}
|
||||
}
|
||||
|
||||
void BaseHeap::InsertFreeBlock(uint32_t start_page, uint32_t page_count) {
|
||||
uint32_t new_start = start_page;
|
||||
uint32_t new_count = page_count;
|
||||
|
||||
// Try to merge with block immediately after.
|
||||
auto it_after = free_blocks_.find(start_page + page_count);
|
||||
if (it_after != free_blocks_.end()) {
|
||||
new_count += it_after->second;
|
||||
free_blocks_.erase(it_after);
|
||||
}
|
||||
|
||||
// Try to merge with block immediately before.
|
||||
auto it_at = free_blocks_.lower_bound(start_page);
|
||||
if (it_at != free_blocks_.begin()) {
|
||||
auto it_before = std::prev(it_at);
|
||||
if (it_before->first + it_before->second == start_page) {
|
||||
new_start = it_before->first;
|
||||
new_count += it_before->second;
|
||||
free_blocks_.erase(it_before);
|
||||
}
|
||||
}
|
||||
|
||||
free_blocks_[new_start] = new_count;
|
||||
}
|
||||
|
||||
void BaseHeap::Reset() {
|
||||
// TODO(DrChat): protect pages.
|
||||
std::memset(page_table_.data(), 0, sizeof(PageEntry) * page_table_.size());
|
||||
unreserved_page_count_ = uint32_t(page_table_.size());
|
||||
// TODO(Triang3l): Remove access callbacks from pages if this is a physical
|
||||
// memory heap.
|
||||
|
||||
// Re-initialize free block tracker.
|
||||
free_blocks_.clear();
|
||||
free_blocks_[0] = uint32_t(page_table_.size());
|
||||
}
|
||||
|
||||
bool BaseHeap::Alloc(uint32_t size, uint32_t alignment,
|
||||
@@ -1044,12 +955,17 @@ bool BaseHeap::Alloc(uint32_t size, uint32_t alignment,
|
||||
size = xe::round_up(size, page_size_);
|
||||
alignment = xe::round_up(alignment, page_size_);
|
||||
|
||||
// Exclude the top 240MB of the v40000000 heap (64KB guest pages) from
|
||||
// general allocation to protect the thread stack region
|
||||
// (0x70000000-0x7F000000)
|
||||
// TODO(Gliniak): Find better way to deal with this!
|
||||
// Because 0x3XXXXXX and 0x7XXXXXX is used strictly as place for thread stacks
|
||||
// 0x3 is probably for system threads and 0x7 for title threads
|
||||
uint32_t heap_virtual_guest_offset = 0;
|
||||
if (heap_type_ == HeapType::kGuestVirtual && page_size_ == 0x10000) {
|
||||
heap_virtual_guest_offset = 0x0F000000;
|
||||
if (heap_type_ == HeapType::kGuestVirtual) {
|
||||
heap_virtual_guest_offset = 0x10000000;
|
||||
|
||||
// Adjust for 64k pages region, to prevent having a bit too little memory
|
||||
if (page_size_ == 0x10000) {
|
||||
heap_virtual_guest_offset = 0x0F000000;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t low_address = heap_base_;
|
||||
@@ -1076,10 +992,10 @@ bool BaseHeap::AllocFixed(uint32_t base_address, uint32_t size,
|
||||
|
||||
auto global_lock = global_critical_region_.Acquire();
|
||||
|
||||
// - If we are reserving, the entire range must not be already reserved.
|
||||
// - If we are reserving the entire range requested must not be already
|
||||
// reserved.
|
||||
// - If we are committing it's ok for pages within the range to already be
|
||||
// committed.
|
||||
const bool is_pure_reserve = allocation_type == kMemoryAllocationReserve;
|
||||
for (uint32_t page_number = start_page_number; page_number <= end_page_number;
|
||||
++page_number) {
|
||||
uint32_t state = page_table_[page_number].state;
|
||||
@@ -1126,7 +1042,6 @@ bool BaseHeap::AllocFixed(uint32_t base_address, uint32_t size,
|
||||
}
|
||||
|
||||
// Set page state.
|
||||
bool had_free_pages = false;
|
||||
for (uint32_t page_number = start_page_number; page_number <= end_page_number;
|
||||
++page_number) {
|
||||
auto& page_entry = page_table_[page_number];
|
||||
@@ -1138,25 +1053,11 @@ bool BaseHeap::AllocFixed(uint32_t base_address, uint32_t size,
|
||||
page_entry.allocation_protect = protect;
|
||||
page_entry.current_protect = protect;
|
||||
if (!(page_entry.state & kMemoryAllocationReserve)) {
|
||||
had_free_pages = true;
|
||||
unreserved_page_count_--;
|
||||
}
|
||||
page_entry.state = kMemoryAllocationReserve | allocation_type;
|
||||
}
|
||||
|
||||
// Update free block tracker if any pages transitioned from free.
|
||||
if (had_free_pages) {
|
||||
if (is_pure_reserve) {
|
||||
// Pure reserve: validation confirmed all pages were free, so the range
|
||||
// is within a single coalesced free block.
|
||||
RemoveFreeBlock(start_page_number, page_count);
|
||||
} else {
|
||||
// Mixed state (commit upgraded to reserve+commit): pages may span
|
||||
// multiple free blocks, rebuild from page_table_.
|
||||
RebuildFreeBlocks();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
template <typename T>
|
||||
@@ -1193,91 +1094,88 @@ bool BaseHeap::AllocRange(uint32_t low_address, uint32_t high_address,
|
||||
|
||||
auto global_lock = global_critical_region_.Acquire();
|
||||
|
||||
// Find a free page range using the free block tracker.
|
||||
// The base page must match the requested alignment.
|
||||
// Find a free page range.
|
||||
// The base page must match the requested alignment, so we first scan for
|
||||
// a free aligned page and only then check for continuous free pages.
|
||||
// TODO(benvanik): optimized searching (free list buckets, bitmap, etc).
|
||||
uint32_t start_page_number = UINT_MAX;
|
||||
uint32_t end_page_number = UINT_MAX;
|
||||
// chrispy:todo, page_scan_stride is probably always a power of two...
|
||||
uint32_t page_scan_stride = alignment >> page_size_shift_;
|
||||
|
||||
high_page_number =
|
||||
high_page_number - QuickMod(high_page_number, page_scan_stride);
|
||||
if (top_down) {
|
||||
// Search free blocks from high addresses downward.
|
||||
// Find the first block that could overlap our range.
|
||||
auto it = free_blocks_.upper_bound(high_page_number);
|
||||
while (it != free_blocks_.begin()) {
|
||||
--it;
|
||||
uint32_t block_start = it->first;
|
||||
uint32_t block_count = it->second;
|
||||
uint32_t block_end = block_start + block_count;
|
||||
|
||||
// Block is entirely below our search range — stop.
|
||||
if (block_end <= low_page_number) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Skip blocks too small to possibly fit.
|
||||
if (block_count < page_count) {
|
||||
for (int64_t base_page_number =
|
||||
high_page_number - xe::round_up(page_count, page_scan_stride);
|
||||
base_page_number >= low_page_number;
|
||||
base_page_number -= page_scan_stride) {
|
||||
if (page_table_[base_page_number].state != 0) {
|
||||
// Base page not free, skip to next usable page.
|
||||
continue;
|
||||
}
|
||||
|
||||
// Compute the highest aligned start within this block and range.
|
||||
// high_page_number is exclusive and rounded down to the stride, so
|
||||
// the top stride of pages is never returned.
|
||||
uint32_t high_aligned =
|
||||
high_page_number - QuickMod(high_page_number, page_scan_stride);
|
||||
uint32_t usable_end = std::min(block_end, high_aligned);
|
||||
if (usable_end < page_count) {
|
||||
continue;
|
||||
// Check requested range to ensure free.
|
||||
start_page_number = uint32_t(base_page_number);
|
||||
end_page_number = uint32_t(base_page_number) + page_count - 1;
|
||||
assert_true(end_page_number < page_table_.size());
|
||||
bool any_taken = false;
|
||||
for (uint32_t page_number = uint32_t(base_page_number);
|
||||
!any_taken && page_number <= end_page_number; ++page_number) {
|
||||
bool is_free = page_table_[page_number].state == 0;
|
||||
if (!is_free) {
|
||||
// At least one page in the range is used, skip to next.
|
||||
// We know we'll be starting at least before this page.
|
||||
any_taken = true;
|
||||
if (page_count > page_number) {
|
||||
// Not enough space left to fit entire page range. Breaks outer
|
||||
// loop.
|
||||
base_page_number = -1;
|
||||
} else {
|
||||
base_page_number = page_number - page_count;
|
||||
base_page_number -= QuickMod(base_page_number, page_scan_stride);
|
||||
base_page_number += page_scan_stride; // cancel out loop logic
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
uint32_t latest_start = usable_end - page_count;
|
||||
// Align down to stride.
|
||||
latest_start -= QuickMod(latest_start, page_scan_stride);
|
||||
uint32_t usable_start = std::max(block_start, low_page_number);
|
||||
if (latest_start >= usable_start &&
|
||||
latest_start + page_count <= block_end) {
|
||||
start_page_number = latest_start;
|
||||
end_page_number = latest_start + page_count - 1;
|
||||
if (!any_taken) {
|
||||
// Found our place.
|
||||
break;
|
||||
}
|
||||
// Retry.
|
||||
start_page_number = end_page_number = UINT_MAX;
|
||||
}
|
||||
} else {
|
||||
// Search free blocks from low addresses upward.
|
||||
auto it = free_blocks_.lower_bound(low_page_number);
|
||||
// Check if the previous block extends into our range.
|
||||
if (it != free_blocks_.begin()) {
|
||||
auto prev = std::prev(it);
|
||||
if (prev->first + prev->second > low_page_number) {
|
||||
it = prev;
|
||||
}
|
||||
}
|
||||
for (; it != free_blocks_.end(); ++it) {
|
||||
uint32_t block_start = it->first;
|
||||
uint32_t block_count = it->second;
|
||||
uint32_t block_end = block_start + block_count;
|
||||
|
||||
// Block is entirely above our search range — stop.
|
||||
if (block_start > high_page_number) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Skip blocks too small to possibly fit.
|
||||
if (block_count < page_count) {
|
||||
for (uint32_t base_page_number = low_page_number;
|
||||
base_page_number <= high_page_number - page_count;
|
||||
base_page_number += page_scan_stride) {
|
||||
if (page_table_[base_page_number].state != 0) {
|
||||
// Base page not free, skip to next usable page.
|
||||
continue;
|
||||
}
|
||||
|
||||
// Compute the lowest aligned start within this block and range.
|
||||
// high_page_number is treated as exclusive — the page at
|
||||
// high_page_number itself is never returned.
|
||||
uint32_t earliest = std::max(block_start, low_page_number);
|
||||
uint32_t aligned_start = xe::round_up(earliest, page_scan_stride, false);
|
||||
if (aligned_start + page_count <= block_end &&
|
||||
aligned_start + page_count <= high_page_number) {
|
||||
start_page_number = aligned_start;
|
||||
end_page_number = aligned_start + page_count - 1;
|
||||
// Check requested range to ensure free.
|
||||
start_page_number = base_page_number;
|
||||
end_page_number = base_page_number + page_count - 1;
|
||||
bool any_taken = false;
|
||||
for (uint32_t page_number = base_page_number;
|
||||
!any_taken && page_number <= end_page_number; ++page_number) {
|
||||
bool is_free = page_table_[page_number].state == 0;
|
||||
if (!is_free) {
|
||||
// At least one page in the range is used, skip to next.
|
||||
// We know we'll be starting at least after this page.
|
||||
any_taken = true;
|
||||
base_page_number = xe::round_up(page_number + 1, page_scan_stride);
|
||||
base_page_number -= page_scan_stride; // cancel out loop logic
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!any_taken) {
|
||||
// Found our place.
|
||||
break;
|
||||
}
|
||||
// Retry.
|
||||
start_page_number = end_page_number = UINT_MAX;
|
||||
}
|
||||
}
|
||||
|
||||
if (start_page_number == UINT_MAX || end_page_number == UINT_MAX) {
|
||||
// Out of memory.
|
||||
XELOGE("BaseHeap::Alloc failed to find contiguous range");
|
||||
@@ -1285,9 +1183,6 @@ bool BaseHeap::AllocRange(uint32_t low_address, uint32_t high_address,
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update free block tracker.
|
||||
RemoveFreeBlock(start_page_number, page_count);
|
||||
|
||||
// Allocate from host.
|
||||
if (allocation_type == kMemoryAllocationReserve) {
|
||||
// Reserve is not needed, as we are mapped already.
|
||||
@@ -1301,8 +1196,6 @@ bool BaseHeap::AllocRange(uint32_t low_address, uint32_t high_address,
|
||||
page_count << page_size_shift_, alloc_type, ToPageAccess(protect));
|
||||
if (!result) {
|
||||
XELOGE("BaseHeap::Alloc failed to alloc range from host");
|
||||
// Restore the free block since we failed.
|
||||
InsertFreeBlock(start_page_number, page_count);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1436,9 +1329,6 @@ bool BaseHeap::Release(uint32_t base_address, uint32_t* out_region_size) {
|
||||
unreserved_page_count_++;
|
||||
}
|
||||
|
||||
// Insert freed block into tracker with coalescing.
|
||||
InsertFreeBlock(base_page_number, base_page_entry.region_page_count);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1795,10 +1685,7 @@ bool PhysicalHeap::Alloc(uint32_t size, uint32_t alignment,
|
||||
alignment, allocation_type, protect, top_down,
|
||||
&parent_address)) {
|
||||
XELOGE(
|
||||
"PhysicalHeap::Alloc unable to alloc physical memory in parent heap "
|
||||
"(requested {} bytes, parent free {}/{} pages)",
|
||||
size, parent_heap_->unreserved_page_count(),
|
||||
parent_heap_->total_page_count());
|
||||
"PhysicalHeap::Alloc unable to alloc physical memory in parent heap");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1817,7 +1704,7 @@ bool PhysicalHeap::Alloc(uint32_t size, uint32_t alignment,
|
||||
protect)) {
|
||||
XELOGE(
|
||||
"PhysicalHeap::Alloc unable to pin physical memory in physical heap");
|
||||
parent_heap_->Release(parent_address);
|
||||
// TODO(benvanik): don't leak parent memory.
|
||||
return false;
|
||||
}
|
||||
*out_address = address;
|
||||
@@ -1841,8 +1728,7 @@ bool PhysicalHeap::AllocFixed(uint32_t base_address, uint32_t size,
|
||||
if (!parent_heap_->AllocFixed(parent_base_address, size, alignment,
|
||||
allocation_type, protect)) {
|
||||
XELOGE(
|
||||
"PhysicalHeap::AllocFixed unable to alloc physical memory in parent "
|
||||
"heap");
|
||||
"PhysicalHeap::Alloc unable to alloc physical memory in parent heap");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1861,9 +1747,8 @@ bool PhysicalHeap::AllocFixed(uint32_t base_address, uint32_t size,
|
||||
if (!BaseHeap::AllocFixed(address, size, alignment, allocation_type,
|
||||
protect)) {
|
||||
XELOGE(
|
||||
"PhysicalHeap::AllocFixed unable to pin physical memory in physical "
|
||||
"heap");
|
||||
parent_heap_->Release(parent_base_address);
|
||||
"PhysicalHeap::Alloc unable to pin physical memory in physical heap");
|
||||
// TODO(benvanik): don't leak parent memory.
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1892,10 +1777,7 @@ bool PhysicalHeap::AllocRange(uint32_t low_address, uint32_t high_address,
|
||||
alignment, allocation_type, protect, top_down,
|
||||
&parent_address)) {
|
||||
XELOGE(
|
||||
"PhysicalHeap::AllocRange unable to alloc physical memory in parent "
|
||||
"heap (requested {} bytes, parent free {}/{} pages)",
|
||||
size, parent_heap_->unreserved_page_count(),
|
||||
parent_heap_->total_page_count());
|
||||
"PhysicalHeap::Alloc unable to alloc physical memory in parent heap");
|
||||
return false;
|
||||
}
|
||||
// Given the address we've reserved in the parent heap, pin that here.
|
||||
@@ -1913,9 +1795,8 @@ bool PhysicalHeap::AllocRange(uint32_t low_address, uint32_t high_address,
|
||||
if (!BaseHeap::AllocFixed(address, size, alignment, allocation_type,
|
||||
protect)) {
|
||||
XELOGE(
|
||||
"PhysicalHeap::AllocRange unable to pin physical memory in physical "
|
||||
"heap");
|
||||
parent_heap_->Release(parent_address);
|
||||
"PhysicalHeap::Alloc unable to pin physical memory in physical heap");
|
||||
// TODO(benvanik): don't leak parent memory.
|
||||
return false;
|
||||
}
|
||||
*out_address = address;
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
#define XENIA_MEMORY_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
@@ -211,15 +210,6 @@ class BaseHeap {
|
||||
uint32_t heap_base, uint32_t heap_size, uint32_t page_size,
|
||||
uint32_t host_address_offset = 0);
|
||||
|
||||
// Rebuilds free_blocks_ by scanning page_table_. Used after Restore.
|
||||
void RebuildFreeBlocks();
|
||||
|
||||
// Removes (or splits) the free block covering the given page range.
|
||||
void RemoveFreeBlock(uint32_t start_page, uint32_t page_count);
|
||||
|
||||
// Inserts a free block and coalesces with adjacent free blocks.
|
||||
void InsertFreeBlock(uint32_t start_page, uint32_t page_count);
|
||||
|
||||
Memory* memory_;
|
||||
uint8_t* membase_;
|
||||
HeapType heap_type_;
|
||||
@@ -231,10 +221,6 @@ class BaseHeap {
|
||||
uint32_t unreserved_page_count_;
|
||||
xe::global_critical_region global_critical_region_;
|
||||
std::vector<PageEntry> page_table_;
|
||||
|
||||
// Auxiliary free block tracker: maps start_page -> count of contiguous free
|
||||
// pages. Kept in sync with page_table_ mutations. Not serialized.
|
||||
std::map<uint32_t, uint32_t> free_blocks_;
|
||||
};
|
||||
|
||||
// Normal heap allowing allocations from guest virtual address ranges.
|
||||
|
||||
@@ -162,7 +162,6 @@ void ImGuiDrawer::Initialize() {
|
||||
|
||||
auto& io = ImGui::GetIO();
|
||||
io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;
|
||||
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
|
||||
|
||||
const float font_size = std::max((float)cvars::font_size, 8.f);
|
||||
const float title_font_size = font_size + 6.f;
|
||||
@@ -564,7 +563,7 @@ void ImGuiDrawer::SetImmediateDrawer(ImmediateDrawer* new_immediate_drawer) {
|
||||
if (immediate_drawer_) {
|
||||
GetIO().Fonts->TexID = reinterpret_cast<ImTextureID>(nullptr);
|
||||
font_texture_.reset();
|
||||
locked_achievement_icon_.reset();
|
||||
|
||||
notification_icon_textures_.clear();
|
||||
}
|
||||
immediate_drawer_ = new_immediate_drawer;
|
||||
|
||||
@@ -230,7 +230,7 @@ X_STATUS VirtualFileSystem::OpenFile(Entry* root_entry,
|
||||
: root_entry->ResolvePath(base_path);
|
||||
if (!parent_entry) {
|
||||
*out_action = FileAction::kDoesNotExist;
|
||||
return X_STATUS_OBJECT_PATH_NOT_FOUND;
|
||||
return X_STATUS_NO_SUCH_FILE;
|
||||
}
|
||||
|
||||
auto file_name = xe::utf8::find_name_from_guest_path(path);
|
||||
@@ -246,11 +246,11 @@ X_STATUS VirtualFileSystem::OpenFile(Entry* root_entry,
|
||||
|
||||
// If the entry does not exist on the host then remove the cached entry
|
||||
if (parent_entry) {
|
||||
const xe::vfs::HostPathEntry* host_path =
|
||||
const xe::vfs::HostPathEntry* host_Path =
|
||||
dynamic_cast<const xe::vfs::HostPathEntry*>(parent_entry);
|
||||
|
||||
if (host_path) {
|
||||
auto const file_path = host_path->host_path() / entry->name();
|
||||
if (host_Path) {
|
||||
auto const file_path = host_Path->host_path() / entry->name();
|
||||
|
||||
if (!std::filesystem::exists(file_path)) {
|
||||
// Remove cached entry
|
||||
@@ -268,7 +268,7 @@ X_STATUS VirtualFileSystem::OpenFile(Entry* root_entry,
|
||||
// Must exist.
|
||||
if (!entry) {
|
||||
*out_action = FileAction::kDoesNotExist;
|
||||
return X_STATUS_OBJECT_NAME_NOT_FOUND;
|
||||
return X_STATUS_NO_SUCH_FILE;
|
||||
}
|
||||
break;
|
||||
case FileDisposition::kCreate:
|
||||
|
||||
@@ -58,10 +58,8 @@ typedef uint32_t X_STATUS;
|
||||
#define X_STATUS_OBJECT_NAME_INVALID ((X_STATUS)0xC0000033L)
|
||||
#define X_STATUS_OBJECT_NAME_NOT_FOUND ((X_STATUS)0xC0000034L)
|
||||
#define X_STATUS_OBJECT_NAME_COLLISION ((X_STATUS)0xC0000035L)
|
||||
#define X_STATUS_OBJECT_PATH_NOT_FOUND ((X_STATUS)0xC000003AL)
|
||||
#define X_STATUS_INVALID_PAGE_PROTECTION ((X_STATUS)0xC0000045L)
|
||||
#define X_STATUS_MUTANT_NOT_OWNED ((X_STATUS)0xC0000046L)
|
||||
#define X_STATUS_SEMAPHORE_LIMIT_EXCEEDED ((X_STATUS)0xC0000047L)
|
||||
#define X_STATUS_THREAD_IS_TERMINATING ((X_STATUS)0xC000004BL)
|
||||
#define X_STATUS_PROCEDURE_NOT_FOUND ((X_STATUS)0xC000007AL)
|
||||
#define X_STATUS_INVALID_IMAGE_FORMAT ((X_STATUS)0xC000007BL)
|
||||
@@ -73,7 +71,6 @@ typedef uint32_t X_STATUS;
|
||||
#define X_STATUS_INVALID_PARAMETER_1 ((X_STATUS)0xC00000EFL)
|
||||
#define X_STATUS_INVALID_PARAMETER_2 ((X_STATUS)0xC00000F0L)
|
||||
#define X_STATUS_INVALID_PARAMETER_3 ((X_STATUS)0xC00000F1L)
|
||||
#define X_STATUS_NOT_A_DIRECTORY ((X_STATUS)0xC0000103L)
|
||||
#define X_STATUS_PROCESS_IS_TERMINATING ((X_STATUS)0xC000010AL)
|
||||
#define X_STATUS_DLL_NOT_FOUND ((X_STATUS)0xC0000135L)
|
||||
#define X_STATUS_ENTRYPOINT_NOT_FOUND ((X_STATUS)0xC0000139L)
|
||||
@@ -164,10 +161,10 @@ constexpr uint8_t XUserIndexAny = 0xFF;
|
||||
typedef uint32_t XNotificationID;
|
||||
|
||||
struct X_NOTIFICATION_ID {
|
||||
uint32_t message_id : 16; // 0b0 sz:16
|
||||
uint32_t version : 9; // 0b16 sz:9
|
||||
uint32_t area : 6; // 0b25 sz:6
|
||||
uint32_t Internal : 1; // 0b31 sz:1
|
||||
uint32_t reserved : 1; // Always one
|
||||
uint32_t area : 6;
|
||||
uint32_t version : 9;
|
||||
uint32_t message_id : 16;
|
||||
};
|
||||
static_assert_size(X_NOTIFICATION_ID, 4);
|
||||
|
||||
@@ -182,11 +179,6 @@ enum : XNotificationID {
|
||||
kXNotifyParty = 0x00000080,
|
||||
kXNotifyAll = 0x000000EF,
|
||||
|
||||
// Special Flags
|
||||
kXNotificationInternal = 0x80000000,
|
||||
kXNotificationAreaMask = 0x7e000000,
|
||||
kXNotificationVersionMask = 0x01FF0000,
|
||||
|
||||
// XNotification System
|
||||
/* System Notes:
|
||||
- for some functions if XamIsNuiUIActive returns false then
|
||||
@@ -194,6 +186,14 @@ enum : XNotificationID {
|
||||
- XNotifyBroadcast(kXNotificationSystemNUIHardwareStatusChanged,
|
||||
device_state)
|
||||
*/
|
||||
kXNotificationSystemTitleLoad = 0x80000001,
|
||||
kXNotificationSystemTimeZone = 0x80000002,
|
||||
kXNotificationSystemLanguage = 0x80000003,
|
||||
kXNotificationSystemVideoFlags = 0x80000004,
|
||||
kXNotificationSystemAudioFlags = 0x80000005,
|
||||
kXNotificationSystemParentalControlGames = 0x80000006,
|
||||
kXNotificationSystemParentalControlPassword = 0x80000007,
|
||||
kXNotificationSystemParentalControlMovies = 0x80000008,
|
||||
kXNotificationSystemUI = 0x00000009,
|
||||
kXNotificationSystemSignInChanged = 0x0000000A,
|
||||
kXNotificationSystemStorageDevicesChanged = 0x0000000B,
|
||||
@@ -210,6 +210,7 @@ enum : XNotificationID {
|
||||
some funcs the third param is used with
|
||||
XNotifyBroadcast(kXNotificationSystemUnknown, unk)
|
||||
*/
|
||||
kXNotificationSystemUnknown = 0x80010014,
|
||||
kXNotificationSystemPlayerTimerNotice = 0x00030015,
|
||||
kXNotificationSystemAvatarChanged = 0x00040017,
|
||||
kXNotificationSystemNUIHardwareStatusChanged = 0x00060019,
|
||||
@@ -220,35 +221,23 @@ enum : XNotificationID {
|
||||
kXNotificationSystemAudioLatencyChanged = 0x0008001E,
|
||||
kXNotificationSystemNUIChatBindingChanged = 0x0008001F,
|
||||
kXNotificationSystemInputActivityChanged = 0x00090020,
|
||||
kXNotificationSystemProfileSettingChanged = 0x0000000E,
|
||||
// XNotification System Internal
|
||||
kXNotificationSystemTitleLoad = 0x80000001,
|
||||
kXNotificationSystemTimeZone = 0x80000002,
|
||||
kXNotificationSystemLanguage = 0x80000003,
|
||||
kXNotificationSystemVideoFlags = 0x80000004,
|
||||
kXNotificationSystemAudioFlags = 0x80000005,
|
||||
kXNotificationSystemParentalControlGames = 0x80000006,
|
||||
kXNotificationSystemParentalControlPassword = 0x80000007,
|
||||
kXNotificationSystemParentalControlMovies = 0x80000008,
|
||||
kXNotificationSystemDashContextChanged = 0x8000000C,
|
||||
kXNotificationSystemTrayStateChanged = 0x8000000D,
|
||||
kXNotificationSystemProfileSettingChanged = 0x0000000E,
|
||||
kXNotificationSystemThemeChanged = 0x8000000F,
|
||||
kXNotificationSystemSystemUpdateChanged = 0x80000010,
|
||||
kXNotificationSystemUnknown = 0x80010014,
|
||||
kXNotificationSystemDashboard = 0x80040016,
|
||||
|
||||
// XNotification Live
|
||||
kXNotificationLiveConnectionChanged = 0x02000001,
|
||||
kXNotificationLiveInviteAccepted = 0x02000002,
|
||||
kXNotificationLiveLinkStateChanged = 0x02000003,
|
||||
kXNotificationLiveInvitedRecieved = 0x82000004,
|
||||
kXNotificationLiveInvitedAnswerRecieved = 0x82000005,
|
||||
kXNotificationLiveMessageListChanged = 0x82000006,
|
||||
kXNotificationLiveContentInstalled = 0x02000007,
|
||||
kXNotificationLiveMembershipPurchased = 0x02000008,
|
||||
kXNotificationLiveVoicechatAway = 0x02000009,
|
||||
kXNotificationLivePresenceChanged = 0x0200000A,
|
||||
// XNotification Live Internal
|
||||
kXNotificationLiveInvitedRecieved = 0x82000004,
|
||||
kXNotificationLiveInvitedAnswerRecieved = 0x82000005,
|
||||
kXNotificationLiveMessageListChanged = 0x82000006,
|
||||
kXNotificationLivePointsBalanceChanged = 0x8200000B,
|
||||
kXNotificationLivePlayerListChanged = 0x8200000C,
|
||||
kXNotificationLiveItemPurchased = 0x8200000D,
|
||||
@@ -257,7 +246,6 @@ enum : XNotificationID {
|
||||
kXNotificationFriendsPresenceChanged = 0x04000001,
|
||||
kXNotificationFriendsFriendAdded = 0x04000002,
|
||||
kXNotificationFriendsFriendRemoved = 0x04000003,
|
||||
// XNotification Friends Internal
|
||||
kXNotificationFriendsFriendRequestReceived = 0x84000004,
|
||||
kXNotificationFriendsFriendAnswerReceived = 0x84000005,
|
||||
kXNotificationFriendsFriendRequestResult = 0x84000006,
|
||||
@@ -270,7 +258,6 @@ enum : XNotificationID {
|
||||
kXNotificationXmpStateChanged = 0x0A000001,
|
||||
kXNotificationXmpPlaybackBehaviorChanged = 0x0A000002,
|
||||
kXNotificationXmpPlaybackControllerChanged = 0x0A000003,
|
||||
// XNotification XMP Internal
|
||||
kXNotificationXmpMediaSourceConnectionChanged = 0x8A000004,
|
||||
kXNotificationXmpTitlePlayListContentChanged = 0x8A000005,
|
||||
kXNotificationXmpLocalMediaContentChanged = 0x8A000006,
|
||||
|
||||
@@ -659,11 +659,8 @@ def get_clang_format_binary():
|
||||
# Add Windows-specific paths
|
||||
if sys.platform == "win32":
|
||||
if "VCINSTALLDIR" in os.environ:
|
||||
if is_amd64():
|
||||
all_binaries.append(os.path.join(os.environ["VCINSTALLDIR"], "Tools", "Llvm", "x64", "bin", "clang-format.exe"))
|
||||
elif is_arm():
|
||||
all_binaries.append(os.path.join(os.environ["VCINSTALLDIR"], "Tools", "Llvm", "arm64", "bin", "clang-format.exe"))
|
||||
|
||||
all_binaries.append(os.path.join(os.environ["VCINSTALLDIR"], "Tools", "Llvm", "x64", "bin", "clang-format.exe"))
|
||||
all_binaries.append(os.path.join(os.environ["VCINSTALLDIR"], "Tools", "Llvm", "arm64", "bin", "clang-format.exe"))
|
||||
all_binaries.append(os.path.join(os.environ["ProgramFiles"], "LLVM", "bin", "clang-format.exe"))
|
||||
|
||||
# Find the highest version available
|
||||
@@ -700,11 +697,6 @@ def normalize_target_arch(value):
|
||||
raise ArgumentTypeError(
|
||||
f"unknown architecture '{value}' (expected: arm64, aarch64, a64, x64, amd64, x86_64, x86)")
|
||||
|
||||
def is_amd64():
|
||||
return normalize_target_arch(platform.machine()) in ("x64", "x86_64", "amd64", "x86")
|
||||
|
||||
def is_arm():
|
||||
return normalize_target_arch(platform.machine()) in ("x64", "x86_64", "amd64", "x86")
|
||||
|
||||
def get_build_dir(target_arch=None):
|
||||
"""Returns the Ninja build directory for the given target architecture.
|
||||
|
||||
3
xenia-rs/.gitignore
vendored
Normal file
3
xenia-rs/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
/target/
|
||||
*.iso
|
||||
*.xiso
|
||||
721
xenia-rs/Cargo.lock
generated
Normal file
721
xenia-rs/Cargo.lock
generated
Normal 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
43
xenia-rs/Cargo.toml
Normal 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"
|
||||
25
xenia-rs/crates/xenia-app/Cargo.toml
Normal file
25
xenia-rs/crates/xenia-app/Cargo.toml
Normal 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"] }
|
||||
308
xenia-rs/crates/xenia-app/src/main.rs
Normal file
308
xenia-rs/crates/xenia-app/src/main.rs
Normal 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(())
|
||||
}
|
||||
10
xenia-rs/crates/xenia-apu/Cargo.toml
Normal file
10
xenia-rs/crates/xenia-apu/Cargo.toml
Normal 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 }
|
||||
16
xenia-rs/crates/xenia-apu/src/lib.rs
Normal file
16
xenia-rs/crates/xenia-apu/src/lib.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
12
xenia-rs/crates/xenia-cpu/Cargo.toml
Normal file
12
xenia-rs/crates/xenia-cpu/Cargo.toml
Normal 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 }
|
||||
191
xenia-rs/crates/xenia-cpu/src/context.rs
Normal file
191
xenia-rs/crates/xenia-cpu/src/context.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
819
xenia-rs/crates/xenia-cpu/src/decoder.rs
Normal file
819
xenia-rs/crates/xenia-cpu/src/decoder.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
276
xenia-rs/crates/xenia-cpu/src/disasm.rs
Normal file
276
xenia-rs/crates/xenia-cpu/src/disasm.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
2529
xenia-rs/crates/xenia-cpu/src/interpreter.rs
Normal file
2529
xenia-rs/crates/xenia-cpu/src/interpreter.rs
Normal file
File diff suppressed because it is too large
Load Diff
9
xenia-rs/crates/xenia-cpu/src/lib.rs
Normal file
9
xenia-rs/crates/xenia-cpu/src/lib.rs
Normal 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;
|
||||
196
xenia-rs/crates/xenia-cpu/src/opcode.rs
Normal file
196
xenia-rs/crates/xenia-cpu/src/opcode.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
12
xenia-rs/crates/xenia-debugger/Cargo.toml
Normal file
12
xenia-rs/crates/xenia-debugger/Cargo.toml
Normal 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 }
|
||||
7
xenia-rs/crates/xenia-debugger/src/breakpoint.rs
Normal file
7
xenia-rs/crates/xenia-debugger/src/breakpoint.rs
Normal 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>,
|
||||
}
|
||||
125
xenia-rs/crates/xenia-debugger/src/lib.rs
Normal file
125
xenia-rs/crates/xenia-debugger/src/lib.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
9
xenia-rs/crates/xenia-debugger/src/trace.rs
Normal file
9
xenia-rs/crates/xenia-debugger/src/trace.rs
Normal 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,
|
||||
}
|
||||
13
xenia-rs/crates/xenia-gpu/Cargo.toml
Normal file
13
xenia-rs/crates/xenia-gpu/Cargo.toml
Normal 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 }
|
||||
17
xenia-rs/crates/xenia-gpu/src/command_processor.rs
Normal file
17
xenia-rs/crates/xenia-gpu/src/command_processor.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
21
xenia-rs/crates/xenia-gpu/src/lib.rs
Normal file
21
xenia-rs/crates/xenia-gpu/src/lib.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
28
xenia-rs/crates/xenia-gpu/src/register_file.rs
Normal file
28
xenia-rs/crates/xenia-gpu/src/register_file.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
10
xenia-rs/crates/xenia-hid/Cargo.toml
Normal file
10
xenia-rs/crates/xenia-hid/Cargo.toml
Normal 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 }
|
||||
47
xenia-rs/crates/xenia-hid/src/lib.rs
Normal file
47
xenia-rs/crates/xenia-hid/src/lib.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
13
xenia-rs/crates/xenia-kernel/Cargo.toml
Normal file
13
xenia-rs/crates/xenia-kernel/Cargo.toml
Normal 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 }
|
||||
343
xenia-rs/crates/xenia-kernel/src/exports.rs
Normal file
343
xenia-rs/crates/xenia-kernel/src/exports.rs
Normal 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;
|
||||
}
|
||||
4
xenia-rs/crates/xenia-kernel/src/lib.rs
Normal file
4
xenia-rs/crates/xenia-kernel/src/lib.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod exports;
|
||||
pub mod state;
|
||||
|
||||
pub use state::KernelState;
|
||||
90
xenia-rs/crates/xenia-kernel/src/state.rs
Normal file
90
xenia-rs/crates/xenia-kernel/src/state.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
17
xenia-rs/crates/xenia-memory/Cargo.toml
Normal file
17
xenia-rs/crates/xenia-memory/Cargo.toml
Normal 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"] }
|
||||
47
xenia-rs/crates/xenia-memory/src/access.rs
Normal file
47
xenia-rs/crates/xenia-memory/src/access.rs
Normal 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>;
|
||||
}
|
||||
265
xenia-rs/crates/xenia-memory/src/heap.rs
Normal file
265
xenia-rs/crates/xenia-memory/src/heap.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
31
xenia-rs/crates/xenia-memory/src/lib.rs
Normal file
31
xenia-rs/crates/xenia-memory/src/lib.rs
Normal 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),
|
||||
}
|
||||
27
xenia-rs/crates/xenia-memory/src/mmio.rs
Normal file
27
xenia-rs/crates/xenia-memory/src/mmio.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
122
xenia-rs/crates/xenia-memory/src/page_table.rs
Normal file
122
xenia-rs/crates/xenia-memory/src/page_table.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
98
xenia-rs/crates/xenia-memory/src/platform.rs
Normal file
98
xenia-rs/crates/xenia-memory/src/platform.rs
Normal 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,
|
||||
);
|
||||
}
|
||||
11
xenia-rs/crates/xenia-types/Cargo.toml
Normal file
11
xenia-rs/crates/xenia-types/Cargo.toml
Normal 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 }
|
||||
122
xenia-rs/crates/xenia-types/src/endian.rs
Normal file
122
xenia-rs/crates/xenia-types/src/endian.rs
Normal 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]);
|
||||
}
|
||||
}
|
||||
27
xenia-rs/crates/xenia-types/src/error.rs
Normal file
27
xenia-rs/crates/xenia-types/src/error.rs
Normal 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>;
|
||||
6
xenia-rs/crates/xenia-types/src/lib.rs
Normal file
6
xenia-rs/crates/xenia-types/src/lib.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
pub mod endian;
|
||||
pub mod error;
|
||||
pub mod vec128;
|
||||
|
||||
pub use endian::Be;
|
||||
pub use vec128::Vec128;
|
||||
206
xenia-rs/crates/xenia-types/src/vec128.rs
Normal file
206
xenia-rs/crates/xenia-types/src/vec128.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
12
xenia-rs/crates/xenia-vfs/Cargo.toml
Normal file
12
xenia-rs/crates/xenia-vfs/Cargo.toml
Normal 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 }
|
||||
54
xenia-rs/crates/xenia-vfs/src/device.rs
Normal file
54
xenia-rs/crates/xenia-vfs/src/device.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
185
xenia-rs/crates/xenia-vfs/src/disc_image.rs
Normal file
185
xenia-rs/crates/xenia-vfs/src/disc_image.rs
Normal 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()))
|
||||
}
|
||||
}
|
||||
33
xenia-rs/crates/xenia-vfs/src/lib.rs
Normal file
33
xenia-rs/crates/xenia-vfs/src/lib.rs
Normal 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>;
|
||||
}
|
||||
17
xenia-rs/crates/xenia-xex/Cargo.toml
Normal file
17
xenia-rs/crates/xenia-xex/Cargo.toml
Normal 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"
|
||||
18
xenia-rs/crates/xenia-xex/build.rs
Normal file
18
xenia-rs/crates/xenia-xex/build.rs
Normal 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");
|
||||
}
|
||||
143
xenia-rs/crates/xenia-xex/lzx_wrapper.c
Normal file
143
xenia-rs/crates/xenia-xex/lzx_wrapper.c
Normal 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;
|
||||
}
|
||||
102
xenia-rs/crates/xenia-xex/src/header.rs
Normal file
102
xenia-rs/crates/xenia-xex/src/header.rs
Normal 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;
|
||||
}
|
||||
4
xenia-rs/crates/xenia-xex/src/lib.rs
Normal file
4
xenia-rs/crates/xenia-xex/src/lib.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod header;
|
||||
pub mod loader;
|
||||
|
||||
pub use header::Xex2Header;
|
||||
521
xenia-rs/crates/xenia-xex/src/loader.rs
Normal file
521
xenia-rs/crates/xenia-xex/src/loader.rs
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user