Batocera Vrr With A Rotated Portrait Monitor
06 Jul 2026 [batocera
linux
emulation
]
I use a monitor mounted physically in portrait orientation, 90 degrees clockwise, for playing vertical shooters using Batocera and various other systems. Batocera’s built-in display rotation option supports this setup, in that it can display EmulationStation and games correctly, but it breaks FreeSync/VRR meaning that it is not possible to run games at their original framerate, where this isn’t the default 60fps.
When attempting DoDonPachi (should run at approx 57 FPS) using this setup, the RetroArch FPS counter reports the desired 57 but the monitor’s built in counter reports a consistent 60.
Batocera uses Xorg rotation and it turns out this does not work with AMD VRR. The AMD Xorg driver documentation describes (at least) two ways of sending frames to the display, Present and TearFree. VRR does not work with TearFree and TearFree is always used when the screen is rotated.
The solution that worked was to leave Batocera’s normal Xorg rotation in place for EmulationStation, then use a Batocera hook to remove that rotation when a libretro game starts. RetroArch then rotates its own output internally. When the game exits, the hook restores the original Xorg rotation so the frontend is upright again.
Why the simpler options did not work
Leaving Xorg rotated during gameplay kept the picture the right way up, but VRR did not engage properly.
EmulationStation also supports --screenrotate 1, but on my Batocera build that only rotated the rendered image without correctly swapping the logical width and height. The result was black bars, cut-off UI, and incorrect window placement. Adding --resolution 1080 1920 did not fix it.
I also confirmed that editing /userdata/system/configs/retroarch/retroarchcustom.cfg directly is not the right answer. Batocera regenerates that file when launching a game. The persistent setting needs to go through Batocera’s own config system instead.
Prerequisites
Enable FreeSync or Adaptive Sync in the monitor’s own settings.
In Batocera, enable Variable Refresh Rate in the system settings.
Confirm that Xorg sees the monitor as VRR-capable:
DISPLAY=:0 xrandr --props | grep -E ' connected|vrr_capable'
On the working machine, the relevant output looked like:
DP-1 connected primary ...
vrr_capable: 1
Confirm that the AMD Xorg configuration enables variable refresh:
grep -Ri VariableRefresh /etc/X11
Expected result:
/etc/X11/xorg.conf.d/20-amdgpu.conf: Option "VariableRefresh" "true"
The Xorg log should also confirm that the option was accepted:
grep -i VariableRefresh /var/log/Xorg.0.log
Expected output:
VariableRefresh: enabled
Steps to replicate
Step 1: Configure the normal EmulationStation rotation
Set Batocera’s display rotation to 90 degrees clockwise:
batocera-settings-set display.rotate 1
Remove any experimental EmulationStation custom arguments:
batocera-settings-set es.customsargs ""
Reboot:
reboot
After reboot, EmulationStation should appear upright and correctly fill the portrait display.
Step 2: Configure RetroArch’s internal rotation
When Xorg is changed back to normal orientation during gameplay, RetroArch needs to rotate its output internally.
Set RetroArch to rotate by 270 degrees:
batocera-settings-set global.retroarch.video_rotation 3
The values are:
0 = no rotation
1 = 90 degrees
2 = 180 degrees
3 = 270 degrees
On this setup, 1 produced an upside-down portrait image. 3 produced the correct orientation.
Step 3: Install the Batocera game hook
Create the scripts directory:
mkdir -p /userdata/system/scripts
Create /userdata/system/scripts/rotate-vrr.sh with the following contents:
#!/bin/bash
OUTPUT="DP-1"
TOKEN_FILE="/userdata/system/rotate-vrr.token"
LOG_FILE="/userdata/system/logs/rotate-vrr.log"
export DISPLAY=:0
log() {
echo "$(date '+%F %T') $*" >> "$LOG_FILE"
}
get_display_state() {
xrandr 2>/dev/null | grep "^${OUTPUT} "
}
is_right() {
local state
state="$(get_display_state)"
case "$state" in
*"1080x1920+0+0 right "*)
return 0
;;
esac
return 1
}
is_normal() {
local state
state="$(get_display_state)"
case "$state" in
*"1920x1080+0+0 (normal "*)
return 0
;;
esac
return 1
}
retroarch_running() {
pgrep -x retroarch >/dev/null
}
token_is_current() {
[ "$(cat "$TOKEN_FILE" 2>/dev/null)" = "$1" ]
}
make_token() {
echo "$1-$$-$(date +%s)"
}
rotate_normal() {
xrandr --output "$OUTPUT" --rotate normal
}
rotate_right() {
xrandr --output "$OUTPUT" --rotate right
}
watch_game_start() {
local token="$1"
local stable=0
local attempts=0
log "Launch watcher started: $token"
while [ "$attempts" -lt 300 ]; do
token_is_current "$token" || {
log "Launch watcher superseded: $token"
return
}
if retroarch_running && is_right; then
stable=$((stable + 1))
elif is_normal; then
log "Launch display already normal"
stable=0
break
else
stable=0
fi
if [ "$stable" -ge 3 ]; then
log "Launch state ready; rotating normal"
rotate_normal
stable=0
break
fi
attempts=$((attempts + 1))
sleep 0.1
done
if [ "$attempts" -ge 300 ]; then
log "Launch watcher timed out"
return
fi
attempts=0
while [ "$attempts" -lt 100 ]; do
token_is_current "$token" || {
log "Launch verification superseded: $token"
return
}
retroarch_running || {
log "RetroArch stopped during launch verification"
return
}
if is_normal; then
stable=$((stable + 1))
else
stable=0
log "Launch rotation was overwritten; reapplying normal"
rotate_normal
fi
if [ "$stable" -ge 10 ]; then
log "Launch rotation verified"
return
fi
attempts=$((attempts + 1))
sleep 0.1
done
log "Launch verification timed out"
}
watch_game_stop() {
local token="$1"
local stable=0
local attempts=0
log "Exit watcher started: $token"
while [ "$attempts" -lt 300 ]; do
token_is_current "$token" || {
log "Exit watcher superseded: $token"
return
}
if ! retroarch_running && is_right; then
stable=$((stable + 1))
if [ "$stable" -ge 3 ]; then
log "Exit display already restored"
return
fi
elif ! retroarch_running && is_normal; then
stable=$((stable + 1))
if [ "$stable" -ge 3 ]; then
log "Exit state ready; rotating right"
rotate_right
stable=0
break
fi
else
stable=0
fi
attempts=$((attempts + 1))
sleep 0.1
done
if [ "$attempts" -ge 300 ]; then
log "Exit watcher timed out"
return
fi
attempts=0
while [ "$attempts" -lt 100 ]; do
token_is_current "$token" || {
log "Exit verification superseded: $token"
return
}
if is_right; then
stable=$((stable + 1))
else
stable=0
log "Exit rotation was overwritten; reapplying right"
rotate_right
fi
if [ "$stable" -ge 10 ]; then
log "Exit rotation verified"
return
fi
attempts=$((attempts + 1))
sleep 0.1
done
log "Exit verification timed out"
}
[ "$3" = "libretro" ] || exit 0
token="$(make_token "$1")"
echo "$token" > "$TOKEN_FILE"
case "$1" in
gameStart)
watch_game_start "$token" >/dev/null 2>&1 &
;;
gameStop)
watch_game_stop "$token" >/dev/null 2>&1 &
;;
esac
exit 0
Make the script executable:
chmod +x /userdata/system/scripts/rotate-vrr.sh
How the script works
Batocera calls scripts in /userdata/system/scripts for events including gameStart and gameStop.
For DoDonPachi under FBNeo, the observed arguments were:
gameStart fbneo libretro fbneo /userdata/roms/fbneo/ddonpach.zip
gameStop fbneo libretro fbneo /userdata/roms/fbneo/ddonpach.zip
The third argument is therefore libretro, so the script ignores non-libretro emulators.
On launch, the script does not rotate immediately just because the event fired. Instead, a background watcher waits until RetroArch is actually running and the display is still in the stable rotated frontend state, then changes Xorg back to normal.
On exit, it waits until RetroArch has stopped and the display is in the stable normal gameplay state, then restores the rotated frontend state.
The generation token prevents an older watcher from acting after a newer launch or exit event has happened, and the verification loops reapply the expected orientation if Batocera briefly overwrites it.
Step 4: Test the setup
Clear the old log:
> /userdata/system/logs/rotate-vrr.log
Launch a RetroArch game such as DoDonPachi.
While the game is running, confirm the Xorg orientation:
DISPLAY=:0 xrandr | grep '^DP-1'
Expected result:
DP-1 connected primary 1920x1080+0+0 (normal left inverted right x axis y axis) ...
Confirm the generated RetroArch rotation:
grep '^video_rotation' /userdata/system/configs/retroarch/retroarchcustom.cfg
Expected result:
video_rotation = "3"
The game should now be upright on the portrait monitor, and the monitor’s refresh-rate display should no longer stay fixed at 60. For DoDonPachi, it should fluctuate through values around 56, 57, 58 and occasionally 60.
Exit the game and confirm the frontend orientation:
DISPLAY=:0 xrandr | grep '^DP-1'
Expected result:
DP-1 connected primary 1080x1920+0+0 right ...
EmulationStation should be upright and fill the display correctly.
Inspect the hook log:
cat /userdata/system/logs/rotate-vrr.log
A successful launch and exit should include messages similar to:
Launch watcher started
Launch state ready; rotating normal
Launch rotation verified
Exit watcher started
Exit state ready; rotating right
Exit rotation verified
Useful diagnostic commands
Show the active display mode:
DISPLAY=:0 batocera-resolution currentMode
Show the active XRandR orientation:
DISPLAY=:0 xrandr | grep '^DP-1'
Show VRR capability:
DISPLAY=:0 xrandr --props | grep -E ' connected|vrr_capable'
Show AMD VRR configuration:
grep -Ri VariableRefresh /etc/X11
Show whether Xorg accepted VRR:
grep -i VariableRefresh /var/log/Xorg.0.log
Show the RetroArch VRR setting:
grep vrr_runloop_enable /userdata/system/configs/retroarch/retroarchcustom.cfg
Expected result:
vrr_runloop_enable = true
Show the RetroArch internal rotation:
grep '^video_rotation' /userdata/system/configs/retroarch/retroarchcustom.cfg
Expected result:
video_rotation = "3"
Show the running RetroArch command:
ps -ef | grep '[r]etroarch'
Show the hook log:
cat /userdata/system/logs/rotate-vrr.log
Hardware-specific values
The script assumes OUTPUT="DP-1". If the monitor uses another connector, determine it with:
DISPLAY=:0 xrandr | grep ' connected'
Then change OUTPUT near the top of the script.
The state checks also assume a normal mode of 1920x1080, a rotated mode of 1080x1920, and a frontend rotation of right. Those values need adjusting for a display with a different native resolution or physical orientation.
Final configuration summary
Batocera configuration:
display.rotate=1
global.retroarch.video_rotation=3
Persistent hook:
/userdata/system/scripts/rotate-vrr.sh
Frontend:
Xorg rotation = right
desktop = 1080x1920
EmulationStation upright
RetroArch game:
Xorg rotation = normal
desktop = 1920x1080
RetroArch internal rotation = 3
VRR active
After game exit:
Xorg rotation restored to right
EmulationStation upright