アジョブジ星通信

進捗が出た頃に更新されるブログ。

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つのリクエストに注目します。

まずはじめに、 RandrSetCrtcConfigmode: None で呼び出しています。これは後述する CRTC とかいうやつを無効化します。

次に RandrSetScreenSize です。引数の width-in-pixelsheight-in-pixels は 16 進数で表されているので、 10 進数に直すと 1024x768 であることが確認できます。 height-in-millimeters が 0 なのはたぶん xscope のバグです。

最後にまた RandrSetCrtcConfig を呼び出していますが、今度は modeoutputs が指定されています。 mode の値を xrandr --verbose の出力と照らし合わせると、設定した解像度の値になっていることがわかります。

Screen, CRTC, Output

仕様書の図を拝借します。

f:id:azyobuzin:20200222220350p:plain

この図からわかる、 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 の大きさを変える手順はこうです。

  1. RandrSetCrtcConfigmode: None で呼び出す
  2. 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 # スクリーンショット

f:id:azyobuzin:20200223013811p:plain
500.png

f:id:azyobuzin:20200223013926p:plain
300.png

さらに、この状態で x11vnc -display :0 -shared -forever を実行して、 VNC で操作することもできました。問題なさそうですね!