Skip to content

Instantly share code, notes, and snippets.

@al3rez
Created June 15, 2026 04:51
Show Gist options
  • Select an option

  • Save al3rez/8d71eaea155c74ca9069d8d75210acbb to your computer and use it in GitHub Desktop.

Select an option

Save al3rez/8d71eaea155c74ca9069d8d75210acbb to your computer and use it in GitHub Desktop.
RPCS3 (PS3 emulator) setup on Arch — install, firmware, DualShock 3 over Bluetooth (pure BlueZ), per-game config
#!/usr/bin/env bash
# ds3-bluez-setup.sh — pair a DualShock 3 / Sixaxis over Bluetooth using pure BlueZ
# (no sixad). Run with the controller connected via USB cable. Needs sudo.
#
# sudo ./ds3-bluez-setup.sh
#
# What it does (the 5 steps):
# 1. Writes this host's adapter MAC into the controller (hidraw report 0xf5)
# 2. Trusted-but-not-bonded device entry under /var/lib/bluetooth
# 3. Pre-seeds a correct DS3 HID SDP record in BlueZ's cache (the key fix)
# 4. ClassicBondedOnly=false in /etc/bluetooth/input.conf
# 5. disable_ertm=1 via /etc/modprobe.d (DS3 L2CAP ERTM workaround)
# Then: unplug USB, press PS button -> connects, /dev/input/js* appears.
set -euo pipefail
[[ $EUID -eq 0 ]] || { echo "run with sudo"; exit 1; }
# --- full DS3 HID SDP record (host-independent; same for any DS3) ---
SDP="3601920900000A000100000900013503191124090004350D35061901000900113503190011090006350909656E09006A0901000900093508350619112409010009000D350F350D350619010009001335031900110901002513576972656C65737320436F6E74726F6C6C65720901012513576972656C65737320436F6E74726F6C6C6572090102251B536F6E7920436F6D707574657220456E7465727461696E6D656E740902000901000902010901000902020800090203082109020428010902052801090206359A35980822259405010904A101A102850175089501150026FF00810375019513150025013500450105091901291381027501950D0600FF8103150026FF0005010901A10075089504350046FF0009300931093209358102C0050175089527090181027508953009019102750895300901B102C0A1028502750895300901B102C0A10285EE750895300901B102C0A10285EF750895300901B102C0C0090207350835060904090901000902082800090209280109020A280109020B09010009020C093E8009020D280009020E2800"
# --- 1. discover adapter MAC + the DS3 hidraw node ---
ADAPTER=$(bluetoothctl show 2>/dev/null); ADAPTER=$(printf '%s\n' "$ADAPTER" | awk '/Controller/{print $2; exit}')
[[ -n "$ADAPTER" ]] || { echo "no Bluetooth adapter found"; exit 1; }
HIDRAW=""
for h in /sys/class/hidraw/hidraw*; do
grep -q "054C:.*0268" "$h/device/uevent" 2>/dev/null && { HIDRAW="/dev/$(basename "$h")"; break; }
done
[[ -n "$HIDRAW" ]] || { echo "DS3 not found on USB — plug it in via cable first"; exit 1; }
echo "adapter: $ADAPTER"
echo "controller: $HIDRAW"
# --- write this host's adapter MAC as the controller's master (report 0xf5) ---
python3 - "$HIDRAW" "$ADAPTER" <<'PY'
import sys, fcntl, struct
dev, mac = sys.argv[1], sys.argv[2]
HIDIOCSFEATURE = lambda n: (3<<30)|(ord('H')<<8)|0x06|(n<<16)
b = [int(x,16) for x in mac.split(':')]
buf = bytes([0xf5,0x00]+b)
with open(dev,'wb') as f:
fcntl.ioctl(f, HIDIOCSFEATURE(len(buf)), buf)
print("master set to", mac)
PY
# controller's own BD address (uppercase, for the dir name)
DS3=$(python3 - "$HIDRAW" <<'PY'
import sys, fcntl
dev=sys.argv[1]
HIDIOCGFEATURE=lambda n:(3<<30)|(ord('H')<<8)|0x07|(n<<16)
buf=bytearray(18); buf[0]=0xf2
with open(dev,'rb+') as f: fcntl.ioctl(f,HIDIOCGFEATURE(len(buf)),buf,True)
print(':'.join('%02X'%x for x in buf[4:10]))
PY
)
echo "controller MAC: $DS3"
# --- 2. trusted device entry (no LinkKey: trusted but not bonded) ---
DDIR="/var/lib/bluetooth/$ADAPTER/$DS3"
mkdir -p "$DDIR"
cat > "$DDIR/info" <<EOF
[General]
Name=Sony PLAYSTATION(R)3 Controller
Class=0x002504
SupportedTechnologies=BR/EDR;
Trusted=true
Blocked=false
Services=00001124-0000-1000-8000-00805f9b34fb;
CablePairing=false
WakeAllowed=true
[DeviceID]
Source=1
Vendor=1356
Product=616
Version=257
EOF
# --- 3. pre-seed the HID SDP record in the cache ---
mkdir -p "/var/lib/bluetooth/$ADAPTER/cache"
cat > "/var/lib/bluetooth/$ADAPTER/cache/$DS3" <<EOF
[General]
Name=Sony PLAYSTATION(R)3 Controller
[ServiceRecords]
0x00010000=$SDP
EOF
# --- 4. allow trusted-but-not-bonded HID ---
if grep -q '^ClassicBondedOnly' /etc/bluetooth/input.conf 2>/dev/null; then
sed -i 's/^ClassicBondedOnly=.*/ClassicBondedOnly=false/' /etc/bluetooth/input.conf
elif grep -q '^#ClassicBondedOnly' /etc/bluetooth/input.conf 2>/dev/null; then
sed -i 's/^#ClassicBondedOnly=.*/ClassicBondedOnly=false/' /etc/bluetooth/input.conf
else
sed -i '/^\[General\]/a ClassicBondedOnly=false' /etc/bluetooth/input.conf
fi
# --- 5. disable L2CAP ERTM (persist) ---
echo 'options bluetooth disable_ertm=1' > /etc/modprobe.d/bluetooth-ds3.conf
echo Y > /sys/module/bluetooth/parameters/disable_ertm 2>/dev/null || true
systemctl restart bluetooth
sleep 2
echo
echo "Done. Now: unplug the USB cable and press the PS button."
echo "Check with: ls /dev/input/js* and bluetoothctl info $DS3"

RPCS3 (PS3 Emulator) Setup on Arch Linux — Full Working Notes

Hardware: Ryzen 7 7800X3D + RTX 5070, Arch Linux, BlueZ 5.86. Everything below is what actually worked, including the gotchas.


1. Install RPCS3

Used the official prebuilt from the AUR (updates frequently, no Flatpak sandbox quirks):

yay -S --noconfirm rpcs3-bin

Binary lands at /usr/bin/rpcs3 (wraps /opt/rpcs3/usr/bin/rpcs3).


2. PS3 firmware (required, legal, free from Sony)

RPCS3 ships no firmware. Get the official PS3UPDAT.PUP from Sony and install it.

# current official firmware URL (v4.93) — from playstation.com/support PS3 system software page
cd /tmp
curl -L -o PS3UPDAT.PUP \
  "http://dus01.ps3.update.playstation.net/update/ps3/image/us/2026_0318_a2b60b6ac1d2e49e230144345616927c/PS3UPDAT.PUP"

# install it (firmware install needs the GUI; --no-gui refuses)
rpcs3 --installfw /tmp/PS3UPDAT.PUP

Verify (should be ~140 modules):

find ~/.config/rpcs3/dev_flash/sys/external -name '*.sprx' | wc -l

3. Add games

PS3 game dumps come in two shapes:

  • JB folder dump (PS3_DISC.SFB + PS3_GAME/ with a plain EBOOT.BIN) → already decrypted, just use it. No decryptor needed.
  • Encrypted redump .iso + .dkey → needs decryption. On Linux use ps3dec (NOT the Windows-only PS3-Quick-Disc-Decryptor).

Example (Uncharted 2 was a JB folder inside a .rar):

mkdir -p ~/Games/PS3
unrar x -o+ ~/Downloads/Uncharted_2_Among_Thieves_BCUS98123.rar ~/Games/PS3/

Then in RPCS3: File → Add Games → point at ~/Games/PS3/. Registers in ~/.config/rpcs3/games.yml by serial (e.g. BCUS98123).


4. DualShock 3 / Sixaxis over Bluetooth — THE HARD PART

Why it's painful

The DS3 cannot pair like a normal BT device. Its internal "master" address must be set to your adapter's MAC over USB first. BlueZ has a sixaxis plugin that's supposed to automate this, but on BlueZ 5.86 the plugin loads and never fires on hotplug — so you must do it all manually.

Symptoms hit along the way:

  • Controller LED 1 lit but never connected → its master was still set to an old host.
  • After sixpair, BlueZ accepted the ACL link but no HID input device appeared.
  • Root cause: BlueZ only pre-loads bonded devices at startup; a hand-written unbonded entry was ignored, so on connect it did fresh SDP discovery (DS3 returns a broken record) → no HID UUID → no joystick.
  • Fix: pre-seed a correct HID SDP record in BlueZ's cache + a trusted device entry with the HID UUID, set ClassicBondedOnly=false, and disable L2CAP ERTM.

The 5 steps (all persist across reboots)

  1. Write this host's adapter MAC into the controller (hidraw feature report 0xf5).
  2. Create a trusted-but-not-bonded device entry under /var/lib/bluetooth/<adapter>/<pad>/info (Class=0x002504, HID service UUID 00001124-..., no LinkKey).
  3. Plant a correct DS3 HID SDP record in /var/lib/bluetooth/<adapter>/cache/<pad> (fixes Could not parse HID SDP record). The hex is host-independent (it's the controller's descriptor).
  4. ClassicBondedOnly=false in /etc/bluetooth/input.conf (DS3 is trusted, never bonds).
  5. options bluetooth disable_ertm=1 in /etc/modprobe.d/bluetooth-ds3.conf (stops connect/disconnect flapping).

Then: unplug USB, press the PS button → connects, /dev/input/js* appears and stays. Press PS anytime to reconnect (it's trusted; no agent needed). One DS3 = one player; the 4 LEDs are just player-slot indicators (1–4), not 4 devices. A 2nd player needs a 2nd physical controller (run the same script with it plugged in).

Reusable script

Auto-detects the adapter MAC + controller node, so it works on any machine (e.g. a ThinkPad too). Run with the pad plugged in via USB:

sudo ./ds3-bluez-setup.sh

(Full script is the second file in this gist: ds3-bluez-setup.sh.)


5. Performance fix — memlock limit

RPCS3 logs Failed to set RLIMIT_MEMLOCK size to 2 GiB. Default was 8 MB. Raise it (needs reboot/re-login):

# PAM
echo '*  soft  memlock  unlimited
*  hard  memlock  unlimited' | sudo tee /etc/security/limits.d/99-rpcs3-memlock.conf

# systemd (this is the one that governs GUI/graphical-session apps)
sudo sed -i 's/^#DefaultLimitMEMLOCK=.*/DefaultLimitMEMLOCK=infinity/' /etc/systemd/system.conf
sudo sed -i 's/^#DefaultLimitMEMLOCK=.*/DefaultLimitMEMLOCK=infinity/' /etc/systemd/user.conf

6. Per-game config — Uncharted 2 (BCUS98123)

Per-game config lives at ~/.config/rpcs3/config/custom_configs/config_<SERIAL>.yml (copy the global config.yml then edit). Settings from the RPCS3 wiki for this title:

Setting Value Why
Renderer Vulkan (RTX 5070)
Write Color Buffers On fixes overly-bright scenes
Read Depth Buffer On fixes flickering; needed by async streaming
Asynchronous Texture Streaming On perf
Resolution scale threshold (Minimum Scalable Dimension) 160 fixes bloom artifacts when upscaling
ZCULL accuracy Precise (default) reflections / heat-wave
Anisotropic filter Auto (default) anything else glitches
Resolution Scale 200–300% RTX 5070 has tons of headroom at native 30fps

IMPORTANT gotcha: patches caused crashes

The wiki's "highly recommended" patches (Disable SPU Post-processing / Disable Mesh Trimming / Enable GPU Lighting) caused a fatal Unknown/Illegal opcode PPU crash (~3:30 in, during level load) on this RPCS3 build. Clearing the cache did not help. Disabling all patches = stable.

# disable all patches
sed -i 's/Enabled: true/Enabled: false/g' ~/.config/rpcs3/patch_config.yml

To force a clean recompile after changing patches/settings, clear the game cache:

rm -rf ~/.cache/rpcs3/cache/<SERIAL>/ppu-* ~/.cache/rpcs3/cache/<SERIAL>/spu-*

Notes that confused us (all normal)

  • 30fps is full speed. Uncharted 2 is a native 30fps game; there is no 60fps unlock patch for serial BCUS98123. A locked, steady 30 = the game running perfectly.
  • White boxes/rectangles = shaders still compiling (Shader Mode: Async Recompiler). Objects render as white placeholders until their shader builds in the background, then pop in. They cache out as you play and don't return on revisits. (Only the Disable SPU Post-processing patch + 100% scale causes persistent white boxes — and that patch is off.)

TL;DR command history

yay -S --noconfirm rpcs3-bin
curl -L -o /tmp/PS3UPDAT.PUP "<sony firmware url>"
rpcs3 --installfw /tmp/PS3UPDAT.PUP
unrar x game.rar ~/Games/PS3/         # JB folder dumps need no decryption
sudo ./ds3-bluez-setup.sh             # DS3 over Bluetooth (pure BlueZ)
# memlock + per-game config as above; leave Uncharted 2 patches OFF
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment