17 Commits

Author SHA1 Message Date
15fe11d5d9 [HID] file-pad: implement GetKeystroke -- menus do not read GetState
Some checks failed
Orchestrator / Commit Message Validation (push) Has been skipped
Orchestrator / Lint (push) Failing after 2m3s
Orchestrator / Windows (x86-64) (push) Has been skipped
Orchestrator / Linux (x86-64) (push) Has been skipped
Orchestrator / Create Release (push) Has been skipped
The pad looked correct and did nothing. Its own log showed A arriving, the
emulator sat on "PRESS (A) BUTTON", and the title never advanced.

Cause: 360 front-ends poll XamInputGetKeystrokeEx, not XamInputGetState. This
title imports both, and its menus use the keystroke path; the driver returned
X_ERROR_EMPTY there, so every scripted press went into the void while GetState
faithfully reported a button nobody asked about.

Implement it edge-triggered, one event per call: KEYUPs for everything released
first, then KEYDOWNs, matching the SDL driver's ordering (so a thumb transition
clears before it sets). Deliberately NO auto-repeat -- scripted input wants
exactly one event per press, and repeat is precisely what makes menu steps
overshoot. Bits without a virtual key (guide, unused) are swallowed rather than
re-offered forever.

Verified on the real game: title -> main menu -> EXTRAS driven entirely from the
pad file, with `[file-pad] keystroke vk=5800 down/up` in the log for each press.
2026-08-13 20:27:38 +00:00
e3e17e4951 [HID] file-pad: nanosecond change detection, and log every state change
Some checks failed
Orchestrator / Commit Message Validation (push) Has been skipped
Orchestrator / Lint (push) Failing after 2m24s
Orchestrator / Windows (x86-64) (push) Has been skipped
Orchestrator / Linux (x86-64) (push) Has been skipped
Orchestrator / Create Release (push) Has been skipped
Two things that only show up once you actually script this pad.

st_mtime is whole seconds. Combined with size it looked like enough and is not:
a script stepping a menu writes several same-length states per second
(`press=A` then `press=B`, both 8 bytes), and every one after the first was
silently dropped -- the emulator simply did not react, with nothing in any log
to say why. Compare st_mtim.tv_nsec as well, and track whether the file existed
at all so a delete is registered once rather than every frame.

Also log one line per state change (not per frame, so it stays quiet). Driving
the emulator headless means there is nothing to watch; this line is the only
proof that a scripted press was picked up, which turns "did my input land?" from
a guess into a grep.
2026-08-13 20:14:29 +00:00
d15c8cfab6 [HID] file-pad: a controller driven by a text file, for scripted RE
Some checks failed
Orchestrator / Commit Message Validation (push) Has been skipped
Orchestrator / Lint (push) Failing after 2m24s
Orchestrator / Windows (x86-64) (push) Has been skipped
Orchestrator / Linux (x86-64) (push) Has been skipped
Orchestrator / Create Release (push) Has been skipped
The scripted-input tool this project uses for reverse engineering created its
pad through /dev/uinput. Input devices are NOT namespaced by the kernel, so a
uinput device created inside a container registers with the HOST's input stack:
every trigger hold and button press is delivered to whatever on the host reads
gamepads, not just to the emulator. That was noticed the hard way, and it made
every runtime experiment -- booting, menu navigation, unit harvesting, flight
measurement -- unusable from inside the box.

This driver takes the kernel out of the loop. Pad state lives in an ordinary
text file only the container can see; GetState re-reads it when it changes.
Nothing is registered with the host and no X server is involved. A bonus for RE:
analogue values are exact rather than whatever a virtual stick quantises to.

    press=A,START     buttons by name, comma separated
    buttons=0x1010    or the raw XINPUT mask
    lt=0 rt=255       triggers, 0..255
    lx=0 ly=0         thumbs, -32768..32767

Absent keys are neutral, so `press=A` alone is a valid file, and a missing or
empty file means no input -- the safe default if it is deleted mid-run.

Selected with --hid=file, path from --pad_file (default /tmp/xenia_pad.txt).
Deliberately NOT part of "any": this pad has to be asked for. Header-only, so it
adds no build target and no cost to anyone not using it.
2026-08-13 20:12:02 +00:00
31366e5cac [GPU] ship-capture: log each draw's index buffer, and key the de-dup on it
The offline XBG7 decoder can only assume where a block's index data lives; the
capture now states it. Each captured draw gains an `ib base=… count=… fmt=…
endian=… len=… delta_vb=… min=… max=… idx: …` line — the guest index base, the
draw's index count, and the min/max index value read out of guest memory, which
together say both where the index buffer sits relative to the vertex buffer and
how much of the vertex pool the draw covers.

Also mix the index range into the capture's de-dup key. The engine issues several
draws over one vertex buffer, each indexing a sub-range (a 119-vertex hull LOD
draws 21 indices first, then 225); keying on (vbase, transform) alone kept only
the first batch, which reads like a mysteriously short draw and cost an earlier
session a spurious "the capture disagrees with the descriptor" mystery.

Read-only diagnostics behind the existing F10 hotkey; no emulation behaviour
changes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NptfmpjdpNCKEez6d2xvA9
2026-08-13 05:58:02 +00:00
MechaCat02
a08526dff0 [GPU] ship-capture: numbered per-press snapshots + capture every instance
Some checks failed
Orchestrator / Commit Message Validation (push) Has been skipped
Orchestrator / Lint (push) Failing after 1m45s
Orchestrator / Windows (x86-64) (push) Has been skipped
Orchestrator / Linux (x86-64) (push) Has been skipped
Orchestrator / Create Release (push) Has been skipped
F10 now writes xenia_ship_capture_NN.log (NN = press number) so several angles
can be captured in one run without overwriting, and de-dups by (vertex-buffer
address, c0..c2 WVP hash) instead of address alone -- the same part buffer
drawn at different transforms (two engine nacelles, each ship of a fleet) now
yields one record per distinct placement. Seen-set cap raised to 8192.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 20:35:20 +02:00
MechaCat02
93fe5d69df [GPU] ship-capture: also dump VS float constants (the placement matrix)
The captured vertex BUFFER holds LOCAL positions (byte-identical to the .xpr) —
capital-ship parts are placed entirely in the vertex shader, not in the buffer.
So the per-part world/WVP matrix lives in the VS float constants. Extend the F10
capture to dump the first 48 vec4 from the SQ_VS_CONST base per draw; diffing two
parts isolates the world matrix (the camera VP block is shared across draws).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 20:35:20 +02:00
MechaCat02
067a373734 [GPU] F10 ship-placement capture: dump world-space draw vertices for RE
One-shot snapshot for reversing capital-ship part placement. F10 arms
RequestShipCaptureFrame(); the next batch of draws is dumped to
xenia_ship_capture.log with each draw's guest vertex-buffer address, vertex/
index counts, and up to 64 WORLD-space vertex positions. Capital-ship parts are
transformed into world space before the draw (unlike stage geometry, which
matches the .xpr byte-for-byte), so these are ground truth for the assembled
placement; a reborn-side correlator affine-fits each decoded .xpr part to a
captured draw to recover its exact transform. De-duped by buffer address, gated
independently of the log_draws cvar, budget-capped at 8000 draws / 4096 buffers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 20:35:20 +02:00
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
19 changed files with 1158 additions and 16 deletions

View File

@@ -314,9 +314,14 @@ else()
)
add_compile_options($<$<CONFIG:Release>:-O3>)
# Link-time optimization (use lld and llvm-ar/ranlib to avoid system
# linker/archiver LTO plugin version mismatch with clang's bitcode)
add_compile_options($<$<CONFIG:Release>:-flto=thin>)
add_link_options($<$<CONFIG:Release>:-flto=thin>)
# linker/archiver LTO plugin version mismatch with clang's bitcode).
# Guarded so memory-constrained hosts can build Release without the
# 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>)
find_program(LLVM_AR NAMES llvm-ar)
find_program(LLVM_RANLIB NAMES llvm-ranlib)

View File

@@ -1067,6 +1067,11 @@ void EmulatorWindow::OnKeyDown(ui::KeyEvent& e) {
case ui::VirtualKey::kF12: {
TakeScreenshot();
} break;
case ui::VirtualKey::kF10: {
// RE: snapshot the next frame's draws (world-space vertex positions) to
// xenia_ship_capture.log for capital-ship placement correlation.
xe::gpu::RequestShipCaptureFrame();
} break;
case ui::VirtualKey::kEscape: {
// Allow users to escape fullscreen (but not enter it).
@@ -1134,10 +1139,20 @@ void EmulatorWindow::OnMouseUp(const ui::MouseEvent& e) {
void EmulatorWindow::TakeScreenshot() {
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);
if (!GetGraphicsSystemPresenter()->CaptureGuestOutput(image) ||
GetGraphicsSystemPresenter() == nullptr) {
if (!presenter->CaptureGuestOutput(image)) {
imgui_drawer_->EnableNotifications(true);
XELOGE("Failed to capture guest output for screenshot");
return;
}

View File

@@ -64,6 +64,12 @@
#include "xenia/hid/winkey/winkey_hid.h"
#include "xenia/hid/xinput/xinput_hid.h"
#endif // XE_PLATFORM_WIN32
// RE aid: a controller driven by a text file instead of a kernel input device.
// A uinput pad created inside a container registers with the HOST's input stack
// (input devices are not namespaced), so scripted input leaks to the desktop.
// This one is visible only to whoever can read the file. Header-only on purpose:
// it adds no build target.
#include "xenia/hid/file/file_input_driver.h"
#if XE_PLATFORM_WIN32
#define APU_OPTIONS "[any, nop, sdl, xaudio2]"
@@ -72,7 +78,7 @@
#elif XE_PLATFORM_LINUX
#define APU_OPTIONS "[any, alsa, nop, sdl]"
#define GPU_OPTIONS "[any, vulkan, null]"
#define HID_OPTIONS "[any, nop, sdl]"
#define HID_OPTIONS "[any, file, nop, sdl]"
#else
#define APU_OPTIONS "[any, nop, sdl]"
#define GPU_OPTIONS "[any, vulkan, null]"
@@ -82,6 +88,11 @@
DEFINE_string(apu, "any", "Audio system. Use: " APU_OPTIONS, "APU");
DEFINE_string(gpu, "any", "Graphics system. Use: " GPU_OPTIONS, "GPU");
DEFINE_string(hid, "any", "Input system. Use: " HID_OPTIONS, "HID");
DEFINE_string(pad_file, "/tmp/xenia_pad.txt",
"Controller state file read by `--hid=file`: key=value pairs such "
"as `press=A,START lt=0 rt=255 lx=0 ly=0`. Absent keys are "
"neutral, a missing file means no input.",
"HID");
DEFINE_path(
storage_root, "",
@@ -446,6 +457,10 @@ std::vector<std::unique_ptr<hid::InputDriver>> EmulatorApp::CreateInputDrivers(
if (cvars::hid.compare("nop") == 0) {
drivers.emplace_back(
xe::hid::nop::Create(window, EmulatorWindow::kZOrderHidInput));
} else if (cvars::hid.compare("file") == 0) {
// Explicit, never part of "any": this pad must be asked for.
drivers.emplace_back(
xe::hid::filepad::Create(window, EmulatorWindow::kZOrderHidInput));
} else {
Factory<hid::InputDriver, ui::Window*, size_t> factory;
#if XE_PLATFORM_WIN32

View File

@@ -336,8 +336,30 @@ void ALSAAudioDriver::WorkerThread() {
size_t current_write = write_index_.load(std::memory_order_acquire);
if (current_read == current_write) {
// No data available, sleep and try again
std::this_thread::sleep_for(std::chrono::milliseconds(5));
// No guest audio available. During long guest stalls (e.g. a mission
// "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;
}

View File

@@ -49,6 +49,22 @@ DEFINE_uint32(audit_jit_prolog_pc, 0,
"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,
@@ -482,11 +498,16 @@ uint64_t AuditLogJitPrologArgs(void* raw_context, uint64_t /*unused*/) {
"r6={:08X} r7={:08X} r8={:08X} r9={:08X} r10={:08X} lr={:08X}",
pc, tid, r3, r4, r5, r6, r7, r8, r9, r10, lr);
// Dump 64 bytes at host(r3) if r3 looks like a plausible guest VA.
// 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 < 0x40; off += 16) {
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);
@@ -503,6 +524,28 @@ uint64_t AuditLogJitPrologArgs(void* raw_context, uint64_t /*unused*/) {
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;
}

View File

@@ -9,6 +9,9 @@
#include "xenia/gpu/command_processor.h"
#include <fstream>
#include <unordered_set>
#include "third_party/fmt/include/fmt/format.h"
#include "xenia/base/byte_stream.h"
#include "xenia/base/clock.h"
@@ -18,6 +21,8 @@
#include "xenia/gpu/gpu_flags.h"
#include "xenia/gpu/graphics_system.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/texture_info.h"
#include "xenia/gpu/xenos_zpd_report.h"
@@ -77,6 +82,14 @@ DEFINE_string(
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(
readback_memexport, false,
"Read data written by memory export in shaders on the CPU. "
@@ -88,6 +101,373 @@ DEFINE_bool(
namespace xe {
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
// ── Ship-placement capture (RE) ─────────────────────────────────────────────
// A hotkey (F10 in the emulator window) requests a one-shot snapshot: the next
// batch of draws is dumped to xenia_ship_capture_NN.log (NN = press number —
// several angles can be captured in one run without overwriting) with each
// draw's guest vertex-buffer address, vertex/index counts, up to 64 LOCAL
// vertex positions, and the first 48 VS float constants (c0..c2 = the WVP rows
// that place the part). De-duped by (buffer address, c0..c2 transform) — NOT by
// address alone — so the SAME part buffer drawn several times (two engine
// nacelles, every ship of a fleet) yields one record per distinct placement.
namespace {
constexpr int kShipCaptureBudget = 8000; // draws to scan per request
std::atomic<uint32_t> g_ship_capture_gen{0};
std::atomic<int> g_ship_capture_remaining{0};
} // namespace
void RequestShipCaptureFrame() {
uint32_t gen = g_ship_capture_gen.fetch_add(1, std::memory_order_relaxed) + 1;
g_ship_capture_remaining.store(kShipCaptureBudget, std::memory_order_relaxed);
XELOGI("[SHIP-CAP] capture armed → xenia_ship_capture_{:02d}.log", gen);
}
void CommandProcessor::CaptureShipDrawForRE(
uint32_t vgt_draw_initiator_value,
const IndexBufferInfo* index_buffer_info) {
if (g_ship_capture_remaining.load(std::memory_order_relaxed) <= 0) {
return;
}
static std::mutex cap_mutex;
static std::ofstream cap_out;
static std::unordered_set<uint64_t> cap_seen; // by (vbase, WVP transform)
static uint32_t cap_gen = 0;
std::lock_guard<std::mutex> lock(cap_mutex);
if (g_ship_capture_remaining.load(std::memory_order_relaxed) <= 0) {
return;
}
// A new request (fresh generation) opens its own numbered snapshot file and
// clears the de-dup set, so each F10 press yields an independent capture.
uint32_t gen = g_ship_capture_gen.load(std::memory_order_relaxed);
if (gen != cap_gen || !cap_out.is_open()) {
if (cap_out.is_open()) {
cap_out.close();
}
auto name = fmt::format("xenia_ship_capture_{:02d}.log", gen);
cap_out.open(name, std::ios::out | std::ios::trunc);
cap_seen.clear();
cap_gen = gen;
XELOGI("[SHIP-CAP] writing {}", name);
}
g_ship_capture_remaining.fetch_sub(1, std::memory_order_relaxed);
if (!cap_out.is_open()) {
return;
}
reg::VGT_DRAW_INITIATOR init;
init.value = vgt_draw_initiator_value;
Shader* vs = active_vertex_shader_;
if (!vs || !vs->is_ucode_analyzed() || vs->vertex_bindings().empty()) {
return;
}
const auto& binding = vs->vertex_bindings()[0];
xenos::xe_gpu_vertex_fetch_t fetch =
register_file_->GetVertexFetch(binding.fetch_constant);
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 || vbase == 0) {
return; // no float-position stream (UI/effects) — skip
}
// One record per distinct (buffer, placement): hash the c0..c2 WVP rows so
// repeated draws of the SAME buffer at DIFFERENT transforms (multi-instance
// parts, fleet ships) each get their own record.
auto vsc0 = register_file_->Get<reg::SQ_VS_CONST>();
uint64_t th = 1469598103934665603ull; // FNV-1a over the 12 c0..c2 floats
for (uint32_t i = 0; i < 12; ++i) {
uint32_t idx = vsc0.base + (i / 4);
if (idx >= 256) {
break;
}
uint32_t r = XE_GPU_REG_SHADER_CONSTANT_000_X + 4 * idx + (i % 4);
uint32_t bits = register_file_->values[r];
th = (th ^ bits) * 1099511628211ull;
}
// Mix the index range into the key as well (added 2026-08-13). The engine
// issues SEVERAL draws over one vertex buffer, each with its own index
// sub-range (the first Stage_02 capture showed a 119-vertex hull LOD drawn
// with 21 indices) — keying on (vbase, transform) alone kept only the first
// batch, which reads like a mysteriously short draw. With the range in the key
// every batch is recorded, so the block's full index extent is observable.
uint64_t ib_key = 0;
if (index_buffer_info) {
ib_key = (uint64_t(index_buffer_info->guest_base) << 20) ^
uint64_t(index_buffer_info->count);
}
uint64_t key = (uint64_t(vbase) << 32) ^ (th & 0xFFFFFFFFull) ^ (th >> 32) ^
(ib_key * 1099511628211ull);
if (!cap_seen.insert(key).second) {
return;
}
if (cap_seen.size() > 8192) {
return;
}
uint32_t vcount = buf_bytes / stride;
cap_out << fmt::format(
"DRAW vbase=0x{:08X} stride={} vcount={} indices={} prim={} vs=0x{:016X}\n",
vbase, stride, vcount, uint32_t(init.num_indices),
uint32_t(init.prim_type), vs->ucode_data_hash());
// The guest's INDEX buffer for this draw. Our offline mesh decoder only
// *assumes* the index buffer sits immediately before the vertex buffer; this
// line is the ground truth for that assumption (ib base vs vbase), and the
// decoded min/max index says how much of the vertex pool the draw really
// covers — which is what the `indices=` field alone cannot answer.
if (index_buffer_info) {
const auto& ib = *index_buffer_info;
bool i32 = ib.format == xenos::IndexFormat::kInt32;
uint32_t icount = ib.count;
cap_out << fmt::format(
" ib base=0x{:08X} count={} fmt={} endian={} len={} delta_vb={}",
ib.guest_base, icount, i32 ? "u32" : "u16", uint32_t(ib.endianness),
ib.length, int64_t(vbase) - int64_t(ib.guest_base));
const uint8_t* ip = memory_->TranslatePhysical<const uint8_t*>(ib.guest_base);
if (ip && icount) {
uint32_t scan = icount < 65536 ? icount : 65536;
uint32_t imin = 0xFFFFFFFFu, imax = 0;
auto rd = [&](uint32_t k) -> uint32_t {
// Guest index data is big-endian in memory (the endianness field says
// how the GPU swaps it); read it that way and record the field so the
// offline side can compensate if a draw ever differs.
const uint8_t* q = ip + (i32 ? k * 4 : k * 2);
return i32 ? (uint32_t(q[0]) << 24) | (uint32_t(q[1]) << 16) |
(uint32_t(q[2]) << 8) | uint32_t(q[3])
: (uint32_t(q[0]) << 8) | uint32_t(q[1]);
};
for (uint32_t k = 0; k < scan; ++k) {
uint32_t v = rd(k);
if (v < imin) imin = v;
if (v > imax) imax = v;
}
cap_out << fmt::format(" min={} max={} idx:", imin, imax);
uint32_t nd = icount < 24 ? icount : 24;
for (uint32_t k = 0; k < nd; ++k) {
cap_out << fmt::format(" {}", rd(k));
}
}
cap_out << "\n";
} else {
cap_out << " ib auto\n";
}
uint32_t n = vcount < 64 ? vcount : 64;
cap_out << " pos:";
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;
};
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;
}
cap_out << fmt::format(" ({:.4f},{:.4f},{:.4f})", be_f32(p), be_f32(p + 4),
be_f32(p + 8));
}
cap_out << "\n";
// Vertex-shader float constants: the buffer holds LOCAL positions, so the
// per-part world (or world-view-projection) matrix that places the part lives
// here as a run of float4 constants. Diffing two parts' constants isolates the
// matrix (the camera VP block is shared). Dump the first 48 vec4 from the VS
// constant base as host-float (the register file stores them host-endian).
auto vsc = register_file_->Get<reg::SQ_VS_CONST>();
uint32_t cbase = vsc.base; // starting float4 index
cap_out << fmt::format(" vsconst base={}:", cbase);
for (uint32_t i = 0; i < 48; ++i) {
uint32_t idx = cbase + i;
if (idx >= 256) {
break;
}
uint32_t r = XE_GPU_REG_SHADER_CONSTANT_000_X + 4 * idx;
float cx = register_file_->Get<float>(r);
float cy = register_file_->Get<float>(r + 1);
float cz = register_file_->Get<float>(r + 2);
float cw = register_file_->Get<float>(r + 3);
cap_out << fmt::format(" c{}=({:.4f},{:.4f},{:.4f},{:.4f})", i, cx, cy, cz,
cw);
}
cap_out << "\n";
cap_out.flush();
}
void CommandProcessor::LogDrawForRE(uint32_t vgt_draw_initiator_value,
const IndexBufferInfo* index_buffer_info) {
// One-shot ship-placement capture runs independently of the log_draws cvar.
CaptureShipDrawForRE(vgt_draw_initiator_value, 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
// types.
void SaveGPUSetting(GPUSetting setting, uint64_t value) {

View File

@@ -35,6 +35,10 @@ class ByteStream;
namespace gpu {
// Arm a one-shot ship-placement capture (see CommandProcessor::CaptureShipDrawForRE).
// Called from the UI thread (F10 hotkey); thread-safe. Defined in command_processor.cc.
void RequestShipCaptureFrame();
enum class GPUSetting { ClearMemoryPageState, ReadbackMemexport };
enum class ReadbackResolveMode {
@@ -446,6 +450,21 @@ class CommandProcessor {
}
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);
// Ship-placement capture (RE): a one-shot snapshot armed by RequestShipCaptureFrame()
// (F10 hotkey). Dumps each draw's guest vertex-buffer address, vertex/index
// counts, and up to 64 WORLD-space positions to xenia_ship_capture.log, so a
// correlator can recover each capital-ship part's exact placement. Defined in
// command_processor.cc.
void CaptureShipDrawForRE(uint32_t vgt_draw_initiator_value,
const IndexBufferInfo* index_buffer_info);
// "Actual" is for the command processor thread, to be read by the
// implementations.
SwapPostEffect GetActualSwapPostEffect() const {

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

@@ -1152,6 +1152,11 @@ bool COMMAND_PROCESSOR::ExecutePacketType3Draw(
uint32_t(vgt_draw_initiator.prim_type),
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

@@ -0,0 +1,341 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* RE aid: a controller whose state comes from a FILE, not from a device.
******************************************************************************
*/
#ifndef XENIA_HID_FILE_FILE_INPUT_DRIVER_H_
#define XENIA_HID_FILE_FILE_INPUT_DRIVER_H_
// Why this exists
// ---------------
// The scripted-input tool used for reverse engineering created its pad through
// `/dev/uinput`. Input devices are NOT namespaced by the kernel, so a uinput
// device created inside a container is registered with the HOST's input stack:
// every trigger hold and button press is delivered to whatever on the host reads
// gamepads, not only to the emulator. That is a real leak, and it was noticed the
// hard way.
//
// This driver takes the kernel out of the loop entirely. The pad state lives in
// an ordinary text file that only this container can see, and `GetState` reads it
// (re-parsing only when the file changes). Nothing is registered with the host,
// no X server is involved either, and — a bonus for RE — the analogue values are
// exact rather than whatever a virtual stick quantises to.
//
// File format: one or more whitespace/newline separated `key=value` pairs.
//
// press=A,START buttons by name (see kButtonNames), comma separated
// buttons=0x1010 or the raw XINPUT mask, if you prefer
// lt=0 rt=255 triggers, 0..255
// lx=0 ly=0 left thumb, -32768..32767
// rx=0 ry=0 right thumb, -32768..32767
//
// Anything absent is neutral, so `press=A` alone is a valid file. An empty or
// missing file means "no input" — which is also the safe default if the file is
// deleted mid-run.
//
// Path: `--pad_file=<path>`, default `/tmp/xenia_pad.txt`. Only user 0 is
// connected; other slots report no device, as a single-pad console would.
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <memory>
#include <string>
#include <sys/stat.h>
#include "xenia/base/cvar.h"
#include "xenia/base/logging.h"
#include "xenia/hid/input_driver.h"
#include "xenia/ui/virtual_key.h"
DECLARE_string(pad_file);
namespace xe {
namespace hid {
namespace filepad {
struct NamedButton {
const char* name;
uint16_t mask;
};
static constexpr NamedButton kButtonNames[] = {
{"UP", X_INPUT_GAMEPAD_DPAD_UP},
{"DOWN", X_INPUT_GAMEPAD_DPAD_DOWN},
{"LEFT", X_INPUT_GAMEPAD_DPAD_LEFT},
{"RIGHT", X_INPUT_GAMEPAD_DPAD_RIGHT},
{"START", X_INPUT_GAMEPAD_START},
{"BACK", X_INPUT_GAMEPAD_BACK},
{"LS", X_INPUT_GAMEPAD_LEFT_THUMB},
{"RS", X_INPUT_GAMEPAD_RIGHT_THUMB},
{"LB", X_INPUT_GAMEPAD_LEFT_SHOULDER},
{"RB", X_INPUT_GAMEPAD_RIGHT_SHOULDER},
{"A", X_INPUT_GAMEPAD_A},
{"B", X_INPUT_GAMEPAD_B},
{"X", X_INPUT_GAMEPAD_X},
{"Y", X_INPUT_GAMEPAD_Y},
};
class FileInputDriver final : public InputDriver {
public:
FileInputDriver(xe::ui::Window* window, size_t window_z_order)
: InputDriver(window, window_z_order) {}
~FileInputDriver() override = default;
X_STATUS Setup() override {
XELOGI("[file-pad] reading controller state from {}", cvars::pad_file);
return X_STATUS_SUCCESS;
}
X_RESULT GetCapabilities(uint32_t user_index, uint32_t flags,
X_INPUT_CAPABILITIES* out_caps) override {
if (user_index != 0) {
return X_ERROR_DEVICE_NOT_CONNECTED;
}
std::memset(out_caps, 0, sizeof(*out_caps));
out_caps->type = 0x01; // XINPUT_DEVTYPE_GAMEPAD
out_caps->sub_type = 0x01; // XINPUT_DEVSUBTYPE_GAMEPAD
out_caps->flags = 0;
out_caps->gamepad.buttons = 0xFFFF;
out_caps->gamepad.left_trigger = 0xFF;
out_caps->gamepad.right_trigger = 0xFF;
out_caps->gamepad.thumb_lx = static_cast<int16_t>(0xFFFFu);
out_caps->gamepad.thumb_ly = static_cast<int16_t>(0xFFFFu);
out_caps->gamepad.thumb_rx = static_cast<int16_t>(0xFFFFu);
out_caps->gamepad.thumb_ry = static_cast<int16_t>(0xFFFFu);
return X_ERROR_SUCCESS;
}
X_RESULT GetState(uint32_t user_index, X_INPUT_STATE* out_state) override {
if (user_index != 0) {
return X_ERROR_DEVICE_NOT_CONNECTED;
}
Refresh();
std::memset(out_state, 0, sizeof(*out_state));
out_state->packet_number = packet_;
out_state->gamepad.buttons = buttons_;
out_state->gamepad.left_trigger = lt_;
out_state->gamepad.right_trigger = rt_;
out_state->gamepad.thumb_lx = lx_;
out_state->gamepad.thumb_ly = ly_;
out_state->gamepad.thumb_rx = rx_;
out_state->gamepad.thumb_ry = ry_;
return X_ERROR_SUCCESS;
}
X_RESULT SetState(uint32_t user_index, X_INPUT_VIBRATION* vibration) override {
return user_index == 0 ? X_ERROR_SUCCESS : X_ERROR_DEVICE_NOT_CONNECTED;
}
// Menus do NOT read the pad through GetState. "PRESS (A) BUTTON" and most
// 360 front-ends poll XamInputGetKeystrokeEx, so a driver that only answers
// GetState looks completely dead on a title screen while its own log happily
// shows the button arriving. Returning X_ERROR_EMPTY here is what made the
// first scripted run press A into the void.
//
// One event per call, edge triggered: KEYUPs for everything released, then
// KEYDOWNs for everything pressed, exactly as the SDL driver orders them.
// Deliberately NO auto-repeat — scripted input wants precisely one event per
// press, and repeat is what makes menu steps overshoot.
X_RESULT GetKeystroke(uint32_t user_index, uint32_t flags,
X_INPUT_KEYSTROKE* out_keystroke) override {
const bool user_any = user_index == 0xFF || user_index == 0xFFFFFFFFu;
if (!user_any && user_index != 0) {
return X_ERROR_DEVICE_NOT_CONNECTED;
}
if (!out_keystroke) {
return X_ERROR_BAD_ARGUMENTS;
}
Refresh();
// Bit index in X_INPUT_GAMEPAD::buttons -> virtual key. Order matters: it is
// the order multiple simultaneous changes are reported in.
static constexpr uint16_t kVk[16] = {
uint16_t(ui::VirtualKey::kXInputPadDpadUp),
uint16_t(ui::VirtualKey::kXInputPadDpadDown),
uint16_t(ui::VirtualKey::kXInputPadDpadLeft),
uint16_t(ui::VirtualKey::kXInputPadDpadRight),
uint16_t(ui::VirtualKey::kXInputPadStart),
uint16_t(ui::VirtualKey::kXInputPadBack),
uint16_t(ui::VirtualKey::kXInputPadLThumbPress),
uint16_t(ui::VirtualKey::kXInputPadRThumbPress),
uint16_t(ui::VirtualKey::kXInputPadLShoulder),
uint16_t(ui::VirtualKey::kXInputPadRShoulder),
0, /* guide */
0, /* unused */
uint16_t(ui::VirtualKey::kXInputPadA),
uint16_t(ui::VirtualKey::kXInputPadB),
uint16_t(ui::VirtualKey::kXInputPadX),
uint16_t(ui::VirtualKey::kXInputPadY),
};
const uint16_t changed = static_cast<uint16_t>(buttons_ ^ reported_);
if (!changed) {
return X_ERROR_EMPTY;
}
for (int pass = 0; pass < 2; ++pass) {
const bool clear_pass = pass == 0;
for (uint8_t i = 0; i < 16; ++i) {
const uint16_t bit = static_cast<uint16_t>(1u << i);
if (!(changed & bit) || kVk[i] == 0) {
continue;
}
const bool pressed = (buttons_ & bit) != 0;
if (clear_pass == pressed) {
continue;
}
reported_ = static_cast<uint16_t>(pressed ? (reported_ | bit)
: (reported_ & ~bit));
out_keystroke->virtual_key = kVk[i];
out_keystroke->unicode = 0;
out_keystroke->flags =
pressed ? X_INPUT_KEYSTROKE_KEYDOWN : X_INPUT_KEYSTROKE_KEYUP;
out_keystroke->user_index = 0;
out_keystroke->hid_code = 0;
XELOGI("[file-pad] keystroke vk={:04X} {}", kVk[i],
pressed ? "down" : "up");
return X_ERROR_SUCCESS;
}
}
// Only bits without a virtual key changed (guide/unused): swallow them so
// the caller is not asked again forever.
reported_ = buttons_;
return X_ERROR_EMPTY;
}
InputType GetInputType() const override { return InputType::Controller; }
private:
// Re-parse only when the file actually changed: `GetState` is polled every
// frame and a stat is far cheaper than a read+parse.
//
// The change test uses **nanosecond** mtime, not `st_mtime`. Whole-second
// granularity plus size looked sufficient and is not: a script that steps a
// menu writes several same-length states per second (`press=A` then `press=B`,
// both 8 bytes), and every one of those after the first would be silently
// dropped. That failure is invisible — the emulator just does not react — so
// it is worth the extra field.
void Refresh() {
struct stat st;
if (::stat(cvars::pad_file.c_str(), &st) != 0) {
if (present_) {
present_ = false;
Neutral();
++packet_;
XELOGI("[file-pad] {} gone -> neutral", cvars::pad_file);
}
return;
}
if (present_ && st.st_mtim.tv_sec == mtime_sec_ &&
st.st_mtim.tv_nsec == mtime_nsec_ && st.st_size == size_) {
return;
}
present_ = true;
mtime_sec_ = st.st_mtim.tv_sec;
mtime_nsec_ = st.st_mtim.tv_nsec;
size_ = st.st_size;
std::FILE* f = std::fopen(cvars::pad_file.c_str(), "rb");
if (!f) {
Neutral();
return;
}
char buf[512] = {0};
size_t n = std::fread(buf, 1, sizeof(buf) - 1, f);
std::fclose(f);
buf[n] = '\0';
Parse(buf);
++packet_;
// One line per change (not per frame): with no display to watch, this log is
// the only proof that a scripted press was actually picked up.
XELOGI("[file-pad] #{} buttons={:04X} lt={} rt={} lx={} ly={} rx={} ry={}",
packet_, buttons_, lt_, rt_, lx_, ly_, rx_, ry_);
}
void Neutral() {
buttons_ = 0;
lt_ = rt_ = 0;
lx_ = ly_ = rx_ = ry_ = 0;
}
void Parse(const char* text) {
Neutral();
std::string s(text);
size_t pos = 0;
while (pos < s.size()) {
size_t end = s.find_first_of(" \t\r\n", pos);
if (end == std::string::npos) {
end = s.size();
}
std::string tok = s.substr(pos, end - pos);
pos = end + 1;
size_t eq = tok.find('=');
if (eq == std::string::npos) {
continue;
}
std::string key = tok.substr(0, eq), val = tok.substr(eq + 1);
if (key == "press") {
size_t p = 0;
while (p < val.size()) {
size_t c = val.find(',', p);
if (c == std::string::npos) {
c = val.size();
}
std::string name = val.substr(p, c - p);
p = c + 1;
for (const auto& b : kButtonNames) {
if (name == b.name) {
buttons_ |= b.mask;
break;
}
}
}
} else if (key == "buttons") {
buttons_ |= static_cast<uint16_t>(std::strtoul(val.c_str(), nullptr, 0));
} else if (key == "lt") {
lt_ = Clamp8(std::strtol(val.c_str(), nullptr, 0));
} else if (key == "rt") {
rt_ = Clamp8(std::strtol(val.c_str(), nullptr, 0));
} else if (key == "lx") {
lx_ = Clamp16(std::strtol(val.c_str(), nullptr, 0));
} else if (key == "ly") {
ly_ = Clamp16(std::strtol(val.c_str(), nullptr, 0));
} else if (key == "rx") {
rx_ = Clamp16(std::strtol(val.c_str(), nullptr, 0));
} else if (key == "ry") {
ry_ = Clamp16(std::strtol(val.c_str(), nullptr, 0));
}
}
}
static uint8_t Clamp8(long v) {
return static_cast<uint8_t>(v < 0 ? 0 : (v > 255 ? 255 : v));
}
static int16_t Clamp16(long v) {
return static_cast<int16_t>(v < -32768 ? -32768 : (v > 32767 ? 32767 : v));
}
uint16_t buttons_ = 0;
uint8_t lt_ = 0, rt_ = 0;
int16_t lx_ = 0, ly_ = 0, rx_ = 0, ry_ = 0;
uint32_t packet_ = 1;
// Buttons already reported through GetKeystroke; the edge detector's memory.
uint16_t reported_ = 0;
bool present_ = false;
time_t mtime_sec_ = 0;
long mtime_nsec_ = -1;
off_t size_ = -1;
};
inline std::unique_ptr<InputDriver> Create(xe::ui::Window* window,
size_t window_z_order) {
return std::make_unique<FileInputDriver>(window, window_z_order);
}
} // namespace filepad
} // namespace hid
} // namespace xe
#endif // XENIA_HID_FILE_FILE_INPUT_DRIVER_H_

View File

@@ -18,3 +18,8 @@ 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

@@ -14,5 +14,6 @@
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

@@ -1071,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);
}
}
@@ -1378,6 +1386,40 @@ void KernelState::EmulateCPInterruptDPC(uint32_t interrupt_callback,
"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();

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

@@ -197,6 +197,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);
@@ -688,11 +712,26 @@ dword_result_t NtCreateEvent_entry(
}
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}",
handle_ptr ? uint32_t(*handle_ptr) : 0u, uint32_t(event_type),
uint32_t(initial_state), lr);
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;
}
@@ -720,9 +759,24 @@ 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) {
uint32_t lr = static_cast<uint32_t>(cpu::ThreadState::Get()->context()->lr);
XELOGKERNEL("AUDIT-HLC NtSetEvent handle={:08X} lr={:08X}",
uint32_t(handle), lr);
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);
}

View File

@@ -9,6 +9,7 @@
#include "xenia/kernel/xconfig.h"
#include "xenia/base/cvar.h"
#include "xenia/base/logging.h"
#include "xenia/base/filesystem.h"
@@ -19,6 +20,17 @@
#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 kernel {
@@ -94,7 +106,7 @@ void XConfig::SetDefaults() {
xconfig_data_.user.language = static_cast<uint32_t>(XLanguage::kEnglish);
xconfig_data_.user.country =
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_component_sz = XHDTVResolution.at(1).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/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);