yarn: forza dualsense adaptive trigger bridge

This commit is contained in:
2026-05-01 14:26:32 -04:00
parent 4d0ba317e1
commit 1fc2f995c7
5 changed files with 1563 additions and 31 deletions

View File

@@ -15,6 +15,7 @@
./impermanence.nix
./lact.nix
./vr.nix
./forza-trigger
inputs.impermanence.nixosModules.impermanence
];
@@ -95,14 +96,9 @@
# live in ./optiscaler-fh5-rdna3.ini; keys not listed there fall through
# to OptiScaler's "auto" defaults.
#
# Required one-time per-game setup the user has to do in Steam (no API):
# - Properties > Compatibility: pick the GE-Proton tool by hand. The
# `compatTool` option is intentionally unset \u2014 nixpkgs registers
# proton-ge-bin under its versioned id (e.g. GE-Proton10-34), and
# writing the generic "GE-Proton" string silently falls back to
# bundled Proton.
# - In-game: switch the Upscaling option from FSR 2.2 to DLSS or XeSS
# (FSR 2 inputs aren't intercepted). Press Insert to open the Opti
# Required one-time in-game setup the user has to do in FH5 (no API):
# - Switch the Upscaling option from FSR 2.2 to DLSS or XeSS (FSR 2
# inputs aren't intercepted). Press Insert to open the OptiScaler
# overlay and set the FFX upscaler to FSR 4.
#
# OptiScaler.ini is dropped with mode = "init" so in-game overlay edits
@@ -125,8 +121,10 @@
{
enable = true;
closeSteam = true;
defaultCompatTool = "proton_10";
apps."fh5" = {
id = 1551360;
compatTool = "proton_10";
launchOptions.env = {
# OptiScaler FSR 4 INT8 path on this RDNA 3 (Navi 32) box.
# PROTON_FSR4_UPGRADE opts FH5 into Proton's FSR 4 DLL upgrade;
@@ -172,4 +170,8 @@
] fromOpti;
};
};
# PS5 DualSense adaptive triggers in Forza Horizon 4 / 5.
services.forzaTrigger.enable = true;
}

View File

@@ -20,19 +20,6 @@
# - in Forza, HUD options → set Data Out: ON, Data Out IP: 127.0.0.1,
# Data Out IP Port: 5300, and (FM7 only) Data Out Packet Format: CAR DASH.
#
# System-interaction notes:
# - With multiple DualSense controllers connected, pydualsense picks one
# non-deterministically (`# TODO: implement multiple controllers working`
# in pydualsense's source). Forza Horizon is single-player so this is
# usually fine. If you need to pin a specific controller, the cleanest
# route is monkey-patching `pydualsense.__find_device`.
# - `pkgs.dualsensectl` is intentionally NOT installed by default
# (single-shot writes from it get overwritten by our BG thread within
# ~4 ms). Bring it in ad-hoc with `nix-shell -p dualsensectl` and stop
# this service first via `systemctl --user stop forza-trigger`.
# - Hot-plug recovery happens in-process: the daemon polls pydualsense's BG
# thread liveness and re-runs `pydualsense.init()` on disconnect. systemd's
# `Restart=on-failure` exists only as a crash-recovery safety net.
let
cfg = config.services.forzaTrigger;
pythonPackages = import ./python-packages.nix { inherit lib pkgs; };
@@ -90,16 +77,45 @@ in
environment.systemPackages = [ forzaTrigger ];
# User-level service so it inherits the seat-bound uaccess ACL on
# /dev/hidraw* and dies cleanly when the user logs out.
systemd.user.services.forza-trigger = {
# Socket-activated by Forza's Data Out UDP stream on port 5300.
# The service starts when Forza Horizon 4/5 sends its first telemetry
# packet, runs for the entire session, and exits on 3 s of silence
# (--exit-on-idle). systemd restarts automatically on the next launch.
#
# Runs as a system service (not user service) so there is exactly one
# instance regardless of how many users have active sessions. The service
# runs as `cfg.user` (primary) for hidraw uaccess compatibility with the
# udev TAG+="uaccess" rules.
#
# Crash-recovery: Restart=on-failure restarts the daemon mid-session if
# it dies abnormally (non-zero exit). The clean exit-on-idle path returns
# 0, which systemd leaves alone — the socket unit stays listening.
systemd.services.forza-trigger = {
description = "Forza Horizon DualSense adaptive trigger bridge";
wantedBy = [ "default.target" ];
after = [ "graphical-session.target" ];
# No wantedBy — socket activation pulls it in when Forza sends UDP.
serviceConfig = {
ExecStart = "${forzaTrigger}/bin/forza-trigger --host 127.0.0.1 --port ${toString cfg.port}";
ExecStart = "${forzaTrigger}/bin/forza-trigger --host 127.0.0.1 --port ${toString cfg.port} --exit-on-idle";
Restart = "on-failure";
RestartSec = 3;
User = cfg.user;
# KillMode=control-group is the default, but be explicit.
# systemd sends SIGTERM by default; Python doesn't route that to
# KeyboardInterrupt, so we tell systemd to send SIGINT instead.
# The daemon registers handlers for both for robustness.
KillMode = "control-group";
KillSignal = "SIGINT";
TimeoutStopSec = 5;
};
};
systemd.sockets.forza-trigger = {
wantedBy = [ "sockets.target" ];
socketConfig = {
ListenDatagram = "127.0.0.1:${toString cfg.port}";
# Guard against rapid re-triggering. One activation per Forza session
# (minutes+) is normal; > 3 within 10 s is pathological (bug).
TriggerLimitIntervalSec = 10;
TriggerLimitBurst = 3;
};
};
};

File diff suppressed because it is too large Load Diff