2 Commits

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

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

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

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-12 21:32:46 +02:00
MechaCat02
06a23212fb Initial xenia-rs 2026-04-12 18:25:46 +02:00
118 changed files with 9689 additions and 5676 deletions

3
.gitignore vendored
View File

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

View File

@@ -78,20 +78,6 @@ file(MAKE_DIRECTORY "${PROJECT_SOURCE_DIR}/scratch")
# Python for shader compilation scripts
find_package(Python3 REQUIRED COMPONENTS Interpreter)
# Generate build-tree version.h via xenia-build.py's generate_version_h()
# (normally invoked by `xb premake`/`xb build`; CMake-direct flows need it too).
execute_process(
COMMAND ${Python3_EXECUTABLE} -c "import importlib.util,sys; spec=importlib.util.spec_from_file_location('xb',r'${PROJECT_SOURCE_DIR}/xenia-build.py'); m=importlib.util.module_from_spec(spec); spec.loader.exec_module(m); m.generate_version_h(r'${CMAKE_BINARY_DIR}')"
WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}"
RESULT_VARIABLE _xenia_version_rc
)
if(NOT _xenia_version_rc EQUAL 0)
message(WARNING "version.h generation failed (rc=${_xenia_version_rc}); writing stub.")
file(WRITE "${CMAKE_BINARY_DIR}/version.h"
"#ifndef GENERATED_VERSION_H_\n#define GENERATED_VERSION_H_\n#define XE_BUILD_BRANCH \"unknown\"\n#define XE_BUILD_COMMIT \"unknown\"\n#define XE_BUILD_COMMIT_SHORT \"unknown\"\n#define XE_BUILD_DATE __DATE__\n#endif\n"
)
endif()
# Include helpers
include(cmake/XeniaHelpers.cmake)
@@ -110,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)
@@ -156,17 +140,8 @@ if(MSVC)
# --- Per-configuration MSVC flags ---
# Checked
# /RTCsu is MSVC-only; clang-cl ignores it with a per-TU warning. Gate it
# so the Checked config stays quiet when cross-compiling with clang-cl.
# ASan (/fsanitize=address) stays active in both branches.
if(CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC"
AND NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
string(APPEND CMAKE_C_FLAGS_CHECKED " /RTCsu /fsanitize=address")
string(APPEND CMAKE_CXX_FLAGS_CHECKED " /RTCsu /fsanitize=address")
else()
string(APPEND CMAKE_C_FLAGS_CHECKED " /fsanitize=address")
string(APPEND CMAKE_CXX_FLAGS_CHECKED " /fsanitize=address")
endif()
string(APPEND CMAKE_C_FLAGS_CHECKED " /RTCsu /fsanitize=address")
string(APPEND CMAKE_CXX_FLAGS_CHECKED " /RTCsu /fsanitize=address")
string(APPEND CMAKE_EXE_LINKER_FLAGS_CHECKED " /INCREMENTAL:NO")
add_compile_definitions($<$<CONFIG:Checked>:DEBUG>)

View File

@@ -39,22 +39,6 @@
"cacheVariables": {
"CMAKE_SYSTEM_PROCESSOR": "ARM64"
}
},
{
"name": "cross-win-clangcl",
"displayName": "Cross (Linux->Win MSVC) clang-cl + xwin",
"generator": "Ninja Multi-Config",
"binaryDir": "${sourceDir}/build-cross",
"toolchainFile": "${sourceDir}/cmake/toolchains/linux-to-win-msvc.cmake",
"condition": {
"type": "notEquals",
"lhs": "${hostSystemName}",
"rhs": "Windows"
},
"cacheVariables": {
"XWIN_DIR": "$env{HOME}/.xwin/splat",
"FXC_PATH": "$env{HOME}/.local/share/xenia-cross/fxc/fxc.exe"
}
}
],
"buildPresets": [
@@ -105,21 +89,6 @@
"name": "vs-arm64-checked",
"configurePreset": "vs-arm64",
"configuration": "Checked"
},
{
"name": "cross-debug",
"configurePreset": "cross-win-clangcl",
"configuration": "Debug"
},
{
"name": "cross-release",
"configurePreset": "cross-win-clangcl",
"configuration": "Release"
},
{
"name": "cross-checked",
"configurePreset": "cross-win-clangcl",
"configuration": "Checked"
}
]
}

164
RUST_PORT_CODEMAP.md Normal file
View File

@@ -0,0 +1,164 @@
## Xenia Xbox 360 Emulator: JIT vs Interpreter Architecture Analysis
Analysis of Xenia's architecture comparing JIT vs interpreter approaches for Rust porting. Key findings: JIT pipeline [1a-1d] uses Xbyak with no Rust equivalent, MMIO relies on hardware exceptions [2a-2d], while interpreter approach uses explicit MMIO checking [3a-3d]. The PPC context [4a-4d] and opcode infrastructure [5a-5d] support both approaches. Memory system [6a-6c] requires unsafe Rust regardless. Kernel HLE [7a-7c] and GPU shader translation [8a-8c] present additional challenges.
### 1. JIT Code Generation Pipeline
The current PPC-to-x64 JIT compilation pipeline using HIR and Xbyak
### 1a. JIT Pipeline Initialization (`ppc_translator.cc:44`)
Sets up scanner, HIR builder, compiler, and Xbyak assembler
```text
PPCTranslator::PPCTranslator(PPCFrontend* frontend) : frontend_(frontend) {
```
### 1b. HIR Optimization Passes (`ppc_translator.cc:57`)
Adds multiple optimization passes to the compiler pipeline
```text
compiler_->AddPass(std::make_unique<passes::ControlFlowAnalysisPass>());
```
### 1c. Xbyak-based Code Emitter (`x64_emitter.h:208`)
X64Emitter inherits from Xbyak for runtime x64 code generation
```text
class X64Emitter : public Xbyak::CodeGenerator {
```
### 1d. Host-to-Guest Thunk (`x64_backend.cc:656`)
Raw x64 assembly emitted for host↔guest ABI transitions
```text
mov(rdi, ptr[rsi + offsetof(ppc::PPCContext, virtual_membase)]); // membase
```
### 2. MMIO Exception Handling
Hardware exception-based MMIO interception used by the JIT
### 2a. Exception Handler Entry (`mmio_handler.cc:402`)
Catches access violations to handle MMIO operations
```text
bool MMIOHandler::ExceptionCallback(Exception* ex) {
```
### 2b. Filter Access Violations (`mmio_handler.cc:403`)
Only processes memory access violations
```text
if (ex->code() != Exception::Code::kAccessViolation) {
```
### 2c. Address Translation (`mmio_handler.cc:427`)
Translates host fault address back to guest virtual address
```text
fault_guest_virtual_address = host_to_guest_virtual_(
```
### 2d. Instruction Decoding (`mmio_handler.cc:449`)
Decodes the faulting x64 instruction to determine the operation
```text
if (!TryDecodeLoadStore(p, decoded_load_store)) {
```
### 3. Interpreter Alternative Approach
Explicit MMIO checking pattern that enables interpreter implementation
### 3a. Explicit MMIO Check Function (`x64_seq_memory.cc:1216`)
Pattern for checking MMIO ranges without exceptions
```text
static T MMIOAwareLoad(void* _ctx, unsigned int guestaddr) {
```
### 3b. Range Lookup (`x64_seq_memory.cc:1225`)
Explicitly checks if address is in mapped MMIO range
```text
auto gaddr = ctx->processor->memory()->LookupVirtualMappedRange(guestaddr);
```
### 3c. MMIO Callback Invocation (`x64_seq_memory.cc:1236`)
Calls MMIO read callback instead of accessing memory directly
```text
value = gaddr->read(nullptr, gaddr->callback_context, guestaddr);
```
### 3d. MMIO Handler Interface (`mmio_handler.h:73`)
Public API for explicit MMIO checking in interpreter mode
```text
bool CheckLoad(uint32_t virtual_address, uint32_t* out_value);
```
### 4. PPC Context and State Management
Register file and thread state structures that would need Rust porting
### 4a. PPC Register File (`ppc_context.h:378`)
32 GPRs, CTR, LR, MSR in the context structure
```text
uint64_t r[32]; // 0x20 General purpose registers
```
### 4b. Floating-Point and Vector Registers (`ppc_context.h:384`)
32 FPRs and 128 VMX128 vector registers
```text
double f[32]; // 0x120 Floating-point registers
```
### 4c. Thread State Context (`thread_state.h:49`)
Each guest thread owns a PPCContext pointer
```text
ppc::PPCContext* context_;
```
### 4d. Big-Endian Wrapper (`byte_order.h:134`)
Type that handles big-endian conversion for guest structures
```text
template <typename T>
using be = endian_store<T, std::endian::big>;
```
### 5. Opcode Dispatch Infrastructure
PPC opcode enumeration and lookup that enables interpreter implementation
### 5a. PPC Opcode Enumeration (`ppc_opcode.h:14`)
Complete enum of all PPC opcodes including VMX128 extensions
```text
enum class PPCOpcode : uint32_t {
```
### 5b. Opcode Dispatch Table (`ppc_opcode_lookup_gen.cc:262`)
Fast lookup table for opcode decoding
```text
case 0b000101: PPC_DECODER_HIT(vrlw128);
```
### 5c. VMX128 Instruction Encoding (`ppc_emit_altivec.cc:37`)
Macros for decoding Xbox 360-specific VMX128 instructions
```text
#define VX128(op, xop) (OP(op) | (((uint32_t)(xop)) & 0x3d0))
```
### 5d. Basic Block Discovery (`ppc_scanner.h:36`)
Finds basic block boundaries for potential interpreter caching
```text
std::vector<BlockInfo> FindBlocks(GuestFunction* function);
```
### 6. Memory System Architecture
4GB virtual address space management required for both approaches
### 6a. Virtual Memory Allocation (`memory.cc:142`)
Creates 4GB+ file-backed mapping for guest address space
```text
mapping_ = xe::memory::CreateFileMappingHandle(
```
### 6b. Virtual Memory Base (`memory.cc:167`)
Sets up virtual and physical memory base addresses
```text
virtual_membase_ = mapping_base_;
```
### 6c. Guest Address Translation (`ppc_context.h:433`)
Fast inline function for translating guest to host addresses
```text
inline T TranslateVirtual(uint32_t guest_address) const {
```
### 7. Kernel HLE System
Massive kernel export table that would need Rust porting
### 7a. Kernel Export Table (`xboxkrnl_table.inc:15`)
96KB+ table of all xboxkrnl exports
```text
XE_EXPORT(xboxkrnl, 0x00000001, DbgBreakPoint, kFunction),
```
### 7b. Guest Thread Structure (`xthread.h:256`)
Packed big-endian X_KTHREAD structure living in guest memory
```text
xe::be<uint32_t> stack_base; // 0x5C
```
### 7c. Kernel State Management (`emulator.h:349`)
Central kernel object tracking all guest kernel state
```text
std::unique_ptr<kernel::KernelState> kernel_state_;
```
### 8. GPU Shader Translation
Complex shader translation pipeline that poses challenges for Rust porting
### 8a. SPIR-V Builder Initialization (`spirv_shader_translator.cc:155`)
Uses glslang C++ API for SPIR-V generation
```text
builder_ = std::make_unique<SpirvBuilder>(
```
### 8b. Shader Interpreter (`shader_interpreter.h:51`)
Existing interpreter for simple shaders without texture fetches
```text
static bool CanInterpretShader(const Shader& shader) {
```
### 8c. C++ Dependency (`premake5.lua:31`)
glslang dependency with no direct Rust equivalent
```text
"glslang-spirv",
```

1085
RUST_PORT_REPORT.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -192,12 +192,6 @@ function(xe_shader_rules_dxbc target shader_dir)
set(_valid_stages vs hs ds gs ps cs)
set(_commands)
set(_bytecode_dir "${shader_dir}/bytecode/d3d12_5_1")
# Propagate FXC_PATH from the configure-time env so ninja-spawned python
# subprocesses can find fxc.exe (CMake `set(ENV{...})` doesn't reach build).
set(_env_prefix "")
if(DEFINED ENV{FXC_PATH})
set(_env_prefix ${CMAKE_COMMAND} -E env "FXC_PATH=$ENV{FXC_PATH}")
endif()
list(APPEND _commands COMMAND ${CMAKE_COMMAND} -E make_directory "${_bytecode_dir}")
foreach(src ${_sources})
get_filename_component(_name ${src} NAME)
@@ -212,7 +206,7 @@ function(xe_shader_rules_dxbc target shader_dir)
if(NOT _stage IN_LIST _valid_stages)
continue()
endif()
list(APPEND _commands COMMAND ${_env_prefix} ${Python3_EXECUTABLE} "${_script}" "${src}" "${_bytecode_dir}/${_id}.h")
list(APPEND _commands COMMAND ${Python3_EXECUTABLE} "${_script}" "${src}" "${_bytecode_dir}/${_id}.h")
endforeach()
add_custom_command(
OUTPUT "${_stamp}"

View File

@@ -1,119 +0,0 @@
# Linux host -> Windows MSVC-ABI cross toolchain using clang-cl + lld-link
# + xwin-supplied Win10 SDK/CRT. Driven by Ninja Multi-Config.
set(CMAKE_SYSTEM_NAME Windows)
set(CMAKE_SYSTEM_PROCESSOR x86_64)
if(NOT DEFINED XWIN_DIR)
if(DEFINED ENV{XWIN_DIR})
set(XWIN_DIR "$ENV{XWIN_DIR}")
else()
set(XWIN_DIR "$ENV{HOME}/.xwin/splat")
endif()
endif()
set(XWIN_DIR "${XWIN_DIR}" CACHE PATH "xwin splat root (contains crt/ and sdk/)")
if(NOT EXISTS "${XWIN_DIR}/crt/include")
message(FATAL_ERROR "XWIN_DIR=${XWIN_DIR} missing crt/include - run xwin splat.")
endif()
set(CMAKE_C_COMPILER clang-cl)
set(CMAKE_CXX_COMPILER clang-cl)
set(CMAKE_LINKER lld-link)
set(CMAKE_RC_COMPILER llvm-rc)
set(CMAKE_AR llvm-lib)
set(CMAKE_MT llvm-mt)
set(CMAKE_C_COMPILER_TARGET x86_64-pc-windows-msvc)
set(CMAKE_CXX_COMPILER_TARGET x86_64-pc-windows-msvc)
set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)
# Force /MD (release CRT) for every config. xenia's CMakeLists.txt does a
# `string(REPLACE "/MDd" "/MD" ...)` on CMAKE_CXX_FLAGS_DEBUG, but with
# clang-cl, the runtime selection comes from CMAKE_MSVC_RUNTIME_LIBRARY
# (which expands to -MDd in dash form), so the substitution misses.
# Pinning the policy here avoids the need for non-redistributable debug
# CRT DLLs (MSVCP140D.dll etc) at runtime, which xwin doesn't ship.
cmake_policy(SET CMP0091 NEW)
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreadedDLL")
# xwin's default splat is a flat layout (crt/include, sdk/include/{ucrt,um,shared,...}),
# which clang-cl's /winsysroot does NOT understand. Use explicit -imsvc + -libpath:
# instead. Re-running `xwin splat --use-winsysroot-style` would also work but
# requires re-downloading ~600 MB.
# Use SHELL: prefix so CMake doesn't deduplicate repeated -imsvc tokens.
add_compile_options(
"SHELL:-imsvc \"${XWIN_DIR}/crt/include\""
"SHELL:-imsvc \"${XWIN_DIR}/sdk/include/ucrt\""
"SHELL:-imsvc \"${XWIN_DIR}/sdk/include/um\""
"SHELL:-imsvc \"${XWIN_DIR}/sdk/include/shared\""
"SHELL:-imsvc \"${XWIN_DIR}/sdk/include/winrt\""
)
add_link_options(
"/libpath:${XWIN_DIR}/crt/lib/x86_64"
"/libpath:${XWIN_DIR}/sdk/lib/ucrt/x86_64"
"/libpath:${XWIN_DIR}/sdk/lib/um/x86_64"
)
# xwin pulls the latest MSVC STL which now hard-asserts Clang >= 19. Our host
# has Clang 18, which works fine in practice — opt out of the version check.
# (See yvals_core.h STL1000 in $XWIN_DIR/crt/include.)
add_compile_definitions(_ALLOW_COMPILER_AND_STL_VERSION_MISMATCH)
# llvm-rc needs SDK headers explicitly + the resource file's own dir so the
# RC's relative ICON path (..\..\..\assets\icon\icon.ico) resolves.
get_filename_component(_toolchain_dir "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY)
get_filename_component(_xenia_root "${_toolchain_dir}/../.." ABSOLUTE)
set(CMAKE_RC_FLAGS_INIT
"/I \"${XWIN_DIR}/sdk/include/um\" /I \"${XWIN_DIR}/sdk/include/shared\" /I \"${_xenia_root}/src/xenia/app\"")
# Quiet MSVC-STL false positives and xenia-specific warnings that clang-cl
# emits but cl.exe doesn't (treated as errors under /WX).
add_compile_options(
-Wno-microsoft-include
-Wno-unused-command-line-argument
-Wno-ignored-pragma-intrinsic
-Wno-nonportable-include-path
-Wno-pragma-pack
-Wno-tautological-pointer-compare
-Wno-microsoft-cast
-Wno-deprecated-declarations
# These are silenced for native Linux Clang in CMakeLists.txt's else() branch,
# but the if(MSVC) branch fires under clang-cl and skips them — so re-add.
-Wno-switch
-Wno-attributes
-Wno-deprecated-register
-Wno-deprecated-volatile
-Wno-deprecated-enum-enum-conversion
# cl.exe accepts __pragma(optimize("s",on)); clang-cl only knows the empty
# argument form. xenia gates XE_MSVC_OPTIMIZE_SMALL on _MSC_VER so the
# rejected pragma still emits from the clang-cl path. Treat as no-op.
-Wno-ignored-pragmas
# xenia decorates several `virtual` methods with XE_FORCEINLINE; clang-cl
# then complains that the inline body isn't visible in includer TUs
# (definitions live in command_processor.cc). cl.exe accepts this silently.
-Wno-undefined-inline
-Wno-sizeof-pointer-memaccess
# `'ZM'` (PE magic, stored little-endian as "MZ" in the binary) — cl.exe
# accepts the multi-char constant silently; clang-cl errors under /WX.
-Wno-multichar
)
# _mm_cvtsi64x_si128 is an MSVC-only alias for the standard _mm_cvtsi64_si128.
# Used (gated on XE_PLATFORM_WIN32) in xenia/gpu/draw_util.cc.
add_compile_definitions(_mm_cvtsi64x_si128=_mm_cvtsi64_si128)
# Plumb FXC for tools/build/compile_shader_dxbc.py (wine fxc auto-prepend).
# Use real Win10 SDK fxc.exe 10.x (supports SM 5_1, produces vkd3d-proton-
# acceptable DXBC). Extracted from the Win11 SDK 26100 ISO -> Store Apps
# Tools MSI's CABs.
if(DEFINED ENV{FXC_PATH})
set(ENV_FXC "$ENV{FXC_PATH}")
else()
set(ENV_FXC "$ENV{HOME}/.local/share/xenia-cross/fxc/fxc.exe")
endif()
set(ENV{FXC_PATH} "${ENV_FXC}")
set(CMAKE_FIND_ROOT_PATH "${XWIN_DIR}")
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)

View File

@@ -1,126 +0,0 @@
#!/usr/bin/env python3
"""Scan xenia source for #include directives and ensure case-aliases exist
in xwin's flat SDK/CRT include dirs. xwin's default splat adds lowercase
aliases for headers that ship with uppercase names (e.g. Windows.h),
but does NOT add aliases in the reverse direction. xenia (and its
third_party deps) include some headers using historical Microsoft casing
(<ObjBase.h>, <Psapi.h>) that don't match the lowercase filenames the
SDK ships with. This helper creates the missing symlinks.
Idempotent: re-running is safe.
"""
from __future__ import annotations
import os
import re
import sys
from pathlib import Path
INCLUDE_RE = re.compile(r'^\s*#\s*include\s*<([A-Za-z][A-Za-z0-9_/.\-]*\.h)>',
re.MULTILINE)
SOURCE_ROOTS = [
"src",
"third_party/SDL2/src",
"third_party/SDL2/include",
"third_party/discord-rpc/src",
"third_party/fmt",
"third_party/imgui",
]
SOURCE_EXTS = {".h", ".hpp", ".inc", ".c", ".cc", ".cpp", ".cxx"}
def collect_includes(repo_root: Path) -> set[str]:
"""Return set of all <foo.h>-style include targets found in source."""
seen: set[str] = set()
for root in SOURCE_ROOTS:
base = repo_root / root
if not base.exists():
continue
for path in base.rglob("*"):
if path.suffix.lower() not in SOURCE_EXTS:
continue
try:
txt = path.read_text(encoding="utf-8", errors="ignore")
except OSError:
continue
for m in INCLUDE_RE.finditer(txt):
seen.add(m.group(1))
return seen
def index_dir(d: Path) -> dict[str, str]:
"""Return mapping lowercased-basename -> actual basename for files in d."""
if not d.is_dir():
return {}
idx: dict[str, str] = {}
for entry in d.iterdir():
idx.setdefault(entry.name.lower(), entry.name)
return idx
def fix_dir(target_dir: Path, want: set[str]) -> tuple[int, int, list[str]]:
"""For each header name in `want` (basename only), create a symlink to
the case-folded sibling if the exact case doesn't exist. Returns
(created, already_ok, missing-from-sdk)."""
if not target_dir.is_dir():
return 0, 0, []
idx = index_dir(target_dir)
created = 0
already_ok = 0
missing: list[str] = []
for name in want:
# Only look at single-segment names; subdir/foo.h handled separately
if "/" in name:
continue
actual = idx.get(name.lower())
if actual is None:
missing.append(name)
continue
if actual == name:
already_ok += 1
continue
link = target_dir / name
if link.exists() or link.is_symlink():
already_ok += 1
continue
try:
link.symlink_to(actual)
created += 1
except OSError as e:
print(f" ! failed to symlink {link} -> {actual}: {e}",
file=sys.stderr)
return created, already_ok, missing
def main() -> int:
if len(sys.argv) != 3:
print("usage: xwin-case-symlinks.py <xenia-canary-root> <xwin-splat-dir>",
file=sys.stderr)
return 2
repo_root = Path(sys.argv[1]).resolve()
xwin_root = Path(sys.argv[2]).resolve()
if not (xwin_root / "crt" / "include").exists():
print(f"error: {xwin_root}/crt/include missing", file=sys.stderr)
return 1
want = collect_includes(repo_root)
print(f"Collected {len(want)} unique #include <...> targets")
sdk_dirs = [
xwin_root / "crt" / "include",
xwin_root / "sdk" / "include" / "ucrt",
xwin_root / "sdk" / "include" / "um",
xwin_root / "sdk" / "include" / "shared",
xwin_root / "sdk" / "include" / "winrt",
]
total_created = 0
for d in sdk_dirs:
created, ok, _missing = fix_dir(d, want)
if created:
print(f" {d}: created {created} case-symlinks")
total_created += created
print(f"Done: created {total_created} case-symlinks total")
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -1,5 +1,3 @@
//{{NO_DEPENDENCIES}}
// Forward slashes: portable across rc.exe and llvm-rc (which on Linux
// treats backslashes as literal filename chars, breaking path resolution).
MAINICON ICON "../../../assets/icon/icon.ico"
MAINICON ICON "..\\..\\..\\assets\\icon\\icon.ico"

View File

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

View File

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

View File

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

View File

@@ -1,455 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* AUDIT-068 host-side memory-write watch — implementation (xenia-base).
*
* Mirrors AUDIT-067 in spirit (value-CSV cvar, lazy parse, atomic-bool
* activation) but observes the HOST-side write paths instead of the JIT'd
* guest store opcodes. Captures writes performed by xe::store_and_swap<T>
* (xenia/base/memory.h) and by Memory::Zero/Fill/Copy (xenia/memory.cc).
*
* Lives in xenia-base so that the slow-path symbols resolve for callers in
* xenia-base / xenia-cpu / xenia-kernel without depending on xenia-core link
* order. The host→guest VA translation is provided by a function-pointer
* thunk that xenia::Memory::Memory() registers at construction.
*
* See xenia/base/audit_68_host_mem_watch_fwd.h for the API.
* See xenia/cpu/cpu_flags.{h,cc} for the cvars.
******************************************************************************
*/
#include "xenia/base/audit_68_host_mem_watch_fwd.h"
#include <algorithm>
#include <atomic>
#include <chrono>
#include <cstring>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
#include "xenia/base/cvar.h"
#include "xenia/base/logging.h"
#include "xenia/base/threading.h"
// We need the cvars but cpu_flags.h lives in xenia-cpu. To avoid an upward
// dep we re-declare them here with the same macros — cvar.h's DECLARE_*
// macros are header-safe (just `extern` declarations) and resolve against the
// definitions in xenia-cpu/cpu_flags.cc at link time. (xenia-cpu links AFTER
// xenia-base in the executable; symbols in xenia-cpu/cpu_flags.cc are still
// resolvable from xenia-base translation units because the lld pass folds
// all libraries together at the executable level.)
DECLARE_string(audit_68_host_mem_watch_values);
DECLARE_string(audit_68_host_mem_watch_addrs);
DECLARE_string(audit_68_host_mem_read_probe);
namespace xe {
namespace audit_68 {
// Hot-path flag (declared in fwd header). Initial sentinel UINT32_MAX means
// "unparsed"; the very first slow-path call invokes ensure_parsed() which
// replaces the sentinel with the actual active bitmask (0 if both cvars are
// empty, 1/2/3 otherwise). After that, hot-path calls observe the real value
// and bail out cheaply when off.
std::atomic<uint32_t> g_active{0xFFFFFFFFu};
// Host→guest VA translation thunk (declared in fwd header). Set by
// xenia::Memory::Memory() at construction; reset to nullptr by ~Memory().
HostToGuestThunk g_host_to_guest_thunk{nullptr};
// AUDIT-068 Session 3: guest→host translation + page-protect query thunks.
GuestToHostThunk g_guest_to_host_thunk{nullptr};
QueryProtectThunk g_query_protect_thunk{nullptr};
namespace {
constexpr size_t kMaxValues = 8;
constexpr size_t kMaxAddrRanges = 8;
struct AddrRange {
uint32_t start; // inclusive
uint32_t end; // inclusive
};
std::vector<uint32_t> g_values;
std::vector<AddrRange> g_addrs;
std::once_flag g_parsed_flag;
std::chrono::steady_clock::time_point g_t0;
std::once_flag g_t0_once;
int64_t host_ns_since_start() {
std::call_once(g_t0_once,
[]() { g_t0 = std::chrono::steady_clock::now(); });
return std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::steady_clock::now() - g_t0)
.count();
}
void trim(std::string& s) {
while (!s.empty() && (s.front() == ' ' || s.front() == '\t')) {
s.erase(s.begin());
}
while (!s.empty() && (s.back() == ' ' || s.back() == '\t')) {
s.pop_back();
}
}
bool parse_u32(const std::string& tok, uint32_t* out) {
try {
*out = static_cast<uint32_t>(std::stoul(tok, nullptr, 0));
return true;
} catch (...) {
return false;
}
}
void parse_values_csv(const std::string& csv) {
size_t pos = 0;
while (pos < csv.size() && g_values.size() < kMaxValues) {
size_t end = csv.find(',', pos);
std::string tok = csv.substr(pos, end - pos);
trim(tok);
if (!tok.empty()) {
uint32_t v;
if (parse_u32(tok, &v)) {
g_values.push_back(v);
}
}
if (end == std::string::npos) break;
pos = end + 1;
}
}
void parse_addrs_csv(const std::string& csv) {
size_t pos = 0;
while (pos < csv.size() && g_addrs.size() < kMaxAddrRanges) {
size_t end = csv.find(',', pos);
std::string tok = csv.substr(pos, end - pos);
trim(tok);
if (!tok.empty()) {
size_t dash = tok.find('-', 2); // skip leading "0x" if present
AddrRange r{};
if (dash != std::string::npos) {
std::string s = tok.substr(0, dash);
std::string e = tok.substr(dash + 1);
trim(s);
trim(e);
uint32_t a, b;
if (parse_u32(s, &a) && parse_u32(e, &b)) {
r.start = a;
r.end = b;
g_addrs.push_back(r);
}
} else {
uint32_t a;
if (parse_u32(tok, &a)) {
r.start = a;
r.end = a + 7;
g_addrs.push_back(r);
}
}
}
if (end == std::string::npos) break;
pos = end + 1;
}
}
void parse_locked() {
parse_values_csv(cvars::audit_68_host_mem_watch_values);
parse_addrs_csv(cvars::audit_68_host_mem_watch_addrs);
uint32_t bits = 0;
if (!g_values.empty()) bits |= 0x1;
if (!g_addrs.empty()) bits |= 0x2;
g_active.store(bits, std::memory_order_release);
XELOGI(
"AUDIT-068-INIT values_csv=\"{}\" addrs_csv=\"{}\" values_parsed={} "
"addr_ranges_parsed={} active=0x{:X}",
cvars::audit_68_host_mem_watch_values,
cvars::audit_68_host_mem_watch_addrs, g_values.size(), g_addrs.size(),
bits);
for (size_t i = 0; i < g_values.size(); ++i) {
XELOGI("AUDIT-068-INIT value[{}] = 0x{:08X}", i, g_values[i]);
}
for (size_t i = 0; i < g_addrs.size(); ++i) {
XELOGI("AUDIT-068-INIT addr_range[{}] = 0x{:08X}-0x{:08X}", i,
g_addrs[i].start, g_addrs[i].end);
}
}
bool value_matches(uint64_t value, uint8_t size) {
for (uint32_t v : g_values) {
if (size >= 4 && static_cast<uint32_t>(value) == v) return true;
if (size == 8 && static_cast<uint32_t>(value >> 32) == v) return true;
if (size == 2 && (v & 0xFFFF) == (value & 0xFFFF)) return true;
if (size == 1 && (v & 0xFF) == (value & 0xFF)) return true;
}
return false;
}
bool addr_matches(uint32_t guest_va, uint8_t size) {
uint32_t lo = guest_va;
uint32_t hi = guest_va + (size ? size - 1 : 0);
for (const auto& r : g_addrs) {
if (lo <= r.end && hi >= r.start) return true;
}
return false;
}
uint32_t current_tid() { return xe::threading::current_thread_id(); }
void emit(uint32_t guest_va, const void* host_ptr, uint64_t value,
uint8_t size, const char* tag) {
XELOGI(
"AUDIT-068-HOST-WRITE guest_va=0x{:08X} host_ptr=0x{:016X} "
"val=0x{:016X} sz={} fn={} host_ns={} tid={}",
guest_va, reinterpret_cast<uintptr_t>(host_ptr), value,
static_cast<uint32_t>(size), tag ? tag : "<null>",
host_ns_since_start(), current_tid());
}
// ===== AUDIT-068 Session 3 — read-mode probe state =====
constexpr size_t kMaxReadProbes = 8;
struct ReadProbe {
uint32_t guest_va;
uint8_t size; // 1, 2, 4, 8
uint64_t period_ns;
uint64_t last_value;
bool last_was_valid;
};
std::vector<ReadProbe> g_read_probes;
std::atomic<bool> g_read_probe_thread_running{false};
std::atomic<bool> g_read_probe_shutdown{false};
std::thread g_read_probe_thread;
std::once_flag g_read_probe_started;
bool parse_read_probe_tok(const std::string& tok, ReadProbe* out) {
// Expected form: "VA:SIZE:PERIOD_NS" — three colon-separated u64.
size_t c1 = tok.find(':');
if (c1 == std::string::npos) return false;
size_t c2 = tok.find(':', c1 + 1);
if (c2 == std::string::npos) return false;
std::string sva = tok.substr(0, c1);
std::string ssz = tok.substr(c1 + 1, c2 - c1 - 1);
std::string sper = tok.substr(c2 + 1);
trim(sva);
trim(ssz);
trim(sper);
try {
out->guest_va = static_cast<uint32_t>(std::stoul(sva, nullptr, 0));
uint32_t sz = static_cast<uint32_t>(std::stoul(ssz, nullptr, 0));
if (sz != 1 && sz != 2 && sz != 4 && sz != 8) return false;
out->size = static_cast<uint8_t>(sz);
out->period_ns = static_cast<uint64_t>(std::stoull(sper, nullptr, 0));
if (out->period_ns < 1000) out->period_ns = 1000; // 1us floor.
out->last_value = 0;
out->last_was_valid = false;
return true;
} catch (...) {
return false;
}
}
void parse_read_probes_csv(const std::string& csv) {
size_t pos = 0;
while (pos < csv.size() && g_read_probes.size() < kMaxReadProbes) {
size_t end = csv.find(',', pos);
std::string tok = csv.substr(pos, end - pos);
trim(tok);
if (!tok.empty()) {
ReadProbe rp{};
if (parse_read_probe_tok(tok, &rp)) {
g_read_probes.push_back(rp);
}
}
if (end == std::string::npos) break;
pos = end + 1;
}
}
uint64_t sample_at(uint32_t guest_va, uint8_t size, bool* out_valid) {
*out_valid = false;
if (!g_guest_to_host_thunk || !g_query_protect_thunk) return 0;
uint32_t prot = 0;
if (!g_query_protect_thunk(guest_va, &prot)) return 0;
// Page must have at least read permission. The protect bits map to
// xe::memory::PageAccess: kReadOnly=1, kReadWrite=2, kExecuteReadOnly=3,
// kExecuteReadWrite=4. kNoAccess=0. Accept anything non-zero — caller
// distinguishes via the second-pass change detector anyway.
if (prot == 0) return 0;
const void* hp = g_guest_to_host_thunk(guest_va);
if (!hp) return 0;
uint64_t v = 0;
// Guest memory is big-endian. We use raw byte loads to avoid alignment
// traps for size>4 on possibly-unaligned VAs. The "value" we log is the
// host-endian interpretation of the BE bytes (matches store_and_swap's
// logging convention: the byte-swapped scalar).
const uint8_t* bp = reinterpret_cast<const uint8_t*>(hp);
switch (size) {
case 1: v = bp[0]; break;
case 2: v = (uint64_t(bp[0]) << 8) | bp[1]; break;
case 4:
v = (uint64_t(bp[0]) << 24) | (uint64_t(bp[1]) << 16) |
(uint64_t(bp[2]) << 8) | bp[3];
break;
case 8:
v = (uint64_t(bp[0]) << 56) | (uint64_t(bp[1]) << 48) |
(uint64_t(bp[2]) << 40) | (uint64_t(bp[3]) << 32) |
(uint64_t(bp[4]) << 24) | (uint64_t(bp[5]) << 16) |
(uint64_t(bp[6]) << 8) | bp[7];
break;
}
*out_valid = true;
return v;
}
void read_probe_thread_main() {
// Compute the GCD-ish min poll period across all probes; sleep that long
// between scans. Each probe fires only when its own period_ns has elapsed
// since the last sample (per-probe `next_fire_ns`).
uint64_t min_period_ns = UINT64_MAX;
for (const auto& p : g_read_probes) {
if (p.period_ns < min_period_ns) min_period_ns = p.period_ns;
}
if (min_period_ns == UINT64_MAX) return;
// Per-probe next-fire times.
std::vector<uint64_t> next_fire(g_read_probes.size(), 0);
XELOGI(
"AUDIT-068-READ-INIT probe_count={} min_period_ns={} thread spawned",
g_read_probes.size(), min_period_ns);
for (size_t i = 0; i < g_read_probes.size(); ++i) {
XELOGI("AUDIT-068-READ-INIT probe[{}] va=0x{:08X} size={} period_ns={}",
i, g_read_probes[i].guest_va,
static_cast<uint32_t>(g_read_probes[i].size),
g_read_probes[i].period_ns);
}
while (!g_read_probe_shutdown.load(std::memory_order_relaxed)) {
int64_t now_ns = host_ns_since_start();
for (size_t i = 0; i < g_read_probes.size(); ++i) {
if (static_cast<uint64_t>(now_ns) < next_fire[i]) continue;
ReadProbe& rp = g_read_probes[i];
bool valid = false;
uint64_t v = sample_at(rp.guest_va, rp.size, &valid);
if (valid) {
if (!rp.last_was_valid) {
// First successful read: emit the initial value, do NOT call it a
// "change" — but log so we know when the VA mapped.
XELOGI(
"AUDIT-068-READ-INITIAL va=0x{:08X} val=0x{:016X} sz={} "
"host_ns={} tid=probe",
rp.guest_va, v, static_cast<uint32_t>(rp.size), now_ns);
rp.last_value = v;
rp.last_was_valid = true;
} else if (v != rp.last_value) {
XELOGI(
"AUDIT-068-READ-CHANGE va=0x{:08X} old=0x{:016X} "
"new=0x{:016X} sz={} host_ns={} tid=probe",
rp.guest_va, rp.last_value, v, static_cast<uint32_t>(rp.size),
now_ns);
rp.last_value = v;
}
} else if (rp.last_was_valid) {
// Was valid, now invalid — page unmapped/reprotected.
XELOGI(
"AUDIT-068-READ-UNMAPPED va=0x{:08X} last=0x{:016X} sz={} "
"host_ns={} tid=probe",
rp.guest_va, rp.last_value, static_cast<uint32_t>(rp.size),
now_ns);
rp.last_was_valid = false;
}
next_fire[i] = static_cast<uint64_t>(now_ns) + rp.period_ns;
}
// Sleep until the next earliest fire, but no shorter than 1us and no
// longer than min_period_ns (to keep shutdown latency bounded).
uint64_t sleep_ns = min_period_ns;
if (sleep_ns < 1000) sleep_ns = 1000;
std::this_thread::sleep_for(std::chrono::nanoseconds(sleep_ns));
}
XELOGI("AUDIT-068-READ-EXIT thread shutting down");
}
void start_read_probe_thread_if_configured() {
std::call_once(g_read_probe_started, []() {
parse_read_probes_csv(cvars::audit_68_host_mem_read_probe);
if (g_read_probes.empty()) return;
if (!g_guest_to_host_thunk || !g_query_protect_thunk) {
XELOGI(
"AUDIT-068-READ-INIT thunks not ready (guest_to_host={} "
"query_protect={}) — read probe deferred",
(void*)g_guest_to_host_thunk, (void*)g_query_protect_thunk);
return;
}
g_read_probe_thread_running.store(true, std::memory_order_release);
g_read_probe_thread = std::thread(&read_probe_thread_main);
g_read_probe_thread.detach(); // best-effort; daemon-style.
});
}
} // namespace
void ensure_parsed() { std::call_once(g_parsed_flag, parse_locked); }
void check_host_write_slowpath(const void* host_ptr, uint64_t value,
uint8_t size, const char* tag) {
// AUDIT-068 Session 2: defer parsing until Memory::Memory() has registered
// the host→guest thunk. This guarantees the cmdline cvar override has been
// applied AND the logging subsystem is alive before we latch g_active.
// Without this gate, a be<T>::set() call during static-init (e.g. from a
// global initializer in another translation unit) would trigger
// parse_locked() before cpu_flags.cc's cvar objects are constructed —
// latching g_active=0 permanently and silencing the watch.
HostToGuestThunk thunk = g_host_to_guest_thunk;
if (!thunk) return;
ensure_parsed();
// AUDIT-068 Session 3: lazy-start the read-probe poll thread. Same gate as
// ensure_parsed() — must come after Memory::Memory() has registered the
// thunks so the probe can read pages safely.
start_read_probe_thread_if_configured();
uint32_t active = g_active.load(std::memory_order_acquire);
if (active == 0) return;
uint32_t guest_va = 0;
if (thunk) {
guest_va = thunk(host_ptr);
}
bool hit = false;
if ((active & 0x1) && value_matches(value, size)) hit = true;
if (!hit && (active & 0x2) && thunk && addr_matches(guest_va, size)) {
hit = true;
}
if (!hit) return;
emit(guest_va, host_ptr, value, size, tag);
}
void check_guest_va_slowpath(uint32_t guest_va, uint64_t value, uint8_t size,
const char* tag) {
// AUDIT-068 Session 2: same static-init gate as check_host_write_slowpath.
// Callers (Memory::Zero/Fill/Copy + xex_module audit68_prescan_memcpy) only
// run after Memory::Memory(), but defensive in case of future expansion.
if (!g_host_to_guest_thunk) return;
ensure_parsed();
uint32_t active = g_active.load(std::memory_order_acquire);
if (active == 0) return;
bool hit = false;
if ((active & 0x1) && value_matches(value, size)) hit = true;
if (!hit && (active & 0x2) && addr_matches(guest_va, size)) hit = true;
if (!hit) return;
emit(guest_va, nullptr, value, size, tag);
}
} // namespace audit_68
} // namespace xe

View File

@@ -1,95 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* AUDIT-068: host-side memory-write watch — forward declarations only.
*
* Declarations here are intentionally minimal so that xenia/base/memory.h can
* include this without pulling in xenia/memory.h (which would create a
* circular dependency: xenia-base → xenia-core → xenia-base). The full
* definitions live in xenia/audit_68_host_mem_watch.{h,cc} (xenia-core).
*
* Hot path: callers (the integer specializations of xe::store_and_swap<T>)
* load the atomic flag once. When it is 0 (default), no further work is done
* — a single relaxed atomic load and a predictable branch.
******************************************************************************
*/
#ifndef XENIA_BASE_AUDIT_68_HOST_MEM_WATCH_FWD_H_
#define XENIA_BASE_AUDIT_68_HOST_MEM_WATCH_FWD_H_
#include <atomic>
#include <cstdint>
namespace xe {
namespace audit_68 {
// 0 = inactive (default). Non-zero = the cvars have been parsed and at least
// one watch is configured. Set lazily by check_host_write_slowpath() on first
// call after cvar parsing. Loaded relaxed on the hot path.
//
// Implementation lives in xenia-base (audit_68_host_mem_watch_base.cc) so
// that callers in xenia-base/xenia-cpu/xenia-kernel can resolve the symbol
// without depending on xenia-core link order.
extern std::atomic<uint32_t> g_active;
// Host-pointer → guest-VA translation thunk. xenia/memory.cc::Memory::Memory()
// registers a function pointer here that wraps Memory::HostToGuestVirtual.
// Until set, the slow path falls back to logging the raw host pointer.
using HostToGuestThunk = uint32_t (*)(const void*);
extern HostToGuestThunk g_host_to_guest_thunk;
// AUDIT-068 Session 3 — read-mode probe support.
//
// Guest-VA → host-pointer translation thunk (wraps Memory::TranslateVirtual).
// Used by the read-probe poll thread to sample bytes at configured guest VAs.
// May return non-null even for unmapped/uncommitted VAs (the underlying
// translation is arithmetic — virtual_membase_ + va) — callers MUST consult
// the QueryProtect thunk before dereferencing.
using GuestToHostThunk = const void* (*)(uint32_t);
extern GuestToHostThunk g_guest_to_host_thunk;
// Returns true iff the page containing `guest_va` is committed and readable;
// out_protect receives the raw page protect bits (kProtectRead, etc.). Wraps
// Memory::LookupHeap() + BaseHeap::QueryProtect(). Used as a guard before the
// read-probe samples bytes (early-boot heap-not-yet-mapped path must NOT
// crash).
using QueryProtectThunk = bool (*)(uint32_t, uint32_t* /*out_protect*/);
extern QueryProtectThunk g_query_protect_thunk;
// Slow path. Only invoked when g_active is non-zero. Implementation in
// xenia/base/audit_68_host_mem_watch_base.cc (xenia-base).
//
// host_ptr: the host pointer being written (from store_and_swap's `mem`).
// value: the value being stored (zero-extended to u64).
// size: 1, 2, 4 or 8.
// tag: caller-provided tag string (e.g. "store_and_swap<u32>"). Logged
// verbatim, no formatting. Must be a static string (lifetime
// beyond this call).
void check_host_write_slowpath(const void* host_ptr, uint64_t value,
uint8_t size, const char* tag);
// Same as above, but with a known guest VA (for callers like Memory::Zero/
// Fill/Copy that have the VA but not a single host pointer).
void check_guest_va_slowpath(uint32_t guest_va, uint64_t value, uint8_t size,
const char* tag);
// Inline hot-path wrappers. Single relaxed atomic load + branch when inactive.
inline void check_host_write(const void* host_ptr, uint64_t value, uint8_t size,
const char* tag) {
if (g_active.load(std::memory_order_relaxed) != 0) [[unlikely]] {
check_host_write_slowpath(host_ptr, value, size, tag);
}
}
inline void check_guest_va(uint32_t guest_va, uint64_t value, uint8_t size,
const char* tag) {
if (g_active.load(std::memory_order_relaxed) != 0) [[unlikely]] {
check_guest_va_slowpath(guest_va, value, size, tag);
}
}
} // namespace audit_68
} // namespace xe
#endif // XENIA_BASE_AUDIT_68_HOST_MEM_WATCH_FWD_H_

View File

@@ -11,7 +11,6 @@
#define XENIA_BASE_BYTE_ORDER_H_
#include <cstdint>
#include <type_traits>
#if defined __has_include
#if __has_include(<version>)
#include <version>
@@ -22,7 +21,6 @@
#endif
#include "xenia/base/assert.h"
#include "xenia/base/audit_68_host_mem_watch_fwd.h"
#include "xenia/base/platform.h"
#if !__cpp_lib_endian
@@ -90,30 +88,6 @@ struct endian_store {
operator T() const { return get(); }
void set(const T& src) {
// AUDIT-068 Session 2: hook the canonical be<T>/le<T> write path. Gated
// on the host→guest thunk being installed by Memory::Memory(); without
// that there is no Memory and therefore no possible guest-memory write.
// This ALSO prevents the slow-path from running during static-init order
// (which would race the cvar object construction in cpu_flags.cc and
// permanently latch g_active=0 before --audit_68_* cmdline override
// applies). See reading-error #35 / Session 2 plan.
if constexpr (sizeof(T) <= 8 && std::is_integral_v<T>) {
if (xe::audit_68::g_host_to_guest_thunk != nullptr) [[unlikely]] {
uint64_t v;
if constexpr (sizeof(T) == 8) {
v = static_cast<uint64_t>(src);
} else if constexpr (sizeof(T) == 4) {
v = static_cast<uint64_t>(static_cast<uint32_t>(src));
} else if constexpr (sizeof(T) == 2) {
v = static_cast<uint64_t>(static_cast<uint16_t>(src));
} else {
v = static_cast<uint64_t>(static_cast<uint8_t>(src));
}
xe::audit_68::check_host_write(
&value, v, static_cast<uint8_t>(sizeof(T)),
E == std::endian::big ? "be<T>::set" : "le<T>::set");
}
}
if constexpr (std::endian::native == E) {
value = src;
} else {

View File

@@ -26,10 +26,8 @@ namespace xe {
class Win32MappedMemory : public MappedMemory {
public:
// CreateFile returns INVALID_HANDLE_VALUE in case of failure.
// INVALID_HANDLE_VALUE expands to a reinterpret_cast which MSVC accepts in
// constexpr as an extension; clang-cl rejects it per the C++ standard.
// `const` works for all the equality/assignment uses below.
static inline const HANDLE kFileHandleInvalid = INVALID_HANDLE_VALUE;
// chrispy: made inline const to get around clang error
static inline constexpr HANDLE kFileHandleInvalid = INVALID_HANDLE_VALUE;
// CreateFileMapping returns nullptr in case of failure.
static constexpr HANDLE kMappingHandleInvalid = nullptr;

View File

@@ -18,7 +18,6 @@
#include <string_view>
#include <type_traits>
#include "xenia/base/audit_68_host_mem_watch_fwd.h"
#include "xenia/base/byte_order.h"
namespace xe {
@@ -355,52 +354,34 @@ template <typename T>
void store(void* mem, const T& value);
template <>
inline void store<int8_t>(void* mem, const int8_t& value) {
xe::audit_68::check_host_write(mem, static_cast<uint64_t>(
static_cast<uint8_t>(value)),
1, "store<i8>");
*reinterpret_cast<int8_t*>(mem) = value;
}
template <>
inline void store<uint8_t>(void* mem, const uint8_t& value) {
xe::audit_68::check_host_write(mem, static_cast<uint64_t>(value), 1,
"store<u8>");
*reinterpret_cast<uint8_t*>(mem) = value;
}
template <>
inline void store<int16_t>(void* mem, const int16_t& value) {
xe::audit_68::check_host_write(mem, static_cast<uint64_t>(
static_cast<uint16_t>(value)),
2, "store<i16>");
*reinterpret_cast<int16_t*>(mem) = value;
}
template <>
inline void store<uint16_t>(void* mem, const uint16_t& value) {
xe::audit_68::check_host_write(mem, static_cast<uint64_t>(value), 2,
"store<u16>");
*reinterpret_cast<uint16_t*>(mem) = value;
}
template <>
inline void store<int32_t>(void* mem, const int32_t& value) {
xe::audit_68::check_host_write(mem, static_cast<uint64_t>(
static_cast<uint32_t>(value)),
4, "store<i32>");
*reinterpret_cast<int32_t*>(mem) = value;
}
template <>
inline void store<uint32_t>(void* mem, const uint32_t& value) {
xe::audit_68::check_host_write(mem, static_cast<uint64_t>(value), 4,
"store<u32>");
*reinterpret_cast<uint32_t*>(mem) = value;
}
template <>
inline void store<int64_t>(void* mem, const int64_t& value) {
xe::audit_68::check_host_write(mem, static_cast<uint64_t>(value), 8,
"store<i64>");
*reinterpret_cast<int64_t*>(mem) = value;
}
template <>
inline void store<uint64_t>(void* mem, const uint64_t& value) {
xe::audit_68::check_host_write(mem, value, 8, "store<u64>");
*reinterpret_cast<uint64_t*>(mem) = value;
}
template <>
@@ -430,52 +411,34 @@ template <typename T>
void store_and_swap(void* mem, const T& value);
template <>
inline void store_and_swap<int8_t>(void* mem, const int8_t& value) {
xe::audit_68::check_host_write(mem, static_cast<uint64_t>(
static_cast<uint8_t>(value)),
1, "store_and_swap<i8>");
*reinterpret_cast<int8_t*>(mem) = value;
}
template <>
inline void store_and_swap<uint8_t>(void* mem, const uint8_t& value) {
xe::audit_68::check_host_write(mem, static_cast<uint64_t>(value), 1,
"store_and_swap<u8>");
*reinterpret_cast<uint8_t*>(mem) = value;
}
template <>
inline void store_and_swap<int16_t>(void* mem, const int16_t& value) {
xe::audit_68::check_host_write(mem, static_cast<uint64_t>(
static_cast<uint16_t>(value)),
2, "store_and_swap<i16>");
*reinterpret_cast<int16_t*>(mem) = byte_swap(value);
}
template <>
inline void store_and_swap<uint16_t>(void* mem, const uint16_t& value) {
xe::audit_68::check_host_write(mem, static_cast<uint64_t>(value), 2,
"store_and_swap<u16>");
*reinterpret_cast<uint16_t*>(mem) = byte_swap(value);
}
template <>
inline void store_and_swap<int32_t>(void* mem, const int32_t& value) {
xe::audit_68::check_host_write(mem, static_cast<uint64_t>(
static_cast<uint32_t>(value)),
4, "store_and_swap<i32>");
*reinterpret_cast<int32_t*>(mem) = byte_swap(value);
}
template <>
inline void store_and_swap<uint32_t>(void* mem, const uint32_t& value) {
xe::audit_68::check_host_write(mem, static_cast<uint64_t>(value), 4,
"store_and_swap<u32>");
*reinterpret_cast<uint32_t*>(mem) = byte_swap(value);
}
template <>
inline void store_and_swap<int64_t>(void* mem, const int64_t& value) {
xe::audit_68::check_host_write(mem, static_cast<uint64_t>(value), 8,
"store_and_swap<i64>");
*reinterpret_cast<int64_t*>(mem) = byte_swap(value);
}
template <>
inline void store_and_swap<uint64_t>(void* mem, const uint64_t& value) {
xe::audit_68::check_host_write(mem, value, 8, "store_and_swap<u64>");
*reinterpret_cast<uint64_t*>(mem) = byte_swap(value);
}
template <>

View File

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

View File

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

View File

@@ -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, &param);
if (ret != 0) {
return -1;
}
return param.sched_priority;
int policy;
sched_param param{};
int ret = pthread_getschedparam(thread_, &policy, &param);
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, &param);
if (res == 0) {
return;
sched_param param{};
param.sched_priority = new_priority;
int res = pthread_setschedparam(thread_, SCHED_FIFO, &param);
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_ =

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,28 +9,12 @@
#include "xenia/cpu/ppc/ppc_emit-private.h"
#include <vector>
#include "xenia/base/assert.h"
#include "xenia/cpu/cpu_flags.h"
#include "xenia/cpu/ppc/ppc_context.h"
#include "xenia/cpu/ppc/ppc_hir_builder.h"
#include <cmath>
// AUDIT-067: forward-decls. Defined in ppc_emit_memory.cc / ppc_hir_builder.cc.
namespace xe {
namespace cpu {
namespace audit67 {
const std::vector<uint32_t>& vals();
}
namespace ppc {
void EmitAudit67ValueWatchVec(PPCHIRBuilder& f, uint32_t pc,
::xe::cpu::hir::Value* vec128,
::xe::cpu::hir::Value* ea);
}
}
}
namespace xe {
namespace cpu {
namespace ppc {
@@ -191,21 +175,6 @@ int InstrEmit_stvewx_(PPCHIRBuilder& f, const InstrData& i, uint32_t vd,
f.Shr(f.And(f.Truncate(ea, INT8_TYPE), f.LoadConstantUint8(0xF)), 2);
Value* v = f.Extract(f.LoadVR(vd), el, INT32_TYPE);
f.Store(ea, f.ByteSwap(v));
if (!::xe::cpu::audit67::vals().empty()) {
// For stvewx: only one lane is actually stored; piggyback on the scalar
// value-watch helper by emitting the equivalent of stw of v at ea.
Value* pc_hi64 =
f.LoadConstantUint64(static_cast<uint64_t>(i.address) << 32);
Value* ea_lo64 = f.ZeroExtend(f.Truncate(ea, INT32_TYPE), INT64_TYPE);
Value* packed = f.Or(pc_hi64, ea_lo64);
const auto& vals = ::xe::cpu::audit67::vals();
for (size_t idx = 0; idx < vals.size(); ++idx) {
Value* cmp = f.CompareEQ(v, f.LoadConstantUint32(vals[idx]));
f.StoreContext(offsetof(::xe::cpu::ppc::PPCContext, scratch), packed);
f.ContextBarrier();
f.TrapTrue(cmp, static_cast<uint16_t>(250 + idx));
}
}
return 0;
}
int InstrEmit_stvewx(PPCHIRBuilder& f, const InstrData& i) {
@@ -218,11 +187,7 @@ int InstrEmit_stvewx128(PPCHIRBuilder& f, const InstrData& i) {
int InstrEmit_stvx_(PPCHIRBuilder& f, const InstrData& i, uint32_t vd,
uint32_t ra, uint32_t rb) {
Value* ea = f.And(CalculateEA_0(f, ra, rb), f.LoadConstantUint64(~0xFull));
Value* vec = f.LoadVR(vd);
f.Store(ea, f.ByteSwap(vec));
if (!::xe::cpu::audit67::vals().empty()) {
EmitAudit67ValueWatchVec(f, i.address, vec, ea);
}
f.Store(ea, f.ByteSwap(f.LoadVR(vd)));
return 0;
}
int InstrEmit_stvx(PPCHIRBuilder& f, const InstrData& i) {

View File

@@ -10,22 +10,11 @@
#include "xenia/cpu/ppc/ppc_emit-private.h"
#include <stddef.h>
#include <vector>
#include "xenia/base/assert.h"
#include "xenia/base/cvar.h"
#include "xenia/cpu/cpu_flags.h"
#include "xenia/cpu/ppc/ppc_context.h"
#include "xenia/cpu/ppc/ppc_hir_builder.h"
// AUDIT-067: forward-decl of value-watch table (defined in ppc_hir_builder.cc).
namespace xe {
namespace cpu {
namespace audit67 {
const std::vector<uint32_t>& vals();
} // namespace audit67
} // namespace cpu
} // namespace xe
DEFINE_bool(
disable_prefetch_and_cachecontrol, true,
"Disables translating ppc prefetch/cache flush instructions to host "
@@ -78,90 +67,6 @@ void StoreEA(PPCHIRBuilder& f, uint32_t rt, Value* ea) {
f.StoreGPR(rt, ea);
}
// AUDIT-067: emit a runtime equality check on the 32-bit value-to-be-stored
// against each configured watch value. On match, store (pc, EA) packed into
// the PPCContext scratch field so the native trap handler can read them,
// then fire a trap with code (kTrapBase + idx). Done host-side as a
// build-time pc constant + a runtime EA truncate, packed as
// (pc << 32) | (ea & 0xFFFFFFFF) so the handler can decompose.
static void EmitAudit67ValueWatch(PPCHIRBuilder& f, uint32_t pc, Value* val32,
Value* ea) {
const auto& vals = ::xe::cpu::audit67::vals();
if (vals.empty()) return;
// pc is known at JIT time → emit as constant; ea is runtime.
Value* pc_hi64 = f.LoadConstantUint64(static_cast<uint64_t>(pc) << 32);
Value* ea_lo64 = f.ZeroExtend(f.Truncate(ea, INT32_TYPE), INT64_TYPE);
Value* packed = f.Or(pc_hi64, ea_lo64);
for (size_t idx = 0; idx < vals.size(); ++idx) {
Value* cmp = f.CompareEQ(val32, f.LoadConstantUint32(vals[idx]));
f.StoreContext(offsetof(::xe::cpu::ppc::PPCContext, scratch), packed);
f.ContextBarrier();
f.TrapTrue(cmp, static_cast<uint16_t>(250 + idx));
}
}
// AUDIT-067 128-bit (vector) variant: checks each of the 4 32-bit lanes in a
// vector store. Used for stvx/stvxl/stvewx (memcpy-derived installs may use
// 128-bit vector stores). The matched lane is reflected in the dst by
// adding (lane * 4) so the handler can see exactly where in memory the
// value lands. Declared with external linkage so altivec.cc can call it.
void EmitAudit67ValueWatchVec(PPCHIRBuilder& f, uint32_t pc,
Value* vec128, Value* ea) {
const auto& vals = ::xe::cpu::audit67::vals();
if (vals.empty()) return;
Value* pc_hi64 = f.LoadConstantUint64(static_cast<uint64_t>(pc) << 32);
for (size_t idx = 0; idx < vals.size(); ++idx) {
Value* watch = f.LoadConstantUint32(vals[idx]);
for (uint8_t lane = 0; lane < 4; ++lane) {
Value* lane_val = f.Extract(vec128, lane, INT32_TYPE);
Value* cmp = f.CompareEQ(lane_val, watch);
Value* lane_off = f.LoadConstantUint32(static_cast<uint32_t>(lane * 4));
Value* dst32 = f.Add(f.Truncate(ea, INT32_TYPE), lane_off);
Value* packed = f.Or(pc_hi64, f.ZeroExtend(dst32, INT64_TYPE));
f.StoreContext(offsetof(::xe::cpu::ppc::PPCContext, scratch), packed);
f.ContextBarrier();
f.TrapTrue(cmp, static_cast<uint16_t>(250 + idx));
}
}
}
// AUDIT-067 64-bit variant: same as above but checks BOTH halves of a 64-bit
// stored value. EA points at the start of the 8-byte store; the matched half
// is encoded into the trap idx via (250 + 2*idx + half), where half=0 means
// upper 32 bits (lower address), half=1 means lower 32 bits (upper address).
static void EmitAudit67ValueWatch64(PPCHIRBuilder& f, uint32_t pc, Value* val64,
Value* ea) {
const auto& vals = ::xe::cpu::audit67::vals();
if (vals.empty()) return;
// PowerPC is big-endian: u64 stored at EA places upper-32 bits at EA+0
// and lower-32 bits at EA+4. Check both halves against each watch value.
Value* upper32 = f.Truncate(f.Shr(val64, int8_t(32)), INT32_TYPE); // bits[63:32]
Value* lower32 = f.Truncate(val64, INT32_TYPE); // bits[31:0]
Value* pc_hi64 = f.LoadConstantUint64(static_cast<uint64_t>(pc) << 32);
for (size_t idx = 0; idx < vals.size(); ++idx) {
// Upper half lands at EA+0.
{
Value* cmp = f.CompareEQ(upper32, f.LoadConstantUint32(vals[idx]));
Value* ea_lo64 = f.ZeroExtend(f.Truncate(ea, INT32_TYPE), INT64_TYPE);
Value* packed = f.Or(pc_hi64, ea_lo64);
f.StoreContext(offsetof(::xe::cpu::ppc::PPCContext, scratch), packed);
f.ContextBarrier();
f.TrapTrue(cmp, static_cast<uint16_t>(250 + idx));
}
// Lower half lands at EA+4.
{
Value* cmp = f.CompareEQ(lower32, f.LoadConstantUint32(vals[idx]));
Value* ea_plus4 =
f.Add(f.Truncate(ea, INT32_TYPE), f.LoadConstantUint32(4));
Value* ea_lo64 = f.ZeroExtend(ea_plus4, INT64_TYPE);
Value* packed = f.Or(pc_hi64, ea_lo64);
f.StoreContext(offsetof(::xe::cpu::ppc::PPCContext, scratch), packed);
f.ContextBarrier();
f.TrapTrue(cmp, static_cast<uint16_t>(250 + idx));
}
}
}
// Integer load (A-13)
int InstrEmit_lbz(PPCHIRBuilder& f, const InstrData& i) {
@@ -613,11 +518,9 @@ int InstrEmit_stw(PPCHIRBuilder& f, const InstrData& i) {
b = f.LoadGPR(i.D.RA);
}
Value* offset = f.LoadConstantInt64(XEEXTS16(i.D.DS));
Value* val32 = f.Truncate(f.LoadGPR(i.D.RT), INT32_TYPE);
f.StoreOffset(b, offset, f.ByteSwap(val32));
if (!::xe::cpu::audit67::vals().empty()) {
EmitAudit67ValueWatch(f, i.address, val32, f.Add(b, offset));
}
f.StoreOffset(b, offset,
f.ByteSwap(f.Truncate(f.LoadGPR(i.D.RT), INT32_TYPE)));
return 0;
}
@@ -629,14 +532,10 @@ int InstrEmit_stmw(PPCHIRBuilder& f, const InstrData& i) {
b = f.LoadGPR(i.D.RA);
}
const bool watch_active = !::xe::cpu::audit67::vals().empty();
for (uint32_t j = 0; j < 32 - i.D.RT; ++j) {
Value* offset = f.LoadConstantInt64(XEEXTS16(i.D.DS) + j * 4);
Value* val32 = f.Truncate(f.LoadGPR(i.D.RT + j), INT32_TYPE);
f.StoreOffset(b, offset, f.ByteSwap(val32));
if (watch_active) {
EmitAudit67ValueWatch(f, i.address, val32, f.Add(b, offset));
}
f.StoreOffset(b, offset,
f.ByteSwap(f.Truncate(f.LoadGPR(i.D.RT + j), INT32_TYPE)));
}
return 0;
}
@@ -646,12 +545,8 @@ int InstrEmit_stwu(PPCHIRBuilder& f, const InstrData& i) {
// MEM(EA, 4) <- (RS)[32:63]
// RA <- EA
Value* ea = CalculateEA_i(f, i.D.RA, XEEXTS16(i.D.DS));
Value* val32 = f.Truncate(f.LoadGPR(i.D.RT), INT32_TYPE);
f.Store(ea, f.ByteSwap(val32));
f.Store(ea, f.ByteSwap(f.Truncate(f.LoadGPR(i.D.RT), INT32_TYPE)));
StoreEA(f, i.D.RA, ea);
if (!::xe::cpu::audit67::vals().empty()) {
EmitAudit67ValueWatch(f, i.address, val32, ea);
}
return 0;
}
@@ -660,12 +555,8 @@ int InstrEmit_stwux(PPCHIRBuilder& f, const InstrData& i) {
// MEM(EA, 4) <- (RS)[32:63]
// RA <- EA
Value* ea = CalculateEA(f, i.X.RA, i.X.RB);
Value* val32 = f.Truncate(f.LoadGPR(i.X.RT), INT32_TYPE);
f.Store(ea, f.ByteSwap(val32));
f.Store(ea, f.ByteSwap(f.Truncate(f.LoadGPR(i.X.RT), INT32_TYPE)));
StoreEA(f, i.X.RA, ea);
if (!::xe::cpu::audit67::vals().empty()) {
EmitAudit67ValueWatch(f, i.address, val32, ea);
}
return 0;
}
@@ -677,11 +568,7 @@ int InstrEmit_stwx(PPCHIRBuilder& f, const InstrData& i) {
// EA <- b + (RB)
// MEM(EA, 4) <- (RS)[32:63]
Value* ea = CalculateEA_0(f, i.X.RA, i.X.RB);
Value* val32 = f.Truncate(f.LoadGPR(i.X.RT), INT32_TYPE);
f.Store(ea, f.ByteSwap(val32));
if (!::xe::cpu::audit67::vals().empty()) {
EmitAudit67ValueWatch(f, i.address, val32, ea);
}
f.Store(ea, f.ByteSwap(f.Truncate(f.LoadGPR(i.X.RT), INT32_TYPE)));
return 0;
}
@@ -700,11 +587,7 @@ int InstrEmit_std(PPCHIRBuilder& f, const InstrData& i) {
}
Value* offset = f.LoadConstantInt64(XEEXTS16(i.DS.DS << 2));
Value* val64 = f.LoadGPR(i.DS.RT);
f.StoreOffset(b, offset, f.ByteSwap(val64));
if (!::xe::cpu::audit67::vals().empty()) {
EmitAudit67ValueWatch64(f, i.address, val64, f.Add(b, offset));
}
f.StoreOffset(b, offset, f.ByteSwap(f.LoadGPR(i.DS.RT)));
return 0;
}
@@ -713,12 +596,8 @@ int InstrEmit_stdu(PPCHIRBuilder& f, const InstrData& i) {
// MEM(EA, 8) <- (RS)
// RA <- EA
Value* ea = CalculateEA_i(f, i.DS.RA, XEEXTS16(i.DS.DS << 2));
Value* val64 = f.LoadGPR(i.DS.RT);
f.Store(ea, f.ByteSwap(val64));
f.Store(ea, f.ByteSwap(f.LoadGPR(i.DS.RT)));
StoreEA(f, i.DS.RA, ea);
if (!::xe::cpu::audit67::vals().empty()) {
EmitAudit67ValueWatch64(f, i.address, val64, ea);
}
return 0;
}
@@ -727,12 +606,8 @@ int InstrEmit_stdux(PPCHIRBuilder& f, const InstrData& i) {
// MEM(EA, 8) <- (RS)
// RA <- EA
Value* ea = CalculateEA(f, i.X.RA, i.X.RB);
Value* val64 = f.LoadGPR(i.X.RT);
f.Store(ea, f.ByteSwap(val64));
f.Store(ea, f.ByteSwap(f.LoadGPR(i.X.RT)));
StoreEA(f, i.X.RA, ea);
if (!::xe::cpu::audit67::vals().empty()) {
EmitAudit67ValueWatch64(f, i.address, val64, ea);
}
return 0;
}
@@ -744,11 +619,7 @@ int InstrEmit_stdx(PPCHIRBuilder& f, const InstrData& i) {
// EA <- b + (RB)
// MEM(EA, 8) <- (RS)
Value* ea = CalculateEA_0(f, i.X.RA, i.X.RB);
Value* val64 = f.LoadGPR(i.X.RT);
f.Store(ea, f.ByteSwap(val64));
if (!::xe::cpu::audit67::vals().empty()) {
EmitAudit67ValueWatch64(f, i.address, val64, ea);
}
f.Store(ea, f.ByteSwap(f.LoadGPR(i.X.RT)));
return 0;
}
@@ -813,11 +684,7 @@ int InstrEmit_stwbrx(PPCHIRBuilder& f, const InstrData& i) {
// EA <- b + (RB)
// MEM(EA, 4) <- bswap((RS)[32:63])
Value* ea = CalculateEA_0(f, i.X.RA, i.X.RB);
Value* val32 = f.Truncate(f.LoadGPR(i.X.RT), INT32_TYPE);
f.Store(ea, val32);
if (!::xe::cpu::audit67::vals().empty()) {
EmitAudit67ValueWatch(f, i.address, val32, ea);
}
f.Store(ea, f.Truncate(f.LoadGPR(i.X.RT), INT32_TYPE));
return 0;
}
@@ -829,11 +696,7 @@ int InstrEmit_stdbrx(PPCHIRBuilder& f, const InstrData& i) {
// EA <- b + (RB)
// MEM(EA, 8) <- bswap(RS)
Value* ea = CalculateEA_0(f, i.X.RA, i.X.RB);
Value* val64 = f.LoadGPR(i.X.RT);
f.Store(ea, val64);
if (!::xe::cpu::audit67::vals().empty()) {
EmitAudit67ValueWatch64(f, i.address, val64, ea);
}
f.Store(ea, f.LoadGPR(i.X.RT));
return 0;
}
@@ -980,8 +843,7 @@ int InstrEmit_stdcx(PPCHIRBuilder& f, const InstrData& i) {
// This will always succeed if under the global lock, however.
Value* ea = CalculateEA_0(f, i.X.RA, i.X.RB);
Value* val64 = f.LoadGPR(i.X.RT);
Value* rt = f.ByteSwap(val64);
Value* rt = f.ByteSwap(f.LoadGPR(i.X.RT));
if (cvars::no_reserved_ops) {
f.Store(ea, rt);
@@ -1000,9 +862,6 @@ int InstrEmit_stdcx(PPCHIRBuilder& f, const InstrData& i) {
if (!cvars::no_reserved_ops) {
f.MemoryBarrier();
}
if (!::xe::cpu::audit67::vals().empty()) {
EmitAudit67ValueWatch64(f, i.address, val64, ea);
}
return 0;
}
@@ -1026,8 +885,7 @@ int InstrEmit_stwcx(PPCHIRBuilder& f, const InstrData& i) {
Value* ea = CalculateEA_0(f, i.X.RA, i.X.RB);
Value* val32 = f.Truncate(f.LoadGPR(i.X.RT), INT32_TYPE);
Value* rt = f.ByteSwap(val32);
Value* rt = f.ByteSwap(f.Truncate(f.LoadGPR(i.X.RT), INT32_TYPE));
if (cvars::no_reserved_ops) {
f.Store(ea, rt);
@@ -1046,9 +904,7 @@ int InstrEmit_stwcx(PPCHIRBuilder& f, const InstrData& i) {
if (!cvars::no_reserved_ops) {
f.MemoryBarrier();
}
if (!::xe::cpu::audit67::vals().empty()) {
EmitAudit67ValueWatch(f, i.address, val32, ea);
}
return 0;
}
// Floating-point load (A-19)

View File

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

View File

@@ -49,40 +49,6 @@ DEFINE_bool(
DECLARE_bool(allow_plugins);
DECLARE_bool(disable_context_promotion);
// AUDIT-068 Session 2: helper that scans a raw byte buffer for 4-byte aligned
// u32 values that match the configured audit_68 value list, emitting a
// per-position event. Used to pre-scan XEX-loader memcpys that bypass all
// other hooked surfaces. Cost when off: a single relaxed atomic load.
static inline void audit68_prescan_memcpy(uint32_t guest_va_dest,
const uint8_t* src, size_t size,
const char* tag) {
uint32_t active = xe::audit_68::g_active.load(std::memory_order_relaxed);
if (active == 0) return;
if ((active & 0x1) && size >= 4) {
size_t aligned_end = size & ~size_t(3);
for (size_t i = 0; i < aligned_end; i += 4) {
uint32_t be_u32 = (uint32_t(src[i + 0]) << 24) |
(uint32_t(src[i + 1]) << 16) |
(uint32_t(src[i + 2]) << 8) | uint32_t(src[i + 3]);
xe::audit_68::check_guest_va(
static_cast<uint32_t>(guest_va_dest + i), be_u32, 4, tag);
}
}
if (active & 0x2) {
// Coarse addr-only event over the full span (dest only).
uint64_t v = 0;
if (size >= 4) {
v = (uint64_t(src[0]) << 24) | (uint64_t(src[1]) << 16) |
(uint64_t(src[2]) << 8) | uint64_t(src[3]);
}
xe::audit_68::check_guest_va(guest_va_dest, v,
static_cast<uint8_t>(std::min<size_t>(size, 8)),
tag);
}
}
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};
@@ -456,10 +422,6 @@ int XexModule::ApplyPatch(XexModule* module) {
// If image_source_offset is set, copy [source_offset:source_size] to
// target_offset
if (patch_header->delta_image_source_offset) {
audit68_prescan_memcpy(
module->base_address_ + patch_header->delta_image_target_offset,
base_exe + patch_header->delta_image_source_offset,
patch_header->delta_image_source_size, "xex_memcpy_patch");
memcpy(base_exe + patch_header->delta_image_target_offset,
base_exe + patch_header->delta_image_source_offset,
patch_header->delta_image_source_size);
@@ -625,8 +587,6 @@ int XexModule::ReadImageUncompressed(const void* xex_addr, size_t xex_length) {
if (exe_length > uncompressed_size) {
return 1;
}
audit68_prescan_memcpy(base_address_, p, exe_length,
"xex_memcpy_uncompressed");
memcpy(buffer, p, exe_length);
return 0;
case XEX_ENCRYPTION_NORMAL:
@@ -703,9 +663,6 @@ int XexModule::ReadImageBasicCompressed(const void* xex_addr,
// Overflow.
return 1;
}
audit68_prescan_memcpy(
base_address_ + static_cast<uint32_t>(d - buffer), p, data_size,
"xex_memcpy_basic_block");
memcpy(d, p, data_size);
break;
case XEX_ENCRYPTION_NORMAL: {
@@ -840,17 +797,6 @@ int XexModule::ReadImageCompressed(const void* xex_addr, size_t xex_length) {
result_code = lzx_decompress(
compress_buffer, d - compress_buffer, buffer, uncompressed_size,
compression_info->normal.window_size, nullptr, 0);
// AUDIT-068 Session 2: lzx_decompress writes directly into guest
// memory via the host pointer `buffer`. There's no host-side hook
// covering its internal bulk writes, so post-scan the produced bytes
// to recover what the XEX loader actually placed at `base_address_`.
// This is THE most likely catch for the vtable install case (vtables
// live in the .rdata section that is part of the LZX-compressed image).
if (result_code == 0) {
audit68_prescan_memcpy(base_address_, buffer, uncompressed_size,
"xex_lzx_decompress_output");
}
} else {
XELOGE("Unable to allocate XEX memory at {:08X}-{:08X}.", base_address_,
uncompressed_size);
@@ -1406,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;
}
@@ -1429,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;
};

View File

@@ -217,13 +217,6 @@ X_STATUS Emulator::Setup(
// logical processors.
xe::threading::EnableAffinityConfiguration();
if (cvars::audit_demo_setup_trace) {
XELOGI("AUDIT-DEMO-SETUP-BEGIN pid={:#x} guest_tick_hz={} time_scalar={}",
reinterpret_cast<uintptr_t>(this),
Clock::guest_tick_frequency(),
Clock::guest_time_scalar());
}
XELOGI("{}: Initializing Memory...", __func__);
// Create memory system first, as it is required for other systems.
memory_ = std::make_unique<Memory>();
@@ -285,10 +278,6 @@ X_STATUS Emulator::Setup(
XELOGE("{}: Cannot initalize graphics_system!", __func__);
return X_STATUS_NOT_IMPLEMENTED;
}
if (cvars::audit_demo_setup_trace) {
XELOGI("AUDIT-DEMO-SETUP-GRAPHICS-OK graphics_system={:p}",
static_cast<const void*>(graphics_system_.get()));
}
XELOGI("{}: Initializing HID...", __func__);
// Initialize the HID.
@@ -1456,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
@@ -1718,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;
}

View File

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

View File

@@ -28,8 +28,6 @@ class HardwarePortal final : public Portal {
HardwarePortal();
~HardwarePortal() override;
virtual bool IsConnected() override;
virtual void OnDeviceArrival() override;
virtual void OnDeviceRemoval() override;

View File

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

View File

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

View File

@@ -1,193 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* AUDIT-069 event-signal watch — implementation (xenia-kernel).
*
* Mirrors AUDIT-068's lazy-parse + UINT32_MAX sentinel pattern. The hot path
* (inline in the header) reads g_active relaxed and bails out cheaply when
* inactive; the slowpath here parses the cvars on first fire and then logs
* matches.
******************************************************************************
*/
#include "xenia/kernel/audit_69_event_signal_watch.h"
#include <algorithm>
#include <atomic>
#include <chrono>
#include <mutex>
#include <string>
#include <vector>
#include "xenia/base/cvar.h"
#include "xenia/base/logging.h"
#include "xenia/kernel/xthread.h"
DECLARE_string(audit_69_event_signal_watch);
DECLARE_string(audit_69_event_signal_native_ptr);
DECLARE_bool(audit_69_log_all_sets);
namespace xe {
namespace kernel {
namespace audit_69 {
std::atomic<uint32_t> g_active{0xFFFFFFFFu};
namespace {
constexpr size_t kMaxEntries = 4;
std::vector<uint32_t> g_handles;
std::vector<uint32_t> g_native_ptrs;
std::once_flag g_parsed_flag;
std::chrono::steady_clock::time_point g_t0;
std::once_flag g_t0_once;
int64_t host_ns_since_start() {
std::call_once(g_t0_once,
[]() { g_t0 = std::chrono::steady_clock::now(); });
return std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::steady_clock::now() - g_t0)
.count();
}
void trim(std::string& s) {
while (!s.empty() && (s.front() == ' ' || s.front() == '\t')) {
s.erase(s.begin());
}
while (!s.empty() && (s.back() == ' ' || s.back() == '\t')) {
s.pop_back();
}
}
bool parse_u32(const std::string& tok, uint32_t* out) {
if (tok.empty()) return false;
const char* p = tok.c_str();
int base = 10;
if (tok.size() >= 2 && (tok[0] == '0') && (tok[1] == 'x' || tok[1] == 'X')) {
p += 2;
base = 16;
}
if (*p == 0) return false;
uint64_t v = 0;
while (*p) {
int d;
if (*p >= '0' && *p <= '9')
d = *p - '0';
else if (base == 16 && *p >= 'a' && *p <= 'f')
d = 10 + (*p - 'a');
else if (base == 16 && *p >= 'A' && *p <= 'F')
d = 10 + (*p - 'A');
else
return false;
v = v * base + d;
if (v > 0xFFFFFFFFu) return false;
++p;
}
*out = static_cast<uint32_t>(v);
return true;
}
void parse_csv_to_u32_vec(const std::string& csv, std::vector<uint32_t>* out) {
out->clear();
std::string cur;
auto flush = [&]() {
trim(cur);
if (!cur.empty()) {
uint32_t v = 0;
if (parse_u32(cur, &v)) {
if (out->size() < kMaxEntries) {
out->push_back(v);
}
}
}
cur.clear();
};
for (char c : csv) {
if (c == ',') {
flush();
} else {
cur.push_back(c);
}
}
flush();
}
void parse_locked() {
parse_csv_to_u32_vec(cvars::audit_69_event_signal_watch, &g_handles);
parse_csv_to_u32_vec(cvars::audit_69_event_signal_native_ptr,
&g_native_ptrs);
uint32_t active = 0;
if (!g_handles.empty()) active |= 1u;
if (!g_native_ptrs.empty()) active |= 2u;
if (cvars::audit_69_log_all_sets) active |= 4u;
g_active.store(active, std::memory_order_relaxed);
if (active != 0) {
XELOGI(
"AUDIT-069-INIT handles_n={} native_ptrs_n={} (active=0x{:X}, "
"host_ns={})",
g_handles.size(), g_native_ptrs.size(), active,
host_ns_since_start());
for (size_t i = 0; i < g_handles.size(); ++i) {
XELOGI("AUDIT-069-INIT-HANDLE[{}]=0x{:08X}", i, g_handles[i]);
}
for (size_t i = 0; i < g_native_ptrs.size(); ++i) {
XELOGI("AUDIT-069-INIT-NATIVE[{}]=0x{:08X}", i, g_native_ptrs[i]);
}
}
}
bool matches(uint32_t host_handle, uint32_t native_ptr) {
if (cvars::audit_69_log_all_sets) {
return true;
}
if (host_handle != 0 &&
std::find(g_handles.begin(), g_handles.end(), host_handle) !=
g_handles.end()) {
return true;
}
if (native_ptr != 0 &&
std::find(g_native_ptrs.begin(), g_native_ptrs.end(), native_ptr) !=
g_native_ptrs.end()) {
return true;
}
return false;
}
} // namespace
void check_event_set_slowpath(uint32_t host_handle, uint32_t native_ptr,
const char* fn_tag) {
std::call_once(g_parsed_flag, parse_locked);
uint32_t active = g_active.load(std::memory_order_relaxed);
if (active == 0) return;
if (!matches(host_handle, native_ptr)) return;
// Capture caller LR + tid via the current XThread (best-effort).
// Note: PPCContext has no PC field — only LR is meaningful here. The
// log emits `pc=lr` for grep-compatibility with AUDIT-061 lines, but the
// actual caller PC == lr-of-current-call, i.e. the return address of
// whichever kernel export wrapper invoked the setter.
uint32_t lr = 0;
uint32_t tid = 0;
if (auto* thr = XThread::TryGetCurrentThread()) {
auto* ctx = thr->thread_state() ? thr->thread_state()->context() : nullptr;
if (ctx) {
lr = static_cast<uint32_t>(ctx->lr);
}
tid = thr->thread_id();
}
XELOGI(
"AUDIT-069-SIGNAL fn={} target_handle=0x{:08X} native_ptr=0x{:08X} "
"lr=0x{:08X} tid={} host_ns={}",
fn_tag, host_handle, native_ptr, lr, tid, host_ns_since_start());
}
} // namespace audit_69
} // namespace kernel
} // namespace xe

View File

@@ -1,51 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* AUDIT-069: event-signal watch — guest-handle/native-pointer matched logger
* for XEvent::Set / Pulse and the Ke/Nt event setter entry points.
*
* Mirrors the AUDIT-068 hot-path discipline:
* - UINT32_MAX sentinel in g_active means "cvars not yet parsed".
* - First slowpath call drives a std::once parse of the configured cvars;
* g_active is replaced with the active-bitmask (0 if both empty).
* - Hot path = single relaxed atomic load + predictable branch.
*
* Slowpath is implemented in audit_69_event_signal_watch.cc.
******************************************************************************
*/
#ifndef XENIA_KERNEL_AUDIT_69_EVENT_SIGNAL_WATCH_H_
#define XENIA_KERNEL_AUDIT_69_EVENT_SIGNAL_WATCH_H_
#include <atomic>
#include <cstdint>
namespace xe {
namespace kernel {
namespace audit_69 {
// 0 = inactive (default observed after first parse). Non-zero = at least one
// watch is configured. UINT32_MAX = sentinel for "not yet parsed".
extern std::atomic<uint32_t> g_active;
// Slowpath. host_handle = caller-known guest event handle (0 if unknown);
// native_ptr = guest VA of the X_KEVENT (0 if unknown); fn_tag = static
// string literal (e.g. "XEvent::Set"). LR is captured from the JIT-saved
// link register via xe::kernel::XThread::GetCurrentThread().
void check_event_set_slowpath(uint32_t host_handle, uint32_t native_ptr,
const char* fn_tag);
// Hot-path wrapper.
inline void check_event_set(uint32_t host_handle, uint32_t native_ptr,
const char* fn_tag) {
if (g_active.load(std::memory_order_relaxed) != 0) [[unlikely]] {
check_event_set_slowpath(host_handle, native_ptr, fn_tag);
}
}
} // namespace audit_69
} // namespace kernel
} // namespace xe
#endif // XENIA_KERNEL_AUDIT_69_EVENT_SIGNAL_WATCH_H_

View File

@@ -1,183 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* AUDIT-070 semaphore-release watch — implementation (xenia-kernel).
*
* Mirrors AUDIT-069's lazy-parse + UINT32_MAX sentinel pattern. The hot path
* (inline in the header) reads g_active relaxed and bails out cheaply when
* inactive; the slowpath here parses the cvars on first fire and then logs
* matches.
******************************************************************************
*/
#include "xenia/kernel/audit_70_semaphore_release_watch.h"
#include <algorithm>
#include <atomic>
#include <chrono>
#include <mutex>
#include <string>
#include <vector>
#include "xenia/base/cvar.h"
#include "xenia/base/logging.h"
#include "xenia/kernel/xthread.h"
DECLARE_string(audit_70_semaphore_release_watch);
DECLARE_bool(audit_70_log_all_releases);
namespace xe {
namespace kernel {
namespace audit_70 {
std::atomic<uint32_t> g_active{0xFFFFFFFFu};
namespace {
constexpr size_t kMaxEntries = 4;
std::vector<uint32_t> g_handles;
std::once_flag g_parsed_flag;
std::chrono::steady_clock::time_point g_t0;
std::once_flag g_t0_once;
int64_t host_ns_since_start() {
std::call_once(g_t0_once,
[]() { g_t0 = std::chrono::steady_clock::now(); });
return std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::steady_clock::now() - g_t0)
.count();
}
void trim(std::string& s) {
while (!s.empty() && (s.front() == ' ' || s.front() == '\t')) {
s.erase(s.begin());
}
while (!s.empty() && (s.back() == ' ' || s.back() == '\t')) {
s.pop_back();
}
}
bool parse_u32(const std::string& tok, uint32_t* out) {
if (tok.empty()) return false;
const char* p = tok.c_str();
int base = 10;
if (tok.size() >= 2 && (tok[0] == '0') && (tok[1] == 'x' || tok[1] == 'X')) {
p += 2;
base = 16;
}
if (*p == 0) return false;
uint64_t v = 0;
while (*p) {
int d;
if (*p >= '0' && *p <= '9')
d = *p - '0';
else if (base == 16 && *p >= 'a' && *p <= 'f')
d = 10 + (*p - 'a');
else if (base == 16 && *p >= 'A' && *p <= 'F')
d = 10 + (*p - 'A');
else
return false;
v = v * base + d;
if (v > 0xFFFFFFFFu) return false;
++p;
}
*out = static_cast<uint32_t>(v);
return true;
}
void parse_csv_to_u32_vec(const std::string& csv, std::vector<uint32_t>* out) {
out->clear();
std::string cur;
auto flush = [&]() {
trim(cur);
if (!cur.empty()) {
uint32_t v = 0;
if (parse_u32(cur, &v)) {
if (out->size() < kMaxEntries) {
out->push_back(v);
}
}
}
cur.clear();
};
for (char c : csv) {
if (c == ',') {
flush();
} else {
cur.push_back(c);
}
}
flush();
}
void parse_locked() {
parse_csv_to_u32_vec(cvars::audit_70_semaphore_release_watch, &g_handles);
uint32_t active = 0;
if (!g_handles.empty()) active |= 1u;
if (cvars::audit_70_log_all_releases) active |= 2u;
g_active.store(active, std::memory_order_relaxed);
if (active != 0) {
XELOGI(
"AUDIT-070-INIT handles_n={} log_all={} (active=0x{:X}, host_ns={})",
g_handles.size(), cvars::audit_70_log_all_releases ? 1 : 0, active,
host_ns_since_start());
for (size_t i = 0; i < g_handles.size(); ++i) {
XELOGI("AUDIT-070-INIT-HANDLE[{}]=0x{:08X}", i, g_handles[i]);
}
}
}
bool matches(uint32_t host_handle) {
if (cvars::audit_70_log_all_releases) {
return true;
}
if (host_handle != 0 &&
std::find(g_handles.begin(), g_handles.end(), host_handle) !=
g_handles.end()) {
return true;
}
return false;
}
} // namespace
void check_release_slowpath(uint32_t host_handle, const char* fn_tag,
int32_t release_count, int32_t previous_count) {
std::call_once(g_parsed_flag, parse_locked);
uint32_t active = g_active.load(std::memory_order_relaxed);
if (active == 0) return;
if (!matches(host_handle)) return;
// Capture caller LR + tid via the current XThread (best-effort).
uint32_t lr = 0;
uint32_t tid = 0;
if (auto* thr = XThread::TryGetCurrentThread()) {
auto* ctx = thr->thread_state() ? thr->thread_state()->context() : nullptr;
if (ctx) {
lr = static_cast<uint32_t>(ctx->lr);
}
tid = thr->thread_id();
}
// new_count = previous + release_count (capped at INT32_MAX). Both engines
// log it pre-emptively for direct comparison; if release would exceed limit
// the slowpath caller's caller logs the limit-exceeded path separately
// (see NtReleaseSemaphore_entry's success-check).
int64_t new_count =
static_cast<int64_t>(previous_count) + static_cast<int64_t>(release_count);
XELOGI(
"AUDIT-070-RELEASE fn={} handle=0x{:08X} count={} prev_count={} "
"new_count={} lr=0x{:08X} tid={} host_ns={}",
fn_tag, host_handle, release_count, previous_count, new_count, lr, tid,
host_ns_since_start());
}
} // namespace audit_70
} // namespace kernel
} // namespace xe

View File

@@ -1,52 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* AUDIT-070 (S5 of AUDIT-069 family): semaphore-release watch — guest-handle
* matched logger for NtReleaseSemaphore_entry and xeKeReleaseSemaphore.
*
* Mirrors the AUDIT-068/069 hot-path discipline:
* - UINT32_MAX sentinel in g_active means "cvars not yet parsed".
* - First slowpath call drives a std::once parse of the configured cvars;
* g_active is replaced with the active-bitmask (0 if both empty).
* - Hot path = single relaxed atomic load + predictable branch.
*
* Slowpath is implemented in audit_70_semaphore_release_watch.cc.
******************************************************************************
*/
#ifndef XENIA_KERNEL_AUDIT_70_SEMAPHORE_RELEASE_WATCH_H_
#define XENIA_KERNEL_AUDIT_70_SEMAPHORE_RELEASE_WATCH_H_
#include <atomic>
#include <cstdint>
namespace xe {
namespace kernel {
namespace audit_70 {
// 0 = inactive (default observed after first parse). Non-zero = at least one
// watch is configured. UINT32_MAX = sentinel for "not yet parsed".
extern std::atomic<uint32_t> g_active;
// Slowpath. host_handle = guest semaphore handle (0 if unknown);
// fn_tag = static string literal (e.g. "NtReleaseSemaphore");
// release_count = the adjustment requested;
// previous_count = the value returned by the underlying Release().
// LR + tid are captured from the current XThread.
void check_release_slowpath(uint32_t host_handle, const char* fn_tag,
int32_t release_count, int32_t previous_count);
// Hot-path wrapper.
inline void check_release(uint32_t host_handle, const char* fn_tag,
int32_t release_count, int32_t previous_count) {
if (g_active.load(std::memory_order_relaxed) != 0) [[unlikely]] {
check_release_slowpath(host_handle, fn_tag, release_count, previous_count);
}
}
} // namespace audit_70
} // namespace kernel
} // namespace xe
#endif // XENIA_KERNEL_AUDIT_70_SEMAPHORE_RELEASE_WATCH_H_

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,922 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Phase B initial-state snapshot. See phase_b_snapshot.h.
******************************************************************************
*/
#include "xenia/kernel/phase_b_snapshot.h"
#include <algorithm>
#include <atomic>
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <map>
#include <string>
#include <vector>
#include "third_party/crypto/sha256.h"
#include "third_party/fmt/include/fmt/format.h"
#include "xenia/base/cvar.h"
#include "xenia/cpu/cpu_flags.h"
#include "xenia/cpu/ppc/ppc_context.h"
#include "xenia/cpu/thread_state.h"
#include "xenia/kernel/kernel_state.h"
#include "xenia/kernel/user_module.h"
#include "xenia/kernel/util/object_table.h"
#include "xenia/kernel/xobject.h"
#include "xenia/kernel/xthread.h"
#include "xenia/memory.h"
#include "xenia/vfs/device.h"
#include "xenia/vfs/entry.h"
#include "xenia/vfs/virtual_file_system.h"
namespace xe {
namespace kernel {
namespace phase_b {
namespace {
constexpr uint32_t kSchemaVersion = 1;
constexpr const char* kEngineName = "canary";
// One-shot guard. CAS-claim to ensure only the entry thread fires the
// snapshot; release on guard-fail so a non-entry thread reaching its
// first instruction first does not steal the shot.
std::atomic<bool> g_claimed{false};
std::atomic<bool> g_done{false};
// ---------- string helpers ----------
std::string JsonEscape(const std::string& s) {
std::string out;
out.reserve(s.size() + 2);
for (unsigned char c : s) {
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;
}
std::string Hex32(uint32_t v) { return fmt::format("\"0x{:08x}\"", v); }
std::string Hex64(uint64_t v) { return fmt::format("\"0x{:016x}\"", v); }
std::string Sha256Hex(const uint8_t* data, size_t len) {
::sha256::SHA256 h;
h.add(data, len);
return h.getHash();
}
// Stream-style writer that produces newline-indented JSON with sorted keys.
// We build a small tree first then serialize, so ordering is deterministic
// independent of any std::unordered_map iteration order.
class JsonNode {
public:
enum class Kind { Null, Bool, Int, UInt, IntStr, Str, Array, Object, Raw };
JsonNode() : kind_(Kind::Null) {}
static JsonNode Null() { JsonNode n; n.kind_ = Kind::Null; return n; }
static JsonNode Boolean(bool b) {
JsonNode n;
n.kind_ = Kind::Bool;
n.bool_ = b;
return n;
}
static JsonNode Integer(int64_t i) {
JsonNode n;
n.kind_ = Kind::Int;
n.int_ = i;
return n;
}
static JsonNode Unsigned(uint64_t u) {
JsonNode n;
n.kind_ = Kind::UInt;
n.uint_ = u;
return n;
}
// Pre-formatted JSON literal (e.g. `"0x..."`, raw object/array source).
static JsonNode Raw(std::string s) {
JsonNode n;
n.kind_ = Kind::Raw;
n.str_ = std::move(s);
return n;
}
static JsonNode String(std::string s) {
JsonNode n;
n.kind_ = Kind::Str;
n.str_ = std::move(s);
return n;
}
static JsonNode Array(std::vector<JsonNode> v) {
JsonNode n;
n.kind_ = Kind::Array;
n.array_ = std::move(v);
return n;
}
static JsonNode Object() {
JsonNode n;
n.kind_ = Kind::Object;
return n;
}
// Object that preserves insertion order (used at the top level of files,
// where the user-facing key ordering is canonical).
static JsonNode OrderedObject() {
JsonNode n;
n.kind_ = Kind::Object;
n.ordered_ = true;
return n;
}
void Set(const std::string& key, JsonNode v) {
obj_[key] = std::move(v);
if (ordered_) ordered_keys_.push_back(key);
}
void Serialize(std::string& out, int indent = 0) const {
auto pad = [&](int n) {
out.append(static_cast<size_t>(n * 2), ' ');
};
switch (kind_) {
case Kind::Null:
out += "null";
break;
case Kind::Bool:
out += bool_ ? "true" : "false";
break;
case Kind::Int:
out += std::to_string(int_);
break;
case Kind::UInt:
out += std::to_string(uint_);
break;
case Kind::Raw:
out += str_;
break;
case Kind::Str:
out.push_back('"');
out += JsonEscape(str_);
out.push_back('"');
break;
case Kind::Array: {
if (array_.empty()) {
out += "[]";
break;
}
out += "[\n";
for (size_t i = 0; i < array_.size(); ++i) {
pad(indent + 1);
array_[i].Serialize(out, indent + 1);
if (i + 1 < array_.size()) out += ",";
out += "\n";
}
pad(indent);
out += "]";
break;
}
case Kind::Object: {
if (obj_.empty()) {
out += "{}";
break;
}
out += "{\n";
std::vector<std::string> keys;
if (ordered_) {
keys = ordered_keys_;
} else {
keys.reserve(obj_.size());
for (const auto& [k, _] : obj_) keys.push_back(k);
std::sort(keys.begin(), keys.end());
}
for (size_t i = 0; i < keys.size(); ++i) {
pad(indent + 1);
out.push_back('"');
out += JsonEscape(keys[i]);
out += "\": ";
obj_.at(keys[i]).Serialize(out, indent + 1);
if (i + 1 < keys.size()) out += ",";
out += "\n";
}
pad(indent);
out += "}";
break;
}
}
}
private:
Kind kind_;
bool bool_ = false;
int64_t int_ = 0;
uint64_t uint_ = 0;
std::string str_;
std::vector<JsonNode> array_;
std::map<std::string, JsonNode> obj_;
bool ordered_ = false;
std::vector<std::string> ordered_keys_;
};
// Sync-then-fclose helper. Returns SHA-256 of the file's bytes.
std::string WriteFileAndHash(const std::filesystem::path& path,
const std::string& content) {
std::FILE* f = std::fopen(path.string().c_str(), "wb");
if (!f) {
return std::string(64, '0');
}
std::fwrite(content.data(), 1, content.size(), f);
std::fflush(f);
#if defined(_MSC_VER)
// Best effort on Windows — _commit takes a file descriptor.
// fmt:omit on cross-build to avoid Win32-only headers in this TU.
#else
// Unix-style fsync would go here; skipped to keep deps minimal in this TU.
#endif
std::fclose(f);
return Sha256Hex(reinterpret_cast<const uint8_t*>(content.data()),
content.size());
}
// ---------- cpu_state.json ----------
JsonNode BuildCpuState(XThread* xthread, cpu::ThreadState* thread_state,
uint32_t entry_pc) {
auto* ctx = thread_state->context();
auto root = JsonNode::OrderedObject();
root.Set("schema_version", JsonNode::Unsigned(kSchemaVersion));
root.Set("engine", JsonNode::String(kEngineName));
// Canary's PPCContext doesn't track PC explicitly — the JIT dispatch
// loop owns it. At the snapshot point, the about-to-execute PC equals
// the `entry_pc` arg passed to FireIfEntryThread.
root.Set("pc", JsonNode::Raw(Hex32(entry_pc)));
root.Set("lr", JsonNode::Raw(Hex64(ctx->lr)));
root.Set("ctr", JsonNode::Raw(Hex64(ctx->ctr)));
root.Set("msr", JsonNode::Raw(Hex64(ctx->msr)));
root.Set("vrsave", JsonNode::Raw(Hex32(ctx->vrsave)));
root.Set("fpscr", JsonNode::Raw(Hex32(ctx->fpscr.value)));
auto xer = JsonNode::Object();
xer.Set("ca", JsonNode::Unsigned(ctx->xer_ca));
xer.Set("ov", JsonNode::Unsigned(ctx->xer_ov));
xer.Set("so", JsonNode::Unsigned(ctx->xer_so));
// tbc is not modelled per-field in canary's PPCContext; emit 0.
xer.Set("tbc", JsonNode::Unsigned(0));
root.Set("xer", std::move(xer));
// CR as 8 nibbles 0xN. Diff tool compares array positionally.
std::vector<JsonNode> cr_arr;
cr_arr.reserve(8);
uint64_t cr = ctx->cr();
for (int i = 0; i < 8; ++i) {
uint32_t nibble = (cr >> (28 - i * 4)) & 0xF;
cr_arr.push_back(JsonNode::Raw(fmt::format("\"0x{:x}\"", nibble)));
}
root.Set("cr", JsonNode::Array(std::move(cr_arr)));
std::vector<JsonNode> gpr;
gpr.reserve(32);
for (int i = 0; i < 32; ++i) {
gpr.push_back(JsonNode::Raw(Hex64(ctx->r[i])));
}
root.Set("gpr", JsonNode::Array(std::move(gpr)));
std::vector<JsonNode> fpr;
fpr.reserve(32);
for (int i = 0; i < 32; ++i) {
uint64_t bits = 0;
std::memcpy(&bits, &ctx->f[i], sizeof(bits));
fpr.push_back(JsonNode::Raw(Hex64(bits)));
}
root.Set("fpr", JsonNode::Array(std::move(fpr)));
// Emit 32 hex chars of the raw 16 bytes (byte 0 first). Ours uses
// big-endian-stored bytes; canary's union exposes u8[16] in the same
// host order. Emitting bytes[0]..bytes[15] keeps both engines' VR
// serializations directly comparable.
std::vector<JsonNode> vr;
vr.reserve(128);
for (int i = 0; i < 128; ++i) {
std::string s;
s.reserve(32);
for (int j = 0; j < 16; ++j) {
s += fmt::format("{:02x}", ctx->v[i].u8[j]);
}
vr.push_back(JsonNode::String(std::move(s)));
}
root.Set("vr", JsonNode::Array(std::move(vr)));
std::string vscr_s;
vscr_s.reserve(32);
for (int j = 0; j < 16; ++j) {
vscr_s += fmt::format("{:02x}", ctx->vscr_vec.u8[j]);
}
root.Set("vscr", JsonNode::String(std::move(vscr_s)));
root.Set("thread_id", JsonNode::Unsigned(xthread ? xthread->thread_id() : 0));
root.Set("hw_id", JsonNode::Unsigned(0));
root.Set("stack_base",
JsonNode::Raw(Hex32(xthread ? xthread->stack_base() : 0)));
root.Set("stack_limit",
JsonNode::Raw(Hex32(xthread ? xthread->stack_limit() : 0)));
root.Set("tls_base",
JsonNode::Raw(Hex32(xthread ? xthread->tls_ptr() : 0)));
root.Set("pcr_base",
JsonNode::Raw(Hex32(xthread ? xthread->pcr_ptr() : 0)));
std::vector<JsonNode> det_skip;
det_skip.push_back(JsonNode::String("hw_id"));
root.Set("deterministic_skip", JsonNode::Array(std::move(det_skip)));
return root;
}
// ---------- memory.json ----------
struct CommittedRegion {
uint32_t start;
uint32_t end;
uint32_t protect;
std::string sha256;
};
void WalkHeapRegions(Memory* memory, uint32_t heap_base_addr,
std::vector<CommittedRegion>& out_regions,
std::map<std::string, uint64_t>& out_hist) {
auto* heap = memory->LookupHeap(heap_base_addr);
if (!heap) return;
const uint32_t heap_base = heap->heap_base();
const uint32_t heap_size = heap->heap_size();
const uint32_t page_size = heap->page_size();
// Read bytes via `virtual_membase + guest_address`. This is sound for
// the four guest-virtual heaps (0x00/0x40/0x80/0x90); physical heaps
// (0xA0/0xC0/0xE0) mirror physical_membase and can include host pages
// that are reserved but not backed at boot — reading them faults.
// Phase B only walks virtual heaps; the caller filters which bases
// to probe.
uint8_t* membase = memory->virtual_membase();
uint32_t cursor = heap_base;
uint32_t end = heap_base + heap_size;
while (cursor < end) {
HeapAllocationInfo info;
if (!heap->QueryRegionInfo(cursor, &info)) break;
if (info.region_size == 0) {
cursor += page_size;
continue;
}
if (info.state == 0) {
out_hist["free"] += info.region_size / page_size;
} else if ((info.state & 0x2) != 0) { // kMemoryAllocationCommit
out_hist["committed"] += info.region_size / page_size;
// Hash region contents from virtual_membase + cursor.
std::string h = membase ? Sha256Hex(membase + cursor, info.region_size)
: std::string(64, '0');
CommittedRegion r;
r.start = cursor;
r.end = cursor + info.region_size;
r.protect = info.protect;
r.sha256 = h;
out_regions.push_back(r);
} else {
out_hist["reserved"] += info.region_size / page_size;
}
cursor += info.region_size;
}
}
JsonNode BuildMemory(KernelState* kstate, bool dump_section_content) {
Memory* memory = kstate->memory();
auto root = JsonNode::OrderedObject();
root.Set("schema_version", JsonNode::Unsigned(kSchemaVersion));
root.Set("engine", JsonNode::String(kEngineName));
root.Set("page_size", JsonNode::Unsigned(4096));
root.Set("guest_address_space_bytes",
JsonNode::Unsigned(uint64_t{0x100000000}));
// Phase B walks a FIXED set of named regions whose host backing is
// guaranteed live at entry_point time: the XEX image, the entry
// thread's stack, its PCR, its TLS block. A blanket "walk every
// committed page across all heaps" approach is unsafe because
// canary's `QueryRegionInfo` reports `state=COMMIT` for pages whose
// host mapping may still be lazy (Windows reserved-but-not-committed,
// physical heap mirrors with unmapped backing). Reading those host
// VAs faults — see Wine page-fault during initial bring-up.
//
// Named regions are sufficient for Phase B's purpose (catalog
// divergences at the snapshot point); the diff tool compares the
// ordered list, so any region present in one engine and absent in
// the other is a σ-structural divergence.
uint8_t* membase = memory->virtual_membase();
std::vector<CommittedRegion> all_regions;
std::map<std::string, uint64_t> global_hist;
auto hash_named_region = [&](uint32_t start, uint32_t size) {
if (size == 0 || !membase) return;
std::string h = Sha256Hex(membase + start, size);
CommittedRegion r;
r.start = start;
r.end = start + size;
r.protect = 0;
r.sha256 = h;
all_regions.push_back(r);
global_hist["committed"] += size / 4096;
};
// 1. XEX image.
if (auto exec_module = kstate->GetExecutableModule()) {
uint32_t image_base = exec_module->xex_module()->base_address();
uint32_t image_size = exec_module->xex_module()->image_size();
if (image_base && image_size) {
hash_named_region(image_base, image_size);
}
}
// 2. Entry thread's stack + PCR + TLS — accessed via the XThread
// that's about to execute (resolved from the snapshot helper's
// arguments by passing a small accessor).
if (auto* xthread = XThread::GetCurrentThread()) {
uint32_t stack_base = xthread->stack_base();
uint32_t stack_limit = xthread->stack_limit();
if (stack_base > stack_limit) {
hash_named_region(stack_limit, stack_base - stack_limit);
}
uint32_t pcr = xthread->pcr_ptr();
if (pcr) {
hash_named_region(pcr, 0x1000);
}
uint32_t tls = xthread->tls_ptr();
if (tls) {
hash_named_region(tls, 0x1000);
}
}
// Heap descriptors — emit the four virtual heaps' bounds. Histograms
// come from QueryRegionInfo (which is safe to call — it doesn't read
// backing pages).
const uint32_t heap_probes[] = {
0x00000000u, 0x40000000u, 0x80000000u, 0x90000000u,
};
std::vector<JsonNode> heaps_arr;
for (uint32_t base : heap_probes) {
auto* heap = memory->LookupHeap(base);
if (!heap) continue;
std::map<std::string, uint64_t> hist;
uint32_t cursor = heap->heap_base();
uint32_t hend = heap->heap_base() + heap->heap_size();
while (cursor < hend) {
HeapAllocationInfo info;
if (!heap->QueryRegionInfo(cursor, &info)) break;
if (info.region_size == 0) {
cursor += heap->page_size();
continue;
}
if (info.state == 0) {
hist["free"] += info.region_size / heap->page_size();
} else if ((info.state & 0x2) != 0) {
hist["committed"] += info.region_size / heap->page_size();
} else {
hist["reserved"] += info.region_size / heap->page_size();
}
cursor += info.region_size;
}
for (const auto& [k, v] : hist) global_hist[k] += v;
auto heap_obj = JsonNode::Object();
heap_obj.Set("name", JsonNode::String(fmt::format("v{:08x}", base)));
heap_obj.Set("base", JsonNode::Raw(Hex32(heap->heap_base())));
heap_obj.Set("size", JsonNode::Raw(Hex32(heap->heap_size())));
heap_obj.Set("page_size", JsonNode::Unsigned(heap->page_size()));
auto hist_obj = JsonNode::Object();
for (const auto& [k, v] : hist) {
hist_obj.Set(k, JsonNode::Unsigned(v));
}
heap_obj.Set("page_state_histogram", std::move(hist_obj));
heaps_arr.push_back(std::move(heap_obj));
}
root.Set("heaps", JsonNode::Array(std::move(heaps_arr)));
// Sort regions by (start, end).
std::sort(all_regions.begin(), all_regions.end(),
[](const CommittedRegion& a, const CommittedRegion& b) {
if (a.start != b.start) return a.start < b.start;
return a.end < b.end;
});
uint64_t committed_pages = 0;
std::vector<JsonNode> regions_arr;
regions_arr.reserve(all_regions.size());
for (const auto& r : all_regions) {
auto ro = JsonNode::Object();
ro.Set("start", JsonNode::Raw(Hex32(r.start)));
ro.Set("end", JsonNode::Raw(Hex32(r.end)));
ro.Set("byte_count", JsonNode::Unsigned(r.end - r.start));
ro.Set("protect", JsonNode::Unsigned(r.protect));
ro.Set("sha256", JsonNode::String(r.sha256));
ro.Set("section_kind", JsonNode::Null());
regions_arr.push_back(std::move(ro));
committed_pages += (r.end - r.start) / 4096;
}
root.Set("regions", JsonNode::Array(std::move(regions_arr)));
root.Set("committed_pages_total", JsonNode::Unsigned(committed_pages));
if (dump_section_content) {
std::vector<JsonNode> sec;
for (const auto& r : all_regions) {
auto so = JsonNode::Object();
so.Set("start", JsonNode::Raw(Hex32(r.start)));
so.Set("end", JsonNode::Raw(Hex32(r.end)));
so.Set("sha256", JsonNode::String(r.sha256));
so.Set("content_b64", JsonNode::String("")); // Stubbed.
sec.push_back(std::move(so));
}
root.Set("section_contents", JsonNode::Array(std::move(sec)));
} else {
root.Set("section_contents", JsonNode::Null());
}
std::vector<JsonNode> det_skip;
det_skip.push_back(JsonNode::String("host_base_pointer"));
root.Set("deterministic_skip", JsonNode::Array(std::move(det_skip)));
return root;
}
// ---------- kernel.json ----------
const char* TypeName(XObject::Type t) {
switch (t) {
case XObject::Type::Event: return "Event";
case XObject::Type::Mutant: return "Mutant";
case XObject::Type::Semaphore: return "Semaphore";
case XObject::Type::Thread: return "Thread";
case XObject::Type::Timer: return "Timer";
case XObject::Type::File: return "File";
case XObject::Type::IOCompletion: return "IOCompletion";
case XObject::Type::Module: return "Module";
case XObject::Type::Enumerator: return "Enumerator";
case XObject::Type::NotifyListener: return "NotifyListener";
case XObject::Type::Session: return "Session";
case XObject::Type::Socket: return "Socket";
case XObject::Type::SymbolicLink: return "SymbolicLink";
case XObject::Type::Device: return "Device";
case XObject::Type::Undefined: return "Undefined";
}
return "Undefined";
}
uint32_t TypeCode(XObject::Type t) {
switch (t) {
case XObject::Type::Event: return 0x01;
case XObject::Type::Mutant: return 0x02;
case XObject::Type::Semaphore: return 0x03;
case XObject::Type::Timer: return 0x04;
case XObject::Type::Thread: return 0x05;
case XObject::Type::File: return 0x06;
case XObject::Type::IOCompletion: return 0x07;
case XObject::Type::Module: return 0x08;
case XObject::Type::Enumerator: return 0x09;
case XObject::Type::NotifyListener: return 0x0B;
default: return 0x00;
}
}
// FNV-1a 64-bit semantic-id, matching event_log.cc::ComputeSemanticId.
// At snapshot time we don't have a meaningful create_site_pc/create_tid/
// create_idx tuple for every object (they were minted before Phase B
// instrumentation existed), so fall back to a stable identity hash over
// (object_type, primary_handle). This is consistent across runs of the
// same engine; diff tool compares semantic IDs across engines only when
// both sides also stamp the same identity inputs. For Phase B's purposes
// (initial-state snapshot), the object population is tiny (≤ 2 entries
// at entry-point time: the main thread, plus an executable module ref),
// so a simple stable hash suffices.
uint64_t StableObjectId(uint32_t type_code, uint32_t raw_handle) {
uint8_t bytes[8];
for (int i = 0; i < 4; ++i) bytes[i] = (type_code >> (i * 8)) & 0xFF;
for (int i = 0; i < 4; ++i) bytes[4 + i] = (raw_handle >> (i * 8)) & 0xFF;
uint64_t h = 0xCBF29CE484222325ULL;
for (int i = 0; i < 8; ++i) {
h ^= bytes[i];
h *= 0x100000001B3ULL;
}
return h;
}
JsonNode BuildKernel(KernelState* kstate, uint32_t entry_pc) {
auto root = JsonNode::OrderedObject();
root.Set("schema_version", JsonNode::Unsigned(kSchemaVersion));
root.Set("engine", JsonNode::String(kEngineName));
auto objects = kstate->object_table()->GetAllObjects();
// Sort by semantic id for set-equivalence.
struct OneObj {
uint64_t sid;
JsonNode node;
};
std::vector<OneObj> entries;
for (auto& o : objects) {
uint32_t tc = TypeCode(o->type());
uint32_t rh = o->handle();
uint64_t sid = StableObjectId(tc, rh);
auto n = JsonNode::Object();
n.Set("handle_semantic_id", JsonNode::String(fmt::format("{:016x}", sid)));
n.Set("raw_handle_id", JsonNode::Raw(Hex32(rh)));
n.Set("type", JsonNode::String(TypeName(o->type())));
n.Set("type_code", JsonNode::Unsigned(tc));
n.Set("name", o->name().empty() ? JsonNode::Null()
: JsonNode::String(o->name()));
auto details = JsonNode::Object();
if (o->type() == XObject::Type::Thread) {
auto* th = reinterpret_cast<XThread*>(o.get());
details.Set("thread_id", JsonNode::Unsigned(th->thread_id()));
details.Set("is_entry_thread",
JsonNode::Boolean(
th->main_thread() ||
(th->creation_params() &&
th->creation_params()->start_address == entry_pc)));
details.Set("priority", JsonNode::Integer(th->priority()));
details.Set(
"stack_size",
JsonNode::Unsigned(th->creation_params()
? th->creation_params()->stack_size
: 0));
details.Set("entry_pc",
JsonNode::Raw(Hex32(th->creation_params()
? th->creation_params()->start_address
: 0)));
details.Set("ctx_ptr",
JsonNode::Raw(Hex32(th->creation_params()
? th->creation_params()->start_context
: 0)));
details.Set("suspended", JsonNode::Boolean(false));
}
n.Set("details", std::move(details));
entries.push_back({sid, std::move(n)});
}
std::sort(entries.begin(), entries.end(),
[](const OneObj& a, const OneObj& b) { return a.sid < b.sid; });
std::vector<JsonNode> obj_arr;
obj_arr.reserve(entries.size());
for (auto& e : entries) obj_arr.push_back(std::move(e.node));
root.Set("objects", JsonNode::Array(std::move(obj_arr)));
// We don't enumerate handle_name_table / notification_listeners /
// exports — accessors are not public. Emit empty arrays so the diff
// tool's structural check still has the field present.
root.Set("handle_name_table", JsonNode::Array({}));
root.Set("notification_listeners", JsonNode::Array({}));
root.Set("exports_registered_count", JsonNode::Unsigned(0));
root.Set("exports_registered_sample", JsonNode::Array({}));
root.Set("exports_registered_sha256",
JsonNode::String(std::string(64, '0')));
std::vector<JsonNode> det_skip;
det_skip.push_back(JsonNode::String("raw_handle_id"));
det_skip.push_back(JsonNode::String("exports_registered_count"));
root.Set("deterministic_skip", JsonNode::Array(std::move(det_skip)));
return root;
}
// ---------- vfs.json ----------
JsonNode BuildVfs(KernelState* kstate) {
auto root = JsonNode::OrderedObject();
root.Set("schema_version", JsonNode::Unsigned(kSchemaVersion));
root.Set("engine", JsonNode::String(kEngineName));
auto* fs = kstate->file_system();
// VirtualFileSystem doesn't expose its `devices_` vector or `symlinks_`
// map publicly. To stay additive (no canary-core API surface changes),
// we probe a canonical set of paths via ResolvePath and report only
// what we can observe. Diff tool sorts mounts_observed by path.
std::vector<std::string> probe_paths = {
"\\Device\\Cdrom0",
"\\Device\\Cdrom0\\default.xex",
"\\Device\\Cdrom0\\dat",
"\\Device\\Cdrom0\\dat\\movie",
"\\Device\\Cdrom0\\dat\\movie\\opening.bik",
"game:\\default.xex",
"game:\\dat",
"cache:\\",
"cache:\\nonexistent_probe",
"\\Device\\HardDisk0\\Partition1",
};
std::sort(probe_paths.begin(), probe_paths.end());
std::vector<JsonNode> probes;
for (const auto& path : probe_paths) {
auto entry = fs->ResolvePath(path);
auto o = JsonNode::Object();
o.Set("path", JsonNode::String(path));
o.Set("resolved", JsonNode::Boolean(entry != nullptr));
if (entry) {
o.Set("is_directory",
JsonNode::Boolean((entry->attributes() & 0x10) != 0)); // FILE_ATTR_DIRECTORY
o.Set("size", JsonNode::Unsigned(entry->size()));
} else {
o.Set("is_directory", JsonNode::Null());
o.Set("size", JsonNode::Null());
}
probes.push_back(std::move(o));
}
root.Set("resolve_path_probes", JsonNode::Array(std::move(probes)));
// Mounts observed: report only what `ResolvePath` saw against the
// device prefixes we know about. The data is derived, not enumerated,
// so this is safe under future-canary device additions.
root.Set("mounted_devices_observed_count",
JsonNode::Unsigned(
(fs->ResolvePath("\\Device\\Cdrom0") != nullptr ? 1u : 0u)));
root.Set("cache_root_listing", JsonNode::Array({}));
std::vector<JsonNode> det_skip;
det_skip.push_back(JsonNode::String("host_path_realpath"));
root.Set("deterministic_skip", JsonNode::Array(std::move(det_skip)));
return root;
}
// ---------- config.json ----------
JsonNode BuildConfig(KernelState* kstate, uint32_t entry_pc) {
auto root = JsonNode::OrderedObject();
root.Set("schema_version", JsonNode::Unsigned(kSchemaVersion));
root.Set("engine", JsonNode::String(kEngineName));
root.Set("build_id", JsonNode::String("canary-phaseB"));
auto exec_module = kstate->GetExecutableModule();
uint32_t image_base = 0;
uint32_t image_size = 0;
std::string image_loaded_sha = std::string(64, '0');
std::string xex_header_sha = std::string(64, '0');
std::string iso_path_str;
if (exec_module) {
image_base = exec_module->xex_module()->base_address();
image_size = exec_module->xex_module()->image_size();
iso_path_str = exec_module->path();
uint8_t* host =
kstate->memory()->TranslateVirtual<uint8_t*>(image_base);
if (host && image_size > 0) {
image_loaded_sha = Sha256Hex(host, image_size);
}
if (exec_module->hash()) {
xex_header_sha = fmt::format("{:016x}", *exec_module->hash());
}
}
root.Set("iso_path", JsonNode::String(iso_path_str));
root.Set("xex_entry_point", JsonNode::Raw(Hex32(entry_pc)));
root.Set("xex_image_base", JsonNode::Raw(Hex32(image_base)));
root.Set("xex_image_size", JsonNode::Unsigned(image_size));
root.Set("image_loaded_sha256", JsonNode::String(image_loaded_sha));
root.Set("xex_header_sha256", JsonNode::String(xex_header_sha));
auto cvars = JsonNode::Object();
cvars.Set("phase_b_snapshot_dir",
JsonNode::String(cvars::phase_b_snapshot_dir));
cvars.Set("phase_b_snapshot_and_exit",
JsonNode::Boolean(cvars::phase_b_snapshot_and_exit));
cvars.Set("phase_b_dump_section_content",
JsonNode::Boolean(cvars::phase_b_dump_section_content));
cvars.Set("phase_a_event_log_path",
JsonNode::String(cvars::phase_a_event_log_path));
root.Set("cvars", std::move(cvars));
auto now = std::chrono::system_clock::now();
auto t = std::chrono::system_clock::to_time_t(now);
// wall_clock_iso8601 is non-deterministic; intended for human reading
// only. Diff tool skips it.
std::string wall = fmt::format("epoch:{}", static_cast<int64_t>(t));
root.Set("wall_clock_iso8601", JsonNode::String(wall));
root.Set("host_ns_at_snapshot", JsonNode::Unsigned(0));
std::vector<JsonNode> det_skip;
det_skip.push_back(JsonNode::String("host_ns_at_snapshot"));
det_skip.push_back(JsonNode::String("wall_clock_iso8601"));
det_skip.push_back(JsonNode::String("build_id"));
det_skip.push_back(JsonNode::String("iso_path"));
det_skip.push_back(JsonNode::String("cvars.phase_b_snapshot_dir"));
root.Set("deterministic_skip", JsonNode::Array(std::move(det_skip)));
return root;
}
void EmitFile(const std::filesystem::path& dir, const char* name,
const JsonNode& node, std::map<std::string, std::string>& hashes) {
std::string body;
node.Serialize(body, 0);
body.push_back('\n');
std::filesystem::path p = dir / name;
std::string h = WriteFileAndHash(p, body);
hashes[name] = h;
}
void WriteSnapshot(XThread* xthread, cpu::ThreadState* thread_state,
uint32_t entry_pc) {
auto* kstate = xthread->kernel_state();
std::filesystem::path base(cvars::phase_b_snapshot_dir);
std::filesystem::path engine_dir = base / "canary";
std::error_code ec;
std::filesystem::create_directories(engine_dir, ec);
std::map<std::string, std::string> hashes;
EmitFile(engine_dir, "cpu_state.json",
BuildCpuState(xthread, thread_state, entry_pc), hashes);
EmitFile(engine_dir, "memory.json",
BuildMemory(kstate, cvars::phase_b_dump_section_content), hashes);
EmitFile(engine_dir, "kernel.json", BuildKernel(kstate, entry_pc), hashes);
EmitFile(engine_dir, "vfs.json", BuildVfs(kstate), hashes);
EmitFile(engine_dir, "config.json", BuildConfig(kstate, entry_pc), hashes);
auto manifest = JsonNode::OrderedObject();
manifest.Set("schema_version", JsonNode::Unsigned(kSchemaVersion));
manifest.Set("engine", JsonNode::String(kEngineName));
// Files object is sorted by key (alphabetic), matching the diff tool's
// assumption.
auto files = JsonNode::Object();
for (const auto& [name, hash] : hashes) {
files.Set(name, JsonNode::String(hash));
}
manifest.Set("files", std::move(files));
std::string body;
manifest.Serialize(body, 0);
body.push_back('\n');
std::filesystem::path mp = engine_dir / "manifest.json";
std::FILE* f = std::fopen(mp.string().c_str(), "wb");
if (f) {
std::fwrite(body.data(), 1, body.size(), f);
std::fflush(f);
std::fclose(f);
}
// Phase C: when dump_section_content is on, write raw bytes of the
// XEX image region to <engine_dir>/image.bin. This is the only
// region positionally matched between canary and ours, so it's the
// only one suitable for byte-level diff.
if (cvars::phase_b_dump_section_content) {
auto exec_module = kstate->GetExecutableModule();
if (exec_module) {
uint32_t image_base = exec_module->xex_module()->base_address();
uint32_t image_size = exec_module->xex_module()->image_size();
uint8_t* host =
kstate->memory()->TranslateVirtual<uint8_t*>(image_base);
if (host && image_size > 0) {
std::filesystem::path ip = engine_dir / "image.bin";
std::FILE* bf = std::fopen(ip.string().c_str(), "wb");
if (bf) {
std::fwrite(host, 1, image_size, bf);
std::fflush(bf);
std::fclose(bf);
}
}
}
}
}
} // namespace
void FireIfEntryThread(XThread* xthread, cpu::ThreadState* thread_state,
uint32_t entry_address) {
// Fast path: cvar empty → zero overhead. The .empty() check is a
// single read of a std::string's size, no syscall.
if (cvars::phase_b_snapshot_dir.empty()) {
return;
}
if (g_done.load(std::memory_order_acquire)) {
return;
}
// Resolve the entry_point of the executable module. If it doesn't
// match this thread's first instruction, this isn't the entry thread
// — release any claim we may have made and return.
auto* kstate = xthread ? xthread->kernel_state() : nullptr;
if (!kstate) return;
auto exec_module = kstate->GetExecutableModule();
if (!exec_module) return;
uint32_t entry_pc = exec_module->entry_point();
if (entry_address != entry_pc) return;
// CAS-claim. Releases on guard-fail (above) so a non-entry thread
// reaching its first instruction before the boot thread doesn't
// steal the shot.
bool expected = false;
if (!g_claimed.compare_exchange_strong(expected, true,
std::memory_order_acq_rel)) {
return;
}
WriteSnapshot(xthread, thread_state, entry_pc);
g_done.store(true, std::memory_order_release);
if (cvars::phase_b_snapshot_and_exit) {
std::_Exit(0);
}
}
} // namespace phase_b
} // namespace kernel
} // namespace xe

View File

@@ -1,43 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Phase B initial-state snapshot. Cvar-gated (default off).
* Spec: xenia-rs/audit-runs/phase-b-state-equivalence/
******************************************************************************
*/
#ifndef XENIA_KERNEL_PHASE_B_SNAPSHOT_H_
#define XENIA_KERNEL_PHASE_B_SNAPSHOT_H_
#include <cstdint>
namespace xe {
namespace cpu {
class ThreadState;
} // namespace cpu
namespace kernel {
class XThread;
namespace phase_b {
// Called immediately before the JIT executes the first guest PPC
// instruction of a thread. Returns silently when:
// * phase_b_snapshot_dir cvar is empty (zero overhead — default off);
// * a snapshot has already been written (one-shot CAS guard);
// * `entry_address` does not match the loaded executable module's
// entry_point (this thread is not the entry thread — a worker
// spawned by an early kernel call could reach its first instruction
// before the boot thread does).
//
// On a match: writes <dir>/canary/{cpu_state,memory,kernel,vfs,config}.json
// + manifest.json, optionally `_Exit(0)` per phase_b_snapshot_and_exit.
void FireIfEntryThread(XThread* xthread, cpu::ThreadState* thread_state,
uint32_t entry_address);
} // namespace phase_b
} // namespace kernel
} // namespace xe
#endif // XENIA_KERNEL_PHASE_B_SNAPSHOT_H_

View File

@@ -11,7 +11,6 @@
#include "xenia/base/byte_stream.h"
#include "xenia/base/logging.h"
#include "xenia/kernel/event_log.h"
#include "xenia/kernel/xobject.h"
#include "xenia/kernel/xthread.h"
@@ -19,34 +18,6 @@ namespace xe {
namespace kernel {
namespace util {
namespace {
// Phase C+15-α: map XObject::Type to schema-v1 object_type code.
// Schema codes are listed in event_log.h::ObjectType. Both engines
// must agree on this mapping (see ours's `KernelObject::schema_object_type`).
uint32_t SchemaObjectType(XObject::Type type) {
using T = XObject::Type;
switch (type) {
case T::Event: return phase_a::kObjEvent;
case T::Mutant: return phase_a::kObjMutant;
case T::Semaphore: return phase_a::kObjSemaphore;
case T::Timer: return phase_a::kObjTimer;
case T::Thread: return phase_a::kObjThread;
case T::File: return phase_a::kObjFile;
case T::IOCompletion: return phase_a::kObjIoCompletion;
case T::Module: return phase_a::kObjModule;
case T::Enumerator: return phase_a::kObjEnumState;
case T::NotifyListener: return phase_a::kObjNotification;
case T::SymbolicLink: return phase_a::kObjUnknown;
case T::Session: return phase_a::kObjUnknown;
case T::Socket: return phase_a::kObjUnknown;
case T::Device: return phase_a::kObjUnknown;
case T::Undefined:
default:
return phase_a::kObjUnknown;
}
}
} // namespace
ObjectTable::ObjectTable() {}
ObjectTable::~ObjectTable() { Reset(); }
@@ -183,25 +154,6 @@ X_STATUS ObjectTable::AddHandle(XObject* object, X_HANDLE* out_handle) {
if (out_handle) {
*out_handle = handle;
}
// Phase C+15-α: schema-v1 `handle.create` event. Symmetric with
// ours's `KernelState::alloc_handle_for`. Cvar-gated default-off
// via `phase_a::IsEnabled()`. Centralized here so every object
// type (XEvent, XThread, XFile, etc.) emits a symmetric event
// when added to the object table.
//
// Phase C+18: skip the emit when called from inside
// `XObject::GetNativeObject` lazy-wrap. The shared-global
// `handle.create` is emitted explicitly by `GetNativeObject`
// with a scheduling-invariant SID via
// `phase_a::EmitHandleCreateSharedGlobal`. This avoids the
// race-prone per-thread SID for process-global dispatchers.
if (phase_a::IsEnabled() && !phase_a::IsInGetNativeObject()) {
uint32_t schema_type = SchemaObjectType(object->type());
const std::string& name = object->name();
phase_a::EmitHandleCreateAuto(/* create_site_pc */ 0, schema_type,
handle,
name.empty() ? nullptr : name.c_str());
}
}
return result;
@@ -287,13 +239,6 @@ X_STATUS ObjectTable::RemoveHandle(X_HANDLE handle) {
if (!object->name().empty()) {
RemoveNameMapping(object->name());
}
// Phase C+15-α: schema-v1 `handle.destroy` event. Symmetric with
// ours's `nt_close`. Emitted at RemoveHandle (which is reached
// when the final handle reference is released, matching ours's
// refcount==0 case).
if (phase_a::IsEnabled()) {
phase_a::EmitHandleDestroyAuto(handle, /* prior_refcount */ 1);
}
// Release now that the object has been removed from the table.
object->Release();
}
@@ -331,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;
}

View File

@@ -499,30 +499,6 @@ enum class KernelModuleId {
xbdm,
};
// Phase A bridge — see kernel/event_log.h. Inline to avoid pulling the
// header into shim_utils.h's transitive set.
namespace phase_a_bridge {
constexpr const char* KernelModuleIdName(KernelModuleId m) {
switch (m) {
case KernelModuleId::xboxkrnl: return "xboxkrnl.exe";
case KernelModuleId::xam: return "xam.xex";
case KernelModuleId::xbdm: return "xbdm.xex";
}
return "unknown";
}
bool Enabled();
void EmitImportAndCall(const char* module_name, uint16_t ord, const char* name);
void EmitReturn(const char* name, uint64_t return_value);
// Phase C+10 schema-v1 extension: when `name` is in the known
// path-bearing export set, resolve the OBJECT_ATTRIBUTES* arg from the
// PPC context and emit a `kernel.call` event whose `args_resolved`
// field includes `{"path":"..."}`. Falls back to the empty form for
// unknown names. Bridge takes a `void*` to avoid pulling the PPCContext
// definition into this header's transitive include set.
void EmitImportAndCallWithCtx(const char* module_name, uint16_t ord,
const char* name, void* ppc_context);
} // namespace phase_a_bridge
template <size_t I = 0, typename... Ps>
requires(I == sizeof...(Ps))
void AppendKernelCallParams(StringBuffer& string_buffer,
@@ -602,18 +578,9 @@ struct ExportRegistrerHelper {
cvars::log_high_frequency_kernel_calls)) {
PrintKernelCall(export_entry, params);
}
const bool phase_a_on = phase_a_bridge::Enabled();
if (phase_a_on) {
phase_a_bridge::EmitImportAndCallWithCtx(
phase_a_bridge::KernelModuleIdName(MODULE), ORDINAL,
export_entry->name, ppc_context);
}
if constexpr (std::is_void<R>::value) {
KernelTrampoline(fn, std::forward<std::tuple<Ps...>>(params),
std::make_index_sequence<sizeof...(Ps)>());
if (phase_a_on) {
phase_a_bridge::EmitReturn(export_entry->name, 0);
}
} else {
auto result =
KernelTrampoline(fn, std::forward<std::tuple<Ps...>>(params),
@@ -623,11 +590,6 @@ struct ExportRegistrerHelper {
(xe::cpu::ExportTag::kLog | xe::cpu::ExportTag::kLogResult)) {
// TODO(benvanik): log result.
}
if (phase_a_on) {
phase_a_bridge::EmitReturn(
export_entry->name,
static_cast<uint64_t>(ppc_context->r[3]));
}
}
}
};
@@ -638,28 +600,14 @@ struct ExportRegistrerHelper {
0,
};
std::tuple<Ps...> params = {Ps(init)...};
const bool phase_a_on = phase_a_bridge::Enabled();
if (phase_a_on) {
phase_a_bridge::EmitImportAndCallWithCtx(
phase_a_bridge::KernelModuleIdName(MODULE), ORDINAL,
export_entry->name, ppc_context);
}
if constexpr (std::is_void<R>::value) {
KernelTrampoline(fn, std::forward<std::tuple<Ps...>>(params),
std::make_index_sequence<sizeof...(Ps)>());
if (phase_a_on) {
phase_a_bridge::EmitReturn(export_entry->name, 0);
}
} else {
auto result =
KernelTrampoline(fn, std::forward<std::tuple<Ps...>>(params),
std::make_index_sequence<sizeof...(Ps)>());
result.Store(ppc_context);
if (phase_a_on) {
phase_a_bridge::EmitReturn(
export_entry->name,
static_cast<uint64_t>(ppc_context->r[3]));
}
}
}
};

View File

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

View File

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

View File

@@ -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_) &&

View File

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

View File

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

View File

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

View File

@@ -13,7 +13,6 @@
#include "xenia/base/atomic.h"
#include "xenia/base/pe_image.h"
#include "xenia/kernel/event_log.h"
#include "xenia/kernel/kernel_state.h"
#include "xenia/kernel/user_module.h"
#include "xenia/kernel/util/shim_utils.h"
@@ -621,13 +620,6 @@ void RtlEnterCriticalSection_entry(pointer_t<X_RTL_CRITICAL_SECTION> cs) {
}
if (xe::atomic_inc(&cs->lock_count) != 0) {
// Phase D Stage 1: signal real contention before parking the host
// thread on the embedded dispatcher event. The Stage-3 manifest
// loader keys on this event's (tid, tid_event_idx) to force
// ours's `rtl_enter_critical_section` to park at the same per-tid
// ordinal. Cvar-gated (default off) inside the helper itself, so
// pre-Stage-1 cold runs are byte-identical.
phase_a::EmitContentionObserved(cs.guest_address(), /*contended=*/true);
// Create a full waiter.
xeKeWaitForSingleObject(reinterpret_cast<void*>(cs.host_address()), 8, 0, 0,
nullptr);

View File

@@ -12,8 +12,6 @@
#include "xenia/base/clock.h"
#include "xenia/base/platform.h"
#include "xenia/cpu/processor.h"
#include "xenia/kernel/audit_70_semaphore_release_watch.h"
#include "xenia/kernel/event_log.h"
#include "xenia/kernel/util/shim_utils.h"
#include "xenia/kernel/xboxkrnl/xboxkrnl_private.h"
#include "xenia/kernel/xsemaphore.h"
@@ -149,25 +147,6 @@ uint32_t ExCreateThread(xe::be<uint32_t>* handle_ptr, uint32_t stack_size,
if (thread_id_ptr) {
*thread_id_ptr = thread->thread_id();
}
// Phase C+15-α: schema-v1 `thread.create` event. Symmetric with
// ours's `ex_create_thread`. Emitted by the **parent** thread.
// handle.create for the thread handle itself was already emitted
// via ObjectTable::AddHandle inside XThread::Create. Here we
// surface the spawn-specific metadata.
if (phase_a::IsEnabled()) {
uint64_t sid = phase_a::LookupHandleSemanticId(thread->handle());
XThread* parent = XThread::TryGetCurrentThread();
uint32_t parent_tid = 0;
if (parent) {
parent_tid = static_cast<uint32_t>(
parent->guest_object<X_KTHREAD>()->thread_id);
}
uint32_t affinity = (creation_flags >> 24) & 0xFF;
bool suspended = (creation_flags & 0x1) != 0;
phase_a::EmitThreadCreate(sid, parent_tid, start_address, start_context,
/* priority */ 0, affinity, actual_stack_size,
suspended);
}
}
return result;
}
@@ -186,9 +165,6 @@ DECLARE_XBOXKRNL_EXPORT1(ExCreateThread, kThreading, kImplemented);
uint32_t ExTerminateThread(uint32_t exit_code) {
XThread* thread = XThread::GetCurrentThread();
// Phase C+15-α: schema-v1 `thread.exit` is emitted inside
// `XThread::Exit` (covers both explicit ExTerminateThread and
// implicit thread-entry returns).
// NOTE: this kills us right now. We won't return from it.
return thread->Exit(exit_code);
@@ -355,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);
@@ -498,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;
@@ -507,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);
@@ -742,9 +712,6 @@ uint32_t xeKeReleaseSemaphore(X_KSEMAPHORE* semaphore_ptr, uint32_t increment,
int32_t previous_count = 0;
[[maybe_unused]] bool success =
sem->ReleaseSemaphore(adjustment, &previous_count);
// AUDIT-070: log Ke-form release fires whose target handle matches.
audit_70::check_release(sem->handle(), "xeKeReleaseSemaphore",
static_cast<int32_t>(adjustment), previous_count);
return static_cast<uint32_t>(previous_count);
}
@@ -807,19 +774,14 @@ 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;
}
// AUDIT-070: log Nt-form release fires whose target handle matches.
// Logged regardless of success/limit-exceeded — distinguished by
// result/previous_count in subsequent analysis.
audit_70::check_release(static_cast<uint32_t>(sem_handle),
"NtReleaseSemaphore",
static_cast<int32_t>(release_count),
previous_count);
} else {
result = X_STATUS_INVALID_HANDLE;
}
@@ -988,19 +950,6 @@ uint32_t xeKeWaitForSingleObject(void* object_ptr, uint32_t wait_reason,
return X_STATUS_ABANDONED_WAIT_0;
}
// Phase C+15-α: schema-v1 `wait.begin` event. Symmetric with ours's
// `ke_wait_for_single_object`. Resolve the SID via the object's
// first registered handle.
if (phase_a::IsEnabled()) {
uint64_t sid = 0;
if (!object->handles().empty()) {
sid = phase_a::LookupHandleSemanticId(object->handles()[0]);
}
int64_t timeout_ns = timeout_ptr ? (static_cast<int64_t>(*timeout_ptr) * 100) : -1;
phase_a::EmitWaitBegin(&sid, 1, timeout_ns, alertable != 0,
/* wait_all */ false);
}
X_STATUS result =
object->Wait(wait_reason, processor_mode, alertable, timeout_ptr);
if (alertable) {
@@ -1027,16 +976,6 @@ uint32_t NtWaitForSingleObjectEx(uint32_t object_handle, uint32_t wait_mode,
uint32_t alertable, uint64_t* timeout_ptr) {
X_STATUS result = X_STATUS_SUCCESS;
// Phase C+15-α: schema-v1 `wait.begin` event. Symmetric with ours's
// `nt_wait_for_single_object_ex`. Resolve SID directly from the
// handle.
if (phase_a::IsEnabled()) {
uint64_t sid = phase_a::LookupHandleSemanticId(object_handle);
int64_t timeout_ns = timeout_ptr ? (static_cast<int64_t>(*timeout_ptr) * 100) : -1;
phase_a::EmitWaitBegin(&sid, 1, timeout_ns, alertable != 0,
/* wait_all */ false);
}
auto object =
kernel_state()->object_table()->LookupObject<XObject>(object_handle);
if (object) {

View File

@@ -11,7 +11,6 @@
#include "xenia/base/byte_stream.h"
#include "xenia/base/logging.h"
#include "xenia/kernel/audit_69_event_signal_watch.h"
namespace xe {
namespace kernel {
@@ -59,19 +58,12 @@ void XEvent::InitializeNative(void* native_ptr, X_DISPATCH_HEADER* header) {
}
int32_t XEvent::Set(uint32_t priority_increment, bool wait) {
// AUDIT-069: log event-signal fires whose target matches the configured
// handle ID or native VA. Hot path is a single relaxed atomic load when
// the cvars are empty (default).
audit_69::check_event_set(this->handle(), this->guest_object(),
"XEvent::Set");
set_priority_increment(priority_increment);
event_->Set();
return 1;
}
int32_t XEvent::Pulse(uint32_t priority_increment, bool wait) {
audit_69::check_event_set(this->handle(), this->guest_object(),
"XEvent::Pulse");
set_priority_increment(priority_increment);
event_->Pulse();
return 1;

View File

@@ -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++) {

View File

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

View File

@@ -10,7 +10,6 @@
#include "xenia/kernel/xobject.h"
#include "xenia/base/byte_stream.h"
#include "xenia/kernel/event_log.h"
#include "xenia/kernel/kernel_state.h"
#include "xenia/kernel/util/shim_utils.h"
#include "xenia/kernel/xboxkrnl/xboxkrnl_private.h"
@@ -430,15 +429,6 @@ object_ref<XObject> XObject::GetNativeObject(KernelState* kernel_state,
// First use, create new.
// https://www.nirsoft.net/kernel_struct/vista/KOBJECTS.html
XObject* object = nullptr;
// Phase C+18: bracket the wrapper ctor + AddHandle path with a
// host-TLS flag so the centralized `ObjectTable::AddHandle` emit
// hook short-circuits its per-thread `handle.create`. We emit a
// shared-global `handle.create` explicitly below with a SID keyed
// on `(native_ptr, object_type)` — scheduling-invariant across
// engines/threads. See `event_log.h`/`event_log.cc` C+18 helpers
// and ours's `ensure_dispatcher_object`.
phase_a::SetInGetNativeObject(true);
uint32_t schema_object_type = phase_a::kObjUnknown;
switch (as_type) {
case 0: // EventNotificationObject
case 1: // EventSynchronizationObject
@@ -446,14 +436,12 @@ object_ref<XObject> XObject::GetNativeObject(KernelState* kernel_state,
auto ev = new XEvent(kernel_state);
ev->InitializeNative(native_ptr, header);
object = ev;
schema_object_type = phase_a::kObjEvent;
} break;
case 2: // MutantObject
{
auto mutant = new XMutant(kernel_state);
mutant->InitializeNative(native_ptr, header);
object = mutant;
schema_object_type = phase_a::kObjMutant;
} break;
case 5: // SemaphoreObject
{
@@ -462,7 +450,6 @@ object_ref<XObject> XObject::GetNativeObject(KernelState* kernel_state,
// Can't report failure to the guest at late initialization:
assert_true(success);
object = sem;
schema_object_type = phase_a::kObjSemaphore;
} break;
case 3: // ProcessObject
case 4: // QueueObject
@@ -481,24 +468,10 @@ object_ref<XObject> XObject::GetNativeObject(KernelState* kernel_state,
assert_always();
result = nullptr;
}
phase_a::SetInGetNativeObject(false);
// Stash pointer in struct.
// FIXME: This assumes the object contains a dispatch header (some don't!)
if (object) {
StashHandle(header, object->handle());
// Phase C+18: emit the shared-global `handle.create` here, AFTER
// StashHandle, with the deterministic SID. The `native_ptr` is
// already a guest VA (`X_DISPATCH_HEADER*`) so a host-pointer cast
// to uint32_t is meaningful in canary's memory model only when the
// value is the guest address — translate via the same memory base.
if (phase_a::IsEnabled()) {
uint32_t guest_ptr =
kernel_state->memory()->HostToGuestVirtual(native_ptr);
const std::string& name = object->name();
phase_a::EmitHandleCreateSharedGlobal(
guest_ptr, schema_object_type, object->handle(),
name.empty() ? nullptr : name.c_str());
}
}
result = object;
}

View File

@@ -21,9 +21,7 @@
#include "xenia/base/threading.h"
#include "xenia/cpu/processor.h"
#include "xenia/emulator.h"
#include "xenia/kernel/event_log.h"
#include "xenia/kernel/kernel_state.h"
#include "xenia/kernel/phase_b_snapshot.h"
#include "xenia/kernel/user_module.h"
#include "xenia/kernel/xboxkrnl/xboxkrnl_threading.h"
@@ -118,11 +116,6 @@ XThread* XThread::GetCurrentThread() {
return thread;
}
// Phase C+15-α: non-asserting variant. See xthread.h.
XThread* XThread::TryGetCurrentThread() {
return reinterpret_cast<XThread*>(current_xthread_tls_);
}
uint32_t XThread::GetCurrentThreadHandle() {
XThread* thread = XThread::GetCurrentThread();
return thread->handle();
@@ -477,14 +470,6 @@ X_STATUS XThread::Create() {
X_STATUS XThread::Exit(int exit_code) {
// This may only be called on the thread itself.
assert_true(XThread::GetCurrentThread() == this);
// Phase C+15-α: schema-v1 `thread.exit` event. Emit BEFORE Exit()
// unwinds via threading::Thread::Exit (does not return). This
// covers both `ExTerminateThread` and implicit thread-entry returns
// — XThread::Execute calls Exit() at line ~641 after entry_pc
// returns. Symmetric with ours's `ex_terminate_thread`.
if (phase_a::IsEnabled()) {
phase_a::EmitThreadExit(static_cast<uint32_t>(exit_code));
}
// TODO(chrispy): not sure if this order is correct, should it come after
// apcs?
auto kthread = guest_object<X_KTHREAD>();
@@ -590,11 +575,6 @@ void XThread::Execute() {
// On Windows, setjmp/longjmp is used because MSVC's longjmp performs SEH
// stack unwinding which already calls destructors.
uint32_t next_address;
// Phase B snapshot. No-op when phase_b_snapshot_dir cvar is empty
// (default). When set, fires once on the entry-point thread immediately
// before its first guest instruction executes. See
// xenia/kernel/phase_b_snapshot.h.
::xe::kernel::phase_b::FireIfEntryThread(this, thread_state_, address);
#if !XE_PLATFORM_WIN32
try {
exit_code = static_cast<int>(kernel_state()->processor()->Execute(

View File

@@ -383,11 +383,6 @@ class XThread : public XObject, public cpu::Thread {
static bool IsInThread(XThread* other);
static bool IsInThread();
static XThread* GetCurrentThread();
// Phase C+15-α: non-asserting accessor for hooks that may run from
// boot / host threads (Phase A event-log emit at ObjectTable::AddHandle).
// Returns nullptr instead of asserting when there is no current
// guest XThread.
static XThread* TryGetCurrentThread();
static uint32_t GetCurrentThreadHandle();
static uint32_t GetCurrentThreadId();

View File

@@ -14,7 +14,6 @@
#include "third_party/fmt/include/fmt/format.h"
#include "xenia/base/assert.h"
#include "xenia/base/audit_68_host_mem_watch_fwd.h"
#include "xenia/base/byte_stream.h"
#include "xenia/base/clock.h"
#include "xenia/base/cvar.h"
@@ -91,9 +90,6 @@ uint32_t get_page_count(uint32_t value, uint32_t page_size) {
static Memory* active_memory_ = nullptr;
// AUDIT-068 — process-global accessor (declared in memory.h).
Memory* Memory::active() { return active_memory_; }
void CrashDump() {
static std::atomic<int> in_crash_dump(0);
if (in_crash_dump.fetch_add(1)) {
@@ -155,41 +151,11 @@ Memory::Memory() {
uint32_t(xe::memory::allocation_granularity());
assert_zero(active_memory_);
active_memory_ = this;
// AUDIT-068: register host→guest translation thunk so the watch slow path
// in xenia-base can resolve guest VAs without depending on xenia-core.
xe::audit_68::g_host_to_guest_thunk = [](const void* host_ptr) -> uint32_t {
Memory* m = active_memory_;
return m ? m->HostToGuestVirtual(host_ptr) : 0u;
};
// AUDIT-068 Session 3: register guest→host translation thunk and a
// page-protect query thunk for the read-mode probe. The probe thread uses
// QueryProtect to skip unmapped/uncommitted pages before dereferencing.
xe::audit_68::g_guest_to_host_thunk = [](uint32_t va) -> const void* {
Memory* m = active_memory_;
return m ? reinterpret_cast<const void*>(m->TranslateVirtual(va))
: nullptr;
};
xe::audit_68::g_query_protect_thunk = [](uint32_t va,
uint32_t* out_protect) -> bool {
Memory* m = active_memory_;
if (!m) return false;
BaseHeap* heap = m->LookupHeap(va);
if (!heap) {
if (out_protect) *out_protect = 0;
return false;
}
return heap->QueryProtect(va, out_protect);
};
}
Memory::~Memory() {
assert_true(active_memory_ == this);
active_memory_ = nullptr;
xe::audit_68::g_host_to_guest_thunk = nullptr;
xe::audit_68::g_guest_to_host_thunk = nullptr;
xe::audit_68::g_query_protect_thunk = nullptr;
// Uninstall the MMIO handler, as we won't be able to service more
// requests.
@@ -574,71 +540,16 @@ uint32_t Memory::GetPhysicalAddress(uint32_t address) const {
}
void Memory::Zero(uint32_t address, uint32_t size) {
// AUDIT-068: log a single span event with value=0; size is capped at 8 for
// the value field. Slow path is gated on the atomic flag.
xe::audit_68::check_guest_va(address, 0,
static_cast<uint8_t>(std::min<uint32_t>(size, 8)),
"Memory::Zero");
std::memset(TranslateVirtual(address), 0, size);
}
void Memory::Fill(uint32_t address, uint32_t size, uint8_t value) {
// Replicate the fill byte across the value field so value_matches can
// recognise e.g. 0xDEADBEEF only if the byte is 0xDE/0xAD/0xBE/0xEF — for
// capture purposes the byte itself in the low slot is enough.
uint64_t v = static_cast<uint64_t>(value);
v |= v << 8;
v |= v << 16;
v |= v << 32;
xe::audit_68::check_guest_va(address, v,
static_cast<uint8_t>(std::min<uint32_t>(size, 8)),
"Memory::Fill");
std::memset(TranslateVirtual(address), value, size);
}
void Memory::Copy(uint32_t dest, uint32_t src, uint32_t size) {
uint8_t* pdest = TranslateVirtual(dest);
const uint8_t* psrc = TranslateVirtual(src);
// AUDIT-068 Session 2: full byte-scan over 4-byte aligned positions of the
// source buffer. Catches XEX-loader-style memcpys where a vptr (the target
// u32 value) is buried somewhere mid-buffer rather than at offset 0. Cost
// O(size/4 * N_values) with N_values capped at 8 inside value_matches —
// negligible vs the underlying memcpy throughput.
//
// Gated on active bit 0x1 (values-mode) AND active != 0. If only addrs are
// configured (Run 2 voice-struct mode), we still emit a single addr-only
// event covering the destination span so addr-watch isn't broken.
uint32_t active = xe::audit_68::g_active.load(std::memory_order_relaxed);
if (active != 0) [[unlikely]] {
if ((active & 0x1) && size >= 4) {
// Scan source for any configured u32 value (big-endian, mirrors how
// guest sees the bytes). 4-byte aligned offsets only.
uint32_t aligned_end = size & ~3u;
for (uint32_t i = 0; i < aligned_end; i += 4) {
uint32_t be_u32 =
(uint32_t(psrc[i + 0]) << 24) | (uint32_t(psrc[i + 1]) << 16) |
(uint32_t(psrc[i + 2]) << 8) | uint32_t(psrc[i + 3]);
xe::audit_68::check_guest_va(dest + i, be_u32, 4, "Memory::Copy");
}
}
if (active & 0x2) {
// Addr-only mode: emit a single coarse event tagged with the dest base
// and first u32 of source for context. The slow-path range check will
// log iff the dest span intersects a configured addr range.
uint64_t v = 0;
if (size >= 4) {
v = (uint64_t(psrc[0]) << 24) | (uint64_t(psrc[1]) << 16) |
(uint64_t(psrc[2]) << 8) | uint64_t(psrc[3]);
} else if (size > 0) {
for (uint32_t i = 0; i < size; ++i) {
v = (v << 8) | psrc[i];
}
}
xe::audit_68::check_guest_va(
dest, v, static_cast<uint8_t>(std::min<uint32_t>(size, 8)),
"Memory::Copy");
}
}
std::memcpy(pdest, psrc, size);
}
@@ -892,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() {
@@ -909,7 +816,6 @@ void BaseHeap::Dispose() {
page_number += page_entry.region_page_count;
}
}
free_blocks_.clear();
}
void BaseHeap::DumpMap() {
@@ -1032,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,
@@ -1133,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_;
@@ -1165,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;
@@ -1215,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];
@@ -1227,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>
@@ -1282,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");
@@ -1374,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.
@@ -1390,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;
}
@@ -1525,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;
}
@@ -1884,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;
}
@@ -1906,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;
@@ -1930,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;
}
@@ -1950,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;
}
@@ -1981,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.
@@ -2002,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;

View File

@@ -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.
@@ -347,13 +333,6 @@ class Memory {
Memory();
~Memory();
// AUDIT-068: process-global Memory singleton accessor. Returns the
// currently-constructed Memory instance, or nullptr if none. Set inside
// Memory::Memory()/~Memory(); see memory.cc `active_memory_`. Used by
// xe::audit_68::check_host_write() to translate a host pointer back to a
// guest VA without an explicit Memory* context.
static Memory* active();
// Initializes the memory system.
// This may fail if the host address space could not be reserved or the
// mapping to the file system fails.

View File

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

View File

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

View File

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

View File

@@ -332,11 +332,12 @@ else()
X86_AVX512VNNI
WITH_GZFILEOP
)
# Real cl.exe exposes all intrinsics unconditionally; GCC, Clang, AND clang-cl
# all need per-file -m<isa> flags to declare them. CMAKE_CXX_COMPILER_ID is
# "Clang" for both Linux Clang and clang-cl.
if(NOT MSVC OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
if(NOT MSVC)
# GCC/Clang have native __builtin_ctz/__builtin_ctzll (MSVC gets these
# from fallback_builtins.h). Defining these enables optimized compare256
# and longest_match codepaths.
target_compile_definitions(zlib-ng PRIVATE HAVE_BUILTIN_CTZ HAVE_BUILTIN_CTZLL)
# GCC/Clang need per-file ISA flags; MSVC handles this via runtime dispatch
set_source_files_properties(
zlib-ng/arch/x86/adler32_avx2.c
zlib-ng/arch/x86/chunkset_avx2.c

View File

@@ -72,29 +72,15 @@ def main():
is_dxc = "dxc" in os.path.basename(fxc).lower()
# Start with base command — use wine on non-Windows platforms.
# fxc.exe sees '/foo' as a switch, so unix-rooted paths must be
# translated to Windows form (Z:\...) before being passed.
if sys.platform != "win32":
def _wineify(p):
try:
out = subprocess.check_output(["winepath", "-w", p],
stderr=subprocess.DEVNULL)
return out.decode("utf-8", "replace").strip()
except (OSError, subprocess.CalledProcessError):
return p
input_path = _wineify(input_path)
output_path = _wineify(output_path)
src_dir = _wineify(src_dir)
compiler_args = ["wine", fxc]
else:
compiler_args = [fxc]
if is_dxc:
# DXC supports both SM 5.x (-> DXBC) and SM 6.x (-> DXIL). xenia's D3D12
# backend reads DXBC (output dir is bytecode/d3d12_5_1/), so target
# SM 5_1 to keep DXBC parity with FXC output.
# DXC only supports SM 6.0+.
compiler_args.extend([
"-T", f"{stage}_5_1",
"-T", f"{stage}_6_0",
"-HV", "2017",
"-D", "SHADING_LANGUAGE_HLSL_XE=1",
"-I", src_dir,

View File

@@ -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
View File

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

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

@@ -0,0 +1,721 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "aes"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
]
[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]]
name = "anstream"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
[[package]]
name = "anstyle-parse"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys 0.61.2",
]
[[package]]
name = "anyhow"
version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "bitflags"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "cc"
version = "1.2.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "cipher"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common",
"inout",
]
[[package]]
name = "clap"
version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
[[package]]
name = "colorchoice"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "inout"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
dependencies = [
"generic-array",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.184"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af"
[[package]]
name = "log"
version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "matchers"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
dependencies = [
"regex-automata",
]
[[package]]
name = "memchr"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
name = "nu-ansi-term"
version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex-automata"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "sharded-slab"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
dependencies = [
"lazy_static",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "thread_local"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
dependencies = [
"cfg-if",
]
[[package]]
name = "tracing"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
"pin-project-lite",
"tracing-attributes",
"tracing-core",
]
[[package]]
name = "tracing-attributes"
version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tracing-core"
version = "0.1.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [
"once_cell",
"valuable",
]
[[package]]
name = "tracing-log"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"log",
"once_cell",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
dependencies = [
"matchers",
"nu-ansi-term",
"once_cell",
"regex-automata",
"sharded-slab",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
]
[[package]]
name = "typenum"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "valuable"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "xenia-app"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"tracing",
"tracing-subscriber",
"xenia-apu",
"xenia-cpu",
"xenia-debugger",
"xenia-gpu",
"xenia-hid",
"xenia-kernel",
"xenia-memory",
"xenia-types",
"xenia-vfs",
"xenia-xex",
]
[[package]]
name = "xenia-apu"
version = "0.1.0"
dependencies = [
"thiserror",
"tracing",
"xenia-types",
]
[[package]]
name = "xenia-cpu"
version = "0.1.0"
dependencies = [
"bitflags",
"thiserror",
"tracing",
"xenia-memory",
"xenia-types",
]
[[package]]
name = "xenia-debugger"
version = "0.1.0"
dependencies = [
"thiserror",
"tracing",
"xenia-cpu",
"xenia-memory",
"xenia-types",
]
[[package]]
name = "xenia-gpu"
version = "0.1.0"
dependencies = [
"anyhow",
"byteorder",
"thiserror",
"tracing",
"xenia-memory",
"xenia-types",
]
[[package]]
name = "xenia-hid"
version = "0.1.0"
dependencies = [
"thiserror",
"tracing",
"xenia-types",
]
[[package]]
name = "xenia-kernel"
version = "0.1.0"
dependencies = [
"anyhow",
"thiserror",
"tracing",
"xenia-cpu",
"xenia-memory",
"xenia-types",
]
[[package]]
name = "xenia-memory"
version = "0.1.0"
dependencies = [
"bitflags",
"libc",
"thiserror",
"tracing",
"windows-sys 0.59.0",
"xenia-types",
]
[[package]]
name = "xenia-types"
version = "0.1.0"
dependencies = [
"bitflags",
"byteorder",
"serde",
"thiserror",
]
[[package]]
name = "xenia-vfs"
version = "0.1.0"
dependencies = [
"anyhow",
"byteorder",
"thiserror",
"tracing",
"xenia-types",
]
[[package]]
name = "xenia-xex"
version = "0.1.0"
dependencies = [
"aes",
"anyhow",
"byteorder",
"cc",
"thiserror",
"tracing",
"xenia-memory",
"xenia-types",
]

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

@@ -0,0 +1,43 @@
[workspace]
resolver = "2"
members = [
"crates/xenia-types",
"crates/xenia-memory",
"crates/xenia-cpu",
"crates/xenia-xex",
"crates/xenia-vfs",
"crates/xenia-kernel",
"crates/xenia-gpu",
"crates/xenia-apu",
"crates/xenia-hid",
"crates/xenia-debugger",
"crates/xenia-app",
]
[workspace.package]
version = "0.1.0"
edition = "2024"
license = "BSD-3-Clause"
[workspace.dependencies]
# Shared types
xenia-types = { path = "crates/xenia-types" }
xenia-memory = { path = "crates/xenia-memory" }
xenia-cpu = { path = "crates/xenia-cpu" }
xenia-xex = { path = "crates/xenia-xex" }
xenia-vfs = { path = "crates/xenia-vfs" }
xenia-kernel = { path = "crates/xenia-kernel" }
xenia-gpu = { path = "crates/xenia-gpu" }
xenia-apu = { path = "crates/xenia-apu" }
xenia-hid = { path = "crates/xenia-hid" }
xenia-debugger = { path = "crates/xenia-debugger" }
# External dependencies
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
bitflags = "2"
byteorder = "1"
thiserror = "2"
anyhow = "1"
serde = { version = "1", features = ["derive"] }
aes = "0.8"

View File

@@ -0,0 +1,25 @@
[package]
name = "xenia-app"
version.workspace = true
edition.workspace = true
license.workspace = true
[[bin]]
name = "xenia-rs"
path = "src/main.rs"
[dependencies]
xenia-types = { workspace = true }
xenia-memory = { workspace = true }
xenia-cpu = { workspace = true }
xenia-xex = { workspace = true }
xenia-vfs = { workspace = true }
xenia-kernel = { workspace = true }
xenia-gpu = { workspace = true }
xenia-apu = { workspace = true }
xenia-hid = { workspace = true }
xenia-debugger = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
anyhow = { workspace = true }
clap = { version = "4", features = ["derive"] }

View File

@@ -0,0 +1,308 @@
use anyhow::Result;
use clap::{Parser, Subcommand};
use tracing_subscriber::EnvFilter;
#[derive(Parser)]
#[command(name = "xenia-rs")]
#[command(about = "Xbox 360 emulator for reverse engineering and preservation")]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Disassemble a XEX file from its entry point
Disasm {
/// Path to XEX file
path: String,
/// Number of instructions to disassemble
#[arg(short = 'n', default_value = "64")]
count: usize,
},
/// Load and execute a XEX file with tracing
Exec {
/// Path to XEX file
path: String,
/// Maximum instructions to execute before stopping
#[arg(short = 'n', default_value = "1000")]
max_instructions: u64,
},
/// Browse XISO disc image contents
Browse {
/// Path to XISO file
path: String,
},
/// Display XEX header information
Info {
/// Path to XEX file
path: String,
},
}
fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env().add_directive("info".parse()?))
.init();
let cli = Cli::parse();
match cli.command {
Commands::Disasm { path, count } => cmd_disasm(&path, count),
Commands::Exec { path, max_instructions } => cmd_exec(&path, max_instructions),
Commands::Browse { path } => cmd_browse(&path),
Commands::Info { path } => cmd_info(&path),
}
}
/// Load XEX data from a path. If the path is an ISO, extract default.xex from it.
fn load_xex_data(path: &str) -> Result<Vec<u8>> {
let lower = path.to_lowercase();
if lower.ends_with(".iso") || lower.ends_with(".xiso") {
use xenia_vfs::VfsDevice;
println!("Detected disc image, extracting default.xex...");
let disc = xenia_vfs::disc_image::DiscImageDevice::open("disc", std::path::Path::new(path))
.map_err(|e| anyhow::anyhow!("Failed to open disc image: {}", e))?;
disc.read_file("default.xex")
.map_err(|e| anyhow::anyhow!("Failed to extract default.xex from disc image: {}", e))
} else {
Ok(std::fs::read(path)?)
}
}
fn cmd_info(path: &str) -> Result<()> {
let data = load_xex_data(path)?;
let header = xenia_xex::loader::parse_xex2_header(&data)?;
println!("=== XEX2 Header ===");
println!("Magic: {:#010x}", header.magic);
println!("Module Flags: {:#010x}", header.module_flags);
println!("Header Size: {:#x}", header.header_size);
println!("Headers: {}", header.header_count);
if let Some(entry) = xenia_xex::loader::get_entry_point(&header) {
println!("Entry Point: {:#010x}", entry);
}
if let Some(base) = xenia_xex::loader::get_image_base(&header) {
println!("Image Base: {:#010x}", base);
}
println!("\n=== Optional Headers ===");
for h in &header.optional_headers {
println!(" Key: {:#010x} Value: {:#010x}", h.key, h.value);
}
if let Some(ref sec) = header.security_info {
println!("\n=== Security Info ===");
println!("Image Size: {:#x}", sec.image_size);
println!("Load Address: {:#010x}", sec.load_address);
println!("Image Flags: {:#010x}", sec.image_flags);
println!("Page Descs: {}", sec.page_descriptors.len());
}
if let Some(ref ffi) = header.file_format_info {
println!("\n=== File Format ===");
println!("Encryption: {}", match ffi.encryption_type {
0 => "None", 1 => "Normal (AES)", _ => "Unknown"
});
println!("Compression: {}", match ffi.compression_type {
0 => "None", 1 => "Basic", 2 => "Normal (LZX)", _ => "Unknown"
});
if !ffi.basic_blocks.is_empty() {
println!("Basic blocks: {}", ffi.basic_blocks.len());
}
if ffi.normal_window_size != 0 {
println!("LZX Window: {:#x}", ffi.normal_window_size);
}
}
if !header.import_libraries.is_empty() {
println!("\n=== Import Libraries ===");
for lib in &header.import_libraries {
println!(" {} (v{:#010x}, {} ordinals)", lib.name, lib.version_cur, lib.ordinals.len());
}
}
Ok(())
}
fn cmd_disasm(path: &str, count: usize) -> Result<()> {
let data = load_xex_data(path)?;
let header = xenia_xex::loader::parse_xex2_header(&data)?;
let entry = xenia_xex::loader::get_entry_point(&header)
.ok_or_else(|| anyhow::anyhow!("No entry point found in XEX2 header"))?;
let base = xenia_xex::loader::get_image_base(&header)
.ok_or_else(|| anyhow::anyhow!("No image base found in XEX2 header"))?;
println!("Entry point: {:#010x}, Image base: {:#010x}", entry, base);
// Load and decompress the image
let image_data = xenia_xex::loader::load_image(&data, &header)?;
println!("Image loaded: {} bytes decompressed", image_data.len());
println!("Disassembly from entry point ({} instructions):\n", count);
let entry_offset = (entry - base) as usize;
if entry_offset + count * 4 <= image_data.len() {
let block = xenia_cpu::disasm::disassemble_block(&image_data[entry_offset..], entry, count);
for (addr, text) in block {
println!(" {:#010x}: {}", addr, text);
}
} else {
println!(" (entry point offset {:#x} is outside image bounds, image is {:#x} bytes)", entry_offset, image_data.len());
}
Ok(())
}
fn cmd_exec(path: &str, max_instructions: u64) -> Result<()> {
let data = load_xex_data(path)?;
let header = xenia_xex::loader::parse_xex2_header(&data)?;
let entry = xenia_xex::loader::get_entry_point(&header)
.ok_or_else(|| anyhow::anyhow!("No entry point found"))?;
let base = xenia_xex::loader::get_image_base(&header)
.ok_or_else(|| anyhow::anyhow!("No image base found"))?;
// Print compression info
if let Some(ref ffi) = header.file_format_info {
println!("Compression: {} (encryption: {})",
match ffi.compression_type {
0 => "none", 1 => "basic", 2 => "normal (LZX)", _ => "unknown"
},
match ffi.encryption_type {
0 => "none", 1 => "normal (AES)", _ => "unknown"
});
}
if !header.import_libraries.is_empty() {
println!("Import libraries:");
for lib in &header.import_libraries {
println!(" {} ({} ordinals)", lib.name, lib.ordinals.len());
}
}
println!("Loading XEX: entry={:#010x} base={:#010x}", entry, base);
// Allocate guest memory
let mut mem = xenia_memory::GuestMemory::new()
.map_err(|e| anyhow::anyhow!("Failed to allocate guest memory: {}", e))?;
// Load and decompress the XEX image
let image_data = xenia_xex::loader::load_image(&data, &header)?;
let alloc_size = ((image_data.len() + 4095) & !4095) as u32;
mem.alloc(
base,
alloc_size,
xenia_memory::page_table::MemoryProtect::READ | xenia_memory::page_table::MemoryProtect::WRITE,
).map_err(|e| anyhow::anyhow!("Failed to allocate guest memory region: {}", e))?;
mem.write_bulk(base, &image_data);
// Allocate stack (1MB at 0x70000000)
let stack_base = 0x7000_0000u32;
let stack_size = 0x10_0000u32;
mem.alloc(
stack_base,
stack_size,
xenia_memory::page_table::MemoryProtect::READ | xenia_memory::page_table::MemoryProtect::WRITE,
).map_err(|e| anyhow::anyhow!("Failed to allocate stack: {}", e))?;
// Set up CPU context
let mut ctx = xenia_cpu::PpcContext::new();
ctx.pc = entry;
ctx.gpr[1] = (stack_base + stack_size - 0x80) as u64; // Stack pointer (with red zone)
ctx.gpr[13] = 0; // Small data area (TLS)
// Set up kernel
let mut _kernel = xenia_kernel::KernelState::new();
// Set up debugger
let mut debugger = xenia_debugger::Debugger::new();
debugger.paused = false;
debugger.step_mode = xenia_debugger::StepMode::Run;
debugger.trace_enabled = true;
println!("Starting execution (max {} instructions)...\n", max_instructions);
use xenia_cpu::interpreter::{step, StepResult};
let mut instruction_count: u64 = 0;
loop {
if instruction_count >= max_instructions {
println!("\nReached max instruction count ({})", max_instructions);
break;
}
// Check if PC is in mapped memory before trying to execute
if !mem.is_mapped(ctx.pc) {
println!("[{:>8}] FAULT: PC {:#010x} is in unmapped memory", instruction_count, ctx.pc);
break;
}
// Pre-step debugger
debugger.pre_step(&ctx, &mem);
let result = step(&mut ctx, &mut mem);
instruction_count += 1;
// Post-step debugger
debugger.post_step(&ctx, &mem);
match result {
StepResult::Continue => {}
StepResult::SystemCall => {
println!("[{:>8}] SYSCALL at {:#010x}", instruction_count, ctx.pc.wrapping_sub(4));
}
StepResult::Unimplemented(op) => {
println!("[{:>8}] UNIMPL: {:?} at {:#010x}", instruction_count, op, ctx.pc.wrapping_sub(4));
}
StepResult::Trap => {
println!("[{:>8}] TRAP at {:#010x}", instruction_count, ctx.pc.wrapping_sub(4));
}
StepResult::Halted => {
println!("[{:>8}] HALTED", instruction_count);
break;
}
}
if debugger.should_break() {
println!("[{:>8}] BREAK at {:#010x}", instruction_count, ctx.pc);
break;
}
}
println!("\n=== Final State ===");
println!("PC: {:#010x}", ctx.pc);
println!("LR: {:#010x}", ctx.lr as u32);
println!("CTR: {:#010x}", ctx.ctr as u32);
println!("CR: {:#010x}", ctx.cr());
println!("XER: CA={} OV={} SO={}", ctx.xer_ca, ctx.xer_ov, ctx.xer_so);
for i in 0..32 {
if ctx.gpr[i] != 0 {
println!("r{:<2}: {:#018x}", i, ctx.gpr[i]);
}
}
println!("\nExecuted {} instructions", instruction_count);
println!("Trace log: {} entries", debugger.trace_log.len());
Ok(())
}
fn cmd_browse(path: &str) -> Result<()> {
use xenia_vfs::VfsDevice;
let disc = xenia_vfs::disc_image::DiscImageDevice::open("disc", std::path::Path::new(path))
.map_err(|e| anyhow::anyhow!("Failed to open disc image: {}", e))?;
println!("=== XISO Contents: {} ===", path);
match disc.list_root() {
Ok(entries) => {
for entry in entries {
let kind = if entry.is_directory { "DIR " } else { "FILE" };
println!(" {} {:>10} {}", kind, entry.size, entry.name);
}
}
Err(e) => println!(" Error listing contents: {}", e),
}
Ok(())
}

View File

@@ -0,0 +1,10 @@
[package]
name = "xenia-apu"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
xenia-types = { workspace = true }
tracing = { workspace = true }
thiserror = { workspace = true }

View File

@@ -0,0 +1,16 @@
/// Audio processing unit stub. Logging only for now.
pub struct AudioSystem {
pub enabled: bool,
}
impl AudioSystem {
pub fn new() -> Self {
Self { enabled: false }
}
}
impl Default for AudioSystem {
fn default() -> Self {
Self::new()
}
}

View File

@@ -0,0 +1,12 @@
[package]
name = "xenia-cpu"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
xenia-types = { workspace = true }
xenia-memory = { workspace = true }
tracing = { workspace = true }
bitflags = { workspace = true }
thiserror = { workspace = true }

View File

@@ -0,0 +1,191 @@
use xenia_types::Vec128;
/// Condition register field (one of CR0-CR7).
#[derive(Debug, Clone, Copy, Default)]
pub struct CrField {
pub lt: bool,
pub gt: bool,
pub eq: bool,
pub so: bool,
}
impl CrField {
pub fn as_u8(&self) -> u8 {
((self.lt as u8) << 3) | ((self.gt as u8) << 2) | ((self.eq as u8) << 1) | (self.so as u8)
}
pub fn from_u8(val: u8) -> Self {
Self {
lt: val & 8 != 0,
gt: val & 4 != 0,
eq: val & 2 != 0,
so: val & 1 != 0,
}
}
}
/// SPR (Special Purpose Register) numbers used by mfspr/mtspr.
pub mod spr {
pub const XER: u32 = 1;
pub const LR: u32 = 8;
pub const CTR: u32 = 9;
pub const TBL: u32 = 268;
pub const TBU: u32 = 269;
pub const SPRG0: u32 = 272;
pub const SPRG1: u32 = 273;
pub const SPRG2: u32 = 274;
pub const SPRG3: u32 = 275;
pub const PVR: u32 = 287;
pub const PIR: u32 = 1023;
}
/// PowerPC processor context. Holds all register state for one guest thread.
/// Mirrors PPCContext from ppc_context.h, minus JIT-specific fields.
#[repr(C, align(64))]
pub struct PpcContext {
// General purpose registers (R0-R31)
pub gpr: [u64; 32],
// Count register
pub ctr: u64,
// Link register
pub lr: u64,
// Machine state register
pub msr: u64,
// Floating-point registers (F0-F31)
pub fpr: [f64; 32],
// VMX128 vector registers (V0-V127, Xbox 360 extended set)
pub vr: [Vec128; 128],
// Condition register fields (CR0-CR7)
pub cr: [CrField; 8],
// Floating-point status and control register
pub fpscr: u32,
// XER register (split for easy individual updates)
pub xer_ca: u8,
pub xer_ov: u8,
pub xer_so: u8,
// Altivec VSCR saturation bit
pub vscr_sat: u8,
// Program counter
pub pc: u32,
// Reservation address/value for lwarx/stwcx
pub reserved_addr: u32,
pub reserved_val: u64,
pub has_reservation: bool,
// Thread ID (for kernel use)
pub thread_id: u32,
// Cycle counter for timing
pub cycle_count: u64,
// Time base (incremented each instruction for debugging)
pub timebase: u64,
}
impl PpcContext {
pub fn new() -> Self {
Self {
gpr: [0; 32],
ctr: 0,
lr: 0,
msr: 0,
fpr: [0.0; 32],
vr: [Vec128::ZERO; 128],
cr: [CrField::default(); 8],
fpscr: 0,
xer_ca: 0,
xer_ov: 0,
xer_so: 0,
vscr_sat: 0,
pc: 0,
reserved_addr: 0,
reserved_val: 0,
has_reservation: false,
thread_id: 0,
cycle_count: 0,
timebase: 0,
}
}
/// Get the full 32-bit condition register.
pub fn cr(&self) -> u32 {
let mut val = 0u32;
for (i, field) in self.cr.iter().enumerate() {
val |= (field.as_u8() as u32) << (28 - i * 4);
}
val
}
/// Set the full 32-bit condition register.
pub fn set_cr(&mut self, val: u32) {
for i in 0..8 {
self.cr[i] = CrField::from_u8(((val >> (28 - i * 4)) & 0xF) as u8);
}
}
/// Get a single CR bit by absolute bit number (0-31).
pub fn get_cr_bit(&self, bit: u32) -> bool {
let field = (bit / 4) as usize;
let sub = bit % 4;
match sub {
0 => self.cr[field].lt,
1 => self.cr[field].gt,
2 => self.cr[field].eq,
3 => self.cr[field].so,
_ => unreachable!(),
}
}
/// Set a single CR bit by absolute bit number (0-31).
pub fn set_cr_bit(&mut self, bit: u32, val: bool) {
let field = (bit / 4) as usize;
let sub = bit % 4;
match sub {
0 => self.cr[field].lt = val,
1 => self.cr[field].gt = val,
2 => self.cr[field].eq = val,
3 => self.cr[field].so = val,
_ => unreachable!(),
}
}
/// Update a condition register field based on a comparison result (signed).
pub fn update_cr_signed(&mut self, field: usize, val: i64) {
self.cr[field] = CrField {
lt: val < 0,
gt: val > 0,
eq: val == 0,
so: self.xer_so != 0,
};
}
/// Update a condition register field based on a comparison result (unsigned).
pub fn update_cr_unsigned(&mut self, field: usize, a: u64, b: u64) {
self.cr[field] = CrField {
lt: a < b,
gt: a > b,
eq: a == b,
so: self.xer_so != 0,
};
}
/// Get the full XER register value.
pub fn xer(&self) -> u32 {
((self.xer_so as u32) << 31) | ((self.xer_ov as u32) << 30) | ((self.xer_ca as u32) << 29)
}
/// Set XER from a full 32-bit value.
pub fn set_xer(&mut self, val: u32) {
self.xer_so = ((val >> 31) & 1) as u8;
self.xer_ov = ((val >> 30) & 1) as u8;
self.xer_ca = ((val >> 29) & 1) as u8;
}
}
impl Default for PpcContext {
fn default() -> Self {
Self::new()
}
}

View File

@@ -0,0 +1,819 @@
use crate::opcode::PpcOpcode;
/// Extract bits [a..=b] from a 32-bit value (PPC bit numbering: 0 = MSB).
#[inline(always)]
const fn extract_bits(v: u32, a: u32, b: u32) -> u32 {
(v >> (32 - 1 - b)) & ((1 << (b - a + 1)) - 1)
}
/// Decoded PPC instruction with extracted operand fields.
#[derive(Debug, Clone, Copy)]
pub struct DecodedInstr {
pub opcode: PpcOpcode,
pub raw: u32,
pub addr: u32,
}
impl DecodedInstr {
// Common field extractors (PPC bit numbering)
/// Primary opcode (bits 0-5)
#[inline] pub fn op(&self) -> u32 { extract_bits(self.raw, 0, 5) }
/// rD/rS/rT (bits 6-10) - destination/source register
#[inline] pub fn rd(&self) -> usize { extract_bits(self.raw, 6, 10) as usize }
#[inline] pub fn rs(&self) -> usize { self.rd() }
#[inline] pub fn rt(&self) -> usize { self.rd() }
/// rA (bits 11-15)
#[inline] pub fn ra(&self) -> usize { extract_bits(self.raw, 11, 15) as usize }
/// rB (bits 16-20)
#[inline] pub fn rb(&self) -> usize { extract_bits(self.raw, 16, 20) as usize }
/// rC (bits 21-25) - for 4-operand instructions
#[inline] pub fn rc(&self) -> usize { extract_bits(self.raw, 21, 25) as usize }
/// SIMM/UIMM (bits 16-31) - signed/unsigned immediate
#[inline] pub fn simm16(&self) -> i16 { (self.raw & 0xFFFF) as i16 }
#[inline] pub fn uimm16(&self) -> u16 { (self.raw & 0xFFFF) as u16 }
/// D-form displacement (signed, bits 16-31)
#[inline] pub fn d(&self) -> i32 { self.simm16() as i32 }
/// DS-form displacement (signed, bits 16-29, shifted left 2)
#[inline] pub fn ds(&self) -> i32 { (self.raw & 0xFFFC) as i16 as i32 }
/// LI field for branch (bits 6-29, sign-extended, shifted left 2)
#[inline] pub fn li(&self) -> i32 {
let li = extract_bits(self.raw, 6, 29);
// Sign-extend from 24 bits, then shift left 2
let sign_extended = ((li as i32) << 8) >> 8;
sign_extended << 2
}
/// BD field for conditional branch (bits 16-29, sign-extended, shifted left 2)
#[inline] pub fn bd(&self) -> i32 {
let bd = extract_bits(self.raw, 16, 29);
let sign_extended = ((bd as i32) << 18) >> 18;
sign_extended << 2
}
/// BO field (bits 6-10) - branch options
#[inline] pub fn bo(&self) -> u32 { extract_bits(self.raw, 6, 10) }
/// BI field (bits 11-15) - branch condition
#[inline] pub fn bi(&self) -> u32 { extract_bits(self.raw, 11, 15) }
/// AA bit (bit 30) - absolute address
#[inline] pub fn aa(&self) -> bool { (self.raw >> 1) & 1 != 0 }
/// LK bit (bit 31) - link (update LR)
#[inline] pub fn lk(&self) -> bool { self.raw & 1 != 0 }
/// Rc bit (bit 31) - record CR0
#[inline] pub fn rc_bit(&self) -> bool { self.raw & 1 != 0 }
/// OE bit (bit 21) - overflow enable
#[inline] pub fn oe(&self) -> bool { extract_bits(self.raw, 21, 21) != 0 }
/// MB, ME fields for rotate instructions
#[inline] pub fn mb(&self) -> u32 { extract_bits(self.raw, 21, 25) }
#[inline] pub fn me(&self) -> u32 { extract_bits(self.raw, 26, 30) }
/// SH field (bits 16-20) for shift instructions
#[inline] pub fn sh(&self) -> u32 { extract_bits(self.raw, 16, 20) }
/// SH field for 64-bit shifts (bits 16-20 + bit 30)
#[inline] pub fn sh64(&self) -> u32 {
(extract_bits(self.raw, 16, 20) << 1) | extract_bits(self.raw, 30, 30)
}
/// SPR field (bits 11-20, swapped halves)
#[inline] pub fn spr(&self) -> u32 {
let spr_raw = extract_bits(self.raw, 11, 20);
((spr_raw & 0x1F) << 5) | ((spr_raw >> 5) & 0x1F)
}
/// CRM field (bits 12-19) for mtcrf
#[inline] pub fn crm(&self) -> u32 { extract_bits(self.raw, 12, 19) }
/// crfD (bits 6-8) - condition register field destination
#[inline] pub fn crfd(&self) -> usize { extract_bits(self.raw, 6, 8) as usize }
/// crfS (bits 11-13)
#[inline] pub fn crfs(&self) -> usize { extract_bits(self.raw, 11, 13) as usize }
/// L bit (bit 10) - 64-bit compare
#[inline] pub fn l(&self) -> bool { extract_bits(self.raw, 10, 10) != 0 }
/// crbD (bits 6-10)
#[inline] pub fn crbd(&self) -> u32 { extract_bits(self.raw, 6, 10) }
/// crbA (bits 11-15)
#[inline] pub fn crba(&self) -> u32 { extract_bits(self.raw, 11, 15) }
/// crbB (bits 16-20)
#[inline] pub fn crbb(&self) -> u32 { extract_bits(self.raw, 16, 20) }
// VMX128 field extractors
/// VA128 (bits 6-10, plus bit from 29)
#[inline] pub fn va128(&self) -> usize {
(extract_bits(self.raw, 6, 10) | (extract_bits(self.raw, 29, 29) << 5)) as usize
}
/// VB128 (bits 16-20, plus bits from 28, 30)
#[inline] pub fn vb128(&self) -> usize {
(extract_bits(self.raw, 16, 20)
| (extract_bits(self.raw, 28, 28) << 5)
| (extract_bits(self.raw, 30, 30) << 6)) as usize
}
/// VD128 (bits 6-10, plus bits from 21, 22)
#[inline] pub fn vd128(&self) -> usize {
(extract_bits(self.raw, 6, 10)
| (extract_bits(self.raw, 21, 21) << 5)
| (extract_bits(self.raw, 22, 22) << 6)) as usize
}
/// VS128 - same encoding as VD128
#[inline] pub fn vs128(&self) -> usize { self.vd128() }
/// NB field (bits 16-20) for lswi/stswi
#[inline] pub fn nb(&self) -> u32 { extract_bits(self.raw, 16, 20) }
}
/// Decode a 32-bit PPC instruction into its opcode.
/// Direct translation of the C++ LookupOpcode from ppc_opcode_lookup_gen.cc.
pub fn decode(raw: u32, addr: u32) -> DecodedInstr {
let opcode = lookup_opcode(raw);
DecodedInstr { opcode, raw, addr }
}
fn lookup_opcode(code: u32) -> PpcOpcode {
match extract_bits(code, 0, 5) {
2 => PpcOpcode::tdi,
3 => PpcOpcode::twi,
4 => decode_op4(code),
5 => decode_op5(code),
6 => decode_op6(code),
7 => PpcOpcode::mulli,
8 => PpcOpcode::subficx,
10 => PpcOpcode::cmpli,
11 => PpcOpcode::cmpi,
12 => PpcOpcode::addic,
13 => PpcOpcode::addicx,
14 => PpcOpcode::addi,
15 => PpcOpcode::addis,
16 => PpcOpcode::bcx,
17 => PpcOpcode::sc,
18 => PpcOpcode::bx,
19 => decode_op19(code),
20 => PpcOpcode::rlwimix,
21 => PpcOpcode::rlwinmx,
23 => PpcOpcode::rlwnmx,
24 => PpcOpcode::ori,
25 => PpcOpcode::oris,
26 => PpcOpcode::xori,
27 => PpcOpcode::xoris,
28 => PpcOpcode::andix,
29 => PpcOpcode::andisx,
30 => decode_op30(code),
31 => decode_op31(code),
32 => PpcOpcode::lwz,
33 => PpcOpcode::lwzu,
34 => PpcOpcode::lbz,
35 => PpcOpcode::lbzu,
36 => PpcOpcode::stw,
37 => PpcOpcode::stwu,
38 => PpcOpcode::stb,
39 => PpcOpcode::stbu,
40 => PpcOpcode::lhz,
41 => PpcOpcode::lhzu,
42 => PpcOpcode::lha,
43 => PpcOpcode::lhau,
44 => PpcOpcode::sth,
45 => PpcOpcode::sthu,
46 => PpcOpcode::lmw,
47 => PpcOpcode::stmw,
48 => PpcOpcode::lfs,
49 => PpcOpcode::lfsu,
50 => PpcOpcode::lfd,
51 => PpcOpcode::lfdu,
52 => PpcOpcode::stfs,
53 => PpcOpcode::stfsu,
54 => PpcOpcode::stfd,
55 => PpcOpcode::stfdu,
58 => match extract_bits(code, 30, 31) {
0b00 => PpcOpcode::ld,
0b01 => PpcOpcode::ldu,
0b10 => PpcOpcode::lwa,
_ => PpcOpcode::Invalid,
},
59 => match extract_bits(code, 26, 30) {
0b10010 => PpcOpcode::fdivsx,
0b10100 => PpcOpcode::fsubsx,
0b10101 => PpcOpcode::faddsx,
0b10110 => PpcOpcode::fsqrtsx,
0b11000 => PpcOpcode::fresx,
0b11001 => PpcOpcode::fmulsx,
0b11100 => PpcOpcode::fmsubsx,
0b11101 => PpcOpcode::fmaddsx,
0b11110 => PpcOpcode::fnmsubsx,
0b11111 => PpcOpcode::fnmaddsx,
_ => PpcOpcode::Invalid,
},
62 => match extract_bits(code, 30, 31) {
0b00 => PpcOpcode::std,
0b01 => PpcOpcode::stdu,
_ => PpcOpcode::Invalid,
},
63 => decode_op63(code),
_ => PpcOpcode::Invalid,
}
}
fn decode_op4(code: u32) -> PpcOpcode {
// VMX128 load/store (op=4, bits 21-27 << 4 | bits 30-31)
let key1 = (extract_bits(code, 21, 27) << 4) | extract_bits(code, 30, 31);
match key1 {
0b00000000011 => return PpcOpcode::lvsl128,
0b00001000011 => return PpcOpcode::lvsr128,
0b00010000011 => return PpcOpcode::lvewx128,
0b00011000011 => return PpcOpcode::lvx128,
0b00110000011 => return PpcOpcode::stvewx128,
0b00111000011 => return PpcOpcode::stvx128,
0b01011000011 => return PpcOpcode::lvxl128,
0b01111000011 => return PpcOpcode::stvxl128,
0b10000000011 => return PpcOpcode::lvlx128,
0b10001000011 => return PpcOpcode::lvrx128,
0b10100000011 => return PpcOpcode::stvlx128,
0b10101000011 => return PpcOpcode::stvrx128,
0b11000000011 => return PpcOpcode::lvlxl128,
0b11001000011 => return PpcOpcode::lvrxl128,
0b11100000011 => return PpcOpcode::stvlxl128,
0b11101000011 => return PpcOpcode::stvrxl128,
_ => {}
}
// Standard VMX (op=4, bits 21-31)
let key2 = extract_bits(code, 21, 31);
match key2 {
0b00000000000 => return PpcOpcode::vaddubm,
0b00000000010 => return PpcOpcode::vmaxub,
0b00000000100 => return PpcOpcode::vrlb,
0b00000001000 => return PpcOpcode::vmuloub,
0b00000001010 => return PpcOpcode::vaddfp,
0b00000001100 => return PpcOpcode::vmrghb,
0b00000001110 => return PpcOpcode::vpkuhum,
0b00001000000 => return PpcOpcode::vadduhm,
0b00001000010 => return PpcOpcode::vmaxuh,
0b00001000100 => return PpcOpcode::vrlh,
0b00001001000 => return PpcOpcode::vmulouh,
0b00001001010 => return PpcOpcode::vsubfp,
0b00001001100 => return PpcOpcode::vmrghh,
0b00001001110 => return PpcOpcode::vpkuwum,
0b00010000000 => return PpcOpcode::vadduwm,
0b00010000010 => return PpcOpcode::vmaxuw,
0b00010000100 => return PpcOpcode::vrlw,
0b00010001100 => return PpcOpcode::vmrghw,
0b00010001110 => return PpcOpcode::vpkuhus,
0b00011001110 => return PpcOpcode::vpkuwus,
0b00100000010 => return PpcOpcode::vmaxsb,
0b00100000100 => return PpcOpcode::vslb,
0b00100001000 => return PpcOpcode::vmulosb,
0b00100001010 => return PpcOpcode::vrefp,
0b00100001100 => return PpcOpcode::vmrglb,
0b00100001110 => return PpcOpcode::vpkshus,
0b00101000010 => return PpcOpcode::vmaxsh,
0b00101000100 => return PpcOpcode::vslh,
0b00101001000 => return PpcOpcode::vmulosh,
0b00101001010 => return PpcOpcode::vrsqrtefp,
0b00101001100 => return PpcOpcode::vmrglh,
0b00101001110 => return PpcOpcode::vpkswus,
0b00110000000 => return PpcOpcode::vaddcuw,
0b00110000010 => return PpcOpcode::vmaxsw,
0b00110000100 => return PpcOpcode::vslw,
0b00110001010 => return PpcOpcode::vexptefp,
0b00110001100 => return PpcOpcode::vmrglw,
0b00110001110 => return PpcOpcode::vpkshss,
0b00111000100 => return PpcOpcode::vsl,
0b00111001010 => return PpcOpcode::vlogefp,
0b00111001110 => return PpcOpcode::vpkswss,
0b01000000000 => return PpcOpcode::vaddubs,
0b01000000010 => return PpcOpcode::vminub,
0b01000000100 => return PpcOpcode::vsrb,
0b01000001000 => return PpcOpcode::vmuleub,
0b01000001010 => return PpcOpcode::vrfin,
0b01000001100 => return PpcOpcode::vspltb,
0b01000001110 => return PpcOpcode::vupkhsb,
0b01001000000 => return PpcOpcode::vadduhs,
0b01001000010 => return PpcOpcode::vminuh,
0b01001000100 => return PpcOpcode::vsrh,
0b01001001000 => return PpcOpcode::vmuleuh,
0b01001001010 => return PpcOpcode::vrfiz,
0b01001001100 => return PpcOpcode::vsplth,
0b01001001110 => return PpcOpcode::vupkhsh,
0b01010000000 => return PpcOpcode::vadduws,
0b01010000010 => return PpcOpcode::vminuw,
0b01010000100 => return PpcOpcode::vsrw,
0b01010001010 => return PpcOpcode::vrfip,
0b01010001100 => return PpcOpcode::vspltw,
0b01010001110 => return PpcOpcode::vupklsb,
0b01011000100 => return PpcOpcode::vsr,
0b01011001010 => return PpcOpcode::vrfim,
0b01011001110 => return PpcOpcode::vupklsh,
0b01100000000 => return PpcOpcode::vaddsbs,
0b01100000010 => return PpcOpcode::vminsb,
0b01100000100 => return PpcOpcode::vsrab,
0b01100001000 => return PpcOpcode::vmulesb,
0b01100001010 => return PpcOpcode::vcfux,
0b01100001100 => return PpcOpcode::vspltisb,
0b01100001110 => return PpcOpcode::vpkpx,
0b01101000000 => return PpcOpcode::vaddshs,
0b01101000010 => return PpcOpcode::vminsh,
0b01101000100 => return PpcOpcode::vsrah,
0b01101001000 => return PpcOpcode::vmulesh,
0b01101001010 => return PpcOpcode::vcfsx,
0b01101001100 => return PpcOpcode::vspltish,
0b01101001110 => return PpcOpcode::vupkhpx,
0b01110000000 => return PpcOpcode::vaddsws,
0b01110000010 => return PpcOpcode::vminsw,
0b01110000100 => return PpcOpcode::vsraw,
0b01110001010 => return PpcOpcode::vctuxs,
0b01110001100 => return PpcOpcode::vspltisw,
0b01111001010 => return PpcOpcode::vctsxs,
0b01111001110 => return PpcOpcode::vupklpx,
0b10000000000 => return PpcOpcode::vsububm,
0b10000000010 => return PpcOpcode::vavgub,
0b10000000100 => return PpcOpcode::vand,
0b10000001010 => return PpcOpcode::vmaxfp,
0b10000001100 => return PpcOpcode::vslo,
0b10001000000 => return PpcOpcode::vsubuhm,
0b10001000010 => return PpcOpcode::vavguh,
0b10001000100 => return PpcOpcode::vandc,
0b10001001010 => return PpcOpcode::vminfp,
0b10001001100 => return PpcOpcode::vsro,
0b10010000000 => return PpcOpcode::vsubuwm,
0b10010000010 => return PpcOpcode::vavguw,
0b10010000100 => return PpcOpcode::vor,
0b10011000100 => return PpcOpcode::vxor,
0b10100000010 => return PpcOpcode::vavgsb,
0b10100000100 => return PpcOpcode::vnor,
0b10101000010 => return PpcOpcode::vavgsh,
0b10110000000 => return PpcOpcode::vsubcuw,
0b10110000010 => return PpcOpcode::vavgsw,
0b11000000000 => return PpcOpcode::vsububs,
0b11000000100 => return PpcOpcode::mfvscr,
0b11000001000 => return PpcOpcode::vsum4ubs,
0b11001000000 => return PpcOpcode::vsubuhs,
0b11001000100 => return PpcOpcode::mtvscr,
0b11001001000 => return PpcOpcode::vsum4shs,
0b11010000000 => return PpcOpcode::vsubuws,
0b11010001000 => return PpcOpcode::vsum2sws,
0b11100000000 => return PpcOpcode::vsubsbs,
0b11100001000 => return PpcOpcode::vsum4sbs,
0b11101000000 => return PpcOpcode::vsubshs,
0b11110000000 => return PpcOpcode::vsubsws,
0b11110001000 => return PpcOpcode::vsumsws,
_ => {}
}
// VMX compare (op=4, bits 22-31)
let key3 = extract_bits(code, 22, 31);
match key3 {
0b0000000110 => return PpcOpcode::vcmpequb,
0b0001000110 => return PpcOpcode::vcmpequh,
0b0010000110 => return PpcOpcode::vcmpequw,
0b0011000110 => return PpcOpcode::vcmpeqfp,
0b0111000110 => return PpcOpcode::vcmpgefp,
0b1000000110 => return PpcOpcode::vcmpgtub,
0b1001000110 => return PpcOpcode::vcmpgtuh,
0b1010000110 => return PpcOpcode::vcmpgtuw,
0b1011000110 => return PpcOpcode::vcmpgtfp,
0b1100000110 => return PpcOpcode::vcmpgtsb,
0b1101000110 => return PpcOpcode::vcmpgtsh,
0b1110000110 => return PpcOpcode::vcmpgtsw,
0b1111000110 => return PpcOpcode::vcmpbfp,
_ => {}
}
// VMX 4-operand (op=4, bits 26-31)
let key4 = extract_bits(code, 26, 31);
match key4 {
0b100000 => return PpcOpcode::vmhaddshs,
0b100001 => return PpcOpcode::vmhraddshs,
0b100010 => return PpcOpcode::vmladduhm,
0b100100 => return PpcOpcode::vmsumubm,
0b100101 => return PpcOpcode::vmsummbm,
0b100110 => return PpcOpcode::vmsumuhm,
0b100111 => return PpcOpcode::vmsumuhs,
0b101000 => return PpcOpcode::vmsumshm,
0b101001 => return PpcOpcode::vmsumshs,
0b101010 => return PpcOpcode::vsel,
0b101011 => return PpcOpcode::vperm,
0b101100 => return PpcOpcode::vsldoi,
0b101110 => return PpcOpcode::vmaddfp,
0b101111 => return PpcOpcode::vnmsubfp,
_ => {}
}
// vsldoi128 (op=4, bit 27)
if extract_bits(code, 27, 27) == 1 {
return PpcOpcode::vsldoi128;
}
PpcOpcode::Invalid
}
fn decode_op5(code: u32) -> PpcOpcode {
// vperm128 (op=5, bits 22,27)
let key1 = (extract_bits(code, 22, 22) << 5) | extract_bits(code, 27, 27);
if key1 == 0b000000 {
return PpcOpcode::vperm128;
}
let key2 = (extract_bits(code, 22, 25) << 2) | extract_bits(code, 27, 27);
match key2 {
0b000001 => PpcOpcode::vaddfp128,
0b000101 => PpcOpcode::vsubfp128,
0b001001 => PpcOpcode::vmulfp128,
0b001101 => PpcOpcode::vmaddfp128,
0b010001 => PpcOpcode::vmaddcfp128,
0b010101 => PpcOpcode::vnmsubfp128,
0b011001 => PpcOpcode::vmsum3fp128,
0b011101 => PpcOpcode::vmsum4fp128,
0b100000 => PpcOpcode::vpkshss128,
0b100001 => PpcOpcode::vand128,
0b100100 => PpcOpcode::vpkshus128,
0b100101 => PpcOpcode::vandc128,
0b101000 => PpcOpcode::vpkswss128,
0b101001 => PpcOpcode::vnor128,
0b101100 => PpcOpcode::vpkswus128,
0b101101 => PpcOpcode::vor128,
0b110000 => PpcOpcode::vpkuhum128,
0b110001 => PpcOpcode::vxor128,
0b110100 => PpcOpcode::vpkuhus128,
0b110101 => PpcOpcode::vsel128,
0b111000 => PpcOpcode::vpkuwum128,
0b111001 => PpcOpcode::vslo128,
0b111100 => PpcOpcode::vpkuwus128,
0b111101 => PpcOpcode::vsro128,
_ => PpcOpcode::Invalid,
}
}
fn decode_op6(code: u32) -> PpcOpcode {
// vpermwi128
let key1 = (extract_bits(code, 21, 22) << 5) | extract_bits(code, 26, 27);
if key1 == 0b0100001 {
return PpcOpcode::vpermwi128;
}
// vpkd3d128, vrlimi128
let key2 = (extract_bits(code, 21, 23) << 4) | extract_bits(code, 26, 27);
match key2 {
0b1100001 => return PpcOpcode::vpkd3d128,
0b1110001 => return PpcOpcode::vrlimi128,
_ => {}
}
// Unary VMX128 ops
let key3 = extract_bits(code, 21, 27);
match key3 {
0b0100011 => return PpcOpcode::vcfpsxws128,
0b0100111 => return PpcOpcode::vcfpuxws128,
0b0101011 => return PpcOpcode::vcsxwfp128,
0b0101111 => return PpcOpcode::vcuxwfp128,
0b0110011 => return PpcOpcode::vrfim128,
0b0110111 => return PpcOpcode::vrfin128,
0b0111011 => return PpcOpcode::vrfip128,
0b0111111 => return PpcOpcode::vrfiz128,
0b1100011 => return PpcOpcode::vrefp128,
0b1100111 => return PpcOpcode::vrsqrtefp128,
0b1101011 => return PpcOpcode::vexptefp128,
0b1101111 => return PpcOpcode::vlogefp128,
0b1110011 => return PpcOpcode::vspltw128,
0b1110111 => return PpcOpcode::vspltisw128,
0b1111111 => return PpcOpcode::vupkd3d128,
_ => {}
}
// VMX128 compare
let key4 = (extract_bits(code, 22, 24) << 3) | extract_bits(code, 27, 27);
match key4 {
0b000000 => return PpcOpcode::vcmpeqfp128,
0b001000 => return PpcOpcode::vcmpgefp128,
0b010000 => return PpcOpcode::vcmpgtfp128,
0b011000 => return PpcOpcode::vcmpbfp128,
0b100000 => return PpcOpcode::vcmpequw128,
_ => {}
}
// VMX128 shift/merge
let key5 = (extract_bits(code, 22, 25) << 2) | extract_bits(code, 27, 27);
match key5 {
0b000101 => return PpcOpcode::vrlw128,
0b001101 => return PpcOpcode::vslw128,
0b010101 => return PpcOpcode::vsraw128,
0b011101 => return PpcOpcode::vsrw128,
0b101000 => return PpcOpcode::vmaxfp128,
0b101100 => return PpcOpcode::vminfp128,
0b110000 => return PpcOpcode::vmrghw128,
0b110100 => return PpcOpcode::vmrglw128,
0b111000 => return PpcOpcode::vupkhsb128,
0b111100 => return PpcOpcode::vupklsb128,
_ => {}
}
PpcOpcode::Invalid
}
fn decode_op19(code: u32) -> PpcOpcode {
match extract_bits(code, 21, 30) {
0b0000000000 => PpcOpcode::mcrf,
0b0000010000 => PpcOpcode::bclrx,
0b0000100001 => PpcOpcode::crnor,
0b0010000001 => PpcOpcode::crandc,
0b0010010110 => PpcOpcode::isync,
0b0011000001 => PpcOpcode::crxor,
0b0011100001 => PpcOpcode::crnand,
0b0100000001 => PpcOpcode::crand,
0b0100100001 => PpcOpcode::creqv,
0b0110100001 => PpcOpcode::crorc,
0b0111000001 => PpcOpcode::cror,
0b1000010000 => PpcOpcode::bcctrx,
_ => PpcOpcode::Invalid,
}
}
fn decode_op30(code: u32) -> PpcOpcode {
match extract_bits(code, 27, 29) {
0b000 => PpcOpcode::rldiclx,
0b001 => PpcOpcode::rldicrx,
0b010 => PpcOpcode::rldicx,
0b011 => PpcOpcode::rldimix,
_ => match extract_bits(code, 27, 30) {
0b1000 => PpcOpcode::rldclx,
0b1001 => PpcOpcode::rldcrx,
_ => PpcOpcode::Invalid,
},
}
}
fn decode_op31(code: u32) -> PpcOpcode {
// sradix has a unique 10-bit key (bits 21-29)
if extract_bits(code, 21, 29) == 0b110011101 {
return PpcOpcode::sradix;
}
// Main op31 table (bits 21-30)
let key = extract_bits(code, 21, 30);
match key {
0b0000000000 => return PpcOpcode::cmp,
0b0000000100 => return PpcOpcode::tw,
0b0000000110 => return PpcOpcode::lvsl,
0b0000000111 => return PpcOpcode::lvebx,
0b0000010011 => return PpcOpcode::mfcr,
0b0000010100 => return PpcOpcode::lwarx,
0b0000010101 => return PpcOpcode::ldx,
0b0000010111 => return PpcOpcode::lwzx,
0b0000011000 => return PpcOpcode::slwx,
0b0000011010 => return PpcOpcode::cntlzwx,
0b0000011011 => return PpcOpcode::sldx,
0b0000011100 => return PpcOpcode::andx,
0b0000100000 => return PpcOpcode::cmpl,
0b0000100110 => return PpcOpcode::lvsr,
0b0000100111 => return PpcOpcode::lvehx,
0b0000110101 => return PpcOpcode::ldux,
0b0000110110 => return PpcOpcode::dcbst,
0b0000110111 => return PpcOpcode::lwzux,
0b0000111010 => return PpcOpcode::cntlzdx,
0b0000111100 => return PpcOpcode::andcx,
0b0001000100 => return PpcOpcode::td,
0b0001000111 => return PpcOpcode::lvewx,
0b0001010011 => return PpcOpcode::mfmsr,
0b0001010100 => return PpcOpcode::ldarx,
0b0001010110 => return PpcOpcode::dcbf,
0b0001010111 => return PpcOpcode::lbzx,
0b0001100111 => return PpcOpcode::lvx,
0b0001110111 => return PpcOpcode::lbzux,
0b0001111100 => return PpcOpcode::norx,
0b0010000111 => return PpcOpcode::stvebx,
0b0010010000 => return PpcOpcode::mtcrf,
0b0010010010 => return PpcOpcode::mtmsr,
0b0010010101 => return PpcOpcode::stdx,
0b0010010110 => return PpcOpcode::stwcx,
0b0010010111 => return PpcOpcode::stwx,
0b0010100111 => return PpcOpcode::stvehx,
0b0010110010 => return PpcOpcode::mtmsrd,
0b0010110101 => return PpcOpcode::stdux,
0b0010110111 => return PpcOpcode::stwux,
0b0011000111 => return PpcOpcode::stvewx,
0b0011010110 => return PpcOpcode::stdcx,
0b0011010111 => return PpcOpcode::stbx,
0b0011100111 => return PpcOpcode::stvx,
0b0011110110 => return PpcOpcode::dcbtst,
0b0011110111 => return PpcOpcode::stbux,
0b0100010110 => return PpcOpcode::dcbt,
0b0100010111 => return PpcOpcode::lhzx,
0b0100011100 => return PpcOpcode::eqvx,
0b0100110111 => return PpcOpcode::lhzux,
0b0100111100 => return PpcOpcode::xorx,
0b0101010011 => return PpcOpcode::mfspr,
0b0101010101 => return PpcOpcode::lwax,
0b0101010111 => return PpcOpcode::lhax,
0b0101100111 => return PpcOpcode::lvxl,
0b0101110011 => return PpcOpcode::mftb,
0b0101110101 => return PpcOpcode::lwaux,
0b0101110111 => return PpcOpcode::lhaux,
0b0110010111 => return PpcOpcode::sthx,
0b0110011100 => return PpcOpcode::orcx,
0b0110110111 => return PpcOpcode::sthux,
0b0110111100 => return PpcOpcode::orx,
0b0111010011 => return PpcOpcode::mtspr,
0b0111010110 => return PpcOpcode::dcbi,
0b0111011100 => return PpcOpcode::nandx,
0b0111100111 => return PpcOpcode::stvxl,
0b1000000000 => return PpcOpcode::mcrxr,
0b1000000111 => return PpcOpcode::lvlx,
0b1000010100 => return PpcOpcode::ldbrx,
0b1000010101 => return PpcOpcode::lswx,
0b1000010110 => return PpcOpcode::lwbrx,
0b1000010111 => return PpcOpcode::lfsx,
0b1000011000 => return PpcOpcode::srwx,
0b1000011011 => return PpcOpcode::srdx,
0b1000100111 => return PpcOpcode::lvrx,
0b1000110111 => return PpcOpcode::lfsux,
0b1001010101 => return PpcOpcode::lswi,
0b1001010110 => return PpcOpcode::sync,
0b1001010111 => return PpcOpcode::lfdx,
0b1001110111 => return PpcOpcode::lfdux,
0b1010000111 => return PpcOpcode::stvlx,
0b1010010100 => return PpcOpcode::stdbrx,
0b1010010101 => return PpcOpcode::stswx,
0b1010010110 => return PpcOpcode::stwbrx,
0b1010010111 => return PpcOpcode::stfsx,
0b1010100111 => return PpcOpcode::stvrx,
0b1010110111 => return PpcOpcode::stfsux,
0b1011010101 => return PpcOpcode::stswi,
0b1011010111 => return PpcOpcode::stfdx,
0b1011110111 => return PpcOpcode::stfdux,
0b1100000111 => return PpcOpcode::lvlxl,
0b1100010110 => return PpcOpcode::lhbrx,
0b1100011000 => return PpcOpcode::srawx,
0b1100011010 => return PpcOpcode::sradx,
0b1100100111 => return PpcOpcode::lvrxl,
0b1100111000 => return PpcOpcode::srawix,
0b1101010110 => return PpcOpcode::eieio,
0b1110000111 => return PpcOpcode::stvlxl,
0b1110010110 => return PpcOpcode::sthbrx,
0b1110011010 => return PpcOpcode::extshx,
0b1110100111 => return PpcOpcode::stvrxl,
0b1110111010 => return PpcOpcode::extsbx,
0b1111010110 => return PpcOpcode::icbi,
0b1111010111 => return PpcOpcode::stfiwx,
0b1111011010 => return PpcOpcode::extswx,
_ => {}
}
// Arithmetic op31 (bits 22-30)
let key2 = extract_bits(code, 22, 30);
match key2 {
0b000001000 => return PpcOpcode::subfcx,
0b000001001 => return PpcOpcode::mulhdux,
0b000001010 => return PpcOpcode::addcx,
0b000001011 => return PpcOpcode::mulhwux,
0b000101000 => return PpcOpcode::subfx,
0b001001001 => return PpcOpcode::mulhdx,
0b001001011 => return PpcOpcode::mulhwx,
0b001101000 => return PpcOpcode::negx,
0b010001000 => return PpcOpcode::subfex,
0b010001010 => return PpcOpcode::addex,
0b011001000 => return PpcOpcode::subfzex,
0b011001010 => return PpcOpcode::addzex,
0b011101000 => return PpcOpcode::subfmex,
0b011101001 => return PpcOpcode::mulldx,
0b011101010 => return PpcOpcode::addmex,
0b011101011 => return PpcOpcode::mullwx,
0b100001010 => return PpcOpcode::addx,
0b111001001 => return PpcOpcode::divdux,
0b111001011 => return PpcOpcode::divwux,
0b111101001 => return PpcOpcode::divdx,
0b111101011 => return PpcOpcode::divwx,
_ => {}
}
// dcbz/dcbz128 special case
let key3 = (extract_bits(code, 6, 10) << 20) | (extract_bits(code, 21, 30));
match key3 {
0b0000000000000001111110110 => return PpcOpcode::dcbz,
0b0000100000000001111110110 => return PpcOpcode::dcbz128,
_ => {}
}
PpcOpcode::Invalid
}
fn decode_op63(code: u32) -> PpcOpcode {
// Primary op63 table (bits 21-30)
match extract_bits(code, 21, 30) {
0b0000000000 => return PpcOpcode::fcmpu,
0b0000001100 => return PpcOpcode::frspx,
0b0000001110 => return PpcOpcode::fctiwx,
0b0000001111 => return PpcOpcode::fctiwzx,
0b0000100000 => return PpcOpcode::fcmpo,
0b0000100110 => return PpcOpcode::mtfsb1x,
0b0000101000 => return PpcOpcode::fnegx,
0b0001000000 => return PpcOpcode::mcrfs,
0b0001000110 => return PpcOpcode::mtfsb0x,
0b0001001000 => return PpcOpcode::fmrx,
0b0010000110 => return PpcOpcode::mtfsfix,
0b0010001000 => return PpcOpcode::fnabsx,
0b0100001000 => return PpcOpcode::fabsx,
0b1001000111 => return PpcOpcode::mffsx,
0b1011000111 => return PpcOpcode::mtfsfx,
0b1100101110 => return PpcOpcode::fctidx,
0b1100101111 => return PpcOpcode::fctidzx,
0b1101001110 => return PpcOpcode::fcfidx,
_ => {}
}
// FPU arithmetic (bits 26-30)
match extract_bits(code, 26, 30) {
0b10010 => PpcOpcode::fdivx,
0b10100 => PpcOpcode::fsubx,
0b10101 => PpcOpcode::faddx,
0b10110 => PpcOpcode::fsqrtx,
0b10111 => PpcOpcode::fselx,
0b11001 => PpcOpcode::fmulx,
0b11010 => PpcOpcode::frsqrtex,
0b11100 => PpcOpcode::fmsubx,
0b11101 => PpcOpcode::fmaddx,
0b11110 => PpcOpcode::fnmsubx,
0b11111 => PpcOpcode::fnmaddx,
_ => PpcOpcode::Invalid,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_decode_addi() {
// addi r3, r1, 0x10 => opcode 14, rD=3, rA=1, SIMM=0x10
let raw: u32 = (14 << 26) | (3 << 21) | (1 << 16) | 0x10;
let instr = decode(raw, 0);
assert_eq!(instr.opcode, PpcOpcode::addi);
assert_eq!(instr.rd(), 3);
assert_eq!(instr.ra(), 1);
assert_eq!(instr.simm16(), 0x10);
}
#[test]
fn test_decode_lwz() {
// lwz r5, 0x20(r1) => opcode 32
let raw: u32 = (32 << 26) | (5 << 21) | (1 << 16) | 0x20;
let instr = decode(raw, 0);
assert_eq!(instr.opcode, PpcOpcode::lwz);
assert_eq!(instr.rd(), 5);
assert_eq!(instr.ra(), 1);
assert_eq!(instr.d(), 0x20);
}
#[test]
fn test_decode_branch() {
// b +0x100 => opcode 18, LI=0x40 (shifted left 2 = 0x100), AA=0, LK=0
let raw: u32 = (18 << 26) | (0x40 << 2);
let instr = decode(raw, 0);
assert_eq!(instr.opcode, PpcOpcode::bx);
assert_eq!(instr.li(), 0x100);
assert!(!instr.aa());
assert!(!instr.lk());
}
#[test]
fn test_decode_stw() {
// stw r7, 0x8(r2)
let raw: u32 = (36 << 26) | (7 << 21) | (2 << 16) | 0x8;
let instr = decode(raw, 0);
assert_eq!(instr.opcode, PpcOpcode::stw);
assert_eq!(instr.rs(), 7);
assert_eq!(instr.ra(), 2);
}
#[test]
fn test_decode_ori_nop() {
// ori r0, r0, 0 = NOP
let raw: u32 = 24 << 26;
let instr = decode(raw, 0);
assert_eq!(instr.opcode, PpcOpcode::ori);
}
#[test]
fn test_extract_bits() {
assert_eq!(extract_bits(0xFFFF_FFFF, 0, 5), 0x3F);
assert_eq!(extract_bits(0x8000_0000, 0, 0), 1);
assert_eq!(extract_bits(0x0000_0001, 31, 31), 1);
}
}

View File

@@ -0,0 +1,276 @@
use crate::decoder::DecodedInstr;
use crate::opcode::PpcOpcode;
use std::fmt::Write;
/// Disassemble a decoded instruction into PPC assembly text.
pub fn disassemble(instr: &DecodedInstr) -> String {
let mut out = String::new();
match instr.opcode {
// Branch instructions
PpcOpcode::bx => {
let target = if instr.aa() {
instr.li() as u32
} else {
instr.addr.wrapping_add(instr.li() as u32)
};
let mnemonic = if instr.lk() { "bl" } else { "b" };
write!(out, "{} 0x{:08X}", mnemonic, target).unwrap();
}
PpcOpcode::bcx => {
let bo = instr.bo();
let bi = instr.bi();
let target = if instr.aa() {
instr.bd() as u32
} else {
instr.addr.wrapping_add(instr.bd() as u32)
};
let mnemonic = if instr.lk() { "bcl" } else { "bc" };
write!(out, "{} {},{},0x{:08X}", mnemonic, bo, bi, target).unwrap();
}
PpcOpcode::bclrx => {
let mnemonic = if instr.lk() { "bclrl" } else { "bclr" };
write!(out, "{} {},{}", mnemonic, instr.bo(), instr.bi()).unwrap();
}
PpcOpcode::bcctrx => {
let mnemonic = if instr.lk() { "bcctrl" } else { "bcctr" };
write!(out, "{} {},{}", mnemonic, instr.bo(), instr.bi()).unwrap();
}
// System call
PpcOpcode::sc => {
write!(out, "sc").unwrap();
}
// D-form load/store
PpcOpcode::lwz | PpcOpcode::lwzu | PpcOpcode::lbz | PpcOpcode::lbzu |
PpcOpcode::lhz | PpcOpcode::lhzu | PpcOpcode::lha | PpcOpcode::lhau |
PpcOpcode::lfs | PpcOpcode::lfsu | PpcOpcode::lfd | PpcOpcode::lfdu => {
write!(out, "{:?} r{},{}(r{})", instr.opcode, instr.rd(), instr.d(), instr.ra()).unwrap();
}
PpcOpcode::stw | PpcOpcode::stwu | PpcOpcode::stb | PpcOpcode::stbu |
PpcOpcode::sth | PpcOpcode::sthu |
PpcOpcode::stfs | PpcOpcode::stfsu | PpcOpcode::stfd | PpcOpcode::stfdu => {
write!(out, "{:?} r{},{}(r{})", instr.opcode, instr.rs(), instr.d(), instr.ra()).unwrap();
}
// D-form immediate ALU
PpcOpcode::addi | PpcOpcode::addis | PpcOpcode::addic | PpcOpcode::addicx |
PpcOpcode::subficx | PpcOpcode::mulli => {
write!(out, "{:?} r{},r{},{}", instr.opcode, instr.rd(), instr.ra(), instr.simm16()).unwrap();
}
// D-form immediate logical
PpcOpcode::ori | PpcOpcode::oris | PpcOpcode::xori | PpcOpcode::xoris |
PpcOpcode::andix | PpcOpcode::andisx => {
write!(out, "{:?} r{},r{},0x{:04X}", instr.opcode, instr.ra(), instr.rs(), instr.uimm16()).unwrap();
}
// Compare
PpcOpcode::cmpi => {
write!(out, "cmp{}i cr{},r{},{}", if instr.l() { "d" } else { "w" },
instr.crfd(), instr.ra(), instr.simm16()).unwrap();
}
PpcOpcode::cmpli => {
write!(out, "cmpl{}i cr{},r{},0x{:04X}", if instr.l() { "d" } else { "w" },
instr.crfd(), instr.ra(), instr.uimm16()).unwrap();
}
PpcOpcode::cmp => {
write!(out, "cmp{} cr{},r{},r{}", if instr.l() { "d" } else { "w" },
instr.crfd(), instr.ra(), instr.rb()).unwrap();
}
PpcOpcode::cmpl => {
write!(out, "cmpl{} cr{},r{},r{}", if instr.l() { "d" } else { "w" },
instr.crfd(), instr.ra(), instr.rb()).unwrap();
}
// X-form ALU (3-register)
PpcOpcode::addx | PpcOpcode::addcx | PpcOpcode::addex | PpcOpcode::addzex |
PpcOpcode::addmex | PpcOpcode::subfx | PpcOpcode::subfcx | PpcOpcode::subfex |
PpcOpcode::subfzex | PpcOpcode::subfmex | PpcOpcode::negx |
PpcOpcode::mullwx | PpcOpcode::mulhwx | PpcOpcode::mulhwux |
PpcOpcode::divwx | PpcOpcode::divwux |
PpcOpcode::mulldx | PpcOpcode::mulhdx | PpcOpcode::mulhdux |
PpcOpcode::divdx | PpcOpcode::divdux => {
write!(out, "{:?} r{},r{},r{}", instr.opcode, instr.rd(), instr.ra(), instr.rb()).unwrap();
}
// X-form logical
PpcOpcode::andx | PpcOpcode::andcx | PpcOpcode::orx | PpcOpcode::orcx |
PpcOpcode::xorx | PpcOpcode::norx | PpcOpcode::nandx | PpcOpcode::eqvx => {
write!(out, "{:?} r{},r{},r{}", instr.opcode, instr.ra(), instr.rs(), instr.rb()).unwrap();
}
// Shift/rotate
PpcOpcode::slwx | PpcOpcode::srwx | PpcOpcode::srawx | PpcOpcode::sldx |
PpcOpcode::srdx | PpcOpcode::sradx => {
write!(out, "{:?} r{},r{},r{}", instr.opcode, instr.ra(), instr.rs(), instr.rb()).unwrap();
}
PpcOpcode::srawix => {
write!(out, "srawi r{},r{},{}", instr.ra(), instr.rs(), instr.sh()).unwrap();
}
PpcOpcode::sradix => {
write!(out, "sradi r{},r{},{}", instr.ra(), instr.rs(), instr.sh64()).unwrap();
}
// Rotate
PpcOpcode::rlwinmx => {
write!(out, "rlwinm r{},r{},{},{},{}", instr.ra(), instr.rs(), instr.sh(), instr.mb(), instr.me()).unwrap();
}
PpcOpcode::rlwimix => {
write!(out, "rlwimi r{},r{},{},{},{}", instr.ra(), instr.rs(), instr.sh(), instr.mb(), instr.me()).unwrap();
}
PpcOpcode::rlwnmx => {
write!(out, "rlwnm r{},r{},r{},{},{}", instr.ra(), instr.rs(), instr.rb(), instr.mb(), instr.me()).unwrap();
}
// Special register moves
PpcOpcode::mfspr => {
let spr_name = match instr.spr() {
1 => "xer",
8 => "lr",
9 => "ctr",
268 => "tbl",
269 => "tbu",
_ => "",
};
if spr_name.is_empty() {
write!(out, "mfspr r{},{}", instr.rd(), instr.spr()).unwrap();
} else {
write!(out, "mf{} r{}", spr_name, instr.rd()).unwrap();
}
}
PpcOpcode::mtspr => {
let spr_name = match instr.spr() {
1 => "xer",
8 => "lr",
9 => "ctr",
_ => "",
};
if spr_name.is_empty() {
write!(out, "mtspr {},r{}", instr.spr(), instr.rs()).unwrap();
} else {
write!(out, "mt{} r{}", spr_name, instr.rs()).unwrap();
}
}
PpcOpcode::mfcr => {
write!(out, "mfcr r{}", instr.rd()).unwrap();
}
PpcOpcode::mtcrf => {
write!(out, "mtcrf 0x{:02X},r{}", instr.crm(), instr.rs()).unwrap();
}
// Extend
PpcOpcode::extsbx => write!(out, "extsb r{},r{}", instr.ra(), instr.rs()).unwrap(),
PpcOpcode::extshx => write!(out, "extsh r{},r{}", instr.ra(), instr.rs()).unwrap(),
PpcOpcode::extswx => write!(out, "extsw r{},r{}", instr.ra(), instr.rs()).unwrap(),
PpcOpcode::cntlzwx => write!(out, "cntlzw r{},r{}", instr.ra(), instr.rs()).unwrap(),
PpcOpcode::cntlzdx => write!(out, "cntlzd r{},r{}", instr.ra(), instr.rs()).unwrap(),
// X-form load/store
PpcOpcode::lwzx | PpcOpcode::lwzux | PpcOpcode::lbzx | PpcOpcode::lbzux |
PpcOpcode::lhzx | PpcOpcode::lhzux | PpcOpcode::lhax | PpcOpcode::lhaux |
PpcOpcode::lwax | PpcOpcode::lwaux | PpcOpcode::ldx | PpcOpcode::ldux |
PpcOpcode::lfsx | PpcOpcode::lfsux | PpcOpcode::lfdx | PpcOpcode::lfdux |
PpcOpcode::lwbrx | PpcOpcode::lhbrx | PpcOpcode::ldbrx |
PpcOpcode::lwarx | PpcOpcode::ldarx => {
write!(out, "{:?} r{},r{},r{}", instr.opcode, instr.rd(), instr.ra(), instr.rb()).unwrap();
}
PpcOpcode::stwx | PpcOpcode::stwux | PpcOpcode::stbx | PpcOpcode::stbux |
PpcOpcode::sthx | PpcOpcode::sthux | PpcOpcode::stdx | PpcOpcode::stdux |
PpcOpcode::stfsx | PpcOpcode::stfsux | PpcOpcode::stfdx | PpcOpcode::stfdux |
PpcOpcode::stwbrx | PpcOpcode::sthbrx | PpcOpcode::stdbrx |
PpcOpcode::stwcx | PpcOpcode::stdcx | PpcOpcode::stfiwx => {
write!(out, "{:?} r{},r{},r{}", instr.opcode, instr.rs(), instr.ra(), instr.rb()).unwrap();
}
// Cache/sync ops (no-ops for interpreter)
PpcOpcode::dcbf | PpcOpcode::dcbi | PpcOpcode::dcbst |
PpcOpcode::dcbt | PpcOpcode::dcbtst | PpcOpcode::icbi => {
write!(out, "{:?} r{},r{}", instr.opcode, instr.ra(), instr.rb()).unwrap();
}
PpcOpcode::dcbz | PpcOpcode::dcbz128 => {
write!(out, "{:?} r{},r{}", instr.opcode, instr.ra(), instr.rb()).unwrap();
}
PpcOpcode::sync | PpcOpcode::eieio | PpcOpcode::isync => {
write!(out, "{:?}", instr.opcode).unwrap();
}
// Load/store multiple
PpcOpcode::lmw => write!(out, "lmw r{},{}(r{})", instr.rd(), instr.d(), instr.ra()).unwrap(),
PpcOpcode::stmw => write!(out, "stmw r{},{}(r{})", instr.rs(), instr.d(), instr.ra()).unwrap(),
// DS-form loads/stores
PpcOpcode::ld | PpcOpcode::ldu | PpcOpcode::lwa => {
write!(out, "{:?} r{},{}(r{})", instr.opcode, instr.rd(), instr.ds(), instr.ra()).unwrap();
}
PpcOpcode::std | PpcOpcode::stdu => {
write!(out, "{:?} r{},{}(r{})", instr.opcode, instr.rs(), instr.ds(), instr.ra()).unwrap();
}
// CR logical ops
PpcOpcode::crand | PpcOpcode::crandc | PpcOpcode::creqv | PpcOpcode::crnand |
PpcOpcode::crnor | PpcOpcode::cror | PpcOpcode::crorc | PpcOpcode::crxor => {
write!(out, "{:?} {},{},{}", instr.opcode, instr.crbd(), instr.crba(), instr.crbb()).unwrap();
}
PpcOpcode::mcrf => {
write!(out, "mcrf cr{},cr{}", instr.crfd(), instr.crfs()).unwrap();
}
// Trap
PpcOpcode::tdi => write!(out, "tdi {},r{},{}", instr.rd(), instr.ra(), instr.simm16()).unwrap(),
PpcOpcode::twi => write!(out, "twi {},r{},{}", instr.rd(), instr.ra(), instr.simm16()).unwrap(),
PpcOpcode::td => write!(out, "td {},r{},r{}", instr.rd(), instr.ra(), instr.rb()).unwrap(),
PpcOpcode::tw => write!(out, "tw {},r{},r{}", instr.rd(), instr.ra(), instr.rb()).unwrap(),
// Default: just print opcode and raw hex
_ => {
write!(out, "{:?} [{:08X}]", instr.opcode, instr.raw).unwrap();
}
}
out
}
/// Disassemble a range of instructions from a byte slice.
pub fn disassemble_block(data: &[u8], base_addr: u32, count: usize) -> Vec<(u32, String)> {
let mut result = Vec::new();
for i in 0..count {
let offset = i * 4;
if offset + 4 > data.len() {
break;
}
let raw = u32::from_be_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
]);
let addr = base_addr + offset as u32;
let instr = crate::decode(raw, addr);
let text = disassemble(&instr);
result.push((addr, text));
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::decoder::decode;
#[test]
fn test_disasm_nop() {
// ori r0, r0, 0 = NOP
let instr = decode(0x60000000, 0);
let text = disassemble(&instr);
assert!(text.contains("ori"), "Expected 'ori', got: {}", text);
}
#[test]
fn test_disasm_addi() {
let raw = (14u32 << 26) | (3 << 21) | (1 << 16) | 16;
let instr = decode(raw, 0);
let text = disassemble(&instr);
assert!(text.contains("addi"), "Got: {}", text);
assert!(text.contains("r3"), "Got: {}", text);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,9 @@
pub mod context;
pub mod decoder;
pub mod disasm;
pub mod interpreter;
pub mod opcode;
pub use context::PpcContext;
pub use decoder::decode;
pub use opcode::PpcOpcode;

View File

@@ -0,0 +1,196 @@
/// All PPC opcodes supported by the Xbox 360, including VMX128 extensions.
/// Directly mirrors the C++ PPCOpcode enum from ppc_opcode.h.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u32)]
#[allow(non_camel_case_types)]
pub enum PpcOpcode {
// ALU
addcx, addex, addi, addic, addicx, addis, addmex, addx, addzex,
andcx, andisx, andix, andx,
// Branch
bcctrx, bclrx, bcx, bx,
// Compare
cmp, cmpi, cmpl, cmpli,
// Count leading zeros
cntlzdx, cntlzwx,
// Condition register
crand, crandc, creqv, crnand, crnor, cror, crorc, crxor,
// Data cache
dcbf, dcbi, dcbst, dcbt, dcbtst, dcbz, dcbz128,
// Division
divdux, divdx, divwux, divwx,
// Sync/barrier
eieio,
// Logical
eqvx, extsbx, extshx, extswx,
// FPU
fabsx, faddsx, faddx, fcfidx, fcmpo, fcmpu, fctidx, fctidzx, fctiwx, fctiwzx,
fdivsx, fdivx, fmaddsx, fmaddx, fmrx, fmsubsx, fmsubx, fmulsx, fmulx,
fnabsx, fnegx, fnmaddsx, fnmaddx, fnmsubsx, fnmsubx, fresx, frspx, frsqrtex,
fselx, fsqrtsx, fsqrtx, fsubsx, fsubx,
// Instruction cache
icbi, isync,
// Load byte
lbz, lbzu, lbzux, lbzx,
// Load doubleword
ld, ldarx, ldbrx, ldu, ldux, ldx,
// Load float
lfd, lfdu, lfdux, lfdx, lfs, lfsu, lfsux, lfsx,
// Load halfword
lha, lhau, lhaux, lhax, lhbrx, lhz, lhzu, lhzux, lhzx,
// Load multiple/string
lmw, lswi, lswx,
// Load vector
lvebx, lvehx, lvewx, lvewx128, lvlx, lvlx128, lvlxl, lvlxl128,
lvrx, lvrx128, lvrxl, lvrxl128,
lvsl, lvsl128, lvsr, lvsr128,
lvx, lvx128, lvxl, lvxl128,
// Load word
lwa, lwarx, lwaux, lwax, lwbrx, lwz, lwzu, lwzux, lwzx,
// Move CR
mcrf, mcrfs, mcrxr,
// Move from special
mfcr, mffsx, mfmsr, mfspr, mftb, mfvscr,
// Move to special
mtcrf, mtfsb0x, mtfsb1x, mtfsfix, mtfsfx, mtmsr, mtmsrd, mtspr, mtvscr,
// Multiply
mulhdux, mulhdx, mulhwux, mulhwx, mulldx, mulli, mullwx,
// Logical
nandx, negx, norx, orcx, ori, oris, orx,
// Rotate
rldclx, rldcrx, rldiclx, rldicrx, rldicx, rldimix, rlwimix, rlwinmx, rlwnmx,
// System call
sc,
// Shift
sldx, slwx, sradix, sradx, srawix, srawx, srdx, srwx,
// Store byte
stb, stbu, stbux, stbx,
// Store doubleword
std, stdbrx, stdcx, stdu, stdux, stdx,
// Store float
stfd, stfdu, stfdux, stfdx, stfiwx, stfs, stfsu, stfsux, stfsx,
// Store halfword
sth, sthbrx, sthu, sthux, sthx,
// Store multiple/string
stmw, stswi, stswx,
// Store vector
stvebx, stvehx, stvewx, stvewx128, stvlx, stvlx128, stvlxl, stvlxl128,
stvrx, stvrx128, stvrxl, stvrxl128,
stvx, stvx128, stvxl, stvxl128,
// Store word
stw, stwbrx, stwcx, stwu, stwux, stwx,
// Subtract
subfcx, subfex, subficx, subfmex, subfx, subfzex,
// Sync
sync,
// Trap
td, tdi, tw, twi,
// VMX integer
vaddcuw, vaddfp, vaddfp128, vaddsbs, vaddshs, vaddsws,
vaddubm, vaddubs, vadduhm, vadduhs, vadduwm, vadduws,
vand, vand128, vandc, vandc128,
vavgsb, vavgsh, vavgsw, vavgub, vavguh, vavguw,
vcfpsxws128, vcfpuxws128, vcfsx, vcfux,
vcmpbfp, vcmpbfp128, vcmpeqfp, vcmpeqfp128,
vcmpequb, vcmpequh, vcmpequw, vcmpequw128,
vcmpgefp, vcmpgefp128, vcmpgtfp, vcmpgtfp128,
vcmpgtsb, vcmpgtsh, vcmpgtsw, vcmpgtub, vcmpgtuh, vcmpgtuw,
vcsxwfp128, vctsxs, vctuxs, vcuxwfp128,
vexptefp, vexptefp128, vlogefp, vlogefp128,
vmaddcfp128, vmaddfp, vmaddfp128,
vmaxfp, vmaxfp128, vmaxsb, vmaxsh, vmaxsw, vmaxub, vmaxuh, vmaxuw,
vmhaddshs, vmhraddshs,
vminfp, vminfp128, vminsb, vminsh, vminsw, vminub, vminuh, vminuw,
vmladduhm,
vmrghb, vmrghh, vmrghw, vmrghw128, vmrglb, vmrglh, vmrglw, vmrglw128,
vmsum3fp128, vmsum4fp128,
vmsummbm, vmsumshm, vmsumshs, vmsumubm, vmsumuhm, vmsumuhs,
vmulesb, vmulesh, vmuleub, vmuleuh, vmulfp128,
vmulosb, vmulosh, vmuloub, vmulouh,
vnmsubfp, vnmsubfp128, vnor, vnor128,
vor, vor128,
vperm, vperm128, vpermwi128, vpkd3d128,
vpkpx, vpkshss, vpkshss128, vpkshus, vpkshus128,
vpkswss, vpkswss128, vpkswus, vpkswus128,
vpkuhum, vpkuhum128, vpkuhus, vpkuhus128,
vpkuwum, vpkuwum128, vpkuwus, vpkuwus128,
vrefp, vrefp128,
vrfim, vrfim128, vrfin, vrfin128, vrfip, vrfip128, vrfiz, vrfiz128,
vrlb, vrlh, vrlimi128, vrlw, vrlw128,
vrsqrtefp, vrsqrtefp128,
vsel, vsel128,
vsl, vslb, vsldoi, vsldoi128, vslh, vslo, vslo128, vslw, vslw128,
vspltb, vsplth, vspltisb, vspltish, vspltisw, vspltisw128, vspltw, vspltw128,
vsr, vsrab, vsrah, vsraw, vsraw128, vsrb, vsrh, vsro, vsro128, vsrw, vsrw128,
vsubcuw, vsubfp, vsubfp128, vsubsbs, vsubshs, vsubsws,
vsububm, vsububs, vsubuhm, vsubuhs, vsubuwm, vsubuws,
vsum2sws, vsum4sbs, vsum4shs, vsum4ubs, vsumsws,
vupkd3d128, vupkhpx, vupkhsb, vupkhsb128, vupkhsh,
vupklpx, vupklsb, vupklsb128, vupklsh,
vxor, vxor128,
// XOR immediate
xori, xoris, xorx,
// Invalid
Invalid,
}
impl PpcOpcode {
/// Returns true if this opcode is a branch instruction.
pub fn is_branch(&self) -> bool {
matches!(self, Self::bx | Self::bcx | Self::bclrx | Self::bcctrx)
}
/// Returns true if this opcode is a system call.
pub fn is_syscall(&self) -> bool {
matches!(self, Self::sc)
}
/// Returns true if this is a load instruction.
pub fn is_load(&self) -> bool {
matches!(self,
Self::lbz | Self::lbzu | Self::lbzux | Self::lbzx |
Self::lhz | Self::lhzu | Self::lhzux | Self::lhzx |
Self::lha | Self::lhau | Self::lhaux | Self::lhax |
Self::lwz | Self::lwzu | Self::lwzux | Self::lwzx |
Self::lwa | Self::lwax | Self::lwaux |
Self::ld | Self::ldu | Self::ldux | Self::ldx |
Self::lfs | Self::lfsu | Self::lfsux | Self::lfsx |
Self::lfd | Self::lfdu | Self::lfdux | Self::lfdx |
Self::lhbrx | Self::lwbrx | Self::ldbrx |
Self::lmw | Self::lswi | Self::lswx |
Self::lwarx | Self::ldarx
)
}
/// Returns true if this is a store instruction.
pub fn is_store(&self) -> bool {
matches!(self,
Self::stb | Self::stbu | Self::stbux | Self::stbx |
Self::sth | Self::sthu | Self::sthux | Self::sthx |
Self::stw | Self::stwu | Self::stwux | Self::stwx |
Self::std | Self::stdu | Self::stdux | Self::stdx |
Self::stfs | Self::stfsu | Self::stfsux | Self::stfsx |
Self::stfd | Self::stfdu | Self::stfdux | Self::stfdx |
Self::sthbrx | Self::stwbrx | Self::stdbrx |
Self::stmw | Self::stswi | Self::stswx |
Self::stwcx | Self::stdcx | Self::stfiwx
)
}
pub fn name(&self) -> &'static str {
match self {
Self::Invalid => "invalid",
_ => {
// Use debug formatting to get the variant name
// This is a placeholder - in practice we'd have a lookup table
"?"
}
}
}
}
impl std::fmt::Display for PpcOpcode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Debug::fmt(self, f)
}
}

View File

@@ -0,0 +1,12 @@
[package]
name = "xenia-debugger"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
xenia-types = { workspace = true }
xenia-memory = { workspace = true }
xenia-cpu = { workspace = true }
tracing = { workspace = true }
thiserror = { workspace = true }

View File

@@ -0,0 +1,7 @@
/// A code breakpoint at a specific guest address.
#[derive(Debug, Clone)]
pub struct Breakpoint {
pub addr: u32,
pub enabled: bool,
pub condition: Option<String>,
}

View File

@@ -0,0 +1,125 @@
pub mod breakpoint;
pub mod trace;
use std::collections::HashMap;
use xenia_cpu::context::PpcContext;
use xenia_memory::MemoryAccess;
pub use breakpoint::Breakpoint;
pub use trace::TraceEntry;
/// The debugger. Hooks into every instruction step for observation.
pub struct Debugger {
pub breakpoints: HashMap<u32, Breakpoint>,
pub trace_log: Vec<TraceEntry>,
pub trace_enabled: bool,
pub max_trace_entries: usize,
pub paused: bool,
pub step_mode: StepMode,
break_pending: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StepMode {
/// Run freely until breakpoint or pause
Run,
/// Execute one instruction then pause
StepInto,
/// Run but break after current function returns (when LR changes)
StepOver { return_addr: u32 },
}
impl Debugger {
pub fn new() -> Self {
Self {
breakpoints: HashMap::new(),
trace_log: Vec::new(),
trace_enabled: true,
max_trace_entries: 100_000,
paused: true, // Start paused for debugging
step_mode: StepMode::StepInto,
break_pending: false,
}
}
/// Called before each instruction executes.
pub fn pre_step(&mut self, ctx: &PpcContext, _mem: &dyn MemoryAccess) {
// Check breakpoints
if let Some(bp) = self.breakpoints.get(&ctx.pc) {
if bp.enabled {
self.break_pending = true;
tracing::info!("Breakpoint hit at {:#010x}", ctx.pc);
}
}
}
/// Called after each instruction executes.
pub fn post_step(&mut self, ctx: &PpcContext, _mem: &dyn MemoryAccess) {
// Log to trace
if self.trace_enabled {
if self.trace_log.len() >= self.max_trace_entries {
self.trace_log.remove(0);
}
self.trace_log.push(TraceEntry {
pc: ctx.pc,
cycle: ctx.cycle_count,
gpr_snapshot: [ctx.gpr[0], ctx.gpr[1], ctx.gpr[3], ctx.gpr[4]],
lr: ctx.lr,
});
}
// Handle step mode
match self.step_mode {
StepMode::StepInto => {
self.break_pending = true;
}
StepMode::StepOver { return_addr } => {
if ctx.pc == return_addr {
self.break_pending = true;
}
}
StepMode::Run => {}
}
}
/// Should we break execution?
pub fn should_break(&self) -> bool {
self.break_pending || self.paused
}
/// Add a breakpoint at the given address.
pub fn add_breakpoint(&mut self, addr: u32) {
self.breakpoints.insert(addr, Breakpoint { addr, enabled: true, condition: None });
}
/// Remove a breakpoint.
pub fn remove_breakpoint(&mut self, addr: u32) {
self.breakpoints.remove(&addr);
}
/// Continue execution.
pub fn continue_execution(&mut self) {
self.paused = false;
self.break_pending = false;
self.step_mode = StepMode::Run;
}
/// Step one instruction.
pub fn step_into(&mut self) {
self.paused = false;
self.break_pending = false;
self.step_mode = StepMode::StepInto;
}
/// Clear break state after handling.
pub fn acknowledge_break(&mut self) {
self.break_pending = false;
self.paused = true;
}
}
impl Default for Debugger {
fn default() -> Self {
Self::new()
}
}

View File

@@ -0,0 +1,9 @@
/// A single entry in the instruction trace log.
#[derive(Debug, Clone)]
pub struct TraceEntry {
pub pc: u32,
pub cycle: u64,
/// Snapshot of key GPRs: [r0, r1(sp), r3(arg0/retval), r4(arg1)]
pub gpr_snapshot: [u64; 4],
pub lr: u64,
}

View File

@@ -0,0 +1,13 @@
[package]
name = "xenia-gpu"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
xenia-types = { workspace = true }
xenia-memory = { workspace = true }
tracing = { workspace = true }
thiserror = { workspace = true }
anyhow = { workspace = true }
byteorder = { workspace = true }

View File

@@ -0,0 +1,17 @@
/// PM4 command processor stub.
/// Will parse the GPU command ring buffer and dispatch to render operations.
pub struct CommandProcessor {
pub enabled: bool,
}
impl CommandProcessor {
pub fn new() -> Self {
Self { enabled: false }
}
}
impl Default for CommandProcessor {
fn default() -> Self {
Self::new()
}
}

View File

@@ -0,0 +1,21 @@
pub mod command_processor;
pub mod register_file;
/// Stub GPU system for initial implementation.
pub struct GpuSystem {
pub register_file: register_file::RegisterFile,
}
impl GpuSystem {
pub fn new() -> Self {
Self {
register_file: register_file::RegisterFile::new(),
}
}
}
impl Default for GpuSystem {
fn default() -> Self {
Self::new()
}
}

View File

@@ -0,0 +1,28 @@
/// Xenos GPU register file. 0x6000 32-bit registers.
pub struct RegisterFile {
pub regs: Vec<u32>,
}
impl RegisterFile {
pub fn new() -> Self {
Self {
regs: vec![0u32; 0x6000],
}
}
pub fn read(&self, index: u32) -> u32 {
self.regs.get(index as usize).copied().unwrap_or(0)
}
pub fn write(&mut self, index: u32, value: u32) {
if let Some(r) = self.regs.get_mut(index as usize) {
*r = value;
}
}
}
impl Default for RegisterFile {
fn default() -> Self {
Self::new()
}
}

View File

@@ -0,0 +1,10 @@
[package]
name = "xenia-hid"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
xenia-types = { workspace = true }
tracing = { workspace = true }
thiserror = { workspace = true }

View File

@@ -0,0 +1,47 @@
/// Human input device system stub.
pub struct InputSystem {
pub gamepad: GamepadState,
}
#[derive(Default, Clone, Copy)]
pub struct GamepadState {
pub buttons: u16,
pub left_trigger: u8,
pub right_trigger: u8,
pub left_stick_x: i16,
pub left_stick_y: i16,
pub right_stick_x: i16,
pub right_stick_y: i16,
}
/// Xbox 360 button flags
pub mod buttons {
pub const DPAD_UP: u16 = 0x0001;
pub const DPAD_DOWN: u16 = 0x0002;
pub const DPAD_LEFT: u16 = 0x0004;
pub const DPAD_RIGHT: u16 = 0x0008;
pub const START: u16 = 0x0010;
pub const BACK: u16 = 0x0020;
pub const LEFT_THUMB: u16 = 0x0040;
pub const RIGHT_THUMB: u16 = 0x0080;
pub const LEFT_SHOULDER: u16 = 0x0100;
pub const RIGHT_SHOULDER: u16 = 0x0200;
pub const A: u16 = 0x1000;
pub const B: u16 = 0x2000;
pub const X: u16 = 0x4000;
pub const Y: u16 = 0x8000;
}
impl InputSystem {
pub fn new() -> Self {
Self {
gamepad: GamepadState::default(),
}
}
}
impl Default for InputSystem {
fn default() -> Self {
Self::new()
}
}

View File

@@ -0,0 +1,13 @@
[package]
name = "xenia-kernel"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
xenia-types = { workspace = true }
xenia-memory = { workspace = true }
xenia-cpu = { workspace = true }
tracing = { workspace = true }
thiserror = { workspace = true }
anyhow = { workspace = true }

View File

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

View File

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

View File

@@ -0,0 +1,90 @@
use std::collections::HashMap;
use xenia_cpu::PpcContext;
use xenia_memory::GuestMemory;
/// Function signature for HLE kernel exports.
pub type KernelExportFn = fn(&mut PpcContext, &mut GuestMemory, &mut KernelState);
/// Module identifier for kernel exports.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ModuleId {
Xboxkrnl,
Xam,
Xbdm,
}
/// Central kernel state tracking all guest OS state.
pub struct KernelState {
exports: HashMap<(ModuleId, u32), (&'static str, KernelExportFn)>,
next_handle: u32,
tls_slots: HashMap<u32, u64>,
}
impl KernelState {
pub fn new() -> Self {
let mut state = Self {
exports: HashMap::new(),
next_handle: 0x1000,
tls_slots: HashMap::new(),
};
crate::exports::register_exports(&mut state);
state
}
pub fn register_export(
&mut self,
module: ModuleId,
ordinal: u32,
name: &'static str,
func: KernelExportFn,
) {
self.exports.insert((module, ordinal), (name, func));
}
pub fn call_export(
&mut self,
module: ModuleId,
ordinal: u32,
ctx: &mut PpcContext,
mem: &mut GuestMemory,
) -> bool {
if let Some(&(name, func)) = self.exports.get(&(module, ordinal)) {
tracing::info!(
"Kernel call: {:?}:{:#x} ({}) args=[{:#x}, {:#x}, {:#x}, {:#x}]",
module, ordinal, name,
ctx.gpr[3], ctx.gpr[4], ctx.gpr[5], ctx.gpr[6]
);
func(ctx, mem, self);
tracing::info!(" -> returned {:#x}", ctx.gpr[3]);
true
} else {
tracing::warn!(
"Unimplemented kernel export: {:?}:{:#x}",
module, ordinal
);
// Return 0 (STATUS_SUCCESS) by default for unimplemented calls
ctx.gpr[3] = 0;
false
}
}
pub fn alloc_handle(&mut self) -> u32 {
let h = self.next_handle;
self.next_handle += 4;
h
}
pub fn tls_get(&self, index: u32) -> u64 {
self.tls_slots.get(&index).copied().unwrap_or(0)
}
pub fn tls_set(&mut self, index: u32, value: u64) {
self.tls_slots.insert(index, value);
}
}
impl Default for KernelState {
fn default() -> Self {
Self::new()
}
}

View File

@@ -0,0 +1,17 @@
[package]
name = "xenia-memory"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
xenia-types = { workspace = true }
tracing = { workspace = true }
bitflags = { workspace = true }
thiserror = { workspace = true }
[target.'cfg(unix)'.dependencies]
libc = "0.2"
[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.59", features = ["Win32_System_Memory", "Win32_Foundation"] }

View File

@@ -0,0 +1,47 @@
/// Trait for all guest memory access. Every load/store goes through this,
/// enabling MMIO checking and debugger observation on every access.
/// This is the key abstraction that eliminates the need for MMIO exception handlers.
pub trait MemoryAccess {
fn read_u8(&self, addr: u32) -> u8;
fn read_u16(&self, addr: u32) -> u16;
fn read_u32(&self, addr: u32) -> u32;
fn read_u64(&self, addr: u32) -> u64;
fn read_f32(&self, addr: u32) -> f32 {
f32::from_bits(self.read_u32(addr))
}
fn read_f64(&self, addr: u32) -> f64 {
f64::from_bits(self.read_u64(addr))
}
fn write_u8(&mut self, addr: u32, val: u8);
fn write_u16(&mut self, addr: u32, val: u16);
fn write_u32(&mut self, addr: u32, val: u32);
fn write_u64(&mut self, addr: u32, val: u64);
fn write_f32(&mut self, addr: u32, val: f32) {
self.write_u32(addr, val.to_bits());
}
fn write_f64(&mut self, addr: u32, val: f64) {
self.write_u64(addr, val.to_bits());
}
/// Read a block of bytes from guest memory.
fn read_bytes(&self, addr: u32, buf: &mut [u8]) {
for (i, byte) in buf.iter_mut().enumerate() {
*byte = self.read_u8(addr.wrapping_add(i as u32));
}
}
/// Write a block of bytes to guest memory.
fn write_bytes(&mut self, addr: u32, buf: &[u8]) {
for (i, &byte) in buf.iter().enumerate() {
self.write_u8(addr.wrapping_add(i as u32), byte);
}
}
/// Get a direct host pointer for the given guest address.
/// Returns None if the address is invalid or in an MMIO region.
fn translate(&self, addr: u32) -> Option<*const u8>;
/// Get a mutable direct host pointer for the given guest address.
fn translate_mut(&mut self, addr: u32) -> Option<*mut u8>;
}

View File

@@ -0,0 +1,265 @@
use crate::access::MemoryAccess;
use crate::mmio::MmioRegion;
use crate::page_table::{AllocationState, MemoryProtect, PageEntry};
use crate::MemoryError;
const PAGE_SIZE: u32 = 4096;
/// Total guest address space: 4GB.
const GUEST_ADDRESS_SPACE: usize = 0x1_0000_0000;
/// Number of 4K pages in the 4GB address space.
const PAGE_COUNT: usize = GUEST_ADDRESS_SPACE / PAGE_SIZE as usize;
/// Physical memory mask (512MB physical address space).
const PHYSICAL_ADDR_MASK: u32 = 0x1FFF_FFFF;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HeapType {
GuestVirtual,
GuestXex,
GuestPhysical,
}
/// The core guest memory system. Manages a 4GB virtual address space
/// via mmap/VirtualAlloc, with page-level tracking and MMIO dispatch.
pub struct GuestMemory {
/// Host pointer to the base of the 4GB guest address space.
membase: *mut u8,
/// Page table tracking allocation state for each 4K page.
page_table: Vec<PageEntry>,
/// Registered MMIO regions (sorted by base address for binary search).
mmio_regions: Vec<MmioRegion>,
/// Whether the memory mapping is owned (should be unmapped on drop).
owned: bool,
}
unsafe impl Send for GuestMemory {}
unsafe impl Sync for GuestMemory {}
impl GuestMemory {
/// Create a new guest memory space by reserving a 4GB virtual address region.
pub fn new() -> Result<Self, MemoryError> {
let membase = crate::platform::reserve_address_space(GUEST_ADDRESS_SPACE)?;
Ok(Self {
membase,
page_table: vec![PageEntry::default(); PAGE_COUNT],
mmio_regions: Vec::new(),
owned: true,
})
}
/// Get the host base pointer for the guest address space.
pub fn membase(&self) -> *const u8 {
self.membase
}
/// Get a mutable host base pointer.
pub fn membase_mut(&mut self) -> *mut u8 {
self.membase
}
/// Translate a guest virtual address to a host pointer.
pub fn translate_virtual(&self, guest_addr: u32) -> *const u8 {
unsafe { self.membase.add(guest_addr as usize) }
}
/// Translate a guest virtual address to a mutable host pointer.
pub fn translate_virtual_mut(&mut self, guest_addr: u32) -> *mut u8 {
unsafe { self.membase.add(guest_addr as usize) }
}
/// Translate a guest physical address to a host pointer.
pub fn translate_physical(&self, guest_addr: u32) -> *const u8 {
let phys = guest_addr & PHYSICAL_ADDR_MASK;
unsafe { self.membase.add(phys as usize) }
}
/// Register an MMIO region.
pub fn add_mmio_region(&mut self, region: MmioRegion) {
let base = region.base_address;
let idx = self
.mmio_regions
.binary_search_by_key(&base, |r| r.base_address)
.unwrap_or_else(|i| i);
self.mmio_regions.insert(idx, region);
}
/// Check if an address is in a registered MMIO region.
fn find_mmio(&self, addr: u32) -> Option<&MmioRegion> {
self.mmio_regions.iter().find(|r| r.contains(addr))
}
/// Allocate a region in the guest address space.
pub fn alloc(
&mut self,
base: u32,
size: u32,
protect: MemoryProtect,
) -> Result<u32, MemoryError> {
let page_start = (base / PAGE_SIZE) as usize;
let page_count = ((size + PAGE_SIZE - 1) / PAGE_SIZE) as usize;
// Commit pages via platform
let host_ptr = unsafe { self.membase.add(base as usize) };
crate::platform::commit_memory(host_ptr, (page_count * PAGE_SIZE as usize) as usize)?;
// Update page table
for i in 0..page_count {
let idx = page_start + i;
if idx < self.page_table.len() {
let entry = &mut self.page_table[idx];
entry.set_base_address(page_start as u32);
entry.set_region_page_count(page_count as u32);
entry.set_allocation_protect(protect);
entry.set_current_protect(protect);
entry.set_state(AllocationState::RESERVE | AllocationState::COMMIT);
}
}
Ok(base)
}
/// Read a slice of bytes from guest memory (bypassing MMIO for bulk reads).
pub fn read_bulk(&self, addr: u32, buf: &mut [u8]) {
let ptr = self.translate_virtual(addr);
unsafe {
std::ptr::copy_nonoverlapping(ptr, buf.as_mut_ptr(), buf.len());
}
}
/// Write a slice of bytes to guest memory (bypassing MMIO for bulk writes).
pub fn write_bulk(&mut self, addr: u32, buf: &[u8]) {
let ptr = self.translate_virtual_mut(addr);
unsafe {
std::ptr::copy_nonoverlapping(buf.as_ptr(), ptr, buf.len());
}
}
/// Check if a guest address has been allocated/committed.
pub fn is_mapped(&self, addr: u32) -> bool {
let page = (addr / PAGE_SIZE) as usize;
if page >= self.page_table.len() {
return false;
}
self.page_table[page].state().contains(AllocationState::COMMIT)
}
/// Get a page table entry for a given address.
pub fn page_entry(&self, addr: u32) -> &PageEntry {
let page = (addr / PAGE_SIZE) as usize;
&self.page_table[page]
}
}
impl MemoryAccess for GuestMemory {
fn read_u8(&self, addr: u32) -> u8 {
if !self.is_mapped(addr) { return 0; }
let ptr = self.translate_virtual(addr);
unsafe { *ptr }
}
fn read_u16(&self, addr: u32) -> u16 {
if let Some(mmio) = self.find_mmio(addr) {
(mmio.read_callback)(addr) as u16
} else if !self.is_mapped(addr) {
0
} else {
let ptr = self.translate_virtual(addr) as *const [u8; 2];
u16::from_be_bytes(unsafe { *ptr })
}
}
fn read_u32(&self, addr: u32) -> u32 {
if let Some(mmio) = self.find_mmio(addr) {
(mmio.read_callback)(addr)
} else if !self.is_mapped(addr) {
0
} else {
let ptr = self.translate_virtual(addr) as *const [u8; 4];
u32::from_be_bytes(unsafe { *ptr })
}
}
fn read_u64(&self, addr: u32) -> u64 {
if let Some(mmio) = self.find_mmio(addr) {
let hi = (mmio.read_callback)(addr) as u64;
let lo = (mmio.read_callback)(addr.wrapping_add(4)) as u64;
(hi << 32) | lo
} else if !self.is_mapped(addr) {
0
} else {
let ptr = self.translate_virtual(addr) as *const [u8; 8];
u64::from_be_bytes(unsafe { *ptr })
}
}
fn write_u8(&mut self, addr: u32, val: u8) {
if !self.is_mapped(addr) { return; }
let ptr = self.translate_virtual_mut(addr);
unsafe { *ptr = val };
}
fn write_u16(&mut self, addr: u32, val: u16) {
if let Some(mmio) = self.find_mmio(addr) {
(mmio.write_callback)(addr, val as u32);
} else if !self.is_mapped(addr) {
return;
} else {
let ptr = self.translate_virtual_mut(addr);
unsafe {
std::ptr::copy_nonoverlapping(val.to_be_bytes().as_ptr(), ptr, 2);
}
}
}
fn write_u32(&mut self, addr: u32, val: u32) {
if let Some(mmio) = self.find_mmio(addr) {
(mmio.write_callback)(addr, val);
} else if !self.is_mapped(addr) {
return;
} else {
let ptr = self.translate_virtual_mut(addr);
unsafe {
std::ptr::copy_nonoverlapping(val.to_be_bytes().as_ptr(), ptr, 4);
}
}
}
fn write_u64(&mut self, addr: u32, val: u64) {
if let Some(mmio) = self.find_mmio(addr) {
(mmio.write_callback)(addr, (val >> 32) as u32);
(mmio.write_callback)(addr.wrapping_add(4), val as u32);
} else if !self.is_mapped(addr) {
return;
} else {
let ptr = self.translate_virtual_mut(addr);
unsafe {
std::ptr::copy_nonoverlapping(val.to_be_bytes().as_ptr(), ptr, 8);
}
}
}
fn translate(&self, addr: u32) -> Option<*const u8> {
if self.find_mmio(addr).is_some() || !self.is_mapped(addr) {
None
} else {
Some(self.translate_virtual(addr))
}
}
fn translate_mut(&mut self, addr: u32) -> Option<*mut u8> {
if self.find_mmio(addr).is_some() {
None
} else {
Some(self.translate_virtual_mut(addr))
}
}
}
impl Drop for GuestMemory {
fn drop(&mut self) {
if self.owned && !self.membase.is_null() {
unsafe {
crate::platform::release_address_space(self.membase, GUEST_ADDRESS_SPACE);
}
}
}
}

View File

@@ -0,0 +1,31 @@
pub mod access;
pub mod heap;
pub mod mmio;
pub mod page_table;
mod platform;
use thiserror::Error;
pub use access::MemoryAccess;
pub use heap::{GuestMemory, HeapType};
pub use mmio::MmioRegion;
pub use page_table::PageEntry;
#[derive(Debug, Error)]
pub enum MemoryError {
#[error("Failed to allocate guest address space: {0}")]
AllocationFailed(String),
#[error("Invalid guest address: {0:#010x}")]
InvalidAddress(u32),
#[error("MMIO access at {0:#010x}")]
MmioAccess(u32),
#[error("Protection violation at {0:#010x}")]
ProtectionViolation(u32),
#[error("Out of memory in heap {0:?}")]
OutOfMemory(HeapType),
}

Some files were not shown because too many files have changed in this diff Show More