16 Commits

Author SHA1 Message Date
MechaCat02
f970a5173f [GPU] log_draws: also dump vertex positions for file correlation
Some checks failed
Orchestrator / Commit Message Validation (push) Has been skipped
Orchestrator / Lint (push) Failing after 1m57s
Orchestrator / Windows (x86-64) (push) Has been skipped
Orchestrator / Linux (x86-64) (push) Has been skipped
Orchestrator / Create Release (push) Has been skipped
Extend the RE draw-logger to dump the first few vertex POSITIONS (read
from guest memory) under each draw. The f32 position bytes are identical
between the guest buffer and the on-disc .xpr (only f16 pairs are
rearranged on load), so these values can be grep'd for in a resource file
to locate a mesh whose in-file offset is otherwise unknown.

Validated: world-space stage geometry byte-matches Stage_*.xpr at exact
offsets. (Load-time-transformed meshes like the player ship don't match,
which is itself a useful finding.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 19:09:08 +02:00
MechaCat02
25eb17b91d [GPU] Add log_draws: dump per-draw vertex declaration for RE
Reverse-engineering aid for decoding game mesh formats against GPU ground
truth. When the `log_draws` cvar is set, each distinct draw's primitive
type, index buffer (guest base / count / format / endianness), and full
per-stream vertex declaration (fetch-constant base + stride, and every
element's format + offset) is written to xenia_re_draws.log.

- command_processor.{h,cc}: CommandProcessor::LogDrawForRE(), no-op unless
  the cvar is set. De-dups by the vertex-declaration fingerprint (shader +
  primitive + element formats/offsets), so animated UI that redraws the
  same format into fresh buffers every frame collapses to one record --
  keeping it near-free (an earlier address-keyed de-dup flooded the log and
  stalled the GPU thread). Capped at 4096 distinct formats.
- pm4_command_processor_implement.h: call it from ExecutePacketType3Draw
  after IssueDraw (so the vertex shader has been analyzed). Backend-agnostic
  base path -- works for the Vulkan build.

Used to confirm Project Sylpheed's XBG7 mesh layout (triangle list, pos
f32x3 / normal f16x4 / uv f16x2, variable stride).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 18:42:33 +02:00
MechaCat02
f10484834c [APU] Fix mission-audio silence: ALSA underrun keepalive + guest_audio_flags
Two Linux mission-audio fixes for Project Sylpheed (audio played in
intro/menu but died when a mission finished loading, never returning):

- alsa_audio_driver: when the guest stalls (mission "Preparing for Sortie"
  load) and the ring buffer empties, feed silence to keep the PCM alive
  instead of sleeping. Previously the small buffer drained, XRUN'd, and
  playback never recovered (matches the known "audio muted permanently").
- xconfig: make the guest speaker config a cvar (guest_audio_flags,
  default Digital Stereo) instead of hardcoded Dolby Digital surround.

(The practical fix on this box also needed log_mask=13 to stop kernel log
spam starving the audio pipeline during missions — config, not code.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 16:49:25 +02:00
MechaCat02
919e526fd8 [Fix] Screenshot: null-check presenter before use (fixes F12 crash)
TakeScreenshot() called GetGraphicsSystemPresenter()->CaptureGuestOutput()
and only tested the pointer for null AFTER dereferencing it, so taking a
screenshot with no live presenter dereferenced null and crashed. Check for
null first, and re-enable notifications on the capture-failure path too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 15:44:11 +02:00
MechaCat02
2233fae235 [Build] Guard ThinLTO behind XENIA_ENABLE_LTO for memory-constrained hosts
Release enables -flto=thin unconditionally; the ThinLTO link spikes memory
past what a 15GB box can handle. Gate it behind an option (default ON, so
normal/CI builds are unchanged); pass -DXENIA_ENABLE_LTO=OFF to build
Release without the LTO memory spike.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 15:43:23 +02:00
MechaCat02
f81e90f03a audit: add poll-based arbitrary guest-VA memory-watch cvars
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-07-12 15:43:23 +02:00
MechaCat02
07f09f8fe0 [Audit] round 24: signal-path probes (XEvent::Set + IO + NtDuplicateObject)
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-07-12 15:43:23 +02:00
MechaCat02
4670afbeb5 [Audit] --audit-r3-dump-bytes: dump N bytes at r3 when probe fires
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-07-12 15:43:23 +02:00
MechaCat02
87e4a67b61 [Audit] JIT-prolog: optional audit_jit_prolog_mem_dump chain (3 levels)
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-07-12 15:43:23 +02:00
MechaCat02
1517a63974 [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-07-12 15:43:23 +02:00
MechaCat02
81d3eb056b [Audit] round 9: generalize JIT-entry probe to PC-configurable cvar
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-07-12 15:43:23 +02:00
MechaCat02
94115aba1c [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-07-12 15:43:23 +02:00
MechaCat02
35f58b147f [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-07-12 15:43:23 +02:00
MechaCat02
3c7aa73512 [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-07-12 15:43:22 +02:00
MechaCat02
fea4ef31ec [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-07-12 15:43:22 +02:00
MechaCat02
ce67514572 [Build] Linux→Windows cross-compile toolchain (clang-cl + xwin)
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-07-12 15:43:22 +02:00
28 changed files with 1095 additions and 19 deletions

1
.gitignore vendored
View File

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

View File

@@ -82,6 +82,23 @@ file(MAKE_DIRECTORY "${PROJECT_SOURCE_DIR}/scratch")
# Python for shader compilation scripts # Python for shader compilation scripts
find_package(Python3 REQUIRED COMPONENTS Interpreter) 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 helpers
include(cmake/XeniaHelpers.cmake) include(cmake/XeniaHelpers.cmake)
@@ -146,8 +163,14 @@ if(MSVC)
# --- Per-configuration MSVC flags --- # --- Per-configuration MSVC flags ---
# Checked # Checked
string(APPEND CMAKE_C_FLAGS_CHECKED " /RTCsu /fsanitize=address") if(CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC"
string(APPEND CMAKE_CXX_FLAGS_CHECKED " /RTCsu /fsanitize=address") 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") string(APPEND CMAKE_EXE_LINKER_FLAGS_CHECKED " /INCREMENTAL:NO")
add_compile_definitions($<$<CONFIG:Checked>:DEBUG>) add_compile_definitions($<$<CONFIG:Checked>:DEBUG>)
@@ -291,9 +314,14 @@ else()
) )
add_compile_options($<$<CONFIG:Release>:-O3>) add_compile_options($<$<CONFIG:Release>:-O3>)
# Link-time optimization (use lld and llvm-ar/ranlib to avoid system # Link-time optimization (use lld and llvm-ar/ranlib to avoid system
# linker/archiver LTO plugin version mismatch with clang's bitcode) # linker/archiver LTO plugin version mismatch with clang's bitcode).
add_compile_options($<$<CONFIG:Release>:-flto=thin>) # Guarded so memory-constrained hosts can build Release without the
add_link_options($<$<CONFIG:Release>:-flto=thin>) # ThinLTO link-time memory spike: pass -DXENIA_ENABLE_LTO=OFF.
option(XENIA_ENABLE_LTO "Enable ThinLTO for Release builds" ON)
if(XENIA_ENABLE_LTO)
add_compile_options($<$<CONFIG:Release>:-flto=thin>)
add_link_options($<$<CONFIG:Release>:-flto=thin>)
endif()
add_link_options($<$<CONFIG:Release>:-fuse-ld=lld>) add_link_options($<$<CONFIG:Release>:-fuse-ld=lld>)
find_program(LLVM_AR NAMES llvm-ar) find_program(LLVM_AR NAMES llvm-ar)
find_program(LLVM_RANLIB NAMES llvm-ranlib) find_program(LLVM_RANLIB NAMES llvm-ranlib)

View File

@@ -39,6 +39,22 @@
"cacheVariables": { "cacheVariables": {
"CMAKE_SYSTEM_PROCESSOR": "ARM64" "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": [ "buildPresets": [
@@ -89,6 +105,9 @@
"name": "vs-arm64-checked", "name": "vs-arm64-checked",
"configurePreset": "vs-arm64", "configurePreset": "vs-arm64",
"configuration": "Checked" "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(_valid_stages vs hs ds gs ps cs)
set(_commands) set(_commands)
set(_bytecode_dir "${shader_dir}/bytecode/d3d12_5_1") 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}") list(APPEND _commands COMMAND ${CMAKE_COMMAND} -E make_directory "${_bytecode_dir}")
foreach(src ${_sources}) foreach(src ${_sources})
get_filename_component(_name ${src} NAME) 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) if(NOT _stage IN_LIST _valid_stages)
continue() continue()
endif() 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() endforeach()
add_custom_command( add_custom_command(
OUTPUT "${_stamp}" 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

@@ -1134,10 +1134,20 @@ void EmulatorWindow::OnMouseUp(const ui::MouseEvent& e) {
void EmulatorWindow::TakeScreenshot() { void EmulatorWindow::TakeScreenshot() {
xe::ui::RawImage image; xe::ui::RawImage image;
// Null-check the presenter BEFORE dereferencing it. The original condition
// called CaptureGuestOutput() on the pointer and only tested it for null
// afterwards, so pressing the screenshot key before/without a live presenter
// dereferenced a null pointer and crashed.
auto* presenter = GetGraphicsSystemPresenter();
if (presenter == nullptr) {
XELOGE("No graphics presenter available for screenshot");
return;
}
imgui_drawer_->EnableNotifications(false); imgui_drawer_->EnableNotifications(false);
if (!GetGraphicsSystemPresenter()->CaptureGuestOutput(image) || if (!presenter->CaptureGuestOutput(image)) {
GetGraphicsSystemPresenter() == nullptr) { imgui_drawer_->EnableNotifications(true);
XELOGE("Failed to capture guest output for screenshot"); XELOGE("Failed to capture guest output for screenshot");
return; return;
} }

View File

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

View File

@@ -336,8 +336,30 @@ void ALSAAudioDriver::WorkerThread() {
size_t current_write = write_index_.load(std::memory_order_acquire); size_t current_write = write_index_.load(std::memory_order_acquire);
if (current_read == current_write) { if (current_read == current_write) {
// No data available, sleep and try again // No guest audio available. During long guest stalls (e.g. a mission
std::this_thread::sleep_for(std::chrono::milliseconds(5)); // "Preparing for Sortie" load) the game stops submitting frames; if we
// merely sleep, ALSA drains its small buffer, XRUNs, and playback never
// recovers (the game's audio is silent for the rest of the session).
// Keep the PCM alive by topping it up with silence whenever the queued
// audio runs low, so real audio resumes seamlessly once the stall ends.
snd_pcm_sframes_t avail = snd_pcm_avail_update(pcm_handle_);
if (avail < 0) {
if (!RecoverFromUnderrun(avail)) {
running_ = false;
}
continue;
}
snd_pcm_uframes_t queued =
(buffer_size_ > (snd_pcm_uframes_t)avail) ? buffer_size_ - avail : 0;
if (queued < period_size_ * 2) {
snd_pcm_sframes_t w =
snd_pcm_writei(pcm_handle_, silence.get(), period_size_);
if (w < 0) {
RecoverFromUnderrun(w);
}
} else {
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
continue; continue;
} }

View File

@@ -26,8 +26,9 @@ namespace xe {
class Win32MappedMemory : public MappedMemory { class Win32MappedMemory : public MappedMemory {
public: public:
// CreateFile returns INVALID_HANDLE_VALUE in case of failure. // CreateFile returns INVALID_HANDLE_VALUE in case of failure.
// chrispy: made inline const to get around clang error // INVALID_HANDLE_VALUE expands to a reinterpret_cast which MSVC accepts
static inline constexpr HANDLE kFileHandleInvalid = INVALID_HANDLE_VALUE; // 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. // CreateFileMapping returns nullptr in case of failure.
static constexpr HANDLE kMappingHandleInvalid = nullptr; static constexpr HANDLE kMappingHandleInvalid = nullptr;

View File

@@ -41,6 +41,30 @@
DEFINE_bool(debugprint_trap_log, false, DEFINE_bool(debugprint_trap_log, false,
"Log debugprint traps to the active debugger", "CPU"); "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, DEFINE_bool(ignore_undefined_externs, true,
"Don't exit when an undefined extern is called.", "CPU"); "Don't exit when an undefined extern is called.", "CPU");
DEFINE_bool(emit_source_annotations, false, 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(), count on no other code modifying it. mov(GetMembaseReg(),
qword[GetContextReg() + offsetof(ppc::PPCContext, virtual_membase)]); 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. // Body.
auto block = builder->first_block(); auto block = builder->first_block();
synchronize_stack_on_next_instruction_ = false; synchronize_stack_on_next_instruction_ = false;
@@ -438,6 +473,82 @@ uint64_t TrapDebugBreak(void* raw_context, uint64_t address) {
return 0; 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) { void X64Emitter::Trap(uint16_t trap_type) {
switch (trap_type) { switch (trap_type) {
case 20: case 20:

View File

@@ -9,6 +9,9 @@
#include "xenia/gpu/command_processor.h" #include "xenia/gpu/command_processor.h"
#include <fstream>
#include <unordered_set>
#include "third_party/fmt/include/fmt/format.h" #include "third_party/fmt/include/fmt/format.h"
#include "xenia/base/byte_stream.h" #include "xenia/base/byte_stream.h"
#include "xenia/base/clock.h" #include "xenia/base/clock.h"
@@ -18,6 +21,8 @@
#include "xenia/gpu/gpu_flags.h" #include "xenia/gpu/gpu_flags.h"
#include "xenia/gpu/graphics_system.h" #include "xenia/gpu/graphics_system.h"
#include "xenia/gpu/packet_disassembler.h" #include "xenia/gpu/packet_disassembler.h"
#include "xenia/gpu/registers.h"
#include "xenia/gpu/shader.h"
#include "xenia/gpu/sampler_info.h" #include "xenia/gpu/sampler_info.h"
#include "xenia/gpu/texture_info.h" #include "xenia/gpu/texture_info.h"
#include "xenia/gpu/xenos_zpd_report.h" #include "xenia/gpu/xenos_zpd_report.h"
@@ -77,6 +82,14 @@ DEFINE_string(
UPDATE_from_string(readback_resolve, 2025, 12, 4, 21, "fast"); UPDATE_from_string(readback_resolve, 2025, 12, 4, 21, "fast");
DEFINE_bool(
log_draws, false,
"Reverse-engineering aid: write each distinct draw's primitive type, index "
"buffer, and per-stream vertex declaration (stream base/stride + per-element "
"format/offset) to xenia_re_draws.log in the working directory. For decoding "
"game mesh formats against GPU ground truth.",
"GPU");
DEFINE_bool( DEFINE_bool(
readback_memexport, false, readback_memexport, false,
"Read data written by memory export in shaders on the CPU. " "Read data written by memory export in shaders on the CPU. "
@@ -88,6 +101,171 @@ DEFINE_bool(
namespace xe { namespace xe {
namespace gpu { namespace gpu {
namespace {
// Short readable name for a guest vertex element format (RE logging only).
const char* ReVertexFormatName(xenos::VertexFormat f) {
switch (f) {
case xenos::VertexFormat::k_32_FLOAT: return "f32";
case xenos::VertexFormat::k_32_32_FLOAT: return "f32x2";
case xenos::VertexFormat::k_32_32_32_FLOAT: return "f32x3";
case xenos::VertexFormat::k_32_32_32_32_FLOAT: return "f32x4";
case xenos::VertexFormat::k_16_16_FLOAT: return "f16x2";
case xenos::VertexFormat::k_16_16_16_16_FLOAT: return "f16x4";
case xenos::VertexFormat::k_16_16: return "s16x2";
case xenos::VertexFormat::k_16_16_16_16: return "s16x4";
case xenos::VertexFormat::k_8_8_8_8: return "8888";
case xenos::VertexFormat::k_2_10_10_10: return "2_10_10_10";
case xenos::VertexFormat::k_10_11_11: return "10_11_11";
case xenos::VertexFormat::k_11_11_10: return "11_11_10";
case xenos::VertexFormat::k_32: return "u32";
case xenos::VertexFormat::k_32_32: return "u32x2";
case xenos::VertexFormat::k_32_32_32_32: return "u32x4";
default: return "?";
}
}
} // namespace
void CommandProcessor::LogDrawForRE(uint32_t vgt_draw_initiator_value,
const IndexBufferInfo* index_buffer_info) {
if (!cvars::log_draws) {
return;
}
static std::mutex re_mutex;
static std::ofstream re_out;
static std::unordered_set<uint64_t> re_seen;
std::lock_guard<std::mutex> lock(re_mutex);
if (!re_out.is_open()) {
re_out.open("xenia_re_draws.log", std::ios::out | std::ios::trunc);
XELOGI("[RE-DRAW] logging distinct draws to xenia_re_draws.log");
}
if (!re_out.is_open()) {
return;
}
reg::VGT_DRAW_INITIATOR init;
init.value = vgt_draw_initiator_value;
Shader* vs = active_vertex_shader_;
// De-dup by the VERTEX-DECLARATION FINGERPRINT (shader + primitive type +
// per-stream element formats/offsets), NOT by buffer address. Animated UI
// that redraws the same mesh format into fresh buffers every frame therefore
// collapses to a single record, keeping the logging near-free — while every
// distinct mesh format (the player plane, each weapon) is still captured once.
uint64_t sig = 1469598103934665603ull; // FNV-ish seed
auto mix = [&sig](uint64_t v) { sig = (sig ^ v) * 1099511628211ull; };
mix(uint64_t(init.prim_type));
if (vs) {
mix(vs->ucode_data_hash());
}
bool analyzed = vs && vs->is_ucode_analyzed();
if (analyzed) {
for (const auto& binding : vs->vertex_bindings()) {
mix(binding.fetch_constant);
mix(binding.stride_words);
for (const auto& attr : binding.attributes) {
mix(uint64_t(attr.fetch_instr.attributes.data_format));
mix(uint64_t(uint32_t(attr.fetch_instr.attributes.offset)));
}
}
} else {
mix(uint64_t(init.num_indices));
}
if (!re_seen.insert(sig).second) {
return;
}
// Safety cap on distinct formats, so a pathological title can't grow the log
// (and the working set) without bound.
if (re_seen.size() > 4096) {
return;
}
re_out << fmt::format("DRAW prim={} indices={} src={} ",
uint32_t(init.prim_type), uint32_t(init.num_indices),
uint32_t(init.source_select));
if (index_buffer_info) {
re_out << fmt::format(
"index[base=0x{:08X} count={} fmt={} endian={} len={}] ",
index_buffer_info->guest_base, index_buffer_info->count,
index_buffer_info->format == xenos::IndexFormat::kInt16 ? "u16" : "u32",
uint32_t(index_buffer_info->endianness), index_buffer_info->length);
} else {
re_out << "index[auto] ";
}
if (vs) {
re_out << fmt::format("vs=0x{:016X}", vs->ucode_data_hash());
}
re_out << "\n";
if (analyzed) {
for (const auto& binding : vs->vertex_bindings()) {
xenos::xe_gpu_vertex_fetch_t fetch =
register_file_->GetVertexFetch(binding.fetch_constant);
re_out << fmt::format(
" stream fc={} base=0x{:08X} stride_words={} size_words={} "
"endian={} type={}\n",
binding.fetch_constant, uint32_t(fetch.address) << 2,
binding.stride_words, uint32_t(fetch.size), uint32_t(fetch.endian),
uint32_t(fetch.type));
for (const auto& attr : binding.attributes) {
const auto& a = attr.fetch_instr.attributes;
re_out << fmt::format(
" attr fmt={}({}) offset_words={} stride_words={} signed={} "
"int={} exp_adjust={}\n",
uint32_t(a.data_format), ReVertexFormatName(a.data_format), a.offset,
a.stride, a.is_signed ? 1 : 0, a.is_integer ? 1 : 0, a.exp_adjust);
}
}
// Dump the first few vertex POSITIONS from guest memory. The f32 position
// bytes are identical between the guest buffer and the on-disc .xpr (only
// f16 pairs are rearranged on load), so these values can be searched for in
// the file to locate a mesh whose in-file offset is otherwise unknown
// (e.g. multi-XBG7 body meshes). See docs/re/structures/xbg7-mesh.md.
if (!vs->vertex_bindings().empty()) {
const auto& binding = vs->vertex_bindings()[0];
xenos::xe_gpu_vertex_fetch_t fetch =
register_file_->GetVertexFetch(binding.fetch_constant);
// Position = the first f32×3 attribute (offset is in dwords).
int32_t pos_off_bytes = -1;
for (const auto& attr : binding.attributes) {
if (attr.fetch_instr.attributes.data_format ==
xenos::VertexFormat::k_32_32_32_FLOAT) {
pos_off_bytes = attr.fetch_instr.attributes.offset * 4;
break;
}
}
uint32_t stride = binding.stride_words * 4;
uint32_t vbase = uint32_t(fetch.address) << 2;
uint32_t buf_bytes = uint32_t(fetch.size) * 4;
if (pos_off_bytes >= 0 && stride > 0) {
uint32_t max_v = buf_bytes / stride;
uint32_t n = max_v < 8 ? max_v : 8;
re_out << " positions:";
for (uint32_t v = 0; v < n; ++v) {
uint32_t a = vbase + v * stride + uint32_t(pos_off_bytes);
const uint8_t* p = memory_->TranslatePhysical<const uint8_t*>(a);
if (!p) {
break;
}
auto be_f32 = [](const uint8_t* q) {
uint32_t w = (uint32_t(q[0]) << 24) | (uint32_t(q[1]) << 16) |
(uint32_t(q[2]) << 8) | uint32_t(q[3]);
float f;
std::memcpy(&f, &w, 4);
return f;
};
re_out << fmt::format(" ({:.4f},{:.4f},{:.4f})", be_f32(p),
be_f32(p + 4), be_f32(p + 8));
}
re_out << "\n";
}
}
} else {
re_out << " (vertex shader not analyzed yet)\n";
}
re_out.flush();
}
// This should be written completely differently with support for different // This should be written completely differently with support for different
// types. // types.
void SaveGPUSetting(GPUSetting setting, uint64_t value) { void SaveGPUSetting(GPUSetting setting, uint64_t value) {

View File

@@ -446,6 +446,13 @@ class CommandProcessor {
} }
virtual bool IssueCopy() { return false; } virtual bool IssueCopy() { return false; }
// Reverse-engineering aid (cvar `log_draws`): dump the guest's exact
// primitive type + index buffer + per-stream vertex declaration for each
// distinct draw to a dedicated file, for decoding game mesh formats.
// No-op unless the cvar is enabled. Defined in command_processor.cc.
void LogDrawForRE(uint32_t vgt_draw_initiator_value,
const IndexBufferInfo* index_buffer_info);
// "Actual" is for the command processor thread, to be read by the // "Actual" is for the command processor thread, to be read by the
// implementations. // implementations.
SwapPostEffect GetActualSwapPostEffect() const { SwapPostEffect GetActualSwapPostEffect() const {

View File

@@ -13,6 +13,8 @@
#include "xenia/base/clock.h" #include "xenia/base/clock.h"
#include "xenia/base/logging.h" #include "xenia/base/logging.h"
#include "xenia/base/math.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/profiling.h"
#include "xenia/base/threading.h" #include "xenia/base/threading.h"
#include "xenia/config.h" #include "xenia/config.h"
@@ -36,6 +38,32 @@ DEFINE_bool(
"runtime spikes and freezes when playing the game not for the first time.", "runtime spikes and freezes when playing the game not for the first time.",
"GPU"); "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 xe {
namespace gpu { namespace gpu {
@@ -333,12 +361,117 @@ void GraphicsSystem::DispatchInterruptCallback(uint32_t source, uint32_t cpu) {
interrupt_callback_data_, source, 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() { void GraphicsSystem::MarkVblank() {
SCOPE_profile_cpu_f("gpu"); SCOPE_profile_cpu_f("gpu");
// Increment vblank counter (so the game sees us making progress). // Increment vblank counter (so the game sees us making progress).
command_processor_->increment_counter(); 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 // 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 // something wrong and the CP will block waiting for code that
// needs to be run in the interrupt. // needs to be run in the interrupt.

View File

@@ -1152,6 +1152,11 @@ bool COMMAND_PROCESSOR::ExecutePacketType3Draw(
uint32_t(vgt_draw_initiator.prim_type), uint32_t(vgt_draw_initiator.prim_type),
uint32_t(vgt_draw_initiator.source_select)); uint32_t(vgt_draw_initiator.source_select));
} }
// Reverse-engineering aid (no-op unless the `log_draws` cvar is set):
// record the guest's primitive type + index buffer + vertex declaration.
// Placed after IssueDraw so the vertex shader has been analyzed.
COMMAND_PROCESSOR::LogDrawForRE(
vgt_draw_initiator.value, is_indexed ? &index_buffer_info : nullptr);
} }
} }

View File

@@ -14,3 +14,12 @@ DEFINE_bool(headless, false,
"UI"); "UI");
DEFINE_bool(log_high_frequency_kernel_calls, false, DEFINE_bool(log_high_frequency_kernel_calls, false,
"Log kernel calls with the kHighFrequency tag.", "Logging"); "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(headless);
DECLARE_bool(log_high_frequency_kernel_calls); DECLARE_bool(log_high_frequency_kernel_calls);
DECLARE_bool(audit_handle_lifecycle);
DECLARE_uint32(audit_track_event_handle);
#endif // XENIA_KERNEL_KERNEL_FLAGS_H_ #endif // XENIA_KERNEL_KERNEL_FLAGS_H_

View File

@@ -15,6 +15,7 @@
#include "xenia/base/logging.h" #include "xenia/base/logging.h"
#include "xenia/emulator.h" #include "xenia/emulator.h"
#include "xenia/hid/input_system.h" #include "xenia/hid/input_system.h"
#include "xenia/kernel/kernel_flags.h"
#include "xenia/kernel/user_module.h" #include "xenia/kernel/user_module.h"
#include "xenia/kernel/util/shim_utils.h" #include "xenia/kernel/util/shim_utils.h"
#include "xenia/kernel/xboxkrnl/xboxkrnl_memory.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); auto ev = object_table()->LookupObject<XEvent>(event_handle);
assert_not_null(ev); assert_not_null(ev);
if (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); ev->Set(0, false);
} }
} }
@@ -1372,6 +1381,47 @@ void KernelState::EmulateCPInterruptDPC(uint32_t interrupt_callback,
return; 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(); auto thread = kernel::XThread::GetCurrentThread();
assert_not_null(thread); assert_not_null(thread);

View File

@@ -8,7 +8,9 @@
*/ */
#include "xenia/base/logging.h" #include "xenia/base/logging.h"
#include "xenia/cpu/thread_state.h"
#include "xenia/kernel/info/file.h" #include "xenia/kernel/info/file.h"
#include "xenia/kernel/kernel_flags.h"
#include "xenia/kernel/kernel_state.h" #include "xenia/kernel/kernel_state.h"
#include "xenia/kernel/util/shim_utils.h" #include "xenia/kernel/util/shim_utils.h"
#include "xenia/kernel/xboxkrnl/xboxkrnl_private.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 (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); ev->Set(0, false);
} }
@@ -294,6 +303,13 @@ dword_result_t NtReadFileScatter_entry(
} }
if (ev && signal_event) { 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); 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 (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); ev->Set(0, false);
} }

View File

@@ -10,6 +10,8 @@
#include "xenia/kernel/xboxkrnl/xboxkrnl_ob.h" #include "xenia/kernel/xboxkrnl/xboxkrnl_ob.h"
#include "xenia/base/logging.h" #include "xenia/base/logging.h"
#include "xenia/cpu/processor.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/kernel_state.h"
#include "xenia/kernel/xboxkrnl/xboxkrnl_private.h" #include "xenia/kernel/xboxkrnl/xboxkrnl_private.h"
#include "xenia/kernel/xboxkrnl/xboxkrnl_threading.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 = X_STATUS result =
kernel_state()->object_table()->DuplicateHandle(handle, &new_handle); 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) { if (new_handle_ptr) {
*new_handle_ptr = new_handle; *new_handle_ptr = new_handle;
} }

View File

@@ -8,10 +8,12 @@
*/ */
#include "xenia/kernel/xboxkrnl/xboxkrnl_threading.h" #include "xenia/kernel/xboxkrnl/xboxkrnl_threading.h"
#include <atomic>
#include "xenia/base/atomic.h" #include "xenia/base/atomic.h"
#include "xenia/base/clock.h" #include "xenia/base/clock.h"
#include "xenia/base/platform.h" #include "xenia/base/platform.h"
#include "xenia/cpu/processor.h" #include "xenia/cpu/processor.h"
#include "xenia/kernel/kernel_flags.h"
#include "xenia/kernel/util/shim_utils.h" #include "xenia/kernel/util/shim_utils.h"
#include "xenia/kernel/xboxkrnl/xboxkrnl_private.h" #include "xenia/kernel/xboxkrnl/xboxkrnl_private.h"
#include "xenia/kernel/xsemaphore.h" #include "xenia/kernel/xsemaphore.h"
@@ -22,6 +24,9 @@ namespace xe {
namespace kernel { namespace kernel {
namespace xboxkrnl { 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 // r13 + 0x100: pointer to thread local state
// Thread local state: // Thread local state:
// 0x058: kernel time // 0x058: kernel time
@@ -192,6 +197,30 @@ dword_result_t ExCreateThread_entry(lpdword_t handle_ptr, dword_t stack_size,
lpvoid_t start_address, lpvoid_t start_address,
lpvoid_t start_context, lpvoid_t start_context,
dword_t creation_flags) { 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, return ExCreateThread(handle_ptr, stack_size, thread_id_ptr,
xapi_thread_startup, start_address, start_context, xapi_thread_startup, start_address, start_context,
creation_flags); creation_flags);
@@ -582,6 +611,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_result_t KeSetEvent_entry(pointer_t<X_KEVENT> event_ptr,
dword_t increment, dword_t wait) { 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); return xeKeSetEvent(event_ptr, increment, wait);
} }
DECLARE_XBOXKRNL_EXPORT2(KeSetEvent, kThreading, kImplemented, kHighFrequency); DECLARE_XBOXKRNL_EXPORT2(KeSetEvent, kThreading, kImplemented, kHighFrequency);
@@ -639,6 +710,29 @@ dword_result_t NtCreateEvent_entry(
if (handle_ptr) { if (handle_ptr) {
*handle_ptr = ev->handle(); *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; return X_STATUS_SUCCESS;
} }
DECLARE_XBOXKRNL_EXPORT1(NtCreateEvent, kThreading, kImplemented); DECLARE_XBOXKRNL_EXPORT1(NtCreateEvent, kThreading, kImplemented);
@@ -664,6 +758,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) { 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); return xeNtSetEvent(handle, previous_state_ptr);
} }
DECLARE_XBOXKRNL_EXPORT2(NtSetEvent, kThreading, kImplemented, kHighFrequency); DECLARE_XBOXKRNL_EXPORT2(NtSetEvent, kThreading, kImplemented, kHighFrequency);
@@ -1038,9 +1152,43 @@ dword_result_t NtWaitForSingleObjectEx_entry(dword_t object_handle,
dword_t wait_mode, dword_t wait_mode,
dword_t alertable, dword_t alertable,
lpqword_t timeout_ptr) { 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; 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); 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, DECLARE_XBOXKRNL_EXPORT3(NtWaitForSingleObjectEx, kThreading, kImplemented,
kBlocking, kHighFrequency); kBlocking, kHighFrequency);

View File

@@ -9,6 +9,7 @@
#include "xenia/kernel/xconfig.h" #include "xenia/kernel/xconfig.h"
#include "xenia/base/cvar.h"
#include "xenia/base/logging.h" #include "xenia/base/logging.h"
#include "xenia/base/filesystem.h" #include "xenia/base/filesystem.h"
@@ -19,6 +20,17 @@
#include <ranges> #include <ranges>
// Speaker configuration reported to the guest (XCONFIG_USER_AUDIO_FLAGS).
// Default = 0 (Digital Stereo): the Linux/ALSA APU driver's 5.1 (6-channel)
// path yields silence in some titles (e.g. Project Sylpheed missions), while
// stereo output works, so stereo is the safe default. Set to 0x00010001 for
// Dolby Digital + Pro Logic surround.
DEFINE_uint32(guest_audio_flags, 0,
"Speaker config reported to the guest (XCONFIG_USER_AUDIO_FLAGS): "
"0 = Digital Stereo (safe on Linux/ALSA), 0x00010001 = Dolby "
"Digital + Pro Logic surround.",
"APU");
namespace xe { namespace xe {
namespace kernel { namespace kernel {
@@ -94,7 +106,7 @@ void XConfig::SetDefaults() {
xconfig_data_.user.language = static_cast<uint32_t>(XLanguage::kEnglish); xconfig_data_.user.language = static_cast<uint32_t>(XLanguage::kEnglish);
xconfig_data_.user.country = xconfig_data_.user.country =
static_cast<uint8_t>(XOnlineCountry::kUnitedStates); static_cast<uint8_t>(XOnlineCountry::kUnitedStates);
xconfig_data_.user.audio_flags = DolbyDigital | DolbyProLogic; xconfig_data_.user.audio_flags = cvars::guest_audio_flags;
xconfig_data_.user.av_pack_hdmi_sz = XHDTVResolution.at(1).to_host(); xconfig_data_.user.av_pack_hdmi_sz = XHDTVResolution.at(1).to_host();
xconfig_data_.user.av_pack_component_sz = XHDTVResolution.at(1).to_host(); xconfig_data_.user.av_pack_component_sz = XHDTVResolution.at(1).to_host();
xconfig_data_.user.av_pack_vga_sz = XVGAResolution.at(3).to_host(); xconfig_data_.user.av_pack_vga_sz = XVGAResolution.at(3).to_host();

View File

@@ -11,6 +11,8 @@
#include "xenia/base/byte_stream.h" #include "xenia/base/byte_stream.h"
#include "xenia/base/logging.h" #include "xenia/base/logging.h"
#include "xenia/cpu/thread_state.h"
#include "xenia/kernel/kernel_flags.h"
namespace xe { namespace xe {
namespace kernel { 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) { 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); set_priority_increment(priority_increment);
event_->Set(); event_->Set();
return 1; return 1;

View File

@@ -21,6 +21,7 @@
#include "xenia/base/threading.h" #include "xenia/base/threading.h"
#include "xenia/cpu/processor.h" #include "xenia/cpu/processor.h"
#include "xenia/emulator.h" #include "xenia/emulator.h"
#include "xenia/kernel/kernel_flags.h"
#include "xenia/kernel/kernel_state.h" #include "xenia/kernel/kernel_state.h"
#include "xenia/kernel/user_module.h" #include "xenia/kernel/user_module.h"
#include "xenia/kernel/xboxkrnl/xboxkrnl_threading.h" #include "xenia/kernel/xboxkrnl/xboxkrnl_threading.h"
@@ -544,6 +545,13 @@ X_STATUS XThread::Terminate(int exit_code) {
void XThread::Execute() { void XThread::Execute() {
XELOGKERNEL("XThread::Execute thid {} (handle={:08X}, '{}', native={:08X})", XELOGKERNEL("XThread::Execute thid {} (handle={:08X}, '{}', native={:08X})",
thread_id_, handle(), thread_name_, thread_->system_id()); 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. // Let the kernel know we are starting.
kernel_state()->OnThreadExecute(this); kernel_state()->OnThreadExecute(this);

View File

@@ -332,7 +332,7 @@ else()
X86_AVX512VNNI X86_AVX512VNNI
WITH_GZFILEOP 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 # GCC/Clang have native __builtin_ctz/__builtin_ctzll (MSVC gets these
# from fallback_builtins.h). Defining these enables optimized compare256 # from fallback_builtins.h). Defining these enables optimized compare256
# and longest_match codepaths. # and longest_match codepaths.

View File

@@ -73,6 +73,16 @@ def main():
# Start with base command — use wine on non-Windows platforms. # Start with base command — use wine on non-Windows platforms.
if sys.platform != "win32": 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] compiler_args = ["wine", fxc]
else: else:
compiler_args = [fxc] compiler_args = [fxc]