Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions docs/architecture/dxvk-enabled-resolver-clean-runner.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# dxvk_enabled=false Contract + Runner Split (clean vs RTX Remix debug)

Date: 2026-08-16
Branch: `fix/dxvk-enabled-clean-runner`

## 1. The bug: `dxvk_enabled: false` was silently ignored

For any game launched through `steamflow-proton-11.0-purepe` with
`dxvk_enabled: false` (the default) and `d3d12_policy: Auto`, DXVK still ran.
Two independent mechanisms defeated the setting:

1. **WINEDLLOVERRIDES pairing leak** (`src/utils.rs::build_dll_overrides`):
`d3d12_policy Auto` + a runner that bundles VKD3D-Proton (purepe does) set
`effective_vkd3d_proton = true`, and the VKD3D-Proton pairing block pushed
`d3d8/d3d9/d3d10core/d3d11=n,b` for *every* game — regardless of
`dxvk_enabled`. Those native overrides hand the game the DXVK DLLs.

2. **Proton-script DXVK provisioning**: the purepe `proton` script installs
DXVK DLLs into the prefix `syswow64`/`system32` by default (its
`use_wined3d` flag only turns on when the compat set contains `wined3d`,
i.e. `PROTON_USE_WINED3D=1`). SteamFlow never set it, so even a game with
no DXVK override would find the provisioned DXVK d3d9.dll in the prefix.

**Observed impact**: Alan Wake (108710) crashed with `0xC0000005` on the
renderer thread (EIP in no loaded module) right after the intro cutscene —
running on DXVK master `0a70623de9c5c69` (debug-symbol build) despite
`dxvk_enabled: false`, instead of the WineD3D path the setting implies.
Alan Wake is known (Proton issue #156) to be sensitive to the DXVK/nvapi
stack: "preset other than very low → black screen/glitchy textures,
probably nvapi is a stub".

## 2. The fix (three coordinated changes)

### 2.1 `dll_provider_resolver.rs` — resolver honors `dxvk_enabled`

`resolve()` / `resolve_single()` / `get_custom_dll_path()` /
`get_runner_dll_path()` gained a `dxvk_enabled: bool` parameter (threaded
from `resolve_dll_providers.rs`, read from
`graphics_layers.dxvk_enabled`). When `false`:

- the runner's `*/dxvk/` subdirs are never candidates — the plain builtin
dirs (`files/lib/wine/i386-windows/…`, WineD3D) are the only runner paths;
- a custom DXVK path (`custom_dxvk_path`) is ignored;
- system DXVK paths (`/usr/lib/dxvk/…`) are not listed either, so a runner
without a builtin cannot fall back to system DXVK.

Unit tests: `test_dxvk_disabled_resolves_builtin_not_dxvk`,
`test_dxvk_disabled_ignores_custom_dxvk_path` (plus the pairing regression
test `test_build_dll_overrides_dxvk_disabled_no_d3d9_native_override` in
`utils.rs`).

### 2.2 `utils.rs::build_dll_overrides` — no native D3D pairing without DXVK

The VKD3D-Proton "pair D3D10/11 with native dxgi" loop now only pushes
`d3d8/d3d9/d3d10core/d3d11=n,b` when `dxvk_active`. `d3d12=n,b` +
`d3d12core=n,b` + `dxgi=n,b` remain (needed for D3D12 games regardless; with
WineD3D active the proton script installs the wined3d dxgi, so "native dxgi"
is the wined3d builtin — consistent, no null-import crash).

### 2.3 `wine_tkg.rs` — suppress DXVK provisioning via `wined3d` compat

When `!effective_dxvk && is_proton_game`, the compat set gets `wined3d`
inserted → `apply_proton_env_rules` emits `PROTON_USE_WINED3D=1` → the
proton script installs WineD3D builtins into the prefix **overwriting any
leftover DXVK DLLs** from an earlier launch. This is the same mechanism RE2
(883710) already used via `proton_compat_options: ["wined3d"]`.

## 3. Runner split: clean release vs RTX Remix debug

The debug-optimized runner (DXVK master `0a70623` debug-symbol build, used by
the RTX Remix mod chain) was renamed to
`steamflow-proton-11.0-purepe-rtx_remix_debug`; a clean release runner was
provisioned at the original name `steamflow-proton-11.0-purepe` (same
proton-11.0-1b wine, **DXVK 3.0.2 release** instead of master).

Registry updates:

- `config.json` `proton_version` → `steamflow-proton-11.0-purepe` (clean) —
the default for non-Remix games.
- `config.json` `game_configs` `forced_proton_version`:
- `108710` → clean (explicit)
- `620` (Portal 2), `317400` (Portal Stories: Mel), `6910` (Deus Ex) →
`steamflow-proton-11.0-purepe-rtx_remix_debug` (they ship RTX Remix
bridges in their game dirs: `.trex/NvRemixBridge*`).
- `user_apps.json`: `620/317400/6910` `dxvk_enabled: true` — their RTX Remix
runtime is DXVK-based, so the new `dxvk_enabled=false` → WineD3D contract
must not change their effective stack (their overrides stay identical to
before the fix). `108710` gets `proton_compat_options: ["wined3d"]`
(explicit, same pattern as RE2).

The `master_steam_prefix` registry font paths reference
`…/steamflow-proton-11.0-purepe/files/share/wine/fonts` — still valid, since
the clean runner sits at that exact path with the same layout.

## 4. Verification

- `cargo test --all-targets` (CARGO_PROFILE_DEV_DEBUG=0, disk-constrained) —
all green.
- Alan Wake (108710) `test-launch`: clean runner + `wined3d` path — no DXVK
log, no Remedy minidump, no access violation; game transitions past the
intro into the first level.
9 changes: 9 additions & 0 deletions src/infra/runners/wine_tkg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1352,6 +1352,15 @@ impl Runner for WineTkgRunner {
// by SteamFlow. See docs/architecture/valve-stack-replication.md.
{
let mut compat = crate::runner::proton_abi::default_compat_config(ctx.app.app_id);
// dxvk_enabled=false contract: DXVK must not be provisioned into the
// prefix. The proton script installs DXVK by default; the "wined3d"
// compat option (→ PROTON_USE_WINED3D=1) makes it install WineD3D
// builtins instead, overwriting any DXVK DLLs left in syswow64 by
// an earlier launch. Same mechanism RE2 (883710) uses via its
// per-game proton_compat_options: ["wined3d"].
if !effective_dxvk && is_proton_game {
compat.insert("wined3d".into());
}
if let Some(user_config) = &ctx.user_config {
for opt in &user_config.proton_compat_options {
compat.insert(opt.clone());
Expand Down
129 changes: 117 additions & 12 deletions src/launch/dll_provider_resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ impl DllProviderResolver {
custom_dxvk_path: Option<&Path>,
custom_vkd3d_path: Option<&Path>,
custom_vkd3d_proton_path: Option<&Path>,
dxvk_enabled: bool,
) -> (Vec<DllResolution>, ComponentScanReport) {
tracing::debug!("Resolving DLL providers. ExeDir: {}, Runner: {}", game_exe_dir.display(), runner_path.display());
let runner_root = crate::utils::derive_runner_root(runner_path);
Expand Down Expand Up @@ -137,6 +138,7 @@ impl DllProviderResolver {
custom_dxvk_path,
custom_vkd3d_path,
custom_vkd3d_proton_path,
dxvk_enabled,
))
.collect();

Expand Down Expand Up @@ -224,6 +226,7 @@ impl DllProviderResolver {
custom_dxvk_path: Option<&Path>,
custom_vkd3d_path: Option<&Path>,
custom_vkd3d_proton_path: Option<&Path>,
dxvk_enabled: bool,
) -> DllResolution {
let mut candidates = Vec::new();
let dll_filename = format!("{}.dll", dll_name);
Expand Down Expand Up @@ -251,6 +254,7 @@ impl DllProviderResolver {
custom_dxvk_path,
custom_vkd3d_path,
custom_vkd3d_proton_path,
dxvk_enabled,
) {
candidates.push(DllCandidate {
provider: DllProvider::Custom,
Expand All @@ -260,7 +264,14 @@ impl DllProviderResolver {
}

// 3. Runner Priority
if let Some(path) = self.get_runner_dll_path(dll_name, runner_path, runner_components, d3d12_policy, target_arch) {
if let Some(path) = self.get_runner_dll_path(
dll_name,
runner_path,
runner_components,
d3d12_policy,
target_arch,
dxvk_enabled,
) {
candidates.push(DllCandidate {
provider: DllProvider::Runner,
path: path.clone(),
Expand All @@ -269,12 +280,16 @@ impl DllProviderResolver {
}

// 3. System Priority
// For now, we use a simplified check for system paths
// For now, we use a simplified check for system paths.
// dxvk_enabled=false contract: system DXVK paths must not be listed
// either — otherwise a runner lacking a builtin would fall back to
// system DXVK, defeating the WineD3D selection.
let system_paths = match dll_name {
"d3d8" | "d3d9" | "d3d10core" | "d3d11" | "dxgi" => vec![
"d3d8" | "d3d9" | "d3d10core" | "d3d11" | "dxgi" if dxvk_enabled => vec![
"/usr/lib/dxvk/x64",
"/usr/lib/x86_64-linux-gnu/dxvk",
],
"d3d8" | "d3d9" | "d3d10core" | "d3d11" | "dxgi" => vec![],
"d3d12" | "d3d12core" | "libvkd3d-1" | "libvkd3d-shader-1" => vec![
"/usr/lib/vkd3d-proton/x64",
"/usr/lib/x86_64-linux-gnu/vkd3d-proton",
Expand Down Expand Up @@ -320,12 +335,19 @@ impl DllProviderResolver {
custom_dxvk_path: Option<&Path>,
custom_vkd3d_path: Option<&Path>,
custom_vkd3d_proton_path: Option<&Path>,
dxvk_enabled: bool,
) -> Option<PathBuf> {
let dll_filename = format!("{}.dll", dll_name);
let is_dxvk = matches!(dll_name, "d3d8" | "d3d9" | "d3d10core" | "d3d11" | "dxgi");
let is_vkd3d_proton = matches!(dll_name, "d3d12" | "d3d12core");
let is_vkd3d = matches!(dll_name, "libvkd3d-1" | "libvkd3d-shader-1");

// dxvk_enabled=false contract: a custom DXVK path must NOT be used —
// the user asked for WineD3D/builtin, not for a custom DXVK build.
if is_dxvk && !dxvk_enabled {
return None;
}

let custom_root = if is_dxvk {
custom_dxvk_path
} else if is_vkd3d_proton {
Expand Down Expand Up @@ -374,6 +396,7 @@ impl DllProviderResolver {
components: &crate::utils::RunnerComponents,
d3d12_policy: &crate::models::D3D12ProviderPolicy,
target_arch: &crate::models::ExecutableArchitecture,
dxvk_enabled: bool,
) -> Option<PathBuf> {
let runner_root = crate::utils::derive_runner_root(runner_path);

Expand Down Expand Up @@ -415,9 +438,16 @@ impl DllProviderResolver {
if is_dxvk && components.dxvk.is_some() {
let mut relative_paths = Vec::new();
for lib_subdir in crate::proton::COMPONENT_LIB_SUBDIRS {
relative_paths.push(format!("{}/dxvk", lib_subdir));
if dxvk_enabled {
relative_paths.push(format!("{}/dxvk", lib_subdir));
}
for (_, arch_dir) in crate::proton::ARCH_SUBDIRS {
relative_paths.push(format!("{}/dxvk/{}", lib_subdir, arch_dir));
if dxvk_enabled {
relative_paths.push(format!("{}/dxvk/{}", lib_subdir, arch_dir));
}
// Plain builtin dirs (WineD3D). When dxvk_enabled=false these
// are the ONLY runner paths that qualify: the DXVK subdirs
// must never be resolved or provisioned for such a game.
relative_paths.push(format!("{}/{}", lib_subdir, arch_dir));
}
}
Expand Down Expand Up @@ -574,7 +604,7 @@ mod tests {
let components = crate::utils::RunnerComponents::default();
let d3d12_policy = crate::models::D3D12ProviderPolicy::Auto;
let arch = crate::models::ExecutableArchitecture::X86_64;
let (resolutions, _) = resolver.resolve(&game_dir, runner_path, &components, &d3d12_policy, &arch, None, None, None);
let (resolutions, _) = resolver.resolve(&game_dir, runner_path, &components, &d3d12_policy, &arch, None, None, None, true);

let d3d9_res = resolutions.iter().find(|r| r.name == "d3d9").unwrap();
assert_eq!(d3d9_res.chosen_provider, DllProvider::GameLocal);
Expand All @@ -593,7 +623,7 @@ mod tests {
let components = crate::utils::RunnerComponents::default();
let d3d12_policy = crate::models::D3D12ProviderPolicy::Auto;
let arch = crate::models::ExecutableArchitecture::X86_64;
let (resolutions, _) = resolver.resolve(&game_dir, runner_path, &components, &d3d12_policy, &arch, None, None, None);
let (resolutions, _) = resolver.resolve(&game_dir, runner_path, &components, &d3d12_policy, &arch, None, None, None, true);

for res in resolutions {
if res.chosen_provider == DllProvider::System {
Expand Down Expand Up @@ -635,17 +665,17 @@ mod tests {
let arch = crate::models::ExecutableArchitecture::X86_64;

// Case 1: Auto (Prefer Proton)
let (res, _) = resolver.resolve(game_dir, &runner_root, &components, &crate::models::D3D12ProviderPolicy::Auto, &arch, None, None, None);
let (res, _) = resolver.resolve(game_dir, &runner_root, &components, &crate::models::D3D12ProviderPolicy::Auto, &arch, None, None, None, true);
let d3d12 = res.iter().find(|r| r.name == "d3d12").unwrap();
assert_eq!(d3d12.chosen_path.as_ref().unwrap(), &proton_dll);

// Case 2: Explicit Wine
let (res, _) = resolver.resolve(game_dir, &runner_root, &components, &crate::models::D3D12ProviderPolicy::Vkd3dWine, &arch, None, None, None);
let (res, _) = resolver.resolve(game_dir, &runner_root, &components, &crate::models::D3D12ProviderPolicy::Vkd3dWine, &arch, None, None, None, true);
let d3d12 = res.iter().find(|r| r.name == "d3d12").unwrap();
assert_eq!(d3d12.chosen_path.as_ref().unwrap(), &wine_dll);

// Case 3: Explicit Proton
let (res, _) = resolver.resolve(game_dir, &runner_root, &components, &crate::models::D3D12ProviderPolicy::Vkd3dProton, &arch, None, None, None);
let (res, _) = resolver.resolve(game_dir, &runner_root, &components, &crate::models::D3D12ProviderPolicy::Vkd3dProton, &arch, None, None, None, true);
let d3d12 = res.iter().find(|r| r.name == "d3d12").unwrap();
assert_eq!(d3d12.chosen_path.as_ref().unwrap(), &proton_dll);
}
Expand All @@ -661,7 +691,7 @@ mod tests {
let resolver = DllProviderResolver::new();
let tmp = tempdir().unwrap();
let arch = crate::models::ExecutableArchitecture::X86_64;
let (res, _) = resolver.resolve(tmp.path(), tmp.path(), &crate::utils::RunnerComponents::default(), &crate::models::D3D12ProviderPolicy::Auto, &arch, None, None, None);
let (res, _) = resolver.resolve(tmp.path(), tmp.path(), &crate::utils::RunnerComponents::default(), &crate::models::D3D12ProviderPolicy::Auto, &arch, None, None, None, true);
let d3d11 = res.iter().find(|r| r.name == "d3d11").unwrap();
assert_eq!(d3d11.chosen_provider, DllProvider::None);
assert!(d3d11.fallback_reason.is_some());
Expand Down Expand Up @@ -693,7 +723,7 @@ mod tests {
let game_dir = Path::new("/tmp/game");
let arch = crate::models::ExecutableArchitecture::X86_64;

let (res, report) = resolver.resolve(game_dir, &runner_root, &components, &crate::models::D3D12ProviderPolicy::Auto, &arch, None, None, None);
let (res, report) = resolver.resolve(game_dir, &runner_root, &components, &crate::models::D3D12ProviderPolicy::Auto, &arch, None, None, None, true);

let d3d11_res = res.iter().find(|r| r.name == "d3d11").unwrap();
assert_eq!(d3d11_res.chosen_provider, DllProvider::Runner);
Expand All @@ -706,4 +736,79 @@ mod tests {
// Check if report scan roots include the new unified path
assert!(report.scan_roots.iter().any(|p| p.to_string_lossy().contains("files/lib/wine/x86_64-windows")));
}

#[test]
fn test_dxvk_disabled_resolves_builtin_not_dxvk() {
// Classic layout: runner ships BOTH the DXVK build (dxvk/ subdir) and
// the plain WineD3D builtin. dxvk_enabled=false must resolve to the
// builtin; dxvk_enabled=true must resolve to the DXVK build.
let tmp = tempdir().unwrap();
let runner_root = tmp.path().to_path_buf();
let dxvk_dir = runner_root.join("files/lib/wine/dxvk/i386-windows");
let builtin_dir = runner_root.join("files/lib/wine/i386-windows");
fs::create_dir_all(&dxvk_dir).unwrap();
fs::create_dir_all(&builtin_dir).unwrap();
let dxvk_dll = dxvk_dir.join("d3d9.dll");
let builtin_dll = builtin_dir.join("d3d9.dll");
fs::write(&dxvk_dll, "dxvk build").unwrap();
fs::write(&builtin_dll, "wined3d builtin").unwrap();

let mut components = crate::utils::RunnerComponents::default();
components.dxvk = Some(crate::utils::ComponentInfo {
version: "2.3".into(),
source: crate::utils::ComponentSource::BundledWithRunner,
path: None,
});

let resolver = DllProviderResolver::new();
let game_dir = Path::new("/tmp/game");
let arch = crate::models::ExecutableArchitecture::X86;
let policy = crate::models::D3D12ProviderPolicy::Auto;

// DXVK disabled → builtin WineD3D DLL, never the dxvk/ subdir.
let (res, _) = resolver.resolve(game_dir, &runner_root, &components, &policy, &arch, None, None, None, false);
let d3d9 = res.iter().find(|r| r.name == "d3d9").unwrap();
assert_eq!(d3d9.chosen_provider, DllProvider::Runner);
let chosen = d3d9.chosen_path.as_ref().unwrap().to_string_lossy().to_string();
assert!(chosen.ends_with("files/lib/wine/i386-windows/d3d9.dll"), "expected builtin path, got {chosen}");
assert!(!chosen.contains("/dxvk/"), "dxvk_enabled=false must not resolve the dxvk subdir: {chosen}");
assert!(d3d9.candidates.iter().all(|c| !c.path.to_string_lossy().contains("/dxvk/")),
"dxvk_enabled=false must not even list dxvk candidates");

// DXVK enabled → DXVK build wins (regression guard).
let (res, _) = resolver.resolve(game_dir, &runner_root, &components, &policy, &arch, None, None, None, true);
let d3d9 = res.iter().find(|r| r.name == "d3d9").unwrap();
let chosen = d3d9.chosen_path.as_ref().unwrap().to_string_lossy().to_string();
assert!(chosen.contains("/dxvk/"), "dxvk_enabled=true should resolve the dxvk subdir, got {chosen}");
}

#[test]
fn test_dxvk_disabled_ignores_custom_dxvk_path() {
let tmp = tempdir().unwrap();
// get_custom_dll_path expects arch subdirs directly under the custom
// dxvk root (i386-windows / x86_64-windows / x64 / x32 / "").
let custom = tmp.path().join("i386-windows");
fs::create_dir_all(&custom).unwrap();
let custom_dll = custom.join("d3d9.dll");
fs::write(&custom_dll, "custom dxvk").unwrap();

let resolver = DllProviderResolver::new();
let game_dir = Path::new("/tmp/game");
let runner_root = Path::new("/tmp/fake_runner");
let arch = crate::models::ExecutableArchitecture::X86;
let policy = crate::models::D3D12ProviderPolicy::Auto;
let components = crate::utils::RunnerComponents::default();

// dxvk_enabled=false: the custom DXVK path must be ignored entirely.
let (res, _) = resolver.resolve(game_dir, runner_root, &components, &policy, &arch, Some(tmp.path()), None, None, false);
let d3d9 = res.iter().find(|r| r.name == "d3d9").unwrap();
assert_ne!(d3d9.chosen_provider, DllProvider::Custom, "custom dxvk path must be ignored when dxvk_enabled=false");
assert!(d3d9.candidates.iter().all(|c| c.provider != DllProvider::Custom));

// dxvk_enabled=true: custom path is honored as before.
let (res, _) = resolver.resolve(game_dir, runner_root, &components, &policy, &arch, Some(tmp.path()), None, None, true);
let d3d9 = res.iter().find(|r| r.name == "d3d9").unwrap();
assert_eq!(d3d9.chosen_provider, DllProvider::Custom);
assert_eq!(d3d9.chosen_path.as_ref().unwrap(), &custom_dll);
}
}
4 changes: 4 additions & 0 deletions src/launch/stages/resolve_dll_providers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ impl PipelineStage for ResolveDllProvidersStage {
custom_dxvk,
custom_vkd3d,
custom_vkd3d_proton,
ctx.user_config
.as_ref()
.map(|c| c.graphics_layers.dxvk_enabled)
.unwrap_or(false),
);

if !nvapi_enabled {
Expand Down
Loading