From 78dbae2b961ccfefb0189f26739e182def510853 Mon Sep 17 00:00:00 2001 From: mescon <5875228+mescon@users.noreply.github.com> Date: Thu, 24 Sep 2026 15:13:37 +0200 Subject: [PATCH 1/2] tf: an experimental capture route for the game's own TrueForce Logitech's library streams 1,000 packets a second through Proton's raw HID, and the wheel answers each one; every packet crosses winedevice and the single-threaded wineserver, which on some setups stalls AC EVO and ACC (#74, #121). Measured in isolation: the raw joystick path costs the same as the normal one, the library's write-and-echo stream is the load. games.conf `tfroute=capture` keeps raw HID off so the library never opens the wheel, and carries the game's own TrueForce through the SDK proxy to logi-tf-sim instead, which streams it natively with the driver's force merged in. A captured. session marker tells the daemon to synthesise nothing for that game, so the wheel gets the game's haptics or silence. The launcher warns when the proxy is missing from the prefix. Experimental until felt against the default route. --- CHANGELOG.md | 18 +++ docs/LAUNCH_OPTIONS.md | 1 + tools/logi-launch.sh | 57 ++++++++- .../crates/logi-tf-sim/src/daemon.rs | 118 ++++++++++++++++-- .../crates/logi-tf-sim/src/native_session.rs | 55 +++++++- 5 files changed, 232 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 251310b..3349ad1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,24 @@ the contract is "it works on RS50 and G Pro as listed here". ## Unreleased +**An experimental route for the game's own TrueForce that skips Wine's +device path.** Assetto Corsa EVO and ACC stutter under `logi-launch` on +some direct-drive setups ([#74](../../issues/74), [#121](../../issues/121)). +A USB capture showed the wire clean, and measured in isolation the cause is +Logitech's library driving the wheel through Proton's raw HID: 1,000 +packets a second, each answered by the wheel, every one crossing Wine's +device process and wineserver, which roughly doubles wineserver's load. +The raw joystick path costs the same as the normal one and is not it. A +new `tfroute=capture` games.conf key keeps raw HID off, so the library +never opens the wheel, and carries the game's own TrueForce another way: +the SDK proxy copies the samples the game hands the library to +`logi-tf-sim`, which streams them natively with the driver's force merged +in. Force feedback reaches the driver the normal way. A new session marker +tells the daemon this game's TrueForce arrives captured on a direct-drive +wheel and that it must synthesise nothing, so what the wheel gets is the +game's own haptics or silence. Experimental until it has been felt against +the default route. + **The DiRT Rally 2.0 recipe is written down, end to end.** With the proxy turned on (`690790 ffb=proxy`), the telemetry set to `extradata="3"`, and a one-line `device_defines.xml` entry that gives the stand-in wheel an diff --git a/docs/LAUNCH_OPTIONS.md b/docs/LAUNCH_OPTIONS.md index 5b12e6a..1a52713 100644 --- a/docs/LAUNCH_OPTIONS.md +++ b/docs/LAUNCH_OPTIONS.md @@ -534,6 +534,7 @@ built-in plan asks for). To force something off, state it: `texture=none`, | `tfsim` | `1`, `0` | run `logi-tf-sim`. Set `0` for a game whose own TrueForce already reaches your wheel. Asking for it alongside `texture=merge` is a combination only a hand-written line can produce, and it works: the two read the same telemetry port, so whichever holds it forwards to the other and both are fed (see "One socket gets the datagrams, every reader gets the telemetry" in [SHARED_MEMORY_RELAY.md](SHARED_MEMORY_RELAY.md)) | | `texture` | `merge`, `none` | mix the driver's engine-note texture into the game's own TrueForce on the wheel. `merge` makes `logi-launch` stage the dinput8 escape proxy into the game's directory, start `logi-rpm-bridge` and switch `wheel_tf_merge` on, undoing all of it when the game exits. The same chain also lights the rev strip from the game's own telemetry: the proxy relays live rpm, first-shift-light rpm and redline, and `logi-rpm-bridge` drives `wheel_rev_level` with them (full bar by default, `LOGI_REV_MODE=shift` for the dash band). Only does anything for a direct-drive wheel in an SDK title with the TrueForce files installed | | `revleds` | `bar`, `shift` | how `logi-rpm-bridge` maps the rev strip while `texture=merge` drives it: `bar` (the default) lights LED 1 as soon as the engine turns and all 10 at the limiter, `shift` is the dash band (dark below the car's first shift light, level 1 exactly there). The apps persist this choice in `~/.config/logi-wheel/launch.conf` and it shows on a merge title's Setup card; a games.conf line overrides it per game like any other key | +| `tfroute` | `capture`, `sdk` | how a game's own TrueForce reaches a direct-drive wheel. `sdk` (the default) lets Logitech's library drive the wheel through raw HID. `capture` keeps raw HID off and carries the same TrueForce another way: the SDK proxy (`logi-shim --proxy`, required in the game's prefix) copies what the game hands the library to `logi-tf-sim`, which streams it natively, with the driver's force merged in, and synthesises nothing. It exists because the library's 1,000 packets a second, each answered by the wheel, all cross Wine's device process and wineserver, and on some setups that stalls the game (#74, #121). Setting it also sets `hidraw=0`, `texture=none`, `tfsim=1` and the game's relay for the rev lights. Experimental: feel it against the default before relying on it | A line that works for you is also exactly the report needed to add the game properly, so please open an issue with it. diff --git a/tools/logi-launch.sh b/tools/logi-launch.sh index 69844ea..1d1c767 100755 --- a/tools/logi-launch.sh +++ b/tools/logi-launch.sh @@ -249,13 +249,45 @@ want_texture=$(plan_get texture) # (LOGI_REV_MODE=shift in the bridge's environment, below). The app # persists the choice in launch.conf and states it in the plan. want_revleds=$(plan_get revleds) +# The capture route (`tfroute=capture`, a games.conf key for now): the +# game's own TrueForce, carried to the wheel without Logitech's library. +# +# On the default route that library drives the wheel through Proton's raw +# HID: a stream of 1,000 packets a second, each answered by the wheel, and +# every packet and answer crosses Wine's device process and wineserver. +# Measured on an RS50 setup that roughly doubles wineserver's load, and a +# busy wineserver stalls the game's own requests: the stutter in #74 and +# #121. The raw joystick path is not the cost; that library's stream is. +# +# On this route raw HID stays off, so the library never opens the wheel and +# none of that traffic exists. Force feedback reaches the driver's engine +# the normal way, through DirectInput. The TrueForce the game hands the +# library is copied, sample block by sample block, by the SDK proxy +# (install-tf-shim.sh --proxy) to logi-tf-sim over a local socket, and the +# daemon streams it natively, with the engine's force merged into the same +# packets. The game's own haptics, not a synthesis: the daemon is told so +# by a session marker and synthesises nothing for this game. +want_tfroute=$(plan_get tfroute) +if [ "$want_tfroute" = "capture" ]; then + want_hidraw=0 + want_texture=none + want_tfsim=1 + # The relay supplies the telemetry for the rev lights and the screen. + # The default direct-drive plan has none for an SDK title, since the + # library drove the lights there; on this route nothing else does. + if [ -z "$want_relay" ] || [ "$want_relay" = "none" ]; then + want_relay=$(relay_game_for "${SteamAppId:-${SteamGameId:-0}}") + [ -n "$want_relay" ] || want_relay=none + fi + export LOGI_TF_CAPTURE=1 +fi # The tfsim default is 1 in BOTH places it is read (here and at the start # below), because a plan that states nothing means no plan was produced at # all, and an unidentified game still gets the daemon: it idles when nothing # is streaming, and withholding it would leave every UDP-telemetry title # unserved (`LaunchPlan::unknown`). This line said 0 while the code did 1, # so the log contradicted the behaviour for exactly those games. -say "plan: wheel=$(plan_get wheel) game=$(plan_get game) hidraw=${want_hidraw:-unset} ffb=${want_ffb:-native} relay=${want_relay:-none} tfsim=${want_tfsim:-1} texture=${want_texture:-none} revleds=${want_revleds:-bar}" +say "plan: wheel=$(plan_get wheel) game=$(plan_get game) hidraw=${want_hidraw:-unset} ffb=${want_ffb:-native} relay=${want_relay:-none} tfsim=${want_tfsim:-1} texture=${want_texture:-none} revleds=${want_revleds:-bar} tfroute=${want_tfroute:-sdk}" # TrueForce in an SDK title needs the game to reach the wheel's raw HID # interface. Set here so nobody has to remember it, and NEVER guessed: on a @@ -302,6 +334,15 @@ if [ -n "$prefix_root" ]; then [ -f "$f" ] && have_tf_proxy=1 && break done fi +# The capture route copies the game's TrueForce out of the SDK proxy; with +# Logitech's library alone in the prefix there is nothing to copy it, and +# the session runs with force feedback but no TrueForce. Said plainly +# rather than left for someone to notice on track. +if [ "$want_tfroute" = "capture" ] && [ -n "$prefix_root" ] && [ "$have_tf_proxy" = "0" ]; then + say "capture route: the SDK proxy is not installed in this prefix, so the game's" + say "TrueForce has nothing to carry it; force feedback still works. Install it once:" + say " logi-shim --prefix \"$prefix_root/pfx\" --proxy" +fi # Nonzero when the plan granted the game raw HID access (an SDK title): # those sessions can leave the wheel's TrueForce engine started, so they @@ -746,6 +787,7 @@ fi # Written whenever raw HID is granted, whether or not this launch is the # one starting the daemon: the marker describes the session, not that. native_marker="" +captured_marker="" if [ -n "${LOGI_WHEEL_RUNTIME_DIR:-}" ]; then marker_dir="$LOGI_WHEEL_RUNTIME_DIR" elif [ -n "${XDG_RUNTIME_DIR:-}" ]; then @@ -764,6 +806,16 @@ if [ -n "$want_relay" ] && [ "$want_relay" != "none" ]; then want_relay_marker=1 rm -f "$marker_dir/native.$safe_id" 2>/dev/null fi + rm -f "$marker_dir/captured.$safe_id" 2>/dev/null + if [ "$want_tfroute" = "capture" ] && [ "$want_relay_marker" = 1 ]; then + mkdir -p "$marker_dir" 2>/dev/null + if : > "$marker_dir/captured.$safe_id" 2>/dev/null; then + captured_marker="$marker_dir/captured.$safe_id" + say "marked this session as the capture route ($captured_marker); the daemon streams the game's own TrueForce and synthesises nothing" + else + say "could not write $marker_dir/captured.$safe_id; the daemon will keep the captured TrueForce off this wheel" + fi + fi if [ -n "$hidraw_granted" ] && [ "$want_relay_marker" = 1 ]; then mkdir -p "$marker_dir" 2>/dev/null if : > "$marker_dir/native.$safe_id" 2>/dev/null; then @@ -1156,9 +1208,10 @@ fi # process behind. if [ -n "$rpm_bridge_pid" ] || [ -n "$merge_attrs" ] || \ [ -n "$hidraw_granted" ] || [ -n "$helper_group_pid" ] || \ - [ -n "$tfsim_child_pid" ]; then + [ -n "$tfsim_child_pid" ] || [ -n "$captured_marker" ]; then session_cleanup() { [ -n "$native_marker" ] && rm -f "$native_marker" 2>/dev/null + [ -n "$captured_marker" ] && rm -f "$captured_marker" 2>/dev/null [ -n "$rpm_bridge_pid" ] && kill "$rpm_bridge_pid" 2>/dev/null if [ -n "$tfsim_child_pid" ]; then kill "$tfsim_child_pid" 2>/dev/null diff --git a/userspace/logi-wheel/crates/logi-tf-sim/src/daemon.rs b/userspace/logi-wheel/crates/logi-tf-sim/src/daemon.rs index 277ad61..f78eef6 100644 --- a/userspace/logi-wheel/crates/logi-tf-sim/src/daemon.rs +++ b/userspace/logi-wheel/crates/logi-tf-sim/src/daemon.rs @@ -494,12 +494,35 @@ fn native_trueforce_here_in(cfg: &Config, id: &str, marker_dir: &std::path::Path if crate::native_session::marker_path_in(marker_dir, id).exists() { return true; } + // The capture route keeps Logitech's library off the wheel, so the + // game's TrueForce is not on it through the SDK, whatever the wheel. + if captured_route_here_in(id, marker_dir) { + return false; + } if !targets_direct_drive(cfg) { return false; } by_live_id(id).is_some_and(|g| g.native_trueforce_reaches(WheelCaps::direct_drive())) } +/// Whether this game's session is on the capture route (see +/// `native_session::captured_marker_path_in`): its TrueForce reaches the +/// wheel only as samples the SDK proxy copies here, and nothing may be +/// synthesised for it. +fn captured_route_here(id: &str) -> bool { + captured_route_here_in(id, &crate::lease::dir()) +} + +fn captured_route_here_in(id: &str, marker_dir: &std::path::Path) -> bool { + crate::native_session::captured_marker_path_in(marker_dir, id).exists() +} + +/// Whether any session is on the capture route. Captured samples carry no +/// game id, so the open their first burst triggers can ask only this. +fn captured_route_any() -> bool { + crate::native_session::any_captured_in(&crate::lease::dir()) +} + /// Whether the captured-TrueForce path may open a stream this tick. /// /// Captured samples carry no game id of their own, and they arrive on @@ -571,6 +594,11 @@ struct Active { /// The retry runs per iteration, and one line per 50 ms is a log nobody /// can read. warned_busy: bool, + /// Whether this session may synthesise haptics from telemetry. False on + /// the capture route, where the only haptics the wheel may get are the + /// game's own captured samples: when they pause (menus, loading) the + /// stream parks rather than filling in with an engine note. + synth: bool, } fn bind(port: u16) -> Result { @@ -804,6 +832,11 @@ pub fn run(cfg: &Config) -> Result<()> { } if let Some((id, tel)) = latest { + // On the capture route the game's TrueForce arrives on its own + // socket and needs a real stream to land in, whichever of the + // two reaches this daemon first; the lights-only openings below + // would drop it. + let captured_route = captured_route_here(id); if cfg.game_enabled(id) { match &mut active { Some(a) => { @@ -822,6 +855,7 @@ pub fn run(cfg: &Config) -> Result<()> { // something a driver who set the strength to zero asked // for. Nothing to do at all when the lights are off too. None if now >= next_open_attempt + && !captured_route && (!wants_haptics(cfg, id) || native_trueforce_here(cfg, id)) && cfg.leds && crate::leds::other_owner().is_none() => @@ -858,6 +892,7 @@ pub fn run(cfg: &Config) -> Result<()> { lease: None, lease_key: String::new(), warned_busy: false, + synth: true, }); } else { next_open_attempt = now + OPEN_RETRY; @@ -867,15 +902,22 @@ pub fn run(cfg: &Config) -> Result<()> { // gets a stream, whatever the strength says; with the // lights off too (or owned elsewhere) there is nothing // for this daemon to do for it. - None if now >= next_open_attempt && native_trueforce_here(cfg, id) => { + None if now >= next_open_attempt && !captured_route && native_trueforce_here(cfg, id) => { next_open_attempt = now + OPEN_RETRY; } None if now >= next_open_attempt => match open_wheel_stream_with_leds(cfg) { Ok(OpenWheel { stream, led_owner, lease, lease_key }) => { - eprintln!( - "logi-tf-sim: stream start ({id}, rpm {:.0}/{:.0}, speed {:.0} m/s)", - tel.rpm, tel.max_rpm, tel.speed - ); + if captured_route { + eprintln!( + "logi-tf-sim: stream start ({id}, capture route: the game's own \ + TrueForce only, nothing synthesised)" + ); + } else { + eprintln!( + "logi-tf-sim: stream start ({id}, rpm {:.0}/{:.0}, speed {:.0} m/s)", + tel.rpm, tel.max_rpm, tel.speed + ); + } // One rev-display writer per session. When the // texture merge's bridge is up it owns the // strip and drives it from the game's own @@ -928,6 +970,7 @@ pub fn run(cfg: &Config) -> Result<()> { lease: Some(lease), lease_key, warned_busy: false, + synth: !captured_route, }); } // A wheel that cannot take the stream at all, and @@ -965,6 +1008,7 @@ pub fn run(cfg: &Config) -> Result<()> { lease: None, lease_key: String::new(), warned_busy: false, + synth: true, }); } None => next_open_attempt = now + OPEN_RETRY, @@ -1027,10 +1071,24 @@ pub fn run(cfg: &Config) -> Result<()> { } } else if now >= next_open_attempt && captured_stream_wanted(cfg, latest) { match open_wheel_stream(cfg) { - Ok(OpenWheel { stream, lease, lease_key, .. }) => { + Ok(OpenWheel { stream, lease, lease_key, led_owner, .. }) => { + // On the capture route nothing else drives the rev + // display: Logitech's library is kept off the wheel, + // so the lights are this daemon's, from the relay's + // telemetry once it arrives. + let route = captured_route_any(); eprintln!( - "logi-tf-sim: stream start (captured TrueForce from the game's own SDK)" + "logi-tf-sim: stream start (captured TrueForce from the game's own SDK{})", + if route { ", capture route" } else { "" } ); + let leds = if route && cfg.leds && crate::leds::other_owner().is_none() { + match led_owner.as_deref() { + Some(owner) => RevLeds::discover_for(owner), + None => RevLeds::discover(), + } + } else { + None + }; // No mixer and no rev display: this path carries the // game's finished haptics, and the game drives its // own rev lights through the SDK it is already @@ -1054,12 +1112,13 @@ pub fn run(cfg: &Config) -> Result<()> { last_gen: now, samples: Vec::with_capacity(MAX_GEN_MS as usize * crate::synth::SAMPLES_PER_MS), last_captured: Some(now), - leds: None, - screen: None, + leds, + screen: if route && cfg.screen { crate::screen::Screen::discover() } else { None }, gate: SilenceGate::default(), lease: Some(lease), lease_key, warned_busy: false, + synth: !route, }); } Err(e) => { @@ -1099,7 +1158,15 @@ pub fn run(cfg: &Config) -> Result<()> { // including the over-redline cap the synth call used to // apply here: an effect's reading of the sample is the // effect's business. - a.mixer.render(&a.tel, intensity, plan.samples, &mut a.samples); + if a.synth { + a.mixer.render(&a.tel, intensity, plan.samples, &mut a.samples); + } else { + // Capture route: silence while the game's own + // samples pause, so the gate parks the stream and + // the next captured burst resumes it. + a.samples.clear(); + a.samples.resize(plan.samples, 0.0); + } // Menus: telemetry keeps flowing while the mixer emits // exact zeros. Past the grace period the stream parks // (teardown pair + silence) instead of holding an open @@ -1530,6 +1597,37 @@ mod lights_only_tests { std::fs::remove_dir_all(&dir).unwrap(); } + /// The capture route on a direct-drive wheel: Logitech's library is kept + /// off the wheel, so the game's TrueForce is NOT already there, and the + /// captured samples are wanted. Without the marker the direct-drive rule + /// keeps them out, which is the guard against two writers when the SDK + /// does drive the wheel. + #[test] + fn the_capture_route_marker_admits_captured_trueforce_on_a_direct_drive_wheel() { + let dir = std::env::temp_dir().join(format!("logi-capture-route-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let dd = Config { wheel: logi_wheel_core::tfsim::WheelChoice::DirectDrive, ..Config::default() }; + let evo = Some(("ac-evo", Telemetry::default())); + + assert!(native_trueforce_here_in(&dd, "ac-evo", &dir), "default: the SDK drives a DD wheel"); + assert!(!captured_stream_wanted_in(&dd, evo, &dir), "so a captured copy would be a second writer"); + assert!(!captured_route_here_in("ac-evo", &dir)); + + std::fs::write(crate::native_session::captured_marker_path_in(&dir, "ac-evo"), b"").unwrap(); + assert!(captured_route_here_in("ac-evo", &dir)); + assert!(!native_trueforce_here_in(&dd, "ac-evo", &dir), "the route keeps the SDK off the wheel"); + assert!(captured_stream_wanted_in(&dd, evo, &dir), "and the game's own samples are the TrueForce"); + assert!(native_trueforce_here_in(&dd, "acc", &dir), "another game on the default route is unaffected"); + assert!(!captured_route_here_in("acc", &dir)); + + // A native marker means a raw-HID SDK session has the wheel; that + // must still refuse captured samples, route marker or not. + std::fs::write(crate::native_session::marker_path_in(&dir, "ac-evo"), b"").unwrap(); + assert!(native_trueforce_here_in(&dd, "ac-evo", &dir), "the native marker wins"); + assert!(!captured_stream_wanted_in(&dd, evo, &dir)); + std::fs::remove_dir_all(&dir).unwrap(); + } + /// The captured-TrueForce path opens a stream itself, outside the /// telemetry match this test module otherwise exercises. `open_wheel_stream` /// needs a real wheel, so this tests the predicate factored out of that diff --git a/userspace/logi-wheel/crates/logi-tf-sim/src/native_session.rs b/userspace/logi-wheel/crates/logi-tf-sim/src/native_session.rs index 5f71e10..f294bf9 100644 --- a/userspace/logi-wheel/crates/logi-tf-sim/src/native_session.rs +++ b/userspace/logi-wheel/crates/logi-tf-sim/src/native_session.rs @@ -13,14 +13,42 @@ use std::path::{Path, PathBuf}; +/// `id` reduced to plain path characters, so a live id can never escape +/// the directory it names a file in. +fn safe_id(id: &str) -> String { + id.chars() + .map(|c| if c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-') { c } else { '-' }) + .collect() +} + /// `dir/native.`, with `id` reduced to plain path characters so a /// live id can never escape the directory. pub fn marker_path_in(dir: &Path, id: &str) -> PathBuf { - let safe: String = id - .chars() - .map(|c| if c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-') { c } else { '-' }) - .collect(); - dir.join(format!("native.{safe}")) + dir.join(format!("native.{}", safe_id(id))) +} + +/// `dir/captured.`: the marker for the capture route. +/// +/// On that route the launcher keeps Logitech's library off the wheel (no +/// raw HID), so the game's own TrueForce reaches the wheel only because the +/// SDK proxy copies it to this daemon. The marker tells the daemon three +/// things the direct-drive rule would otherwise get wrong for this game: +/// its TrueForce is NOT already on the wheel, the captured samples are +/// wanted even though the wheel is a direct-drive one, and nothing may be +/// synthesised for it, since what reaches the wheel must be the game's own. +/// Written and removed by the launcher around the session, like the native +/// marker. +pub fn captured_marker_path_in(dir: &Path, id: &str) -> PathBuf { + dir.join(format!("captured.{}", safe_id(id))) +} + +/// Whether any capture-route marker exists in `dir`, whatever its id. +/// Captured samples carry no game id, so the open that follows the first +/// burst can only ask whether some session is on this route. +pub fn any_captured_in(dir: &Path) -> bool { + std::fs::read_dir(dir).is_ok_and(|entries| { + entries.flatten().any(|e| e.file_name().to_string_lossy().starts_with("captured.")) + }) } /// Whether any session marker exists in `dir`, whatever its id. @@ -65,6 +93,23 @@ mod tests { std::fs::remove_dir_all(&dir).unwrap(); } + #[test] + fn the_capture_route_marker_is_its_own_kind() { + let dir = std::env::temp_dir().join(format!("logi-captured-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + assert!(!any_captured_in(&dir)); + std::fs::write(marker_path_in(&dir, "ac-evo"), b"").unwrap(); + assert!(!any_captured_in(&dir), "a native marker is not a capture-route marker"); + assert!(any_active_in(&dir), "and the native one still counts as native"); + let p = captured_marker_path_in(&dir, "ac-evo"); + assert_eq!(p, dir.join("captured.ac-evo")); + std::fs::write(&p, b"").unwrap(); + assert!(any_captured_in(&dir)); + std::fs::remove_dir_all(&dir).unwrap(); + assert!(!any_captured_in(&dir), "a missing directory is no marker"); + assert_eq!(captured_marker_path_in(Path::new("/run/x"), "../x"), Path::new("/run/x").join("captured...-x")); + } + #[test] fn a_hostile_id_cannot_leave_the_directory() { let dir = Path::new("/run/x"); From 7e7cf8e79b5c7c14c3bf2343ef049e4da28b7c98 Mon Sep 17 00:00:00 2001 From: mescon <5875228+mescon@users.noreply.github.com> Date: Thu, 24 Sep 2026 15:32:13 +0200 Subject: [PATCH 2/2] launch: start the logi-tf-sim the versions line reports systemd-run --user resolves the command on the user manager's PATH, not the session's, so a daemon earlier on the game's PATH was reported while the installed one ran. Resolve the binary before handing it over. --- CHANGELOG.md | 5 +++++ tools/logi-launch.sh | 8 ++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3349ad1..d4e6832 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ the contract is "it works on RS50 and G Pro as listed here". ## Unreleased +**`logi-launch` starts the `logi-tf-sim` it reports.** The daemon runs as a +user service, and the user manager looked the binary up on its own PATH, so +a copy earlier on the game's PATH was named in the log's versions line while +the installed one ran. The launcher now resolves the binary itself. + **An experimental route for the game's own TrueForce that skips Wine's device path.** Assetto Corsa EVO and ACC stutter under `logi-launch` on some direct-drive setups ([#74](../../issues/74), [#121](../../issues/121)). diff --git a/tools/logi-launch.sh b/tools/logi-launch.sh index 1d1c767..af27fd6 100755 --- a/tools/logi-launch.sh +++ b/tools/logi-launch.sh @@ -847,17 +847,21 @@ if [ "${LOGI_LAUNCH_TF_SIM:-1}" = "1" ] && [ "${want_tfsim:-1}" = "1" ]; then # Steam, a system without a user manager), the daemon runs as a child # of this wrapper instead and is stopped when the game exits, which # is the honest alternative: a later session starts it again. + # The service is started by the user manager, which searches its own + # PATH, not this session's; the binary is resolved here so the daemon + # that runs is the one the versions line above reported. start_tf_sim() { + tfsim_bin=$(command -v logi-tf-sim 2>/dev/null || echo logi-tf-sim) if command -v systemd-run >/dev/null 2>&1 && \ systemd-run --user --quiet --collect \ --description="logi-tf-sim (started by logi-launch)" \ --property=StandardOutput=append:"$LOG" \ --property=StandardError=append:"$LOG" \ - env "$@" logi-tf-sim 2>/dev/null; then + env "$@" "$tfsim_bin" 2>/dev/null; then say "logi-tf-sim runs as a user service, outside Steam's process tree" return 0 fi - setsid env "$@" logi-tf-sim >>"$LOG" 2>&1 >"$LOG" 2>&1