Thursday 2 December 2021

Tutorial: Enter/Exit Batocera from Raspbian OS on Raspberry Pi 4

The Batocera OS is highly optimized for games, as a result, it has left out all other capabilities a normal operating system has. This makes life a bit difficult for users who wants both playing games and doing computing stuffs, because you need to install a separate operating system, need to buy and flash a separate microSD/SSD/USD-HDD, and reboot in order to switch between operating systems.


In this tutorial, we describe how to directly run Batocera's EmulationStation from inside Raspbian OS on Raspberry Pi 4, the same principle can be applied to PC or other systems (such as Ubuntu) as well.

This is an advance Linux tutorial, you are assumed to be familiar with basic Linux principles, file systems, Linux kernels, files and commands.

The steps are as follows:
1. Download and extract all Batocera files.
i). You have already downloaded the Batocera image for RPi (either the official image from Batocera Official Download , or pre-made game pack images from Arcade Punks, etc.) This is typically an XXX.img.gz or XXX.img file. If it is gzipped, extract it first, gunzip XXX.img.gz
ii). mount the image as a loop device, losetup -vfP XXX.img
iii). mount the two partitions /boot (the boot partition BATOCERA) and /rootfs (the main data partition SHARE containing all roms/save-games/game-previews/etc.)
mkdir -p /mnt/batocera-boot /mnt/batocera-rootfs
mount /dev/loop0p1 /mnt/batocera-boot
mount /dev/loop0p2 /mnt/batocera-rootfs
iv). copy over all folders in these two partitions, you can use any folder other than /opt (this step is optional since you can access files on mounted loop device images directly, however it will be super slow when accessing files)
cp -rfpP /mnt/batocera-boot /opt/batocera-boot
cp -rfpP /mnt/batocera-rootfs /opt/batocera-rootfs

2. Mount the /opt/batocera-boot/boot/batocera SquashFS (~850MB) root file-system which contains emulationstation as well as all its dependencies. For space-speed efficiency, Batocera uses ZSTD-compressed Squash File System for its main root file-system, however, the stock Raspbian kernel does not support SquashFS. Therefore, you can either:

2a. re-compile and install the Raspbian kernel with SquashFS (ZSTD format) enabled, refer to the RPi4 stock kernel guide, make sure you backup the /boot partition before you install.

2b. install squashfs-tools and extract manually:
apt install squashfs-tools
unsquashfs -f -d /opt/batocera-squashfs /opt/batocera-boot/boot/batocera

3. Create the following shell script on your desktop and make it executable:

#!/bin/bash

MOUNT_POINT=192.168.50.2:/nfs/batocera
DISPLAY_MANAGER=lightdm

# run as root if not
if [ "`whoami`" != root ]; then
    sudo "$0"
    exit 0
fi

# install tmux if not
if [ ! "`which tmux`" ]; then
    apt-get install -y tmux
fi


set -e -x -o pipefail

mount_if_not () {
    if [ $# -lt 2 ]; then
        echo "Usage: mount_if_not source target [options]"
        exit 1
    fi
    if ! mountpoint -q "$2"; then
        mkdir -p "$2"
        if [ `ls "$1"/ 2>/dev/null | wc -l` -ge 1 ]; then
            mount --bind "$1" "$2"
        else
            mount "${@:3}" "$1" "$2"
        fi
    fi
}

# 1. mount Batocera /boot
mount_if_not $MOUNT_POINT-boot /batocera/boot

# 2. mount Batocera root filesystem (/batocera/rootfs) as an overlay (upper:/bool/boot/overlay, lower:/bool/boot/batocera)
# 2a. create an overlay on memory
mount_if_not tmpfs /batocera/overlay_root -t tmpfs -o size=256M
for d in base overlay work saved; do
    mkdir -p /batocera/overlay_root/$d
done
# 2b. copy out overlay files into upper
mount_if_not /batocera/boot/boot/overlay /batocera/overlay_root/saved
cp -pr /batocera/overlay_root/saved/* /batocera/overlay_root/overlay/
umount /batocera/overlay_root/saved
# 2c. mount batocera squashfs onto lower
if [ `ls $MOUNT_POINT-squashfs/ 2>/dev/null | wc -l` -ge 5 ]; then
    mount_if_not $MOUNT_POINT-squashfs /batocera/overlay_root/base
else
    mount_if_not /batocera/boot/boot/batocera /batocera/overlay_root/base
fi
# 2d. mount the overlay filesystem
mount_if_not overlay /batocera/chroot -t overlay -o rw,lowerdir=/batocera/overlay_root/base,upperdir=/batocera/overlay_root/overlay,workdir=/batocera/overlay_root/work

# 3. mount Batocera data partition onto /userdata
mount_if_not $MOUNT_POINT-rootfs /batocera/chroot/userdata

# 4. bind batocera/boot and batocera/overlay folder
mount_if_not /batocera/boot /batocera/chroot/boot --bind
mount_if_not /batocera/overlay_root /batocera/chroot/overlay --bind

# 5. mount bind system runtime directories
for p in sys proc dev run var tmp; do
    mount_if_not /$p /batocera/chroot/$p --bind
done


# Prepare shutdown signal for returning to Raspbian
if [ ! -p /batocera/chroot/signal.fifo ]; then
    mkfifo /batocera/chroot/signal.fifo
fi
echo -e "#/bin/bash\necho exit>/signal.fifo" >/batocera/chroot/sbin/shutdown
chmod +x /batocera/chroot/sbin/shutdown

# Switch into Batocera, run commands in tmux so as to survive logging out the current session
if [ ! "`tmux ls | grep switch_to_bato`" ]; then
    if [ ! -s /etc/rc.local ]; then
        echo -e "#!/bin/sh -e\nexit 0" >/etc/rc.local
        chmod +x /etc/rc.local
    fi
    if [ ! "`grep switch_to_bato /etc/rc.local`" ]; then
        sed -i "s:^exit:tmux new-session -s switch_to_bato -d -x 240 -y 60\nexit:g" /etc/rc.local
        reboot
    fi
    echo "tmux daemon has not started during boot, please reboot"
    exit 0
fi
tmux send-keys -t switch_to_bato.0 -l "service $DISPLAY_MANAGER stop; chroot /batocera/chroot/ /etc/init.d/S31emulationstation start;read </batocera/chroot/signal.fifo; service lightdm start"
tmux send-keys Enter

The above script should be run as root, or it will sudo itself. It will install tmux if not yet installed and add a line into /etc/rc.local to launch tmux server during boot if not added. It will reboot the first time it adds the tmux line into /etc/rc.local. The reason why I use tmux is because you are running this script inside the current display manager (lightdm), so if you stop the display manager, you will get logged out, all processes including this script itself will be killed before it can chroot and launch Batocera's emulationstation.

In summary, the overall underlying principle is very simple: mount all Batocera's file systems as it does (in BATOCERA/boot/initrd.gz's /init), chroot into its root folder, and start its emulationstation by /etc/init.d/S31emulationstation start. In addition, since Batocera's emulationstation acts a standalone display manager, you need to stop your current display manager (there are lightdm, gdm, xdm, sddm, etc., Raspbian uses lightdm) before entering Batocera and restart your current display manager after exiting Batocera.