aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSulfurNitride <lukew19@proton.me>2026-06-24 14:45:04 -0500
committerSulfurNitride <lukew19@proton.me>2026-06-24 14:45:04 -0500
commit0e993b45d3c4db36bf01ec0e56e11fd836cc59a2 (patch)
tree0882e6f3bada56a32d7e36a16ed180c0b3093dc8
parent59f3ba27e5d180c8e1861b41a1db3f58663bdd0f (diff)
Fix shader cache write failures and INI key clobber
Shader cache (mo2_lookup): Starfield skips mkdir("ShaderCache/Lighting/") and goes straight to CreateFile("ShaderCache/Lighting/X.pso"). USVFS handles this by intercepting at Win32 level and auto-creating parent dirs in overwrite. FUSE operates below the kernel path-resolver, so if a parent component is missing the kernel returns ENOENT before mo2_create is ever called. Fix: in mo2_lookup, when a name has no '.' (directory-like) and is not found in the VFS, auto-create it in staging and return a positive inode so the kernel can continue resolving the path. createDirectory uses create_directories internally, so staging parent dirs are also created when the parent came from overwrite rather than this session. INI key clobber (mo2_open / mo2_write): keep_cache=1 with AUTO_INVAL_DATA disabled let the kernel serve stale pre-COW page cache on re-reads. Wine's WritePrivateProfileString does open→read→close→open(O_TRUNC)→write per key; after the first write triggered COW, subsequent reads got old cached content, so each new key was written on top of the original file — only the last key survived. Fix: call fuse_lowlevel_notify_inval_inode at all four COW and O_TRUNC paths to flush stale kernel pages precisely at the moment the backing path changes, without the blanket performance cost of keep_cache=0. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
-rw-r--r--src/src/vfs/mo2filesystem.cpp66
1 files changed, 65 insertions, 1 deletions
diff --git a/src/src/vfs/mo2filesystem.cpp b/src/src/vfs/mo2filesystem.cpp
index 5176614..9d2eb14 100644
--- a/src/src/vfs/mo2filesystem.cpp
+++ b/src/src/vfs/mo2filesystem.cpp
@@ -12,6 +12,7 @@
#include <cstdint>
#include <cstring>
#include <filesystem>
+#include <string_view>
#include <system_error>
#include <utility>
@@ -1380,6 +1381,54 @@ void mo2_lookup(fuse_req_t req, fuse_ino_t parent, const char* name)
const auto lr = lookupChild(ctx, parent, name);
if (!lr.found) {
+ // USVFS-parity: games (e.g. Starfield) sometimes skip mkdir for intermediate
+ // directories and go straight to CreateFile("ShaderCache/Lighting/X.pso").
+ // USVFS intercepts at Win32 level and auto-creates parent dirs in overwrite.
+ // FUSE sits below the kernel path-resolver, so ENOENT here prevents mo2_create
+ // from ever being called. Work around it: if the requested name looks like a
+ // directory (no '.' — file-like names are excluded to avoid spurious dirs),
+ // auto-create it in staging so the kernel can continue resolving the path.
+ // createDirectory uses create_directories internally, so staging parent dirs
+ // are also created if the parent came from overwrite rather than this session.
+ const std::string_view nameView(name);
+ if (nameView.find('.') == std::string_view::npos) {
+ bool parentOk = false;
+ const std::string parentPath = inodeToPath(ctx, parent, &parentOk);
+ if (parentOk) {
+ const std::string childName = canonicalChildName(ctx, parentPath, name);
+ const std::string childPath = joinPath(parentPath, childName);
+ std::error_code createErr;
+ if (ctx->overwrite->createDirectory(childPath, &createErr)) {
+ {
+ std::unique_lock lock(ctx->tree_mutex);
+ ctx->tree->root.insertDirectory(splitPath(childPath));
+ ++ctx->tree->dir_count;
+ invalidateNodeCache(ctx, childPath);
+ }
+ invalidateDirCache(ctx, parentPath);
+ fuse_ino_t dirIno;
+ {
+ std::unique_lock lock(ctx->inode_mutex);
+ dirIno = ctx->inodes->getOrCreate(childPath);
+ }
+ struct fuse_entry_param e;
+ std::memset(&e, 0, sizeof(e));
+ e.ino = dirIno;
+ e.attr_timeout = TTL_SECONDS;
+ e.entry_timeout = TTL_SECONDS;
+ fillStatForDir(&e.attr, dirIno, ctx->uid, ctx->gid);
+ {
+ std::scoped_lock cacheLock(ctx->lookup_cache_mutex);
+ ctx->lookup_cache[cacheKey] =
+ Mo2FsContext::LookupCacheEntry{.child_ino=dirIno, .entry=e};
+ }
+ std::fprintf(stderr, "[VFS] auto-mkdir staging: %s\n", childPath.c_str());
+ fuse_reply_entry(req, &e);
+ return;
+ }
+ }
+ }
+
struct fuse_entry_param e;
std::memset(&e, 0, sizeof(e));
e.ino = 0;
@@ -1849,6 +1898,9 @@ void mo2_open(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi)
openMtime = std::chrono::system_clock::now();
updateFileNodeKnown(ctx, path, realPath, originForPath(ctx, realPath),
openSize, openMtime);
+ // Flush stale kernel page cache so the next read sees the truncated
+ // content, not the pre-truncation data cached from a prior open.
+ fuse_lowlevel_notify_inval_inode(fuse_req_session(req), ino, 0, 0);
}
} else {
// Mod file disappeared — fall through to normal handling
@@ -1868,6 +1920,7 @@ void mo2_open(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi)
openMtime = std::chrono::system_clock::now();
updateFileNodeKnown(ctx, path, realPath, originForPath(ctx, realPath),
openSize, openMtime);
+ fuse_lowlevel_notify_inval_inode(fuse_req_session(req), ino, 0, 0);
}
} else if (fd < 0 && truncateOnOpen) {
try {
@@ -1887,6 +1940,10 @@ void mo2_open(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi)
openMtime = std::chrono::system_clock::now();
updateFileNodeKnown(ctx, path, newPath, originForPath(ctx, newPath),
openSize, openMtime);
+ // The file's backing path changed from mod/base-game to staging.
+ // Flush the kernel page cache so subsequent reads go through FUSE
+ // and see the staging content, not the pre-COW cached data.
+ fuse_lowlevel_notify_inval_inode(fuse_req_session(req), ino, 0, 0);
}
} catch (...) {
fuse_reply_err(req, EIO);
@@ -2016,7 +2073,7 @@ void mo2_read(fuse_req_t req, fuse_ino_t /*ino*/, size_t size, off_t off,
fuse_reply_data(req, &buf, FUSE_BUF_SPLICE_MOVE);
}
-void mo2_write(fuse_req_t req, fuse_ino_t /*ino*/, const char* buf, size_t size,
+void mo2_write(fuse_req_t req, fuse_ino_t ino, const char* buf, size_t size,
off_t off, struct fuse_file_info* fi)
{
Mo2FsContext* ctx = getContext(req);
@@ -2089,6 +2146,13 @@ void mo2_write(fuse_req_t req, fuse_ino_t /*ino*/, const char* buf, size_t size,
}
realPath = newPath;
updateFileNode(ctx, relativePath, newPath, originForPath(ctx, newPath));
+ // Lazy COW: backing path just changed to staging. Flush the kernel page
+ // cache so the caller's next read sees staging content, not the stale
+ // pre-COW pages. This is the counterpart to the O_TRUNC invalidation in
+ // mo2_open — without it, repeated WritePrivateProfileString calls on the
+ // same INI file each read the old mod-file content and only the last
+ // write survives.
+ fuse_lowlevel_notify_inval_inode(fuse_req_session(req), ino, 0, 0);
} catch (...) {
fuse_reply_err(req, EIO);
return;