Last active
October 9, 2025 14:40
-
-
Save jmiskovic/533f75fa4b57c0cb04dde6866d3a6309 to your computer and use it in GitHub Desktop.
Measure input lag
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # Measures input lag of an application that draws a blue circle on the last known cursor location | |
| # automates cursor motion for consistency; takes a screenshot and measures distance; averages over N runs | |
| CURSOR_COLOR="#7c7c7c" # set this to your system cursor primary color | |
| CIRCLE_COLOR="#0000ff" # the measured app should draw a circle of this color | |
| TESTS_N=50 | |
| get_window_info() { | |
| local xwininfo_output=$(xwininfo) | |
| WINDOW_ID=$(echo "$xwininfo_output" | grep -m 1 "Window id:" | cut -d ' ' -f 4) | |
| WIDTH=$(echo "$xwininfo_output" | grep "Width" | cut -d ":" -f 2 | tr -d " ") | |
| HEIGHT=$(echo "$xwininfo_output" | grep "Height" | cut -d ":" -f 2 | tr -d " ") | |
| TOP_X=$(echo "$xwininfo_output" | grep "Absolute upper-left X" | cut -d ":" -f 2 | tr -d " ") | |
| TOP_Y=$(echo "$xwininfo_output" | grep "Absolute upper-left Y" | cut -d ":" -f 2 | tr -d " ") | |
| echo "Dimensions: $WIDTH x $HEIGHT" | |
| echo "Window position: $TOP_X, $TOP_Y" | |
| wmctrl -ia $WINDOW_ID | |
| } | |
| move_mouse_to_left_edge() { | |
| xdotool mousemove --window $WINDOW_ID -- 0 $((HEIGHT / 2)) | |
| } | |
| smooth_move_mouse_to_right_edge() { | |
| local step=1 | |
| local measure_point=$((2 * WIDTH / 3)) | |
| local start_time=$(date +%s%N) | |
| local distance=0 | |
| while [ $distance -le $measure_point ]; do | |
| xdotool mousemove_relative -- $step 0 | |
| distance=$((distance + step)) | |
| done | |
| local end_time=$(date +%s%N) | |
| local elapsed_time=$((end_time - start_time)) | |
| local actual_speed=$(echo "scale=2; $distance / ($elapsed_time / 1000000000)" | bc) | |
| capture_screenshot | |
| measure_distance $actual_speed | |
| } | |
| capture_screenshot() { | |
| xfce4-screenshooter -m --no-border -w -c | |
| screenshot_base64=$(xclip -selection clipboard -t image/png -o | base64) | |
| } | |
| find_entity_position() { | |
| local color=$1 | |
| bounding_box=$(echo "$screenshot_base64" | base64 -d | | |
| magick - -fuzz 10% -fill white -opaque "$color" -fill black +opaque white -format "%@" info:-) | |
| if [[ -z "$bounding_box" ]]; then | |
| echo "" | |
| return | |
| fi | |
| local width=$(echo $bounding_box | cut -d'x' -f1) | |
| local height=$(echo $bounding_box | cut -d'x' -f2 | cut -d'+' -f1) | |
| local x=$(echo $bounding_box | cut -d'+' -f2) | |
| local y=$(echo $bounding_box | cut -d'+' -f3) | |
| local center_x=$(echo "$x + $width / 2" | bc) | |
| local center_y=$(echo "$y + $height / 2" | bc) | |
| echo "$center_x $center_y" | |
| } | |
| measure_distance() { | |
| local actual_speed=$1 | |
| cursor_position=$(find_entity_position $CURSOR_COLOR) | |
| circle_position=$(find_entity_position $CIRCLE_COLOR) | |
| if [[ -z "$cursor_position" ]]; then | |
| echo "Cursor not found in screenshot." | |
| return | |
| fi | |
| if [[ -z "$circle_position" ]]; then | |
| echo "Circle not found in screenshot." | |
| return | |
| fi | |
| cursor_x=$(echo $cursor_position | cut -d' ' -f1) | |
| cursor_y=$(echo $cursor_position | cut -d' ' -f2) | |
| circle_x=$(echo $circle_position | cut -d' ' -f1) | |
| circle_y=$(echo $circle_position | cut -d' ' -f2) | |
| distance=$(echo "scale=2; sqrt(($cursor_x - $circle_x)^2 + ($cursor_y - $circle_y)^2)" | bc) | |
| # Convert actual_speed from pixels per second to pixels per millisecond | |
| actual_speed_ms=$(echo "scale=2; $actual_speed / 1000" | bc) | |
| lag=$(echo "scale=2; $distance / $actual_speed_ms" | bc) | |
| echo -n "$lag, " | |
| lags+=($lag) | |
| } | |
| echo "Click on app window to measure its input lag" | |
| get_window_info | |
| echo "Taking control over mouse, please wait until $TESTS_N tests are captured" | |
| sleep 0.2 | |
| lags=() | |
| for ((test_i = 1; test_i <= $TESTS_N; test_i++)); do | |
| move_mouse_to_left_edge | |
| smooth_move_mouse_to_right_edge | |
| done | |
| echo | |
| if [ ${#lags[@]} -gt 1 ]; then | |
| total=0 | |
| for ((i = 1; i < ${#lags[@]}; i++)); do | |
| total=$(echo "$total + ${lags[$i]}" | bc) | |
| done | |
| average=$(echo "scale=1; $total / (${#lags[@]} - 1)" | bc) | |
| sum_sq_diff=0 | |
| for ((i = 1; i < ${#lags[@]}; i++)); do | |
| diff=$(echo "${lags[$i]} - $average" | bc) | |
| sq_diff=$(echo "$diff * $diff" | bc) | |
| sum_sq_diff=$(echo "$sum_sq_diff + $sq_diff" | bc) | |
| done | |
| variance=$(echo "scale=1; $sum_sq_diff / (${#lags[@]} - 1)" | bc) | |
| stddev=$(echo "scale=1; sqrt($variance)" | bc) | |
| echo "Average input lag (excluding first measurement): $average ms" | |
| echo "Standard deviation (excluding first measurement): $stddev ms" | |
| else | |
| echo "Not enough valid measurements were taken." | |
| fi |
Author
Author
The standard deviation across 50 samples is around ~7ms, so these results are a bit spread.
Captured on Fedora with X11 and 50Hz monitor.
vsync=off
lövE 20 ms
lövR 22 ms same, one frame behind for 50Hz
vsync=on
godot 81 ms
lövE 55 ms
rest are lövR:
headset=false 77 ms
with vr sim 77 ms the headset sim pose reading doesn't introduce additional lag
early getPass 57 ms call getWindowPass() earlier in run(), before pollEvents()
^same; vr sim 60 ms again, lag is within standard deviation of non-vrsim
Author
The recent change on dev decreased the lag, here's the repeated measuring of lag:
vsync=off 27 ms
headset=false 57 ms
with vr sim 56 ms
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The measuring process:
measure_lag_conv.mp4
The tested LÖVR app simply draws a sphere at cursor position: