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:
MechaCat02
2026-06-05 07:19:08 +02:00
parent acd1656753
commit ef93a4fa14
620 changed files with 108303 additions and 1 deletions

View File

@@ -0,0 +1,179 @@
# Phase C+12 — investigation (canary's NtQueryFullAttributesFile)
## Canary's verified behavior
### Entry point
`NtQueryFullAttributesFile_entry` lives in
`xenia-canary/src/xenia/kernel/xboxkrnl/xboxkrnl_io.cc:474-513`. It
takes `(X_OBJECT_ATTRIBUTES*, X_FILE_NETWORK_OPEN_INFORMATION*)` and:
1. Translates the ANSI object name to a guest VFS path
(`util::TranslateAnsiPath`).
2. Calls `kernel_state()->file_system()->ResolvePath(target_path)`.
3. On hit, populates the OUT struct from `entry->{create,access,
write}_timestamp()` + `entry->allocation_size()` + `entry->size()`
+ `entry->attributes()`. Returns `X_STATUS_SUCCESS`.
4. On miss, returns `X_STATUS_NO_SUCH_FILE` (NOT
`OBJECT_NAME_NOT_FOUND`).
`xboxkrnl_io.cc:498-512`:
```cpp
auto entry = kernel_state()->file_system()->ResolvePath(target_path);
if (entry) {
file_info->creation_time = entry->create_timestamp();
file_info->last_access_time= entry->access_timestamp();
file_info->last_write_time = entry->write_timestamp();
file_info->change_time = entry->write_timestamp(); // <-- same as write
file_info->allocation_size = entry->allocation_size();
file_info->end_of_file = entry->size();
file_info->attributes = entry->attributes();
return X_STATUS_SUCCESS;
}
return X_STATUS_NO_SUCH_FILE;
```
### Resolution mechanism
`VirtualFileSystem::ResolvePath`
(`xenia-canary/src/xenia/vfs/virtual_file_system.cc:129-158`) walks
the in-memory `Entry` tree. It does NOT re-stat the host FS. The
tree is populated:
* **Eagerly at mount time** by
`HostPathDevice::Initialize` →
`HostPathDevice::PopulateEntry`
(`xenia-canary/src/xenia/vfs/devices/host_path_device.cc:31-75`)
which recursively walks the host directory and builds
`HostPathEntry` instances via `HostPathEntry::Create`.
* **Per-create** by `Entry::CreateEntry` /
`HostPathEntry::CreateEntryInternal`
(`xenia-canary/src/xenia/vfs/entry.cc:88` /
`host_path_entry.cc:78-98`), called from
`VirtualFileSystem::OpenFile` → `CreatePath`
(`virtual_file_system.cc:160-187`) on successful NtCreateFile create.
### Struct layout
`X_FILE_NETWORK_OPEN_INFORMATION` in
`xenia-canary/src/xenia/kernel/info/file.h:117-127`:
```cpp
struct X_FILE_NETWORK_OPEN_INFORMATION { // 56 bytes
be<uint64_t> creation_time;
be<uint64_t> last_access_time;
be<uint64_t> last_write_time;
be<uint64_t> change_time;
be<uint64_t> allocation_size;
be<uint64_t> end_of_file;
be<uint32_t> attributes;
be<uint32_t> pad;
};
```
All multibyte fields are `be<>`, i.e. byte-swapped on write to guest
memory (Xbox 360 is big-endian).
### Attribute / size derivation
`HostPathEntry::Create`
(`xenia-canary/src/xenia/vfs/devices/host_path_entry.cc:32-54`):
* `attributes_ = kFileAttributeDirectory (0x10)` if host is a dir.
* `attributes_ = kFileAttributeNormal (0x80)` for regular files.
`kFileAttributeReadOnly (0x01)` is OR'd in iff
`device->is_read_only()`. `cache:` mounts are read-WRITE
(`xenia_main.cc:641-651`), so the bit is NOT set.
* `size_ = file_info.total_size`.
* `allocation_size_ = round_up(total_size, device->bytes_per_sector())`.
The base `Device` ctor defaults `bytes_per_sector_ = 512`
(`xenia-canary/src/xenia/vfs/device.h`); host-path devices don't
override.
### Timestamp format
Windows FILETIME — 100-ns ticks since 1601-01-01 UTC. On the Windows
debug build, canary derives these via
`COMBINE_TIME(WIN32_FILE_ATTRIBUTE_DATA::ftCreationTime)`
(`filesystem_win.cc:226-228`), which is already FILETIME-format.
### Why ours diverged at idx 102404
ours's `nt_query_full_attributes_file` previously consulted
`std::fs::metadata` directly. On TRUE cold-vs-cold (both engines'
cache wiped), the host file `cache/d4ea4615/e/46ee8ca` doesn't
exist → ours returned `STATUS_NO_SUCH_FILE`. So why does canary
return SUCCESS?
The original Phase C+11.1 protocol wiped `~/.local/share/Xenia/cache`
but the Windows-debug canary running under wine actually stores its
cache under
`xenia-canary/build-cross/bin/Windows/Debug/cache/` (canary's
storage root resolves to `GetUserFolder()` when no `portable.txt`
is present, but the build-cross binary writes there anyway — verified
by `Storage root: Z:\home\fabi\RE - Project Sylpheed\xenia-canary\
build-cross\bin\Windows\Debug` in the canary stdout). So the
"cold-vs-cold" baseline at C+11.1 was actually
ours-cold-vs-canary-warm: canary's binary-dir cache survived the
wipe and the pre-existing `46ee8ca` file (May 12 timestamp) made
canary's mount-time scan populate the in-memory tree.
A TRUE cold-vs-cold (Phase C+12 protocol, see `re-validation.md`)
that ALSO wipes the binary-dir cache shows canary ALSO returns
NO_SUCH_FILE for `cache:\d4ea4615\e\46ee8ca` at idx 102404 — and
the divergence at 102404 dissolves.
But the underlying gap remains: canary maintains an in-memory entry
tree that ours did not, and any boot where canary has a populated
host cache (Sylpheed's normal warm/persistent operation) would see
canary succeed where ours misses. Fixing this is in scope per the
user directive.
## Architecture decision
Architecture **B** (in-memory entry tree mirroring canary):
* Add `cache_entries: HashMap<String, CacheEntryMeta>` to `KernelState`.
* Eager mount-time scan via `populate_cache_entries_from_host`
(mirrors canary's `HostPathDevice::PopulateEntry`).
* Per-create registration via `register_cache_entry` (mirrors canary's
`Entry::CreateEntry`).
* `nt_query_full_attributes_file` consults the tree first;
defensive host-FS fallback only fires when the tree missed but
the file is on disk (refreshes tree as a side effect).
Architecture A alone (just stat host FS) would technically work for
the specific 102404 case after the corrected cold-vs-cold protocol,
but would diverge from canary on any sequence where the in-memory
tree's freshness matters (e.g. an entry created earlier this same
boot whose host write hasn't flushed yet — rare but possible if a
future game pre-allocates entries before the corresponding write).
Architecture B keeps semantic parity with canary regardless of the
host-FS flush timing.
## Cold-vs-cold protocol correction
The C+11.1 protocol wiped `~/.local/share/Xenia/cache`. The Phase
C+12 protocol additionally wipes
`xenia-canary/build-cross/bin/Windows/Debug/cache/`
(plus `cache0/`, `cache1/`) because that's where the Windows-debug
canary actually writes its cache under wine. Backup is preserved at
`/tmp/canary-binary-cache-backup.tar.gz` (4.7 MB, 23 files) and a
copy lives in this audit-run dir as
`canary-binary-cache-pre-wipe.tar.gz`.
## File:line citations
| canary source | line(s) | what |
|---|---|---|
| `xboxkrnl_io.cc` | 474-513 | `NtQueryFullAttributesFile_entry` body |
| `xboxkrnl_io.cc` | 504 | `change_time = write_timestamp()` |
| `xboxkrnl_io.cc` | 512 | `return X_STATUS_NO_SUCH_FILE` |
| `info/file.h` | 117-127 | `X_FILE_NETWORK_OPEN_INFORMATION` layout |
| `vfs/entry.h` | 67-95 | `kFileAttribute*` consts + accessors |
| `vfs/entry.cc` | 88-104 | `Entry::CreateEntry` |
| `vfs/virtual_file_system.cc` | 129-158 | `ResolvePath` |
| `vfs/devices/host_path_device.cc` | 31-75 | `Initialize` + `PopulateEntry` |
| `vfs/devices/host_path_entry.cc` | 32-54 | `HostPathEntry::Create` (attrs + size) |
| `base/filesystem_win.cc` | 208-230 | `GetInfo` (FILETIME timestamps) |
| `app/xenia_main.cc` | 641-651 | `cache:` mount registration |