Skip to content

Commit 1c2f825

Browse files
authored
Merge pull request #34 from weter11/feature/symlink-deployment-mode-4874256893497921374
Symlink Deployment Mode and Custom Components
2 parents 8ab8f6a + 2751db0 commit 1c2f825

13 files changed

Lines changed: 944 additions & 193 deletions

src/infra/logging/tests.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,10 @@ mod tests {
130130
vkd3d_enabled: false,
131131
graphics_backend_policy: crate::models::GraphicsBackendPolicy::WineD3D,
132132
d3d12_policy: crate::models::D3D12ProviderPolicy::Auto,
133+
use_symlinks_in_prefix: false,
134+
custom_dxvk_path: None,
135+
custom_vkd3d_path: None,
136+
custom_vkd3d_proton_path: None,
133137
};
134138

135139
let warnings = check_environment_sanity(&env, "Wine", Some(&config));

src/infra/logging/wine_capture.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ pub fn classify_graphics_evidence(log_line: &str) -> Option<String> {
22
let line_lower = log_line.to_lowercase();
33

44
// DXVK signatures
5-
if line_lower.contains("dxvk: v") {
5+
if line_lower.contains("dxvk: v") ||
6+
line_lower.contains("info: game:") ||
7+
line_lower.contains("d3d11internalcreatedevice") ||
8+
line_lower.contains("presenter: actual swapchain properties") ||
9+
(line_lower.contains("vulkan:") && line_lower.contains("found vkgetinstanceprocaddr")) {
610
return Some(format!("DXVK Detected: {}", log_line.trim()));
711
}
812

src/infra/runners/wine_tkg.rs

Lines changed: 94 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,21 @@ impl Runner for WineTkgRunner {
1818
.map(|c| c.steam_prefix_mode.clone())
1919
.unwrap_or(ctx.launcher_config.steam_prefix_mode.clone());
2020

21-
let compat_data_path = library_root
22-
.join("steamapps")
23-
.join("compatdata")
24-
.join(ctx.app.app_id.to_string());
25-
let target_prefix_path = compat_data_path.join("pfx");
26-
std::fs::create_dir_all(&target_prefix_path)
27-
.map_err(|e| LaunchError::new(LaunchErrorKind::Permission, format!("failed creating {}", target_prefix_path.display())).with_source(anyhow!(e)))?;
21+
let effective_game_prefix = crate::utils::steam_wineprefix_for_game(
22+
&ctx.launcher_config,
23+
ctx.app.app_id,
24+
&ctx.user_config.as_ref().map(|c| {
25+
let mut store = HashMap::new();
26+
store.insert(ctx.app.app_id, c.clone());
27+
store
28+
}).unwrap_or_default().into()
29+
);
30+
std::fs::create_dir_all(&effective_game_prefix)
31+
.map_err(|e| LaunchError::new(LaunchErrorKind::Permission, format!("failed creating {}", effective_game_prefix.display())).with_source(anyhow!(e)))?;
2832

29-
let mut steam_wineprefix = target_prefix_path.clone();
33+
tracing::info!("Effective game prefix: {}", effective_game_prefix.display());
34+
tracing::info!("Shared steam compatibility data enabled: {}", ctx.launcher_config.use_shared_compat_data);
35+
tracing::info!("Steam Runtime Prefix Mode: {:?}", steam_prefix_mode);
3036

3137
if use_steam_runtime {
3238
let base_config = crate::config::config_dir().map_err(|e| LaunchError::new(LaunchErrorKind::Environment, "failed to get config dir").with_source(e))?;
@@ -46,51 +52,74 @@ impl Runner for WineTkgRunner {
4652
).with_context("master_prefix", master_prefix.to_string_lossy()));
4753
}
4854
Some(master_steam_dir) => {
49-
let master_wineprefix_original = crate::utils::resolve_master_wineprefix();
50-
51-
let prefix_steam_dir = match steam_prefix_mode {
55+
let (prefix_steam_dir, steam_wineprefix) = match steam_prefix_mode {
5256
crate::models::SteamPrefixMode::Shared => {
53-
steam_wineprefix = master_wineprefix_original.clone();
54-
master_steam_dir.clone()
57+
(master_steam_dir.clone(), crate::utils::resolve_master_wineprefix())
5558
}
5659
crate::models::SteamPrefixMode::PerGame => {
57-
let target_steam_dir = target_prefix_path
60+
let target_steam_dir = effective_game_prefix
5861
.join("drive_c/Program Files (x86)/Steam");
5962

6063
tracing::info!(
61-
"Linking/Cloning {} → {}",
62-
master_steam_dir.display(),
64+
"Deploying required Steam runtime files to {}",
6365
target_steam_dir.display()
6466
);
65-
let _ = std::fs::create_dir_all(target_steam_dir.parent().unwrap());
66-
#[cfg(unix)]
67-
{
68-
if !target_steam_dir.exists() {
69-
if let Err(e) =
70-
std::os::unix::fs::symlink(&master_steam_dir, &target_steam_dir)
67+
let _ = std::fs::create_dir_all(&target_steam_dir);
68+
69+
let required_files = [
70+
"steam.exe",
71+
"steamclient.dll",
72+
"steamclient64.dll",
73+
"tier0_s.dll",
74+
"tier0_s64.dll",
75+
"vstdlib_s.dll",
76+
"vstdlib_s64.dll",
77+
];
78+
79+
for file in required_files {
80+
let src = master_steam_dir.join(file);
81+
let dst = target_steam_dir.join(file);
82+
if src.exists() && !dst.exists() {
83+
#[cfg(unix)]
84+
{
85+
if let Err(e) = std::os::unix::fs::symlink(&src, &dst) {
86+
tracing::warn!("Symlink failed for {}, falling back to copy: {}", file, e);
87+
let _ = std::fs::copy(&src, &dst);
88+
}
89+
}
90+
#[cfg(not(unix))]
7191
{
72-
tracing::warn!("Symlink failed, falling back to copy: {}", e);
73-
let _ = crate::utils::copy_dir_all(
74-
&master_steam_dir,
75-
&target_steam_dir,
76-
);
92+
let _ = std::fs::copy(&src, &dst);
7793
}
7894
}
7995
}
80-
#[cfg(not(unix))]
81-
{
82-
let _ = crate::utils::copy_dir_all(
83-
&master_steam_dir,
84-
&target_steam_dir,
85-
);
96+
97+
// Also symlink required subdirectories
98+
let required_dirs = ["bin", "public"];
99+
for dir in required_dirs {
100+
let src = master_steam_dir.join(dir);
101+
let dst = target_steam_dir.join(dir);
102+
if src.exists() && !dst.exists() {
103+
#[cfg(unix)]
104+
{
105+
if let Err(e) = std::os::unix::fs::symlink(&src, &dst) {
106+
tracing::warn!("Symlink failed for {}, falling back to copy: {}", dir, e);
107+
let _ = crate::utils::copy_dir_all(&src, &dst);
108+
}
109+
}
110+
#[cfg(not(unix))]
111+
{
112+
let _ = crate::utils::copy_dir_all(&src, &dst);
113+
}
114+
}
86115
}
87-
target_steam_dir
116+
117+
(target_steam_dir, effective_game_prefix.clone())
88118
}
89119
};
90120

91-
println!("--- STEAM LAUNCH DEBUG ---");
92-
println!("Prefix Steam dir : {}", prefix_steam_dir.display());
93-
println!("Steam WINEPREFIX : {}", steam_wineprefix.display());
121+
tracing::debug!("Runtime Steam dir : {}", prefix_steam_dir.display());
122+
tracing::debug!("Runtime WINEPREFIX : {}", steam_wineprefix.display());
94123

95124
SteamClient::write_headless_steam_cfg(&prefix_steam_dir);
96125

@@ -254,25 +283,24 @@ impl Runner for WineTkgRunner {
254283
let app_id_str = ctx.app.app_id.to_string();
255284

256285
let library_root = PathBuf::from(&ctx.launcher_config.steam_library_path);
257-
let steam_prefix_mode = ctx.user_config.as_ref()
258-
.map(|c| c.steam_prefix_mode.clone())
259-
.unwrap_or(ctx.launcher_config.steam_prefix_mode.clone());
260-
let use_steam_runtime = ctx.user_config.as_ref().map(|c| c.use_steam_runtime).unwrap_or(false);
261-
262286
let compat_data_path = library_root
263287
.join("steamapps")
264288
.join("compatdata")
265289
.join(&app_id_str);
266-
let target_prefix_path = compat_data_path.join("pfx");
267290

268-
let mut game_wineprefix = target_prefix_path.clone();
269-
if use_steam_runtime && matches!(steam_prefix_mode, crate::models::SteamPrefixMode::Shared) {
270-
game_wineprefix = crate::utils::resolve_master_wineprefix();
271-
}
291+
let effective_game_prefix = crate::utils::steam_wineprefix_for_game(
292+
&ctx.launcher_config,
293+
ctx.app.app_id,
294+
&ctx.user_config.as_ref().map(|c| {
295+
let mut store = HashMap::new();
296+
store.insert(ctx.app.app_id, c.clone());
297+
store
298+
}).unwrap_or_default().into()
299+
);
272300

273301
env.insert("SteamAppId".to_string(), app_id_str.clone());
274302
env.insert("SteamGameId".to_string(), app_id_str);
275-
env.insert("WINEPREFIX".to_string(), game_wineprefix.to_string_lossy().to_string());
303+
env.insert("WINEPREFIX".to_string(), effective_game_prefix.to_string_lossy().to_string());
276304
env.insert("STEAM_COMPAT_DATA_PATH".to_string(), compat_data_path.to_string_lossy().to_string());
277305

278306
let glc = ctx.user_config.as_ref()
@@ -310,7 +338,7 @@ impl Runner for WineTkgRunner {
310338

311339
let _components = crate::utils::detect_runner_components(
312340
&crate::utils::resolve_runner(proton, &library_root),
313-
Some(&game_wineprefix),
341+
Some(&effective_game_prefix),
314342
);
315343

316344
// 1. Resolve DX8-11 policy (GraphicsBackendPolicy) - CONSERVATIVE
@@ -339,6 +367,13 @@ impl Runner for WineTkgRunner {
339367
let effective_vkd3d_proton = glc.vkd3d_proton_enabled || policy_vkd3dp;
340368
let effective_vkd3d = glc.vkd3d_enabled || policy_vkd3dw;
341369

370+
// NVAPI Support
371+
let nvapi_active = _components.nvapi.is_some();
372+
if nvapi_active {
373+
tracing::info!("NVAPI component detected, will be exposed to game");
374+
}
375+
376+
let use_symlinks = glc.use_symlinks_in_prefix;
342377
let mut dll_overrides = crate::utils::build_dll_overrides(
343378
effective_dxvk,
344379
effective_vkd3d_proton,
@@ -351,8 +386,10 @@ impl Runner for WineTkgRunner {
351386

352387
// Enhance overrides with resolved DLL providers
353388
for res in &ctx.dll_resolutions {
354-
if let crate::launch::dll_provider_resolver::DllProvider::GameLocal = res.chosen_provider {
355-
// Ensure native wins for game-local DLLs
389+
if res.chosen_provider == crate::launch::dll_provider_resolver::DllProvider::GameLocal ||
390+
(res.chosen_provider == crate::launch::dll_provider_resolver::DllProvider::Custom && !use_symlinks) ||
391+
(res.chosen_provider == crate::launch::dll_provider_resolver::DllProvider::Runner && res.name.contains("nvapi")) {
392+
// Ensure native wins for game-local or non-symlinked custom DLLs
356393
if !dll_overrides.contains(&format!("{}=n", res.name)) {
357394
dll_overrides.push_str(&format!(";{}=n", res.name));
358395
}
@@ -370,15 +407,19 @@ impl Runner for WineTkgRunner {
370407
// WITHOUT THIS, d3d12=n,b finds whatever is in the prefix's system32 instead.
371408
// CONSERVATIVE: only include paths for DLLs that are actually requested to be native.
372409
let mut wine_dll_dirs: Vec<String> = Vec::new();
410+
let use_symlinks = glc.use_symlinks_in_prefix;
373411

374412
for res in &ctx.dll_resolutions {
375-
if let crate::launch::dll_provider_resolver::DllProvider::Runner = res.chosen_provider {
413+
if (res.chosen_provider == crate::launch::dll_provider_resolver::DllProvider::Runner ||
414+
res.chosen_provider == crate::launch::dll_provider_resolver::DllProvider::Custom) && !use_symlinks
415+
{
376416
// Check if this DLL is actually selected for use by the current policy/overrides
377417
let name = res.name.to_lowercase();
378418
let is_dxvk_dll = matches!(name.as_str(), "d3d8" | "d3d9" | "d3d10" | "d3d10_1" | "d3d10core" | "d3d11" | "dxgi");
379419
let is_d3d12_dll = matches!(name.as_str(), "d3d12" | "d3d12core" | "libvkd3d-1" | "libvkd3d-shader-1");
380420

381-
let selected = (is_dxvk_dll && effective_dxvk) || (is_d3d12_dll && (effective_vkd3d_proton || effective_vkd3d));
421+
let is_nvapi_dll = matches!(name.as_str(), "nvapi" | "nvapi64" | "nvofapi64");
422+
let selected = (is_dxvk_dll && effective_dxvk) || (is_d3d12_dll && (effective_vkd3d_proton || effective_vkd3d)) || is_nvapi_dll;
382423

383424
if !selected {
384425
continue;

0 commit comments

Comments
 (0)