diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/src/CMakeLists.txt | 2 | ||||
| -rw-r--r-- | src/src/fuseconnector.cpp | 39 | ||||
| -rw-r--r-- | src/src/fuseconnector.h | 2 | ||||
| -rw-r--r-- | src/src/organizercore.cpp | 32 | ||||
| -rw-r--r-- | src/src/protonlauncher.cpp | 23 | ||||
| -rw-r--r-- | src/src/protonlauncher.h | 2 | ||||
| -rw-r--r-- | src/src/settingsdialog.ui | 44 | ||||
| -rw-r--r-- | src/src/settingsdialogproton.cpp | 69 | ||||
| -rw-r--r-- | src/src/settingsdialogproton.h | 3 | ||||
| -rw-r--r-- | src/src/spawn.cpp | 2 | ||||
| -rw-r--r-- | src/src/vfs/mo2filesystem.cpp | 125 | ||||
| -rw-r--r-- | src/src/vfs/mo2filesystem.h | 12 | ||||
| -rw-r--r-- | src/src/vfs/trackedwrites.cpp | 20 |
13 files changed, 283 insertions, 92 deletions
diff --git a/src/src/CMakeLists.txt b/src/src/CMakeLists.txt index e69a814..d7a7642 100644 --- a/src/src/CMakeLists.txt +++ b/src/src/CMakeLists.txt @@ -115,7 +115,7 @@ target_link_libraries(organizer PRIVATE target_compile_definitions(organizer PRIVATE SPDLOG_USE_STD_FORMAT - FUSE_USE_VERSION=35 + FUSE_USE_VERSION=312 $<$<TARGET_EXISTS:Qt6::WebEngineWidgets>:MO2_WEBENGINE>) if(NOT WIN32) diff --git a/src/src/fuseconnector.cpp b/src/src/fuseconnector.cpp index c749d69..b146359 100644 --- a/src/src/fuseconnector.cpp +++ b/src/src/fuseconnector.cpp @@ -160,9 +160,10 @@ void setupFuseOps(struct fuse_lowlevel_ops* ops) ops->init = mo2_init; ops->lookup = mo2_lookup; ops->getattr = mo2_getattr; - ops->opendir = mo2_opendir; - ops->readdir = mo2_readdir; - ops->open = mo2_open; + ops->opendir = mo2_opendir; + ops->readdir = mo2_readdir; + ops->readdirplus = mo2_readdirplus; + ops->open = mo2_open; ops->read = mo2_read; ops->write = mo2_write; ops->create = mo2_create; @@ -291,14 +292,15 @@ bool FuseConnector::mount( std::fprintf(stderr, "[VFS] WARNING: tracking file path is empty!\n"); } - m_context = std::make_shared<Mo2FsContext>(); - m_context->tree = tree; - m_context->inodes = std::make_unique<InodeTable>(); - m_context->overwrite = std::make_unique<OverwriteManager>(m_stagingDir, m_overwriteDir); - m_context->tracked_writes = m_trackedWrites; - m_context->backing_dir_fd = m_backingFd; - m_context->uid = ::getuid(); - m_context->gid = ::getgid(); + m_context = std::make_shared<Mo2FsContext>(); + m_context->tree = tree; + m_context->inodes = std::make_unique<InodeTable>(); + m_context->overwrite = std::make_unique<OverwriteManager>(m_stagingDir, m_overwriteDir); + m_context->tracked_writes = m_trackedWrites; + m_context->backing_dir_fd = m_backingFd; + m_context->uid = ::getuid(); + m_context->gid = ::getgid(); + m_context->passthrough_requested = m_passthroughEnabled; // NOTE: Do NOT include mount_point here — low-level API passes it // separately to fuse_session_mount(). Including it here causes @@ -335,7 +337,13 @@ bool FuseConnector::mount( } m_fuseThread = std::thread([this]() { - fuse_session_loop_mt(m_session, nullptr); + // Enable clone_fd: each worker thread gets its own /dev/fuse fd, + // eliminating contention on a single fd lock under heavy parallel I/O. + struct fuse_loop_config* cfg = fuse_loop_cfg_create(); + fuse_loop_cfg_set_clone_fd(cfg, 1); + fuse_loop_cfg_set_max_threads(cfg, 16); + fuse_session_loop_mt(m_session, cfg); + fuse_loop_cfg_destroy(cfg); }); m_mounted = true; @@ -438,6 +446,13 @@ void FuseConnector::setTrackingFilePath(const std::string& path) std::fprintf(stderr, "[VFS] setTrackingFilePath: '%s'\n", path.c_str()); } +void FuseConnector::setPassthroughEnabled(bool enabled) +{ + m_passthroughEnabled = enabled; + std::fprintf(stderr, "[VFS] passthrough %s by user\n", + enabled ? "enabled" : "disabled"); +} + std::shared_ptr<TrackedWrites> FuseConnector::trackedWrites() const { return m_trackedWrites; diff --git a/src/src/fuseconnector.h b/src/src/fuseconnector.h index a13f014..6d089bb 100644 --- a/src/src/fuseconnector.h +++ b/src/src/fuseconnector.h @@ -42,6 +42,7 @@ public: void setPluginLoadOrder(const std::vector<std::string>& load_order); void setTrackingFilePath(const std::string& path); + void setPassthroughEnabled(bool enabled); std::shared_ptr<TrackedWrites> trackedWrites() const; void unmount(); @@ -96,6 +97,7 @@ private: std::thread m_fuseThread; bool m_mounted = false; bool m_discardStaging = false; + bool m_passthroughEnabled = false; }; #endif diff --git a/src/src/organizercore.cpp b/src/src/organizercore.cpp index c0f1bf9..3896039 100644 --- a/src/src/organizercore.cpp +++ b/src/src/organizercore.cpp @@ -661,6 +661,13 @@ void OrganizerCore::prepareVFS() owPath.toStdString().c_str(), trackPath.toStdString().c_str());
m_USVFS.setTrackingFilePath(trackPath.toStdString());
}
+
+ // FUSE passthrough: read the per-instance setting and pass it to the VFS.
+ {
+ const bool passthrough =
+ QSettings().value("fluorine/fuse_passthrough", false).toBool();
+ m_USVFS.setPassthroughEnabled(passthrough);
+ }
#endif
m_USVFS.updateMapping(fileMapping(m_CurrentProfile->name(), QString()));
}
@@ -2100,20 +2107,29 @@ void OrganizerCore::syncOverwrite() #ifndef _WIN32
// Track files that were moved out of overwrite to mods.
// Files that existed before but are gone now were synced to a mod.
+ // Use profile priority order (highest-priority mod wins) so writes
+ // go to the correct mod when multiple mods contain the same path.
const QString modsDir = QDir::fromNativeSeparators(m_Settings.paths().mods());
+
+ // Mod names in ascending priority order (last = highest priority).
+ const QStringList modsByPriority =
+ m_ModList.allModsByProfilePriority(m_CurrentProfile.get());
+
for (const auto& relPath : beforeFiles) {
const QString owFile = modInfo->absolutePath() + "/" + relPath;
if (!QFile::exists(owFile)) {
- // Find which mod folder it ended up in
- QDirIterator modDirIter(modsDir, QDir::Dirs | QDir::NoDotAndDotDot);
- while (modDirIter.hasNext()) {
- modDirIter.next();
- const QString candidate = modDirIter.filePath() + "/" + relPath;
- if (QFile::exists(candidate)) {
- trackOverwriteMove(relPath, modDirIter.filePath());
- break;
+ // Find highest-priority mod that has this file.
+ QString bestModPath;
+ for (const auto& modName : modsByPriority) {
+ const QString modPath = modsDir + "/" + modName;
+ if (QFile::exists(modPath + "/" + relPath)) {
+ bestModPath = modPath;
+ // Don't break — keep going to find the highest-priority match.
}
}
+ if (!bestModPath.isEmpty()) {
+ trackOverwriteMove(relPath, bestModPath);
+ }
}
}
#endif
diff --git a/src/src/protonlauncher.cpp b/src/src/protonlauncher.cpp index 4339920..e2895e2 100644 --- a/src/src/protonlauncher.cpp +++ b/src/src/protonlauncher.cpp @@ -200,19 +200,6 @@ void wrapProgram(const QStringList& wrapperCommands, const QString& program, wrappedArguments.append(arguments); } -void maybeWrapWithSteamRun(bool useSteamRun, QString& program, QStringList& arguments) -{ - if (!useSteamRun) { - return; - } - - QStringList wrappedArgs; - wrappedArgs.append(program); - wrappedArgs.append(arguments); - program = QStringLiteral("steam-run"); - arguments = wrappedArgs; -} - bool isValidEnvKey(const QString& key) { if (key.isEmpty()) { @@ -252,7 +239,7 @@ bool parseEnvAssignment(const QString& token, QString& keyOut, QString& valueOut } // namespace ProtonLauncher::ProtonLauncher() - : m_steamAppId(0), m_useSteamRun(false), m_useSteamDrm(true) + : m_steamAppId(0), m_useSteamDrm(true) {} ProtonLauncher& ProtonLauncher::setBinary(const QString& path) @@ -313,12 +300,6 @@ ProtonLauncher& ProtonLauncher::setWrapper(const QString& wrapperCmd) return *this; } -ProtonLauncher& ProtonLauncher::setUseSteamRun(bool useSteamRun) -{ - m_useSteamRun = useSteamRun; - return *this; -} - ProtonLauncher& ProtonLauncher::setSteamDrm(bool useSteamDrm) { m_useSteamDrm = useSteamDrm; @@ -375,7 +356,6 @@ bool ProtonLauncher::launchWithProton(qint64& pid) const QString program; QStringList arguments; wrapProgram(m_wrapperCommands, protonScript, protonArgs, program, arguments); - maybeWrapWithSteamRun(m_useSteamRun, program, arguments); QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); env.remove("PYTHONHOME"); @@ -448,7 +428,6 @@ bool ProtonLauncher::launchDirect(qint64& pid) const QString program; QStringList arguments; wrapProgram(m_wrapperCommands, m_binary, m_arguments, program, arguments); - maybeWrapWithSteamRun(m_useSteamRun, program, arguments); QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); env.remove("PYTHONHOME"); diff --git a/src/src/protonlauncher.h b/src/src/protonlauncher.h index 6b3ee42..363d92b 100644 --- a/src/src/protonlauncher.h +++ b/src/src/protonlauncher.h @@ -20,7 +20,6 @@ public: ProtonLauncher& setPrefix(const QString& path); ProtonLauncher& setSteamAppId(uint32_t id); ProtonLauncher& setWrapper(const QString& wrapperCmd); - ProtonLauncher& setUseSteamRun(bool useSteamRun); ProtonLauncher& setSteamDrm(bool useSteamDrm); ProtonLauncher& setStoreVariant(const QString& variant); ProtonLauncher& addEnvVar(const QString& key, const QString& value); @@ -40,7 +39,6 @@ private: QString m_prefixPath; uint32_t m_steamAppId; QStringList m_wrapperCommands; - bool m_useSteamRun; bool m_useSteamDrm; QString m_storeVariant; // "GOG", "Epic", or empty for Steam QMap<QString, QString> m_envVars; diff --git a/src/src/settingsdialog.ui b/src/src/settingsdialog.ui index 6588a71..7704b56 100644 --- a/src/src/settingsdialog.ui +++ b/src/src/settingsdialog.ui @@ -1866,16 +1866,6 @@ If you disable this feature, MO will only display official DLCs this way. Please </property> <layout class="QVBoxLayout" name="verticalLayout_38"> <item> - <widget class="QCheckBox" name="steamRunCheckBox"> - <property name="text"> - <string>Wrap launches with steam-run</string> - </property> - <property name="toolTip"> - <string>Run launch and prefix setup commands through steam-run. Useful on NixOS and other immutable setups.</string> - </property> - </widget> - </item> - <item> <layout class="QHBoxLayout" name="horizontalLayout_14"> <item> <widget class="QLabel" name="label_65"> @@ -1893,6 +1883,40 @@ If you disable this feature, MO will only display official DLCs this way. Please </widget> </item> <item> + <widget class="QGroupBox" name="groupBox_vfsPerformance"> + <property name="title"> + <string>VFS Performance</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_vfsPerf"> + <item> + <widget class="QCheckBox" name="fusePassthroughCheckBox"> + <property name="text"> + <string>Enable FUSE Passthrough (near-native I/O)</string> + </property> + <property name="toolTip"> + <string>Let the kernel serve game file reads directly, bypassing userspace. Dramatically improves I/O performance for loading textures, meshes, and archives. Requires a one-time privilege grant (sudo prompt). Needs kernel 6.9+ and libfuse 3.16+.</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="passthroughStatusLabel"> + <property name="text"> + <string/> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + <property name="font"> + <font> + <pointsize>9</pointsize> + </font> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> <spacer name="verticalSpacer_16"> <property name="orientation"> <enum>Qt::Vertical</enum> diff --git a/src/src/settingsdialogproton.cpp b/src/src/settingsdialogproton.cpp index 0199744..36838c4 100644 --- a/src/src/settingsdialogproton.cpp +++ b/src/src/settingsdialogproton.cpp @@ -9,7 +9,9 @@ #include <uibase/utility.h> #include <nak_ffi.h> #include <atomic> +#include <QCheckBox> #include <QComboBox> +#include <QCoreApplication> #include <QDateTime> #include <QDialog> #include <QDialogButtonBox> @@ -40,12 +42,56 @@ ProtonSettingsTab::ProtonSettingsTab(Settings& s, SettingsDialog& d) ui->protonProgressBar->setValue(0); ui->protonProgressBar->setVisible(false); - ui->steamRunCheckBox->setChecked( - QSettings().value("fluorine/use_steam_run", false).toBool()); - ui->launchWrapperEdit->setPlaceholderText("mangohud --dlsym"); ui->launchWrapperEdit->setText(QSettings().value("fluorine/launch_wrapper").toString()); + // FUSE passthrough toggle — requires CAP_SYS_ADMIN on the binary. + ui->fusePassthroughCheckBox->setChecked( + QSettings().value("fluorine/fuse_passthrough", false).toBool()); + ui->passthroughStatusLabel->setText(QString()); + + QObject::connect(ui->fusePassthroughCheckBox, &QCheckBox::toggled, this, + [this](bool checked) { + if (!checked) { + // Disabling doesn't need sudo — just save the setting. + QSettings().setValue("fluorine/fuse_passthrough", false); + ui->passthroughStatusLabel->setText(tr("Passthrough disabled. Takes effect on next game launch.")); + return; + } + + // Enabling requires setting CAP_SYS_ADMIN on the binary. + // Find the actual binary path (ModOrganizer-core). + const QString binary = QCoreApplication::applicationFilePath(); + if (binary.isEmpty()) { + ui->fusePassthroughCheckBox->setChecked(false); + ui->passthroughStatusLabel->setText(tr("Could not determine binary path.")); + return; + } + + ui->passthroughStatusLabel->setText( + tr("Granting FUSE passthrough capability... (sudo prompt)")); + + // Use pkexec (graphical sudo) to set the file capability. + // This is a one-time operation — the capability persists across runs. + QProcess proc; + proc.setProgram("pkexec"); + proc.setArguments({"setcap", "cap_sys_admin+ep", binary}); + proc.start(); + proc.waitForFinished(60000); // 60s timeout for user to auth + + if (proc.exitCode() == 0) { + QSettings().setValue("fluorine/fuse_passthrough", true); + ui->passthroughStatusLabel->setText( + tr("Passthrough enabled. Takes effect on next game launch.")); + } else { + ui->fusePassthroughCheckBox->setChecked(false); + const QString err = QString::fromUtf8(proc.readAllStandardError()).trimmed(); + ui->passthroughStatusLabel->setText( + tr("Failed to set capability: %1").arg( + err.isEmpty() ? tr("authorization denied or pkexec not found") : err)); + } + }); + populateProtons(); QObject::connect(ui->protonVersionCombo, &QComboBox::currentIndexChanged, this, @@ -114,8 +160,6 @@ ProtonSettingsTab::ProtonSettingsTab(Settings& s, SettingsDialog& d) void ProtonSettingsTab::update() { - QSettings().setValue("fluorine/use_steam_run", - ui->steamRunCheckBox->isChecked()); QSettings().setValue("fluorine/launch_wrapper", ui->launchWrapperEdit->text()); } @@ -237,8 +281,7 @@ void ProtonSettingsTab::onCreatePrefix() ui->nakInstallLog->setVisible(true); ui->toggleInstallLog->setChecked(true); - startInstallTask(0, pfxPath, protonName, protonPath, - ui->steamRunCheckBox->isChecked()); + startInstallTask(0, pfxPath, protonName, protonPath); } void ProtonSettingsTab::onDeletePrefix() @@ -287,8 +330,7 @@ void ProtonSettingsTab::onRecreatePrefix() ui->toggleInstallLog->setChecked(true); startInstallTask(cfg->app_id, cfg->prefix_path, cfg->proton_name, - cfg->proton_path, - ui->steamRunCheckBox->isChecked()); + cfg->proton_path); } void ProtonSettingsTab::onOpenPrefixFolder() @@ -579,8 +621,7 @@ void ProtonSettingsTab::showGameRegistryDialog() void ProtonSettingsTab::startInstallTask(uint32_t appId, const QString& prefixPath, const QString& protonName, - const QString& protonPath, - bool useSteamRun) + const QString& protonPath) { m_pendingAppId = appId; m_pendingPrefixPath = prefixPath; @@ -595,14 +636,11 @@ void ProtonSettingsTab::startInstallTask(uint32_t appId, const QString& prefixPa appId, prefixPath, protonName, - protonPath, - useSteamRun]() -> InstallResult { + protonPath]() -> InstallResult { const QByteArray prefixPathUtf8 = prefixPath.toUtf8(); const QByteArray protonNameUtf8 = protonName.toUtf8(); const QByteArray protonPathUtf8 = protonPath.toUtf8(); - qputenv("NAK_USE_STEAM_RUN", useSteamRun ? "1" : "0"); - // Set WINEPREFIX so NAK (and its child processes like winetricks) always // target the correct prefix during Proton init. qputenv("WINEPREFIX", prefixPathUtf8); @@ -629,7 +667,6 @@ void ProtonSettingsTab::startInstallTask(uint32_t appId, const QString& prefixPa } const auto restoreNakEnv = qScopeGuard([protonWineUtf8] { - qunsetenv("NAK_USE_STEAM_RUN"); qunsetenv("WINEPREFIX"); if (!protonWineUtf8.isEmpty()) { qunsetenv("WINE"); diff --git a/src/src/settingsdialogproton.h b/src/src/settingsdialogproton.h index 58c43f2..a1221c7 100644 --- a/src/src/settingsdialogproton.h +++ b/src/src/settingsdialogproton.h @@ -39,8 +39,7 @@ private: QString findProtonWine(const QString& protonPath); void startInstallTask(uint32_t appId, const QString& prefixPath, - const QString& protonName, const QString& protonPath, - bool useSteamRun); + const QString& protonName, const QString& protonPath); void enqueueStatus(const QString& message); void enqueueProgress(float progress); diff --git a/src/src/spawn.cpp b/src/src/spawn.cpp index 28010d0..4c91fbe 100644 --- a/src/src/spawn.cpp +++ b/src/src/spawn.cpp @@ -656,8 +656,6 @@ int spawn(const SpawnParameters &sp, pid_t &processId) { .setArguments(argList)
.setWorkingDir(cwd)
.setSteamAppId(parseSteamAppId(sp.steamAppID))
- .setUseSteamRun(
- QSettings().value("fluorine/use_steam_run", false).toBool())
.setSteamDrm(useSteamDrm)
.setStoreVariant(storeVariant);
diff --git a/src/src/vfs/mo2filesystem.cpp b/src/src/vfs/mo2filesystem.cpp index 962a458..42e90c0 100644 --- a/src/src/vfs/mo2filesystem.cpp +++ b/src/src/vfs/mo2filesystem.cpp @@ -18,9 +18,12 @@ namespace { namespace fs = std::filesystem; -constexpr double TTL_SECONDS = 30.0; -constexpr double NEGATIVE_TTL_SECONDS = 5.0; -constexpr double ATTR_CACHE_SECONDS = 30.0; +// Mod files are immutable during a game session, so cache aggressively. +// The VFS tree is built once at mount time and only mutated by our own +// create/rename/unlink handlers (which invalidate affected entries). +constexpr double TTL_SECONDS = 86400.0; // 24 hours +constexpr double NEGATIVE_TTL_SECONDS = 3600.0; // 1 hour — Wine probes many non-existent files +constexpr double ATTR_CACHE_SECONDS = 86400.0; void fillStatForDir(struct stat* st, fuse_ino_t ino, uid_t uid, gid_t gid); void fillStatForFile(struct stat* st, fuse_ino_t ino, uid_t uid, gid_t gid, @@ -513,11 +516,80 @@ void mo2_init(void* userdata, struct fuse_conn_info* conn) { auto* ctx = static_cast<Mo2FsContext*>(userdata); - // Keep kernel page cache valid across open/close as long as mtime/size are - // unchanged. Mod files are immutable during a game session, so this avoids - // re-reading file data from userspace on every open(). - if (conn->capable & FUSE_CAP_AUTO_INVAL_DATA) { - conn->want |= FUSE_CAP_AUTO_INVAL_DATA; + // ── Disable AUTO_INVAL_DATA (CRITICAL for performance) ── + // AUTO_INVAL_DATA forces a getattr() on EVERY read() to check mtime, + // completely bypassing attr_timeout. This alone causes ~4x throughput + // reduction. Our VFS tree is immutable during a session — we handle + // invalidation ourselves via fuse_lowlevel_notify_inval_inode() when + // files are created/renamed/deleted through our own handlers. + conn->want &= ~FUSE_CAP_AUTO_INVAL_DATA; + + // Let us control page cache invalidation explicitly. + if (conn->capable & FUSE_CAP_EXPLICIT_INVAL_DATA) { + conn->want |= FUSE_CAP_EXPLICIT_INVAL_DATA; + } + + // Force readdirplus always (unset adaptive mode). Our stat info is + // free (in-memory tree), so always return full entries to pre-populate + // the kernel dentry + attr caches on every directory listing. + if (conn->capable & FUSE_CAP_READDIRPLUS) { + conn->want |= FUSE_CAP_READDIRPLUS; + conn->want &= ~FUSE_CAP_READDIRPLUS_AUTO; + } + + // NOTE: FUSE_CAP_WRITEBACK_CACHE intentionally NOT enabled. + // It causes extra getattr calls for cache coherency, which hurts + // our read-heavy VFS more than the write buffering helps. + + // Cache symlink targets in the kernel page cache. + if (conn->capable & FUSE_CAP_CACHE_SYMLINKS) { + conn->want |= FUSE_CAP_CACHE_SYMLINKS; + } + + // Allow concurrent lookup()/readdir() on the same directory. + if (conn->capable & FUSE_CAP_PARALLEL_DIROPS) { + conn->want |= FUSE_CAP_PARALLEL_DIROPS; + } + + // Splice: reduce kernel↔userspace data copies for reads and writes. + if (conn->capable & FUSE_CAP_SPLICE_WRITE) { + conn->want |= FUSE_CAP_SPLICE_WRITE; + } + if (conn->capable & FUSE_CAP_SPLICE_MOVE) { + conn->want |= FUSE_CAP_SPLICE_MOVE; + } + if (conn->capable & FUSE_CAP_SPLICE_READ) { + conn->want |= FUSE_CAP_SPLICE_READ; + } + + // More async readahead/writeback slots (default is 12, far too low). + conn->max_background = 128; + conn->congestion_threshold = 96; + + // FUSE passthrough: if requested by the user and supported by the kernel, + // enable kernel-level passthrough for read-only file opens. This lets the + // kernel serve reads directly from the backing file without round-tripping + // through userspace — near-native I/O performance for game file reads. + // Requires: kernel 6.9+, libfuse 3.16+, CAP_SYS_ADMIN on the binary. + ctx->passthrough_active = false; + if constexpr (FUSE_CAP_PASSTHROUGH != 0) { + if (ctx->passthrough_requested && + (conn->capable & FUSE_CAP_PASSTHROUGH)) { + conn->want |= FUSE_CAP_PASSTHROUGH; + ctx->passthrough_active = true; + std::fprintf(stderr, "[VFS] FUSE passthrough enabled (kernel supports it)\n"); + } else if (ctx->passthrough_requested) { + std::fprintf(stderr, + "[VFS] FUSE passthrough requested but NOT supported by kernel " + "(capable=0x%x). Falling back to userspace I/O.\n", + conn->capable); + } + } else { + if (ctx->passthrough_requested) { + std::fprintf(stderr, + "[VFS] FUSE passthrough requested but NOT available at compile time " + "(libfuse too old). Falling back to userspace I/O.\n"); + } } // Increase max read/write buffer to 1MB (kernel 4.20+ supports this). @@ -531,9 +603,13 @@ void mo2_init(void* userdata, struct fuse_conn_info* conn) } std::fprintf(stderr, - "[VFS] init: auto_inval=%d max_readahead=%u max_write=%u\n", + "[VFS] init: auto_inval=%d explicit_inval=%d readdirplus=%d " + "passthrough=%d max_bg=%u max_readahead=%u\n", (conn->want & FUSE_CAP_AUTO_INVAL_DATA) ? 1 : 0, - conn->max_readahead, conn->max_write); + (conn->want & FUSE_CAP_EXPLICIT_INVAL_DATA) ? 1 : 0, + (conn->want & FUSE_CAP_READDIRPLUS) ? 1 : 0, + ctx->passthrough_active ? 1 : 0, + conn->max_background, conn->max_readahead); } void mo2_lookup(fuse_req_t req, fuse_ino_t parent, const char* name) @@ -685,7 +761,8 @@ void mo2_opendir(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) } fi->fh = dh; - fi->cache_readdir = 1; + fi->keep_cache = 1; // Don't invalidate cached readdir on reopen + fi->cache_readdir = 1; // Let kernel cache directory entries fuse_reply_open(req, fi); } @@ -964,6 +1041,32 @@ void mo2_open(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) fi->fh = fh; fi->keep_cache = 1; + + // FUSE passthrough: for read-only opens, let the kernel serve reads directly + // from the backing file. This bypasses userspace entirely — the kernel handles + // read() syscalls by reading the real file, giving near-native I/O performance. + // Only eligible for non-writable, non-COW files (pure reads). + if constexpr (FUSE_CAP_PASSTHROUGH != 0) { + if (ctx->passthrough_active && !writable && !cowPending && fd < 0) { + // Open the real file so the kernel can passthrough reads from it. + int ptFd = -1; + if (isBacking && ctx->backing_dir_fd >= 0) { + ptFd = openat(ctx->backing_dir_fd, realPath.c_str(), O_RDONLY); + } else { + ptFd = open(realPath.c_str(), O_RDONLY); + } + if (ptFd >= 0) { + fi->backing_id = static_cast<uint32_t>(ptFd); + // Store the fd so we can close it on release + std::scoped_lock lock(ctx->open_files_mutex); + auto it = ctx->open_files.find(fh); + if (it != ctx->open_files.end()) { + it->second.fd = ptFd; + } + } + } + } + fuse_reply_open(req, fi); } diff --git a/src/src/vfs/mo2filesystem.h b/src/src/vfs/mo2filesystem.h index 2c4b257..2883a18 100644 --- a/src/src/vfs/mo2filesystem.h +++ b/src/src/vfs/mo2filesystem.h @@ -16,6 +16,13 @@ #include <unordered_map> #include <vector> +// FUSE passthrough support (kernel 6.9+, libfuse 3.16+). +// When available, the kernel serves reads directly from the backing file +// without round-tripping through userspace — near-native I/O performance. +#ifndef FUSE_CAP_PASSTHROUGH +#define FUSE_CAP_PASSTHROUGH 0 +#endif + struct Mo2FsContext { std::shared_ptr<VfsTree> tree; @@ -90,6 +97,11 @@ struct Mo2FsContext uid_t uid = 0; gid_t gid = 0; + + // FUSE passthrough: when true AND kernel supports it, read-only file opens + // use kernel-level passthrough (zero-copy I/O bypassing userspace). + bool passthrough_requested = false; + bool passthrough_active = false; // set in mo2_init after capability check }; void mo2_init(void* userdata, struct fuse_conn_info* conn); diff --git a/src/src/vfs/trackedwrites.cpp b/src/src/vfs/trackedwrites.cpp index 66e83e5..4392db3 100644 --- a/src/src/vfs/trackedwrites.cpp +++ b/src/src/vfs/trackedwrites.cpp @@ -272,7 +272,8 @@ void TrackedWrites::detectManualMoves( if (missing.empty()) return; - // For each missing file, check if it now exists in any mod folder + // For each missing file, check if it now exists in any mod folder. + // Use the LAST match (highest priority — mods are ordered low→high). int detected = 0; for (const auto& relPath : missing) { const std::string key = toLower(relPath); @@ -280,16 +281,23 @@ void TrackedWrites::detectManualMoves( if (m_tracked.count(key)) continue; + std::string matchedMod; + std::string matchedPath; for (const auto& [modName, modPath] : mods) { std::error_code ec; if (fs::exists(fs::path(modPath) / relPath, ec)) { - m_tracked[key] = modPath; - ++detected; - std::fprintf(stderr, "[VFS] detected manual move: %s -> %s\n", - relPath.c_str(), modName.c_str()); - break; + matchedMod = modName; + matchedPath = modPath; + // Don't break — keep going to find the highest-priority match. } } + + if (!matchedPath.empty()) { + m_tracked[key] = matchedPath; + ++detected; + std::fprintf(stderr, "[VFS] detected manual move: %s -> %s\n", + relPath.c_str(), matchedMod.c_str()); + } } if (detected > 0) |
