Files
xenia-rs/audit-runs/audit-069-wait-signal-producer/fix-canary-s5.diff
MechaCat02 ef93a4fa14 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>
2026-06-05 07:19:08 +02:00

304 lines
15 KiB
Diff
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
diff --git a/src/xenia/cpu/cpu_flags.cc b/src/xenia/cpu/cpu_flags.cc
index 3ff067e15..e6f412f91 100644
--- a/src/xenia/cpu/cpu_flags.cc
+++ b/src/xenia/cpu/cpu_flags.cc
@@ -57,3 +57,110 @@ 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");
+
+// AUDIT-068: host-side memory-write watch. See cpu_flags.h header for format.
+// Mirrors AUDIT-067 but covers host-side writes (xe::store_and_swap<T>,
+// Memory::Zero/Fill/Copy). Empty default = zero cost.
+DEFINE_string(audit_68_host_mem_watch_values, "",
+ "AUDIT-068: CSV of u32 values (max 8) — log every host-side "
+ "guest-memory write whose value matches.",
+ "Audit");
+DEFINE_string(audit_68_host_mem_watch_addrs, "",
+ "AUDIT-068: CSV of guest VAs or VA ranges 'START-END' (max 8) "
+ "— log every host-side guest-memory write whose guest VA falls "
+ "within the configured set.",
+ "Audit");
+
+// AUDIT-068 Session 3: read-mode probe. See cpu_flags.h for format.
+DEFINE_string(audit_68_host_mem_read_probe, "",
+ "AUDIT-068 Session 3: CSV of 'VA:SIZE:PERIOD_NS' tuples (max 8) "
+ "— a dedicated poll thread reads the value at each VA every "
+ "PERIOD_NS and emits AUDIT-068-READ-CHANGE on transition.",
+ "Audit");
+
+// AUDIT-069: see cpu_flags.h header. Empty default = zero cost.
+DEFINE_string(audit_69_event_signal_watch, "",
+ "AUDIT-069: CSV of guest event-handle IDs (max 4) — log each "
+ "XEvent::Set / Ke*Event / Nt*Event fire whose target matches.",
+ "Audit");
+DEFINE_string(audit_69_event_signal_native_ptr, "",
+ "AUDIT-069: CSV of guest event native VAs (X_KEVENT*) (max 4) "
+ "— log each set fire whose native pointer matches.",
+ "Audit");
+DEFINE_bool(audit_69_log_all_sets, false,
+ "AUDIT-069: when true, log EVERY XEvent::Set/Pulse fire (used "
+ "for one-run wait→signal correlation across handle drift). "
+ "Default false; use only with --mute=true.",
+ "Audit");
+
+// AUDIT-070 (S5 of AUDIT-069 family): semaphore-release watch. See header.
+DEFINE_string(audit_70_semaphore_release_watch, "",
+ "AUDIT-070: CSV of guest semaphore handle IDs (max 4) — log "
+ "each NtReleaseSemaphore / xeKeReleaseSemaphore fire whose "
+ "target matches.",
+ "Audit");
+DEFINE_bool(audit_70_log_all_releases, false,
+ "AUDIT-070: when true, log EVERY NtReleaseSemaphore / "
+ "xeKeReleaseSemaphore fire (used to identify the work-semaphore "
+ "handle on first run). Default false; use only with --mute=true.",
+ "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");
+
+// Phase D Stage 1 — see kernel/event_log.h `EmitContentionObserved`.
+DEFINE_bool(kernel_emit_contention, false,
+ "Phase D Stage 1: emit `contention.observed` events when "
+ "RtlEnterCriticalSection's spin loop is exhausted and the call "
+ "falls through to xeKeWaitForSingleObject. Default false (zero "
+ "cost when disabled). Requires --phase_a_event_log_path to be "
+ "set as well.",
+ "Audit");
+
+// Phase B — see kernel/phase_b_snapshot.h.
+DEFINE_string(phase_b_snapshot_dir, "",
+ "Phase B: write 5-file structured state snapshot to "
+ "<dir>/canary/ at the moment immediately before the first "
+ "guest PPC instruction of entry_point. Empty (default) = "
+ "disabled, zero overhead.",
+ "Audit");
+DEFINE_bool(phase_b_snapshot_and_exit, false,
+ "Phase B: after writing the snapshot, exit the process "
+ "immediately (std::_Exit(0)) so re-runs are byte-deterministic.",
+ "Audit");
+DEFINE_bool(phase_b_dump_section_content, false,
+ "Phase B: in memory.json, populate section_contents[].content_b64 "
+ "with raw bytes of every committed XEX-image region. Default "
+ "false — per-region SHA-256 is enough for the routine diff; "
+ "this is the escape hatch for the STOP-and-report condition "
+ "(image_loaded_sha256 mismatch).",
+ "Audit");diff --git a/src/xenia/cpu/cpu_flags.h b/src/xenia/cpu/cpu_flags.h
index 38c4f98ba..95fe8cb22 100644
--- a/src/xenia/cpu/cpu_flags.h
+++ b/src/xenia/cpu/cpu_flags.h
@@ -35,4 +35,76 @@ 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);
+
+// AUDIT-068: host-side memory-write watch — emit a log line for each host-side
+// write to guest memory whose VALUE matches any configured u32 value, or whose
+// guest VA falls within any configured ADDR or ADDR-range. Mirrors AUDIT-067
+// but covers the host-side write paths (xe::store_and_swap<T>, Memory::Zero/
+// Fill/Copy) that AUDIT-067's JIT store-opcode hooks cannot see.
+//
+// VALUES: CSV of u32 values, max 8 entries; e.g. "0x8200A208,0x8200A928".
+// ADDRS: CSV of guest VAs or VA ranges, max 8 entries; range form is
+// "0xSTART-0xEND" (inclusive). e.g. "0x42500000-0x42600000,0xBCE25340".
+// Default empty (off); zero cost on the hot path when both are empty.
+DECLARE_string(audit_68_host_mem_watch_values);
+DECLARE_string(audit_68_host_mem_watch_addrs);
+
+// AUDIT-068 Session 3: read-mode probe. CSV of "VA:SIZE:PERIOD_NS" tuples
+// (max 8). A dedicated low-priority thread polls each VA every PERIOD_NS and
+// emits AUDIT-068-READ-CHANGE when the value transitions. SIZE in {1,2,4,8}.
+// Example: "0xBCE25340:4:1000000" = poll u32 at 0xBCE25340 every 1 ms.
+// Default empty (off); the poll thread is not spawned when empty.
+DECLARE_string(audit_68_host_mem_read_probe);
+
+// AUDIT-069: event-signal watch. CSV of guest handle IDs (e.g. "0xF8000098")
+// to log on every XEvent::Set / KeSetEvent / NtSetEvent / KePulseEvent /
+// NtPulseEvent fire whose target matches. Max 4 entries. Default empty (off);
+// zero cost on the hot path when empty.
+DECLARE_string(audit_69_event_signal_watch);
+// AUDIT-069: event-signal watch by native guest VA (X_KEVENT*). CSV of guest
+// VAs (max 4). Default empty (off). Use when the handle id varies across
+// boots but the native dispatcher pointer is stable.
+DECLARE_string(audit_69_event_signal_native_ptr);
+// AUDIT-069: when true, log EVERY XEvent::Set / XEvent::Pulse fire (subject
+// to the slowpath gate). Use only with --mute=true and short windows — high
+// volume. Default false (off).
+DECLARE_bool(audit_69_log_all_sets);
+
+// AUDIT-070 (S5 of AUDIT-069 family): semaphore-release watch. CSV of guest
+// handle IDs (e.g. "0xF8000098") to log on every NtReleaseSemaphore /
+// xeKeReleaseSemaphore fire whose target matches. Max 4 entries. Default
+// empty (off); zero cost on the hot path when empty.
+DECLARE_string(audit_70_semaphore_release_watch);
+// AUDIT-070: when true, log EVERY NtReleaseSemaphore / xeKeReleaseSemaphore
+// fire. Use only with --mute=true and short windows — used to identify the
+// canary work-semaphore handle on first run. Default false (off).
+DECLARE_bool(audit_70_log_all_releases);
+
+// 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);
+
+// Phase B: initial-state snapshot. When the dir cvar is non-empty, the
+// engine writes a five-file structured state snapshot (cpu_state.json,
+// memory.json, kernel.json, vfs.json, config.json, plus manifest.json) to
+// `<dir>/canary/` at the moment immediately before the first guest PPC
+// instruction of the XEX entry_point executes. See
+// `xenia-rs/audit-runs/phase-b-state-equivalence/`.
+DECLARE_string(phase_b_snapshot_dir);
+DECLARE_bool(phase_b_snapshot_and_exit);
+DECLARE_bool(phase_b_dump_section_content);
+
#endif // XENIA_CPU_CPU_FLAGS_H_diff --git a/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc b/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc
index ced21a600..e1c74d7ec 100644
--- a/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc
+++ b/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc
@@ -12,6 +12,8 @@
#include "xenia/base/clock.h"
#include "xenia/base/platform.h"
#include "xenia/cpu/processor.h"
+#include "xenia/kernel/audit_70_semaphore_release_watch.h"
+#include "xenia/kernel/event_log.h"
#include "xenia/kernel/util/shim_utils.h"
#include "xenia/kernel/xboxkrnl/xboxkrnl_private.h"
#include "xenia/kernel/xsemaphore.h"
@@ -147,6 +149,25 @@ uint32_t ExCreateThread(xe::be<uint32_t>* handle_ptr, uint32_t stack_size,
if (thread_id_ptr) {
*thread_id_ptr = thread->thread_id();
}
+ // Phase C+15-α: schema-v1 `thread.create` event. Symmetric with
+ // ours's `ex_create_thread`. Emitted by the **parent** thread.
+ // handle.create for the thread handle itself was already emitted
+ // via ObjectTable::AddHandle inside XThread::Create. Here we
+ // surface the spawn-specific metadata.
+ if (phase_a::IsEnabled()) {
+ uint64_t sid = phase_a::LookupHandleSemanticId(thread->handle());
+ XThread* parent = XThread::TryGetCurrentThread();
+ uint32_t parent_tid = 0;
+ if (parent) {
+ parent_tid = static_cast<uint32_t>(
+ parent->guest_object<X_KTHREAD>()->thread_id);
+ }
+ uint32_t affinity = (creation_flags >> 24) & 0xFF;
+ bool suspended = (creation_flags & 0x1) != 0;
+ phase_a::EmitThreadCreate(sid, parent_tid, start_address, start_context,
+ /* priority */ 0, affinity, actual_stack_size,
+ suspended);
+ }
}
return result;
}
@@ -165,6 +186,9 @@ DECLARE_XBOXKRNL_EXPORT1(ExCreateThread, kThreading, kImplemented);
uint32_t ExTerminateThread(uint32_t exit_code) {
XThread* thread = XThread::GetCurrentThread();
+ // Phase C+15-α: schema-v1 `thread.exit` is emitted inside
+ // `XThread::Exit` (covers both explicit ExTerminateThread and
+ // implicit thread-entry returns).
// NOTE: this kills us right now. We won't return from it.
return thread->Exit(exit_code);
@@ -718,6 +742,9 @@ uint32_t xeKeReleaseSemaphore(X_KSEMAPHORE* semaphore_ptr, uint32_t increment,
int32_t previous_count = 0;
[[maybe_unused]] bool success =
sem->ReleaseSemaphore(adjustment, &previous_count);
+ // AUDIT-070: log Ke-form release fires whose target handle matches.
+ audit_70::check_release(sem->handle(), "xeKeReleaseSemaphore",
+ static_cast<int32_t>(adjustment), previous_count);
return static_cast<uint32_t>(previous_count);
}
@@ -786,6 +813,13 @@ dword_result_t NtReleaseSemaphore_entry(dword_t sem_handle,
uint32_t(release_count), previous_count);
result = X_STATUS_SEMAPHORE_LIMIT_EXCEEDED;
}
+ // AUDIT-070: log Nt-form release fires whose target handle matches.
+ // Logged regardless of success/limit-exceeded — distinguished by
+ // result/previous_count in subsequent analysis.
+ audit_70::check_release(static_cast<uint32_t>(sem_handle),
+ "NtReleaseSemaphore",
+ static_cast<int32_t>(release_count),
+ previous_count);
} else {
result = X_STATUS_INVALID_HANDLE;
}
@@ -954,6 +988,19 @@ uint32_t xeKeWaitForSingleObject(void* object_ptr, uint32_t wait_reason,
return X_STATUS_ABANDONED_WAIT_0;
}
+ // Phase C+15-α: schema-v1 `wait.begin` event. Symmetric with ours's
+ // `ke_wait_for_single_object`. Resolve the SID via the object's
+ // first registered handle.
+ if (phase_a::IsEnabled()) {
+ uint64_t sid = 0;
+ if (!object->handles().empty()) {
+ sid = phase_a::LookupHandleSemanticId(object->handles()[0]);
+ }
+ int64_t timeout_ns = timeout_ptr ? (static_cast<int64_t>(*timeout_ptr) * 100) : -1;
+ phase_a::EmitWaitBegin(&sid, 1, timeout_ns, alertable != 0,
+ /* wait_all */ false);
+ }
+
X_STATUS result =
object->Wait(wait_reason, processor_mode, alertable, timeout_ptr);
if (alertable) {
@@ -980,6 +1027,16 @@ uint32_t NtWaitForSingleObjectEx(uint32_t object_handle, uint32_t wait_mode,
uint32_t alertable, uint64_t* timeout_ptr) {
X_STATUS result = X_STATUS_SUCCESS;
+ // Phase C+15-α: schema-v1 `wait.begin` event. Symmetric with ours's
+ // `nt_wait_for_single_object_ex`. Resolve SID directly from the
+ // handle.
+ if (phase_a::IsEnabled()) {
+ uint64_t sid = phase_a::LookupHandleSemanticId(object_handle);
+ int64_t timeout_ns = timeout_ptr ? (static_cast<int64_t>(*timeout_ptr) * 100) : -1;
+ phase_a::EmitWaitBegin(&sid, 1, timeout_ns, alertable != 0,
+ /* wait_all */ false);
+ }
+
auto object =
kernel_state()->object_table()->LookupObject<XObject>(object_handle);
if (object) {