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>
192 lines
9.1 KiB
C++
192 lines
9.1 KiB
C++
/**
|
||
******************************************************************************
|
||
* 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_
|