diff options
| author | SulfurNitride <lukew19@proton.me> | 2026-06-24 14:45:04 -0500 |
|---|---|---|
| committer | SulfurNitride <lukew19@proton.me> | 2026-06-24 14:45:04 -0500 |
| commit | 0e993b45d3c4db36bf01ec0e56e11fd836cc59a2 (patch) | |
| tree | 0882e6f3bada56a32d7e36a16ed180c0b3093dc8 | |
| parent | 59f3ba27e5d180c8e1861b41a1db3f58663bdd0f (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.cpp | 66 |
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; |
