diff --git a/src/xenia/cpu/cpu_flags.cc b/src/xenia/cpu/cpu_flags.cc index 3ff067e15..e57ec5a7b 100644 --- a/src/xenia/cpu/cpu_flags.cc +++ b/src/xenia/cpu/cpu_flags.cc @@ -57,3 +57,35 @@ DEFINE_bool(break_condition_truncate, true, "truncate value to 32-bits", "CPU"); DEFINE_bool(break_on_debugbreak, true, "int3 on JITed __debugbreak requests.", "CPU"); + +// AUDIT-DEMO: smoke marker (memory entry: emulator.cc:225,283). Always-on bool. +DEFINE_bool(audit_demo_setup_trace, true, + "Audit smoke marker: log AUDIT-DEMO-SETUP-BEGIN at emulator setup.", + "Audit"); + +// AUDIT-061: comma-separated list of guest PCs to log on each fire. +// Format: "0xPC1,0xPC2,..." (max 32 PCs). Each fire emits +// AUDIT-061-BR pc=X lr=X cr0=LGE cr6=LGE r3=X r4=X r5=X r6=X r31=X tid=N. +// Default empty (off); no perf cost when empty. +DEFINE_string(audit_61_branch_probe_pcs, "", + "AUDIT-061: CSV of guest PCs to trace (cr0/cr6 + regs/tid).", + "Audit"); + +// AUDIT-067: comma-separated list of u32 values to watch. When non-empty, +// every 4-byte guest store (stw/stwu/stwx/stwux/stmw) emits a runtime +// equality check; matches log AUDIT-067-VAL pc=X lr=X val=X dst=X r3..r6 r31 tid=N. +// Max 4 values. Default empty (off); zero overhead when empty. +DEFINE_string(audit_67_value_watch, "", + "AUDIT-067: CSV of u32 values (max 4) — log every guest " + "store whose value matches.", + "Audit"); + +// Phase A — see kernel/event_log.h. +DEFINE_string(phase_a_event_log_path, "", + "Phase A: write schema-v1 JSONL event log to this path. " + "Empty (default) = disabled.", + "Audit"); +DEFINE_bool(phase_a_event_log_mem_writes, false, + "Phase A: include mem.write events in the JSONL log. RESERVED — " + "not wired in this phase. Default false.", + "Audit"); diff --git a/src/xenia/cpu/cpu_flags.h b/src/xenia/cpu/cpu_flags.h index 38c4f98ba..c7f5a2711 100644 --- a/src/xenia/cpu/cpu_flags.h +++ b/src/xenia/cpu/cpu_flags.h @@ -35,4 +35,22 @@ DECLARE_bool(break_condition_truncate); DECLARE_bool(break_on_debugbreak); +// AUDIT-DEMO smoke marker. +DECLARE_bool(audit_demo_setup_trace); + +// AUDIT-061: multi-PC branch probe — emits one log line per fire with +// (pc, lr, cr0 LGE, cr6 LGE, r3, r4, r5, r6, r31, tid). CSV of guest PCs. +DECLARE_string(audit_61_branch_probe_pcs); + +// AUDIT-067: value-watch — emit a log line for each 32-bit guest store whose +// value-to-be-stored matches any configured value. CSV of u32 values +// ("0xDEADBEEF,..."), max 4 entries. Default empty (off); zero cost when empty. +DECLARE_string(audit_67_value_watch); + +// Phase A: JSONL event-log emitter path. When non-empty, the engine writes +// schema-v1 JSONL events to this file. Empty (default) = no overhead, no +// behavior change. Schema: xenia-rs/audit-runs/phase-a-diff-harness/schema-v1.md +DECLARE_string(phase_a_event_log_path); +DECLARE_bool(phase_a_event_log_mem_writes); + #endif // XENIA_CPU_CPU_FLAGS_H_ diff --git a/src/xenia/kernel/event_log.cc b/src/xenia/kernel/event_log.cc new file mode 100644 index 000000000..a4ae4b41d --- /dev/null +++ b/src/xenia/kernel/event_log.cc @@ -0,0 +1,335 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Phase A event-log emitter — see event_log.h and schema-v1.md. + ****************************************************************************** + */ + +#include "xenia/kernel/event_log.h" + +#include +#include +#include +#include +#include +#include + +#include "third_party/fmt/include/fmt/format.h" + +#include "xenia/base/cvar.h" +#include "xenia/kernel/xthread.h" + +DECLARE_string(phase_a_event_log_path); + +namespace xe { +namespace kernel { +namespace phase_a { + +namespace { + +// Cached enabled state, computed lazily from cvar (cheap fast-path). +std::atomic g_state{0}; // 0=untouched, 1=enabled, 2=disabled +std::FILE* g_file = nullptr; +std::mutex g_file_mu; +std::once_flag g_init_once; + +// Per-thread monotonic event index (key for the diff tool). +thread_local uint64_t t_tid_event_idx = 0; + +// Process-start ns for the host_ns field. Captured on first use; debug only. +std::chrono::steady_clock::time_point g_t0; +std::once_flag g_t0_once; + +void EnsureT0() { + std::call_once(g_t0_once, + []() { g_t0 = std::chrono::steady_clock::now(); }); +} + +int64_t HostNsSinceStart() { + EnsureT0(); + auto now = std::chrono::steady_clock::now(); + return std::chrono::duration_cast(now - g_t0) + .count(); +} + +void OpenIfNeeded() { + std::call_once(g_init_once, []() { + const std::string& path = cvars::phase_a_event_log_path; + if (path.empty()) { + g_state.store(2, std::memory_order_release); + return; + } + g_file = std::fopen(path.c_str(), "wb"); + if (!g_file) { + g_state.store(2, std::memory_order_release); + return; + } + g_state.store(1, std::memory_order_release); + // Write the schema header as the first line — synthetic tid=0. + auto header = fmt::format( + "{{\"schema_version\":1,\"engine\":\"canary\",\"kind\":\"schema_version" + "\",\"tid\":0,\"tid_event_idx\":0,\"guest_cycle\":0,\"host_ns\":{},\"" + "deterministic\":true,\"payload\":{{\"version\":1,\"emitter_build\":\"" + "canary-phaseA\"}}}}\n", + HostNsSinceStart()); + std::fwrite(header.data(), 1, header.size(), g_file); + std::fflush(g_file); + }); +} + +uint32_t CurrentTid() { + // XThread::GetCurrentThreadId returns 0 if no current XThread (boot thread). + return XThread::GetCurrentThreadId(); +} + +void WriteLine(const std::string& line) { + std::lock_guard lock(g_file_mu); + if (!g_file) return; + std::fwrite(line.data(), 1, line.size(), g_file); + std::fputc('\n', g_file); + // Flush every line so a crash mid-boot still produces a useful prefix. + std::fflush(g_file); +} + +// Common-fields prefix. Caller appends `,\"payload\":{...}}`. +// kind, tid, tid_event_idx, guest_cycle=0 (canary has no kernel-layer cycle), +// host_ns, deterministic, engine. +std::string CommonPrefix(const char* kind, uint32_t tid, uint64_t idx, + bool deterministic) { + return fmt::format( + "{{\"schema_version\":1,\"engine\":\"canary\",\"kind\":\"{}\",\"tid\":{}," + "\"tid_event_idx\":{},\"guest_cycle\":0,\"host_ns\":{},\"deterministic\":" + "{}", + kind, tid, idx, HostNsSinceStart(), deterministic ? "true" : "false"); +} + +// Escape a JSON string. Keep it minimal — kernel names are ASCII. +std::string EscapeJson(const char* s) { + if (!s) return "null"; + std::string out; + out.reserve(std::strlen(s) + 2); + for (const char* p = s; *p; ++p) { + unsigned char c = static_cast(*p); + if (c == '\\' || c == '"') { + out.push_back('\\'); + out.push_back(static_cast(c)); + } else if (c == '\n') { + out += "\\n"; + } else if (c == '\r') { + out += "\\r"; + } else if (c == '\t') { + out += "\\t"; + } else if (c < 0x20) { + out += fmt::format("\\u{:04x}", c); + } else { + out.push_back(static_cast(c)); + } + } + return out; +} + +} // namespace + +bool IsEnabled() { + int s = g_state.load(std::memory_order_acquire); + if (s == 0) { + OpenIfNeeded(); + s = g_state.load(std::memory_order_acquire); + } + return s == 1; +} + +uint64_t PeekTidEventIdx() { return t_tid_event_idx; } + +uint64_t ComputeSemanticId(uint32_t create_site_pc, uint32_t creating_tid, + uint64_t tid_event_idx_at_creation, + uint32_t object_type) { + uint8_t bytes[4 + 4 + 8 + 4]; + auto put_u32 = [&](size_t off, uint32_t v) { + bytes[off + 0] = static_cast(v & 0xFF); + bytes[off + 1] = static_cast((v >> 8) & 0xFF); + bytes[off + 2] = static_cast((v >> 16) & 0xFF); + bytes[off + 3] = static_cast((v >> 24) & 0xFF); + }; + auto put_u64 = [&](size_t off, uint64_t v) { + for (int i = 0; i < 8; ++i) + bytes[off + i] = static_cast((v >> (i * 8)) & 0xFF); + }; + put_u32(0, create_site_pc); + put_u32(4, creating_tid); + put_u64(8, tid_event_idx_at_creation); + put_u32(16, object_type); + uint64_t h = 0xCBF29CE484222325ULL; + for (size_t i = 0; i < sizeof(bytes); ++i) { + h ^= bytes[i]; + h *= 0x100000001B3ULL; + } + return h; +} + +void EmitSchemaHeader() { + if (!IsEnabled()) return; + // tid=0, tid_event_idx=0, deterministic=true. NOT consuming the per-tid + // counter (the header is on a synthetic tid 0). + std::string line = fmt::format( + "{{\"schema_version\":1,\"engine\":\"canary\",\"kind\":\"schema_version" + "\",\"tid\":0,\"tid_event_idx\":0,\"guest_cycle\":0,\"host_ns\":{},\"" + "deterministic\":true,\"payload\":{{\"version\":1,\"emitter_build\":\"" + "canary-phaseA\"}}}}", + HostNsSinceStart()); + WriteLine(line); +} + +void EmitImportCall(const char* module_name, uint16_t ordinal, + const char* fn_name) { + if (!IsEnabled()) return; + uint32_t tid = CurrentTid(); + uint64_t idx = t_tid_event_idx++; + std::string line = CommonPrefix("import.call", tid, idx, true); + line += fmt::format( + ",\"payload\":{{\"module\":\"{}\",\"ord\":{},\"name\":\"{}\"}}}}", + EscapeJson(module_name), ordinal, EscapeJson(fn_name)); + WriteLine(line); +} + +void EmitKernelCall(const char* name) { + if (!IsEnabled()) return; + uint32_t tid = CurrentTid(); + uint64_t idx = t_tid_event_idx++; + std::string line = CommonPrefix("kernel.call", tid, idx, true); + line += fmt::format(",\"payload\":{{\"name\":\"{}\",\"args\":{{}},\"args_" + "resolved\":{{}}}}}}", + EscapeJson(name)); + WriteLine(line); +} + +void EmitKernelReturn(const char* name, uint64_t return_value) { + if (!IsEnabled()) return; + uint32_t tid = CurrentTid(); + uint64_t idx = t_tid_event_idx++; + std::string line = CommonPrefix("kernel.return", tid, idx, true); + line += fmt::format( + ",\"payload\":{{\"name\":\"{}\",\"return_value\":{},\"status\":\"0x{:08x}" + "\",\"side_effects\":[]}}}}", + EscapeJson(name), return_value, static_cast(return_value)); + WriteLine(line); +} + +void EmitHandleCreate(uint64_t semantic_id, uint32_t object_type, + uint32_t raw_handle_id, const char* object_name) { + if (!IsEnabled()) return; + uint32_t tid = CurrentTid(); + uint64_t idx = t_tid_event_idx++; + std::string line = CommonPrefix("handle.create", tid, idx, true); + if (object_name && *object_name) { + line += fmt::format( + ",\"payload\":{{\"handle_semantic_id\":\"{:016x}\",\"object_type\":{}," + "\"object_name\":\"{}\",\"raw_handle_id\":\"0x{:08x}\"}}}}", + semantic_id, object_type, EscapeJson(object_name), raw_handle_id); + } else { + line += fmt::format( + ",\"payload\":{{\"handle_semantic_id\":\"{:016x}\",\"object_type\":{}," + "\"object_name\":null,\"raw_handle_id\":\"0x{:08x}\"}}}}", + semantic_id, object_type, raw_handle_id); + } + WriteLine(line); +} + +void EmitHandleDestroy(uint64_t semantic_id, uint32_t raw_handle_id, + uint32_t prior_refcount) { + if (!IsEnabled()) return; + uint32_t tid = CurrentTid(); + uint64_t idx = t_tid_event_idx++; + std::string line = CommonPrefix("handle.destroy", tid, idx, true); + line += fmt::format( + ",\"payload\":{{\"handle_semantic_id\":\"{:016x}\",\"raw_handle_id\":\"" + "0x{:08x}\",\"prior_refcount\":{}}}}}", + semantic_id, raw_handle_id, prior_refcount); + WriteLine(line); +} + +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) { + if (!IsEnabled()) return; + uint32_t tid = CurrentTid(); + uint64_t idx = t_tid_event_idx++; + std::string line = CommonPrefix("thread.create", tid, idx, true); + line += fmt::format( + ",\"payload\":{{\"handle_semantic_id\":\"{:016x}\",\"parent_tid\":{}," + "\"entry_pc\":\"0x{:08x}\",\"ctx_ptr\":\"0x{:08x}\",\"priority\":{}," + "\"affinity\":{},\"stack_size\":{},\"suspended\":{}}}}}", + semantic_id, parent_tid, entry_pc, ctx_ptr, priority, affinity, + stack_size, suspended ? "true" : "false"); + WriteLine(line); +} + +void EmitThreadExit(uint32_t exit_code) { + if (!IsEnabled()) return; + uint32_t tid = CurrentTid(); + uint64_t idx = t_tid_event_idx++; + std::string line = CommonPrefix("thread.exit", tid, idx, true); + line += fmt::format(",\"payload\":{{\"exit_code\":{}}}}}", exit_code); + WriteLine(line); +} + +void EmitWaitBegin(const uint64_t* handles_semantic_ids, uint32_t count, + int64_t timeout_ns, bool alertable, bool wait_all) { + if (!IsEnabled()) return; + uint32_t tid = CurrentTid(); + uint64_t idx = t_tid_event_idx++; + std::string line = CommonPrefix("wait.begin", tid, idx, true); + std::string ids = "["; + for (uint32_t i = 0; i < count; ++i) { + if (i) ids += ","; + ids += fmt::format("\"{:016x}\"", handles_semantic_ids[i]); + } + ids += "]"; + line += fmt::format( + ",\"payload\":{{\"handles_semantic_ids\":{},\"timeout_ns\":{}," + "\"alertable\":{},\"wait_type\":\"{}\"}}}}", + ids, timeout_ns, alertable ? "true" : "false", + wait_all ? "all" : "any"); + WriteLine(line); +} + +void EmitWaitEnd(uint32_t status, uint64_t woken_by_semantic_id_or_zero) { + if (!IsEnabled()) return; + uint32_t tid = CurrentTid(); + uint64_t idx = t_tid_event_idx++; + std::string line = CommonPrefix("wait.end", tid, idx, false); + if (woken_by_semantic_id_or_zero) { + line += fmt::format( + ",\"payload\":{{\"status\":\"0x{:08x}\",\"woken_by_semantic_id\":\"" + "{:016x}\",\"wait_duration_cycles\":0}}}}", + status, woken_by_semantic_id_or_zero); + } else { + line += fmt::format( + ",\"payload\":{{\"status\":\"0x{:08x}\",\"woken_by_semantic_id\":null," + "\"wait_duration_cycles\":0}}}}", + status); + } + WriteLine(line); +} + +} // namespace phase_a + +// Bridge entry points referenced from shim_utils.h. Defined here so the +// template-heavy header does not need to include event_log.h directly. +namespace shim { +namespace phase_a_bridge { +bool Enabled() { return ::xe::kernel::phase_a::IsEnabled(); } +void EmitImportAndCall(const char* module_name, uint16_t ord, + const char* name) { + ::xe::kernel::phase_a::EmitImportCall(module_name, ord, name); + ::xe::kernel::phase_a::EmitKernelCall(name); +} +void EmitReturn(const char* name, uint64_t return_value) { + ::xe::kernel::phase_a::EmitKernelReturn(name, return_value); +} +} // namespace phase_a_bridge +} // namespace shim + +} // namespace kernel +} // namespace xe diff --git a/src/xenia/kernel/event_log.h b/src/xenia/kernel/event_log.h new file mode 100644 index 000000000..c51e71b61 --- /dev/null +++ b/src/xenia/kernel/event_log.h @@ -0,0 +1,84 @@ +/** + ****************************************************************************** + * 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 + +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, +}; + +// 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); +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(); + +} // namespace phase_a +} // namespace kernel +} // namespace xe + +#endif // XENIA_KERNEL_EVENT_LOG_H_ diff --git a/src/xenia/kernel/util/shim_utils.h b/src/xenia/kernel/util/shim_utils.h index 0fa254157..209eeed97 100644 --- a/src/xenia/kernel/util/shim_utils.h +++ b/src/xenia/kernel/util/shim_utils.h @@ -499,6 +499,22 @@ enum class KernelModuleId { xbdm, }; +// Phase A bridge — see kernel/event_log.h. Inline to avoid pulling the +// header into shim_utils.h's transitive set. +namespace phase_a_bridge { +constexpr const char* KernelModuleIdName(KernelModuleId m) { + switch (m) { + case KernelModuleId::xboxkrnl: return "xboxkrnl.exe"; + case KernelModuleId::xam: return "xam.xex"; + case KernelModuleId::xbdm: return "xbdm.xex"; + } + return "unknown"; +} +bool Enabled(); +void EmitImportAndCall(const char* module_name, uint16_t ord, const char* name); +void EmitReturn(const char* name, uint64_t return_value); +} // namespace phase_a_bridge + template requires(I == sizeof...(Ps)) void AppendKernelCallParams(StringBuffer& string_buffer, @@ -578,9 +594,18 @@ struct ExportRegistrerHelper { cvars::log_high_frequency_kernel_calls)) { PrintKernelCall(export_entry, params); } + const bool phase_a_on = phase_a_bridge::Enabled(); + if (phase_a_on) { + phase_a_bridge::EmitImportAndCall( + phase_a_bridge::KernelModuleIdName(MODULE), ORDINAL, + export_entry->name); + } if constexpr (std::is_void::value) { KernelTrampoline(fn, std::forward>(params), std::make_index_sequence()); + if (phase_a_on) { + phase_a_bridge::EmitReturn(export_entry->name, 0); + } } else { auto result = KernelTrampoline(fn, std::forward>(params), @@ -590,6 +615,11 @@ struct ExportRegistrerHelper { (xe::cpu::ExportTag::kLog | xe::cpu::ExportTag::kLogResult)) { // TODO(benvanik): log result. } + if (phase_a_on) { + phase_a_bridge::EmitReturn( + export_entry->name, + static_cast(ppc_context->r[3])); + } } } }; @@ -600,14 +630,28 @@ struct ExportRegistrerHelper { 0, }; std::tuple params = {Ps(init)...}; + const bool phase_a_on = phase_a_bridge::Enabled(); + if (phase_a_on) { + phase_a_bridge::EmitImportAndCall( + phase_a_bridge::KernelModuleIdName(MODULE), ORDINAL, + export_entry->name); + } if constexpr (std::is_void::value) { KernelTrampoline(fn, std::forward>(params), std::make_index_sequence()); + if (phase_a_on) { + phase_a_bridge::EmitReturn(export_entry->name, 0); + } } else { auto result = KernelTrampoline(fn, std::forward>(params), std::make_index_sequence()); result.Store(ppc_context); + if (phase_a_on) { + phase_a_bridge::EmitReturn( + export_entry->name, + static_cast(ppc_context->r[3])); + } } } };