Replaces three handfuls of custom code with upstream / static data:
- Per-app Steam launch options now declared via different-name/steam-
config-nix's `programs.steam.config.apps.<n>` instead of a custom
~70-line `apply_launch_options` Python function. The dropped writer
was racy: it edited localconfig.vdf without checking for a running
Steam, so any timer firing while Steam was open would lose its
changes on the next Steam shutdown. steam-config-nix's `closeSteam`
flag closes that race.
Also moves the GE-Proton compat-tool pin to declarative config —
one fewer manual click in Steam UI to remember.
- `mods.<>.launchOptions` option, the `launchOptionsData` aggregation,
and `LAUNCH_OPTIONS_DATA` are removed from desktop-game-mods.nix.
The module now does file-drops only; Steam config lives in its own
`programs.steam.config` namespace, where it belongs.
fh5-vkd3d-no-hvv (which existed only to set VKD3D_CONFIG) collapses
into the FH5 launchOptions block in hosts/yarn/default.nix.
- `unitConfig.X-ConfigHash` on game-mods.service is replaced with
`restartTriggers`. NixOS already emits `X-Restart-Triggers=<hash>`
on the unit; the workaround was redundant. The Type=oneshot,
RemainAfterExit=no semantics make `systemctl restart` re-run
ExecStart cleanly on hash change.
- The awk pipeline that patched OptiScaler's stock OptiScaler.ini at
build time is replaced with a hand-written hosts/yarn/optiscaler-
fh5-rdna3.ini containing only the keys we override (5 of them).
OptiScaler's Config::readString defaults missing keys to "auto"
(Config.cpp:1568), so a minimal file is sufficient. Side benefits:
one upstream-source dependency removed, a key-rename in upstream
becomes a behavior change rather than a silent awk-no-match.
Override values + sources:
Fsr4Update=true FH5 wiki, FSR4 Linux Setup
DlssReactiveMaskBias=0.65 FH5 wiki, "Known Issues"
FsrNonLinearColorSpace=true FSR4 wiki, "Image Quality"
EnableFsr2Inputs=false FH5 wiki, "Known Issues"
Dxgi=false FH5 wiki
- forza-trigger's three custom Python derivations (pydualsense,
hidapi-usb, fdp) factored out of default.nix into a sibling
python-packages.nix. Same logic, single-purpose file. Bumping a
version is now a one-place hash roll.
- pkgs.dualsensectl removed from the daemon's environment.system-
Packages. Single-shot writes from the CLI get clobbered by the BG
sendReport thread within ~4ms anyway, so the tool is only useful
with the daemon stopped — not worth the unconditional install.
Bring it in ad-hoc with `nix-shell -p dualsensectl`.
User report: with the clutch in (pedal pressed, engine disconnected from
wheels), steering left still produced resistance on R2. The throttle
shouldn't have any feel when it's mechanically irrelevant.
RacingDSX's throttle resistance formula is
`avgAccel = sqrt(0.25*X^2 + 1.0*Z^2)`
derived from the accelerometer alone. It never checks clutch state, so
cornering G-forces keep producing trigger resistance even while the
clutch pedal is floored. Bug.
Fix: when Forza's clutch byte > 128 (clutch fully or mostly disengaged)
bypass the entire throttle path \u2014 slip detection and non-slip Feedback
both \u2014 and release the trigger. Uses the same one-shot 0x05 (active
retract) on transition + steady-state 0x00 (no-op) pattern as the
in-race \u2192 not-in-race transition (divergence #4) so we don't get the
trigger-motor whine from re-asserting 0x05 every frame.
Brake is unaffected: brake calipers operate independently of clutch
state, so ABS feel during clutch-in is still correct.
For auto-clutch users this also produces brief (~100 ms) trigger
relaxations during shifts \u2014 physically accurate (the engine *is*
momentarily disconnected during a shift) and matches the haptic feel of
a real manual transmission.
Documented as divergence #5 in the module docstring.
The previous fix used canonical Off (mode 0x05) everywhere we wanted the
trigger to feel released \u2014 pre-race per-frame, idle timeout, shutdown.
Per Sony's docs (Nielk1 Rev 6) mode 0x05 "actively returns the trigger
stop to the neutral position". Re-asserting it 60 times/sec from main
thread, propagated by pydualsense's BG thread to the controller at
~250 Hz, made the trigger motor audibly whine as the firmware repeatedly
snapped the (already-neutral) trigger back to neutral.
Right answer: hybrid. One-shot 0x05 on the in-race \u2192 not-in-race
transition (and on the telemetry-idle timeout) so the firmware actually
retracts the motor; mode 0x00 (TriggerModes.Off, no-op clear) for
steady-state pre-race / idle frames so we're not yelling RESET in the
firmware's ear forever.
Implementation: prev_in_race tracks the last frame's race state. Steady
non-race frames call _apply_normal (mode 0x00); the first frame after a
race-end transition calls _apply_off (mode 0x05). pydualsense's BG
thread holds the 0x05 in memory long enough (one main-thread frame =
~16ms = ~4 BG iterations) to publish it to the controller before main
switches the in-memory state to 0x00.
Restores _apply_normal and DS_MODE_NORMAL that the previous commit
deleted. Updates divergence #4 in the module docstring.
Two issues in the deployed daemon:
1. After FH5 exits, the lightbar stayed lit. reset_triggers() touched
only triggers; pydualsense's BG sendReport thread kept re-publishing
whatever TouchpadColor we last set, so the controller stayed in the
last race color forever.
2. R2 had residual tension in FH5's main menu and on the desktop after
a race. Pre-race / idle states were emitting RacingDSX's NormalTrigger
(mode byte 0x00), which per Sony's docs (Nielk1 Rev6) only clears
state without retracting the trigger motor; mode 0x05 (canonical Off
/ Reset) actively returns the trigger to neutral. RacingDSX-on-Windows
gets away with 0x00 because something else (Steam Input or the OS)
reliably resets the motor on focus loss; on Linux nothing does.
Fixes:
- Drop _apply_normal/DS_MODE_NORMAL. Use _apply_off (mode 0x05) for every
'release the trigger' intent: pre-race per-frame, idle timeout, mid-race
zero-strength fallback, shutdown.
- Add reset_lightbar() that writes RGB(0,0,0).
- Track have_telemetry and fire the idle-timeout branch whenever
telemetry has been silent for IDLE_TIMEOUT_S, regardless of in_race.
Reset both triggers and lightbar in that branch.
Documented as divergence #4 in the module docstring.