snapshot: preserve the uncommitted Canary instrumentation (not authored here)
Some checks failed
Orchestrator / Commit Message Validation (push) Has been skipped
Orchestrator / Lint (push) Failing after 2m0s
Orchestrator / Windows (x86-64) (push) Has been skipped
Orchestrator / Linux (x86-64) (push) Has been skipped
Orchestrator / Create Release (push) Has been skipped

Working-tree snapshot taken 2026-07-28 so this work is not lost. To be clear
about provenance: NONE of this was written in the session that committed it —
it is pre-existing uncommitted work on phase-a-tracing, last committed 2026-07-09,
from the xenia-rs decoder/deadlock investigation. It is recorded verbatim, not
reviewed and not tested.

Contents: 24 tracked modifications (ppc_emit_memory, xex_module, object_table,
shim_utils, xboxkrnl threading/rtl, xevent/xobject/xthread, memory) plus the
untracked probe sources audit_68_host_mem_watch, audit_69_event_signal_watch,
audit_70_semaphore_release_watch, phase_b_snapshot, and cmake/toolchains.

Deliberately excluded: build-cross/ (51 GB of build output) and
vkd3d-proton.cache. Note third_party/snappy is a submodule pointer change whose
target may not be pushed anywhere, so this branch is a preservation record rather
than a guaranteed-buildable tree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-28 20:03:33 +00:00
parent a154309022
commit 30d05ee97b
34 changed files with 2981 additions and 32 deletions

View File

@@ -78,6 +78,20 @@ 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)
@@ -142,8 +156,17 @@ if(MSVC)
# --- Per-configuration MSVC flags ---
# Checked
string(APPEND CMAKE_C_FLAGS_CHECKED " /RTCsu /fsanitize=address")
string(APPEND CMAKE_CXX_FLAGS_CHECKED " /RTCsu /fsanitize=address")
# /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_EXE_LINKER_FLAGS_CHECKED " /INCREMENTAL:NO")
add_compile_definitions($<$<CONFIG:Checked>:DEBUG>)

View File

@@ -39,6 +39,22 @@
"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": [
@@ -89,6 +105,21 @@
"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"
}
]
}

View File

@@ -192,6 +192,12 @@ 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)
@@ -206,7 +212,7 @@ function(xe_shader_rules_dxbc target shader_dir)
if(NOT _stage IN_LIST _valid_stages)
continue()
endif()
list(APPEND _commands COMMAND ${Python3_EXECUTABLE} "${_script}" "${src}" "${_bytecode_dir}/${_id}.h")
list(APPEND _commands COMMAND ${_env_prefix} ${Python3_EXECUTABLE} "${_script}" "${src}" "${_bytecode_dir}/${_id}.h")
endforeach()
add_custom_command(
OUTPUT "${_stamp}"

View File

@@ -0,0 +1,119 @@
# 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

@@ -0,0 +1,126 @@
#!/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,3 +1,5 @@
//{{NO_DEPENDENCIES}}
MAINICON ICON "..\\..\\..\\assets\\icon\\icon.ico"
// 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"

View File

@@ -0,0 +1,455 @@
/**
******************************************************************************
* 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

@@ -0,0 +1,95 @@
/**
******************************************************************************
* 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,6 +11,7 @@
#define XENIA_BASE_BYTE_ORDER_H_
#include <cstdint>
#include <type_traits>
#if defined __has_include
#if __has_include(<version>)
#include <version>
@@ -21,6 +22,7 @@
#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
@@ -88,6 +90,30 @@ 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,8 +26,10 @@ namespace xe {
class Win32MappedMemory : public MappedMemory {
public:
// CreateFile returns INVALID_HANDLE_VALUE in case of failure.
// chrispy: made inline const to get around clang error
static inline constexpr HANDLE kFileHandleInvalid = INVALID_HANDLE_VALUE;
// 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;
// CreateFileMapping returns nullptr in case of failure.
static constexpr HANDLE kMappingHandleInvalid = nullptr;

View File

@@ -18,6 +18,7 @@
#include <string_view>
#include <type_traits>
#include "xenia/base/audit_68_host_mem_watch_fwd.h"
#include "xenia/base/byte_order.h"
namespace xe {
@@ -354,34 +355,52 @@ 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 <>
@@ -411,34 +430,52 @@ 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

@@ -9,12 +9,28 @@
#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 {
@@ -175,6 +191,21 @@ 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) {
@@ -187,7 +218,11 @@ 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));
f.Store(ea, f.ByteSwap(f.LoadVR(vd)));
Value* vec = f.LoadVR(vd);
f.Store(ea, f.ByteSwap(vec));
if (!::xe::cpu::audit67::vals().empty()) {
EmitAudit67ValueWatchVec(f, i.address, vec, ea);
}
return 0;
}
int InstrEmit_stvx(PPCHIRBuilder& f, const InstrData& i) {

View File

@@ -10,11 +10,22 @@
#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 "
@@ -67,6 +78,90 @@ 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) {
@@ -518,9 +613,11 @@ int InstrEmit_stw(PPCHIRBuilder& f, const InstrData& i) {
b = f.LoadGPR(i.D.RA);
}
Value* offset = f.LoadConstantInt64(XEEXTS16(i.D.DS));
f.StoreOffset(b, offset,
f.ByteSwap(f.Truncate(f.LoadGPR(i.D.RT), INT32_TYPE)));
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));
}
return 0;
}
@@ -532,10 +629,14 @@ 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);
f.StoreOffset(b, offset,
f.ByteSwap(f.Truncate(f.LoadGPR(i.D.RT + j), INT32_TYPE)));
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));
}
}
return 0;
}
@@ -545,8 +646,12 @@ 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));
f.Store(ea, f.ByteSwap(f.Truncate(f.LoadGPR(i.D.RT), INT32_TYPE)));
Value* val32 = f.Truncate(f.LoadGPR(i.D.RT), INT32_TYPE);
f.Store(ea, f.ByteSwap(val32));
StoreEA(f, i.D.RA, ea);
if (!::xe::cpu::audit67::vals().empty()) {
EmitAudit67ValueWatch(f, i.address, val32, ea);
}
return 0;
}
@@ -555,8 +660,12 @@ 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);
f.Store(ea, f.ByteSwap(f.Truncate(f.LoadGPR(i.X.RT), INT32_TYPE)));
Value* val32 = f.Truncate(f.LoadGPR(i.X.RT), INT32_TYPE);
f.Store(ea, f.ByteSwap(val32));
StoreEA(f, i.X.RA, ea);
if (!::xe::cpu::audit67::vals().empty()) {
EmitAudit67ValueWatch(f, i.address, val32, ea);
}
return 0;
}
@@ -568,7 +677,11 @@ 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);
f.Store(ea, f.ByteSwap(f.Truncate(f.LoadGPR(i.X.RT), INT32_TYPE)));
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);
}
return 0;
}
@@ -587,7 +700,11 @@ int InstrEmit_std(PPCHIRBuilder& f, const InstrData& i) {
}
Value* offset = f.LoadConstantInt64(XEEXTS16(i.DS.DS << 2));
f.StoreOffset(b, offset, f.ByteSwap(f.LoadGPR(i.DS.RT)));
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));
}
return 0;
}
@@ -596,8 +713,12 @@ 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));
f.Store(ea, f.ByteSwap(f.LoadGPR(i.DS.RT)));
Value* val64 = f.LoadGPR(i.DS.RT);
f.Store(ea, f.ByteSwap(val64));
StoreEA(f, i.DS.RA, ea);
if (!::xe::cpu::audit67::vals().empty()) {
EmitAudit67ValueWatch64(f, i.address, val64, ea);
}
return 0;
}
@@ -606,8 +727,12 @@ int InstrEmit_stdux(PPCHIRBuilder& f, const InstrData& i) {
// MEM(EA, 8) <- (RS)
// RA <- EA
Value* ea = CalculateEA(f, i.X.RA, i.X.RB);
f.Store(ea, f.ByteSwap(f.LoadGPR(i.X.RT)));
Value* val64 = f.LoadGPR(i.X.RT);
f.Store(ea, f.ByteSwap(val64));
StoreEA(f, i.X.RA, ea);
if (!::xe::cpu::audit67::vals().empty()) {
EmitAudit67ValueWatch64(f, i.address, val64, ea);
}
return 0;
}
@@ -619,7 +744,11 @@ 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);
f.Store(ea, f.ByteSwap(f.LoadGPR(i.X.RT)));
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);
}
return 0;
}
@@ -684,7 +813,11 @@ 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);
f.Store(ea, f.Truncate(f.LoadGPR(i.X.RT), INT32_TYPE));
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);
}
return 0;
}
@@ -696,7 +829,11 @@ 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);
f.Store(ea, f.LoadGPR(i.X.RT));
Value* val64 = f.LoadGPR(i.X.RT);
f.Store(ea, val64);
if (!::xe::cpu::audit67::vals().empty()) {
EmitAudit67ValueWatch64(f, i.address, val64, ea);
}
return 0;
}
@@ -843,7 +980,8 @@ 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* rt = f.ByteSwap(f.LoadGPR(i.X.RT));
Value* val64 = f.LoadGPR(i.X.RT);
Value* rt = f.ByteSwap(val64);
if (cvars::no_reserved_ops) {
f.Store(ea, rt);
@@ -862,6 +1000,9 @@ 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;
}
@@ -885,7 +1026,8 @@ int InstrEmit_stwcx(PPCHIRBuilder& f, const InstrData& i) {
Value* ea = CalculateEA_0(f, i.X.RA, i.X.RB);
Value* rt = f.ByteSwap(f.Truncate(f.LoadGPR(i.X.RT), INT32_TYPE));
Value* val32 = f.Truncate(f.LoadGPR(i.X.RT), INT32_TYPE);
Value* rt = f.ByteSwap(val32);
if (cvars::no_reserved_ops) {
f.Store(ea, rt);
@@ -904,7 +1046,9 @@ 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

@@ -51,6 +51,38 @@ 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};
@@ -424,6 +456,10 @@ 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);
@@ -589,6 +625,8 @@ 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:
@@ -665,6 +703,9 @@ 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: {
@@ -799,6 +840,17 @@ 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);

View File

@@ -217,6 +217,13 @@ 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>();
@@ -278,6 +285,10 @@ 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.

View File

@@ -0,0 +1,193 @@
/**
******************************************************************************
* 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

@@ -0,0 +1,51 @@
/**
******************************************************************************
* 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

@@ -0,0 +1,183 @@
/**
******************************************************************************
* 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

@@ -0,0 +1,52 @@
/**
******************************************************************************
* 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

@@ -0,0 +1,922 @@
/**
******************************************************************************
* 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

@@ -0,0 +1,43 @@
/**
******************************************************************************
* 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,6 +11,7 @@
#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"
@@ -18,6 +19,34 @@ 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(); }
@@ -154,6 +183,25 @@ 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;
@@ -239,6 +287,13 @@ 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();
}

View File

@@ -499,6 +499,30 @@ 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,
@@ -578,9 +602,18 @@ 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),
@@ -590,6 +623,11 @@ 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]));
}
}
}
};
@@ -600,14 +638,28 @@ 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

@@ -13,6 +13,7 @@
#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"
@@ -620,6 +621,13 @@ 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,6 +12,8 @@
#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"
@@ -147,6 +149,25 @@ 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;
}
@@ -165,6 +186,9 @@ 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);
@@ -718,6 +742,9 @@ 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);
}
@@ -786,6 +813,13 @@ dword_result_t NtReleaseSemaphore_entry(dword_t sem_handle,
uint32_t(release_count), previous_count);
result = X_STATUS_SEMAPHORE_LIMIT_EXCEEDED;
}
// 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;
}
@@ -954,6 +988,19 @@ 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) {
@@ -980,6 +1027,16 @@ 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,6 +11,7 @@
#include "xenia/base/byte_stream.h"
#include "xenia/base/logging.h"
#include "xenia/kernel/audit_69_event_signal_watch.h"
namespace xe {
namespace kernel {
@@ -58,12 +59,19 @@ 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

@@ -10,6 +10,7 @@
#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"
@@ -429,6 +430,15 @@ 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
@@ -436,12 +446,14 @@ 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
{
@@ -450,6 +462,7 @@ 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
@@ -468,10 +481,24 @@ 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,7 +21,9 @@
#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"
@@ -116,6 +118,11 @@ 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();
@@ -470,6 +477,14 @@ 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>();
@@ -575,6 +590,11 @@ 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,6 +383,11 @@ 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,6 +14,7 @@
#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"
@@ -90,6 +91,9 @@ 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)) {
@@ -151,11 +155,41 @@ 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.
@@ -540,16 +574,71 @@ 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);
}

View File

@@ -347,6 +347,13 @@ 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

@@ -332,12 +332,11 @@ else()
X86_AVX512VNNI
WITH_GZFILEOP
)
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.
# 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")
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,15 +72,29 @@ 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 only supports SM 6.0+.
# 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.
compiler_args.extend([
"-T", f"{stage}_6_0",
"-T", f"{stage}_5_1",
"-HV", "2017",
"-D", "SHADING_LANGUAGE_HLSL_XE=1",
"-I", src_dir,