Compare commits

...

1 Commits

Author SHA1 Message Date
MechaCat02
3f5d5cf5f7 [iterate-2Z] Implement NtSetInformationFile FileRenameInformation for cache: files
The GamePart title-logo gate first-divergence: Sylpheed's asset cache
decompresses each packed resource to a staging `cache:\<hash><tail>.tmp`
file, then renames it into its final nested path `cache:\<hash>\<dir>\<file>`
(e.g. the title logo texture `\69d8e45c\e\534ffea`) via
NtSetInformationFile class 10 (XFileRenameInformation). Our handler treated
class 10 as a permissive no-op (catch-all `_ => STATUS_SUCCESS`), so the host
rename never happened: the nested target directories were created but left
EMPTY while the decompressed data stayed in the flat `.tmp` file. When the
title later reads back `\69d8e45c\...` to build the logo texture the read
misses, so the textured logo pixel shader (canary `E59B2B3D`, tfetch2D) is
never dispatched and the logo never renders.

Fix: implement class 10 faithfully, mirroring canary
`xboxkrnl_io_info.cc:226` (`X_FILE_RENAME_INFORMATION{ replace_existing@0,
root_dir_handle@4, ANSI_STRING@8 }` -> `file->Rename(TranslateAnsiPath)`).
Read the target path from the embedded ANSI_STRING at info_ptr+8, resolve it
against the host cache backing dir (`resolve_cache_path`), create the parent
dirs, `std::fs::rename` the backing file, and update the handle's `path` +
`host_path`. Non-cache (read-only VFS) sources keep the prior permissive
acknowledge. Verified at runtime: 20 renames/80M now move
`69d8e45ce534ffea.tmp -> 69d8e45c/e/534ffea` etc., and the nested cache tree
now matches canary's HostPathDevice layout byte-for-byte (data present, not
empty dirs).

Made `path::read_ansi_string` pub so the handler can parse the rename target.

Deterministic + golden-invariant: two `check --gpu-inline --stable-digest
-n 50000000` runs are byte-identical and the 50M stable digest is unchanged
(draws=718/swaps=147/6 shaders/tex=0); the logo read-back occurs later than
the observable window so GPU counters at 1B/2.5B are unchanged
(2.5B: draws=48734, swaps=16060, still 6 flat shaders, texture_decodes=0).
The fix is a verified-necessary precondition — without it the nested asset
read-back is guaranteed to miss. A downstream gate (the 2nd title thread's
load-completion post skipped when its notify target `[r29+8]==0`, and the
later read-back phase being beyond 2.5B) remains for follow-up.

New test: `nt_set_information_file_rename_moves_cache_file` (678 total, was
677).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 21:33:25 +02:00
2 changed files with 135 additions and 1 deletions

View File

@@ -1652,6 +1652,79 @@ fn nt_set_information_file(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut
return;
}
// XFileRenameInformation (10): move the backing file to a new path.
// Sylpheed's asset-cache decompresses each packed resource to a staging
// `cache:\<hash><tail>.tmp` then renames it into its final nested path
// `cache:\<hash>\<dir>\<file>`. Without an actual host-FS rename the
// nested target stays empty, the later read-back of the decompressed
// asset (e.g. the title logo texture `\69d8e45c\e\534ffea`) misses, and
// the logo never loads. Mirror canary `xboxkrnl_io_info.cc:226`
// (`X_FILE_RENAME_INFORMATION{ replace_existing@0, root_dir_handle@4,
// ansi_string@8 }` → `file->Rename(TranslateAnsiPath(ansi_string))`).
if info_class == 10 {
// Read the target path from the embedded ANSI_STRING at info_ptr+8.
let target_raw = match crate::path::read_ansi_string(mem, info_ptr + 8) {
Some(s) if !s.is_empty() => s,
_ => {
const STATUS_OBJECT_NAME_INVALID: u64 = 0xC000_0033;
ctx.gpr[3] = STATUS_OBJECT_NAME_INVALID;
return;
}
};
// Resolve the destination against the host cache backing dir. We only
// support renames within the writable `cache:` mount (the only place
// a guest can create files); disc/synth entries are read-only.
let new_host = state.resolve_cache_path(&target_raw);
// Current backing host path of the handle.
let old_host = match state.objects.get(&handle) {
Some(KernelObject::File { host_path: Some(hp), .. }) => Some(hp.clone()),
Some(KernelObject::File { .. }) => None,
_ => {
ctx.gpr[3] = STATUS_INVALID_HANDLE;
return;
}
};
let status: u64 = match (old_host, new_host) {
(Some(old), Some(new)) => {
if let Some(parent) = new.parent() {
let _ = std::fs::create_dir_all(parent);
}
match std::fs::rename(&old, &new) {
Ok(()) => {
// Update the handle so subsequent I/O targets the new
// host path + guest path.
if let Some(KernelObject::File { path, host_path, .. }) =
state.objects.get_mut(&handle)
{
*path = crate::path::normalize_path(&target_raw);
*host_path = Some(new.clone());
}
tracing::info!(
"NtSetInformationFile rename cache {:?} -> {:?} ({:?})",
old, new, target_raw
);
STATUS_SUCCESS
}
Err(e) => {
tracing::warn!(
"NtSetInformationFile rename {:?} -> {:?} failed: {}",
old, new, e
);
STATUS_UNSUCCESSFUL
}
}
}
// Non-cache (read-only VFS) source/target: acknowledge without a
// host move, matching the prior permissive behaviour.
_ => STATUS_SUCCESS,
};
if iosb_ptr != 0 {
write_io_status_block(mem, iosb_ptr, status as u32, info_length);
}
ctx.gpr[3] = status;
return;
}
// Handle lookup.
let Some(KernelObject::File { size, position, host_path, .. }) = state.objects.get_mut(&handle) else {
ctx.gpr[3] = STATUS_INVALID_HANDLE;
@@ -5581,6 +5654,67 @@ mod tests {
}
}
/// `NtSetInformationFile` class 10 (`XFileRenameInformation`) must move
/// the backing host file to the new `cache:` path and update the handle.
/// Mirrors Sylpheed's asset-cache `.tmp` → `\<hash>\<dir>\<file>` move;
/// without it the nested target stays empty and the decompressed asset
/// (logo texture) never reads back. Faithful to canary `file->Rename`.
#[test]
fn nt_set_information_file_rename_moves_cache_file() {
let (mut ctx, mut mem, mut state) = fresh();
// Real temp cache root + a staging `.tmp` file with known bytes.
let root = std::env::temp_dir().join(format!("xenia-rs-rename-test-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(&root).unwrap();
let old_host = root.join("69d8e45ce534ffea.tmp");
std::fs::write(&old_host, b"LOGOTEX!").unwrap();
state.cache_root = Some(root.clone());
// Open handle whose backing host_path is the staging file.
let handle = state.alloc_handle_for(KernelObject::File {
path: "69d8e45ce534ffea.tmp".to_string(),
size: 8,
position: 0,
data: Arc::new(Vec::new()),
dir_enum_pos: None,
host_path: Some(old_host.clone()),
});
// X_FILE_RENAME_INFORMATION { replace@0, root_dir@4, ANSI_STRING@8 }.
// ANSI_STRING { len u16, max u16, buf u32 } at info_ptr+8; buffer holds
// the target path "cache:\69d8e45c\e\534ffea".
let info_ptr = SCRATCH_BASE + 0x100;
let str_buf = SCRATCH_BASE + 0x200;
let target = b"cache:\\69d8e45c\\e\\534ffea";
for (i, b) in target.iter().enumerate() {
mem.write_u8(str_buf + i as u32, *b);
}
mem.write_u32(info_ptr, 0); // replace_existing
mem.write_u32(info_ptr + 4, 0); // root_dir_handle
mem.write_u16(info_ptr + 8, target.len() as u16); // ANSI_STRING.Length
mem.write_u16(info_ptr + 10, target.len() as u16); // MaximumLength
mem.write_u32(info_ptr + 12, str_buf); // Buffer
let iosb_ptr = SCRATCH_BASE + 0x140;
ctx.gpr[3] = handle as u64;
ctx.gpr[4] = iosb_ptr as u64;
ctx.gpr[5] = info_ptr as u64;
ctx.gpr[6] = 16;
ctx.gpr[7] = 10; // XFileRenameInformation
nt_set_information_file(&mut ctx, &mut mem, &mut state);
assert_eq!(ctx.gpr[3], STATUS_SUCCESS);
// Staging file gone; nested target exists with the same bytes.
let new_host = root.join("69d8e45c").join("e").join("534ffea");
assert!(!old_host.exists(), "staging .tmp should be moved away");
assert_eq!(std::fs::read(&new_host).unwrap(), b"LOGOTEX!");
// Handle now points at the new host + guest path.
match state.objects.get(&handle) {
Some(KernelObject::File { host_path: Some(hp), path, .. }) => {
assert_eq!(hp, &new_host);
assert_eq!(path, "cache:/69d8e45c/e/534ffea");
}
_ => panic!("file handle lost or host_path missing"),
}
let _ = std::fs::remove_dir_all(&root);
}
/// Read-only VFS — truncating to a different size must fail with
/// `STATUS_UNSUCCESSFUL`, matching Canary's error path when
/// `file->SetLength(...)` can't honour the request.

View File

@@ -13,7 +13,7 @@ use xenia_memory::{GuestMemory, MemoryAccess};
/// u16 Length
/// u16 MaximumLength
/// u32 Buffer (guest pointer)
fn read_ansi_string(mem: &GuestMemory, ptr: u32) -> Option<String> {
pub fn read_ansi_string(mem: &GuestMemory, ptr: u32) -> Option<String> {
if ptr == 0 {
return None;
}