Page 1 of 1

[Linux] Yet another Factorio Launcher (Wayland, mimalloc)

Posted: Wed May 25, 2022 1:28 am
by ptx0
This is a script I've been using for some time, and completely overhauled it for public consumption today.
v1.1:
- better mimalloc support so that we're using 2M pages instead of 1G, which typically aren't configured on systems. ignore libhugetlbfs unless mimalloc is absent, it only degrades performance for mimalloc.
- fixes for KDocker icon detection (Xorg only, not Wayland compatible)
- use hugeadm to manage hugepage pool dynamically. this is a new dependency, make sure you've got it installed so you don't have to statically allocate your pages at boot (no bootloader config changes required)
- use hugeadm to restore the hugepages
- $HOME/factorio.env allows persistently overriding a few variables, like DRI_PRIME and __GL_YIELD="USELEEP" if you need those for your specific system
- one of those is the HUGEADM_PAGESZ variable, which can be used to specify how much RAM you would like Factorio to have access to. default is 8192, which would reserve 16GiB of 2MiB pages.
- wmctrl invocation fixes so that the window can be renamed correctly (Xorg only, not Wayland compatible)

v1.0:
- mimalloc, libhugetlbfs support for better mem allocators
- set CPU to performance mode and then restore to previous mode after Factorio exits. this doesn't happen on battery, if you're in a laptop. (requires upower to detect laptops)
- if not using Wayland, KDocker may be used to plop Factorio into your system tray (custom icon paths supported)
- if running Wayland, SDL2 override
- allows running multiple copies of Factorio from the one script, folder name provided as the first argument
- run Factorio reniced to -19 so it gets more CPU time
- disable Optimus for laptops with NVIDIA support, unless you've explicitly enabled Optimus. it will leave it alone.
- renames the Factorio window to exclude the version string, adding the folder name for the installation as a suffix instead. this makes it easier to use in OBS Studio.

Re: Yet another Factorio Launcher (Wayland, mimalloc)

Posted: Wed May 25, 2022 1:28 am
by ptx0

Code: Select all

#!/bin/bash

#### CONFIGURATION

if [ -f "${HOME}/.factorio.env" ]; then
	# Place environment variables here and we'll grab them on each startup.
	# This is so you can override the config values without modifying this script.
	source "${HOME}/.factorio.env"
else
	### Change this to wherever your factorio installations are stored.
	### Multiple copies of Factorio may be installed alongside each other.
	### Value:   		 /home/$USER/factorio	
	### Example module path: /home/$USER/factorio/vanilla	
	export FACTORIO_PATH="${HOME}/factorio"

	### 16GiB of Hugepages might be too much for your system.
	export HUGEADM_PAGESZ
	HUGEADM_PAGESZ=8192
	export HUGEADM_POOLSIZE
	HUGEADM_POOLSIZE=2097152
fi

#### FUNCTIONS

disable_optimus() {
	if [ -z "$__GLX_VENDOR_LIBRARY_NAME" ] && [ -z "$__NV_PRIME_RENDER_OFFLOAD" ]; then
		echo "Overriding NVIDIA Optimus flags with:"
		export DRI_PRIME=0
		export __NV_PRIME_RENDER_OFFLOAD=0
		export __GLX_VENDOR_LIBRARY_NAME=NOT_nvidia
	else
		echo "Not disabling pre-existing NVIDIA Optimus flags:"
	fi
	# Use AMD or Intel iGPU.
	echo "	DRI_PRIME: ${DRI_PRIME}"
	echo "	__NV_PRIME_RENDER_OFFLOAD: ${__NV_PRIME_RENDER_OFFLOAD}"
	echo "	__GLX_VENDOR_LIBRARY_NAME: ${__GLX_VENDOR_LIBRARY_NAME}"
}

enable_mimalloc() {

	! [ -z "${MIMALLOC_DISABLE}" ] && echo "mimalloc disabled." && return
	LIBMIMALLOC_PATH='/usr/lib64/libmimalloc.so'
	if ! [ -f "${LIBMIMALLOC_PATH}" ]; then
		echo "mimalloc doesn't exist. You might really want to install this."
	else
		echo "Enabled mimalloc."
		export MIMALLOC_LARGE_OS_PAGES=1 # Use 2MiB pages
		export MIMALLOC_RESERVE_HUGE_OS_PAGES=0 # Use n 1GiB pages
		export MALLOC_ARENA_MAX=1 # Tell Glibc to only allocate memory in a single "arena".
		export MIMALLOC_PAGE_RESET=0 # Signal when pages are empty
		export MIMALLOC_EAGER_COMMIT_DELAY=4 # The first 4MiB of allocated memory won't be hugepages
		export MIMALLOC_SHOW_STATS=0 # Display mimalloc stats
		export LD_PRELOAD="${LD_PRELOAD} ${LIBMIMALLOC_PATH}"
		return
	fi
	LIBHUGETLBFS_PATH="/usr/lib64/libhugetlbfs.so"
	if [ -f "${LIBHUGETLBFS_PATH}" ]; then
		export LD_PRELOAD="${LD_PRELOAD} ${LIBHUGETLBFS_PATH}"
		export HUGETLB_MORECORE=thp
		export HUGETLB_RESTRICT_EXE=factorio
		echo "Enabled libhugetlbfs parameters for easy huge page support."
	else
		echo "You do not even have libhugetlbfs installed. There is very little we can do for your performance here."
	fi
}

configure_mempool() {
	export HUGEADM_PATH

	export HUGEADM_CURRENTSIZE

	# Current pool size (allocated hugepages)
	HUGEADM_CURRENTSIZE=$(hugeadm --pool-list | grep "${HUGEADM_POOLSIZE}" | awk '{ print $3; }')
	# Maximum pool size (how many hugepages)
	HUGEADM_MAXIMUMSIZE=$(hugeadm --pool-list | grep "${HUGEADM_POOLSIZE}" | awk '{ print $4; }')
	HUGEADM_PATH=$(which hugeadm)
	if [ -z "${HUGEADM_PATH}" ]; then
		echo 'hugeadm is not installed. Was unable to configure the system hugepages pool size.'
	fi
	export HUGEADM_FREE
	export TARGET_HUGEPAGESZ=0 # By default, we'll assume we need to allocate zero pages.
	HUGEADM_FREE=$(expr "${HUGEADM_MAXIMUMSIZE}" - "${HUGEADM_CURRENTSIZE}")
	if [ "${HUGEADM_FREE}" -lt "${HUGEADM_PAGESZ}" ]; then
		# We don't have enough free hugepages. Let's go for gold and increase it by the current desired amount.
		TARGET_HUGEPAGESZ=$(expr "${HUGEADM_PAGESZ}" - "${HUGEADM_FREE}")
		sudo "${HUGEADM_PATH}" --hard --pool-pages-max "2MB:${TARGET_HUGEPAGESZ}" || echo "Could not configure hugepages pool size via hugeadm."
		echo "Added ${TARGET_HUGEPAGESZ} to system hugepages memory pool."
	else
		echo "We have enough free pages (${HUGEADM_FREE} / ${HUGEADM_MAXIMUMSIZE}). Continuing."
	fi
}

restore_mempool() {
	if [ "${TARGET_HUGEPAGESZ}" -gt 0 ]; then
		echo "Being a good citizen and restoring memory pool size back to ${HUGEADM_MAXIMUMSIZE}."
		sudo "${HUGEADM_PATH}" --hard --pool-pages-max "2MB:${HUGEADM_MAXIMUMSIZE}" || echo "Could not configure hugepages pool size via hugeadm."
	else
		TOTAL_MEM_WASTED=$(expr "${HUGEADM_MAXIMUMSIZE}" \* 2)
		echo "There were no extra hugepages allocated at startup, so there is nothing to clean up now. You could free ${TOTAL_MEM_WASTED}M for other applications by reducing the maximum pool size to zero by default."
	fi
}
cpu_performance_mode() {
	# Default is to assume we're on a desktop.
	export CURRENT_GOVERNOR
	CURRENT_GOVERNOR=$(cat /sys/devices/system/cpu/cpufreq/policy*/scaling_governor | head -n1)
	export IS_LAPTOP=0
	# Detect if we're on a laptop, and if so, if we're charged/charging.
	upower -i /org/freedesktop/UPower/devices/battery_BAT0 | grep -o 'power supply.*yes' > /dev/null 2>&1  && export IS_LAPTOP=1 && export POWER_STATE=$(upower -i /org/freedesktop/UPower/devices/battery_BAT0 | grep -E -o '(fully-charged|charging)')
	# If we're not on a laptop, or we are fully charged/charging, go into "performance" CPU governor
	if [ "$IS_LAPTOP" -eq 0 ] || ! [ -z "$POWER_STATE" ]; then
		echo "Enabling CPU governor: performance"
		# This requires sudo access. It's not important to have this work, but it might be nice on low power systems or huge save files.
		echo performance | sudo tee /sys/devices/system/cpu/cpufreq/policy*/scaling_governor > /dev/null
	else
		echo "Not changing CPU governor. We are not plugged in to a power source."
	fi
}
cpu_governor_reset() {
	echo "Enabling CPU governor: ${CURRENT_GOVERNOR}"
	echo "${CURRENT_GOVERNOR}" | sudo tee /sys/devices/system/cpu/cpufreq/policy*/scaling_governor > /dev/null
}
custom_icon() {
	export KDOCKER_PREFIX='' # Do not use kdocker unless it exists.
	export KDOCKER_PATH=$(which kdocker)
	if [ -z "${KDOCKER_PATH}" ]; then
		echo "Not running KDocker, it is not installed."
		return
	fi
	if ! [ -z "${WAYLAND_DISPLAY}" ]; then
		echo "Not running KDocker on Wayland."
		return
	fi
	echo "Detect custom tray icon..."
	export ICON_PATH="${FACTORIO_PATH}/'${MODULE}'/data/core/graphics/factorio-icon.png" # Use the default Factorio icon as provided by Wube
	ls "${HOME}/.icons/factorio.png" > /dev/null 2>&1 && export ICON_PATH="${HOME}/.icons/factorio.png" # Use an override icon, if user wants a special one.
	ls "${MODULE_DIR}/icon.png" > /dev/null 2>&1 && export ICON_PATH="${MODULE_DIR}/icon.png" # Use a module-specific override icon, so that each install can have their own icon.
	export KDOCKER_SECONDS=60 # Wait 60 seconds until Factorio is most likely ready.
	export KDOCKER_PREFIX="${KDOCKER_PATH} -i ${ICON_PATH} -x "${FACTORIO_PID}" -d ${KDOCKER_SECONDS}"
	echo "Using KDocker command: ${KDOCKER_PREFIX}"
	${KDOCKER_PREFIX} && export KDOCKER_PID=$!
}
kill_existing_factorio() {
	export FACTORIO_PID
	FACTORIO_PID=$(fuser "${MODULE_DIR}/.lock" 2>/dev/null | grep -E -o '[0-9]+' | head -n1)
	if ! [ -z "$FACTORIO_PID" ]; then
		kill -9 "${FACTORIO_PID}"
	fi
}
wait_for_factorio() {
	COUNT=0
	while [ "$COUNT" -lt 10 ]
	do
		COUNT=$(expr $COUNT + 1)
		export FACTORIO_PID
		FACTORIO_PID=$(fuser "${MODULE_DIR}/.lock" 2>/dev/null | grep -E -o '[0-9]+' | head -n1)
		[ -n "${FACTORIO_PID}" ] && break
		sleep .25
	done
	if [ "$COUNT" -eq 10 ]; then
		echo "Giving up. Couldn't start Factorio client."
		exit 1
	fi
}
renice_factorio() {
	echo "Looking for pid and renicing to realtime status..."
	sudo renice -n -19 "${FACTORIO_PID}" > /dev/null 2>&1 
}
rename_window() {
	WMCTRL_PATH=$(which wmctrl)
	if [ -z "${WMCTRL_PATH}" ]; then
		echo "Changing Factorio window title for consistency, so that OBS can find it easily..."
		WINDOW_ID=$(wmctrl -l -p | awk -v vpid="$FACTORIO_PID" '$3 == vpid{ print $1 }')
		if ! [ -z "${WINDOW_ID}" ]; then
			echo "No window ID found."
			return
		fi
		FACTORIO_TITLE="Factorio ${MODULE}"
		wmctrl -i -r "${WINDOW_ID}" -I "Factorio" -N "${FACTORIO_TITLE}"
		echo "Window renamed to ${FACTORIO_TITLE}."
	fi
}
factorio_ping() {
	echo "Watching for Factorio process to end."
	while true; do
		sleep 30
		if ! [ -d "/proc/${FACTORIO_PID}" ]; then
			echo "It looks like Factorio has exited. Ending script execution..."
			break
		fi
	done
}
kill_kdocker() {
	if ! [ -z "${KDOCKER_PREFIX}" ] && ! [ -z "${KDOCKER_PID}" ]; then
		echo "Killing KDocker..."
		kill -9 ${KDOCKER_PID}
	fi
}
wayland_sdl_override(){
	if ! [ -z "${WAYLAND_DISPLAY}" ]; then
		export QT_QPA_PLATFORM=wayland
		export SDL_DYNAMIC_API=/usr/lib64/libSDL2.so
		if ! [ -f "${SDL_DYNAMIC_API}" ]; then
			echo "SDL 2.0 library not found?"
		else
			echo "SDL2 override for Wayland enabled."
		fi
		export SDL_VIDEODRIVER=wayland
	else
		echo "Not running Wayland, no SDL2 override needed."
	fi
}

#### SCRIPT BELOW

### Using Wayland display server? We'll overload SDL2 with the OS-provided copy and force-enable Wayland output.
wayland_sdl_override

### This is laptop-specific optimization for Factorio.
### If it's not provided by the OS, we assume you do not want to use the discrete GPU for rendering Factorio.
### This wakes the GPU up (obviously) and ensures the laptop uses more power than it needs to. If you don't 
### want this behaviour, export these variables from your shell profile, or change these values to 1 & nvidia
disable_optimus



### Are we plugged in, or on a desktop?
### Switch CPU governor to performance mode.
cpu_performance_mode

### Support multiple copies of Factorio.
### To have spaces in the name, just 'Quote It' when running the script.
### Example: ./factorio.sh "Space Exploration"
MODULE="vanilla"
if ! [ -z "$1" ]; then
	MODULE="$1"
fi
MODULE_DIR="${FACTORIO_PATH}/${MODULE}"
pushd "${MODULE_DIR}" > /dev/null || (echo "Can not enter installation directory. Ensure it exists: ${MODULE_DIR}" && exit 1)

### Detect an existing Factorio instance, and kill it.
kill_existing_factorio


### Microsoft mimalloc is useful to boost FPS in megabases as a more efficient memory allocator.
configure_mempool
enable_mimalloc

### Try and execute Factorio.
./bin/x64/factorio > "${HOME}/.factorio.log" 2>&1 &

#### Wait for Factorio and grab the PID. Exit if we don't find it in 10 attempts.
wait_for_factorio

### We do not want to use KDocker on Wayland. It is not supported yet.
custom_icon

### Now that it's running, renice it so we can grab more CPU.
renice_factorio

### Wait until Factorio exits.
factorio_ping

# Unconfigure hugepages if we've altered the system environment.
restore_mempool

### Go back to the previous CPU governor.
cpu_governor_reset

### End the KDocker process.
kill_kdocker

exit 0

Re: [Linux] Yet another Factorio Launcher (Wayland, mimalloc)

Posted: Sat Jun 18, 2022 9:55 pm
by ptx0
v1.1:
- better mimalloc support so that we're using 2M pages instead of 1G, which typically aren't configured on systems. ignore libhugetlbfs unless mimalloc is absent, it only degrades performance for mimalloc.
- fixes for KDocker icon detection (Xorg only, not Wayland compatible)
- use hugeadm to manage hugepage pool dynamically. this is a new dependency, make sure you've got it installed so you don't have to statically allocate your pages at boot (no bootloader config changes required)
- use hugeadm to restore the hugepages
- $HOME/factorio.env allows persistently overriding a few variables, like DRI_PRIME and __GL_YIELD="USELEEP" if you need those for your specific system
- one of those is the HUGEADM_PAGESZ variable, which can be used to specify how much RAM you would like Factorio to have access to. default is 8192, which would reserve 16GiB of 2MiB pages.
- wmctrl invocation fixes so that the window can be renamed correctly (Xorg only, not Wayland compatible)
updated the post with v1.1 of the launcher, it properly detects icon paths and more.