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>
7.5 KiB
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:
- Translates the ANSI object name to a guest VFS path
(
util::TranslateAnsiPath). - Calls
kernel_state()->file_system()->ResolvePath(target_path). - On hit, populates the OUT struct from
entry->{create,access, write}_timestamp()+entry->allocation_size()+entry->size()entry->attributes(). ReturnsX_STATUS_SUCCESS.
- On miss, returns
X_STATUS_NO_SUCH_FILE(NOTOBJECT_NAME_NOT_FOUND).
xboxkrnl_io.cc:498-512:
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 buildsHostPathEntryinstances viaHostPathEntry::Create. - Per-create by
Entry::CreateEntry/HostPathEntry::CreateEntryInternal(xenia-canary/src/xenia/vfs/entry.cc:88/host_path_entry.cc:78-98), called fromVirtualFileSystem::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:
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 iffdevice->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 baseDevicector defaultsbytes_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>toKernelState. - Eager mount-time scan via
populate_cache_entries_from_host(mirrors canary'sHostPathDevice::PopulateEntry). - Per-create registration via
register_cache_entry(mirrors canary'sEntry::CreateEntry). nt_query_full_attributes_fileconsults 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 |