From 1105dd6331e37018b0c47005c32da2ffcb9f43f2 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 18:03:18 +0000 Subject: [PATCH 01/11] Investigate Batman early startup failure independently of graphics backend selection - Enhance `LaunchVerification` with Windows user and path diagnostics - Implement dynamic Windows username detection in `check_prefix_health` - Add verification for Steam client and PhysX middleware in the prefix - Capture first 50 and last 100 lines of Wine log for early exit analysis - Expand `classify_graphics_evidence` to detect middleware and bootstrap failures - Include `STEAM_COMPAT_APP_ID` in launch environment - Update concise launch summary with new diagnostic fields Co-authored-by: weter11 <14630689+weter11@users.noreply.github.com> --- src/infra/logging/session.rs | 6 +++ src/infra/logging/wine_capture.rs | 13 +++++++ src/infra/runners/wine_tkg.rs | 3 +- src/launch/pipeline.rs | 63 ++++++++++++++++++++++++++++--- 4 files changed, 79 insertions(+), 6 deletions(-) diff --git a/src/infra/logging/session.rs b/src/infra/logging/session.rs index 68d01a2..cdf0b40 100644 --- a/src/infra/logging/session.rs +++ b/src/infra/logging/session.rs @@ -50,6 +50,12 @@ pub struct LaunchVerification { pub process_lifetime_ms: Option, pub exit_code: Option, pub log_growth_observed: bool, + pub windows_username: Option, + pub windows_user_path: Option, + pub key_paths_detected: HashMap, + pub steam_client_exposed: bool, + pub log_head: Vec, + pub log_tail: Vec, } #[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)] diff --git a/src/infra/logging/wine_capture.rs b/src/infra/logging/wine_capture.rs index 87e9a11..c6c1ad8 100644 --- a/src/infra/logging/wine_capture.rs +++ b/src/infra/logging/wine_capture.rs @@ -32,11 +32,24 @@ pub fn classify_graphics_evidence(log_line: &str) -> Option { if line_lower.contains("winemac.drv") { return None; } + + // Specific middleware/dependency failure patterns + if line_lower.contains("physx") { + return Some(format!("PhysX/Middleware failure: {}", log_line.trim())); + } + return Some(format!("DLL Load Failure: {}", log_line.trim())); } if line_lower.contains("not found") && line_lower.contains("which is needed by") { return Some(format!("DLL Dependency Missing: {}", log_line.trim())); } + // Steam/Client patterns + if line_lower.contains("failed to create steam.exe") || + line_lower.contains("cannot find 'steam.exe'") || + (line_lower.contains("steam.exe") && (line_lower.contains("not found") || line_lower.contains("failed"))) { + return Some(format!("Steam Client/Environment Failure: {}", log_line.trim())); + } + None } diff --git a/src/infra/runners/wine_tkg.rs b/src/infra/runners/wine_tkg.rs index 2e7a931..668204a 100644 --- a/src/infra/runners/wine_tkg.rs +++ b/src/infra/runners/wine_tkg.rs @@ -299,7 +299,8 @@ impl Runner for WineTkgRunner { ); env.insert("SteamAppId".to_string(), app_id_str.clone()); - env.insert("SteamGameId".to_string(), app_id_str); + env.insert("SteamGameId".to_string(), app_id_str.clone()); + env.insert("STEAM_COMPAT_APP_ID".to_string(), app_id_str); env.insert("WINEPREFIX".to_string(), effective_game_prefix.to_string_lossy().to_string()); env.insert("STEAM_COMPAT_DATA_PATH".to_string(), compat_data_path.to_string_lossy().to_string()); diff --git a/src/launch/pipeline.rs b/src/launch/pipeline.rs index f59cbdd..e1855a5 100644 --- a/src/launch/pipeline.rs +++ b/src/launch/pipeline.rs @@ -391,6 +391,14 @@ impl LaunchPipeline { let mut fatal_error = None; for evidence in &ctx.graphics_stack.graphics_stack_evidence { + if evidence.contains("Steam Client/Environment Failure") { + fatal_error = Some("steam_environment_incomplete"); + break; + } + if evidence.contains("PhysX/Middleware failure") { + fatal_error = Some("middleware_dependency_failure"); + break; + } if evidence.contains("DLL Load Failure") { fatal_error = Some("missing_required_module"); break; @@ -756,6 +764,12 @@ impl LaunchPipeline { let lines: Vec<&str> = content.lines().collect(); ctx.graphics_stack.runtime_evidence.scan_metadata.line_count = lines.len(); + // Capture Log Head/Tail + ctx.verification.log_head = lines.iter().take(50).map(|s| s.to_string()).collect(); + if lines.len() > 50 { + ctx.verification.log_tail = lines.iter().rev().take(100).rev().map(|s| s.to_string()).collect(); + } + // Derive component paths from dll resolutions let mut component_paths = HashMap::new(); for res in &ctx.dll_resolutions { @@ -964,17 +978,51 @@ impl LaunchPipeline { let mut metadata = HashMap::new(); metadata.insert("prefix_path".to_string(), prefix.clone()); + // Detect Windows username + let users_dir = prefix_path.join("drive_c/users"); + if users_dir.exists() { + if let Ok(entries) = std::fs::read_dir(&users_dir) { + let mut usernames = Vec::new(); + for entry in entries.flatten() { + if entry.file_type().map(|t| t.is_dir()).unwrap_or(false) { + let name = entry.file_name().to_string_lossy().to_string(); + if name != "Public" && name != "All Users" && name != "Default User" { + usernames.push(name); + } + } + } + if !usernames.is_empty() { + // Sort and pick first (most likely primary) + usernames.sort(); + let primary = usernames[0].clone(); + ctx.verification.windows_username = Some(primary.clone()); + ctx.verification.windows_user_path = Some(format!("C:\\users\\{}", primary)); + metadata.insert("detected_windows_username".to_string(), primary); + } + } + } + + let username = ctx.verification.windows_username.as_deref().unwrap_or("steamuser"); let common_dirs = [ - "drive_c/users/steamuser/Documents", - "drive_c/users/steamuser/AppData/Local", - "drive_c/users/steamuser/AppData/Roaming", + format!("drive_c/users/{}/Documents", username), + format!("drive_c/users/{}/AppData/Local", username), + format!("drive_c/users/{}/AppData/Roaming", username), + "drive_c/Program Files (x86)/Steam/steam.exe".to_string(), + "drive_c/windows/system32/PhysXLoader.dll".to_string(), + "drive_c/windows/syswow64/PhysXLoader.dll".to_string(), ]; for dir in common_dirs { - let full_path = prefix_path.join(dir); - metadata.insert(format!("dir_exists:{}", dir), full_path.exists().to_string()); + let full_path = prefix_path.join(&dir); + let exists = full_path.exists(); + metadata.insert(format!("path_exists:{}", dir), exists.to_string()); + ctx.verification.key_paths_detected.insert(dir, exists); } + // Check steam client exposure + ctx.verification.steam_client_exposed = spec.env.contains_key("STEAM_COMPAT_CLIENT_INSTALL_PATH") || + spec.env.get("WINEPATH").map(|wp| wp.contains("Steam")).unwrap_or(false); + if let Some(logger) = &ctx.logger { let _ = logger.info("prefix_health_check", "WINEPREFIX sanity check complete".to_string(), None, metadata); } @@ -1148,6 +1196,11 @@ impl LaunchPipeline { metadata.insert("verification_detailed".to_string(), detailed.clone()); } + if let Some(ref username) = ctx.verification.windows_username { + metadata.insert("windows_user".to_string(), username.clone()); + } + metadata.insert("steam_client_exposed".to_string(), ctx.verification.steam_client_exposed.to_string()); + let _ = logger.info("launch_summary_concise", "Concise launch summary recorded".to_string(), None, metadata); } } From 34776202781cb847732cb9c295b5b27becad16c2 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 20:05:08 +0000 Subject: [PATCH 02/11] Investigate and fix Batman startup regression - Modify `build_dll_overrides` to exclude `d3d10` and `d3d10_1` from explicit DXVK overrides, allowing Wine's built-in wrappers to delegate to `d3d10core` natively. - Add `DANGEROUS_ALIAS_OVERRIDE` validation to `LaunchInvariantValidator` to warn against explicit native overrides for alias DLLs. - Enhance `classify_early_exit` and `classify_graphics_evidence` with new regression categories: `startup_environment_regression`, `override_policy_regression`, and `steam_runtime_exposure_regression`. - Improve tracing logs in `WineTkgRunner` to record override generation and alias skipping. - Ensure `STEAM_COMPAT_APP_ID` is included in the launch environment. - Implement dynamic Windows username detection and middleware/client path verification in prefix health checks. - Capture first 50 and last 100 lines of Wine log for early exit analysis. Co-authored-by: weter11 <14630689+weter11@users.noreply.github.com> --- src/infra/logging/wine_capture.rs | 6 ++++++ src/infra/runners/wine_tkg.rs | 4 ++++ src/launch/pipeline.rs | 12 ++++++++++-- src/launch/validators/invariants.rs | 18 ++++++++++++++++++ src/utils.rs | 7 +------ 5 files changed, 39 insertions(+), 8 deletions(-) diff --git a/src/infra/logging/wine_capture.rs b/src/infra/logging/wine_capture.rs index c6c1ad8..b044ffc 100644 --- a/src/infra/logging/wine_capture.rs +++ b/src/infra/logging/wine_capture.rs @@ -51,5 +51,11 @@ pub fn classify_graphics_evidence(log_line: &str) -> Option { return Some(format!("Steam Client/Environment Failure: {}", log_line.trim())); } + // Override/Policy regressions + if line_lower.contains("invalid dll") || + (line_lower.contains("failed to load") && (line_lower.contains("d3d10") || line_lower.contains("d3d11"))) { + return Some(format!("Override Policy Conflict: {}", log_line.trim())); + } + None } diff --git a/src/infra/runners/wine_tkg.rs b/src/infra/runners/wine_tkg.rs index 668204a..6497a30 100644 --- a/src/infra/runners/wine_tkg.rs +++ b/src/infra/runners/wine_tkg.rs @@ -395,11 +395,15 @@ impl Runner for WineTkgRunner { (res.chosen_provider == crate::launch::dll_provider_resolver::DllProvider::Runner && res.name.contains("nvapi")) { // Ensure native wins for game-local or non-symlinked custom DLLs if !dll_overrides.contains(&format!("{}=n", res.name)) { + tracing::info!("Adding native override for resolved DLL: {} (provider: {:?})", res.name, res.chosen_provider); dll_overrides.push_str(&format!(";{}=n", res.name)); } + } else if res.chosen_provider == crate::launch::dll_provider_resolver::DllProvider::Internal { + tracing::info!("Resolved DLL {} is handled internally (alias), skipping explicit override", res.name); } } + tracing::info!("Final WINEDLLOVERRIDES: {}", dll_overrides); env.insert("WINEDLLOVERRIDES".to_string(), dll_overrides); // Track effective state for diagnostics (HACK: should ideally be done in a separate stage) diff --git a/src/launch/pipeline.rs b/src/launch/pipeline.rs index e1855a5..6a0d771 100644 --- a/src/launch/pipeline.rs +++ b/src/launch/pipeline.rs @@ -391,8 +391,12 @@ impl LaunchPipeline { let mut fatal_error = None; for evidence in &ctx.graphics_stack.graphics_stack_evidence { + if evidence.contains("Override Policy Conflict") { + fatal_error = Some("override_policy_regression"); + break; + } if evidence.contains("Steam Client/Environment Failure") { - fatal_error = Some("steam_environment_incomplete"); + fatal_error = Some("steam_runtime_exposure_regression"); break; } if evidence.contains("PhysX/Middleware failure") { @@ -400,7 +404,11 @@ impl LaunchPipeline { break; } if evidence.contains("DLL Load Failure") { - fatal_error = Some("missing_required_module"); + if evidence.contains("d3d10") || evidence.contains("d3d11") { + fatal_error = Some("startup_environment_regression"); + } else { + fatal_error = Some("missing_required_module"); + } break; } if evidence.contains("DLL Dependency Missing") { diff --git a/src/launch/validators/invariants.rs b/src/launch/validators/invariants.rs index 03626fa..0ac29bd 100644 --- a/src/launch/validators/invariants.rs +++ b/src/launch/validators/invariants.rs @@ -250,6 +250,24 @@ impl LaunchValidator for LaunchInvariantValidator { )); } + // Invariant E: DXVK alias DLLs (d3d10, d3d10_1) should not be explicitly overridden as native. + if let Some(spec) = &ctx.command_spec { + if let Some(overrides) = spec.env.get("WINEDLLOVERRIDES") { + let alias_dlls = ["d3d10", "d3d10_1"]; + for part in overrides.split(';') { + if let Some((dll, mode)) = part.split_once('=') { + let dll_trimmed = dll.trim().to_lowercase(); + if alias_dlls.contains(&dll_trimmed.as_str()) && mode.contains('n') { + warnings.push(( + "DANGEROUS_ALIAS_OVERRIDE", + format!("Found native override for DXVK alias DLL: {}. This often causes startup failures. These should be handled via Wine's built-in wrappers delegating to d3d10core.", dll), + )); + } + } + } + } + } + for (code, msg) in warnings { ctx.add_warning(code, msg); } diff --git a/src/utils.rs b/src/utils.rs index 7e1cc35..e26e6c4 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -761,17 +761,12 @@ pub fn build_dll_overrides( for dll in &[ "d3d8.dll", "d3d9.dll", - "d3d10.dll", - "d3d10_1.dll", "d3d10core.dll", "d3d11.dll", "dxgi.dll", ] { let stem = dll.trim_end_matches(".dll"); - // Special case: d3d10 and d3d10_1 should always allow builtin fallback (n,b) - // even in strict DXVK mode, because DXVK typically doesn't provide them - // standalone; it uses d3d10core instead. - let mode = if strict_dxvk && stem != "d3d10" && stem != "d3d10_1" { "n" } else { "n,b" }; + let mode = if strict_dxvk { "n" } else { "n,b" }; if strict_dxvk || !game_has(dll) { overrides.push(format!("{stem}={mode}")); From 9cae845a2fdbf533c0a91530ae2714d750d9607f Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 21:15:24 +0000 Subject: [PATCH 03/11] Stop emitting d3d10/d3d10_1 overrides for DXVK aliases Co-authored-by: weter11 <14630689+weter11@users.noreply.github.com> --- src/infra/runners/wine_tkg.rs | 7 +++++++ src/launch/validators/invariants.rs | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/src/infra/runners/wine_tkg.rs b/src/infra/runners/wine_tkg.rs index 6497a30..6f79703 100644 --- a/src/infra/runners/wine_tkg.rs +++ b/src/infra/runners/wine_tkg.rs @@ -393,6 +393,13 @@ impl Runner for WineTkgRunner { if res.chosen_provider == crate::launch::dll_provider_resolver::DllProvider::GameLocal || (res.chosen_provider == crate::launch::dll_provider_resolver::DllProvider::Custom && !use_symlinks) || (res.chosen_provider == crate::launch::dll_provider_resolver::DllProvider::Runner && res.name.contains("nvapi")) { + + // Do not emit overrides for DLLs that are handled via internal capabilities + if res.chosen_provider == crate::launch::dll_provider_resolver::DllProvider::Internal { + tracing::info!("Resolved DLL {} is handled internally (alias), skipping explicit override", res.name); + continue; + } + // Ensure native wins for game-local or non-symlinked custom DLLs if !dll_overrides.contains(&format!("{}=n", res.name)) { tracing::info!("Adding native override for resolved DLL: {} (provider: {:?})", res.name, res.chosen_provider); diff --git a/src/launch/validators/invariants.rs b/src/launch/validators/invariants.rs index 0ac29bd..58a1358 100644 --- a/src/launch/validators/invariants.rs +++ b/src/launch/validators/invariants.rs @@ -38,6 +38,11 @@ impl LaunchValidator for LaunchInvariantValidator { if let Some((dll, mode)) = part.split_once('=') { let dll_trimmed = dll.trim().to_lowercase(); if dxvk_dlls.contains(&dll_trimmed.as_str()) && mode.contains('n') { + // Exclude d3d10/10_1 from being flagged as DXVK-forcing if they are not in the concrete list + if dll_trimmed == "d3d10" || dll_trimmed == "d3d10_1" { + continue; + } + warnings.push(( "INVARIANT_B_VIOLATION", format!("Effective backend is not DXVK but found native override for DXVK DLL: {}", dll), From 71a4a7ba902f2f0484acf2bb6327e81636fb71c2 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 22:10:11 +0000 Subject: [PATCH 04/11] Remove d3d10/d3d10_1 runtime logic and keep only real DXVK DLLs - Remove `d3d10` and `d3d10_1` from `DllProviderResolver` target list. - Clean up `ResolveDllProvidersStage` to remove DXVK capability fixup for aliases. - Audit `utils.rs` and remove aliases from `build_dll_overrides` and `cleanup_dll_symlinks`. - Update `LaunchInvariantValidator` and `OverrideConflictValidator` to remove alias-specific rules. - Refine error classification patterns in `pipeline.rs` and `wine_capture.rs`. - Focus diagnostics and launch configuration entirely on concrete DXVK DLLs (`d3d8`, `d3d9`, `d3d10core`, `d3d11`, `dxgi`). Co-authored-by: weter11 <14630689+weter11@users.noreply.github.com> --- src/infra/logging/wine_capture.rs | 2 +- src/launch/dll_provider_resolver.rs | 8 ++---- src/launch/pipeline.rs | 2 +- src/launch/stages/resolve_dll_providers.rs | 33 +++------------------- src/launch/validators/invariants.rs | 24 +--------------- src/launch/validators/overrides.rs | 2 +- src/utils.rs | 4 +-- 7 files changed, 12 insertions(+), 63 deletions(-) diff --git a/src/infra/logging/wine_capture.rs b/src/infra/logging/wine_capture.rs index b044ffc..058ff63 100644 --- a/src/infra/logging/wine_capture.rs +++ b/src/infra/logging/wine_capture.rs @@ -53,7 +53,7 @@ pub fn classify_graphics_evidence(log_line: &str) -> Option { // Override/Policy regressions if line_lower.contains("invalid dll") || - (line_lower.contains("failed to load") && (line_lower.contains("d3d10") || line_lower.contains("d3d11"))) { + (line_lower.contains("failed to load") && (line_lower.contains("d3d11"))) { return Some(format!("Override Policy Conflict: {}", log_line.trim())); } diff --git a/src/launch/dll_provider_resolver.rs b/src/launch/dll_provider_resolver.rs index 128c91e..8a57be6 100644 --- a/src/launch/dll_provider_resolver.rs +++ b/src/launch/dll_provider_resolver.rs @@ -56,8 +56,6 @@ impl DllProviderResolver { "d3d8".into(), "d3d9".into(), "dxgi".into(), - "d3d10".into(), - "d3d10_1".into(), "d3d10core".into(), "d3d11".into(), "d3d12".into(), @@ -273,7 +271,7 @@ impl DllProviderResolver { // 3. System Priority // For now, we use a simplified check for system paths let system_paths = match dll_name { - "d3d8" | "d3d9" | "d3d10" | "d3d10_1" | "d3d10core" | "d3d11" | "dxgi" => vec![ + "d3d8" | "d3d9" | "d3d10core" | "d3d11" | "dxgi" => vec![ "/usr/lib/dxvk/x64", "/usr/lib/x86_64-linux-gnu/dxvk", ], @@ -324,7 +322,7 @@ impl DllProviderResolver { custom_vkd3d_proton_path: Option<&Path>, ) -> Option { let dll_filename = format!("{}.dll", dll_name); - let is_dxvk = matches!(dll_name, "d3d8" | "d3d9" | "d3d10" | "d3d10_1" | "d3d10core" | "d3d11" | "dxgi"); + 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"); @@ -382,7 +380,7 @@ impl DllProviderResolver { let dll_filename = format!("{}.dll", dll_name); // Match DLL to component and look for it in runner root - let is_dxvk = matches!(dll_name, "d3d8" | "d3d9" | "d3d10" | "d3d10_1" | "d3d10core" | "d3d11" | "dxgi"); + let is_dxvk = matches!(dll_name, "d3d8" | "d3d9" | "d3d10core" | "d3d11" | "dxgi"); if is_dxvk && components.dxvk.is_some() { let mut relative_paths = vec![ "lib/wine/dxvk/x86_64-windows", diff --git a/src/launch/pipeline.rs b/src/launch/pipeline.rs index 6a0d771..df8d290 100644 --- a/src/launch/pipeline.rs +++ b/src/launch/pipeline.rs @@ -404,7 +404,7 @@ impl LaunchPipeline { break; } if evidence.contains("DLL Load Failure") { - if evidence.contains("d3d10") || evidence.contains("d3d11") { + if evidence.contains("d3d11") { fatal_error = Some("startup_environment_regression"); } else { fatal_error = Some("missing_required_module"); diff --git a/src/launch/stages/resolve_dll_providers.rs b/src/launch/stages/resolve_dll_providers.rs index 2568f62..4ad5fde 100644 --- a/src/launch/stages/resolve_dll_providers.rs +++ b/src/launch/stages/resolve_dll_providers.rs @@ -106,29 +106,6 @@ impl PipelineStage for ResolveDllProvidersStage { ctx.dll_resolutions = resolutions; - // DXVK Capability Fixup for D3D10/10.1 - if let Some(config) = &ctx.user_config { - if config.graphics_layers.graphics_backend_policy == crate::models::GraphicsBackendPolicy::DXVK { - let has_dxvk_core = |name: &str| -> bool { - ctx.dll_resolutions.iter() - .find(|r| r.name == name) - .map(|r| r.chosen_provider != crate::launch::dll_provider_resolver::DllProvider::None) - .unwrap_or(false) - }; - - let d3d10_supported = has_dxvk_core("d3d10core") && has_dxvk_core("d3d11") && has_dxvk_core("dxgi"); - - if d3d10_supported { - for res in &mut ctx.dll_resolutions { - if (res.name == "d3d10" || res.name == "d3d10_1") && res.chosen_provider == crate::launch::dll_provider_resolver::DllProvider::None { - res.chosen_provider = crate::launch::dll_provider_resolver::DllProvider::Internal; - res.fallback_reason = Some("Satisfied via DXVK D3D10 capability (d3d10core + d3d11 + dxgi)".to_string()); - } - } - } - } - } - // Strict Backend Policy Enforcement if let Some(config) = &ctx.user_config { let backend_policy = &config.graphics_layers.graphics_backend_policy; @@ -155,12 +132,10 @@ impl PipelineStage for ResolveDllProvidersStage { if !has_dx11_dxgi { missing_capabilities.push("DX11/DXGI (requires d3d11.dll and dxgi.dll)"); } - if !has_dx10_core || !has_dx11_dxgi { - if !has_dx10_core { - missing_capabilities.push("DX10/10.1 capability incomplete: missing d3d10core.dll"); - } else { - missing_capabilities.push("DX10/10.1 support unavailable because d3d11.dll or dxgi.dll could not be resolved"); - } + if !has_dx10_core { + missing_capabilities.push("DX10/10.1 capability incomplete: missing d3d10core.dll"); + } else if !has_dx11_dxgi { + missing_capabilities.push("DX10/10.1 support unavailable because d3d11.dll or dxgi.dll could not be resolved"); } if !has_dx9 { missing_capabilities.push("DX9 (requires d3d9.dll)"); diff --git a/src/launch/validators/invariants.rs b/src/launch/validators/invariants.rs index 58a1358..a72d0f5 100644 --- a/src/launch/validators/invariants.rs +++ b/src/launch/validators/invariants.rs @@ -33,16 +33,11 @@ impl LaunchValidator for LaunchInvariantValidator { if ctx.graphics_stack.effective_backend != "DXVK" { if let Some(spec) = &ctx.command_spec { if let Some(overrides) = spec.env.get("WINEDLLOVERRIDES") { - let dxvk_dlls = ["d3d8", "d3d9", "d3d10", "d3d10core", "d3d11", "dxgi"]; + let dxvk_dlls = ["d3d8", "d3d9", "d3d10core", "d3d11", "dxgi"]; for part in overrides.split(';') { if let Some((dll, mode)) = part.split_once('=') { let dll_trimmed = dll.trim().to_lowercase(); if dxvk_dlls.contains(&dll_trimmed.as_str()) && mode.contains('n') { - // Exclude d3d10/10_1 from being flagged as DXVK-forcing if they are not in the concrete list - if dll_trimmed == "d3d10" || dll_trimmed == "d3d10_1" { - continue; - } - warnings.push(( "INVARIANT_B_VIOLATION", format!("Effective backend is not DXVK but found native override for DXVK DLL: {}", dll), @@ -255,23 +250,6 @@ impl LaunchValidator for LaunchInvariantValidator { )); } - // Invariant E: DXVK alias DLLs (d3d10, d3d10_1) should not be explicitly overridden as native. - if let Some(spec) = &ctx.command_spec { - if let Some(overrides) = spec.env.get("WINEDLLOVERRIDES") { - let alias_dlls = ["d3d10", "d3d10_1"]; - for part in overrides.split(';') { - if let Some((dll, mode)) = part.split_once('=') { - let dll_trimmed = dll.trim().to_lowercase(); - if alias_dlls.contains(&dll_trimmed.as_str()) && mode.contains('n') { - warnings.push(( - "DANGEROUS_ALIAS_OVERRIDE", - format!("Found native override for DXVK alias DLL: {}. This often causes startup failures. These should be handled via Wine's built-in wrappers delegating to d3d10core.", dll), - )); - } - } - } - } - } for (code, msg) in warnings { ctx.add_warning(code, msg); diff --git a/src/launch/validators/overrides.rs b/src/launch/validators/overrides.rs index 8e2fe9f..03e7a3c 100644 --- a/src/launch/validators/overrides.rs +++ b/src/launch/validators/overrides.rs @@ -16,7 +16,7 @@ impl LaunchValidator for OverrideConflictValidator { // 1. Check for D3D/DXGI override conflicts with DXVK if dxvk_enabled { - let conflicts = ["d3d9", "d3d10", "d3d10core", "d3d11", "dxgi"]; + let conflicts = ["d3d9", "d3d10core", "d3d11", "dxgi"]; if let Some(val) = user_config.env_variables.get("WINEDLLOVERRIDES") { for part in val.split(';') { if let Some((dll, mode)) = part.split_once('=') { diff --git a/src/utils.rs b/src/utils.rs index e26e6c4..3ba3c80 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -737,8 +737,6 @@ pub fn build_dll_overrides( for dll in &[ "d3d8", "d3d9", - "d3d10", - "d3d10_1", "d3d10core", "d3d11", "dxgi", @@ -1094,7 +1092,7 @@ fn find_sibling_dll( pub fn cleanup_dll_symlinks(prefix: &Path) -> Result<()> { let target_dlls = [ - "d3d8.dll", "d3d9.dll", "dxgi.dll", "d3d10.dll", "d3d10_1.dll", "d3d10core.dll", + "d3d8.dll", "d3d9.dll", "dxgi.dll", "d3d10core.dll", "d3d11.dll", "d3d12.dll", "d3d12core.dll", "libvkd3d-1.dll", "libvkd3d-shader-1.dll" ]; From eb1f22e84640cadb41c9b2bf04d665a72cba5b44 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 19:55:33 +0000 Subject: [PATCH 05/11] Compare Batman startup path against Amnesia to identify failure point - Implement `StartupMilestone` detection to track launch phases. - Add dependency family detection (PhysX vs SDL2) to identify title-specific ecosystems. - Remove `d3d10` and `d3d10_1` from runtime configuration and overrides. - Capture 'Head' and 'Tail' of Wine logs for diagnostic context. - Update launch summaries with milestone and dependency metadata. Co-authored-by: weter11 <14630689+weter11@users.noreply.github.com> --- src/infra/logging/session.rs | 2 + src/infra/logging/wine_capture.rs | 71 +++++++++++++++++++++++++++++++ src/launch/pipeline.rs | 30 +++++++++++++ 3 files changed, 103 insertions(+) diff --git a/src/infra/logging/session.rs b/src/infra/logging/session.rs index cdf0b40..3090b2c 100644 --- a/src/infra/logging/session.rs +++ b/src/infra/logging/session.rs @@ -54,6 +54,8 @@ pub struct LaunchVerification { pub windows_user_path: Option, pub key_paths_detected: HashMap, pub steam_client_exposed: bool, + pub last_successful_startup_milestone: String, + pub dependency_families_detected: Vec, pub log_head: Vec, pub log_tail: Vec, } diff --git a/src/infra/logging/wine_capture.rs b/src/infra/logging/wine_capture.rs index 058ff63..f959261 100644 --- a/src/infra/logging/wine_capture.rs +++ b/src/infra/logging/wine_capture.rs @@ -1,6 +1,77 @@ +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum StartupMilestone { + None = 0, + InitialProcessBootstrap = 1, + SteamBootstrapInitialized = 2, + GameLocalDllsLoaded = 3, + GraphicsRendererInitStarted = 4, + RunningState = 5, +} + +impl std::fmt::Display for StartupMilestone { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = match self { + Self::None => "none", + Self::InitialProcessBootstrap => "initial_process_bootstrap", + Self::SteamBootstrapInitialized => "steam_bootstrap_initialized", + Self::GameLocalDllsLoaded => "game_local_dlls_loaded", + Self::GraphicsRendererInitStarted => "graphics_renderer_init_started", + Self::RunningState => "running_state", + }; + write!(f, "{}", s) + } +} + +pub fn detect_startup_milestone(log_line: &str) -> Option { + let line_lower = log_line.to_lowercase(); + + // Generic bootstrap markers + if line_lower.contains("wine_init") || line_lower.contains("ntdll:ldrload_dll") || line_lower.contains("kernelbase:loadlibrary") { + return Some(StartupMilestone::InitialProcessBootstrap); + } + + // Steam bootstrap + if line_lower.contains("steamapi_init") || line_lower.contains("steamapi_restartappifnecessary") || line_lower.contains("steam_client: initialized") { + return Some(StartupMilestone::SteamBootstrapInitialized); + } + + // Game local DLLs (representative families for Batman/Amnesia) + if line_lower.contains("loaddll") && ( + line_lower.contains("physx") || line_lower.contains("gfsdk") || line_lower.contains("nvtt") || // Batman + line_lower.contains("sdl2") || line_lower.contains("newton") || line_lower.contains("devil") // Amnesia + ) { + return Some(StartupMilestone::GameLocalDllsLoaded); + } + + // Renderer initialization + if line_lower.contains("d3d11internalcreatedevice") || + line_lower.contains("dxvk: v") || + line_lower.contains("vkd3d-proton: v") || + line_lower.contains("presenter: actual swapchain properties") || + line_lower.contains("wined3d: v") { + return Some(StartupMilestone::GraphicsRendererInitStarted); + } + + // Likely running state + if line_lower.contains("initialization complete") || line_lower.contains("game: main loop started") { + return Some(StartupMilestone::RunningState); + } + + None +} + pub fn classify_graphics_evidence(log_line: &str) -> Option { let line_lower = log_line.to_lowercase(); + // Noise Filter: generic Wine loader noise is ignored for evidence but preserved for milestones + if line_lower.contains("find_builtin_dll") || + line_lower.contains("winemac.drv") || + line_lower.contains("winecoreaudio.drv") || + line_lower.contains("xinput") || + line_lower.contains("libegl") { + return None; + } + // DXVK signatures if line_lower.contains("info: dxvk:") || line_lower.contains("dxvk: v") || diff --git a/src/launch/pipeline.rs b/src/launch/pipeline.rs index df8d290..95082c5 100644 --- a/src/launch/pipeline.rs +++ b/src/launch/pipeline.rs @@ -778,6 +778,8 @@ impl LaunchPipeline { ctx.verification.log_tail = lines.iter().rev().take(100).rev().map(|s| s.to_string()).collect(); } + let mut max_milestone = crate::infra::logging::StartupMilestone::None; + // Derive component paths from dll resolutions let mut component_paths = HashMap::new(); for res in &ctx.dll_resolutions { @@ -797,6 +799,14 @@ impl LaunchPipeline { for line in &lines { let mut line_matched = false; + + // Update Milestone + if let Some(m) = crate::infra::logging::detect_startup_milestone(line) { + if m > max_milestone { + max_milestone = m; + } + } + if let Some(evidence) = crate::infra::logging::classify_graphics_evidence(line) { if !ctx.graphics_stack.graphics_stack_evidence.contains(&evidence) { ctx.graphics_stack.graphics_stack_evidence.push(evidence.clone()); @@ -890,6 +900,8 @@ impl LaunchPipeline { ctx.graphics_stack.runtime_evidence.scan_metadata.candidate_matches += 1; } } + + ctx.verification.last_successful_startup_milestone = max_milestone.to_string(); } } else { ctx.graphics_stack.runtime_evidence.scan_metadata.file_exists = false; @@ -1027,6 +1039,20 @@ impl LaunchPipeline { ctx.verification.key_paths_detected.insert(dir, exists); } + // Title-Specific Dependency Detection + let mut families = Vec::new(); + let families_to_check = [ + ("Batman (PhysX/APEX)", vec!["drive_c/windows/system32/PhysXLoader.dll", "drive_c/windows/syswow64/PhysXLoader.dll"]), + ("Amnesia (SDL2/Newton)", vec!["drive_c/windows/system32/SDL2.dll", "drive_c/windows/syswow64/SDL2.dll"]), + ]; + + for (family, paths) in families_to_check { + if paths.iter().any(|p| prefix_path.join(p).exists()) { + families.push(family.to_string()); + } + } + ctx.verification.dependency_families_detected = families; + // Check steam client exposure ctx.verification.steam_client_exposed = spec.env.contains_key("STEAM_COMPAT_CLIENT_INSTALL_PATH") || spec.env.get("WINEPATH").map(|wp| wp.contains("Steam")).unwrap_or(false); @@ -1208,6 +1234,10 @@ impl LaunchPipeline { metadata.insert("windows_user".to_string(), username.clone()); } metadata.insert("steam_client_exposed".to_string(), ctx.verification.steam_client_exposed.to_string()); + metadata.insert("last_milestone".to_string(), ctx.verification.last_successful_startup_milestone.clone()); + if !ctx.verification.dependency_families_detected.is_empty() { + metadata.insert("dependency_families".to_string(), ctx.verification.dependency_families_detected.join(", ")); + } let _ = logger.info("launch_summary_concise", "Concise launch summary recorded".to_string(), None, metadata); } From 76d13f49bf7a7bab1a15add01e67863bfad36c1c Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 19:43:22 +0000 Subject: [PATCH 06/11] Fix Batman early startup failure and enhance environment diagnostics - Implement Startup Milestone tracking (Bootstrap -> Steam -> Game DLLs -> Renderer) - Add dynamic Windows username and home path detection in prefix - Add explicit verification for Steam runtime (steamclient.dll, tier0_s.dll) and PhysX - Remove problematic d3d10/d3d10_1 native overrides (handled via d3d10core) - Capture first 50 and last 100 lines of Wine log for early-exit diagnostics - Inject STEAM_COMPAT_APP_ID into launch environment - Update failure classification to distinguish between Steam runtime and graphics failures Co-authored-by: weter11 <14630689+weter11@users.noreply.github.com> --- src/infra/logging/session.rs | 5 +++++ src/infra/logging/wine_capture.rs | 5 +++++ src/infra/runners/tests.rs | 2 ++ src/infra/runners/trait.rs | 4 ++++ src/infra/runners/wine_tkg.rs | 35 +++++++++++++++++++++++++++++ src/launch/pipeline.rs | 9 ++++++++ src/launch/stages/build_command.rs | 1 + src/launch/stages/prepare_prefix.rs | 1 + 8 files changed, 62 insertions(+) diff --git a/src/infra/logging/session.rs b/src/infra/logging/session.rs index 3090b2c..70ff863 100644 --- a/src/infra/logging/session.rs +++ b/src/infra/logging/session.rs @@ -56,6 +56,11 @@ pub struct LaunchVerification { pub steam_client_exposed: bool, pub last_successful_startup_milestone: String, pub dependency_families_detected: Vec, + pub steam_runtime_exe: Option, + pub steam_runtime_args: Vec, + pub steam_runtime_exit_code: Option, + pub steam_runtime_lifetime_ms: Option, + pub steam_runtime_milestone: String, pub log_head: Vec, pub log_tail: Vec, } diff --git a/src/infra/logging/wine_capture.rs b/src/infra/logging/wine_capture.rs index f959261..2fae492 100644 --- a/src/infra/logging/wine_capture.rs +++ b/src/infra/logging/wine_capture.rs @@ -25,6 +25,11 @@ impl std::fmt::Display for StartupMilestone { pub fn detect_startup_milestone(log_line: &str) -> Option { let line_lower = log_line.to_lowercase(); + // Steam Runtime specific milestones (can be used for background Steam too) + if line_lower.contains("steam.exe") && line_lower.contains("starting") { + return Some(StartupMilestone::InitialProcessBootstrap); + } + // Generic bootstrap markers if line_lower.contains("wine_init") || line_lower.contains("ntdll:ldrload_dll") || line_lower.contains("kernelbase:loadlibrary") { return Some(StartupMilestone::InitialProcessBootstrap); diff --git a/src/infra/runners/tests.rs b/src/infra/runners/tests.rs index c1f8385..19e3cfe 100644 --- a/src/infra/runners/tests.rs +++ b/src/infra/runners/tests.rs @@ -33,6 +33,7 @@ mod tests { proton_path: Some("/tmp/proton".to_string()), target_architecture: crate::models::ExecutableArchitecture::X86_64, dll_resolutions: Vec::new(), + verification_ptr: std::ptr::null_mut(), } } @@ -111,6 +112,7 @@ mod tests { proton_path: Some(runner_path.to_string_lossy().to_string()), target_architecture: crate::models::ExecutableArchitecture::X86_64, dll_resolutions: Vec::new(), + verification_ptr: std::ptr::null_mut(), }; let runner = WineTkgRunner; diff --git a/src/infra/runners/trait.rs b/src/infra/runners/trait.rs index 4430cd5..9c1d3a8 100644 --- a/src/infra/runners/trait.rs +++ b/src/infra/runners/trait.rs @@ -13,8 +13,12 @@ pub struct LaunchContext { pub proton_path: Option, pub target_architecture: crate::models::ExecutableArchitecture, pub dll_resolutions: Vec, + pub verification_ptr: *mut crate::infra::logging::LaunchVerification, // HACK: for Runner to write diagnostics } +unsafe impl Send for LaunchContext {} +unsafe impl Sync for LaunchContext {} + #[derive(Debug, Clone, Default)] pub struct CommandSpec { pub program: PathBuf, diff --git a/src/infra/runners/wine_tkg.rs b/src/infra/runners/wine_tkg.rs index 6f79703..43adffa 100644 --- a/src/infra/runners/wine_tkg.rs +++ b/src/infra/runners/wine_tkg.rs @@ -196,9 +196,26 @@ impl Runner for WineTkgRunner { println!("Args: {:?}", steam_cmd.get_args().collect::>()); println!("--------------------------"); + // Record Steam runtime diagnostics + unsafe { + if !ctx.verification_ptr.is_null() { + let v = &mut *ctx.verification_ptr; + v.steam_runtime_exe = Some(steam_cmd.get_program().to_string_lossy().to_string()); + v.steam_runtime_args = steam_cmd.get_args().map(|a| a.to_string_lossy().to_string()).collect(); + v.steam_runtime_milestone = "steam_process_spawn_requested".to_string(); + } + } + + let start_time = std::time::Instant::now(); let mut steam_process = steam_cmd.spawn().map_err(|e| LaunchError::new(LaunchErrorKind::Process, "Failed to spawn background Steam").with_source(anyhow!(e)))?; + unsafe { + if !ctx.verification_ptr.is_null() { + (*ctx.verification_ptr).steam_runtime_milestone = "steam_process_spawned".to_string(); + } + } + println!("Waiting for Steam to initialise (max 30s)..."); let steam_pid_path = prefix_steam_dir.join("steam.pid"); @@ -213,6 +230,14 @@ impl Runner for WineTkgRunner { // Crash detection — bail immediately if let Ok(Some(status)) = steam_process.try_wait() { println!("❌ FATAL: Background Steam exited after {}s with: {}", i + 1, status); + unsafe { + if !ctx.verification_ptr.is_null() { + let v = &mut *ctx.verification_ptr; + v.steam_runtime_exit_code = status.code(); + v.steam_runtime_lifetime_ms = Some(start_time.elapsed().as_millis() as u64); + v.steam_runtime_milestone = "steam_process_exited_early".to_string(); + } + } break 'wait false; } @@ -240,12 +265,22 @@ impl Runner for WineTkgRunner { .unwrap_or(0); if log_count >= 2 { println!("✅ Steam ready after {}s ({} log files found)", i + 1, log_count); + unsafe { + if !ctx.verification_ptr.is_null() { + (*ctx.verification_ptr).steam_runtime_milestone = "steam_ready_signal_observed".to_string(); + } + } break 'wait true; } println!(" Waiting... {}s", i + 1); } println!("⚠️ Steam did not signal ready after 30s, launching game anyway"); + unsafe { + if !ctx.verification_ptr.is_null() { + (*ctx.verification_ptr).steam_runtime_milestone = "steam_ready_timeout".to_string(); + } + } true }; diff --git a/src/launch/pipeline.rs b/src/launch/pipeline.rs index 95082c5..5fb2bdb 100644 --- a/src/launch/pipeline.rs +++ b/src/launch/pipeline.rs @@ -383,6 +383,10 @@ impl LaunchPipeline { fn classify_early_exit(&self, ctx: &mut PipelineContext) { if ctx.verification.status != "failed_after_spawn" { + if ctx.verification.steam_runtime_milestone == "steam_process_exited_early" { + ctx.verification.detailed_status = Some("steam_runtime_failed_before_game_start".to_string()); + return; + } return; } @@ -1028,6 +1032,8 @@ impl LaunchPipeline { format!("drive_c/users/{}/AppData/Local", username), format!("drive_c/users/{}/AppData/Roaming", username), "drive_c/Program Files (x86)/Steam/steam.exe".to_string(), + "drive_c/Program Files (x86)/Steam/steamclient.dll".to_string(), + "drive_c/Program Files (x86)/Steam/tier0_s.dll".to_string(), "drive_c/windows/system32/PhysXLoader.dll".to_string(), "drive_c/windows/syswow64/PhysXLoader.dll".to_string(), ]; @@ -1238,6 +1244,9 @@ impl LaunchPipeline { if !ctx.verification.dependency_families_detected.is_empty() { metadata.insert("dependency_families".to_string(), ctx.verification.dependency_families_detected.join(", ")); } + if !ctx.verification.steam_runtime_milestone.is_empty() { + metadata.insert("steam_runtime_milestone".to_string(), ctx.verification.steam_runtime_milestone.clone()); + } let _ = logger.info("launch_summary_concise", "Concise launch summary recorded".to_string(), None, metadata); } diff --git a/src/launch/stages/build_command.rs b/src/launch/stages/build_command.rs index 361dd0b..e59cd7e 100644 --- a/src/launch/stages/build_command.rs +++ b/src/launch/stages/build_command.rs @@ -21,6 +21,7 @@ impl PipelineStage for BuildCommandStage { proton_path: ctx.proton_path.clone(), target_architecture: ctx.target_architecture, dll_resolutions: ctx.dll_resolutions.clone(), + verification_ptr: &mut ctx.verification as *mut _, }; let spec = runner.build_command(&runner_ctx).await?; ctx.command_spec = Some(spec); diff --git a/src/launch/stages/prepare_prefix.rs b/src/launch/stages/prepare_prefix.rs index 2dcf4c0..63e161e 100644 --- a/src/launch/stages/prepare_prefix.rs +++ b/src/launch/stages/prepare_prefix.rs @@ -24,6 +24,7 @@ impl PipelineStage for PreparePrefixStage { proton_path: ctx.proton_path.clone(), target_architecture: ctx.target_architecture, dll_resolutions: ctx.dll_resolutions.clone(), + verification_ptr: &mut ctx.verification as *mut _, }; runner.prepare_prefix(&runner_ctx).await?; From ec1320a8635941998485cdad535ff004df71d062 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 4 Apr 2026 12:24:14 +0000 Subject: [PATCH 07/11] Enhance Steam handoff diagnostics and detect running Steam instances - Add `steam_running_before_launch`, `steam_auto_start_attempted`, and `steam_auto_start_failed` to `LaunchVerification`. - Update `WineTkgRunner` to detect existing Steam processes in the prefix and skip auto-start if present. - Implement `GameFailedWithSteamAlreadyRunning` and `SteamRuntimeFailedToStart` failure classifications. - Add log evidence patterns for `SteamAPI_Init` failures and `SteamAPI_RestartAppIfNecessary` triggers to detect handoff issues. - Include Steam-related status flags in the concise launch summary. Co-authored-by: weter11 <14630689+weter11@users.noreply.github.com> --- src/infra/logging/session.rs | 3 +++ src/infra/logging/wine_capture.rs | 8 ++++++++ src/infra/runners/wine_tkg.rs | 19 +++++++++++++++++-- src/launch/pipeline.rs | 9 +++++++++ 4 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/infra/logging/session.rs b/src/infra/logging/session.rs index 70ff863..cb5287d 100644 --- a/src/infra/logging/session.rs +++ b/src/infra/logging/session.rs @@ -61,6 +61,9 @@ pub struct LaunchVerification { pub steam_runtime_exit_code: Option, pub steam_runtime_lifetime_ms: Option, pub steam_runtime_milestone: String, + pub steam_running_before_launch: bool, + pub steam_auto_start_attempted: bool, + pub steam_auto_start_failed: bool, pub log_head: Vec, pub log_tail: Vec, } diff --git a/src/infra/logging/wine_capture.rs b/src/infra/logging/wine_capture.rs index 2fae492..6911da3 100644 --- a/src/infra/logging/wine_capture.rs +++ b/src/infra/logging/wine_capture.rs @@ -127,6 +127,14 @@ pub fn classify_graphics_evidence(log_line: &str) -> Option { return Some(format!("Steam Client/Environment Failure: {}", log_line.trim())); } + if line_lower.contains("steamapi_init") && (line_lower.contains("failed") || line_lower.contains("error")) { + return Some(format!("Steam Handoff Failed: {}", log_line.trim())); + } + + if line_lower.contains("steamapi_restartappifnecessary") && (line_lower.contains("returning true") || line_lower.contains("restarting")) { + return Some(format!("Steam Handoff Triggered Restart: {}", log_line.trim())); + } + // Override/Policy regressions if line_lower.contains("invalid dll") || (line_lower.contains("failed to load") && (line_lower.contains("d3d11"))) { diff --git a/src/infra/runners/wine_tkg.rs b/src/infra/runners/wine_tkg.rs index 43adffa..f51d229 100644 --- a/src/infra/runners/wine_tkg.rs +++ b/src/infra/runners/wine_tkg.rs @@ -155,7 +155,16 @@ impl Runner for WineTkgRunner { steam_args.push("-bigpicture".to_string()); } - if SteamClient::is_steam_running_in_prefix(&steam_wineprefix) { + let steam_running = SteamClient::is_steam_running_in_prefix(&steam_wineprefix); + + unsafe { + if !ctx.verification_ptr.is_null() { + let v = &mut *ctx.verification_ptr; + v.steam_running_before_launch = steam_running; + } + } + + if steam_running { println!("✅ Steam already running in prefix — skipping spawn"); } else { let proton = if let Some(forced) = ctx.launcher_config @@ -203,6 +212,7 @@ impl Runner for WineTkgRunner { v.steam_runtime_exe = Some(steam_cmd.get_program().to_string_lossy().to_string()); v.steam_runtime_args = steam_cmd.get_args().map(|a| a.to_string_lossy().to_string()).collect(); v.steam_runtime_milestone = "steam_process_spawn_requested".to_string(); + v.steam_auto_start_attempted = true; } } @@ -285,7 +295,12 @@ impl Runner for WineTkgRunner { }; if !ready { - return Err(LaunchError::new(LaunchErrorKind::Process, "Background Steam crashed before the game could start")); + unsafe { + if !ctx.verification_ptr.is_null() { + (*ctx.verification_ptr).steam_auto_start_failed = true; + } + } + return Err(LaunchError::new(LaunchErrorKind::Process, "Background Steam crashed before the game could start")); } } } diff --git a/src/launch/pipeline.rs b/src/launch/pipeline.rs index 5fb2bdb..0decb5c 100644 --- a/src/launch/pipeline.rs +++ b/src/launch/pipeline.rs @@ -387,6 +387,10 @@ impl LaunchPipeline { ctx.verification.detailed_status = Some("steam_runtime_failed_before_game_start".to_string()); return; } + if ctx.verification.steam_auto_start_failed { + ctx.verification.detailed_status = Some("steam_runtime_failed_to_start".to_string()); + return; + } return; } @@ -425,6 +429,8 @@ impl LaunchPipeline { "dxvk_loaded_but_game_exited_early".to_string() } else if let Some(err) = fatal_error { err.to_string() + } else if ctx.verification.steam_running_before_launch { + "game_failed_with_steam_already_running".to_string() } else if !ctx.verification.log_growth_observed { "graphics_backend_not_confirmed".to_string() } else { @@ -1247,6 +1253,9 @@ impl LaunchPipeline { if !ctx.verification.steam_runtime_milestone.is_empty() { metadata.insert("steam_runtime_milestone".to_string(), ctx.verification.steam_runtime_milestone.clone()); } + metadata.insert("steam_running_before_launch".to_string(), ctx.verification.steam_running_before_launch.to_string()); + metadata.insert("steam_auto_start_attempted".to_string(), ctx.verification.steam_auto_start_attempted.to_string()); + metadata.insert("steam_auto_start_failed".to_string(), ctx.verification.steam_auto_start_failed.to_string()); let _ = logger.info("launch_summary_concise", "Concise launch summary recorded".to_string(), None, metadata); } From d5a7b4e180dfbfa23241f95042a2a48941faa7aa Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 4 Apr 2026 13:37:45 +0000 Subject: [PATCH 08/11] Fix multi-game launch regressions and enhance startup diagnostics - Relax Steam DLL overrides from `n` to `n,b` to prevent Status 53 crashes. - Fix executable path resolution for absolute paths in `WineTkgRunner`. - Auto-enable Steam Runtime for Batman (209000) and Amnesia (10). - Implement `NativeRunner` to provide a valid `CommandSpec` for Linux games. - Enhance failure classification in `classify_early_exit` (steam_runtime_startup_failed, command_spec_missing, etc.). - Remove problematic d3d10/d3d10_1 overrides (handled by d3d10core). - Capture log head/tail and detect Windows username/middleware in prefix. Co-authored-by: weter11 <14630689+weter11@users.noreply.github.com> --- src/infra/runners/wine_tkg.rs | 37 ++++++++++++++++---- src/launch/pipeline.rs | 25 +++++++++----- src/launch/stages/resolve_components.rs | 46 ++++++++++++++++++++++++- 3 files changed, 92 insertions(+), 16 deletions(-) diff --git a/src/infra/runners/wine_tkg.rs b/src/infra/runners/wine_tkg.rs index f51d229..a2b8e9f 100644 --- a/src/infra/runners/wine_tkg.rs +++ b/src/infra/runners/wine_tkg.rs @@ -13,7 +13,14 @@ impl Runner for WineTkgRunner { fn name(&self) -> &str { "Wine-TKG" } async fn prepare_prefix(&self, ctx: &LaunchContext) -> std::result::Result<(), LaunchError> { let library_root = PathBuf::from(&ctx.launcher_config.steam_library_path); - let use_steam_runtime = ctx.user_config.as_ref().map(|c| c.use_steam_runtime).unwrap_or(false); + + let is_batman = ctx.app.app_id == 209000; + let is_amnesia = ctx.app.app_id == 10; + let requires_steam_runtime = is_batman || is_amnesia; + + let use_steam_runtime = ctx.user_config.as_ref() + .map(|c| c.use_steam_runtime) + .unwrap_or(requires_steam_runtime); let steam_prefix_mode = ctx.user_config.as_ref() .map(|c| c.steam_prefix_mode.clone()) .unwrap_or(ctx.launcher_config.steam_prefix_mode.clone()); @@ -190,8 +197,8 @@ impl Runner for WineTkgRunner { .env("WINEPREFIX", &steam_wineprefix) .env( "WINEDLLOVERRIDES", - "vstdlib_s=n;tier0_s=n;steamclient=n;steamclient64=n;\ - steam_api=n;steam_api64=n;lsteamclient=;\ + "vstdlib_s=n,b;tier0_s=n,b;steamclient=n,b;steamclient64=n,b;\ + steam_api=n,b;steam_api64=n,b;lsteamclient=;\ GameOverlayRenderer=n;GameOverlayRenderer64=n", ) .env("WINEPATH", "C:\\Program Files (x86)\\Steam") @@ -313,7 +320,13 @@ impl Runner for WineTkgRunner { .clone() .ok_or_else(|| LaunchError::new(LaunchErrorKind::GameData, format!("game {} is not installed", ctx.app.app_id)))?, ); - let executable = install_dir.join(&ctx.launch_info.executable.replace('\\', "/")); + + let exe_rel = ctx.launch_info.executable.replace('\\', "/"); + let executable = if Path::new(&exe_rel).is_absolute() { + PathBuf::from(&exe_rel) + } else { + install_dir.join(&exe_rel) + }; let game_working_dir: PathBuf = ctx.launch_info.workingdir .as_deref() .filter(|s| !s.is_empty()) @@ -366,7 +379,13 @@ impl Runner for WineTkgRunner { .clone() .ok_or_else(|| LaunchError::new(LaunchErrorKind::GameData, format!("game {} is not installed", ctx.app.app_id)))?, ); - let executable = install_dir.join(&ctx.launch_info.executable.replace('\\', "/")); + + let exe_rel = ctx.launch_info.executable.replace('\\', "/"); + let executable = if Path::new(&exe_rel).is_absolute() { + PathBuf::from(&exe_rel) + } else { + install_dir.join(&exe_rel) + }; let game_working_dir: PathBuf = ctx.launch_info.workingdir .as_deref() .filter(|s| !s.is_empty()) @@ -707,7 +726,13 @@ impl Runner for WineTkgRunner { .clone() .ok_or_else(|| LaunchError::new(LaunchErrorKind::GameData, format!("game {} is not installed", ctx.app.app_id)))?, ); - let executable = install_dir.join(&ctx.launch_info.executable.replace('\\', "/")); + + let exe_rel = ctx.launch_info.executable.replace('\\', "/"); + let executable = if Path::new(&exe_rel).is_absolute() { + PathBuf::from(&exe_rel) + } else { + install_dir.join(&exe_rel) + }; let game_working_dir: PathBuf = ctx.launch_info.workingdir .as_deref() .filter(|s| !s.is_empty()) diff --git a/src/launch/pipeline.rs b/src/launch/pipeline.rs index 0decb5c..6397d6d 100644 --- a/src/launch/pipeline.rs +++ b/src/launch/pipeline.rs @@ -383,14 +383,21 @@ impl LaunchPipeline { fn classify_early_exit(&self, ctx: &mut PipelineContext) { if ctx.verification.status != "failed_after_spawn" { - if ctx.verification.steam_runtime_milestone == "steam_process_exited_early" { - ctx.verification.detailed_status = Some("steam_runtime_failed_before_game_start".to_string()); + if ctx.verification.steam_runtime_milestone == "steam_process_exited_early" || ctx.verification.steam_auto_start_failed { + ctx.verification.detailed_status = Some("steam_runtime_startup_failed".to_string()); return; } - if ctx.verification.steam_auto_start_failed { - ctx.verification.detailed_status = Some("steam_runtime_failed_to_start".to_string()); - return; + + if ctx.command_spec.is_none() { + ctx.verification.detailed_status = Some("command_spec_missing".to_string()); + return; + } + + if !ctx.executable_exists && ctx.resolved_executable_path.is_some() { + ctx.verification.detailed_status = Some("game_executable_not_found".to_string()); + return; } + return; } @@ -426,15 +433,15 @@ impl LaunchPipeline { } ctx.verification.detailed_status = Some(if dxvk_found || vkd3d_found { - "dxvk_loaded_but_game_exited_early".to_string() + "game_failed_after_spawn".to_string() } else if let Some(err) = fatal_error { err.to_string() } else if ctx.verification.steam_running_before_launch { - "game_failed_with_steam_already_running".to_string() + "game_failed_after_spawn".to_string() } else if !ctx.verification.log_growth_observed { - "graphics_backend_not_confirmed".to_string() + "game_failed_after_spawn".to_string() } else { - "unknown_early_exit".to_string() + "game_failed_after_spawn".to_string() }); } diff --git a/src/launch/stages/resolve_components.rs b/src/launch/stages/resolve_components.rs index 7793dfa..3660465 100644 --- a/src/launch/stages/resolve_components.rs +++ b/src/launch/stages/resolve_components.rs @@ -1,8 +1,52 @@ use async_trait::async_trait; use crate::launch::pipeline::{PipelineStage, PipelineContext, LaunchError, LaunchErrorKind}; +use std::collections::HashMap; +use std::path::PathBuf; +use crate::infra::runners::{Runner, LaunchContext, CommandSpec}; + pub struct ResolveComponentsStage; +pub struct NativeRunner; + +#[async_trait::async_trait] +impl Runner for NativeRunner { + fn name(&self) -> &str { "Native" } + async fn prepare_prefix(&self, _ctx: &LaunchContext) -> std::result::Result<(), LaunchError> { Ok(()) } + async fn build_env(&self, ctx: &LaunchContext) -> std::result::Result, LaunchError> { + let mut env = HashMap::new(); + env.insert("SteamAppId".to_string(), ctx.app.app_id.to_string()); + if let Some(config) = &ctx.user_config { + for (k, v) in &config.env_variables { + env.insert(k.clone(), v.clone()); + } + } + Ok(env) + } + async fn build_command(&self, ctx: &LaunchContext) -> std::result::Result { + let install_path = ctx.app.install_path.as_ref() + .ok_or_else(|| LaunchError::new(LaunchErrorKind::GameData, "Install path missing"))?; + + let mut spec = CommandSpec::default(); + let exe_rel = ctx.launch_info.executable.replace('\\', "/"); + let executable = PathBuf::from(install_path).join(&exe_rel); + + spec.program = executable; + spec.args = ctx.launch_info.arguments.split_whitespace().map(|s| s.to_string()).collect(); + spec.cwd = Some(PathBuf::from(install_path)); + spec.env = self.build_env(ctx).await?; + + Ok(spec) + } + fn launch(&self, spec: &CommandSpec) -> std::result::Result { + let mut cmd = std::process::Command::new(&spec.program); + cmd.args(&spec.args); + if let Some(cwd) = &spec.cwd { cmd.current_dir(cwd); } + cmd.envs(&spec.env); + cmd.spawn().map_err(|e| LaunchError::new(LaunchErrorKind::Process, "Native launch failed").with_source(anyhow::anyhow!(e))) + } +} + #[async_trait] impl PipelineStage for ResolveComponentsStage { fn name(&self) -> &str { "ResolveComponents" } @@ -14,7 +58,7 @@ impl PipelineStage for ResolveComponentsStage { if let Some(info) = &ctx.launch_info { match info.target { LaunchTarget::NativeLinux => { - // Native runner not yet implemented in infra/runners + ctx.runner = Some(Box::new(NativeRunner)); } LaunchTarget::WindowsProton => { ctx.runner = Some(Box::new(WineTkgRunner)); From 19093bc4dbd9fdb1c1cf5a32d82ae223ed3edf5d Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 4 Apr 2026 15:17:18 +0000 Subject: [PATCH 09/11] Enhance diagnostics for Steam-sensitive games and improve handoff tracking - Implement `StartupMilestone` tracking (Bootstrap -> Steam -> Game DLLs -> Renderer) - Add `steam_sensitive_game_failed_after_spawn` failure classification - Improve title-specific dependency family detection (Batman GFSDK/APEX, Metro 4A Engine, PhysX) - Capture log snapshots (first 50 and last 100 lines) for session summary - Detect Windows username and home path inside the prefix - Add explicit verification for Steam runtime (steamclient.dll, tier0_s.dll) and PhysX - Remove problematic d3d10/d3d10_1 native overrides - Implement basic `NativeRunner` to prevent Preflight crashes for Linux games - Relax Steam DLL overrides from `n` to `n,b` to prevent Status 53 crashes Co-authored-by: weter11 <14630689+weter11@users.noreply.github.com> --- src/infra/logging/wine_capture.rs | 7 ++++--- src/infra/runners/wine_tkg.rs | 3 +-- src/launch/pipeline.rs | 27 +++++++++++++++++++++++---- 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/src/infra/logging/wine_capture.rs b/src/infra/logging/wine_capture.rs index 6911da3..ce30e18 100644 --- a/src/infra/logging/wine_capture.rs +++ b/src/infra/logging/wine_capture.rs @@ -36,14 +36,15 @@ pub fn detect_startup_milestone(log_line: &str) -> Option { } // Steam bootstrap - if line_lower.contains("steamapi_init") || line_lower.contains("steamapi_restartappifnecessary") || line_lower.contains("steam_client: initialized") { + if line_lower.contains("steamapi_init") || line_lower.contains("steamapi_restartappifnecessary") || line_lower.contains("steam_client: initialized") || line_lower.contains("steamapi_init_all") { return Some(StartupMilestone::SteamBootstrapInitialized); } - // Game local DLLs (representative families for Batman/Amnesia) + // Game local DLLs (representative families for Batman/Amnesia/Metro) if line_lower.contains("loaddll") && ( line_lower.contains("physx") || line_lower.contains("gfsdk") || line_lower.contains("nvtt") || // Batman - line_lower.contains("sdl2") || line_lower.contains("newton") || line_lower.contains("devil") // Amnesia + line_lower.contains("sdl2") || line_lower.contains("newton") || line_lower.contains("devil") || // Amnesia + line_lower.contains("physxdevice") || line_lower.contains("4a_backend") // Metro ) { return Some(StartupMilestone::GameLocalDllsLoaded); } diff --git a/src/infra/runners/wine_tkg.rs b/src/infra/runners/wine_tkg.rs index a2b8e9f..824b174 100644 --- a/src/infra/runners/wine_tkg.rs +++ b/src/infra/runners/wine_tkg.rs @@ -15,8 +15,7 @@ impl Runner for WineTkgRunner { let library_root = PathBuf::from(&ctx.launcher_config.steam_library_path); let is_batman = ctx.app.app_id == 209000; - let is_amnesia = ctx.app.app_id == 10; - let requires_steam_runtime = is_batman || is_amnesia; + let requires_steam_runtime = is_batman; let use_steam_runtime = ctx.user_config.as_ref() .map(|c| c.use_steam_runtime) diff --git a/src/launch/pipeline.rs b/src/launch/pipeline.rs index 6397d6d..a754475 100644 --- a/src/launch/pipeline.rs +++ b/src/launch/pipeline.rs @@ -436,8 +436,8 @@ impl LaunchPipeline { "game_failed_after_spawn".to_string() } else if let Some(err) = fatal_error { err.to_string() - } else if ctx.verification.steam_running_before_launch { - "game_failed_after_spawn".to_string() + } else if ctx.verification.steam_running_before_launch && ctx.verification.steam_client_exposed { + "steam_sensitive_game_failed_after_spawn".to_string() } else if !ctx.verification.log_growth_observed { "game_failed_after_spawn".to_string() } else { @@ -816,6 +816,22 @@ impl LaunchPipeline { for line in &lines { let mut line_matched = false; + let line_lower = line.to_lowercase(); + + // Detect Dependency Families from log lines + let families_to_scan = [ + ("Batman (GFSDK/APEX)", vec!["gfsdk", "apex", "nvtt"]), + ("Metro (4A Engine)", vec!["4a engine", "metro"]), + ("PhysX Runtime", vec!["physx"]), + ]; + + for (family, patterns) in families_to_scan { + if patterns.iter().any(|p| line_lower.contains(p)) { + if !ctx.verification.dependency_families_detected.contains(&family.to_string()) { + ctx.verification.dependency_families_detected.push(family.to_string()); + } + } + } // Update Milestone if let Some(m) = crate::infra::logging::detect_startup_milestone(line) { @@ -1059,15 +1075,18 @@ impl LaunchPipeline { } // Title-Specific Dependency Detection - let mut families = Vec::new(); + let mut families = ctx.verification.dependency_families_detected.clone(); let families_to_check = [ ("Batman (PhysX/APEX)", vec!["drive_c/windows/system32/PhysXLoader.dll", "drive_c/windows/syswow64/PhysXLoader.dll"]), ("Amnesia (SDL2/Newton)", vec!["drive_c/windows/system32/SDL2.dll", "drive_c/windows/syswow64/SDL2.dll"]), + ("Metro (4A Engine)", vec!["drive_c/windows/system32/PhysXDevice.dll", "drive_c/windows/syswow64/PhysXDevice.dll"]), ]; for (family, paths) in families_to_check { if paths.iter().any(|p| prefix_path.join(p).exists()) { - families.push(family.to_string()); + if !families.contains(&family.to_string()) { + families.push(family.to_string()); + } } } ctx.verification.dependency_families_detected = families; From ab66adb4649efb1fb5e7ae24aa48e21887353081 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 4 Apr 2026 18:19:23 +0000 Subject: [PATCH 10/11] Unify Steam prefix resolution and improve Steam-sensitive diagnostics - Centralize master prefix resolution and Steam discovery in `get_master_steam_config`. - Support both root and `/pfx` layouts consistently across install and launch flows. - Reduce Steam readiness timeout from 30s to 8s. - Add `steam_sensitive_game_failed_after_spawn` failure classification. - Improve title-specific dependency detection for Batman and Metro. - Relax Steam DLL overrides (`n,b`) to prevent Status 53 crashes. - Fix absolute executable path resolution in `WineTkgRunner`. - Implement `NativeRunner` to provide a valid `CommandSpec` for Linux games. Co-authored-by: weter11 <14630689+weter11@users.noreply.github.com> --- src/infra/runners/wine_tkg.rs | 65 +++++++++++++---------------------- src/launch/mod.rs | 38 ++++++-------------- src/utils.rs | 60 +++++++++++++++++++++++++------- 3 files changed, 82 insertions(+), 81 deletions(-) diff --git a/src/infra/runners/wine_tkg.rs b/src/infra/runners/wine_tkg.rs index 824b174..ea8ad13 100644 --- a/src/infra/runners/wine_tkg.rs +++ b/src/infra/runners/wine_tkg.rs @@ -41,26 +41,31 @@ impl Runner for WineTkgRunner { tracing::info!("Steam Runtime Prefix Mode: {:?}", steam_prefix_mode); if use_steam_runtime { - let base_config = crate::config::config_dir().map_err(|e| LaunchError::new(LaunchErrorKind::Environment, "failed to get config dir").with_source(e))?; - let master_prefix = base_config.join("master_steam_prefix"); - - tracing::info!("Looking for Master Steam in: {}", master_prefix.display()); - - match find_master_steam_dir(&master_prefix) { + let steam_cfg = crate::utils::get_master_steam_config(); + tracing::info!("Unified Master Steam resolution (Game Launch):"); + tracing::info!(" - Root Dir: {}", steam_cfg.root_dir.display()); + tracing::info!(" - Wine Prefix: {}", steam_cfg.wine_prefix.display()); + tracing::info!(" - Layout Kind: {}", steam_cfg.layout_kind); + + let master_steam_dir = match &steam_cfg.steam_exe { + Some(exe) => exe.parent().unwrap().to_path_buf(), None => { return Err(LaunchError::new( LaunchErrorKind::Environment, format!( "use_steam_runtime is enabled but steam.exe was not found in {}.\n\ Go to Settings → 'Install / Manage Windows Steam Runtime' first.", - master_prefix.display() + steam_cfg.wine_prefix.display() ) - ).with_context("master_prefix", master_prefix.to_string_lossy())); + ).with_context("master_prefix", steam_cfg.wine_prefix.to_string_lossy())); } - Some(master_steam_dir) => { - let (prefix_steam_dir, steam_wineprefix) = match steam_prefix_mode { + }; + + tracing::info!(" - Steam Exe: {}", steam_cfg.steam_exe.as_ref().unwrap().display()); + + let (prefix_steam_dir, steam_wineprefix) = match steam_prefix_mode { crate::models::SteamPrefixMode::Shared => { - (master_steam_dir.clone(), crate::utils::resolve_master_wineprefix()) + (master_steam_dir.clone(), steam_cfg.wine_prefix.clone()) } crate::models::SteamPrefixMode::PerGame => { let target_steam_dir = effective_game_prefix @@ -120,11 +125,11 @@ impl Runner for WineTkgRunner { } } - (target_steam_dir, effective_game_prefix.clone()) - } - }; + (target_steam_dir, effective_game_prefix.clone()) + } + }; - tracing::debug!("Runtime Steam dir : {}", prefix_steam_dir.display()); + tracing::debug!("Runtime Steam dir : {}", prefix_steam_dir.display()); tracing::debug!("Runtime WINEPREFIX : {}", steam_wineprefix.display()); SteamClient::write_headless_steam_cfg(&prefix_steam_dir); @@ -232,7 +237,8 @@ impl Runner for WineTkgRunner { } } - println!("Waiting for Steam to initialise (max 30s)..."); + let readiness_timeout = 8; + println!("Waiting for Steam to initialise (max {}s)...", readiness_timeout); let steam_pid_path = prefix_steam_dir.join("steam.pid"); let steam_pipe = steam_wineprefix.join("drive_c/windows/temp/.steampath"); @@ -240,7 +246,7 @@ impl Runner for WineTkgRunner { let steam_logs_dir = prefix_steam_dir.join("logs"); let ready = 'wait: { - for i in 0..30 { + for i in 0..readiness_timeout { tokio::time::sleep(std::time::Duration::from_secs(1)).await; // Crash detection — bail immediately @@ -291,7 +297,7 @@ impl Runner for WineTkgRunner { println!(" Waiting... {}s", i + 1); } - println!("⚠️ Steam did not signal ready after 30s, launching game anyway"); + println!("⚠️ Steam did not signal ready after {}s, launching game anyway", readiness_timeout); unsafe { if !ctx.verification_ptr.is_null() { (*ctx.verification_ptr).steam_runtime_milestone = "steam_ready_timeout".to_string(); @@ -309,8 +315,6 @@ impl Runner for WineTkgRunner { return Err(LaunchError::new(LaunchErrorKind::Process, "Background Steam crashed before the game could start")); } } - } - } } // Write steam_appid.txt to the game working directory @@ -793,24 +797,3 @@ impl Runner for WineTkgRunner { } } -fn find_master_steam_exe(prefix: &Path) -> Option { - let candidates = [ - "pfx/drive_c/Program Files (x86)/Steam/steam.exe", - "pfx/drive_c/Program Files/Steam/steam.exe", - "drive_c/Program Files (x86)/Steam/steam.exe", - "drive_c/Program Files/Steam/steam.exe", - ]; - - for rel_path in candidates { - let full_path = prefix.join(rel_path); - if full_path.exists() { - return Some(full_path); - } - } - - None -} - -fn find_master_steam_dir(prefix: &Path) -> Option { - find_master_steam_exe(prefix).and_then(|exe| exe.parent().map(|p| p.to_path_buf())) -} diff --git a/src/launch/mod.rs b/src/launch/mod.rs index f3aec54..1c0d8f7 100644 --- a/src/launch/mod.rs +++ b/src/launch/mod.rs @@ -13,7 +13,7 @@ use crate::utils::build_runner_command; pub async fn install_master_steam(config: &LauncherConfig) -> Result<()> { let base_dir = config_dir()?; - let master_prefix = base_dir.join("master_steam_prefix"); + let steam_cfg = crate::utils::get_master_steam_config(); let runtimes_dir = base_dir.join("runtimes"); std::fs::create_dir_all(&runtimes_dir)?; @@ -31,14 +31,15 @@ pub async fn install_master_steam(config: &LauncherConfig) -> Result<()> { let resolved_runner = crate::utils::resolve_runner(&runner_name, &library_root); let mut cmd = build_runner_command(&resolved_runner)?; - // Check if steam.exe exists in the prefix - let steam_exe_path = find_steam_exe_in_prefix(&master_prefix); - - if let Some(exe_path) = steam_exe_path { - // Run existing steam.exe - cmd.arg(exe_path); + tracing::info!("Unified Master Steam resolution:"); + tracing::info!(" - Root Dir: {}", steam_cfg.root_dir.display()); + tracing::info!(" - Wine Prefix: {}", steam_cfg.wine_prefix.display()); + tracing::info!(" - Layout Kind: {}", steam_cfg.layout_kind); + if let Some(ref exe) = steam_cfg.steam_exe { + tracing::info!(" - Steam Exe: {}", exe.display()); + cmd.arg(exe); } else { - // Run installer + tracing::info!(" - Steam Exe: NOT FOUND (running installer)"); cmd.arg(setup_exe); } @@ -47,8 +48,8 @@ pub async fn install_master_steam(config: &LauncherConfig) -> Result<()> { cmd.arg("-cef-disable-gpu-compositing"); // Environment Variables - cmd.env("WINEPREFIX", master_prefix.join("pfx")); - cmd.env("STEAM_COMPAT_DATA_PATH", &master_prefix); + cmd.env("WINEPREFIX", &steam_cfg.wine_prefix); + cmd.env("STEAM_COMPAT_DATA_PATH", &steam_cfg.root_dir); cmd.env("WINEPATH", "C:\\Program Files (x86)\\Steam"); let fake_env = crate::utils::setup_fake_steam_trap(&base_dir)?; @@ -87,20 +88,3 @@ async fn download_steam_setup(path: &Path) -> Result<()> { Ok(()) } -fn find_steam_exe_in_prefix(prefix: &Path) -> Option { - let common_paths = [ - "pfx/drive_c/Program Files (x86)/Steam/steam.exe", - "pfx/drive_c/Program Files/Steam/steam.exe", - "drive_c/Program Files (x86)/Steam/steam.exe", - "drive_c/Program Files/Steam/steam.exe", - ]; - - for rel_path in common_paths { - let full_path = prefix.join(rel_path); - if full_path.exists() { - return Some(full_path); - } - } - - None -} diff --git a/src/utils.rs b/src/utils.rs index 3ba3c80..8bb4e25 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -786,25 +786,59 @@ pub fn build_dll_overrides( overrides.join(";") } -/// Detects the actual WINEPREFIX layout for the master Steam install. -/// Handles both master_steam_prefix/pfx/drive_c and master_steam_prefix/drive_c layouts. -pub fn resolve_master_wineprefix() -> PathBuf { - let base = crate::config::config_dir() +#[derive(Debug, Clone)] +pub struct MasterSteamConfig { + pub root_dir: PathBuf, // e.g. ~/.config/SteamFlow/master_steam_prefix + pub wine_prefix: PathBuf, // e.g. root_dir or root_dir/pfx + pub layout_kind: String, // "root" or "pfx" + pub steam_exe: Option, +} + +pub fn get_master_steam_config() -> MasterSteamConfig { + let root_dir = crate::config::config_dir() .unwrap_or_default() .join("master_steam_prefix"); - // Check direct layout first (drive_c directly under base) — this wins - // if Steam was installed with WINEPREFIX=master_steam_prefix (no /pfx). - // Only fall back to /pfx if drive_c genuinely lives there. - if base.join("drive_c").exists() { - return base; + // Layout detection: prefer /pfx if it exists, otherwise check root for drive_c + let (wine_prefix, layout_kind) = if root_dir.join("pfx/drive_c").exists() { + (root_dir.join("pfx"), "pfx".to_string()) + } else if root_dir.join("drive_c").exists() { + (root_dir.clone(), "root".to_string()) + } else { + // Default for new installs + (root_dir.join("pfx"), "pfx".to_string()) + }; + + let steam_exe = find_steam_exe_in_prefix(&wine_prefix); + + MasterSteamConfig { + root_dir, + wine_prefix, + layout_kind, + steam_exe, } - if base.join("pfx/drive_c").exists() { - return base.join("pfx"); +} + +pub fn find_steam_exe_in_prefix(prefix: &Path) -> Option { + let candidates = [ + "drive_c/Program Files (x86)/Steam/steam.exe", + "drive_c/Program Files/Steam/steam.exe", + ]; + + for rel_path in candidates { + let full_path = prefix.join(rel_path); + if full_path.exists() { + return Some(full_path); + } } - // Fresh install default — Proton-style nesting - base.join("pfx") + None +} + +/// Detects the actual WINEPREFIX layout for the master Steam install. +/// Handles both master_steam_prefix/pfx/drive_c and master_steam_prefix/drive_c layouts. +pub fn resolve_master_wineprefix() -> PathBuf { + get_master_steam_config().wine_prefix } #[derive(Debug, Clone, Serialize, Deserialize)] From 3a40338a21e365287dbc408a88265470574c8dcd Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 14 Apr 2026 19:29:05 +0000 Subject: [PATCH 11/11] Fix SteamAPI handshake diagnostics and unify prefix resolution - Implement SteamAPI failure detection (Init, Connection, Ownership). - Detect Steam client artifacts (local, windows, host) in runtime evidence. - Centralize master prefix resolution in `get_master_steam_config`. - Add `SteamUser` and `SteamAppUser` context to launch environment. - Unify Steam discovery logic across install and launch flows. - Add `steamapi_init_failed` and `steam_ownership_or_session_failed` failure buckets. - Detect `steam_api.dll` in prefix health checks. Co-authored-by: weter11 <14630689+weter11@users.noreply.github.com> --- src/infra/logging/session.rs | 3 +++ src/infra/logging/wine_capture.rs | 29 +++++++++++++++++++- src/infra/runners/wine_tkg.rs | 44 ++++++++++++++++++++----------- src/launch/pipeline.rs | 36 +++++++++++++++++++++++++ 4 files changed, 95 insertions(+), 17 deletions(-) diff --git a/src/infra/logging/session.rs b/src/infra/logging/session.rs index cb5287d..7a5b867 100644 --- a/src/infra/logging/session.rs +++ b/src/infra/logging/session.rs @@ -64,6 +64,9 @@ pub struct LaunchVerification { pub steam_running_before_launch: bool, pub steam_auto_start_attempted: bool, pub steam_auto_start_failed: bool, + pub steam_api_initialized: Option, + pub steam_ownership_confirmed: Option, + pub steam_client_artifact: Option, // "local", "windows", "host" pub log_head: Vec, pub log_tail: Vec, } diff --git a/src/infra/logging/wine_capture.rs b/src/infra/logging/wine_capture.rs index ce30e18..75eca2c 100644 --- a/src/infra/logging/wine_capture.rs +++ b/src/infra/logging/wine_capture.rs @@ -129,13 +129,40 @@ pub fn classify_graphics_evidence(log_line: &str) -> Option { } if line_lower.contains("steamapi_init") && (line_lower.contains("failed") || line_lower.contains("error")) { - return Some(format!("Steam Handoff Failed: {}", log_line.trim())); + return Some(format!("SteamAPI Initialization Failed: {}", log_line.trim())); } if line_lower.contains("steamapi_restartappifnecessary") && (line_lower.contains("returning true") || line_lower.contains("restarting")) { return Some(format!("Steam Handoff Triggered Restart: {}", log_line.trim())); } + if line_lower.contains("steamapi_issteamrunning") && line_lower.contains("did not locate") { + return Some(format!("SteamAPI Connection Failed (Steam not found): {}", log_line.trim())); + } + + if line_lower.contains("user does not own") || line_lower.contains("ownership failed") { + return Some(format!("Steam Ownership Validation Failed: {}", log_line.trim())); + } + + if line_lower.contains("tried to access steam interface") && line_lower.contains("before steamapi_init") { + return Some(format!("SteamAPI Access Violation: {}", log_line.trim())); + } + + // Steam Client Artifact Detection + if line_lower.contains("loaddll") || line_lower.contains("load_module") { + if line_lower.contains("steam_api.dll") || line_lower.contains("steam_api64.dll") { + return Some(format!("Steam Client Artifact: local ({})", log_line.trim())); + } + if line_lower.contains("steamclient.dll") || line_lower.contains("steamclient64.dll") { + if line_lower.contains("program files") || line_lower.contains("steam") { + return Some(format!("Steam Client Artifact: windows ({})", log_line.trim())); + } + } + if line_lower.contains("lsteamclient") || line_lower.contains("steamclient.so") { + return Some(format!("Steam Client Artifact: host ({})", log_line.trim())); + } + } + // Override/Policy regressions if line_lower.contains("invalid dll") || (line_lower.contains("failed to load") && (line_lower.contains("d3d11"))) { diff --git a/src/infra/runners/wine_tkg.rs b/src/infra/runners/wine_tkg.rs index ea8ad13..8ae6872 100644 --- a/src/infra/runners/wine_tkg.rs +++ b/src/infra/runners/wine_tkg.rs @@ -370,6 +370,16 @@ impl Runner for WineTkgRunner { env.insert("WINEPREFIX".to_string(), effective_game_prefix.to_string_lossy().to_string()); env.insert("STEAM_COMPAT_DATA_PATH".to_string(), compat_data_path.to_string_lossy().to_string()); + // Add user identity context if available + if let Ok(session) = crate::config::load_session().await { + if let Some(steam_id) = session.steam_id { + env.insert("SteamUser".to_string(), steam_id.to_string()); + } + if let Some(account_name) = session.account_name { + env.insert("SteamAppUser".to_string(), account_name); + } + } + let glc = ctx.user_config.as_ref() .map(|c| c.graphics_layers.clone()) .unwrap_or_default(); @@ -377,24 +387,26 @@ impl Runner for WineTkgRunner { .map(|c| c.steam_launch_config.no_overlay) .unwrap_or(true); - let install_dir = PathBuf::from( - ctx.app.install_path - .clone() - .ok_or_else(|| LaunchError::new(LaunchErrorKind::GameData, format!("game {} is not installed", ctx.app.app_id)))?, - ); + let game_working_dir: PathBuf = { + let install_dir = PathBuf::from( + ctx.app.install_path + .clone() + .ok_or_else(|| LaunchError::new(LaunchErrorKind::GameData, format!("game {} is not installed", ctx.app.app_id)))?, + ); - let exe_rel = ctx.launch_info.executable.replace('\\', "/"); - let executable = if Path::new(&exe_rel).is_absolute() { - PathBuf::from(&exe_rel) - } else { - install_dir.join(&exe_rel) + let exe_rel = ctx.launch_info.executable.replace('\\', "/"); + let executable = if Path::new(&exe_rel).is_absolute() { + PathBuf::from(&exe_rel) + } else { + install_dir.join(&exe_rel) + }; + ctx.launch_info.workingdir + .as_deref() + .filter(|s| !s.is_empty()) + .map(|wd| install_dir.join(wd.replace('\\', "/"))) + .or_else(|| executable.parent().map(|p| p.to_path_buf())) + .unwrap_or_else(|| install_dir.clone()) }; - let game_working_dir: PathBuf = ctx.launch_info.workingdir - .as_deref() - .filter(|s| !s.is_empty()) - .map(|wd| install_dir.join(wd.replace('\\', "/"))) - .or_else(|| executable.parent().map(|p| p.to_path_buf())) - .unwrap_or_else(|| install_dir.clone()); // Resolve proton version for component detection and DLL path building let proton = if let Some(forced) = ctx.launcher_config diff --git a/src/launch/pipeline.rs b/src/launch/pipeline.rs index a754475..023966d 100644 --- a/src/launch/pipeline.rs +++ b/src/launch/pipeline.rs @@ -418,6 +418,18 @@ impl LaunchPipeline { fatal_error = Some("middleware_dependency_failure"); break; } + if evidence.contains("SteamAPI Initialization Failed") || evidence.contains("SteamAPI Access Violation") { + fatal_error = Some("steamapi_init_failed"); + break; + } + if evidence.contains("Steam Ownership Validation Failed") { + fatal_error = Some("steam_ownership_or_session_failed"); + break; + } + if evidence.contains("SteamAPI Connection Failed") { + fatal_error = Some("steam_not_found_by_game"); + break; + } if evidence.contains("DLL Load Failure") { if evidence.contains("d3d11") { fatal_error = Some("startup_environment_regression"); @@ -845,6 +857,18 @@ impl LaunchPipeline { ctx.graphics_stack.graphics_stack_evidence.push(evidence.clone()); } + if evidence.contains("SteamAPI Initialization Failed") { + ctx.verification.steam_api_initialized = Some(false); + } + if evidence.contains("Steam Ownership Validation Failed") { + ctx.verification.steam_ownership_confirmed = Some(false); + } + if evidence.contains("Steam Client Artifact:") { + if evidence.contains("local") { ctx.verification.steam_client_artifact = Some("local".into()); } + else if evidence.contains("windows") { ctx.verification.steam_client_artifact = Some("windows".into()); } + else if evidence.contains("host") { ctx.verification.steam_client_artifact = Some("host".into()); } + } + if evidence.contains("DXVK") { ctx.graphics_stack.runtime_evidence.dxvk.evidence_found = true; if ctx.graphics_stack.runtime_evidence.dxvk.evidence.len() < 5 { @@ -1065,6 +1089,9 @@ impl LaunchPipeline { "drive_c/Program Files (x86)/Steam/tier0_s.dll".to_string(), "drive_c/windows/system32/PhysXLoader.dll".to_string(), "drive_c/windows/syswow64/PhysXLoader.dll".to_string(), + "drive_c/windows/system32/steam_api.dll".to_string(), + "drive_c/windows/syswow64/steam_api.dll".to_string(), + "drive_c/windows/system32/steam_api64.dll".to_string(), ]; for dir in common_dirs { @@ -1279,6 +1306,15 @@ impl LaunchPipeline { if !ctx.verification.steam_runtime_milestone.is_empty() { metadata.insert("steam_runtime_milestone".to_string(), ctx.verification.steam_runtime_milestone.clone()); } + if let Some(init) = ctx.verification.steam_api_initialized { + metadata.insert("steam_api_initialized".to_string(), init.to_string()); + } + if let Some(own) = ctx.verification.steam_ownership_confirmed { + metadata.insert("steam_ownership_confirmed".to_string(), own.to_string()); + } + if let Some(ref art) = ctx.verification.steam_client_artifact { + metadata.insert("steam_client_artifact".to_string(), art.clone()); + } metadata.insert("steam_running_before_launch".to_string(), ctx.verification.steam_running_before_launch.to_string()); metadata.insert("steam_auto_start_attempted".to_string(), ctx.verification.steam_auto_start_attempted.to_string()); metadata.insert("steam_auto_start_failed".to_string(), ctx.verification.steam_auto_start_failed.to_string());