Horizontally Scaling Displays

I had old hardware, but I wanted more display space.

The first three monitors were easy (all code examples are adopted from the actual script, expect minor syntax errors)

xrandr --fb 3920x1024 \
  --output DVI-D-0 --pos 0x0 --panning 1280x1024+0+0 \
  --output HDMI-0 --pos 1280x0 --panning 1360x768+1280+0 \
  --output VGA-0 --pos $((1280 + 1360))x0 --panning 1280x1024+$((1280 + 1360))+0

But then the video card had no more output ports. And I still had a laptop which was uselessly sitting around. And it came with a screen. Surely there must be a way...

( while sleep 0.1; do x11vnc -forever -viewonly -localhost -clip 1366x768+0+0; done ) &
( ssh -R 5900:localhost:5900 drahflow@laptop 'export DISPLAY=:0; vncviewer FullScreen=1 ViewOnly=1 UseLocalCursor=0 geometry=1366x768+0+0 localhost' ) &

... ok, but the laptop is left of the other three monitors, and anyway I'd not like a copy, but more space. And also, someone gave me an old monitor. Maybe I can attach this to the laptop? Oh, and here is another old laptop, I wonder... Ah yeah, and some tablets. And while I'm at it, my phone has, technically, a screen, right?

So what features can we ducktape together to make this work?

Xorg framebuffer setup

The XRandR extension allows to define a virtual framebuffer to near-arbitrary dimensions (depending on how many pixels the video card + driver feel like they can handle). My GeForce GT710 is willing to do 16384x16384, i.e. plenty.

So let's order some.

xrandr --fb 5286x3248

Furthermore each output can be put at a specific position with —pos:

  xrandr --fb 5286x3248 \
    --output DVI-D-0 --pos 1366x1024 \
    --output HDMI-0 --pos $((1366 + 1280))x1024 \
    --output VGA-0 --pos $((1366 + 1280 + 1360))x$((2048 - 1024 - 600))

Unfortunately, Xorg will be very clever and make sure the mouse pointer cannot hide in the dark corners where "no display" can ever go. Usually, a larger framebuffer would be used with panning displays which follow the mouse. But of course, in a true multi-monitor setup, each monitor should display the same area. But, the —panning parameter takes (optionally) both an area where the display may pan to, and an area where the display will actually track the pointer. The mouse pointer hiding logic looks at the first, so it is possible to have the mouse in areas the display could, but won't, pan to. It might be necessary to move the mouse pointer around a bit to pan the displays until they all arrive at their final panning position, though.

xrandr --fb 5286x3248 \
  --output DVI-D-0 --pos 1366x1024 --panning $((1366+1280))x3248+0+0/1280x1024+1366+1024 \
  --output HDMI-0 --pos $((1366 + 1280))x1024 --panning 1360x$((768 + 600))+$((1366 + 1280))+$((2048 - 768 - 600))/1360x768+$((1366+1280))+$((2048 - 768 - 600)) \
  --output VGA-0 --pos $((1366 + 1280 + 1360))x$((2048 - 1024 - 600)) --panning 1280x$((1024 + 600))+$((1366 + 1280 + 1360))+$((2048 - 1024 - 600))/1280x1024+$((1366 + 1280 + 1360))+$((2048 - 1024 - 600))

Now, x11vnc can copy pixels from outside the panable area and forward them to whereever. But my window manager (i3) will believe that I have very large monitors now, and the workspaces will not correctly lie on the actually displayed parts.

However, there is a fix:

xrandr --setmonitor DVI-D-0-FIXED 1280/376x1024/301+1366+1024 DVI-D-0
xrandr --setmonitor HDMI-0-FIXED 1360/160x768/90+$((1366+1280))+$((2048-768-600)) HDMI-0
xrandr --setmonitor VGA-0-FIXED 1280/338x1024/270+$((1366+1280+1360))+$((2048-1024-600)) VGA-0

(and restart i3)

Now, there are three workspaces exactly where configured. But what about those other areas planned for x11vnc forwarding? Turns out the output argument at the end of —setmonitor can be "none":

xrandr --setmonitor VIRTUAL-0 1366/376x768/211+0+$((2048-768)) none
xrandr --setmonitor VIRTUAL-1 1024/100x600/50+$((1024))+$((2048)) none
xrandr --setmonitor VIRTUAL-2 1280/376x1024/301+1366+0 none
xrandr --setmonitor VIRTUAL-3 1366/376x768/211+0+$((2048-768-768)) none
xrandr --setmonitor VIRTUAL-4 800/100x480/50+$((1366+1280+1024))+$((2048-600)) none
xrandr --setmonitor VIRTUAL-5 1024/100x600/50+0+$((2048)) none
xrandr --setmonitor VIRTUAL-6 1024/100x600/50+0+$((2048+600)) none

Seven more "monitors". And (after i3 restart) corresponding workspaces. The numbers after the slashes are DPI, and some of them guaranteed to be guessed wrong, but so far it did not seem to matter.


It would be nice to have each x11vnc on a fixed, separate port from the others. And also not blocking script invocation:

uxterm -e "while sleep 0.1; do x11vnc -forever -passwd VerySecret -rfbport 5903 -viewonly -localhost -clip 1366x768+0+$((2048-768-768)); done" &
uxterm -e "ssh -R 5903:localhost:5903 root@display0 'export DISPLAY=:0; vncviewer -fullscreen -viewonly -encoding \"tight copyrect\" localhost:5903'" &

(per virtual monitor, obviously).

Or, less securely though, to offer a VNC endpoint in another WiFi network, by SSHing to the router and forwarding from there (note the missing -viewonly to allow touch-input from the phone, and the explicit listening address on the port forwarding ssh):

uxterm -e "while sleep 0.1; do x11vnc -forever -passwd VerySecret -rfbport 5903 -localhost -clip 1366x768+0+$((2048-768-768)); done" &
uxterm -e "ssh -R root@wifirouter" &

... and open the android VNC viewer on the phone manually.

And for those cases of Android displays which don't offer adb reverse port connections via TCP forwarding over adb stdio (disabling the connection-level escape key)

uxterm -e "ssh -R 5901:localhost:5901 -tt root@ socat TCP-CONNECT: SYSTEM:\\'adb -s 300427771550a700 shell -e none su -c /system/debian/enter.sh /socat.sh\\'" &

with the device-side script (disabling console escape processing and echo)

android# cat /socat.sh

stty raw -echo
exec socat TCP-LISTEN:5900 -

and with an explicit encoding list to xtightvncviewer, which otherwise figures the connection goes to "the same machine" and doesn't need compression

android# cat /vnc.sh
echo 'VerySecret' | DISPLAY=:0 xtightvncviewer -fullscreen -autopass -encodings "tight copyrect"

Comandeering i3

For the laptop driving two devices, each of which will be showing a different vncviewer, a simple -fullscreen is not going to cut it. Instead I'm opening vncviewers and then push them exactly to the right spot via i3-msg:

ssh drahflow@laptop 'DISPLAY=:0 xrandr --output HDMI1 --auto --right-of eDP1'
uxterm -e "ssh -R 5900:localhost:5900 drahflow@laptop 'export DISPLAY=:0; ~/i3.install/bin/i3-msg workspace 1; ~/i3.install/bin/i3-msg move workspace to output eDP1; vncviewer FullScreen=0 ViewOnly=1 UseLocalCursor=0 geometry=1366x768+0+0 localhost'" &

sleep 2

ssh drahflow@laptop 'DISPLAY=:0 ~/i3.install/bin/i3-msg floating enable'
ssh drahflow@laptop 'DISPLAY=:0 ~/i3.install/bin/i3-msg resize shrink up 10px'
ssh drahflow@laptop 'DISPLAY=:0 ~/i3.install/bin/i3-msg resize set 1368 770'

... and to get the pesky client-side curser out-of-view

ssh drahflow@laptop 'DISPLAY=:0 xdotool mousemove 0 767'

... or, where possible, start Xorg with

Xorg -nocursor ...

Also DPMS really likes to get in the way of uninterrupted display enjoyment:

ssh drahflow@laptop 'DISPLAY=:0 xset b off'
ssh drahflow@laptop 'DISPLAY=:0 xset dpms 0 0 0'
ssh drahflow@laptop 'DISPLAY=:0 xset s 0 0'
ssh drahflow@laptop 'DISPLAY=:0 xset dpms force on'

More Workspaces on i3

For completeness sake, I have these workspace bindings in .i3/config

bindsym Mod4+1 workspace 1
bindsym Mod4+2 workspace 2
bindsym Mod4+3 workspace 3
bindsym Mod4+4 workspace 4
bindsym Mod4+5 workspace 5
bindsym Mod4+6 workspace 6
bindsym Mod4+7 workspace 7
bindsym Mod4+8 workspace 8
bindsym Mod4+9 workspace 9
bindsym Mod4+0 workspace 10
bindsym Mod4+F1 workspace 11
bindsym Mod4+F2 workspace 12
bindsym Mod4+F3 workspace 13
bindsym Mod4+F4 workspace 14
bindsym Mod4+F5 workspace 15
bindsym Mod4+F6 workspace 16
bindsym Mod4+F7 workspace 17
bindsym Mod4+F8 workspace 18
bindsym Mod4+F9 workspace 19
bindsym Mod4+F10 workspace 20
bindsym Mod4+F11 workspace 21
bindsym Mod4+F12 workspace 22

bindsym Mod4+Shift+1 move container to workspace 1
bindsym Mod4+Shift+2 move container to workspace 2
bindsym Mod4+Shift+3 move container to workspace 3
bindsym Mod4+Shift+4 move container to workspace 4
bindsym Mod4+Shift+5 move container to workspace 5
bindsym Mod4+Shift+6 move container to workspace 6
bindsym Mod4+Shift+7 move container to workspace 7
bindsym Mod4+Shift+8 move container to workspace 8
bindsym Mod4+Shift+9 move container to workspace 9
bindsym Mod4+Shift+0 move container to workspace 10
bindsym Mod4+Shift+F1 move container to workspace 11
bindsym Mod4+Shift+F2 move container to workspace 12
bindsym Mod4+Shift+F3 move container to workspace 13
bindsym Mod4+Shift+F4 move container to workspace 14
bindsym Mod4+Shift+F5 move container to workspace 15
bindsym Mod4+Shift+F6 move container to workspace 16
bindsym Mod4+Shift+F7 move container to workspace 17
bindsym Mod4+Shift+F8 move container to workspace 18
bindsym Mod4+Shift+F9 move container to workspace 19
bindsym Mod4+Shift+F10 move container to workspace 20
bindsym Mod4+Shift+F11 move container to workspace 21
bindsym Mod4+Shift+F12 move container to workspace 22

And I created a script which orchestrates correct workspace to output assignment and start some programs in "expected" locations (but I read that i3 has actually better features for this):

i3.install/bin/i3-msg workspace 2
i3.install/bin/i3-msg move workspace to output VGA-0

xterm -e "fetchmail -N" &
xterm -e 'cd c/intercom; ./receiver 4000' &

sleep 2

i3.install/bin/i3-msg workspace 6
i3.install/bin/i3-msg move workspace to output HDMI-0

xterm -e 'LIBV4LCONTROL_FLAGS=1 LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libv4l/v4l2convert.so firefox/firefox' &


Of course, you wouldn't want to run all those shell commands every time you needed a display. Nor is it particularly fun (after maybe the fifth time) to recalculate the required coordinates after physically shuffling devices.

To make my life easier, I thus created this management script.

And together with

assign [title="^displays$"] workspace 1

in the i3 config, this is quite comfortable.

Finally, to soft-switch the tablets on and off, it is useful to collect the required backlight control commands into a script as well.


All these screens belong to the same desktop session (even the weird one on the right, where I replaced a failed backlight with ... other luminous things).