Xvfbのスクリーンサイズを変更する
Xvfb といえば、以前(主人公の好感度問題 完結編)、仮想環境に X Window System のディスプレイを用意するのに使ったツールです。使い方は Xvfb :0 -screen 0 1280x720x24
みたいな感じなのですが、 Xvfb を起動した後に、指定したサイズから変更したいときはどうしたらいいのでしょうか?
TL;DR
最初に指定したサイズ以下への変更なら xrandr --fb 1000x700
でできます。初回はエラーになるけど気にしてはいけません。
ただし、 X サーバーはすべてのクライアントが切断されると、状態をリセットしてしまうので、何かしらのクライアントが常に接続された状態(例えばウィンドウマネージャ―が動いている)で実行する必要があります。
例
$ Xvfb -screen 0 1280x720x24 & $ export DISPLAY=:0 $ xeyes & # 適当なクライアント $ xrandr xrandr: Failed to get size of gamma for output screen Screen 0: minimum 1 x 1, current 1280 x 720, maximum 1280 x 720 screen connected 1280x720+0+0 0mm x 0mm 1280x720 0.00* $ xrandr --fb 1000x700 xrandr: Failed to get size of gamma for output screen xrandr: specified screen 1000x700 not large enough for output screen (1280x720+0+0) X Error of failed request: BadValue (integer parameter out of range for operation) Major opcode of failed request: 140 (RANDR) Minor opcode of failed request: 21 (RRSetCrtcConfig) Value in failed request: 0x0 Serial number of failed request: 22 Current serial number in output stream: 22 $ xrandr Screen 0: minimum 1 x 1, current 1000 x 700, maximum 1280 x 720 screen connected 1280x720 0.00
やっていきの方針
X サーバーを再起動せずに画面解像度を変更する RandR という拡張プロトコルが用意されています。通常は GUI なり xrandr コマンドで、ディスプレイが対応している解像度の中で解像度を変更するために使用されます。しかし、 Xvfb には決まった DPI もピクセル数もないので、もう少し強引にサイズ(対応する物理サイズがないので、解像度とは言わない?)を変更してあげる必要があります。
xrandr の通信解析
xrandr がどのように解像度変更を行っているのか、まずは普通の例で見てみたいと思います。
X11 の通信のデバッグには xscope が便利です。 Ubuntu なら、 xutils-dev, x11proto-dev あたりのパッケージを入れて、 ./autogen.sh && make
でビルドできます。
xscope を引数を指定せずに起動すると、ディスプレイ :1 としてふるまいます。 :1 への通信は :0 へ転送され、そのログがコンソールに表示されます。つまり xrandr を :1 あてに実行すれば、ログが取れます。
まず、対応している解像度を確認しておきます。 xrandr に引数を指定せずに起動すると、一覧が取得できます。次の例は、ノートPCで、外部ディスプレイをつないでいない状態で試した結果です。「LVDS-1」が内蔵ディスプレイっぽいですね。
$ xrandr Screen 0: minimum 320 x 200, current 1366 x 768, maximum 8192 x 8192 LVDS-1 connected primary 1366x768+0+0 (normal left inverted right x axis y axis) 293mm x 164mm 1366x768 60.02*+ 1360x768 59.80 59.96 1280x720 60.00 59.99 59.86 59.74 1024x768 60.04 60.00 960x720 60.00 928x696 60.05 896x672 60.01 1024x576 59.95 59.96 59.90 59.82 960x600 59.93 60.00 960x540 59.96 59.99 59.63 59.82 800x600 60.00 60.32 56.25 840x525 60.01 59.88 864x486 59.92 59.57 800x512 60.17 700x525 59.98 800x450 59.95 59.82 640x512 60.02 720x450 59.89 700x450 59.96 59.88 640x480 60.00 59.94 720x405 59.51 58.99 684x384 59.88 59.85 680x384 59.80 59.96 640x400 59.88 59.98 576x432 60.06 640x360 59.86 59.83 59.84 59.32 512x384 60.00 512x288 60.00 59.92 480x270 59.63 59.82 400x300 60.32 56.34 432x243 59.92 59.57 320x240 60.05 360x202 59.51 59.13 320x180 59.84 59.32 VGA-1 disconnected (normal left inverted right x axis y axis) HDMI-1 disconnected (normal left inverted right x axis y axis) DP-1 disconnected (normal left inverted right x axis y axis)
では、この中から、適当に 1024x768 にでも変更してみましょう。ここで -d :1
を指定することで、 xscope に通信を記録してもらいます。
$ xrandr -d :1 --output LVDS-1 --mode 1024x768
実行すると、実際に画面が切り替わり、 xscope にもログが表示されます。大量にログが出ますが、ほとんどが現在の状態を取得する通信なので、最後のほうの設定を変更しているところだけに注目します。その結果、こんな感じに解像度変更が行われていました。
0.01: Client --> 32 bytes ............REQUEST: GrabServer ............REQUEST: RandrRequest RANDRREQUEST: RandrSetCrtcConfig crtc: CRTC 0000003f timestamp: CurrentTime config timestamp: TIM 00006ae1 x: 0 y: 0 mode: None rotation/reflection: Rotate_0 0.28: 32 bytes <-- X11 Server (pid 909 Xorg) ..............REPLY: RandrReply RANDRREPLY: SetCrtcConfig status: 00 timestamp: TIM 005b4480 0.28: Client --> 52 bytes ............REQUEST: RandrRequest RANDRREQUEST: RandrSetScreenSize window: WIN 0000013f width-in-pixels: 0400 height-in-pixels: 0300 width-in-millimeters: 010e height-in-millimeters: 0000 ............REQUEST: RandrRequest RANDRREQUEST: RandrSetCrtcConfig crtc: CRTC 0000003f timestamp: CurrentTime config timestamp: TIM 00006ae1 x: 0 y: 0 mode: MODE 0000004f rotation/reflection: Rotate_0 outputs: (1) 2.24: 32 bytes <-- X11 Server (pid 909 Xorg) ..............REPLY: RandrReply RANDRREPLY: SetCrtcConfig status: 00 timestamp: TIM 005b4d02
GrabServer
は排他制御のためなので、置いておいて、その後の3つのリクエストに注目します。
まずはじめに、 RandrSetCrtcConfig
を mode: None
で呼び出しています。これは後述する CRTC とかいうやつを無効化します。
次に RandrSetScreenSize
です。引数の width-in-pixels
と height-in-pixels
は 16 進数で表されているので、 10 進数に直すと 1024x768 であることが確認できます。 height-in-millimeters
が 0 なのはたぶん xscope のバグです。
最後にまた RandrSetCrtcConfig
を呼び出していますが、今度は mode
と outputs
が指定されています。 mode
の値を xrandr --verbose
の出力と照らし合わせると、設定した解像度の値になっていることがわかります。
Screen, CRTC, Output
仕様書の図を拝借します。
この図からわかる、 RandR のアーキテクチャはこういうことです: まず仮想的な Screen があります。その一部を CRTC(ブラウン管コントローラっていつの時代……)が写し取ります。 CRTC が写し取った映像が、 Output(先程の LVDS-1 とか VGA-1 とかのような物理デバイスのドライバ)に転送されます。
一番基本的な形は、「Screen の大きさ = CRTC の大きさ」で、スクリーンすべてが出力される状態です。そして、先程の xrandr による解像度変更の例では、この等式を保つように、Screen と CRTC の大きさを変更してくれたわけです。
Xvfb にも同じように Screen, CRTC, Output があります。 Screen は -screen
引数で指定した数だけあり、CRT、Output はそれぞれひとつずつあります。また対応する Mode は、引数に指定したサイズのひとつだけです。
$ Xvfb -screen 0 1280x720x24 & $ xrandr -d :0 --verbose xrandr: Failed to get size of gamma for output screen Screen 0: minimum 1 x 1, current 1280 x 720, maximum 1280 x 720 screen connected 1280x720+0+0 (0x3b) normal (normal) 0mm x 0mm Identifier: 0x3d Timestamp: 33193102 Subpixel: unknown Clones: CRTC: 0 CRTCs: 0 Transform: 1.000000 0.000000 0.000000 0.000000 1.000000 0.000000 0.000000 0.000000 1.000000 filter: 1280x720 (0x3b) 0.000MHz *current h: width 1280 start 0 end 0 total 0 skew 0 clock 0.00KHz v: height 720 start 0 end 0 total 0 clock 0.00Hz
Screen の大きさだけ変える
通信解析から、 RandrSetScreenSize
で Screen の大きさを変えられそうだということはわかりました。そして、 xrandr コマンドにも、 Mode に関係なくサイズを変更できる --fb
というオプションがあります。というわけで実行してみる、と、最初に示したようにエラーが出ますが、うまくいきます。もう一度実行すると、今度はエラーが出ず、うまくいきます。
$ Xvfb -screen 0 1280x720x24 & $ export DISPLAY=:0 $ xeyes & # 適当なクライアント $ xrandr --fb 1000x700 xrandr: Failed to get size of gamma for output screen xrandr: specified screen 1000x700 not large enough for output screen (1280x720+0+0) X Error of failed request: BadValue (integer parameter out of range for operation) Major opcode of failed request: 140 (RANDR) Minor opcode of failed request: 21 (RRSetCrtcConfig) Value in failed request: 0x0 Serial number of failed request: 22 Current serial number in output stream: 22 $ xrandr --fb 1000x700 $ xrandr --fb 1000x701 $ xrandr Screen 0: minimum 1 x 1, current 1000 x 701, maximum 1280 x 720 screen connected 1280x720 0.00
どういうことかというと、解像度を変更するとき、アーキテクチャの図から、 CRTC の範囲は Screen の範囲内にある必要がありました。そして、 xrandr は、(1) CRTC の無効化 → (2) Screen のサイズ変更 → (3) CRTC 設定の復元という操作を行います。したがって、 (3) の操作をすると、 1280x720 である CRTC が範囲外に出てしまうのでエラーになります。そして xrandr が終了したとき、 CRTC は無効化された状態になっています。したがって、 2 回目以降は、元の状態が無効なので (3) で何もしないため、エラーになりません。なお、実際のディスプレイを相手に --fb
で CRTC より小さいサイズを指定しても、 GPU に接続されているデバイスは判定方法が違うようなので、完全に無効になることはなく、それっぽく表示されます。
ということで、 Xvfb を相手に Screen の大きさを変える手順はこうです。
RandrSetCrtcConfig
をmode: None
で呼び出すRandrSetScreenSize
でサイズを指定する
動作確認
最後に、これでちゃんとうまくいくのか確認しておきます。 xrandr では無駄な処理をされることがわかったので、シンプルに先程の手順を実行するだけのプログラムを書きました。 Rust 製です。
extern crate xcb; // xcb = { version = "0.9", features = ["randr"] } use xcb::randr; fn main() { let (width, height) = { let args: Vec<_> = std::env::args().collect(); if args.len() != 3 { panic!("Invalid args"); } ( args[1].parse::<u16>().expect("Failed to parse width"), args[2].parse::<u16>().expect("Failed to parse height"), ) }; let (conn, screen_num) = xcb::Connection::connect(None).expect("Failed to connect"); let screen = conn.get_setup().roots().nth(screen_num as usize).unwrap(); let root = screen.root(); let mm_width = (width as f64 * (screen.width_in_millimeters() as f64 / screen.width_in_pixels() as f64)) as u32; let mm_height = (height as f64 * (screen.height_in_millimeters() as f64 / screen.height_in_pixels() as f64)) as u32; // CRTC を取得する let (crtc, config_timestamp) = { let reply = randr::get_screen_resources_current(&conn, root) .get_reply() .unwrap(); (reply.crtcs()[0], reply.config_timestamp()) }; // CRTC を無効化する randr::set_crtc_config( &conn, crtc, xcb::CURRENT_TIME, config_timestamp, 0, 0, xcb::NONE, randr::ROTATION_ROTATE_0 as u16, &[], ) .get_reply() .unwrap(); // Screen のサイズ変更 randr::set_screen_size_checked(&conn, root, width, height, mm_width, mm_height) .request_check() .unwrap(); println!("OK"); }
適当なウィンドウマネージャーとアプリを動かして、スクリーンショットを撮って、問題なく使えることを確認します。
$ Xvfb -screen 0 1280x720x24 & $ export DISPLAY=:0 $ openbox & # 適当なウィンドウマネージャー $ cargo run -q -- 500 500 # 500x500 にリサイズ OK $ xcalc & $ xcalc & $ import -window root 500.png # スクリーンショット $ cargo run -q -- 300 300 # 300x300 にリサイズ OK $ import -window root 300.png # スクリーンショット
さらに、この状態で x11vnc -display :0 -shared -forever
を実行して、 VNC で操作することもできました。問題なさそうですね!