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 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 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
vsync=on