From 32ccc6a470212f062dc7e043af75c8db5e0ac7e4 Mon Sep 17 00:00:00 2001 From: Samuel Stegall Date: Sat, 4 Jul 2026 01:12:13 -0500 Subject: [PATCH 1/5] fix: build Wayland-compatible login webview on Linux The login WebView subprocess built its wry WebView via WebViewBuilder::new(window), which uses the raw window handle. wry's WebKitGTK backend only supports that under X11, so under a Wayland session it failed with "the window handle kind is not supported" and Launch reported "Login failed: building wry webview: ...". Build from the tao window's GTK vbox via WebViewBuilderExtUnix::new_gtk on Linux instead; it embeds WebKitGTK at the widget level and works under both X11 and Wayland (as wry's own docs recommend). Windows keeps the raw-handle path; macOS is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/login/webview.rs | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/login/webview.rs b/src/login/webview.rs index 369a65f..c8bf815 100644 --- a/src/login/webview.rs +++ b/src/login/webview.rs @@ -31,8 +31,12 @@ use anyhow::{Context, Result}; use tao::dpi::LogicalSize; use tao::event::{Event, WindowEvent}; use tao::event_loop::{ControlFlow, EventLoop}; +#[cfg(target_os = "linux")] +use tao::platform::unix::WindowExtUnix; use tao::window::WindowBuilder; use wry::WebViewBuilder; +#[cfg(target_os = "linux")] +use wry::WebViewBuilderExtUnix; use crate::crypto::SESSION_ID_LEN; use crate::version::APP_NAME; @@ -76,7 +80,24 @@ pub fn run_webview(login_url: &str) -> Result<()> { Ok(()) } -#[cfg(any(target_os = "linux", target_os = "windows"))] +#[cfg(target_os = "linux")] +fn build_webview(window: &tao::window::Window, login_url: &str) -> Result { + // `WebViewBuilder::new(window)` builds from the raw window handle, which wry + // only supports under X11 — under Wayland it fails with "the window handle + // kind is not supported". Building from the tao window's GTK vbox embeds the + // WebKitGTK view at the widget level, which works under both X11 and Wayland + // (wry's own docs recommend `new_gtk` for Wayland support). + let vbox = window + .default_vbox() + .context("tao login window is missing its default GTK vbox")?; + WebViewBuilder::new_gtk(vbox) + .with_url(login_url) + .with_navigation_handler(navigation_handler) + .build() + .context("building wry webview") +} + +#[cfg(target_os = "windows")] fn build_webview(window: &tao::window::Window, login_url: &str) -> Result { WebViewBuilder::new(window) .with_url(login_url) From bb6d1277e3e68be969ec1de55386513a70f686cd Mon Sep 17 00:00:00 2001 From: Samuel Stegall Date: Sat, 4 Jul 2026 01:14:36 -0500 Subject: [PATCH 2/5] Added configure ubuntu script. --- scripts/configure-ubuntu.sh | 206 ++++++++++++++++++++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100755 scripts/configure-ubuntu.sh diff --git a/scripts/configure-ubuntu.sh b/scripts/configure-ubuntu.sh new file mode 100755 index 0000000..ab1bafc --- /dev/null +++ b/scripts/configure-ubuntu.sh @@ -0,0 +1,206 @@ +#!/usr/bin/env bash +# Install the system dependencies Garlemald Client needs to build and run on +# Ubuntu / Debian, skipping anything already present. +# +# It installs the same native dev libraries CI uses (see docs/dev-environment.md): +# the GTK3 / WebKit2GTK-4.1 stack the login WebView (wry/tao) links against, the +# GL / X11 / Wayland libraries the eframe/egui window needs, plus the build +# toolchain (build-essential, pkg-config). Wine and the Rust toolchain are only +# *checked* and reported — not force-installed — so this script won't clobber a +# WineHQ install or a rustup toolchain you already manage. +# +# Usage: +# scripts/configure-ubuntu.sh [--dry-run] [--install-wine] [-h|--help] +# +# -n, --dry-run List what's missing and exit without installing anything. +# --install-wine Also apt-install the distro `wine` package when no `wine` +# is on PATH (skipped when Wine is already present, e.g. +# from WineHQ). +# +# Uses sudo automatically when not run as root. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +DRY_RUN=0 +INSTALL_WINE=0 + +say() { printf '==> %s\n' "$*"; } +info() { printf ' %s\n' "$*"; } +warn() { printf 'warning: %s\n' "$*" >&2; } +die() { printf 'error: %s\n' "$*" >&2; exit 1; } + +usage() { + cat <<'EOF' +Install the system dependencies Garlemald Client needs to build and run on +Ubuntu / Debian, skipping anything already present. + +Usage: + scripts/configure-ubuntu.sh [--dry-run] [--install-wine] [-h|--help] + + -n, --dry-run List what's missing and exit without installing anything. + --install-wine Also apt-install the distro `wine` package when no `wine` + is on PATH (skipped when Wine is already present). + +Uses sudo automatically when not run as root. +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + -n|--dry-run) DRY_RUN=1; shift ;; + --install-wine) INSTALL_WINE=1; shift ;; + -h|--help) usage; exit 0 ;; + *) die "unknown flag: $1 (see --help)" ;; + esac +done + +# --- environment guards ----------------------------------------------------- + +if ! command -v apt-get >/dev/null 2>&1; then + die "this script targets Debian/Ubuntu (needs apt-get). On another distro, + install the equivalents of the packages in docs/dev-environment.md." +fi + +# Build the sudo prefix once: empty when we're already root, `sudo` otherwise. +SUDO="" +if [[ "$(id -u)" -ne 0 ]]; then + if command -v sudo >/dev/null 2>&1; then + SUDO="sudo" + else + die "not running as root and sudo is unavailable; re-run as root." + fi +fi + +# --- the dependency set (mirrors docs/dev-environment.md / CI) -------------- + +REQUIRED_PKGS=( + # Build toolchain + build-essential + pkg-config + # Login WebView (wry/tao): WebKit2GTK 4.1 ABI (the libsoup3 generation) + libwebkit2gtk-4.1-dev + libjavascriptcoregtk-4.1-dev + libsoup-3.0-dev + # GTK3 — also provides glib-2.0 / gobject-2.0 / gio-2.0 / gdk / pango / cairo / atk + libgtk-3-dev + # Native dialogs (rfd), tray/appindicator, and SVG icon rendering + libxdo-dev + libayatana-appindicator3-dev + librsvg2-dev + # eframe glow + winit backends: OpenGL, X11, and Wayland + libgl1-mesa-dev + libxkbcommon-dev + libwayland-dev + libxcursor-dev + libxrandr-dev + libxi-dev +) + +# Is the package installed and configured? +pkg_installed() { + dpkg-query -W -f='${Status}' "$1" 2>/dev/null | grep -q 'install ok installed' +} + +# Does apt have an installable candidate for it? (Guards against a package name +# that drifted between Ubuntu releases so one bad name can't fail the whole run.) +pkg_available() { + local cand + cand="$(apt-cache policy "$1" 2>/dev/null | awk -F': ' '/Candidate:/{print $2; exit}')" + [[ -n "$cand" && "$cand" != "(none)" ]] +} + +say "Checking ${#REQUIRED_PKGS[@]} required packages" + +missing=() +unavailable=() +for p in "${REQUIRED_PKGS[@]}"; do + if pkg_installed "$p"; then + continue + fi + if pkg_available "$p"; then + missing+=("$p") + else + unavailable+=("$p") + fi +done + +if [[ ${#unavailable[@]} -gt 0 ]]; then + warn "no apt candidate for: ${unavailable[*]}" + warn "the package names may differ on this Ubuntu release; install the" + warn "equivalents by hand (see docs/dev-environment.md)." +fi + +# --- install (or, with --dry-run, just report) ------------------------------ + +if [[ ${#missing[@]} -eq 0 ]]; then + say "All build dependencies already present — nothing to install." +else + say "Missing (${#missing[@]}): ${missing[*]}" + if [[ $DRY_RUN -eq 1 ]]; then + info "(dry run) would run: ${SUDO:+$SUDO }apt-get install -y ${missing[*]}" + else + say "Updating package lists" + $SUDO apt-get update + say "Installing missing packages" + $SUDO apt-get install -y "${missing[@]}" + say "Installed: ${missing[*]}" + fi +fi + +# --- Wine (runtime for the game itself) ------------------------------------- + +# Detect the `wine` *command*, not a distro package: WineHQ installs (winehq-stable) +# provide `wine` without the distro `wine`/`wine64` packages, and force-installing +# the distro package on top of WineHQ would conflict. +if command -v wine >/dev/null 2>&1; then + say "Wine present: $(wine --version 2>/dev/null || echo 'version unknown')" +elif [[ $INSTALL_WINE -eq 1 ]]; then + if [[ $DRY_RUN -eq 1 ]]; then + info "(dry run) would run: ${SUDO:+$SUDO }apt-get install -y wine" + else + say "Installing distro wine (--install-wine)" + $SUDO apt-get install -y wine + fi +else + warn "no 'wine' on PATH — the launcher builds and runs, but launching the" + warn "game needs Wine 7+. Install the distro package with --install-wine, or" + warn "WineHQ stable (recommended, newer): https://wiki.winehq.org/Ubuntu" +fi + +# --- Rust toolchain (advisory only) ----------------------------------------- + +if command -v cargo >/dev/null 2>&1; then + say "Rust present: $(cargo --version 2>/dev/null || echo 'version unknown')" +else + warn "no 'cargo' on PATH — install rustup, then the pinned toolchain from" + warn "rust-toolchain.toml installs automatically on first build:" + warn " curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh" +fi + +# --- verify pkg-config can now see the key libraries ------------------------ + +say "Verifying pkg-config can resolve the core libraries" +verify_ok=1 +for mod in glib-2.0 gtk+-3.0 webkit2gtk-4.1; do + if pkg-config --exists "$mod" 2>/dev/null; then + info "ok $mod ($(pkg-config --modversion "$mod" 2>/dev/null))" + else + info "MISS $mod" + verify_ok=0 + fi +done + +echo "" +if [[ $verify_ok -eq 1 ]]; then + say "Done. Build and run with: cargo run --release" +else + if [[ $DRY_RUN -eq 1 ]]; then + say "Dry run complete — re-run without --dry-run to install the above." + else + warn "some libraries still don't resolve; see the MISS lines above." + exit 1 + fi +fi From 1255c6cc0f2e2fac70a61c1f4d4d37d385fdb048 Mon Sep 17 00:00:00 2001 From: Samuel Stegall Date: Sat, 4 Jul 2026 02:04:02 -0500 Subject: [PATCH 3/5] perf: drop +seh from default WINEDEBUG to stop in-world log flood FFXIV 1.0 fires a storm of longjmp unwinds (RtlUnwindEx code=STATUS_LONGJUMP) once in-world. The +seh channel in the default WINEDEBUG traced a full CPU register dump per unwind into wine.log, pinning the CPU on trace I/O and dropping the game to a slideshow (GPU near-idle). err+all still surfaces genuine crashes as err:seh; full seh tracing stays available via the verbose developer toggle (VERBOSE_WINE_DEBUG). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/platform/wine.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/platform/wine.rs b/src/platform/wine.rs index b61f1fc..dcb0c27 100644 --- a/src/platform/wine.rs +++ b/src/platform/wine.rs @@ -47,10 +47,8 @@ use crate::config; /// `-fixme` / `+err` treat those as *channel* names (which don't exist), so /// the obvious-looking `-fixme,+err` is a silent no-op. Correct form: /// * `fixme-all` — silence fixme for every channel -/// * `err+all` — keep err class enabled (it's on by default, but explicit -/// makes our intent clear) -/// * `+seh` — all classes for the seh channel, so crashes and unhandled -/// exceptions still surface +/// * `err+all` — keep the err class on for every channel. This also +/// surfaces genuine crashes / unhandled exceptions as `err:seh`. /// * `+debugstr` — log every `OutputDebugStringA/W` call. Pairs with the /// `assert_log_patch` PE patch, which redirects the /// game's silent assert handler into `OutputDebugStringA` @@ -62,9 +60,19 @@ use crate::config; /// WARN class is suppressed by default; the cinematic /// crash trace lives here. /// +/// `+seh` is deliberately NOT in this default. It enables every class +/// (including `trace`) on the seh channel, so Wine writes a full CPU register +/// dump on every stack unwind — and FFXIV 1.0 fires a storm of `longjmp` +/// unwinds (`RtlUnwindEx code=STATUS_LONGJUMP`) once in-world. With Wine's +/// stdout/stderr redirected to `wine.log`, that floods the disk and drops the +/// game to a slideshow (GPU near-idle, CPU pinned in the trace path). Genuine +/// crashes still surface via `err+all` (`err:seh`); full seh tracing is +/// available on demand through the "verbose Wine debug logging" developer +/// toggle (see `VERBOSE_WINE_DEBUG` in `app/developer_window.rs`). +/// /// Callers that want more verbosity (e.g. `+relay,+module,+loaddll`) can set /// `WINEDEBUG` in the environment; we only fill this in as a default. -const WINEDEBUG_DEFAULT: &str = "fixme-all,err+all,+seh,+debugstr,warn+d3d"; +const WINEDEBUG_DEFAULT: &str = "fixme-all,err+all,+debugstr,warn+d3d"; /// Relative path inside the prefix to the FFXIV install root, matching the /// default the InstallShield installer uses. From 6c4ccb8c4a29da2a56b41a3a5fc185cd71236b41 Mon Sep 17 00:00:00 2001 From: Samuel Stegall Date: Sat, 4 Jul 2026 02:04:23 -0500 Subject: [PATCH 4/5] feat: auto-provision DXVK on Linux for accelerated Direct3D 9 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wine's builtin wined3d translates the 32-bit FFXIV 1.0 client's D3D9 on a single CPU thread, starving the GPU (slideshow in-world, GPU near-idle). Add a Linux-only DXVK provisioner (src/platform/dxvk.rs): after prefix init, detect Vulkan, download a pinned DXVK release into the runtime cache, install its 32-bit d3d9/dxgi into syswow64, and merge d3d9=n,b;dxgi=n,b into WINEDLLOVERRIDES at launch. Best-effort: no Vulkan / offline / GARLEMALD_DISABLE_DXVK=1 falls back to wined3d, so the launch never breaks. Also logs the resolved wine binary + version. macOS is untouched — its CrossOver engine already provides the accelerated D3D path. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/platform/dxvk.rs | 186 ++++++++++++++++++++++++++++++++++++++++++ src/platform/linux.rs | 27 +++++- src/platform/macos.rs | 3 + src/platform/mod.rs | 3 + src/platform/wine.rs | 44 +++++----- 5 files changed, 243 insertions(+), 20 deletions(-) create mode 100644 src/platform/dxvk.rs diff --git a/src/platform/dxvk.rs b/src/platform/dxvk.rs new file mode 100644 index 0000000..eb0cce6 --- /dev/null +++ b/src/platform/dxvk.rs @@ -0,0 +1,186 @@ +// garlemald-client — cross-platform launcher for FINAL FANTASY XIV 1.x private servers +// Copyright (C) 2026 Samuel Stegall +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +//! Linux DXVK provisioning. +//! +//! FFXIV 1.0 is a 32-bit Direct3D 9 game. Wine's builtin `wined3d` translates +//! D3D9 → OpenGL on a single thread; it is CPU-bound and starves the GPU on +//! this title (single-digit framerate in-world, GPU near-idle). DXVK translates +//! D3D9 → Vulkan with far lower CPU overhead — the same accelerated path the +//! macOS CrossOver engine already provides, which is why macOS runs it well. +//! +//! On first launch we download a pinned DXVK release into the runtime cache, +//! drop its 32-bit `d3d9.dll` / `dxgi.dll` into the prefix's `syswow64`, and +//! return the `WINEDLLOVERRIDES` fragment that makes Wine load them natively. +//! The `,b` builtin fallback means a DXVK/Vulkan failure degrades to wined3d +//! rather than breaking the launch. Everything here is best-effort: any error +//! (no Vulkan, offline, disabled) leaves the game on wined3d, logged. +//! +//! macOS is intentionally not handled here — its CrossOver engine ships its own +//! accelerated D3D path, and forcing upstream DXVK into that prefix can conflict +//! with CrossOver's patched Wine + bundled MoltenVK. + +use std::path::{Path, PathBuf}; +use std::process::Command; + +use anyhow::{Context, Result, anyhow}; + +use crate::platform::wine::WineRuntime; + +/// Pinned DXVK release. Bump together with [`DXVK_URL`]. +const DXVK_VERSION: &str = "3.0"; +const DXVK_URL: &str = "https://github.com/doitsujin/dxvk/releases/download/v3.0/dxvk-3.0.tar.gz"; + +/// 32-bit DXVK DLLs we install (FFXIV 1.0 is a 32-bit D3D9 client, so it loads +/// from the prefix's `syswow64`). +const DXVK_DLLS: &[&str] = &["d3d9.dll", "dxgi.dll"]; + +/// `WINEDLLOVERRIDES` fragment: prefer the native (DXVK) DLLs, fall back to the +/// builtin (wined3d) if DXVK can't initialize. +const DXVK_OVERRIDES: &str = "d3d9=n,b;dxgi=n,b"; + +/// Set `GARLEMALD_DISABLE_DXVK=1` to force Wine's builtin wined3d path. +const DISABLE_ENV: &str = "GARLEMALD_DISABLE_DXVK"; + +/// Best-effort: make DXVK available in `runtime`'s prefix and return the +/// `WINEDLLOVERRIDES` fragment to apply at launch, or `None` to stay on wined3d +/// (DXVK disabled, no Vulkan, or a setup error — all logged). +/// +/// `cache_root` is where the downloaded DXVK release is unpacked and reused +/// across launches. The prefix must already be initialized (its `syswow64` +/// present). +pub fn ensure_dxvk(runtime: &WineRuntime, cache_root: &Path) -> Option { + if std::env::var_os(DISABLE_ENV).is_some() { + log::info!("{DISABLE_ENV} is set — using builtin wined3d"); + return None; + } + if !vulkan_available() { + log::warn!( + "no Vulkan ICD found under /usr/share/vulkan or /etc/vulkan — DXVK unavailable, using wined3d" + ); + return None; + } + match install_dxvk(runtime, cache_root) { + Ok(()) => { + log::info!("DXVK {DXVK_VERSION} active (Direct3D 9 → Vulkan)"); + Some(DXVK_OVERRIDES.to_string()) + } + Err(e) => { + log::warn!("DXVK setup failed ({e:#}); falling back to wined3d"); + None + } + } +} + +/// A present Vulkan ICD manifest is the most reliable signal that a usable +/// Vulkan driver is installed. (The `,b` override fallback covers a driver that +/// is listed but non-functional.) +fn vulkan_available() -> bool { + ["/usr/share/vulkan/icd.d", "/etc/vulkan/icd.d"] + .iter() + .filter_map(|d| std::fs::read_dir(d).ok()) + .flatten() + .filter_map(|e| e.ok()) + .any(|e| e.path().extension().is_some_and(|ext| ext == "json")) +} + +/// Copy the pinned DXVK 32-bit DLLs into the prefix's `syswow64`, downloading +/// and unpacking the release into `cache_root` on first use. Idempotent via a +/// `.dxvk-version` marker written into the prefix. +fn install_dxvk(runtime: &WineRuntime, cache_root: &Path) -> Result<()> { + let syswow64 = runtime.prefix.join("drive_c/windows/syswow64"); + if !syswow64.is_dir() { + return Err(anyhow!( + "prefix syswow64 missing at {} (prefix not initialized, or a 32-bit prefix?)", + syswow64.display() + )); + } + let marker = runtime.prefix.join(".dxvk-version"); + if std::fs::read_to_string(&marker) + .ok() + .as_deref() + .map(str::trim) + == Some(DXVK_VERSION) + { + return Ok(()); // already installed + } + + let dxvk_dir = ensure_downloaded(cache_root)?; + for dll in DXVK_DLLS { + let src = dxvk_dir.join("x32").join(dll); + let dst = syswow64.join(dll); + std::fs::copy(&src, &dst) + .with_context(|| format!("copying {} -> {}", src.display(), dst.display()))?; + } + std::fs::write(&marker, DXVK_VERSION) + .with_context(|| format!("writing DXVK marker {}", marker.display()))?; + log::info!("installed DXVK {DXVK_VERSION} into {}", syswow64.display()); + Ok(()) +} + +/// Ensure `cache_root/dxvk-/x32/d3d9.dll` exists, downloading and +/// unpacking the release tarball if needed. Returns the `dxvk-` dir. +fn ensure_downloaded(cache_root: &Path) -> Result { + let dxvk_dir = cache_root.join(format!("dxvk-{DXVK_VERSION}")); + if dxvk_dir.join("x32").join("d3d9.dll").exists() { + return Ok(dxvk_dir); + } + std::fs::create_dir_all(cache_root) + .with_context(|| format!("creating DXVK cache dir {}", cache_root.display()))?; + let tmp = tempfile::tempdir().context("creating tmp dir for DXVK archive")?; + let archive = tmp.path().join("dxvk.tar.gz"); + log::info!("downloading DXVK {DXVK_VERSION} ({DXVK_URL})"); + download_to(DXVK_URL, &archive)?; + extract_tar_gz(&archive, cache_root)?; + if !dxvk_dir.join("x32").join("d3d9.dll").exists() { + return Err(anyhow!( + "DXVK archive did not contain the expected {}/x32/d3d9.dll", + dxvk_dir.display() + )); + } + Ok(dxvk_dir) +} + +fn download_to(url: &str, dst: &Path) -> Result<()> { + let response = ureq::get(url) + .call() + .with_context(|| format!("GET {url}"))?; + let mut reader = response.into_reader(); + let mut out = + std::fs::File::create(dst).with_context(|| format!("creating {}", dst.display()))?; + std::io::copy(&mut reader, &mut out).context("streaming DXVK download to disk")?; + Ok(()) +} + +fn extract_tar_gz(archive: &Path, dst: &Path) -> Result<()> { + let status = Command::new("tar") + .arg("-xzf") + .arg(archive) + .arg("-C") + .arg(dst) + .status() + .context("running tar -xzf for the DXVK archive")?; + if !status.success() { + return Err(anyhow!( + "tar -xzf {} -> {} failed with {status:?}", + archive.display(), + dst.display() + )); + } + Ok(()) +} diff --git a/src/platform/linux.rs b/src/platform/linux.rs index d8d13b7..4088c93 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -19,7 +19,7 @@ //! Linux platform backend. Relies on a system-installed `wine` plus a //! user-managed prefix under `$XDG_DATA_HOME/garlemald-client/prefix`. -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use anyhow::{Context, Result, anyhow}; @@ -30,6 +30,7 @@ use crate::launcher::{ lobby_host_patch, null_member8_write_nop_patch, null_this_guard_patch, }; use crate::platform::Platform; +use crate::platform::dxvk; use crate::platform::wine::{ WineRuntime, copy_exe_for_patching, ensure_prefix_initialized, launch_ffxiv_game, monotonic_ms_since_boot, @@ -79,8 +80,19 @@ impl Platform for LinuxPlatform { fn launch_game(&self, request: &GameLaunchRequest) -> Result<()> { let runtime = Self::runtime_paths()?; + log::info!( + "using wine: {} ({})", + runtime.wine_bin.display(), + wine_version(&runtime.wine_bin).unwrap_or_else(|| "version unknown".into()), + ); ensure_prefix_initialized(&runtime)?; + // Provision DXVK (Direct3D 9 → Vulkan) into the managed prefix. This is + // best-effort: on no-Vulkan / offline / disabled it returns None and the + // game runs on Wine's builtin wined3d. The returned fragment is merged + // into WINEDLLOVERRIDES by launch_ffxiv_game. + let dxvk_overrides = dxvk::ensure_dxvk(&runtime, &config::data_dir()?.join("runtime")); + let tick = monotonic_ms_since_boot(); let launch_args = crypto::build_launch_arguments(&request.session_id, tick)?; @@ -103,11 +115,24 @@ impl Platform for LinuxPlatform { &launch_args.encoded_argument, request.wine_debug_override.as_deref(), request.enable_winsock_proxy, + dxvk_overrides.as_deref(), )?; Ok(()) } } +/// Best-effort `wine --version` for logging which runtime we resolved. +fn wine_version(wine_bin: &Path) -> Option { + let out = std::process::Command::new(wine_bin) + .arg("--version") + .output() + .ok()?; + if !out.status.success() { + return None; + } + Some(String::from_utf8_lossy(&out.stdout).trim().to_string()) +} + fn which_wine() -> Result { let out = std::process::Command::new("sh") .arg("-c") diff --git a/src/platform/macos.rs b/src/platform/macos.rs index c34e567..daf4b87 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -175,6 +175,9 @@ impl Platform for MacosPlatform { &launch_args.encoded_argument, request.wine_debug_override.as_deref(), request.enable_winsock_proxy, + // DXVK auto-provisioning is Linux-only; the CrossOver engine + // provides the accelerated D3D path on macOS. + None, )?; Ok(()) } diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 6609040..348a48d 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -60,3 +60,6 @@ pub fn current() -> ActivePlatform { #[cfg(any(target_os = "macos", target_os = "linux"))] mod wine; + +#[cfg(target_os = "linux")] +mod dxvk; diff --git a/src/platform/wine.rs b/src/platform/wine.rs index dcb0c27..cde8bca 100644 --- a/src/platform/wine.rs +++ b/src/platform/wine.rs @@ -183,6 +183,7 @@ pub fn launch_ffxiv_game( encoded_argument: &str, wine_debug_override: Option<&str>, enable_winsock_proxy: bool, + extra_dll_overrides: Option<&str>, ) -> Result<()> { // Resolve / tear down the ws2_32 DLL-hijack proxy before we fork // the game process. The proxy logs every `send`/`recv`/etc. call to @@ -193,18 +194,13 @@ pub fn launch_ffxiv_game( apply_winsock_proxy(runtime, game_dir, enable_winsock_proxy) .unwrap_or_else(|e| log::warn!("winsock proxy deploy skipped: {e:#}")); } - // Wine classifies `ws2_32.dll` as a builtin and loads it from its - // own WINEDLLPATH bundle regardless of what sits next to the exe. - // `WINEDLLOVERRIDES=ws2_32=n,b` tells the loader to try the native - // (file-based) copy first, then fall back to the builtin on miss. - // The comma separates the preferred order; `n` alone would reject - // the builtin entirely and break the unhooked fallback when the - // proxy isn't deployed, so we pair it with `b`. - let winsock_override = if enable_winsock_proxy { - Some("ws2_32=n,b".to_string()) - } else { - None - }; + // Wine classifies `ws2_32.dll` as a builtin and loads it from its own + // WINEDLLPATH bundle regardless of what sits next to the exe. `ws2_32=n,b` + // tells the loader to try the native (file-based) copy first, then fall + // back to the builtin on miss. The comma separates the preferred order; `n` + // alone would reject the builtin entirely and break the unhooked fallback + // when the proxy isn't deployed, so we pair it with `b`. It is assembled + // into WINEDLLOVERRIDES alongside any DXVK overrides just before launch. let log_path = wine_log_path()?; let log_file = OpenOptions::new() .create(true) @@ -239,13 +235,23 @@ pub fn launch_ffxiv_game( cmd.current_dir(cwd); } runtime.configure_command_with_debug(&mut cmd, wine_debug_override); - if let Some(ovr) = &winsock_override { - // Setting the env var on the specific Command — not the parent - // process — keeps the override scoped to this launch. Wine - // reads WINEDLLOVERRIDES on process start so this happens - // before any DLL resolution inside the game. - cmd.env("WINEDLLOVERRIDES", ovr); - log::info!("WINEDLLOVERRIDES={}", ovr); + // Assemble WINEDLLOVERRIDES for this launch: DXVK's native D3D DLLs (Linux, + // `d3d9=n,b;dxgi=n,b`) plus, when the winsock proxy is on, `ws2_32=n,b` (see + // the note above). Entries are `;`-joined. Setting it on this specific + // Command — not the parent process — keeps the override scoped to this + // launch; Wine reads WINEDLLOVERRIDES on process start, before any DLL + // resolution inside the game. + let mut dll_overrides: Vec<&str> = Vec::new(); + if let Some(dxvk) = extra_dll_overrides { + dll_overrides.push(dxvk); + } + if enable_winsock_proxy { + dll_overrides.push("ws2_32=n,b"); + } + if !dll_overrides.is_empty() { + let joined = dll_overrides.join(";"); + log::info!("WINEDLLOVERRIDES={joined}"); + cmd.env("WINEDLLOVERRIDES", joined); } log::info!( From 596f41f4515e543a8847be1fecb230087171f768 Mon Sep 17 00:00:00 2001 From: Samuel Stegall Date: Sat, 4 Jul 2026 23:50:24 -0500 Subject: [PATCH 5/5] feat: GARLEMALD_WINE env override for the system wine lookup on Linux Lets Ubuntu testing point the launcher at a specific Wine build (an older/patched engine or a self-managed one) without touching PATH. A set-but-invalid path fails loudly instead of falling through to the PATH lookup, and the no-wine error now mentions the override. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01EchKYpba7cMDD5B6g86Z2Q --- src/platform/linux.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 4088c93..c109b04 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -134,6 +134,18 @@ fn wine_version(wine_bin: &Path) -> Option { } fn which_wine() -> Result { + // Explicit override wins — point Garlemald at a specific Wine build (to test + // an older/patched engine, or a self-managed one) without touching PATH. + if let Some(p) = std::env::var_os("GARLEMALD_WINE") { + let path = PathBuf::from(&p); + if path.is_file() { + return Ok(path); + } + return Err(anyhow!( + "GARLEMALD_WINE is set but {} is not a file", + path.display() + )); + } let out = std::process::Command::new("sh") .arg("-c") .arg("command -v wine") @@ -141,7 +153,8 @@ fn which_wine() -> Result { .context("locating wine via `command -v`")?; if !out.status.success() { return Err(anyhow!( - "no `wine` binary in PATH — install Wine 7+ via your distro package manager" + "no `wine` binary in PATH — install Wine 7+ via your distro package manager, \ + or set GARLEMALD_WINE to a wine binary" )); } let text = String::from_utf8_lossy(&out.stdout).trim().to_string();