handoff: VSync/event-wedge fixes + iterate 2.A–2.BC research notes
Source changes (dormant parity infra, retained from iterate 2.AI/2.AO): - xenia-kernel/exports.rs: nt_create_event manual_reset polarity + related event wiring - xenia-gpu/mmio_region.rs: D1MODE_VBLANK_VLINE_STATUS hardcode parity Also lands the audit-runs/ analysis notes (.md/.txt/.json digests) for the iterate 2.x VSync/0x10e8/0x1004 wedge investigation. Raw trace dumps (.jsonl/.gz/.csv/.stdout) and agent worktrees (.claude/) are gitignored as regenerable local artifacts — see memory + HANDOFF for the running findings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
585
audit-runs/phase-a-diff-harness/canary-patch.diff
Normal file
585
audit-runs/phase-a-diff-harness/canary-patch.diff
Normal file
@@ -0,0 +1,585 @@
|
||||
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 <atomic>
|
||||
+#include <chrono>
|
||||
+#include <cstdio>
|
||||
+#include <cstring>
|
||||
+#include <mutex>
|
||||
+#include <string>
|
||||
+
|
||||
+#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<int> 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<std::chrono::nanoseconds>(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<std::mutex> 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<unsigned char>(*p);
|
||||
+ if (c == '\\' || c == '"') {
|
||||
+ out.push_back('\\');
|
||||
+ out.push_back(static_cast<char>(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<char>(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<uint8_t>(v & 0xFF);
|
||||
+ bytes[off + 1] = static_cast<uint8_t>((v >> 8) & 0xFF);
|
||||
+ bytes[off + 2] = static_cast<uint8_t>((v >> 16) & 0xFF);
|
||||
+ bytes[off + 3] = static_cast<uint8_t>((v >> 24) & 0xFF);
|
||||
+ };
|
||||
+ auto put_u64 = [&](size_t off, uint64_t v) {
|
||||
+ for (int i = 0; i < 8; ++i)
|
||||
+ bytes[off + i] = static_cast<uint8_t>((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<uint32_t>(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 <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,
|
||||
+};
|
||||
+
|
||||
+// 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 <size_t I = 0, typename... Ps>
|
||||
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<R>::value) {
|
||||
KernelTrampoline(fn, std::forward<std::tuple<Ps...>>(params),
|
||||
std::make_index_sequence<sizeof...(Ps)>());
|
||||
+ if (phase_a_on) {
|
||||
+ phase_a_bridge::EmitReturn(export_entry->name, 0);
|
||||
+ }
|
||||
} else {
|
||||
auto result =
|
||||
KernelTrampoline(fn, std::forward<std::tuple<Ps...>>(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<uint64_t>(ppc_context->r[3]));
|
||||
+ }
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -600,14 +630,28 @@ struct ExportRegistrerHelper {
|
||||
0,
|
||||
};
|
||||
std::tuple<Ps...> 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<R>::value) {
|
||||
KernelTrampoline(fn, std::forward<std::tuple<Ps...>>(params),
|
||||
std::make_index_sequence<sizeof...(Ps)>());
|
||||
+ if (phase_a_on) {
|
||||
+ phase_a_bridge::EmitReturn(export_entry->name, 0);
|
||||
+ }
|
||||
} else {
|
||||
auto result =
|
||||
KernelTrampoline(fn, std::forward<std::tuple<Ps...>>(params),
|
||||
std::make_index_sequence<sizeof...(Ps)>());
|
||||
result.Store(ppc_context);
|
||||
+ if (phase_a_on) {
|
||||
+ phase_a_bridge::EmitReturn(
|
||||
+ export_entry->name,
|
||||
+ static_cast<uint64_t>(ppc_context->r[3]));
|
||||
+ }
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user