]> vilimpoc.org git repositories - bl2-split/commitdiff
Simultaneous launch + CAPS mouse-wiggle banner, move windows after 30s; fix kill status message master
authorMax Vilimpoc <max@vilimpoc.org>
Mon, 13 Jul 2026 09:24:19 +0000 (11:24 +0200)
committerMax Vilimpoc <max@vilimpoc.org>
Mon, 13 Jul 2026 09:24:19 +0000 (11:24 +0200)
README.md
bl2-splitscreen.sh

index d25a765db489d17b1f403941d08130a6e851bafc..0e9eed8cbf092297407b90d3a8dc3ebee24706fb 100644 (file)
--- a/README.md
+++ b/README.md
@@ -23,11 +23,8 @@ their own controller, save, and screen. Tested on **Arch + KDE Plasma 6 (Wayland
   ```
   sudo pacman -S umu-launcher qt6-tools bubblewrap libarchive curl binutils
   ```
-- **Recommended:** an entropy daemon so the second instance doesn't stall on
-  startup waiting for randomness (see Troubleshooting):
-  ```
-  sudo pacman -S haveged && sudo systemctl enable --now haveged
-  ```
+- *Optional:* `gamemode` (+ `lib32-gamemode`) — the script runs each game under
+  `gamemoderun` if present (performance CPU governor while playing). Not required.
 - Two Xbox controllers (any two distinguishable pads).
 
 ## Setup
@@ -53,9 +50,11 @@ their own controller, save, and screen. Tested on **Arch + KDE Plasma 6 (Wayland
    ```
    ./bl2-splitscreen.sh run
    ```
-   With two monitors each game opens fullscreen on its own display; with one, they
-   tile side-by-side. In-game: **Player 1** *Play → host over LAN*; **Player 2**
-   *Play → Join → pick the LAN game*. `Ctrl-C` kills both.
+   Both instances launch at once. While they load, **wiggle the mouse** until both
+   reach the main menu (the script prints a loud reminder), and leave the windows
+   alone — after ~30 s the script moves each onto its screen (fullscreen per monitor
+   with two displays, side-by-side with one). In-game: **Player 1** *Play → host over
+   LAN*; **Player 2** *Play → Join → pick the LAN game*. `Ctrl-C` kills both.
 
 ## Commands
 
@@ -123,13 +122,18 @@ So another person clones the repo, forces Proton on their own BL2 in Steam, runs
   `rm p{1,2}/game/Binaries/Win32/steam_settings/configs.main.ini`
 - **They don't see each other on LAN:** Player 1 hosts first (the script staggers
   launches). Discovery uses `custom_broadcasts.txt` (localhost).
-- **The 2nd instance takes ages to reach the menu — and speeds up when you jiggle
-  the mouse:** classic **entropy starvation** — something in the Wine/Goldberg/crypto
-  path blocks on `/dev/random` waiting for randomness, and mouse movement feeds the
-  pool. Fix it with an entropy daemon (also in Requirements):
-  `sudo pacman -S haveged && sudo systemctl enable --now haveged`.
-  The two instances also share a DXVK shader cache (`dxvk-cache/`) so the 2nd reuses
-  the 1st's shaders; if still slow, raise the launch gap: `STAGGER=45 ./bl2-splitscreen.sh run`.
+- **The 2nd instance is visible but stuck on the loading screen — and only advances
+  when you jiggle the mouse.** Root cause: BL2's `bPauseOnLossOfFocus=TRUE` — the
+  game **pauses itself whenever its window isn't focused**. In split-screen only one
+  window is focused at a time, so the unfocused instance freezes until an input event
+  wakes it. `setup`/`run` patch each prefix's `WillowEngine.ini` to
+  `bPauseOnLossOfFocus=FALSE` so every instance keeps running unfocused. (This was
+  *not* CPU scaling, entropy, or window occlusion — those were dead ends.) The
+  instances also share a DXVK shader cache so the 2nd reuses the 1st's compiled
+  shaders; `STAGGER=45` further separates their loads if a cold first run is slow.
+- **Two soundtracks at once:** each instance sets `bMuteAudioWhenNotInFocus=TRUE`, so
+  only the focused window plays audio — the sound follows whichever instance you're
+  focused on, instead of both playing together.
 - **In-game "Quit Borderlands 2 → Yes" does nothing / hangs:** the game's shutdown
   path stalls under Goldberg (Steam-networking cleanup, worse with offline mode).
   Don't use in-game Quit — close with **`./bl2-splitscreen.sh kill`** or **Ctrl-C**
index ed4e04077779abc148b1bbbdf09bf86d78a173de..86c35170c02d0278c60cfd5ad71e95793a2772ed 100755 (executable)
@@ -78,15 +78,14 @@ PROTONPATH="${PROTONPATH:-}"
 # experimental and regular) segfaults this binary; classic goldberg works.
 NATIVE_RUNTIME="${NATIVE_RUNTIME:-/home/max/.steam/steam/ubuntu12_32/steam-runtime/run.sh}"
 
-# Seconds to wait before auto-tiling, i.e. how long the intro logos/movies take to
-# clear on your machine. Bump it if the windows snap into place too early (while
-# still on the splash). You can always re-run `./bl2-splitscreen.sh tile`.
-SPLASH_WAIT="${SPLASH_WAIT:-45}"
-
-# Seconds to wait after launching Player 1 before launching Player 2. Giving P1
-# time to reach the menu and start hosting reduces a co-op-startup race that can
-# crash the second (fragile Aspyr) instance.
-STAGGER="${STAGGER:-25}"
+# Seconds after launch before the windows are moved onto their screens (long enough
+# for both to load past the resize-y splash). You can always re-run `.../tile`.
+SPLASH_WAIT="${SPLASH_WAIT:-30}"
+
+# Seconds between launching Player 1 and Player 2. 0 = both at once (this machine is
+# fast enough). Native mode can be crash-prone launching both together - set e.g.
+# STAGGER=25 there if the 2nd instance dies on startup.
+STAGGER="${STAGGER:-0}"
 # ===========================================================================
 
 c_red=$'\e[31m'; c_grn=$'\e[32m'; c_yel=$'\e[33m'; c_dim=$'\e[2m'; c_rst=$'\e[0m'
@@ -94,6 +93,13 @@ say()  { printf '%s\n' "$*"; }
 ok()   { printf '%s✓%s %s\n' "$c_grn" "$c_rst" "$*"; }
 warn() { printf '%s!%s %s\n' "$c_yel" "$c_rst" "$*"; }
 die()  { printf '%s✗ %s%s\n' "$c_red" "$*" "$c_rst" >&2; exit 1; }
+# Loud, unmissable, bold-bright-yellow banner. Each arg is one line (keep it CAPS).
+banner() {
+  local b=$'\e[1;93m' r=$'\e[0m' bar="############################################################" l
+  printf '\n%s%s%s\n' "$b" "$bar" "$r"
+  for l in "$@"; do printf '%s##  %-54s##%s\n' "$b" "$l" "$r"; done
+  printf '%s%s%s\n\n' "$b" "$bar" "$r"
+}
 
 # -------------------------------------------------------- mode detection ---
 WIN_EXE_REL="Binaries/Win32/Borderlands2.exe"
@@ -297,8 +303,13 @@ patch_native_config() {  # $1=player home  $2=width  $3=height
     }' "$f" > "$f.tmp" && mv "$f.tmp" "$f"
 }
 
-# Proton keeps its config inside the Wine prefix; drop the intro movies there so
-# the game goes straight to the menu (resolution is handled by -ResX/-ResY args).
+# Proton keeps its config inside the Wine prefix. We patch WillowEngine.ini to:
+#  - drop the intro movies (straight to menu; resolution is set by -ResX/-ResY);
+#  - always keep RUNNING when unfocused (bPauseOnLossOfFocus=FALSE) - split-screen
+#    focuses only one window at a time, and the default TRUE freezes the unfocused
+#    instance (its load stalls until you nudge the mouse);
+#  - mute when unfocused (bMuteAudioWhenNotInFocus=TRUE) so audio follows the focused
+#    window instead of every instance playing at once.
 patch_proton_config() {  # $1 = prefix dir
   local f
   f=$(find "$1/drive_c/users" -ipath "*Borderlands 2*Config/WillowEngine.ini" 2>/dev/null | head -1)
@@ -308,6 +319,10 @@ patch_proton_config() {  # $1 = prefix dir
     /^\[/ { sec=$0 }
     { l=$0
       if (sec=="[FullScreenMovie]") { if (l ~ /^StartupMovies=/) next; else if (l ~ /^bForceNoMovies=/) l="bForceNoMovies=TRUE" }
+      else if (sec=="[Engine.Engine]") {
+        if (l ~ /^bPauseOnLossOfFocus=/)     l="bPauseOnLossOfFocus=FALSE"
+        else if (l ~ /^bMuteAudioWhenNotInFocus=/) l="bMuteAudioWhenNotInFocus=TRUE"
+      }
       printf "%s%s\n", l, cr }' "$f" > "$f.tmp" && mv "$f.tmp" "$f"
 }
 
@@ -326,6 +341,10 @@ launch_player() {  # $1=n  $2=pad-node  $3=vidpid  -> sets LAUNCHED_PID
   local n="$1" pad="$2" vidpid="$3" pg="$BASE/p$1/game" wd
   local rw="${GW[$n]}" rh="${GH[$n]}"     # this player's render size
   local -a env cmd
+  # Run under gamemode if installed: it flips the CPU to the performance governor
+  # (system-wide) while playing, so instances load at full speed instead of stalling
+  # on a power-saving governor until you jiggle the mouse. No-op if not installed.
+  local -a gm=(); command -v gamemoderun >/dev/null && gm=(gamemoderun)
   # No gamescope in either mode: it doesn't pass raw controllers to nested clients,
   # so the game sees no pad. Run the game windowed and let KWin place it.
   if [ "$MODE" = native ]; then
@@ -335,10 +354,10 @@ launch_player() {  # $1=n  $2=pad-node  $3=vidpid  -> sets LAUNCHED_PID
           "LD_LIBRARY_PATH=$pg:${LD_LIBRARY_PATH:-}"
           "SteamAppId=$APPID" "SteamGameId=$APPID"
           "SDL_JOYSTICK_ALLOW_BACKGROUND_EVENTS=1" )
-    cmd=( "$NATIVE_RUNTIME" "./$NATIVE_BIN" -windowed "-ResX=$rw" "-ResY=$rh" )
+    cmd=( "${gm[@]}" "$NATIVE_RUNTIME" "./$NATIVE_BIN" -windowed "-ResX=$rw" "-ResY=$rh" )
   else
     wd="$pg/Binaries/Win32"
-    patch_proton_config "$BASE/p$n/prefix"     # skip intro movies (if prefix exists)
+    patch_proton_config "$BASE/p$n/prefix"     # movies off, no focus-pause, mute-when-unfocused
     mkdir -p "$BASE/dxvk-cache"
     env=( "WINEPREFIX=$BASE/p$n/prefix" "GAMEID=0" "STORE=none"
           "UMU_RUNTIME_UPDATE=0"                # skip umu's startup update check
@@ -349,7 +368,7 @@ launch_player() {  # $1=n  $2=pad-node  $3=vidpid  -> sets LAUNCHED_PID
     # redundant AND can over-filter (it wrongly dropped the 360 pad), so skip it.
     [ "$ISOLATION" = sdl ] && env+=( "SDL_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT=$vidpid" )
     [ -n "$PROTONPATH" ] && env+=( "PROTONPATH=$PROTONPATH" )
-    cmd=( umu-run "$pg/$WIN_EXE_REL" -windowed "-ResX=$rw" "-ResY=$rh" )
+    cmd=( "${gm[@]}" umu-run "$pg/$WIN_EXE_REL" -windowed "-ResX=$rw" "-ResY=$rh" )
   fi
 
   # bwrap (if used) masks the other pad's /dev/input nodes for this instance.
@@ -423,22 +442,26 @@ cmd_run() {
   say "${c_dim}P1 ${GW[1]}x${GH[1]}@${GX[1]},${GY[1]}   P2 ${GW[2]}x${GH[2]}@${GX[2]},${GY[2]}${c_rst}"
   local pid1 pid2
   launch_player 1 "$p1" "$P1_VIDPID"; pid1=$LAUNCHED_PID
-  sleep "$STAGGER"
+  [ "$STAGGER" -gt 0 ] 2>/dev/null && sleep "$STAGGER"
   launch_player 2 "$p2" "$P2_VIDPID"; pid2=$LAUNCHED_PID
 
-  # Place windows AFTER the splash (the game resizes itself during it). Fire a few
-  # times so both instances get caught once settled.
+  local where="THEIR MONITORS"; [ "$DISPLAY_MODE" = split ] && where="THEIR HALVES"
+  banner "WIGGLE YOUR MOUSE NOW -- KEEP IT MOVING" \
+         "UNTIL BOTH GAMES REACH THE MAIN MENU." \
+         "" \
+         "DO NOT TOUCH THE WINDOWS: I WILL MOVE THEM" \
+         "ONTO $where IN $SPLASH_WAIT SECONDS."
+
+  # Move windows AFTER the splash - the game resizes itself during it, so tiling too
+  # early makes the window flicker. Fire a few times so both settle.
   ( sleep "$SPLASH_WAIT"; cmd_tile >/dev/null 2>&1
-    sleep 10; cmd_tile >/dev/null 2>&1
-    sleep 15; cmd_tile >/dev/null 2>&1 ) &
+    sleep 8; cmd_tile >/dev/null 2>&1
+    sleep 12; cmd_tile >/dev/null 2>&1 ) &
 
-  say ""
   ok "Both instances launching (pids $pid1 / $pid2)."
   say "In-game: ${c_grn}Player 1${c_rst} → Play → host over LAN.  ${c_grn}Player 2${c_rst} → Play → Join → pick the LAN game."
-  local where="their monitors"; [ "$DISPLAY_MODE" = split ] && where="their halves"
-  say "${c_dim}Windows settle onto $where ~${SPLASH_WAIT}s in (after the splash)."
-  say "Re-place any time with:  ./bl2-splitscreen.sh tile${c_rst}"
-  say ""; say "Press Ctrl-C to kill both instances."
+  say "${c_dim}Re-place windows any time with:  ./bl2-splitscreen.sh tile${c_rst}"
+  say "Press Ctrl-C to kill both instances."
   # Killing $pid1/$pid2 only hits the top of each tree; the game runs deeper inside
   # umu/pressure-vessel and survives. kill_instances tears down the whole tree.
   trap 'echo; kill_instances; exit 130' INT TERM
@@ -466,7 +489,9 @@ cmd_kill() {
   pgrep 'Borderlands2' >/dev/null || { ok "No Borderlands 2 instances running."; return 0; }
   say "Killing Borderlands 2 instances..."
   kill_instances
-  local left; left=$(pgrep -c 'Borderlands2' 2>/dev/null || echo 0)
+  # pgrep -c prints "0" and exits non-zero on no match, so DON'T add `|| echo 0`
+  # (that would double it). Command substitution keeps the "0" regardless of exit.
+  local left; left=$(pgrep -c 'Borderlands2' 2>/dev/null); left=${left:-0}
   [ "$left" = 0 ] && ok "All instances killed." || warn "$left process(es) still lingering."
 }