Intel-Arc-Gaming-Omarchy/ARCGames_installv2.sh
nosignal 07ad43df9c v1.6.0 — add Arc-branded iGPU support (Lunar Lake Xe2, Panther Lake Xe3); drop Mesa-git management
- GPU detection rewritten: xe-driver-only, bus-based iGPU/dGPU classification, generation
  tagging (alchemist/battlemage/xe2/xe3/other) for gen-specific workarounds
- Selection ranks dGPU+display > dGPU > iGPU+display > iGPU
- Removed: MESA MANAGEMENT and MESA-GIT REBUILD sections, USE_MESA_GIT config option,
  cleanup_on_exit trap (was only relevant during mesa swap)
- Added: STEAM FIRST-RUN BOOTSTRAP — interactive Steam login before Gaming Mode
- Added: is_omarchy() helper
- Gen-specific wrapper env: INTEL_DEBUG=norbc + ANV_QUEUE_THREAD_DISABLE=1 only on Alchemist
- README: refreshed support matrix, removed mesa-git/recovery sections, documented gen-aware workarounds
2026-05-01 22:42:51 +01:00

1639 lines
62 KiB
Bash
Executable file

#!/bin/bash
#
# ARCGames - Gaming Mode Installer for Intel Arc GPUs
# Version: 1.6.0
#
# Description:
# Sets up a SteamOS-like gaming experience on Arch Linux with Hyprland.
# Supports Intel Arc discrete GPUs (Alchemist DG2, Battlemage) and modern
# Intel Arc-branded iGPUs (Lunar Lake Xe2, Panther Lake Xe3).
#
# Features:
# - Steam and gaming dependencies installation
# - Mesa-git or stable Mesa driver selection
# - Gamescope session switching (Hyprland <-> Gaming Mode)
# - Performance tuning (GPU, audio, memory)
# - External Steam library auto-mounting
#
# Usage:
# ./ARCGames_install.sh [--help|--version]
#
# Keybinds (after installation):
# Super+Shift+S - Switch to Gaming Mode (from Hyprland)
# Super+Shift+R - Return to Desktop (from Gaming Mode)
#
###############################################################################
set -uo pipefail
ARCGAMES_VERSION="1.6.0"
###############################################################################
# CONFIGURATION
###############################################################################
CONFIG_FILE="/etc/gaming-mode.conf"
# Note: REAL_HOME not yet defined here, check both locations
[[ -f "${HOME}/.gaming-mode.conf" ]] && CONFIG_FILE="${HOME}/.gaming-mode.conf"
[[ -n "${SUDO_USER:-}" ]] && {
_sudo_home=$(getent passwd "$SUDO_USER" | cut -d: -f6)
[[ -n "$_sudo_home" && -f "$_sudo_home/.gaming-mode.conf" ]] && CONFIG_FILE="$_sudo_home/.gaming-mode.conf"
}
# Parse config file safely (no arbitrary code execution)
if [[ -f "$CONFIG_FILE" ]]; then
while IFS='=' read -r _key _value; do
_key="${_key#"${_key%%[![:space:]]*}"}" # trim leading whitespace
_key="${_key%"${_key##*[![:space:]]}"}" # trim trailing whitespace
_value="${_value#"${_value%%[![:space:]]*}"}"
_value="${_value%"${_value##*[![:space:]]}"}"
case "$_key" in
PERFORMANCE_MODE) PERFORMANCE_MODE="$_value" ;;
esac
done < "$CONFIG_FILE" 2>/dev/null || true
fi
: "${PERFORMANCE_MODE:=enabled}"
# Global state
NEEDS_RELOGIN=0
INTEL_ARC_VK_DEVICE=""
INTEL_ARC_DRM_CARD=""
INTEL_GPU_TIER="" # dgpu | igpu
INTEL_GPU_GEN="" # alchemist | battlemage | xe2 | xe3 | other
# Resolve actual user (handles sudo case)
REAL_USER="${SUDO_USER:-$USER}"
REAL_HOME=$(getent passwd "$REAL_USER" | cut -d: -f6)
###############################################################################
# UTILITY FUNCTIONS
###############################################################################
info() { echo "[*] $*"; }
warn() { echo "[!] $*" >&2; }
err() { echo "[!] $*" >&2; }
die() {
local msg="$1"
local code="${2:-1}"
echo "FATAL: $msg" >&2
logger -t arcgames "Installation failed: $msg"
exit "$code"
}
check_package() {
pacman -Qi "$1" &>/dev/null
}
check_aur_helper_functional() {
local helper="$1"
"$helper" --version &>/dev/null
}
is_omarchy() {
[[ -d "${REAL_HOME}/.local/share/omarchy" ]]
}
# Validate REAL_HOME was resolved (must be after die() is defined)
[[ -z "$REAL_HOME" ]] && die "Could not resolve home directory for user: $REAL_USER"
# Run command as the original user (handles case where script is run with sudo)
run_as_user() {
if [[ -n "${SUDO_USER:-}" ]] && [[ "$EUID" -eq 0 ]]; then
sudo -u "$SUDO_USER" "$@"
else
"$@"
fi
}
###############################################################################
# ENVIRONMENT VALIDATION
###############################################################################
validate_environment() {
command -v pacman >/dev/null || die "pacman required"
command -v hyprctl >/dev/null || die "hyprctl required"
[[ -d "$REAL_HOME/.config/hypr" ]] || die "Hyprland config directory not found (~/.config/hypr)"
# Ensure lspci is available for GPU detection
if ! command -v lspci >/dev/null 2>&1; then
info "Installing pciutils for GPU detection..."
sudo pacman -S --needed --noconfirm pciutils || die "Failed to install pciutils"
fi
}
###############################################################################
# GPU DETECTION
###############################################################################
# Classify a DRM card as iGPU (on root complex) or dGPU.
# Bus location is authoritative — Intel may brand iGPUs as "Arc" (Lunar Lake,
# Panther Lake), so name patterns alone misclassify them.
is_intel_igpu() {
local card_path="$1" pci_slot=""
[[ -L "$card_path/device" ]] && pci_slot=$(basename "$(readlink -f "$card_path/device")")
[[ -z "$pci_slot" ]] && return 1
[[ "$pci_slot" =~ ^0000:00: ]] && return 0
return 1
}
# Get the Vulkan device ID (vendor:device) for a PCI slot
get_vk_device_id() {
local pci_slot="$1"
local vendor device
vendor=$(cat "/sys/bus/pci/devices/$pci_slot/vendor" 2>/dev/null | sed 's/0x//')
device=$(cat "/sys/bus/pci/devices/$pci_slot/device" 2>/dev/null | sed 's/0x//')
if [[ -n "$vendor" && -n "$device" ]]; then
echo "${vendor}:${device}"
fi
}
# Map an Intel Vulkan device ID (vendor:device) to a generation tag.
# Used to gate gen-specific workarounds (e.g. norbc on Alchemist only).
detect_intel_gen() {
local vk_id="$1"
local dev="${vk_id#*:}"
case "$dev" in
4f8?|4f9?|56[89ab]?|56c?) echo "alchemist" ;; # DG2 (A-series)
e20?|e21?|e22?|e23?) echo "battlemage" ;; # BMG (B-series dGPU + iGPU)
64a?|64b?) echo "xe2" ;; # Lunar Lake iGPU
fd??|b0??) echo "xe3" ;; # Panther Lake iGPU (provisional)
*) echo "other" ;;
esac
}
# Walk Intel DRM cards and pick the best one for gaming.
# Preference: dGPU with connected display > dGPU > iGPU with display > iGPU.
find_intel_gpu() {
local best_card="" best_pci="" best_tier="" best_has_display=false
_consider() {
# $1=card $2=pci $3=tier $4=has_display
local rank_new rank_old
case "$3:$4" in
dgpu:true) rank_new=4 ;;
dgpu:false) rank_new=3 ;;
igpu:true) rank_new=2 ;;
igpu:false) rank_new=1 ;;
esac
case "$best_tier:$best_has_display" in
dgpu:true) rank_old=4 ;;
dgpu:false) rank_old=3 ;;
igpu:true) rank_old=2 ;;
igpu:false) rank_old=1 ;;
*) rank_old=0 ;;
esac
if (( rank_new > rank_old )); then
best_card="$1" best_pci="$2" best_tier="$3" best_has_display="$4"
fi
}
for card_path in /sys/class/drm/card[0-9]*; do
local card_name
card_name=$(basename "$card_path")
[[ "$card_name" == render* ]] && continue
local driver_link="$card_path/device/driver"
[[ -L "$driver_link" ]] || continue
local driver
driver=$(basename "$(readlink "$driver_link")")
# xe-only: skip i915-bound GPUs (older UHD/Iris). Arc dGPU + Arc-branded
# iGPU (Xe2 Lunar Lake, Xe3 Panther Lake) all use xe.
[[ "$driver" == "xe" ]] || continue
local pci_slot tier has_display=false
pci_slot=$(basename "$(readlink -f "$card_path/device")")
if is_intel_igpu "$card_path"; then tier=igpu; else tier=dgpu; fi
for connector in "$card_path"/"$card_name"-*/status; do
if [[ -f "$connector" ]] && grep -q "^connected$" "$connector" 2>/dev/null; then
has_display=true
break
fi
done
info "Found Intel GPU: $card_name (tier=$tier, pci=$pci_slot, display=$has_display)"
_consider "$card_name" "$pci_slot" "$tier" "$has_display"
done
[[ -z "$best_card" ]] && return 1
INTEL_ARC_DRM_CARD="$best_card"
INTEL_ARC_VK_DEVICE=$(get_vk_device_id "$best_pci")
INTEL_GPU_TIER="$best_tier"
INTEL_GPU_GEN=$(detect_intel_gen "$INTEL_ARC_VK_DEVICE")
info "Selected: $INTEL_ARC_DRM_CARD (tier=$INTEL_GPU_TIER, gen=$INTEL_GPU_GEN, vk=$INTEL_ARC_VK_DEVICE)"
$best_has_display || warn "No monitor detected on selected GPU"
return 0
}
# Verify a usable Intel GPU is present and select one.
check_intel_gpu() {
local gpu_info
gpu_info=$(lspci 2>/dev/null | grep -iE 'vga|3d|display' || echo "")
if ! echo "$gpu_info" | grep -iq intel; then
die "No Intel GPU detected. This script targets Intel Arc dGPUs and Arc-branded iGPUs."
fi
if ! find_intel_gpu; then
die "No xe-driven Intel GPU found.
This script targets Intel Arc (xe driver) only. Older UHD/Iris GPUs on i915
are intentionally ignored. Check 'lsmod | grep -E xe\\|i915' and 'lspci -k'."
fi
info "Intel GPU detected and selected: $INTEL_ARC_DRM_CARD ($INTEL_GPU_TIER/$INTEL_GPU_GEN)"
return 0
}
###############################################################################
# MULTILIB REPOSITORY
###############################################################################
enable_multilib_repo() {
info "Enabling multilib repository..."
sudo cp /etc/pacman.conf "/etc/pacman.conf.backup.$(date +%Y%m%d%H%M%S)" || die "Failed to backup pacman.conf"
# Uncomment only the [multilib] header and its Include line
sudo sed -i '/^#\[multilib\]$/{s/^#//;n;s/^#//}' /etc/pacman.conf || die "Failed to enable multilib"
if grep -q "^\[multilib\]" /etc/pacman.conf 2>/dev/null; then
info "Multilib repository enabled successfully"
sudo pacman -Syu --noconfirm || die "Failed to update system"
else
die "Failed to enable multilib repository"
fi
}
###############################################################################
# STEAM DEPENDENCIES
###############################################################################
check_steam_dependencies() {
info "Checking Steam dependencies for Intel Arc..."
#---------------------------------------------------------------------------
# System Update
#---------------------------------------------------------------------------
echo ""
echo "================================================================"
echo " SYSTEM UPDATE RECOMMENDED"
echo "================================================================"
echo ""
read -p "Upgrade system now? [Y/n]: " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Nn]$ ]]; then
info "Upgrading system..."
sudo pacman -Syu --noconfirm || die "Failed to upgrade system"
fi
echo ""
#---------------------------------------------------------------------------
# Multilib Repository Check
#---------------------------------------------------------------------------
local -a missing_deps=()
local -a optional_deps=()
local multilib_enabled=false
if grep -q "^\[multilib\]" /etc/pacman.conf 2>/dev/null; then
multilib_enabled=true
info "Multilib repository: enabled"
else
err "Multilib repository: NOT enabled (required for Steam)"
echo ""
echo "================================================================"
echo " MULTILIB REPOSITORY REQUIRED"
echo "================================================================"
echo ""
echo " Steam requires 32-bit libraries from the multilib repository."
echo ""
read -p "Enable multilib repository now? [Y/n]: " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Nn]$ ]]; then
enable_multilib_repo
multilib_enabled=true
else
die "Multilib repository is required for Steam"
fi
fi
#---------------------------------------------------------------------------
# Mesa Status (stable only)
# Install stable mesa if no mesa is present. If mesa-git is already
# installed (e.g. from a previous version of this script), leave it alone.
#---------------------------------------------------------------------------
local has_mesa=false has_lib32_mesa=false
if check_package "mesa-git"; then
has_mesa=true
info "Mesa: mesa-git already installed (leaving as-is)"
elif check_package "mesa"; then
has_mesa=true
info "Mesa: stable mesa already installed"
else
info "Mesa: not installed (will install stable)"
fi
if check_package "lib32-mesa-git" || check_package "lib32-mesa"; then
has_lib32_mesa=true
fi
#---------------------------------------------------------------------------
# Define Package Lists
#---------------------------------------------------------------------------
local -a core_deps=(
"steam"
"lib32-vulkan-icd-loader"
"vulkan-icd-loader"
"mesa-utils"
"lib32-glibc"
"lib32-gcc-libs"
"lib32-libx11"
"lib32-libxss"
"lib32-alsa-plugins"
"lib32-libpulse"
"lib32-openal"
"lib32-nss"
"lib32-libcups"
"lib32-sdl2-compat"
"lib32-freetype2"
"lib32-fontconfig"
"lib32-libnm"
"networkmanager"
"gamemode"
"lib32-gamemode"
"ttf-liberation"
"xdg-user-dirs"
"kbd"
)
# Stable mesa packages: only added when no mesa is currently installed.
if ! $has_mesa; then
core_deps+=("mesa" "vulkan-intel" "vulkan-mesa-layers")
fi
if $multilib_enabled && ! $has_lib32_mesa; then
core_deps+=("lib32-mesa" "lib32-vulkan-intel" "lib32-vulkan-mesa-layers")
fi
local -a gpu_deps=(
"intel-media-driver"
"vulkan-tools"
)
local -a recommended_deps=(
"gamescope"
"mangohud"
"lib32-mangohud"
"proton-ge-custom-bin"
"proton-cachyos-slr"
"udisks2"
)
#---------------------------------------------------------------------------
# Check Dependencies
#---------------------------------------------------------------------------
info "Checking core Steam dependencies..."
for dep in "${core_deps[@]}"; do
check_package "$dep" || missing_deps+=("$dep")
done
info "Checking Intel GPU dependencies..."
for dep in "${gpu_deps[@]}"; do
check_package "$dep" || missing_deps+=("$dep")
done
info "Checking recommended dependencies..."
for dep in "${recommended_deps[@]}"; do
check_package "$dep" || optional_deps+=("$dep")
done
#---------------------------------------------------------------------------
# Display Results
#---------------------------------------------------------------------------
echo ""
echo "================================================================"
echo " STEAM DEPENDENCY CHECK RESULTS"
echo "================================================================"
echo ""
#---------------------------------------------------------------------------
# Install Missing Packages
#---------------------------------------------------------------------------
if ((${#missing_deps[@]})); then
echo " MISSING REQUIRED PACKAGES (${#missing_deps[@]}):"
for dep in "${missing_deps[@]}"; do
echo " - $dep"
done
echo ""
read -p "Install missing required packages? [Y/n]: " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Nn]$ ]]; then
info "Installing missing dependencies..."
sudo pacman -S --needed --noconfirm "${missing_deps[@]}" || die "Failed to install dependencies"
info "Required dependencies installed successfully"
else
die "Missing required Steam dependencies"
fi
else
info "All required pacman dependencies are installed!"
fi
#---------------------------------------------------------------------------
# Install Optional Packages
#---------------------------------------------------------------------------
echo ""
if ((${#optional_deps[@]})); then
echo " RECOMMENDED PACKAGES (${#optional_deps[@]}):"
for dep in "${optional_deps[@]}"; do
echo " - $dep"
done
echo ""
read -p "Install recommended packages? [y/N]: " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
info "Installing recommended packages..."
local -a pacman_optional=()
local -a aur_optional=()
for dep in "${optional_deps[@]}"; do
if pacman -Si "$dep" &>/dev/null; then
pacman_optional+=("$dep")
else
aur_optional+=("$dep")
fi
done
if ((${#pacman_optional[@]})); then
sudo pacman -S --needed --noconfirm "${pacman_optional[@]}" || info "Some optional packages failed"
fi
if ((${#aur_optional[@]})); then
local aur_helper=""
command -v yay >/dev/null 2>&1 && check_aur_helper_functional yay && aur_helper="yay"
[[ -z "$aur_helper" ]] && command -v paru >/dev/null 2>&1 && check_aur_helper_functional paru && aur_helper="paru"
if [[ -n "$aur_helper" ]]; then
read -p "Install AUR packages with $aur_helper? [y/N]: " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
run_as_user "$aur_helper" -S --needed --noconfirm "${aur_optional[@]}" || info "Some AUR packages failed"
fi
fi
fi
fi
fi
check_steam_config
}
###############################################################################
# STEAM CONFIGURATION
###############################################################################
check_steam_config() {
info "Checking Steam configuration..."
# Check the real user's groups (not root's when run with sudo)
local user_groups
user_groups=$(id -Gn "$REAL_USER" 2>/dev/null || groups "$REAL_USER" 2>/dev/null || echo "")
local missing_groups=()
echo "$user_groups" | grep -qw 'video' || missing_groups+=("video")
echo "$user_groups" | grep -qw 'input' || missing_groups+=("input")
echo "$user_groups" | grep -qw 'wheel' || missing_groups+=("wheel")
if ((${#missing_groups[@]})); then
echo ""
echo "================================================================"
echo " USER GROUP PERMISSIONS"
echo "================================================================"
echo ""
read -p "Add $REAL_USER to ${missing_groups[*]} group(s)? [Y/n]: " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Nn]$ ]]; then
local groups_to_add
groups_to_add=$(IFS=,; echo "${missing_groups[*]}")
if sudo usermod -aG "$groups_to_add" "$REAL_USER"; then
info "Successfully added $REAL_USER to group(s): $groups_to_add"
NEEDS_RELOGIN=1
fi
fi
else
info "User $REAL_USER is in video, input, and wheel groups - permissions OK"
fi
}
###############################################################################
# STEAM FIRST-RUN BOOTSTRAP
###############################################################################
# Detect whether Steam has been initialized for $REAL_USER (logged in once).
# Checks both the canonical and symlinked loginusers.vdf locations.
steam_already_bootstrapped() {
local f
for f in "${REAL_HOME}/.local/share/Steam/config/loginusers.vdf" \
"${REAL_HOME}/.steam/steam/config/loginusers.vdf"; do
[[ -s "$f" ]] && grep -q '"AccountName"' "$f" 2>/dev/null && return 0
done
return 1
}
# Run Steam in the user's graphical session, blocking until the window closes.
# Handles three invocation modes: as the user (just exec), under sudo with the
# user's $DISPLAY/$WAYLAND_DISPLAY visible (preserve env), or under sudo with
# only $SUDO_USER (reconstruct XDG_RUNTIME_DIR + bus path from UID).
launch_steam_for_user() {
if [[ "$EUID" -ne 0 ]]; then
steam
return $?
fi
[[ -z "${SUDO_USER:-}" ]] && { warn "Cannot launch Steam as root with no SUDO_USER"; return 1; }
local uid; uid=$(id -u "$REAL_USER")
sudo -u "$REAL_USER" --preserve-env=DISPLAY,WAYLAND_DISPLAY,XDG_SESSION_TYPE \
env XDG_RUNTIME_DIR="/run/user/${uid}" \
DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/${uid}/bus" \
steam
}
bootstrap_steam_login() {
if steam_already_bootstrapped; then
info "Steam already initialized for $REAL_USER (loginusers.vdf has account entry)"
return 0
fi
if ! command -v steam >/dev/null 2>&1; then
warn "steam command not found — skipping bootstrap. Re-run the installer once Steam is installed."
return 0
fi
echo ""
echo "================================================================"
echo " STEAM FIRST-RUN BOOTSTRAP"
echo "================================================================"
echo ""
echo " Gaming Mode (gamescope-session-steam) expects Steam to be"
echo " initialized and logged in. We'll launch Steam now so you can:"
echo ""
echo " 1. Wait for it to download/install runtime files"
echo " 2. Log in with your Steam account"
echo " 3. Close the Steam window when finished"
echo ""
echo " This script resumes automatically after Steam exits."
echo ""
read -p "Launch Steam now? [Y/n]: " -n 1 -r
echo
if [[ $REPLY =~ ^[Nn]$ ]]; then
warn "Skipped Steam bootstrap. Gaming Mode may fail to start until"
warn "you launch Steam at least once and log in."
return 0
fi
info "Launching Steam — close the window when finished..."
launch_steam_for_user || warn "Steam exited with non-zero status (continuing anyway)"
if steam_already_bootstrapped; then
info "Steam bootstrap complete — login verified"
else
warn "Steam closed but no login was detected. Gaming Mode may not"
warn "work until you've logged in at least once. Re-run the installer"
warn "or launch Steam manually to retry."
fi
}
###############################################################################
# PERFORMANCE CONFIGURATION
###############################################################################
setup_performance_permissions() {
local udev_rules_file="/etc/udev/rules.d/99-gaming-performance.rules"
local sudoers_file="/etc/sudoers.d/gaming-mode-sysctl"
if [[ -f "$udev_rules_file" ]] && [[ -f "$sudoers_file" ]]; then
info "Performance permissions already configured"
return 0
fi
echo ""
echo "================================================================"
echo " PERFORMANCE PERMISSIONS SETUP"
echo "================================================================"
echo ""
read -p "Set up passwordless performance controls? [Y/n]: " -n 1 -r
echo
[[ $REPLY =~ ^[Nn]$ ]] && { info "Skipping permissions setup"; return 0; }
# Udev rules for GPU frequency control (xe driver only).
# Both flat (early xe: gt_*_freq_mhz) and per-tile (newer xe on Xe2/Xe3:
# device/tile0/gt0/freq0/*) sysfs layouts exist depending on kernel + hardware.
# The chmod helper tolerates absent files so a single rule covers both.
if [[ ! -f "$udev_rules_file" ]]; then
info "Creating udev rules for Intel xe GPU performance control..."
sudo tee "$udev_rules_file" > /dev/null <<'UDEV_RULES'
# Gaming Mode Performance Control Rules - Intel xe driver
# Group-writable (video group) instead of world-writable for security.
KERNEL=="cpu[0-9]*", SUBSYSTEM=="cpu", ACTION=="add", RUN+="/bin/sh -c 'chgrp video /sys/devices/system/cpu/%k/cpufreq/scaling_governor 2>/dev/null && chmod 664 /sys/devices/system/cpu/%k/cpufreq/scaling_governor 2>/dev/null; :'"
# Flat layout (early xe): /sys/class/drm/card*/gt_{boost,min,max}_freq_mhz
KERNEL=="card[0-9]", SUBSYSTEM=="drm", DRIVERS=="xe", ACTION=="add", RUN+="/bin/sh -c 'for f in gt_boost_freq_mhz gt_min_freq_mhz gt_max_freq_mhz; do [ -e /sys/class/drm/%k/$f ] && chgrp video /sys/class/drm/%k/$f && chmod 664 /sys/class/drm/%k/$f; done; :'"
# Per-tile layout (xe on Xe2/Xe3 multi-tile): device/tile*/gt*/freq*/{min,max}_freq
KERNEL=="card[0-9]", SUBSYSTEM=="drm", DRIVERS=="xe", ACTION=="add", RUN+="/bin/sh -c 'for f in /sys/class/drm/%k/device/tile*/gt*/freq*/min_freq /sys/class/drm/%k/device/tile*/gt*/freq*/max_freq; do [ -e $f ] && chgrp video $f && chmod 664 $f; done; :'"
UDEV_RULES
sudo udevadm control --reload-rules || true
sudo udevadm trigger --subsystem-match=cpu --subsystem-match=drm || true
fi
# Sudoers rules for sysctl
if [[ ! -f "$sudoers_file" ]]; then
info "Creating sudoers rule for Performance Mode..."
sudo tee "$sudoers_file" > /dev/null << 'SUDOERS_PERF'
# Gaming Mode - Allow passwordless sysctl for performance tuning
%video ALL=(ALL) NOPASSWD: /usr/bin/sysctl -w kernel.sched_autogroup_enabled=*
%video ALL=(ALL) NOPASSWD: /usr/bin/sysctl -w kernel.sched_migration_cost_ns=*
%video ALL=(ALL) NOPASSWD: /usr/bin/sysctl -w kernel.sched_min_granularity_ns=*
%video ALL=(ALL) NOPASSWD: /usr/bin/sysctl -w kernel.sched_latency_ns=*
%video ALL=(ALL) NOPASSWD: /usr/bin/sysctl -w vm.swappiness=*
%video ALL=(ALL) NOPASSWD: /usr/bin/sysctl -w vm.dirty_ratio=*
%video ALL=(ALL) NOPASSWD: /usr/bin/sysctl -w vm.dirty_background_ratio=*
%video ALL=(ALL) NOPASSWD: /usr/bin/sysctl -w vm.dirty_writeback_centisecs=*
%video ALL=(ALL) NOPASSWD: /usr/bin/sysctl -w vm.dirty_expire_centisecs=*
%video ALL=(ALL) NOPASSWD: /usr/bin/sysctl -w fs.inotify.max_user_watches=*
%video ALL=(ALL) NOPASSWD: /usr/bin/sysctl -w fs.inotify.max_user_instances=*
%video ALL=(ALL) NOPASSWD: /usr/bin/sysctl -w fs.file-max=*
%video ALL=(ALL) NOPASSWD: /usr/bin/sysctl -w net.core.rmem_max=*
%video ALL=(ALL) NOPASSWD: /usr/bin/sysctl -w net.core.wmem_max=*
SUDOERS_PERF
sudo chmod 0440 "$sudoers_file"
fi
# Memory lock limits
local memlock_file="/etc/security/limits.d/99-gaming-memlock.conf"
if [[ ! -f "$memlock_file" ]]; then
info "Creating memlock limits..."
# Set memlock to ~25% of total RAM (in KB)
local total_ram_kb
total_ram_kb=$(awk '/^MemTotal:/ {print $2}' /proc/meminfo)
local memlock_kb=$(( total_ram_kb / 4 ))
# Clamp: minimum 2GB, maximum 16GB
(( memlock_kb < 2097152 )) && memlock_kb=2097152
(( memlock_kb > 16777216 )) && memlock_kb=16777216
info "Setting memlock to $(( memlock_kb / 1024 ))MB (based on $(( total_ram_kb / 1024 ))MB total RAM)"
sudo tee "$memlock_file" > /dev/null << MEMLOCKCONF
# Gaming memlock limits (auto-calculated: ~25% of total RAM)
* soft memlock ${memlock_kb}
* hard memlock ${memlock_kb}
MEMLOCKCONF
fi
# PipeWire low-latency config
local pipewire_conf_dir="/etc/pipewire/pipewire.conf.d"
local pipewire_conf="$pipewire_conf_dir/10-gaming-latency.conf"
if [[ ! -f "$pipewire_conf" ]]; then
info "Creating PipeWire low-latency configuration..."
sudo mkdir -p "$pipewire_conf_dir"
sudo tee "$pipewire_conf" > /dev/null << 'PIPEWIRECONF'
# Low-latency PipeWire tuning
context.properties = {
default.clock.min-quantum = 256
}
PIPEWIRECONF
fi
info "Performance permissions configured"
}
setup_shader_cache() {
local env_file="/etc/environment.d/99-shader-cache.conf"
if [[ -f "$env_file" ]]; then
info "Shader cache configuration already exists"
return 0
fi
echo ""
echo "================================================================"
echo " SHADER CACHE OPTIMIZATION"
echo "================================================================"
echo ""
read -p "Configure shader cache optimization? [Y/n]: " -n 1 -r
echo
[[ $REPLY =~ ^[Nn]$ ]] && return 0
info "Creating shader cache configuration..."
sudo mkdir -p /etc/environment.d
sudo tee "$env_file" > /dev/null << 'SHADERCACHE'
# Shader cache tuning for Intel Arc
MESA_SHADER_CACHE_MAX_SIZE=12G
MESA_SHADER_CACHE_DISABLE_CLEANUP=1
__GL_SHADER_DISK_CACHE=1
__GL_SHADER_DISK_CACHE_SIZE=12884901888
__GL_SHADER_DISK_CACHE_SKIP_CLEANUP=1
DXVK_STATE_CACHE=1
SHADERCACHE
sudo chmod 644 "$env_file"
info "Shader cache configured for Intel Arc"
}
setup_requirements() {
local -a required_packages=(
"steam" "gamescope" "mangohud" "python" "python-evdev"
"libcap" "gamemode" "curl" "pciutils" "ntfs-3g" "xcb-util-cursor"
)
local -a packages_to_install=()
for pkg in "${required_packages[@]}"; do
check_package "$pkg" || packages_to_install+=("$pkg")
done
if ((${#packages_to_install[@]})); then
info "The following packages are required: ${packages_to_install[*]}"
read -p "Install missing packages? [Y/n]: " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Nn]$ ]]; then
sudo pacman -S --needed --noconfirm "${packages_to_install[@]}" || die "package install failed"
else
die "Required packages missing"
fi
else
info "All required packages present."
fi
setup_performance_permissions
setup_shader_cache
# Grant cap_sys_nice to gamescope
if [[ "${PERFORMANCE_MODE,,}" == "enabled" ]] && command -v gamescope >/dev/null 2>&1; then
if ! getcap "$(command -v gamescope)" 2>/dev/null | grep -q 'cap_sys_nice'; then
echo ""
read -p "Grant cap_sys_nice to gamescope? [Y/n]: " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Nn]$ ]]; then
sudo setcap 'cap_sys_nice=eip' "$(command -v gamescope)" || warn "Failed to set capability"
info "Capability granted to gamescope"
fi
fi
fi
}
###############################################################################
# SESSION SWITCHING
###############################################################################
setup_session_switching() {
echo ""
echo "================================================================"
echo " SESSION SWITCHING SETUP (Hyprland <-> Gamescope)"
echo " Intel Arc / Xe Gaming Configuration"
echo "================================================================"
echo ""
read -p "Set up session switching? [Y/n]: " -n 1 -r
echo
[[ $REPLY =~ ^[Nn]$ ]] && return 0
# Use global REAL_USER and REAL_HOME for consistency
local current_user="$REAL_USER"
local user_home="$REAL_HOME"
#---------------------------------------------------------------------------
# Detect Monitor Resolution
#---------------------------------------------------------------------------
local monitor_width=1920
local monitor_height=1080
local monitor_refresh=60
local monitor_output=""
if command -v hyprctl >/dev/null 2>&1; then
local monitor_json
monitor_json=$(hyprctl monitors -j 2>/dev/null)
if [[ -n "$monitor_json" ]]; then
if command -v jq >/dev/null 2>&1; then
monitor_width=$(echo "$monitor_json" | jq -r '.[0].width // 1920') || monitor_width=1920
monitor_height=$(echo "$monitor_json" | jq -r '.[0].height // 1080') || monitor_height=1080
monitor_refresh=$(echo "$monitor_json" | jq -r '.[0].refreshRate // 60 | floor') || monitor_refresh=60
monitor_output=$(echo "$monitor_json" | jq -r '.[0].name // ""') || monitor_output=""
else
# Fallback: parse JSON without jq
monitor_width=$(echo "$monitor_json" | grep -o '"width":[[:space:]]*[0-9]*' | head -1 | grep -o '[0-9]*$') || monitor_width=1920
monitor_height=$(echo "$monitor_json" | grep -o '"height":[[:space:]]*[0-9]*' | head -1 | grep -o '[0-9]*$') || monitor_height=1080
# refreshRate can be decimal (e.g., 143.998), extract integer part
monitor_refresh=$(echo "$monitor_json" | grep -o '"refreshRate":[[:space:]]*[0-9.]*' | head -1 | grep -o '[0-9.]*$' | cut -d. -f1) || monitor_refresh=60
monitor_output=$(echo "$monitor_json" | grep -o '"name":[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"\([^"]*\)"$/\1/') || monitor_output=""
fi
fi
fi
info "Detected display: ${monitor_width}x${monitor_height}@${monitor_refresh}Hz${monitor_output:+ on $monitor_output}"
#---------------------------------------------------------------------------
# Install ChimeraOS Packages
#---------------------------------------------------------------------------
info "Checking for ChimeraOS gamescope-session packages..."
local -a aur_packages=()
local -a packages_to_remove=()
local -a steam_compat_scripts=(
"/usr/bin/steamos-session-select"
"/usr/bin/steamos-update"
"/usr/bin/jupiter-biosupdate"
"/usr/bin/steamos-select-branch"
)
# Check gamescope-session base package
if ! check_package "gamescope-session-git" && ! check_package "gamescope-session"; then
aur_packages+=("gamescope-session-git")
fi
# Check gamescope-session-steam package
if ! check_package "gamescope-session-steam-git"; then
if check_package "gamescope-session-steam"; then
warn "gamescope-session-steam (non-git) is installed but may be missing Steam compatibility scripts"
local scripts_missing=false
for script in "${steam_compat_scripts[@]}"; do
[[ ! -f "$script" ]] && { scripts_missing=true; break; }
done
$scripts_missing && packages_to_remove+=("gamescope-session-steam")
fi
aur_packages+=("gamescope-session-steam-git")
else
local scripts_missing=false
for script in "${steam_compat_scripts[@]}"; do
if [[ ! -f "$script" ]]; then
warn "gamescope-session-steam-git is installed but $script is missing!"
scripts_missing=true
break
fi
done
if $scripts_missing; then
warn "Reinstalling gamescope-session-steam-git to fix missing scripts..."
packages_to_remove+=("gamescope-session-steam-git")
aur_packages+=("gamescope-session-steam-git")
fi
fi
# Find AUR helper
local aur_helper=""
command -v yay >/dev/null 2>&1 && check_aur_helper_functional yay && aur_helper="yay"
[[ -z "$aur_helper" ]] && command -v paru >/dev/null 2>&1 && check_aur_helper_functional paru && aur_helper="paru"
# Remove problematic packages
if ((${#packages_to_remove[@]})) && [[ -n "$aur_helper" ]]; then
info "Removing incomplete packages: ${packages_to_remove[*]}"
sudo pacman -Rns --noconfirm "${packages_to_remove[@]}" 2>/dev/null || true
fi
# Install missing packages
if ((${#aur_packages[@]})); then
if [[ -n "$aur_helper" ]]; then
read -p "Install ChimeraOS session packages with $aur_helper? [Y/n]: " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Nn]$ ]]; then
info "Installing ChimeraOS gamescope-session packages..."
run_as_user "$aur_helper" -S --noconfirm --overwrite '/usr/share/gamescope-session*' --overwrite '/usr/bin/steamos-*' --answeredit None --answerclean None --answerdiff None "${aur_packages[@]}" || \
err "Failed to install gamescope-session packages"
fi
else
warn "No AUR helper found (yay/paru). Please install manually: ${aur_packages[*]}"
fi
else
info "ChimeraOS gamescope-session packages already installed (correct -git versions)"
fi
#---------------------------------------------------------------------------
# NetworkManager Integration
#---------------------------------------------------------------------------
info "Setting up NetworkManager integration..."
if systemctl is-active --quiet iwd; then
sudo mkdir -p /etc/NetworkManager/conf.d
sudo tee /etc/NetworkManager/conf.d/10-iwd-backend.conf > /dev/null << 'NM_IWD_CONF'
[device]
wifi.backend=iwd
wifi.scan-rand-mac-address=no
[main]
plugins=ifupdown,keyfile
[ifupdown]
managed=false
[connection]
connection.autoconnect-slaves=0
NM_IWD_CONF
fi
# NM start script
local nm_start_script="/usr/local/bin/gamescope-nm-start"
sudo tee "$nm_start_script" > /dev/null << 'NM_START'
#!/bin/bash
NM_MARKER="/tmp/.gamescope-started-nm"
if ! systemctl is-active --quiet NetworkManager.service; then
if systemctl start NetworkManager.service; then
touch "$NM_MARKER"
for _ in {1..20}; do
nmcli general status &>/dev/null && break
sleep 0.5
done
fi
fi
NM_START
sudo chmod +x "$nm_start_script"
# NM stop script - also restores iwd and bluetooth after gaming session
local nm_stop_script="/usr/local/bin/gamescope-nm-stop"
sudo tee "$nm_stop_script" > /dev/null << 'NM_STOP'
#!/bin/bash
NM_MARKER="/tmp/.gamescope-started-nm"
if [ -f "$NM_MARKER" ]; then
rm -f "$NM_MARKER"
systemctl stop NetworkManager.service 2>/dev/null || true
fi
# Restore iwd (WiFi) and bluetooth if they are enabled but got disrupted
if systemctl is-enabled --quiet iwd.service 2>/dev/null; then
systemctl restart iwd.service 2>/dev/null || true
fi
if systemctl is-enabled --quiet bluetooth.service 2>/dev/null; then
systemctl restart bluetooth.service 2>/dev/null || true
fi
NM_STOP
sudo chmod +x "$nm_stop_script"
#---------------------------------------------------------------------------
# Polkit Rules
#---------------------------------------------------------------------------
local polkit_created=false
local polkit_rules="/etc/polkit-1/rules.d/50-gamescope-networkmanager.rules"
if ! sudo test -f "$polkit_rules"; then
sudo mkdir -p /etc/polkit-1/rules.d
sudo tee "$polkit_rules" > /dev/null << 'POLKIT_RULES'
polkit.addRule(function(action, subject) {
if ((action.id == "org.freedesktop.NetworkManager.enable-disable-network" ||
action.id == "org.freedesktop.NetworkManager.enable-disable-wifi" ||
action.id == "org.freedesktop.NetworkManager.network-control" ||
action.id == "org.freedesktop.NetworkManager.wifi.scan" ||
action.id == "org.freedesktop.NetworkManager.settings.modify.system" ||
action.id == "org.freedesktop.NetworkManager.settings.modify.own" ||
action.id == "org.freedesktop.NetworkManager.settings.modify.hostname") &&
subject.isInGroup("wheel")) {
return polkit.Result.YES;
}
});
POLKIT_RULES
sudo chmod 644 "$polkit_rules"
polkit_created=true
fi
local udisks_polkit="/etc/polkit-1/rules.d/50-udisks-gaming.rules"
if ! sudo test -f "$udisks_polkit"; then
sudo tee "$udisks_polkit" > /dev/null << 'UDISKS_POLKIT'
polkit.addRule(function(action, subject) {
if ((action.id == "org.freedesktop.udisks2.filesystem-mount" ||
action.id == "org.freedesktop.udisks2.filesystem-mount-system" ||
action.id == "org.freedesktop.udisks2.filesystem-unmount-others" ||
action.id == "org.freedesktop.udisks2.encrypted-unlock" ||
action.id == "org.freedesktop.udisks2.power-off-drive") &&
subject.isInGroup("wheel")) {
return polkit.Result.YES;
}
});
UDISKS_POLKIT
sudo chmod 644 "$udisks_polkit"
polkit_created=true
fi
$polkit_created && { sudo systemctl restart polkit.service 2>/dev/null || true; info "Polkit rules created"; }
#---------------------------------------------------------------------------
# Gamescope Session Configuration
#---------------------------------------------------------------------------
info "Creating gamescope-session-plus configuration for Intel Arc..."
local env_dir="${user_home}/.config/environment.d"
local gamescope_conf="${env_dir}/gamescope-session-plus.conf"
run_as_user mkdir -p "$env_dir"
run_as_user tee "$gamescope_conf" > /dev/null << GAMESCOPE_CONF
# Gamescope Session Plus Configuration
# Generated by ARCGames Installer v${ARCGAMES_VERSION}
# NOTE: environment.d format does NOT use 'export' keyword.
# Variables here apply to the systemd --user session (so also to Hyprland).
# Gen-specific Intel workarounds live in the gamescope wrapper instead.
# OUTPUT_CONNECTOR is picked dynamically at session start by
# gaming-pick-connector (lid-state aware) so plugging an external display
# in/out doesn't require reinstalling.
# Adaptive sync / VRR disabled
ADAPTIVE_SYNC=0
# Generic mesa/Vulkan tuning (safe across all Intel gens)
DISABLE_LAYER_MESA_ANTI_LAG=1
VKD3D_CONFIG=dxr11,dxr
mesa_glthread=true
# Storage and drive management
STEAM_ALLOW_DRIVE_UNMOUNT=1
# Misc
FCITX_NO_WAYLAND_DIAGNOSE=1
SDL_VIDEO_MINIMIZE_ON_FOCUS_LOSS=0
GAMESCOPE_CONF
info "Created $gamescope_conf"
#---------------------------------------------------------------------------
# Session Wrapper Script
#---------------------------------------------------------------------------
# Build gen-specific Intel env block at install time so the wrapper is
# static at runtime. norbc is an Alchemist-only RBC artifact workaround;
# ANV_QUEUE_THREAD_DISABLE was a stability hack from old mesa and hurts
# perf on Battlemage/Xe2/Xe3 with current drivers.
local intel_env_lines=""
case "$INTEL_GPU_GEN" in
alchemist)
intel_env_lines='export INTEL_DEBUG=norbc'$'\n''export ANV_QUEUE_THREAD_DISABLE=1'
;;
battlemage|xe2|xe3)
intel_env_lines='# No gen-specific workarounds needed for '"$INTEL_GPU_GEN"
;;
*)
intel_env_lines='# Unknown Intel gen ('"${INTEL_GPU_GEN:-unset}"') - no workarounds applied'
;;
esac
local nm_wrapper="/usr/local/bin/gamescope-session-nm-wrapper"
sudo tee "$nm_wrapper" > /dev/null << NM_WRAPPER
#!/bin/bash
# Gamescope session wrapper (NM + keybind monitor)
# Intel gen detected at install time: ${INTEL_GPU_GEN:-unknown} (${INTEL_GPU_TIER:-unknown})
# Intel gen-specific environment (gamescope-only, does not leak to desktop)
${intel_env_lines}
log() { logger -t gamescope-wrapper "\$*"; echo "\$*"; }
cleanup() {
pkill -f steam-library-mount 2>/dev/null || true
pkill -f gaming-keybind-monitor 2>/dev/null || true
sudo -n /usr/local/bin/gamescope-nm-stop 2>/dev/null || true
rm -f /tmp/.gaming-session-active
}
trap cleanup EXIT INT TERM
# Start NetworkManager
sudo -n /usr/local/bin/gamescope-nm-start 2>/dev/null || {
log "Warning: Could not start NetworkManager - Steam network features may not work"
}
# Start Steam library drive auto-mounter
if [[ -x /usr/local/bin/steam-library-mount ]]; then
/usr/local/bin/steam-library-mount &
log "Steam library drive monitor started"
else
log "Warning: steam-library-mount not found - external Steam libraries will not auto-mount"
fi
# Mark gaming session active
echo "gamescope" > /tmp/.gaming-session-active
# Pre-flight check for keybind monitor
keybind_ok=true
if ! python3 -c "import evdev" 2>/dev/null; then
log "WARNING: python-evdev not installed - Super+Shift+R keybind disabled"
log "Fix: sudo pacman -S python-evdev"
keybind_ok=false
fi
if ! groups | grep -qw input; then
log "WARNING: User not in 'input' group - Super+Shift+R keybind disabled"
log "Fix: sudo usermod -aG input \$USER && log out/in"
keybind_ok=false
fi
if \$keybind_ok && ! ls /dev/input/event* >/dev/null 2>&1; then
log "WARNING: No input devices accessible - Super+Shift+R keybind disabled"
keybind_ok=false
fi
if \$keybind_ok; then
/usr/local/bin/gaming-keybind-monitor &
log "Keybind monitor started (Super+Shift+R to exit)"
else
log "Keybind monitor NOT started - use Steam > Power > Exit to Desktop instead"
fi
# Steam-specific environment variables
export QT_IM_MODULE=steam
export GTK_IM_MODULE=Steam
export STEAM_DISABLE_AUDIO_DEVICE_SWITCHING=1
export STEAM_ENABLE_VOLUME_HANDLER=1
# Pick output connector at runtime (lid state + display connection).
# Honors a manual override via /etc/gaming-mode.conf or ~/.gaming-mode.conf
# if FORCE_OUTPUT_CONNECTOR=<name> is set.
gm_force=""
for gm_conf in /etc/gaming-mode.conf "\$HOME/.gaming-mode.conf"; do
[[ -r "\$gm_conf" ]] || continue
val=\$(awk -F= '/^[[:space:]]*FORCE_OUTPUT_CONNECTOR[[:space:]]*=/ {gsub(/[[:space:]]/, "", \$2); print \$2; exit}' "\$gm_conf")
[[ -n "\$val" ]] && gm_force="\$val"
done
if [[ -n "\$gm_force" ]]; then
export OUTPUT_CONNECTOR="\$gm_force"
log "Using forced connector: \$OUTPUT_CONNECTOR (FORCE_OUTPUT_CONNECTOR)"
elif [[ -x /usr/local/bin/gaming-pick-connector ]]; then
chosen=\$(/usr/local/bin/gaming-pick-connector 2>/dev/null)
if [[ -n "\$chosen" ]]; then
export OUTPUT_CONNECTOR="\$chosen"
log "Using connector: \$OUTPUT_CONNECTOR (lid-aware autopick)"
else
log "Warning: gaming-pick-connector found no connected display"
fi
fi
log "Starting gamescope-session-plus (gen=${INTEL_GPU_GEN:-unknown})"
/usr/share/gamescope-session-plus/gamescope-session-plus steam
rc=\$?
exit "\$rc"
NM_WRAPPER
sudo chmod +x "$nm_wrapper"
#---------------------------------------------------------------------------
# SDDM Session Entry
#---------------------------------------------------------------------------
local session_desktop="/usr/share/wayland-sessions/gamescope-session-steam-nm.desktop"
sudo tee "$session_desktop" > /dev/null << 'SESSION_DESKTOP'
[Desktop Entry]
Name=Gaming Mode
Comment=Steam Big Picture with gamescope-session
Exec=/usr/local/bin/gamescope-session-nm-wrapper
Type=Application
DesktopNames=gamescope
SESSION_DESKTOP
#---------------------------------------------------------------------------
# Session Switch Scripts
#---------------------------------------------------------------------------
local os_session_select="/usr/lib/os-session-select"
sudo tee "$os_session_select" > /dev/null << 'OS_SESSION_SELECT'
#!/bin/bash
rm -f /tmp/.gaming-session-active
sudo -n /usr/local/bin/gaming-session-switch desktop 2>/dev/null || true
timeout 5 steam -shutdown 2>/dev/null || true
sleep 1
nohup sudo -n systemctl restart sddm &>/dev/null &
disown
exit 0
OS_SESSION_SELECT
sudo chmod +x "$os_session_select"
local switch_script="/usr/local/bin/switch-to-gaming"
sudo tee "$switch_script" > /dev/null << 'SWITCH_SCRIPT'
#!/bin/bash
sudo -n /usr/local/bin/gaming-session-switch gaming 2>/dev/null || {
notify-send -u critical -t 3000 "Gaming Mode" "Failed to update session config" 2>/dev/null || true
}
notify-send -u normal -t 2000 "Gaming Mode" "Switching to Gaming Mode..." 2>/dev/null || true
pkill gamescope 2>/dev/null || true
pkill -f gamescope-session 2>/dev/null || true
sleep 2
pkill -9 gamescope 2>/dev/null || true
pkill -9 -f gamescope-session 2>/dev/null || true
sudo -n chvt 2 2>/dev/null || true
sleep 0.3
sudo -n systemctl restart sddm
SWITCH_SCRIPT
sudo chmod +x "$switch_script"
local switch_desktop_script="/usr/local/bin/switch-to-desktop"
sudo tee "$switch_desktop_script" > /dev/null << 'SWITCH_DESKTOP'
#!/bin/bash
[[ ! -f /tmp/.gaming-session-active ]] && exit 0
rm -f /tmp/.gaming-session-active
sudo -n /usr/local/bin/gaming-session-switch desktop 2>/dev/null || true
timeout 5 steam -shutdown 2>/dev/null || true
sleep 1
pkill gamescope 2>/dev/null || true
pkill -f gamescope-session 2>/dev/null || true
sleep 2
pkill -9 gamescope 2>/dev/null || true
pkill -9 -f gamescope-session 2>/dev/null || true
sudo -n chvt 2 2>/dev/null || true
sleep 0.3
nohup sudo -n systemctl restart sddm &>/dev/null &
disown
exit 0
SWITCH_DESKTOP
sudo chmod +x "$switch_desktop_script"
#---------------------------------------------------------------------------
# Keybind Monitor (Python)
#---------------------------------------------------------------------------
local keybind_monitor="/usr/local/bin/gaming-keybind-monitor"
sudo tee "$keybind_monitor" > /dev/null << 'KEYBIND_MONITOR'
#!/usr/bin/env python3
"""Gaming Mode Keybind Monitor - Super+Shift+R to exit"""
import sys, subprocess, time
try:
import evdev
from evdev import ecodes
except ImportError:
sys.exit(1)
def find_keyboards():
keyboards = []
for path in evdev.list_devices():
try:
device = evdev.InputDevice(path)
caps = device.capabilities()
if ecodes.EV_KEY in caps:
keys = caps[ecodes.EV_KEY]
if ecodes.KEY_A in keys and ecodes.KEY_R in keys:
keyboards.append(device)
except Exception:
continue
return keyboards
def monitor_keyboards(keyboards):
meta_pressed = shift_pressed = False
from selectors import DefaultSelector, EVENT_READ
selector = DefaultSelector()
for kbd in keyboards:
selector.register(kbd, EVENT_READ)
try:
while True:
for key, mask in selector.select():
device = key.fileobj
try:
for event in device.read():
if event.type != ecodes.EV_KEY:
continue
if event.code in (ecodes.KEY_LEFTMETA, ecodes.KEY_RIGHTMETA):
meta_pressed = event.value > 0
elif event.code in (ecodes.KEY_LEFTSHIFT, ecodes.KEY_RIGHTSHIFT):
shift_pressed = event.value > 0
elif event.code == ecodes.KEY_R and event.value == 1:
if meta_pressed and shift_pressed:
subprocess.run(['/usr/local/bin/switch-to-desktop'])
return
except Exception:
continue
except KeyboardInterrupt:
pass
finally:
selector.close()
def main():
time.sleep(2)
keyboards = find_keyboards()
if keyboards:
monitor_keyboards(keyboards)
if __name__ == '__main__':
main()
KEYBIND_MONITOR
sudo chmod +x "$keybind_monitor"
#---------------------------------------------------------------------------
# Steam Library Auto-Mounter
#---------------------------------------------------------------------------
local steam_mount_script="/usr/local/bin/steam-library-mount"
sudo tee "$steam_mount_script" > /dev/null << 'STEAM_MOUNT'
#!/bin/bash
# Steam Library Drive Auto-Mounter
check_steam_library() {
local mount_point="$1"
[[ -d "$mount_point/steamapps" ]] || [[ -d "$mount_point/SteamLibrary/steamapps" ]] || \
[[ -f "$mount_point/libraryfolder.vdf" ]] || [[ -f "$mount_point/steamapps/libraryfolder.vdf" ]]
}
handle_device() {
local device="$1"
findmnt -n "$device" &>/dev/null && return
[[ "$device" =~ [0-9]$ ]] || return
local fstype
fstype="$(lsblk -n -o FSTYPE --nodeps "$device" 2>/dev/null)"
case "$fstype" in
ext4|ext3|ext2|btrfs|xfs|ntfs|vfat|exfat|f2fs) ;;
*) return ;;
esac
command -v udisksctl &>/dev/null || return
udisksctl mount -b "$device" --no-user-interaction 2>/dev/null || return
local mount_point
mount_point="$(findmnt -n -o TARGET "$device" 2>/dev/null)"
[[ -z "$mount_point" ]] && return
check_steam_library "$mount_point" || udisksctl unmount -b "$device" --no-user-interaction 2>/dev/null
}
shopt -s nullglob
for dev in /dev/sd*[0-9]* /dev/nvme*p[0-9]* /dev/mmcblk*p[0-9]*; do
[[ -b "$dev" ]] && handle_device "$dev"
done
shopt -u nullglob
action="" dev_path=""
udevadm monitor --kernel --property --subsystem-match=block 2>/dev/null | while read -r line; do
case "$line" in
ACTION=*) action="${line#ACTION=}" ;;
DEVNAME=*) dev_path="${line#DEVNAME=}" ;;
"")
if [[ "$action" == "add" && -n "$dev_path" && "$dev_path" =~ [0-9]$ && -b "$dev_path" ]]; then
sleep 1
handle_device "$dev_path"
fi
action="" dev_path=""
;;
esac
done
STEAM_MOUNT
sudo chmod +x "$steam_mount_script"
#---------------------------------------------------------------------------
# Output Connector Picker (lid-aware)
#
# Picks a DRM connector for gamescope at session start. Logic:
# lid closed -> prefer external (HDMI/DP), fall back to any connected
# lid open -> prefer internal (eDP/LVDS/DSI), fall back to external
# no lid -> any connected (desktop case)
# Lid state is read from /proc/acpi/button/lid/<id>/state when present.
# Connector status from /sys/class/drm/card*-*/status (Omarchy-style).
#---------------------------------------------------------------------------
local pick_connector_script="/usr/local/bin/gaming-pick-connector"
sudo tee "$pick_connector_script" > /dev/null << 'PICK_CONNECTOR'
#!/bin/bash
# gaming-pick-connector — print the best DRM connector for gamescope.
# Scoped to the xe-bound DRM card (matches the GPU the installer selected).
# Empty output means no connected display was found.
set -u
is_internal() {
case "$1" in
eDP-*|eDP*|LVDS-*|LVDS*|DSI-*|DSI*) return 0 ;;
*) return 1 ;;
esac
}
# Find the xe-bound card. If multiple are xe (e.g. Battlemage dGPU + Xe2/Xe3
# iGPU), prefer the dGPU (PCI bus != 00) — matches find_intel_gpu().
pick_xe_card() {
local best_card="" best_is_dgpu=-1
for d in /sys/class/drm/card[0-9]*; do
local n; n=$(basename "$d")
[[ "$n" == render* ]] && continue
[[ -L "$d/device/driver" ]] || continue
local drv; drv=$(basename "$(readlink "$d/device/driver")")
[[ "$drv" == "xe" ]] || continue
local pci; pci=$(basename "$(readlink -f "$d/device")")
local is_dgpu=1
[[ "$pci" =~ ^0000:00: ]] && is_dgpu=0
if (( is_dgpu > best_is_dgpu )); then
best_card="$n"
best_is_dgpu=$is_dgpu
fi
done
echo "$best_card"
}
xe_card=$(pick_xe_card)
# Fallback: if no xe card found (shouldn't happen post-install), scan all
glob_root="${xe_card:-card*}"
# Lid state: open / closed / unknown
lid_state="unknown"
for f in /proc/acpi/button/lid/*/state; do
[[ -r "$f" ]] || continue
case "$(awk '{print $NF}' "$f" 2>/dev/null)" in
closed) lid_state="closed" ;;
open) lid_state="open" ;;
esac
break
done
# Collect connected connectors on the chosen card only
connected=()
for status in /sys/class/drm/${glob_root}-*/status; do
[[ -r "$status" ]] || continue
[[ "$(<"$status")" == "connected" ]] || continue
name="$(basename "${status%/status}")"
name="${name#card*-}"
connected+=("$name")
done
(( ${#connected[@]} == 0 )) && exit 0
# Split into internal vs external preserving order
internal=() external=()
for c in "${connected[@]}"; do
if is_internal "$c"; then internal+=("$c"); else external+=("$c"); fi
done
# Pick by lid state
if [[ "$lid_state" == "closed" ]]; then
if (( ${#external[@]} > 0 )); then echo "${external[0]}"; exit 0; fi
(( ${#internal[@]} > 0 )) && echo "${internal[0]}"; exit 0
fi
# lid open or unknown (desktop): prefer internal, fall back to external
if (( ${#internal[@]} > 0 )); then echo "${internal[0]}"; exit 0; fi
(( ${#external[@]} > 0 )) && echo "${external[0]}"
PICK_CONNECTOR
sudo chmod +x "$pick_connector_script"
#---------------------------------------------------------------------------
# SDDM Configuration
#---------------------------------------------------------------------------
sudo mkdir -p /etc/sddm.conf.d
local sddm_gaming_conf="/etc/sddm.conf.d/zz-gaming-session.conf"
local autologin_user="$current_user"
[[ -f /etc/sddm.conf.d/autologin.conf ]] && \
autologin_user=$(sed -n 's/^User=//p' /etc/sddm.conf.d/autologin.conf 2>/dev/null | head -1)
[[ -z "$autologin_user" ]] && autologin_user="$current_user"
sudo tee "$sddm_gaming_conf" > /dev/null << SDDM_GAMING
[Autologin]
User=${autologin_user}
Session=hyprland-uwsm
Relogin=true
SDDM_GAMING
local session_helper="/usr/local/bin/gaming-session-switch"
sudo tee "$session_helper" > /dev/null << 'SESSION_HELPER'
#!/bin/bash
CONF="/etc/sddm.conf.d/zz-gaming-session.conf"
[[ ! -f "$CONF" ]] && exit 1
case "$1" in
gaming) sed -i 's/^Session=.*/Session=gamescope-session-steam-nm/' "$CONF" ;;
desktop) sed -i 's/^Session=.*/Session=hyprland-uwsm/' "$CONF" ;;
*) exit 1 ;;
esac
SESSION_HELPER
sudo chmod +x "$session_helper"
#---------------------------------------------------------------------------
# Sudoers Rules
#---------------------------------------------------------------------------
local sudoers_session="/etc/sudoers.d/gaming-session-switch"
sudo tee "$sudoers_session" > /dev/null << 'SUDOERS_SWITCH'
%video ALL=(ALL) NOPASSWD: /usr/local/bin/gaming-session-switch
%video ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart sddm
%video ALL=(ALL) NOPASSWD: /usr/bin/chvt
%wheel ALL=(ALL) NOPASSWD: /usr/bin/systemctl start NetworkManager.service
%wheel ALL=(ALL) NOPASSWD: /usr/bin/systemctl stop NetworkManager.service
%wheel ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart iwd.service
%wheel ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart bluetooth.service
%wheel ALL=(ALL) NOPASSWD: /usr/local/bin/gamescope-nm-start
%wheel ALL=(ALL) NOPASSWD: /usr/local/bin/gamescope-nm-stop
SUDOERS_SWITCH
sudo chmod 0440 "$sudoers_session"
#---------------------------------------------------------------------------
# Hyprland Keybind
#
# Combo: SUPER SHIFT, S. The screenshot template at this combo is
# commented out by default in Omarchy's bindings.conf (Print Screen key
# handles screenshots), so the combo is free in practice. SUPER SHIFT, G
# is taken by Signal in Omarchy.
#
# Exec is wrapped with `uwsm-app --` on Omarchy so the helper runs under
# the user's uwsm graphical session like every other Omarchy launcher.
#---------------------------------------------------------------------------
local hypr_bindings_conf="${user_home}/.config/hypr/bindings.conf"
local hypr_main_conf="${user_home}/.config/hypr/hyprland.conf"
local keybind_target=""
if [[ -f "$hypr_bindings_conf" ]]; then
keybind_target="$hypr_bindings_conf"
elif [[ -f "$hypr_main_conf" ]]; then
keybind_target="$hypr_main_conf"
fi
# Build the keybind line — uwsm-app wrapping if available (Omarchy convention)
local keybind_exec="/usr/local/bin/switch-to-gaming"
if command -v uwsm-app >/dev/null 2>&1; then
keybind_exec="uwsm-app -- /usr/local/bin/switch-to-gaming"
fi
local keybind_line="bindd = SUPER SHIFT, S, Gaming Mode, exec, ${keybind_exec}"
# Collision pre-check across all sourced binding files (Omarchy + user).
# Active (uncommented) lines only — Omarchy's screenshot template at this
# combo is commented out by default and doesn't count as a collision.
local -a binding_sources=()
[[ -d "${user_home}/.local/share/omarchy/default/hypr/bindings" ]] && \
while IFS= read -r f; do binding_sources+=("$f"); done < <(find "${user_home}/.local/share/omarchy/default/hypr/bindings" -name '*.conf' 2>/dev/null)
[[ -f "$hypr_bindings_conf" ]] && binding_sources+=("$hypr_bindings_conf")
[[ -f "$hypr_main_conf" ]] && binding_sources+=("$hypr_main_conf")
local collision=""
if ((${#binding_sources[@]})); then
collision=$(grep -hE '^bindd? = SUPER SHIFT, S,' "${binding_sources[@]}" 2>/dev/null | head -1)
fi
if [[ -n "$collision" && "$collision" != *"switch-to-gaming"* ]]; then
warn "SUPER SHIFT, S is already bound: ${collision}"
warn "Skipping keybind install. Add manually with a free combo:"
warn " ${keybind_line}"
elif [[ -n "$keybind_target" ]] && ! grep -q "switch-to-gaming" "$keybind_target" 2>/dev/null; then
run_as_user tee -a "$keybind_target" > /dev/null << HYPR_GAMING
# Gaming Mode - Switch to Gamescope session (Intel Arc)
${keybind_line}
HYPR_GAMING
info "Added Gaming Mode keybind to $(basename "$keybind_target")"
elif [[ -z "$keybind_target" ]]; then
warn "No Hyprland config found - please add keybind manually:"
warn " ${keybind_line}"
fi
# Reload Hyprland — prefer omarchy-restart-hyprctl on Omarchy
if is_omarchy && command -v omarchy-restart-hyprctl >/dev/null 2>&1; then
run_as_user omarchy-restart-hyprctl >/dev/null 2>&1 || true
elif command -v hyprctl >/dev/null 2>&1 && hyprctl monitors >/dev/null 2>&1; then
hyprctl reload >/dev/null 2>&1 || true
fi
#---------------------------------------------------------------------------
# Done
#---------------------------------------------------------------------------
echo ""
echo "================================================================"
echo " SESSION SWITCHING CONFIGURED (Intel Arc)"
echo "================================================================"
echo ""
echo " Usage:"
echo " - Press Super+Shift+S in Hyprland to switch to Gaming Mode"
echo " - Press Super+Shift+R in Gaming Mode to return to Hyprland"
echo ""
}
###############################################################################
# MAIN ENTRY POINT
###############################################################################
execute_setup() {
sudo -k
sudo -v || die "sudo authentication required"
validate_environment
check_intel_gpu
echo ""
echo "================================================================"
echo " ARCGames INSTALLER v${ARCGAMES_VERSION}"
echo " Intel Arc Gaming Mode Setup (dGPU + Xe2/Xe3 iGPU)"
echo "================================================================"
echo ""
check_steam_dependencies
bootstrap_steam_login
setup_requirements
setup_session_switching
if [[ "$NEEDS_RELOGIN" -eq 1 ]]; then
echo ""
echo "================================================================"
echo " IMPORTANT: LOG OUT REQUIRED"
echo "================================================================"
echo ""
echo " User groups have been updated. Please log out and log back in."
echo ""
else
echo ""
echo "================================================================"
echo " SETUP COMPLETE"
echo "================================================================"
echo ""
echo " To switch to Gaming Mode: Press Super+Shift+S"
echo " To return to Desktop: Press Super+Shift+R"
echo ""
fi
}
show_help() {
cat << EOF
ARCGames Installer v${ARCGAMES_VERSION}
Gaming Mode installer for Intel Arc GPUs (dGPU and Xe2/Xe3 iGPU).
Usage: $0 [OPTIONS]
Options:
--help, -h Show this help message
--version Show version number
EOF
}
###############################################################################
# COMMAND LINE HANDLING
###############################################################################
case "${1:-}" in
--help|-h) show_help; exit 0 ;;
--version) echo "ARCGames Installer v${ARCGAMES_VERSION}"; exit 0 ;;
"") execute_setup ;;
*) echo "Unknown option: $1"; exit 1 ;;
esac