# Phase C+15-α Schema-Wiring Audit (2026-05-14) ## Phase 1 — Wired/unwired matrix (pre-session) | Kind | Canary emits? | Ours emits? | Status (pre) | Priority | |---------------------|---------------|-------------|---------------|----------| | `schema_version` | yes | yes | wired | — | | `import.call` | yes | yes | wired | — | | `kernel.call` | yes | yes | wired (+C+10) | — | | `kernel.return` | yes | yes | wired | — | | `handle.create` | declared | declared | **stubbed** | HIGH | | `handle.destroy` | declared | declared | **stubbed** | HIGH | | `thread.create` | declared | declared | **stubbed** | HIGH | | `thread.exit` | declared | declared | **stubbed** | HIGH | | `wait.begin` | declared | declared | **stubbed** | HIGH | | `wait.end` | declared | declared | **stubbed** | HIGH | | `thread.suspend` | declared | not in API | unwired | LOW | | `thread.resume` | declared | not in API | unwired | LOW | | `vfs.open` | declared | not in API | redundant? | MEDIUM | | `vfs.read` | declared | not in API | high-vol | LOW | | `vfs.close` | declared | not in API | redundant? | MEDIUM | | `mem.write` | declared | not in API | opt-in | LOW | ## Phase 2/3 — Kinds wired this session Wired symmetrically in both engines (cvar-gated default-off): - **`handle.create`** — emitted from `KernelState::alloc_handle_for` (ours) / `ObjectTable::AddHandle` (canary). 39+ call sites covered via centralized hook. - **`handle.destroy`** — emitted from `nt_close` + `xam_task_close_handle` (ours) / `ObjectTable::RemoveHandle` (canary). - **`thread.create`** — emitted from `ex_create_thread` (ours) / `ExCreateThread` in `xboxkrnl_threading.cc` (canary). After spawn succeeds. - **`thread.exit`** — emitted from `ex_terminate_thread` (ours) / `XThread::Exit` (canary). Canary's `XThread::Exit` covers both explicit `ExTerminateThread` and implicit thread-entry returns. - **`wait.begin`** — emitted from `nt_wait_for_single_object_ex` + `ke_wait_for_single_object` (ours) / `xeKeWaitForSingleObject` + `NtWaitForSingleObjectEx` (canary). Deferred (v1.2): - **`wait.end`** — design challenge: wait can park the guest thread, and the wake-status path differs between engines. Sync outcome status is already captured in the immediately-following `kernel.return`. Async wake outcome surfaced in subsequent events. - **`thread.suspend` / `thread.resume`** — low-frequency; defer until needed. - **`vfs.*`** — redundant with `kernel.call` for Nt*File. Skip per schema-v1 audit recommendation. - **`mem.write`** — opt-in only (separate cvar); high-volume. ## Code summary ### Ours (~140 LOC) - `crates/xenia-kernel/src/event_log.rs` — registry + auto helpers (`register_handle_semantic_id`, `lookup_handle_semantic_id`, `forget_handle_semantic_id`, `emit_handle_create_auto`, `emit_handle_destroy_auto`). +85 LOC. - `crates/xenia-kernel/src/objects.rs` — `KernelObject::schema_object_type()`. +14 LOC. - `crates/xenia-kernel/src/state.rs` — `alloc_handle_for` emit hook. +24 LOC. - `crates/xenia-kernel/src/exports.rs` — `nt_close` destroy emit, `ex_create_thread` thread.create emit, `ex_terminate_thread` thread.exit emit, `nt_wait_for_single_object_ex` + `ke_wait_for_single_object` wait.begin emits, + `decode_timeout_ns` helper. +85 LOC. - `crates/xenia-kernel/src/xam.rs` — `xam_task_close_handle` destroy emit. +14 LOC. ### Canary (~130 LOC) - `src/xenia/kernel/event_log.h` — registry API (`RegisterHandleSemanticId`, `LookupHandleSemanticId`, `ForgetHandleSemanticId`, `EmitHandleCreateAuto`, `EmitHandleDestroyAuto`). +20 LOC. - `src/xenia/kernel/event_log.cc` — per-tid counter map (was per-host-thread `thread_local`; produced duplicate `tid_event_idx` for tid=0 across host threads — a bug in the pre-session implementation), `CurrentTid` non-asserting via new `XThread::TryGetCurrentThread`, registry helpers, auto-emit wrappers. +60 LOC net. - `src/xenia/kernel/xthread.h` + `xthread.cc` — `TryGetCurrentThread` accessor + `XThread::Exit` thread.exit emit. +12 LOC. - `src/xenia/kernel/util/object_table.cc` — `AddHandle`/`RemoveHandle` hooks + `SchemaObjectType` mapping. +35 LOC. - `src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc` — `ExCreateThread` thread.create emit, `xeKeWaitForSingleObject` + `NtWaitForSingleObjectEx` wait.begin emits. +30 LOC. ### Diff tool - `tools/diff-events/diff_events.py` — `SKIP_PAYLOAD_FIELDS_BY_KIND` now skips `handle_semantic_id` (cross-engine `creating_tid` differs, so SIDs are engine-local), `parent_tid`, `handles_semantic_ids`, `woken_by_semantic_id`. +6 LOC. ## Bug found and fixed this session **Pre-session bug**: canary's `t_tid_event_idx` was a host-thread-local global, not a tid-keyed counter. When `AddHandle` runs from multiple host threads with tid==0 (boot init + early XThread bootstrap before guest tid is assigned), each host thread had its own counter starting at 0, producing duplicate `tid_event_idx` values within the tid=0 stream. The diff tool rejected the file with "events out of order at index 8". Fixed by replacing the thread_local with a tid-keyed `std::unordered_map` + mutex (matches ours's design).