[Phase A] Port args / file.read extraction tracing onto instrument-current

Bring the cvar-gated JSONL extraction tracer (event_log) forward from the
phase-a-tracing line so args/args_resolved + file.read events are
available alongside the GPU draw logging. Purely additive:

- src/xenia/kernel/event_log.{cc,h}: the tracer (auto-globbed into the
  kernel target). Tier 1 fills kernel.call args (raw r3..r10) +
  args_resolved.path under --phase_a_trace_args; Tier 2 resolves
  NtReadFile handle->path via the object table and emits file.read.
- cpu_flags: phase_a_event_log_path / _mem_writes / _trace_args /
  _hash_probe / kernel_emit_contention (all default-off).
- shim_utils.h: phase_a_bridge decl + EmitImportAndCallWithCtx/EmitReturn
  hooks in the active X::Trampoline, skipped when Enabled() is false.

Deliberately EXCLUDES the Tier-3 hash-probe HIR/emitter trap (the IPFB
name-hash was recovered statically, so the probe is moot). The
phase_a_hash_probe cvar is defined but inert.

Default-off => instrument-current behaviour byte-identical when unused.
Not yet compile-verified against instrument-current's toolchain.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-13 19:13:36 +02:00
parent f970a5173f
commit a40bc50159
5 changed files with 967 additions and 0 deletions

View File

@@ -0,0 +1,191 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Phase A event-log emitter. Cvar-gated (default off). Schema v1.
* Companion: xenia-rs/audit-runs/phase-a-diff-harness/schema-v1.md
******************************************************************************
*/
#ifndef XENIA_KERNEL_EVENT_LOG_H_
#define XENIA_KERNEL_EVENT_LOG_H_
#include <cstdint>
namespace xe {
namespace kernel {
namespace phase_a {
// Object-type codes (must match ours's enum exactly — see schema-v1.md).
enum ObjectType : uint32_t {
kObjUnknown = 0x00,
kObjEvent = 0x01,
kObjMutant = 0x02,
kObjSemaphore = 0x03,
kObjTimer = 0x04,
kObjThread = 0x05,
kObjFile = 0x06,
kObjIoCompletion = 0x07,
kObjModule = 0x08,
kObjEnumState = 0x09,
kObjSection = 0x0A,
kObjNotification = 0x0B,
// Phase D Stage 1: pseudo-type used as the `object_type` input to
// `ComputeSharedGlobalSemanticId` for RTL_CRITICAL_SECTION pointers. CS
// is not a regular `XObject` (it lives as a guest-memory struct, not a
// handle-tabled kernel object), but the `site_sid` field of
// `contention.observed` reuses the shared-global SID recipe so the
// Stage-3 manifest loader can compute the same SID in both engines.
kObjCriticalSection = 0x0C,
};
// Fast bool check (default off). Inlinable so we can guard hot paths cheaply.
bool IsEnabled();
// Emitted once at startup if enabled (first line of the JSONL).
void EmitSchemaHeader();
// FNV-1a 64-bit identity (see schema-v1.md). Both engines compute identically.
uint64_t ComputeSemanticId(uint32_t create_site_pc, uint32_t creating_tid,
uint64_t tid_event_idx_at_creation,
uint32_t object_type);
// One emit per imported kernel function invocation. Emitted by the export
// trampoline before the kernel.call event.
void EmitImportCall(const char* module_name, uint16_t ordinal,
const char* fn_name);
// Kernel call entry / return. args/args_resolved are deferred to a later
// phase; v1 emits the name + return value only (sufficient for the diff
// tool to align by sequence).
void EmitKernelCall(const char* name);
// Phase C+10: schema-v1 extension — emit kernel.call whose
// `args_resolved` field carries a resolved `"path"` value (see
// schema-v1.md kernel.call payload). `path == nullptr || *path == '\0'`
// degrades to the existing empty-args_resolved form.
void EmitKernelCallWithPath(const char* name, const char* path);
void EmitKernelReturn(const char* name, uint64_t return_value);
// Handle lifecycle. raw_handle_id is engine-local; the diff key is the
// FNV-1a semantic id.
void EmitHandleCreate(uint64_t semantic_id, uint32_t object_type,
uint32_t raw_handle_id, const char* object_name);
void EmitHandleDestroy(uint64_t semantic_id, uint32_t raw_handle_id,
uint32_t prior_refcount);
// Thread create/exit. parent_tid is the caller; entry_pc is the spawned
// thread's first instruction.
void EmitThreadCreate(uint64_t semantic_id, uint32_t parent_tid,
uint32_t entry_pc, uint32_t ctx_ptr, uint32_t priority,
uint32_t affinity, uint32_t stack_size, bool suspended);
void EmitThreadExit(uint32_t exit_code);
// Wait begin/end. handles_count + handles_semantic_ids array.
void EmitWaitBegin(const uint64_t* handles_semantic_ids, uint32_t count,
int64_t timeout_ns, bool alertable, bool wait_all);
void EmitWaitEnd(uint32_t status, uint64_t woken_by_semantic_id_or_zero);
// Returns the next per-tid event index (post-increment). Useful for
// `tid_event_idx_at_creation` capture before calling ComputeSemanticId.
uint64_t PeekTidEventIdx();
// Phase C+15-α: handle-semantic-ID registry. Maps raw handle id ->
// FNV-1a 64-bit SID assigned at handle creation, so subsequent
// `handle.destroy` / `wait.begin` / etc. events can emit a stable
// cross-engine identity. No-op when event_log is disabled.
void RegisterHandleSemanticId(uint32_t raw_handle_id, uint64_t sid);
uint64_t LookupHandleSemanticId(uint32_t raw_handle_id);
uint64_t ForgetHandleSemanticId(uint32_t raw_handle_id);
// Convenience: at handle creation time, peek tid_event_idx, compute
// the FNV-1a SID, register it for `raw_handle_id`, and emit a
// `handle.create` event. Returns the SID. `create_site_pc` is set to
// 0 by callers that cannot resolve a guest LR — both engines agree
// on `0` for canonicalization at v1.1.
uint64_t EmitHandleCreateAuto(uint32_t create_site_pc, uint32_t object_type,
uint32_t raw_handle_id,
const char* object_name);
// Convenience: forget mapping + emit `handle.destroy` with the
// previously registered SID.
void EmitHandleDestroyAuto(uint32_t raw_handle_id, uint32_t prior_refcount);
// Phase C+18: marker constant used as `create_site_pc` in shared-global
// SID computation. Both engines must use exactly this value. See
// schema-v1.md §"Shared-global SIDs".
constexpr uint32_t kSharedGlobalSidMarker = 0xC01AB005;
// Phase C+18: compute the scheduling-invariant SID for a
// **process-global** kernel dispatcher (canary's
// `XObject::GetNativeObject` lazy-wrap on first guest-thread touch).
// Keyed on `(SHARED_GLOBAL_SID_MARKER, 0, pointer, object_type)` so the
// SID depends only on the object's identity, not on the scheduling order.
uint64_t ComputeSharedGlobalSemanticId(uint32_t pointer, uint32_t object_type);
// Phase C+18: emit `handle.create` with a scheduling-invariant SID for
// a process-global kernel dispatcher synthesized by
// `XObject::GetNativeObject`. The SID is computed via
// `ComputeSharedGlobalSemanticId(pointer, object_type)` so the same
// dispatcher yields the same SID in both engines regardless of which
// guest thread happens to be the first toucher. `raw_handle_id` is the
// XObject's allocated handle; `pointer` is the guest dispatcher
// (`X_DISPATCH_HEADER*`) pointer (used only to derive the SID).
//
// Caller MUST avoid the regular AddHandle emit for this XObject (set
// the in-flight thread-local marker around the `new XEvent` /
// `new XSemaphore` ctor and have the AddHandle hook short-circuit).
void EmitHandleCreateSharedGlobal(uint32_t pointer, uint32_t object_type,
uint32_t raw_handle_id,
const char* object_name);
// Phase C+18: thread-local flag indicating the current host thread is
// inside `XObject::GetNativeObject` lazy-wrap, which constructs a new
// XEvent/XSemaphore wrapper for a process-global guest dispatcher.
// `AddHandle` consults this to skip its regular per-thread
// `handle.create` emit; the explicit shared-global emit happens in
// `GetNativeObject` after `StashHandle`. The flag is a single
// host-thread-local bool — re-entrancy is not expected (the global
// critical region is held throughout).
void SetInGetNativeObject(bool active);
bool IsInGetNativeObject();
// Phase D Stage 1: emit a `contention.observed` event from
// `RtlEnterCriticalSection_entry` when the spin-loop is exhausted and
// control falls through to `xeKeWaitForSingleObject`. The Stage-2 manifest
// builder filters on `contended=true` and keys on `(tid, tid_event_idx)`
// so the Stage-3 replay loop in ours can park its own `rtl_enter_critical_section`
// at exactly the same per-tid ordinal.
//
// `cs_guest_ptr` is the guest VA of the `X_RTL_CRITICAL_SECTION` (the
// argument to RtlEnterCS). `site_sid` is computed via
// `ComputeSharedGlobalSemanticId(cs_guest_ptr, kObjCriticalSection)` so
// both engines agree on the SID for the same CS pointer.
//
// Gated on `cvars::kernel_emit_contention && IsEnabled()`. Default
// behavior (cvar off) is byte-identical to pre-Stage-1 canary.
void EmitContentionObserved(uint32_t cs_guest_ptr, bool contended);
// ── Expansive extraction tracing (game-data RE; gated by phase_a_trace_args /
// phase_a_hash_probe, default off) ─────────────────────────────────────────
// kernel.call with populated `args` (raw r3..r10 from the PPCContext, passed
// as void*) and `args_resolved` (the resolved file path when known).
void EmitKernelCallArgs(const char* name, void* ppc_context,
const char* resolved_path);
// file.read — a guest read from an open file handle (path resolved from the
// handle via the object table). Enables correlating .pak reads to TOC entries.
void EmitFileRead(uint32_t handle, const char* path, uint64_t offset,
uint32_t length, uint32_t buffer_va);
// guest.call — emitted by the guest-PC hash probe: `arg_str` is r3 dereferenced
// as a guest C-string (the archive path being hashed). Used to recover the
// custom IPFB/IDXD name-hash by observing (string -> hash) pairs.
void EmitGuestCall(uint32_t pc, const char* arg_str, uint32_t r3, uint32_t r4,
uint32_t r5, uint32_t r6);
} // namespace phase_a
} // namespace kernel
} // namespace xe
#endif // XENIA_KERNEL_EVENT_LOG_H_