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