Files
xenia-rs/audit-runs/phase-c13-game-dat-files-tbl/investigation.md
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

4.8 KiB

Phase C+13 investigation — NtCreateFile game:\dat\files.tbl

Predecessor state (from Phase C+12)

Cold-vs-cold first divergence at tid_event_idx=103862 on the canary tid=6 → ours tid=1 main chain:

canary: NtCreateFile(path="game:\\dat\\files.tbl") -> 0xc0000034 (STATUS_OBJECT_NAME_NOT_FOUND)
ours:   NtCreateFile(path="game:\\dat\\files.tbl") -> 0x00000000 (STATUS_SUCCESS, synth-empty stub)

Verified from the C+12 archived JSONLs:

  • xenia-rs/audit-runs/phase-c12-NtQueryFullAttributesFile/ours-cold.jsonl events 103860-103862: import.call → kernel.call → kernel.return = 0.
  • xenia-rs/audit-runs/phase-c12-NtQueryFullAttributesFile/canary-cold-tid6-250k.jsonl events 103860-103862: same import.call → kernel.call → kernel.return = 0xc0000034.

Canary's next event (idx 103863): RtlNtStatusToDosError(0xc0000034) -> 2 (ERROR_FILE_NOT_FOUND). Sylpheed has proper NOT_FOUND handling and continues; the synth-empty was masking the correct branch.

Canary semantics (xboxkrnl_io.cc:39-111)

NtCreateFile_entry is a thin wrapper:

  1. Translate the OBJECT_ATTRIBUTES.name ANSI_STRING to a path.
  2. Path validation: IsValidPathSTATUS_OBJECT_NAME_INVALID on reject.
  3. Resolve the root_directory handle if non-zero.
  4. Call kernel_state()->file_system()->OpenFile(...).
  5. Return the OpenFile status verbatim. If the file isn't present in any mounted device, that's STATUS_OBJECT_NAME_NOT_FOUND (VirtualFileSystem::ResolvePath returns nullptr for unresolved paths, which OpenFile translates to NOT_FOUND).
  6. On success: alloc handle and write to handle_out.

For game:\dat\files.tbl specifically: the game: symlink points to the disc device. The disc image ISO doesn't contain dat/files.tbl (disc dump omission — likely a region-specific or build-time-only file). OpenFile returns NOT_FOUND, the function returns NOT_FOUND.

Ours pre-fix (exports.rs open_vfs_file)

open_vfs_file had three paths:

  1. Empty path (root-of-device) → synth-empty handle (special-cased for NtCreateFile("game:\") disc-validation probe).
  2. cache: prefix → host-FS-backed via open_cache_file (AUDIT-038).
  3. Otherwise: call vfs.read_file(&path). On Ok(bytes) return handle. On Err(e): synth a zero-byte file and return SUCCESS.

The (3) Err branch was the synth-empty fallback. Its docstring acknowledged two rationales:

  1. Writable system partitions (cache:, partition0:, partition1:) aren't backed by the disc — we synth so opens succeed.
  2. Disc files that didn't make it into the ISO rip — returning NOT_FOUND would make Sylpheed's boot validator call XamShowDirtyDiscErrorUI → dashboard exit.

But rationale (1) was already covered by the AUDIT-038 cache: short-circuit, and rationale (2) is empirically WRONG: canary returns NOT_FOUND for game:\dat\files.tbl and Sylpheed continues — there is no XamShowDirtyDiscErrorUI event in canary's trace anywhere near idx 103862. The synth-empty was misleading the boot trajectory in a non-canary direction.

Fix shape

Two surgical changes:

  1. New helper is_disc_prefix(raw_path: &str) -> bool that recognises game:\, d:\/D:\, and \Device\Cdrom0\ (the subset of path::DEVICE_PREFIXES that resolves to the read-only disc).

  2. open_vfs_file captures the raw path via crate::path::object_attributes_raw_name (alongside the existing normalised path). In the Err(e) arm, if is_disc_prefix(&raw_path), return STATUS_OBJECT_NAME_NOT_FOUND (mirroring canary). Otherwise keep the legacy synth-empty.

The cache: short-circuit is upstream of this branch and unchanged; its existing NOT_FOUND-on-FILE_OPEN-miss behaviour is preserved.

Tests

  • is_disc_prefix_recognises_disc_aliases (12 assertions, mix of disc and non-disc prefixes including case variants).
  • nt_create_file_game_prefix_missing_returns_not_found — primary.
  • nt_create_file_cdrom_prefix_missing_returns_not_found\Device\Cdrom0\ alias.
  • nt_create_file_non_disc_prefix_missing_still_synthesizes — regression guard for \Device\Harddisk0\Partition1\sys.bin.

Risk analysis (pre-fix)

  • Sylpheed might crash on NOT_FOUND: REFUTED. Canary returns NOT_FOUND, then Sylpheed calls RtlNtStatusToDosError and continues; +712 events of further progress observed in cold-vs-cold (no fault, no XamShowDirtyDiscErrorUI).
  • Reading-error #23 regression: matched-prefix MUST grow, not shrink. Cold-vs-cold confirms 103862 → 104574 (+712); no regression.
  • Sister-chain regression: confirmed unchanged for all 5 sister chains (see cold-vs-cold-result.md).
  • Stable digest shift: expected; the synth-empty was emitting one extra kernel.call/return pair (synth-success path); removing it for disc paths drops imports 40447 → 40390 (-57). Phase B image_loaded_sha256 is unchanged.