11 Commits

Author SHA1 Message Date
MechaCat02
bbffd8d20c audit: add poll-based arbitrary guest-VA memory-watch cvars
Some checks failed
Orchestrator / Commit Message Validation (push) Has been skipped
Orchestrator / Lint (push) Failing after 1m37s
Orchestrator / Windows (x86-64) (push) Has been skipped
Orchestrator / Linux (x86-64) (push) Has been skipped
Orchestrator / Create Release (push) Has been skipped
Adds audit_mem_watch_addr (comma-separated hex guest VAs) and
audit_mem_watch_size (1/2/4/8 bytes) to watch arbitrary guest memory
locations at runtime. Once per vblank, GraphicsSystem::MarkVblank()
reads each VA big-endian via Memory::TranslateVirtual and emits an
XELOGKERNEL "AUDIT-MEM-WATCH" line on every value change vs the prior
frame (same greppable log stream as AUDIT-HLC / AUDIT-MEM-READ).

Mechanism is poll / value-change: it captures WHEN a value changes
(vblank index) but NOT the writer guest-PC; pair with audit_jit_prolog_pc
on a suspected writer to recover the PC. Heap VAs vary per run/engine,
so they are supplied at runtime via cvar/CLI. Default empty => disabled
=> zero overhead and no emulation-behavior change.

Validated against the controller vsync "clock A" field (+0x58): the
watch fires once per change, advancing monotonically in lockstep with
vblanks. Game still boots and runs muted with the cvar unset.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 12:21:02 +02:00
MechaCat02
17f4612b79 [Audit] round 24: signal-path probes (XEvent::Set + IO + NtDuplicateObject)
Some checks failed
Orchestrator / Commit Message Validation (push) Has been skipped
Orchestrator / Lint (push) Failing after 1m26s
Orchestrator / Windows (x86-64) (push) Has been skipped
Orchestrator / Linux (x86-64) (push) Has been skipped
Orchestrator / Create Release (push) Has been skipped
Adds AUDIT-HLC probes for every path that signals an XEvent or duplicates
its handle, so the round-23 puzzle (waits completing without visible
NtSetEvent/KeSetEvent) can be cross-referenced by underlying X_KEVENT VA
and by primary handle.

  * NtCreateEvent_inner: always log kevent_va for handle/VA cross-ref
  * XEvent::Set: universal hook prints primary handle + kevent_va + LR;
    catches all paths into the event regardless of shim used
  * NtSetEvent: extended with PPC back-chain walk for caller's guest_lr
    (same idiom as the wait probe), so the signaler function is
    immediately identifiable
  * NtReadFile / NtReadFileScatter / NtWriteFile: log when signal_event
    path fires ev->Set() inline at IO completion (bypasses both
    NtSetEvent and KeSetEvent shims)
  * CompleteOverlappedEx (kernel_state.cc): log when overlapped event
    is signaled at completion
  * NtDuplicateObject: log src/dst handle pair (round-24 confirmed silph
    event is dup'd before signaling, explaining the handle-mismatch
    puzzle between NtSetEvent's `handle=` argument and XEvent::Set's
    primary handle())

Result: silph wait at sub_821CB030+0x1B0 on handle F80000A0 is signaled
via NtSetEvent on handle F80000A8 (the duplicate created by sub_8245D9D8
at lr=0x82450DF4 inside the worker chain sub_82450A28 ← 0xA68 ← 0xB68).
Verdict A confirmed: signaler exists and is reachable, but round-17.β
missed it because the search keyed on the wrong handle. No bypass path;
the worker chain is identical between canary and ours-impl, but ours
signals different handles (work-item queue divergence at sub_82452DC0).

Audit-only diagnostic. No semantic changes. Net delta +86 LOC across 7
files. All probes gated on the existing audit_handle_lifecycle cvar
(default off). Tested via Sylpheed boot (35 s, 4 MB log). Audit-handle-
lifecycle-probes branch, local-only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-08 21:09:40 +02:00
MechaCat02
33dee75c03 [Audit] --audit-r3-dump-bytes: dump N bytes at r3 when probe fires
Some checks failed
Orchestrator / Commit Message Validation (push) Has been skipped
Orchestrator / Lint (push) Failing after 1m20s
Orchestrator / Windows (x86-64) (push) Has been skipped
Orchestrator / Linux (x86-64) (push) Has been skipped
Orchestrator / Create Release (push) Has been skipped
AUDIT-059 round 15 — diagnostic. New cvar `audit_jit_prolog_r3_bytes`
(default 64 = existing behaviour, capped at 256, rounded up to 16B
multiple) controls how many bytes are dumped at host(r3) when the
audit_jit_prolog_pc probe fires. Set to 80 to capture audit-051's
stack-local struct at sub_82452DC0's r31+96.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-07 19:39:26 +02:00
MechaCat02
5427f14f1b [Audit] JIT-prolog: optional audit_jit_prolog_mem_dump chain (3 levels)
Some checks failed
Orchestrator / Commit Message Validation (push) Has been skipped
Orchestrator / Lint (push) Failing after 1m19s
Orchestrator / Windows (x86-64) (push) Has been skipped
Orchestrator / Linux (x86-64) (push) Has been skipped
Orchestrator / Create Release (push) Has been skipped
Round-14 of AUDIT-2BF (singleton-dump). Pairs with xenia-rs'
--audit-mem-read-hex to emit one comparable XELOGKERNEL line resolving
the bctrl target at sub_822F1AA8+0x90 (PC 0x822F1B4C):

  [0x828E1F08]        -> singleton instance ptr
  [singleton+0]       -> vtable
  [vtable+0]          -> vtable[0]  (= first virtual method, bctrl tgt)
  [vtable+24]         -> vtable[24] (= slot 6, silph chain target)

Two complementary hooks:

1. src/xenia/cpu/backend/x64/x64_emitter.cc: extend
   AuditLogJitPrologArgs. New cvar `audit_jit_prolog_mem_dump` (uint32).
   When non-zero and an `audit_jit_prolog_pc` fire happens, the host
   side dereferences the VA 3 levels deep and emits one
   AUDIT-MEM-READ line in the same format ours emits. Defensive
   per-level null + VA-range checks.

2. src/xenia/kernel/kernel_state.cc: one-shot dump in
   EmulateCPInterruptDPC of the same chain (hard-coded to
   0x828E1F08). Useful when audit_jit_prolog_pc isn't set; fires the
   first time the CP interrupt path runs (after the singleton ctor
   has had time to populate).

Read-only. Both gates default-off; no impact when cvars unset.
~65 LOC total across the two files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-07 12:15:33 +02:00
MechaCat02
a594f95f77 [Audit] round 12: ExCreateThread + XThread::Execute probes
Two cvar-gated AUDIT-HLC probes for thread-spawn lineage attribution:

1. ExCreateThread_entry shim logs (start_address, start_context, xapi,
   flags, kernel lr, guest_lr). guest_lr is recovered via the same
   one-frame-up PPC back-chain walk used by the NtWaitForSingleObjectEx
   probe (lr saved at [back2 - 8]).

2. XThread::Execute emits (tid, start_address, start_context, xapi) at
   the very top so each running thread can be matched back to its
   creating ExCreateThread entry by (start_address, start_context).

Gated on the existing cvars::audit_handle_lifecycle; no new cvar.
Pulled in xenia/kernel/kernel_flags.h from xthread.cc to expose the
cvar declaration there.

Used by audit-059 round 12 to enumerate the 23 thread spawns in
canary's 35 s boot, attribute each to its guest_lr caller, and cross-
reference against ours' 10 spawns to identify the 7 missing
spawner functions (including sub_824F7800 which spawns the 4 silph
PKEVENT workers at entry=0x82506528/58/88/B8 via guest_lr=0x824F7B24).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-07 11:12:18 +02:00
MechaCat02
dde48f287b [Audit] round 9: generalize JIT-entry probe to PC-configurable cvar
Some checks failed
Orchestrator / Commit Message Validation (push) Has been skipped
Orchestrator / Lint (push) Failing after 1m18s
Orchestrator / Windows (x86-64) (push) Has been skipped
Orchestrator / Linux (x86-64) (push) Has been skipped
Orchestrator / Create Release (push) Has been skipped
Replace the round-7 hardcoded `current_guest_function_ == 0x824F7800`
gate with a runtime cvar `audit_jit_prolog_pc` (uint32, 0 = disabled).
The cvar names the guest entry PC at which the x64 emitter inserts a
CallNative to the prolog probe; the probe now dumps the active PC in
each log line so multiple instrumentation campaigns can share output.

Renames `audit_log_sub824F7800_args` (cvar) and `AuditLogSub824F7800Args`
(host function) — the round-7 sub_824F7800-specific cvar and function
are deleted, not left dormant. The `audit_handle_lifecycle` cvar and
its existing AUDIT-HLC probe sites in xboxkrnl_threading.cc /
kernel_state.cc are untouched.

Validated by audit-059 round 9: with `--audit_jit_prolog_pc=0x821B55D8`
the silph WorkerCtx-init chain entry fires 1× in canary at tid=6
lr=0x82172D8C (call site at sub_82172BA0+0x1E8 `bctrl`, the virtual
dispatch through vtable slot 6 that statically-blind xref-walkers
cannot enumerate).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-07 10:40:35 +02:00
MechaCat02
fc89e68155 [Audit] round 7: JIT-entry probe for sub_824F7800 args
Adds cvar audit_log_sub824F7800_args (off by default). When set, the x64
JIT emits one CallNative to AuditLogSub824F7800Args at the start of the
JIT-compiled body of guest function 0x824F7800 (silph::WorkerCtx ctor's
immediate caller per audit-058). The hook reads r3..r10, LR, and 64
bytes at host(r3) from PPCContext and writes them to XELOGKERNEL.

The hook is placed *after* the JIT prolog (stack push, GUEST_RET_ADDR /
GUEST_CALL_RET_ADDR setup, optional trace-functions block) and *before*
the first HIR body instruction, so r3..r10 and lr in PPCContext still
reflect the caller's args (no LOAD/STORE_CONTEXT has executed yet inside
the callee). The compile-time gate `current_guest_function_ == 0x824F7800`
makes this a zero-cost no-op for all other functions and a single extra
CallNative for the one target.

Audit-059 round 7 probe captures r3..r10 + LR for a-prime synthetic
replay of sub_824F7800 from a host hook in xenia-rs. Run with:
--audit_handle_lifecycle=true --audit_log_sub824F7800_args=true

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-06 19:48:47 +02:00
MechaCat02
289c2f3a3e [Audit] handle-lifecycle: one-shot silph::WorkerCtx hexdump on first KeSetEvent
Adds a 41-LOC cvar-gated probe (audit_handle_lifecycle) that emits a one-shot
0x300-byte hexdump of the silph::WorkerCtx context the first time KeSetEvent
fires into the silph UI PKEVENT cluster (0xBCE25200..0xBCE25300 in current
builds). Recovers ctx_base by anchoring on the canonical layout (events at
ctx+0x54/+0x64/+0x74/+0x84 with 16-byte stride; ctx_base = ev_addr - (ev_addr
- 0xBCE251C0)). Also emits an explicit per-slot summary of the 8 candidate
KEVENT headers at +0x54..+0xC4 for sanity.

Produces 48 DUMP rows + 8 slot rows in canary.log on the first triggering
KeSetEvent. Guarded by std::atomic_bool so subsequent fires are silent. Used
by iterate 2.BF context-replication in ours: we need the live [ctx+0] vtable
pointer and the 4 quiet/4 active KEVENT slot config to synthesize a matching
WorkerCtx and spawn the four sub_82506xxx entries.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-06 18:42:15 +02:00
MechaCat02
1e5648c9a5 [Audit] handle-lifecycle: include guest LR + one-frame stack-walk on waits
Extend the AUDIT-HLC probes with guest LR capture:
  - NtCreateEvent, NtSetEvent, KeSetEvent: log immediate guest LR
    (cpu::ThreadState::Get()->context()->lr) so callers can be
    attributed to specific guest functions.
  - NtWaitForSingleObjectEx entry + _done: also walk one PPC stack
    frame up to recover the guest_lr of the wait wrapper's caller
    (Xbox 360 EABI: saved LR at [prev_sp - 8], see xenia-rs's
    walk_guest_back_chain). lr_enter alone gives only the kernel
    wait wrapper return address (e.g. 0x824AC578); guest_lr surfaces
    the actual call site.

Still cvar-gated on audit_handle_lifecycle. Used in audit-059 round 4
to map canary handles F8000xxx to ours' 0x12xx wedges via guest LR
matching against silph::UImpl/GamePart cluster.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-06 18:17:15 +02:00
MechaCat02
d031d7c513 [Audit] handle-lifecycle XELOGKERNEL probes (cvar-gated)
New cvar `audit_handle_lifecycle` (Auditing group, default false).
When enabled, emits one-line XELOGKERNEL traces tagged AUDIT-HLC at:
  NtCreateEvent return, NtSetEvent entry, KeSetEvent entry,
  NtWaitForSingleObjectEx entry + completion,
  EmulateCPInterruptDPC entry.

Observation-only: zero-overhead when cvar off (single branch + flag
read). Intended for handle disambiguation between xenia-rs and
canary under the Wine cross-build oracle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-06 11:57:34 +02:00
MechaCat02
03362b59fe [Build] Linux→Windows cross-compile toolchain (clang-cl + xwin)
Some checks failed
Orchestrator / Commit Message Validation (push) Has been skipped
Orchestrator / Lint (push) Successful in 1m19s
Orchestrator / Windows (x86-64) (push) Failing after 5m16s
Orchestrator / Linux (x86-64) (push) Failing after 13m23s
Orchestrator / Create Release (push) Has been skipped
New CMake preset `cross-win-clangcl` for Ninja Multi-Config on
non-Windows hosts. Toolchain: clang-cl (MSVC-ABI), lld-link, xwin SDK/CRT
splat. Shader compilation routes through wine fxc.exe with FXC_PATH
env forwarding and unix→Windows path translation via `winepath -w`.
Plus per-file build fixes (constexpr→const, llvm-rc forward-slash,
zlib-ng AVX guard, /RTCsu MSVC-only). third_party/snappy bumped to
fabi/sylpheed-crossbuild for the target-aware POSIX-gate header.

Produces `xenia_canary.exe` (Debug MSVC) suitable for use as the
audit oracle under Wine for xenia-rs work. See
docs/CROSS_BUILD_SETUP.md for the full reproduction recipe.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-05 23:52:27 +02:00
22 changed files with 848 additions and 11 deletions

1
.gitignore vendored
View File

@@ -94,6 +94,7 @@ node_modules/.bin/
/build/
/build-arm64/
/build-x64/
/build-cross/
# ==============================================================================
# Local-only paths

View File

@@ -82,6 +82,23 @@ 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)
@@ -146,8 +163,14 @@ if(MSVC)
# --- Per-configuration MSVC flags ---
# Checked
if(CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC"
AND NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
string(APPEND CMAKE_C_FLAGS_CHECKED " /RTCsu /fsanitize=address")
string(APPEND CMAKE_CXX_FLAGS_CHECKED " /RTCsu /fsanitize=address")
else()
string(APPEND CMAKE_C_FLAGS_CHECKED " /fsanitize=address")
string(APPEND CMAKE_CXX_FLAGS_CHECKED " /fsanitize=address")
endif()
string(APPEND CMAKE_EXE_LINKER_FLAGS_CHECKED " /INCREMENTAL:NO")
add_compile_definitions($<$<CONFIG:Checked>:DEBUG>)

View File

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

View File

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

View File

@@ -0,0 +1,119 @@
# cmake/toolchains/linux-to-win-msvc.cmake
# 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).
if(DEFINED ENV{FXC_PATH})
set(ENV_FXC "$ENV{FXC_PATH}")
else()
set(ENV_FXC "$ENV{HOME}/.local/share/xenia-cross/fxc/fxc.exe")
endif()
set(ENV{FXC_PATH} "${ENV_FXC}")
set(CMAKE_FIND_ROOT_PATH "${XWIN_DIR}")
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)

View File

@@ -0,0 +1,55 @@
#!/usr/bin/env python3
"""Scan xenia source for #include <...> directives and ensure case-aliases
exist in xwin's flat SDK/CRT include dirs. Idempotent — safe to re-run."""
from __future__ import annotations
import os, re, 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]:
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 fix_dir(target_dir: Path, want: set[str]) -> int:
if not target_dir.is_dir(): return 0
idx = {e.name.lower(): e.name for e in target_dir.iterdir()}
created = 0
for name in want:
if "/" in name: continue
actual = idx.get(name.lower())
if actual is None or actual == name: continue
link = target_dir / name
if link.exists() or link.is_symlink(): continue
try:
link.symlink_to(actual)
created += 1
except OSError: pass
return created
if __name__ == "__main__":
if len(sys.argv) != 3:
sys.exit("usage: xwin-case-symlinks.py <xenia-canary-root> <xwin-splat-dir>")
repo = Path(sys.argv[1]).resolve()
xwin = Path(sys.argv[2]).resolve()
want = collect_includes(repo)
total = 0
for d in (xwin/"crt/include", xwin/"sdk/include/ucrt",
xwin/"sdk/include/um", xwin/"sdk/include/shared",
xwin/"sdk/include/winrt"):
total += fix_dir(d, want)
print(f"Created {total} case-symlinks across {len(want)} unique includes.")

100
docs/CROSS_BUILD_SETUP.md Normal file
View File

@@ -0,0 +1,100 @@
# Building and Running Xenia Canary Windows Debug from Linux
## What we built and why
A Windows-MSVC debug `xenia_canary.exe` cross-compiled on Linux, runnable under Wine. This serves as the audit oracle for the xenia-rs Rust port: the Linux-native canary build stalls before the front-end UI, so all audit comparisons (memory entries audit_044audit_059) need the Wine-targeted debug binary as the ground-truth reference engine.
## Phase 1 — Host setup (run in your terminal)
sudo apt install -y clang-18 lld-18 llvm-18 # clang-cl driver mode + lld-link + binutils
sudo ln -s /usr/bin/clang-18 /usr/bin/clang-cl # activates MSVC-compatible driver mode
xwin --accept-license --arch x86_64 splat --include-debug-libs --output ~/.xwin/splat
# ~807 MB Win10 SDK + MSVC CRT sysroot
curl ... linkid=2361406 -o ~/winsdk.iso # Win10 SDK ISO for real fxc.exe (SM 5_1)
winetricks -q vkd3d # vkd3d-proton d3d12core.dll (5.96 MB)
winetricks -q dxvk # DXVK dxgi.dll (2.95 MB)
Ubuntu's clang-18 / lld-18 apt packages ship binutils suffixed (llvm-lib-18, lld-link-18, etc.); the toolchain expects unsuffixed names on PATH. Create 4 symlinks in `~/.local/bin/` (no sudo needed since it was already on PATH).
## Phase 2 — In-tree edits
### 8 build-fix patches (compile-time only; zero runtime semantics changes)
| § | File | What changed |
|---|---|---|
| 7.1 | `src/xenia/base/mapped_memory_win.cc:30` | `constexpr``const` for `kFileHandleInvalid` (clang-cl rejects `reinterpret_cast` in constant expressions) |
| 7.2 | `src/xenia/app/main_resources.rc:3` | `..\\..\\..\\assets\\icon\\icon.ico` → forward slashes (llvm-rc backslash literalism) |
| 7.3 | `third_party/snappy/snappy-stubs-public.h` | Already in place via the `fabi/sylpheed-crossbuild` snappy fork submodule |
| 7.4 | `third_party/zlib-ng/zconf-ng.h:113` | `#if 1``#if !defined(_WIN32)` for `Z_HAVE_UNISTD_H` (header was pre-generated on Linux) |
| 7.5 | `third_party/CMakeLists.txt:335` | Extended `if(NOT MSVC)``if(NOT MSVC OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang")` for zlib-ng AVX flags |
| 7.6a | `CMakeLists.txt` | Wrapped `/RTCsu` appends in `CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC" AND NOT … "Clang"` |
| 7.6b | `CMakeLists.txt:85-101` | Inserted `execute_process` block invoking `xenia-build.py:generate_version_h()` so CMake-direct flow produces `version.h` |
| 7.7 | `cmake/XeniaHelpers.cmake` | Added `_env_prefix` to wrap per-shader commands in `cmake -E env FXC_PATH=…` (`cmake set(ENV{…})` doesn't survive ninja's subprocess spawn) |
| 7.8 | `tools/build/compile_shader_dxbc.py:74` | Added `_wineify()` helper translating unix paths via `winepath -w` before fxc sees them (fxc parses `/home/…` as a `/h` switch) |
### 3 new cross-compile config files
| File | Purpose |
|---|---|
| `cmake/toolchains/linux-to-win-msvc.cmake` | clang-cl + lld-link + xwin sysroot driver. Forces `MultiThreadedDLL` runtime (no debug-CRT DLLs needed), feeds `-imsvc` flags via `SHELL:` prefix, suppresses 15+ clang-cl-specific warnings, defines `_mm_cvtsi64x_si128` alias, opts out of MSVC STL's Clang≥19 assertion, plumbs `FXC_PATH` |
| `cmake/toolchains/xwin-case-symlinks.py` | Scans xenia includes for mixed-case Windows headers and creates the missing case-aliases in xwin's flat splat (5 needed: `ObjBase.h`, `Psapi.h`, etc.) |
| `CMakePresets.json` | Added `cross-win-clangcl` configure preset + `cross-debug` / `cross-release` / `cross-checked` build presets, gated on `${hostSystemName}` ≠ Windows |
## Phase 3 — Configure + build
python3 cmake/toolchains/xwin-case-symlinks.py "$PWD" "$HOME/.xwin/splat"
# Created 5 case-symlinks across 556 unique includes
cmake --preset cross-win-clangcl
# Generates build-cross/ with build-Debug.ninja + auto-generated version.h
cmake --build build-cross --preset cross-debug --target xenia-app -j6
# 936 ninja targets. -j6 not -j12 because host RAM is tight (~3.4 GiB free at start)
# Wall time ~25 min total
Final output: `xenia_canary.exe` (27 MB PE32+ GUI x86-64) + `xenia_canary.pdb` (116 MB CodeView).
## Phase 4 — Running the game
cd "/home/fabi/RE Project Sylpheed/xenia-canary/build-cross/bin/Windows/Debug"
WINEDEBUG=-all wine ./xenia_canary.exe --mute=true \
"/home/fabi/RE Project Sylpheed/Project Sylpheed - Arc of Deception (USA, Europe) (En,Ja).iso"
Kill after the boot phase completes (the audit workflow only needs ~30 s of trajectory):
sleep 30
wineserver -k
### Expected boot signals in `xenia.log`
| Signal | Value | Meaning |
|---|---|---|
| Total log lines | 1091 | Healthy boot (≥1000 = passes) |
| `^x>` fatals | 0 | No `xe::FatalError()` triggered |
| `^!>` errors | 28 | Non-fatal (occasional pipeline misses — normal) |
| `^w>` warnings | 28 | Normal startup |
| `i> Setup: Initializing chain` | Memory → Exports → Processor → Audio → Graphics → HID → VFS → Kernel | All eight subsystems came up |
| `i> Setup: Starting graphics_system + Starting audio_system` | present | Threads spawned cleanly |
| DXGI adapter | NVIDIA GeForce GTX 1070 Ti | vkd3d-proton + DXVK loaded |
| `K> XThread::Execute thid N count` | 29 | Guest threads spawned (game is past `XexLoadImage`) |
| `Skipping draw - pipeline not ready: VS … PS …` repeating | yes | D3D12 backend is async-compiling pipelines — expected on cold boot |
| `F>` channel | 321 lines | VFS is mounting and walking the disc image |
| `K>` channel | 29 lines | Kernel dispatching XThread creation |
## Audit-workflow integration
The cross-build is now the sole canary oracle. The Linux-native `xenia-canary/build/bin/Linux/Debug/xenia_canary` is deprecated for audits.
All canary launches must include `--mute=true` (project policy 2026-05-12). XAudio2 still initializes — same code paths, silent output. Do not substitute `--apu=nop`; that switches backend entirely.
Point logs at `audit-runs` with `--log_file=`:
LOG="$XENIA_RS_ROOT/xenia-rs/audit-runs/audit_NN/canary.log" \
WINEDEBUG=-all wine xenia_canary.exe \
--mute=true --log_file="$LOG" \
--audit_NN_my_probe=true \
"$ISO_PATH"
Add new audit instrumentation as cvars in `cpu_flags.h` / `gpu_flags.h` / `kernel_flags.h`, guard with `if (cvars::audit_NN_my_probe)`, emit `XELOGI("AUDIT-NN-EVENT …")`. Rebuild incrementally — only the touched TU recompiles, ≤60 s.
The build is observation-only: cvars + `XELOG*()` + `#if`-gated diagnostic blocks are permitted; changing control flow, return values, struct layouts, or timing is forbidden, since that would invalidate every downstream audit comparison.

View File

@@ -1,3 +1,3 @@
//{{NO_DEPENDENCIES}}
MAINICON ICON "..\\..\\..\\assets\\icon\\icon.ico"
MAINICON ICON "../../../assets/icon/icon.ico"

View File

@@ -26,8 +26,9 @@ 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 C++ standard.
static inline const HANDLE kFileHandleInvalid = INVALID_HANDLE_VALUE;
// CreateFileMapping returns nullptr in case of failure.
static constexpr HANDLE kMappingHandleInvalid = nullptr;

View File

@@ -41,6 +41,30 @@
DEFINE_bool(debugprint_trap_log, false,
"Log debugprint traps to the active debugger", "CPU");
DEFINE_uint32(audit_jit_prolog_pc, 0,
"Guest function entry PC at which to emit one XELOGKERNEL line "
"per JIT-compiled entry. When non-zero, the x64 emitter inserts "
"a CallNative to AuditLogJitPrologArgs at the start of the JIT "
"body of the matching guest function. Dumps r3..r10, LR, and 64 "
"bytes at host(r3). Zero (default) disables the probe. Audit-059 "
"round 7+ JIT-prolog probe — generic PC-configurable variant.",
"Auditing");
DEFINE_uint32(audit_jit_prolog_mem_dump, 0,
"Audit-059 round 14 — paired memory-dump VA. When non-zero "
"AND audit_jit_prolog_pc fires, dereference the guest VA 3 "
"levels deep and emit one XELOGKERNEL line: addr -> val "
"(singleton instance), val -> vtable, vtable -> vtable[0] / "
"vtable[24]. Reads the singleton at [0x828E1F08], its vtable, "
"and its vtable[0] (first virtual method = bctrl target at "
"sub_822F1AA8+0x90). Zero (default) disables.",
"Auditing");
DEFINE_uint32(audit_jit_prolog_r3_bytes, 0x40,
"Audit-052 — number of bytes to dump from host(r3) on every "
"audit_jit_prolog_pc fire (capped at 256, 16-byte aligned). "
"Default 64 (existing behaviour). Set to 80 to capture the "
"audit-051 stack-local struct at sub_82452DC0's r31+96 "
"(probe sub_8245B000 entry where r3 IS the struct ptr).",
"Auditing");
DEFINE_bool(ignore_undefined_externs, true,
"Don't exit when an undefined extern is called.", "CPU");
DEFINE_bool(emit_source_annotations, false,
@@ -270,6 +294,17 @@ bool X64Emitter::Emit(HIRBuilder* builder, EmitFunctionInfo& func_info) {
count on no other code modifying it. mov(GetMembaseReg(),
qword[GetContextReg() + offsetof(ppc::PPCContext, virtual_membase)]);
*/
// Audit-059: PC-configurable JIT-prolog probe. Runtime-gated on the cvar
// audit_jit_prolog_pc (uint32; 0 disables). When non-zero and the current
// guest function's entry PC matches, emit a single CallNative to
// AuditLogJitPrologArgs that dumps r3..r10, LR, and 64 bytes at host(r3).
// Emits *before* any body instruction runs, so r3..r10 / LR in PPCContext
// still reflect the caller's args (no LOAD/STORE_CONTEXT has executed yet).
if (cvars::audit_jit_prolog_pc != 0u &&
current_guest_function_ == cvars::audit_jit_prolog_pc) {
extern uint64_t AuditLogJitPrologArgs(void* raw_context, uint64_t arg0);
CallNative(AuditLogJitPrologArgs, 0);
}
// Body.
auto block = builder->first_block();
synchronize_stack_on_next_instruction_ = false;
@@ -438,6 +473,82 @@ uint64_t TrapDebugBreak(void* raw_context, uint64_t address) {
return 0;
}
// Audit-059 JIT-prolog probe: dump r3..r10, LR, and 64 bytes at host(r3)
// when the guest hits the JIT-compiled entry of the function whose entry PC
// matches cvars::audit_jit_prolog_pc. Generic PC-configurable variant of the
// round-7 sub_824F7800 hook. The hook logs the current guest PC so multiple
// instrumentation campaigns can share log output.
uint64_t AuditLogJitPrologArgs(void* raw_context, uint64_t /*unused*/) {
auto* ctx = reinterpret_cast<ppc::PPCContext_s*>(raw_context);
uint32_t pc = static_cast<uint32_t>(cvars::audit_jit_prolog_pc);
uint32_t r3 = static_cast<uint32_t>(ctx->r[3]);
uint32_t r4 = static_cast<uint32_t>(ctx->r[4]);
uint32_t r5 = static_cast<uint32_t>(ctx->r[5]);
uint32_t r6 = static_cast<uint32_t>(ctx->r[6]);
uint32_t r7 = static_cast<uint32_t>(ctx->r[7]);
uint32_t r8 = static_cast<uint32_t>(ctx->r[8]);
uint32_t r9 = static_cast<uint32_t>(ctx->r[9]);
uint32_t r10 = static_cast<uint32_t>(ctx->r[10]);
uint32_t lr = static_cast<uint32_t>(ctx->lr);
uint32_t tid = ctx->thread_state ? ctx->thread_state->thread_id() : 0u;
XELOGKERNEL(
"AUDIT-HLC JitProlog pc={:08X} tid={:08X} r3={:08X} r4={:08X} r5={:08X} "
"r6={:08X} r7={:08X} r8={:08X} r9={:08X} r10={:08X} lr={:08X}",
pc, tid, r3, r4, r5, r6, r7, r8, r9, r10, lr);
// Dump N bytes at host(r3) if r3 looks like a plausible guest VA.
// N comes from cvar `audit_jit_prolog_r3_bytes` (default 64 = existing
// behaviour). Round up to a 16-byte multiple; cap at 256.
uint32_t r3_dump_bytes = static_cast<uint32_t>(cvars::audit_jit_prolog_r3_bytes);
if (r3_dump_bytes > 256) r3_dump_bytes = 256;
r3_dump_bytes = (r3_dump_bytes + 15) & ~15u;
if (r3 >= 0x10000 && r3 < 0xE0000000) {
uint8_t* host = ctx->TranslateVirtual(r3);
if (host) {
for (uint32_t off = 0; off < r3_dump_bytes; off += 16) {
uint32_t d0 = xe::load_and_swap<uint32_t>(host + off + 0);
uint32_t d1 = xe::load_and_swap<uint32_t>(host + off + 4);
uint32_t d2 = xe::load_and_swap<uint32_t>(host + off + 8);
uint32_t d3 = xe::load_and_swap<uint32_t>(host + off + 12);
XELOGKERNEL(
"AUDIT-HLC JitProlog pc={:08X} r3+{:02X}: {:08X} {:08X} {:08X} "
"{:08X}",
pc, off, d0, d1, d2, d3);
}
} else {
XELOGKERNEL("AUDIT-HLC JitProlog pc={:08X} r3 translate failed", pc);
}
} else {
XELOGKERNEL("AUDIT-HLC JitProlog pc={:08X} r3 out of VA range, skipping dump",
pc);
}
// Audit-059 round 14 — paired 3-level dereference. When
// `audit_jit_prolog_mem_dump` is set, read the singleton at that VA,
// its vtable, vtable[0] (first virtual method = bctrl target), and
// vtable[24] (slot 6 = silph chain method per round 9).
uint32_t mem_addr = static_cast<uint32_t>(cvars::audit_jit_prolog_mem_dump);
if (mem_addr != 0u && mem_addr >= 0x10000 && mem_addr < 0xE0000000) {
auto load_be32 = [&](uint32_t va) -> uint32_t {
if (va < 0x10000 || va >= 0xE0000000) return 0u;
uint8_t* h = ctx->TranslateVirtual(va);
if (!h) return 0u;
return xe::load_and_swap<uint32_t>(h);
};
uint32_t val = load_be32(mem_addr);
uint32_t vtable = val != 0u ? load_be32(val) : 0u;
uint32_t m0 = vtable != 0u ? load_be32(vtable) : 0u;
uint32_t m6 = vtable != 0u ? load_be32(vtable + 24u) : 0u;
XELOGKERNEL(
"AUDIT-MEM-READ addr={:08X} val={:08X} vtable={:08X} vtable[0]={:08X} "
"vtable[24]={:08X} pc={:08X} tid={:08X}",
mem_addr, val, vtable, m0, m6, pc, tid);
}
return 0;
}
void X64Emitter::Trap(uint16_t trap_type) {
switch (trap_type) {
case 20:

View File

@@ -13,6 +13,8 @@
#include "xenia/base/clock.h"
#include "xenia/base/logging.h"
#include "xenia/base/math.h"
#include "xenia/base/memory.h"
#include "xenia/base/string_util.h"
#include "xenia/base/profiling.h"
#include "xenia/base/threading.h"
#include "xenia/config.h"
@@ -36,6 +38,32 @@ DEFINE_bool(
"runtime spikes and freezes when playing the game not for the first time.",
"GPU");
// Audit memory-watch: poll-based value-change logger for arbitrary guest VAs.
// Reads the configured guest virtual address(es) once per vblank (per frame)
// from inside GraphicsSystem::MarkVblank() and emits one XELOGKERNEL
// "AUDIT-MEM-WATCH" line whenever the read value changes vs the previous
// frame. Greppable on the same log stream as AUDIT-HLC / AUDIT-MEM-READ.
// Default empty => disabled => zero overhead. Mechanism is poll/value-change
// (NOT a write-trap), so it captures WHEN a value changes (vblank index +
// emulator instruction count) but NOT the writer guest-PC. To find the writer
// PC, pair this with audit_jit_prolog_pc on the suspected writer function.
DEFINE_string(
audit_mem_watch_addr, "",
"Audit memory-watch — comma-separated list of guest virtual addresses "
"(hex, e.g. \"0x40d09a40\" or \"0x40d09a40,0x40929c00\") to poll once per "
"vblank. On every value change vs the previous frame, emit one "
"XELOGKERNEL AUDIT-MEM-WATCH line (watched VA, old value, new value, "
"vblank index, instruction count). Empty (default) disables the watch. "
"Poll-based value-change log: tells you WHEN a value changes, not the "
"writer PC.",
"Auditing");
DEFINE_uint32(
audit_mem_watch_size, 4,
"Audit memory-watch — number of bytes to read at each watched VA (1, 2, "
"4, or 8). Read big-endian (guest byte order) and compared as a 64-bit "
"value. Default 4.",
"Auditing");
namespace xe {
namespace gpu {
@@ -333,12 +361,117 @@ void GraphicsSystem::DispatchInterruptCallback(uint32_t source, uint32_t cpu) {
interrupt_callback_data_, source, cpu);
}
// Audit memory-watch poll. Called once per vblank from MarkVblank(). Parses
// the comma-separated VA list from cvars::audit_mem_watch_addr exactly once
// (cached), then reads each VA (big-endian, audit_mem_watch_size bytes) and
// logs an AUDIT-MEM-WATCH line on every value change vs the previous frame.
// Poll/value-change mechanism: captures WHEN, not the writer PC. Zero work
// when the cvar is empty.
static void AuditMemWatchPoll(Memory* memory, uint64_t vblank_index) {
struct WatchEntry {
uint32_t va;
uint64_t last_value;
bool have_last;
};
static std::vector<WatchEntry> entries;
static std::string parsed_spec;
static bool parse_failed = false;
const std::string& spec = cvars::audit_mem_watch_addr;
if (spec.empty()) {
return;
}
// (Re)parse only when the cvar string changes (set once at startup).
if (spec != parsed_spec) {
parsed_spec = spec;
entries.clear();
parse_failed = false;
size_t start = 0;
while (start <= spec.size()) {
size_t comma = spec.find(',', start);
std::string tok = spec.substr(
start, comma == std::string::npos ? std::string::npos : comma - start);
// Trim 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()) {
uint32_t va = 0;
try {
va = static_cast<uint32_t>(std::stoul(tok, nullptr, 16));
} catch (...) {
XELOGKERNEL("AUDIT-MEM-WATCH bad VA token '{}' in '{}'", tok, spec);
parse_failed = true;
va = 0;
}
if (va != 0) {
entries.push_back({va, 0, false});
}
}
if (comma == std::string::npos) break;
start = comma + 1;
}
XELOGKERNEL("AUDIT-MEM-WATCH armed: {} address(es), size={} bytes",
entries.size(),
static_cast<uint32_t>(cvars::audit_mem_watch_size));
}
if (entries.empty()) {
return;
}
(void)parse_failed;
uint32_t size = static_cast<uint32_t>(cvars::audit_mem_watch_size);
for (auto& e : entries) {
if (e.va < 0x10000 || e.va >= 0xE0000000) {
continue;
}
uint8_t* host = memory->TranslateVirtual(e.va);
if (!host) {
continue;
}
uint64_t value = 0;
switch (size) {
case 1:
value = *host;
break;
case 2:
value = xe::load_and_swap<uint16_t>(host);
break;
case 8:
value = xe::load_and_swap<uint64_t>(host);
break;
case 4:
default:
value = xe::load_and_swap<uint32_t>(host);
break;
}
if (!e.have_last) {
e.have_last = true;
e.last_value = value;
XELOGKERNEL(
"AUDIT-MEM-WATCH va={:08X} init={:016X} vblank={} (first read)",
e.va, value, vblank_index);
continue;
}
if (value != e.last_value) {
XELOGKERNEL(
"AUDIT-MEM-WATCH va={:08X} old={:016X} new={:016X} vblank={}",
e.va, e.last_value, value, vblank_index);
e.last_value = value;
}
}
}
void GraphicsSystem::MarkVblank() {
SCOPE_profile_cpu_f("gpu");
// Increment vblank counter (so the game sees us making progress).
command_processor_->increment_counter();
// Audit memory-watch (poll-based; no-op unless audit_mem_watch_addr set).
AuditMemWatchPoll(memory_, command_processor_->counter());
// TODO(benvanik): we shouldn't need to do the dispatch here, but there's
// something wrong and the CP will block waiting for code that
// needs to be run in the interrupt.

View File

@@ -14,3 +14,12 @@ DEFINE_bool(headless, false,
"UI");
DEFINE_bool(log_high_frequency_kernel_calls, false,
"Log kernel calls with the kHighFrequency tag.", "Logging");
DEFINE_bool(audit_handle_lifecycle, false,
"Emit XELOGKERNEL on Event/Semaphore/Wait lifecycle "
"(create/set/wait/complete). Audit oracle probe — off by default.",
"Auditing");
DEFINE_uint32(audit_track_event_handle, 0,
"If non-zero and AUDIT-HLC enabled, log underlying X_KEVENT "
"guest VA whenever NtCreateEvent returns this handle. "
"Audit round-24 oracle probe.",
"Auditing");

View File

@@ -13,5 +13,7 @@
DECLARE_bool(headless);
DECLARE_bool(log_high_frequency_kernel_calls);
DECLARE_bool(audit_handle_lifecycle);
DECLARE_uint32(audit_track_event_handle);
#endif // XENIA_KERNEL_KERNEL_FLAGS_H_

View File

@@ -15,6 +15,7 @@
#include "xenia/base/logging.h"
#include "xenia/emulator.h"
#include "xenia/hid/input_system.h"
#include "xenia/kernel/kernel_flags.h"
#include "xenia/kernel/user_module.h"
#include "xenia/kernel/util/shim_utils.h"
#include "xenia/kernel/xboxkrnl/xboxkrnl_memory.h"
@@ -1070,6 +1071,14 @@ void KernelState::CompleteOverlappedEx(uint32_t overlapped_ptr, X_RESULT result,
auto ev = object_table()->LookupObject<XEvent>(event_handle);
assert_not_null(ev);
if (ev) {
if (cvars::audit_handle_lifecycle) {
uint32_t lr =
static_cast<uint32_t>(cpu::ThreadState::Get()->context()->lr);
XELOGKERNEL(
"AUDIT-HLC CompleteOverlappedEx_signal_event handle={:08X} "
"kevent_va={:08X} lr={:08X}",
uint32_t(event_handle), ev->guest_object(), lr);
}
ev->Set(0, false);
}
}
@@ -1373,6 +1382,47 @@ void KernelState::EmulateCPInterruptDPC(uint32_t interrupt_callback,
return;
}
if (cvars::audit_handle_lifecycle) {
XELOGKERNEL(
"AUDIT-HLC EmulateCPInterruptDPC callback={:08X} data={:08X} source={} "
"cpu={}",
interrupt_callback, interrupt_callback_data, source, cpu);
// AUDIT-2BF round 14 — one-shot singleton + vtable dump. Resolves
// the silph init chain bctrl target at PC 0x822F1B4C (vtable[0] of
// ANON_Class_2D56F86D at [0x828E1F08]). Dereferences 3 deep:
// [0x828E1F08] (singleton) → vtable → vtable[0] (=first virtual
// method, the bctrl target) and vtable[24] (=slot 6, canary's silph
// chain target sub_821B55D8). Done once per process via atomic
// flag. Defensive null-checks at each level.
static std::atomic<bool> g_audit_singleton_dumped{false};
bool expected = false;
if (g_audit_singleton_dumped.compare_exchange_strong(expected, true)) {
const uint32_t addr = 0x828E1F08;
uint32_t val = 0, vtable = 0, m0 = 0, m6 = 0;
auto* host_addr = memory()->TranslateVirtual<uint32_t*>(addr);
if (host_addr) {
val = xe::load_and_swap<uint32_t>(host_addr);
}
if (val) {
auto* host_val = memory()->TranslateVirtual<uint32_t*>(val);
if (host_val) {
vtable = xe::load_and_swap<uint32_t>(host_val);
}
}
if (vtable) {
auto* host_vt0 = memory()->TranslateVirtual<uint32_t*>(vtable);
auto* host_vt6 = memory()->TranslateVirtual<uint32_t*>(vtable + 24);
if (host_vt0) m0 = xe::load_and_swap<uint32_t>(host_vt0);
if (host_vt6) m6 = xe::load_and_swap<uint32_t>(host_vt6);
}
XELOGKERNEL(
"AUDIT-HLC singleton[0x828E1F08]={:08X} vtable={:08X} "
"vtable[0]={:08X} vtable[24]={:08X}",
val, vtable, m0, m6);
}
}
auto thread = kernel::XThread::GetCurrentThread();
assert_not_null(thread);

View File

@@ -8,7 +8,9 @@
*/
#include "xenia/base/logging.h"
#include "xenia/cpu/thread_state.h"
#include "xenia/kernel/info/file.h"
#include "xenia/kernel/kernel_flags.h"
#include "xenia/kernel/kernel_state.h"
#include "xenia/kernel/util/shim_utils.h"
#include "xenia/kernel/xboxkrnl/xboxkrnl_private.h"
@@ -208,6 +210,13 @@ dword_result_t NtReadFile_entry(dword_t file_handle, dword_t event_handle,
}
if (ev && signal_event) {
if (cvars::audit_handle_lifecycle) {
uint32_t lr = static_cast<uint32_t>(cpu::ThreadState::Get()->context()->lr);
XELOGKERNEL(
"AUDIT-HLC NtReadFile_signal_event handle={:08X} kevent_va={:08X} "
"lr={:08X}",
uint32_t(event_handle), ev->guest_object(), lr);
}
ev->Set(0, false);
}
@@ -294,6 +303,13 @@ dword_result_t NtReadFileScatter_entry(
}
if (ev && signal_event) {
if (cvars::audit_handle_lifecycle) {
uint32_t lr = static_cast<uint32_t>(cpu::ThreadState::Get()->context()->lr);
XELOGKERNEL(
"AUDIT-HLC NtReadFileScatter_signal_event handle={:08X} "
"kevent_va={:08X} lr={:08X}",
uint32_t(event_handle), ev->guest_object(), lr);
}
ev->Set(0, false);
}
@@ -381,6 +397,13 @@ dword_result_t NtWriteFile_entry(dword_t file_handle, dword_t event_handle,
}
if (ev && signal_event) {
if (cvars::audit_handle_lifecycle) {
uint32_t lr = static_cast<uint32_t>(cpu::ThreadState::Get()->context()->lr);
XELOGKERNEL(
"AUDIT-HLC NtWriteFile_signal_event handle={:08X} kevent_va={:08X} "
"lr={:08X}",
uint32_t(event_handle), ev->guest_object(), lr);
}
ev->Set(0, false);
}

View File

@@ -10,6 +10,8 @@
#include "xenia/kernel/xboxkrnl/xboxkrnl_ob.h"
#include "xenia/base/logging.h"
#include "xenia/cpu/processor.h"
#include "xenia/cpu/thread_state.h"
#include "xenia/kernel/kernel_flags.h"
#include "xenia/kernel/kernel_state.h"
#include "xenia/kernel/xboxkrnl/xboxkrnl_private.h"
#include "xenia/kernel/xboxkrnl/xboxkrnl_threading.h"
@@ -398,6 +400,14 @@ dword_result_t NtDuplicateObject_entry(dword_t handle, lpdword_t new_handle_ptr,
X_STATUS result =
kernel_state()->object_table()->DuplicateHandle(handle, &new_handle);
if (cvars::audit_handle_lifecycle) {
uint32_t lr = static_cast<uint32_t>(cpu::ThreadState::Get()->context()->lr);
XELOGKERNEL(
"AUDIT-HLC NtDuplicateObject src={:08X} dst={:08X} options={:08X} "
"lr={:08X}",
uint32_t(handle), uint32_t(new_handle), uint32_t(options), lr);
}
if (new_handle_ptr) {
*new_handle_ptr = new_handle;
}

View File

@@ -8,10 +8,12 @@
*/
#include "xenia/kernel/xboxkrnl/xboxkrnl_threading.h"
#include <atomic>
#include "xenia/base/atomic.h"
#include "xenia/base/clock.h"
#include "xenia/base/platform.h"
#include "xenia/cpu/processor.h"
#include "xenia/kernel/kernel_flags.h"
#include "xenia/kernel/util/shim_utils.h"
#include "xenia/kernel/xboxkrnl/xboxkrnl_private.h"
#include "xenia/kernel/xsemaphore.h"
@@ -22,6 +24,9 @@ namespace xe {
namespace kernel {
namespace xboxkrnl {
// AUDIT-HLC: one-shot guard for silph::WorkerCtx context dump (round5).
static std::atomic<bool> g_audit_silph_ctx_dumped{false};
// r13 + 0x100: pointer to thread local state
// Thread local state:
// 0x058: kernel time
@@ -157,6 +162,30 @@ dword_result_t ExCreateThread_entry(lpdword_t handle_ptr, dword_t stack_size,
lpvoid_t start_address,
lpvoid_t start_context,
dword_t creation_flags) {
if (cvars::audit_handle_lifecycle) {
auto* ctx = cpu::ThreadState::Get()->context();
uint32_t lr = static_cast<uint32_t>(ctx->lr);
// Walk one PPC stack frame up from the shim's frame to recover the GUEST
// caller's LR (lr_enter is just the kernel-internal shim's return
// address; we want the function that called *it*). Same convention as
// the NtWaitForSingleObjectEx probe above.
uint32_t guest_lr = 0;
uint32_t sp = static_cast<uint32_t>(ctx->r[1]);
uint32_t back1 = xe::load_and_swap<uint32_t>(ctx->TranslateVirtual(sp));
if (back1) {
uint32_t back2 =
xe::load_and_swap<uint32_t>(ctx->TranslateVirtual(back1));
if (back2) {
guest_lr =
xe::load_and_swap<uint32_t>(ctx->TranslateVirtual(back2 - 8));
}
}
XELOGKERNEL(
"AUDIT-HLC ExCreateThread entry={:08X} start_ctx={:08X} xapi={:08X} "
"flags={:08X} lr={:08X} guest_lr={:08X}",
uint32_t(start_address), uint32_t(start_context),
uint32_t(xapi_thread_startup), uint32_t(creation_flags), lr, guest_lr);
}
return ExCreateThread(handle_ptr, stack_size, thread_id_ptr,
xapi_thread_startup, start_address, start_context,
creation_flags);
@@ -547,6 +576,48 @@ uint32_t xeKeSetEvent(X_KEVENT* event_ptr, uint32_t increment, uint32_t wait) {
dword_result_t KeSetEvent_entry(pointer_t<X_KEVENT> event_ptr,
dword_t increment, dword_t wait) {
if (cvars::audit_handle_lifecycle) {
uint32_t lr = static_cast<uint32_t>(cpu::ThreadState::Get()->context()->lr);
XELOGKERNEL("AUDIT-HLC KeSetEvent guest_ptr={:08X} lr={:08X}",
uint32_t(event_ptr.guest_address()), lr);
// AUDIT-HLC round5: one-shot hexdump of the silph::WorkerCtx context when
// KeSetEvent first fires into the silph UI PKEVENT cluster
// (0xBCE25214/24/34/44 in current builds; widened a bit for allocator
// drift). Used by iterate 2.BF context-replication.
uint32_t ev_addr = uint32_t(event_ptr.guest_address());
if (ev_addr >= 0xBCE25200 && ev_addr < 0xBCE25300) {
if (!g_audit_silph_ctx_dumped.exchange(true)) {
// Events live at ctx+0x54, +0x64, +0x74, +0x84 (16-byte stride).
// Compute ctx_base assuming the canonical layout (lowest event at
// ctx+0x54 → ctx_base = 0xBCE251C0 when ev_addr == 0xBCE25214).
uint32_t event_offset_in_ctx = ev_addr - 0xBCE251C0;
uint32_t ctx_base = ev_addr - event_offset_in_ctx;
auto* ctx = cpu::ThreadState::Get()->context();
uint8_t* host = ctx->TranslateVirtual(ctx_base);
XELOGKERNEL(
"AUDIT-HLC silph_ctx_dump ctx_base={:08X} (event {:08X} at +{:02X})",
ctx_base, ev_addr, event_offset_in_ctx);
for (uint32_t off = 0; off < 0x300; off += 16) {
uint32_t d0 = xe::load_and_swap<uint32_t>(host + off + 0);
uint32_t d1 = xe::load_and_swap<uint32_t>(host + off + 4);
uint32_t d2 = xe::load_and_swap<uint32_t>(host + off + 8);
uint32_t d3 = xe::load_and_swap<uint32_t>(host + off + 12);
XELOGKERNEL("AUDIT-HLC DUMP {:08X}: {:08X} {:08X} {:08X} {:08X}",
ctx_base + off, d0, d1, d2, d3);
}
XELOGKERNEL("AUDIT-HLC silph_ctx event-slots:");
for (uint32_t i = 0; i < 8; ++i) {
uint32_t ev_off = 0x54 + i * 0x10;
uint32_t hdr = xe::load_and_swap<uint32_t>(host + ev_off + 0);
uint32_t state = xe::load_and_swap<uint32_t>(host + ev_off + 4);
XELOGKERNEL(
"AUDIT-HLC slot[{}] off=+{:02X} addr={:08X} hdr={:08X} "
"state={:08X}",
i, ev_off, ctx_base + ev_off, hdr, state);
}
}
}
}
return xeKeSetEvent(event_ptr, increment, wait);
}
DECLARE_XBOXKRNL_EXPORT2(KeSetEvent, kThreading, kImplemented, kHighFrequency);
@@ -604,6 +675,29 @@ dword_result_t NtCreateEvent_entry(
if (handle_ptr) {
*handle_ptr = ev->handle();
}
if (cvars::audit_handle_lifecycle) {
uint32_t lr = static_cast<uint32_t>(cpu::ThreadState::Get()->context()->lr);
uint32_t out_handle = handle_ptr ? uint32_t(*handle_ptr) : 0u;
XELOGKERNEL(
"AUDIT-HLC NtCreateEvent handle={:08X} type={} initial_state={} "
"lr={:08X}",
out_handle, uint32_t(event_type), uint32_t(initial_state), lr);
// Round-24: also log the underlying X_KEVENT guest VA for every
// NtCreateEvent so KeSetEvent probes can be cross-referenced by ptr.
XELOGKERNEL(
"AUDIT-HLC NtCreateEvent_inner handle={:08X} kevent_va={:08X} "
"lr={:08X}",
out_handle, ev->guest_object(), lr);
// When caller pinned a target handle, additionally emit the legacy
// _target line so older round-24 grep recipes still match.
if (cvars::audit_track_event_handle != 0 &&
out_handle == cvars::audit_track_event_handle) {
XELOGKERNEL(
"AUDIT-HLC NtCreateEvent_target handle={:08X} object_va={:08X} "
"lr={:08X}",
out_handle, ev->guest_object(), lr);
}
}
return X_STATUS_SUCCESS;
}
DECLARE_XBOXKRNL_EXPORT1(NtCreateEvent, kThreading, kImplemented);
@@ -629,6 +723,26 @@ uint32_t xeNtSetEvent(uint32_t handle, xe::be<uint32_t>* previous_state_ptr) {
}
dword_result_t NtSetEvent_entry(dword_t handle, lpdword_t previous_state_ptr) {
if (cvars::audit_handle_lifecycle) {
auto* ctx = cpu::ThreadState::Get()->context();
uint32_t lr = static_cast<uint32_t>(ctx->lr);
// Round-24: walk PPC back-chain for caller's guest LR (same idiom as
// NtWaitForSingleObjectEx probe).
uint32_t guest_lr = 0;
uint32_t sp = static_cast<uint32_t>(ctx->r[1]);
uint32_t back1 = xe::load_and_swap<uint32_t>(ctx->TranslateVirtual(sp));
if (back1) {
uint32_t back2 =
xe::load_and_swap<uint32_t>(ctx->TranslateVirtual(back1));
if (back2) {
guest_lr = xe::load_and_swap<uint32_t>(
ctx->TranslateVirtual(back2 - 8));
}
}
XELOGKERNEL(
"AUDIT-HLC NtSetEvent handle={:08X} lr={:08X} guest_lr={:08X}",
uint32_t(handle), lr, guest_lr);
}
return xeNtSetEvent(handle, previous_state_ptr);
}
DECLARE_XBOXKRNL_EXPORT2(NtSetEvent, kThreading, kImplemented, kHighFrequency);
@@ -1003,9 +1117,43 @@ dword_result_t NtWaitForSingleObjectEx_entry(dword_t object_handle,
dword_t wait_mode,
dword_t alertable,
lpqword_t timeout_ptr) {
uint32_t lr_enter = 0;
uint32_t guest_lr = 0;
if (cvars::audit_handle_lifecycle) {
auto* ctx = cpu::ThreadState::Get()->context();
lr_enter = static_cast<uint32_t>(ctx->lr);
// Walk one PPC stack frame up from the wait wrapper's frame to recover
// the GUEST caller's LR (lr_enter is just the kernel-internal wait
// wrapper's return address; we want the function that called *it*).
// Xbox 360 PPC convention: prologue stores caller's LR at [old_sp - 8]
// *before* bumping r1 to the new frame, so from any frame, the LR saved
// by that frame's prologue lives at (back_chain - 8). See xenia-rs
// walk_guest_back_chain in crates/xenia-kernel/src/state.rs.
uint32_t sp = static_cast<uint32_t>(ctx->r[1]);
uint32_t back1 = xe::load_and_swap<uint32_t>(ctx->TranslateVirtual(sp));
if (back1) {
uint32_t back2 =
xe::load_and_swap<uint32_t>(ctx->TranslateVirtual(back1));
if (back2) {
guest_lr = xe::load_and_swap<uint32_t>(
ctx->TranslateVirtual(back2 - 8));
}
}
XELOGKERNEL(
"AUDIT-HLC NtWaitForSingleObjectEx handle={:08X} alertable={} "
"lr={:08X} guest_lr={:08X}",
uint32_t(object_handle), uint32_t(alertable), lr_enter, guest_lr);
}
uint64_t timeout = timeout_ptr ? static_cast<uint64_t>(*timeout_ptr) : 0u;
return NtWaitForSingleObjectEx(object_handle, wait_mode, alertable,
uint32_t result = NtWaitForSingleObjectEx(object_handle, wait_mode, alertable,
timeout_ptr ? &timeout : nullptr);
if (cvars::audit_handle_lifecycle) {
XELOGKERNEL(
"AUDIT-HLC NtWaitForSingleObjectEx_done handle={:08X} result={:08X} "
"lr={:08X} guest_lr={:08X}",
uint32_t(object_handle), result, lr_enter, guest_lr);
}
return result;
}
DECLARE_XBOXKRNL_EXPORT3(NtWaitForSingleObjectEx, kThreading, kImplemented,
kBlocking, kHighFrequency);

View File

@@ -11,6 +11,8 @@
#include "xenia/base/byte_stream.h"
#include "xenia/base/logging.h"
#include "xenia/cpu/thread_state.h"
#include "xenia/kernel/kernel_flags.h"
namespace xe {
namespace kernel {
@@ -58,6 +60,13 @@ void XEvent::InitializeNative(void* native_ptr, X_DISPATCH_HEADER* header) {
}
int32_t XEvent::Set(uint32_t priority_increment, bool wait) {
if (cvars::audit_handle_lifecycle) {
auto* ts = cpu::ThreadState::Get();
uint32_t lr = ts ? static_cast<uint32_t>(ts->context()->lr) : 0u;
XELOGKERNEL(
"AUDIT-HLC XEvent::Set handle={:08X} kevent_va={:08X} prio={} lr={:08X}",
handle(), guest_object(), priority_increment, lr);
}
set_priority_increment(priority_increment);
event_->Set();
return 1;

View File

@@ -21,6 +21,7 @@
#include "xenia/base/threading.h"
#include "xenia/cpu/processor.h"
#include "xenia/emulator.h"
#include "xenia/kernel/kernel_flags.h"
#include "xenia/kernel/kernel_state.h"
#include "xenia/kernel/user_module.h"
#include "xenia/kernel/xboxkrnl/xboxkrnl_threading.h"
@@ -544,6 +545,13 @@ X_STATUS XThread::Terminate(int exit_code) {
void XThread::Execute() {
XELOGKERNEL("XThread::Execute thid {} (handle={:08X}, '{}', native={:08X})",
thread_id_, handle(), thread_name_, thread_->system_id());
if (cvars::audit_handle_lifecycle) {
XELOGKERNEL(
"AUDIT-HLC XThread::Execute tid={} start_address={:08X} "
"start_context={:08X} xapi={:08X}",
thread_id_, creation_params_.start_address,
creation_params_.start_context, creation_params_.xapi_thread_startup);
}
// Let the kernel know we are starting.
kernel_state()->OnThreadExecute(this);

View File

@@ -332,7 +332,7 @@ else()
X86_AVX512VNNI
WITH_GZFILEOP
)
if(NOT MSVC)
if(NOT MSVC OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
# GCC/Clang have native __builtin_ctz/__builtin_ctzll (MSVC gets these
# from fallback_builtins.h). Defining these enables optimized compare256
# and longest_match codepaths.

View File

@@ -73,6 +73,16 @@ def main():
# Start with base command — use wine on non-Windows platforms.
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]