From 07ac26ba1ca08f430bd5faae89b98d5b2704c5de Mon Sep 17 00:00:00 2001 From: SulfurNitride Date: Thu, 28 May 2026 01:59:16 -0500 Subject: Add FUSE io_uring toggle and update libfuse --- docker/Dockerfile | 25 +++++- src/src/settingsdialog.ui | 20 +++-- src/src/settingsdialogproton.cpp | 179 ++++++++++++++++++++++++++++++++++++++- src/src/settingsdialogproton.h | 2 + 4 files changed, 219 insertions(+), 7 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 161706e..c721021 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -10,7 +10,7 @@ ENV PATH="/opt/rust/cargo/bin:${PATH}" # Python comes from uv (python-build-standalone), not apt. RUN apt-get update && apt-get install -y --no-install-recommends \ # Build essentials - build-essential cmake ninja-build ccache \ + build-essential cmake meson ninja-build ccache \ git curl wget file binutils patchelf pkg-config ca-certificates \ # Library dependencies libboost-all-dev \ @@ -19,6 +19,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ libfontconfig1-dev \ libspdlog-dev \ libfuse3-dev fuse3 libcap-dev \ + liburing-dev libnuma-dev \ liblz4-dev zlib1g-dev libzstd-dev libbz2-dev liblzma-dev \ libssl-dev libcurl4-openssl-dev \ libtomlplusplus-dev \ @@ -39,6 +40,28 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ clang-tidy \ && rm -rf /var/lib/apt/lists/* +# Ubuntu 25.10 currently carries libfuse 3.17.x, which cannot mount with +# the stable FUSE-over-io_uring option. Build the pinned upstream release +# so Fluorine compiles and bundles matching 3.18.x headers/runtime. +ARG LIBFUSE_VERSION=3.18.2 +RUN curl -L --fail \ + "https://github.com/libfuse/libfuse/releases/download/fuse-${LIBFUSE_VERSION}/fuse-${LIBFUSE_VERSION}.tar.gz" \ + -o /tmp/libfuse.tar.gz && \ + tar -xf /tmp/libfuse.tar.gz -C /tmp && \ + meson setup /tmp/libfuse-build "/tmp/fuse-${LIBFUSE_VERSION}" \ + --prefix=/usr \ + --libdir=lib/x86_64-linux-gnu \ + -Dexamples=false \ + -Dutils=true \ + -Dtests=false \ + -Duseroot=false \ + -Denable-io-uring=true && \ + meson compile -C /tmp/libfuse-build && \ + meson install -C /tmp/libfuse-build && \ + ldconfig && \ + test "$(pkg-config --modversion fuse3)" = "${LIBFUSE_VERSION}" && \ + rm -rf /tmp/libfuse.tar.gz "/tmp/fuse-${LIBFUSE_VERSION}" /tmp/libfuse-build + # ── uv (Astral) ── # Single static binary; manages Python interpreters and packages. COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv diff --git a/src/src/settingsdialog.ui b/src/src/settingsdialog.ui index a7e2a90..50c47e0 100644 --- a/src/src/settingsdialog.ui +++ b/src/src/settingsdialog.ui @@ -1832,28 +1832,38 @@ If you disable this feature, MO will only display official DLCs this way. Please - + + + + Enable the kernel FUSE io_uring transport for new VFS mounts. Requires administrator authentication and a VFS remount; Fluorine falls back automatically when unsupported. + + + Enable FUSE io_uring + + + + Status: - + No Prefix - + 0 - + Show Install Log @@ -1866,7 +1876,7 @@ If you disable this feature, MO will only display official DLCs this way. Please - + true diff --git a/src/src/settingsdialogproton.cpp b/src/src/settingsdialogproton.cpp index 896bcb9..fdb8cbd 100644 --- a/src/src/settingsdialogproton.cpp +++ b/src/src/settingsdialogproton.cpp @@ -11,6 +11,7 @@ #include "steamdetection.h" #include "slrmanager.h" #include +#include #include #include #include @@ -26,15 +27,120 @@ #include #include #include +#include #include #include #include #include +#include #include namespace { std::atomic g_activeInstallTab = nullptr; + +constexpr const char* kFuseIoUringParameter = + "/sys/module/fuse/parameters/enable_uring"; + +enum class FuseIoUringState +{ + Unsupported, + Disabled, + Enabled, + Unknown +}; + +FuseIoUringState readFuseIoUringState() +{ + QFile parameter(QString::fromUtf8(kFuseIoUringParameter)); + if (!parameter.exists()) { + return FuseIoUringState::Unsupported; + } + + if (!parameter.open(QIODevice::ReadOnly | QIODevice::Text)) { + return FuseIoUringState::Unknown; + } + + const QByteArray value = parameter.readAll().trimmed().toUpper(); + if (value == "Y" || value == "1") { + return FuseIoUringState::Enabled; + } + if (value == "N" || value == "0") { + return FuseIoUringState::Disabled; + } + + return FuseIoUringState::Unknown; +} + +QString findPrivilegeHelper(QStringList* arguments) +{ + const QString command = + QStringLiteral("printf Y > %1").arg(QString::fromUtf8(kFuseIoUringParameter)); + + const QString pkexec = QStandardPaths::findExecutable(QStringLiteral("pkexec")); + if (!pkexec.isEmpty()) { + *arguments = {QStringLiteral("/bin/sh"), QStringLiteral("-c"), command}; + return pkexec; + } + + const QString sudo = QStandardPaths::findExecutable(QStringLiteral("sudo")); + if (!sudo.isEmpty()) { + const QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + if (!env.value(QStringLiteral("SUDO_ASKPASS")).isEmpty()) { + *arguments = {QStringLiteral("-A"), QStringLiteral("/bin/sh"), + QStringLiteral("-c"), command}; + return sudo; + } + + const QFileInfo stdinInfo(QStringLiteral("/proc/self/fd/0")); + const QString stdinTarget = stdinInfo.symLinkTarget(); + if (stdinInfo.exists() && + (stdinTarget.startsWith(QStringLiteral("/dev/pts/")) || + stdinTarget == QStringLiteral("/dev/tty"))) { + *arguments = {QStringLiteral("/bin/sh"), QStringLiteral("-c"), command}; + return sudo; + } + } + + return {}; +} + +QString enableFuseIoUring() +{ + QStringList arguments; + const QString helper = findPrivilegeHelper(&arguments); + if (helper.isEmpty()) { + return QObject::tr("Install pkexec/polkit, or configure sudo askpass, then try " + "again."); + } + + QProcess process; + process.start(helper, arguments); + if (!process.waitForStarted(5000)) { + return QObject::tr("Failed to start %1.").arg(helper); + } + + if (!process.waitForFinished(120000)) { + process.kill(); + process.waitForFinished(3000); + return QObject::tr("Timed out waiting for administrator authentication."); + } + + if (process.exitStatus() != QProcess::NormalExit || process.exitCode() != 0) { + const QString stderrText = QString::fromLocal8Bit(process.readAllStandardError()) + .trimmed(); + if (!stderrText.isEmpty()) { + return stderrText; + } + return QObject::tr("Administrator authentication was cancelled or failed."); + } + + if (readFuseIoUringState() != FuseIoUringState::Enabled) { + return QObject::tr("The kernel did not accept the FUSE io_uring setting."); + } + + return {}; +} } ProtonSettingsTab::ProtonSettingsTab(Settings& s, SettingsDialog& d) @@ -97,6 +203,8 @@ ProtonSettingsTab::ProtonSettingsTab(Settings& s, SettingsDialog& d) &ProtonSettingsTab::onBrowsePrefixLocation); QObject::connect(ui->downloadSLRButton, &QPushButton::clicked, this, &ProtonSettingsTab::onDownloadSLR); + QObject::connect(ui->fuseIoUringCheckBox, &QCheckBox::clicked, this, + &ProtonSettingsTab::onFuseIoUringToggled); QObject::connect(&m_installWatcher, &QFutureWatcher::finished, this, &ProtonSettingsTab::onInstallFinished); @@ -183,6 +291,8 @@ void ProtonSettingsTab::refreshState() ui->openPrefixFolderButton->setEnabled(!m_busy && active); ui->winetricksButton->setEnabled(!m_busy && active); ui->protonVersionCombo->setEnabled(!m_busy); + + refreshFuseIoUringState(); } void ProtonSettingsTab::setBusy(bool busy) @@ -332,6 +442,74 @@ void ProtonSettingsTab::onDownloadSLR() progress->show(); } +void ProtonSettingsTab::refreshFuseIoUringState() +{ + QSignalBlocker blocker(ui->fuseIoUringCheckBox); + Q_UNUSED(blocker); + + const FuseIoUringState state = readFuseIoUringState(); + ui->fuseIoUringCheckBox->setVisible(true); + + switch (state) { + case FuseIoUringState::Enabled: + ui->fuseIoUringCheckBox->setChecked(true); + ui->fuseIoUringCheckBox->setEnabled(false); + ui->fuseIoUringCheckBox->setToolTip( + tr("FUSE io_uring is enabled in the kernel. It applies to new VFS " + "mounts after relaunch/remount.")); + break; + case FuseIoUringState::Disabled: + ui->fuseIoUringCheckBox->setChecked(false); + ui->fuseIoUringCheckBox->setEnabled(!m_busy); + ui->fuseIoUringCheckBox->setToolTip( + tr("Enable the kernel FUSE io_uring transport for new VFS mounts. " + "Requires administrator authentication and a VFS remount; Fluorine " + "falls back automatically when unsupported.")); + break; + case FuseIoUringState::Unsupported: + ui->fuseIoUringCheckBox->setChecked(false); + ui->fuseIoUringCheckBox->setEnabled(false); + ui->fuseIoUringCheckBox->setToolTip( + tr("This kernel does not expose %1. FUSE io_uring requires kernel " + "support for CONFIG_FUSE_IO_URING.") + .arg(QString::fromUtf8(kFuseIoUringParameter))); + break; + case FuseIoUringState::Unknown: + ui->fuseIoUringCheckBox->setChecked(false); + ui->fuseIoUringCheckBox->setEnabled(!m_busy); + ui->fuseIoUringCheckBox->setToolTip( + tr("Fluorine could not read the kernel FUSE io_uring setting. Click " + "to try enabling it with administrator authentication.")); + break; + } +} + +void ProtonSettingsTab::onFuseIoUringToggled(bool checked) +{ + if (!checked) { + refreshFuseIoUringState(); + return; + } + + MOBase::log::info("[VFS] user requested enabling FUSE io_uring via settings UI"); + + const QString error = enableFuseIoUring(); + if (!error.isEmpty()) { + MOBase::log::warn("[VFS] failed to enable FUSE io_uring: {}", error); + QMessageBox::warning(parentWidget(), tr("FUSE io_uring"), + tr("Could not enable FUSE io_uring:\n%1").arg(error)); + refreshFuseIoUringState(); + return; + } + + MOBase::log::info("[VFS] kernel FUSE io_uring parameter is now enabled"); + QMessageBox::information( + parentWidget(), tr("FUSE io_uring"), + tr("FUSE io_uring is enabled. Restart the game/VFS mount before testing; " + "existing mounts keep their current transport.")); + refreshFuseIoUringState(); +} + void ProtonSettingsTab::onBrowsePrefixLocation() { const QString dir = QFileDialog::getExistingDirectory( @@ -560,4 +738,3 @@ void ProtonSettingsTab::onInstallFinished() ui->protonStatusLabel->setText(tr("Done")); refreshState(); } - diff --git a/src/src/settingsdialogproton.h b/src/src/settingsdialogproton.h index 5eee3bd..321e5a2 100644 --- a/src/src/settingsdialogproton.h +++ b/src/src/settingsdialogproton.h @@ -33,9 +33,11 @@ private: void onWinetricks(); void onBrowsePrefixLocation(); void onDownloadSLR(); + void onFuseIoUringToggled(bool checked); static QString ensureWinetricks(); static QString findProtonWine(const QString& protonPath); + void refreshFuseIoUringState(); void runPrefixSetupDialog(uint32_t appId, const QString& prefixPath, const QString& protonName, const QString& protonPath); -- cgit v1.3.1