存档

文章标签 ‘Spice’

Spice 分析(4) – spice 客户端实现

2013年6月28日 8 条评论

目前上游社区支持的客户端是 spice-gtk, 它其实是一个库的项目, 编译后能得到两个库:

  • libspice-client-glib: spice client 的协议处理部分
  • libspice-client-gtk: 整合了 gtk 的更完善的实现

virt-manager 通过 spice-gtk 实现了 spice client 的功能, 可执行的程序称是 remote-view.

下面对现在市面上存在的各种客户端做一下分类和探讨, 其中对 spice-gtk 重点做 一下探讨, 毕竟这是东宫太子.

使用 spice-gtk

如果要实现 spice 客户端, 最省事最简单的办法莫过于在 libspice-client-gtk 这个库的基础上进行开发了. 比如我曾经介绍过的 编写简单的 Spice Client

但是由于以下种种原因, 基于 spice-gtk 的客户端可能不会是最佳的选择:

gtk3 不支持 X11 的共享内存

如果使用 gtk3, 那么渲染的后端只能是 cairo, 无法选择 X11, 同时意味者 X11 和应用程序的共享内存无法使用, 即每次做 bitmap 渲染的时候, 会在 spice client 和 X11 server 之间多做一次内存拷贝, 损失不必要的性能.

某些平台没有 gtk

在其它平台并没有 gtk 实现, 比如 android, IOS 等, 要移植 gtk 到这些 平台, 不亚于重写 spice client.

性能的考虑

同上面, 如果渲染后端使用 cairo, 在某些平台达不到最好的效果, 实际上, 在 Linux 下, 应该使用 X11 的共享内存技术, 在 windows 或者其它平台也应该使用 相应的技术.

以下几个客户端即是基于 libspice-client-gtk 实现的.

  • remote-view
  • spice-osx
  • spicy(just to be used to test spice-gtk)

使用 spice-glib

libspice-client-glib 完整的实现了 spice 协议的客户端, 只不过用的 C 库是 glib 而已, 它完全和 gtk 无关. 理论上, 每个平台都应该调用 libspice-client-glib, 而图形的渲染 部分, 则由平台相关的 API 来实现, 也即实现 spice-widget.c 部分.

PS: 在 Bitmap 的处理部分, 应该使用平台相关的 API, 这样能达到最佳的性能(最大化的 使用显卡的加速功能), 比如在 linux(X86/ARM) 平台, 使用硬件加速(opengl?) 来处理 Bitmap, 在 windows 平台, 使用 GDI 来处理, 在 macos 平台(?). 遗憾的是, 虽然开发者留下了这些平台独立的接口和文件, 但是并没有完全实现, 这些接口都没有 用到, 目前的实现都是用统一的 pixman 库(纯 CPU 运算, 虽然对各个平台也会做一些优化) 操作, 具体的实现在 common/sw_canvas.c.

言归正传, 下面描述怎么使用 libspice-client-glib, 大部分代码参照以前我在 android 上的 实现的 spice-client.

首先需要实现一个 display 的类(python/c++/c)用来做渲染, 在这个类的构造函数里面, 注册相应的信号处理函数(这些信号由 SpiceSession 在有事件到来的时候通知你), 例如:

    AndroidDisplay *display;
    AndroidDisplayPrivate *d;
 
        // ......
 
    if (!d->session) {
        g_error("AndroidDisplay constructed without a session");
    }
 
    spice_g_signal_connect_object(d->session, "channel-new",
                                  G_CALLBACK(channel_new), display, 0);
    spice_g_signal_connect_object(d->session, "channel-destroy",
                                  G_CALLBACK(channel_destroy), display, 0);

其中 channel-new 这个信号, 表示 SpiceSession 监听到有新的 channel(display, input) 在的时候, 主动通知我们的 AndroidDisplay 对象, 我们使用 channel_new() 来处理这个 信号:

static void channel_new(SpiceSession *s, SpiceChannel *channel, gpointer data)
{
    AndroidDisplay *display = data;
    AndroidDisplayPrivate *d = ANDROID_DISPLAY_GET_PRIVATE(display);
    int id;
 
    g_object_get(channel, "channel-id", &id, NULL);
    if (SPICE_IS_MAIN_CHANNEL(channel)) {
            // ......
    }
 
    if (SPICE_IS_DISPLAY_CHANNEL(channel)) {
        SpiceDisplayPrimary primary;
        if (id != d->channel_id)
            return;
        d->display = channel;
                // 处理 primary 信号, 下面会讲到
        spice_g_signal_connect_object(channel, "display-primary-create",
                                      G_CALLBACK(primary_create), display, 0);
        spice_g_signal_connect_object(channel, "display-primary-destroy",
                                      G_CALLBACK(primary_destroy), display, 0);
        // 这个信号比较重要, 每次 guest 做任何 UI 的操作, blit, copy 等, 必然会
        // 重绘某些区域, 等到 SpiceDisplay 处理完 bitmap 的变换之后, 会给注册
        // 了这个信号的组件发送 display-invalidate 信号, 告诉这些组件哪些区域需要
        // 重绘.
        spice_g_signal_connect_object(channel, "display-invalidate",
                                      G_CALLBACK(invalidate), display, 0);
        spice_g_signal_connect_object(channel, "display-mark",
                                      G_CALLBACK(mark), display, G_CONNECT_AFTER | G_CONNECT_SWAPPED);
        if (spice_display_get_primary(channel, 0, &primary)) {
            primary_create(channel, primary.format, primary.width, primary.height,
                           primary.stride, primary.shmid, primary.data, display);
            mark(display, primary.marked);
        }
        spice_channel_connect(channel);
        spice_main_set_display_enabled(d->main, get_display_id(display), TRUE);
        return;
    }
 
    // input 通道, 处理鼠标键盘事件
    if (SPICE_IS_INPUTS_CHANNEL(channel)) {
        d->inputs = SPICE_INPUTS_CHANNEL(channel);
        spice_channel_connect(channel);
        return;
    }
 
    // handle other channels, e.g. curse, usb-redir
    return;
}

下面是 invalidate 的实现.

static void invalidate(SpiceChannel *channel,
        gint x, gint y, gint w, gint h, gpointer data)
{
    AndroidDisplay *display = data;
    AndroidDisplayPrivate *d = ANDROID_DISPLAY_GET_PRIVATE(display);
    android_pd = d;
    /* 正如上面讲到的, 这个是重绘的处理函数, 不同的平台这里可能会不同.
       如果是 gtk: 应该调用相应的 gtk_widget_queue_draw_area() 等函数通知上层
       UI 更新相应的显示区域. 因为这里 android 没有 gtk, 所以这里使用了 java
       的 JNI 技术通知 Android UI 需要重绘某些区域, 把控制权交给 Android UI. */
    spice_callback("OnGraphicsUpdate", "(IIII)V", x, y, w, h);
 
    /* 另外提醒一点, 所有的 bitmap 数据都存放在 d->data 里, 所以真正重绘的时候,
       可以从这里取数据, 如果后端的渲染是 cairo 的话, 每次 "display-primary-create
       发生的时候,  spice-gtk 会使用 cairo_image_surface_create_for_data()
       把 d->data 映射到它的画板(canvas)上去. */
 
    return ;
}

如果要写 IOS 或者 Android 客户端的话, 应该采用这种方式.

不依赖于其它模块

如果没有 glib 用, 或者其它的什么原因, 那么只能从头自己实现. spice-html5 就是 这么做的, 因为这是浏览器上运行的客户端, 编程语言是 javascript, 也没有 glib.

小玩具

如果对 spice-client 的渲染了如指掌的话, 可以做一些小 hack, 比如我喜欢用酷狗 来听音乐, 因为它的歌词显示得比较好, 但是我用的是 linux 操作系统, 酷狗音乐没有 linux 的版本. 所以如果能只显示虚拟机里面的酷狗歌词到我的 linux 桌面, 那也蛮帅的.

下面是相关的实现代码(trick and ugly):

// file: spice-widget.c
static void invalidate(SpiceChannel *channel,
                       gint x, gint y, gint w, gint h, gpointer data)
{
    // ......
    if (d->ad_drawing_area) {
        // 我在 SpiceDisplayPrivate 定义了一个 GtkWidget *ad_drawing_area 用来
        // 画歌词的窗口区域, hard code
        gtk_widget_queue_draw_area(d->ad_drawing_area, 0, 0, 1024, 113);
    }
}
 
// file: spice-widget-cairo.c
int spicex_image_create(SpiceDisplay *display)
{
    // other code
    if (d->ad_drawing_area) {
        // 我用歌词窗口的信息来创建在 linux 上的窗口的 bitmap 数据.
        d->ad_ximage = cairo_image_surface_create_for_data
                ((guint8 *)d->data + 1024 * 446 * 4, CAIRO_FORMAT_RGB24, 1024, 113, 1024 * 4);
    }
    return 0;
}
 
// file: spicy.c
static gboolean ad_draw_event(GtkWidget *widget, cairo_t *cr, void *opaque)
{
    int ww, wh;
    SpiceDisplayPrivate *d = opaque;
 
    ww = gdk_window_get_width(gtk_widget_get_window(widget));
    wh = gdk_window_get_height(gtk_widget_get_window(widget));
 
    cairo_rectangle(cr, 0, 0, ww, wh);
 
    cairo_set_source_surface(cr, d->ad_ximage, 0, 0);
    cairo_paint(cr);
 
    return TRUE;
}
 
static SpiceWindow *create_spice_window(spice_connection *conn, SpiceChannel *channel, int id, gint monitor_id)
{
    // other codes
    d->ad_drawing_area = gtk_drawing_area_new();
    // 注册 draw 信号, 当 gtk_widget_queue_draw_area() 被调用的时候,
    // 会触发 draw 信号, 表示某些区域需要被更新.
    g_signal_connect(d->ad_drawing_area, "draw",
                     G_CALLBACK(ad_draw_event), d);
    // other codes
}

附上截图一张 kugou-geci-redir

分类: QEMU, Spice 标签: ,

How to install spice client on Raspberry Pi

2013年1月7日 没有评论

Download source

First, we need to download the source of spice client from spice official website, the latest stable version is 0.14.

$ wget http://spice-space.org/download/gtk/spice-gtk-0.14.tar.bz2

Install dependencies packages

Spice client depends many other packages, e.g. jpeg, gtk, audio … We must install these packages before compiling source.

Furthermore, since we install spice client from source, packages related to compile are also needed, e.g. gcc, autoconf, libtool

The celt package in raspbian’s repository(0.7.1) is newer than Spice client requires(0.5.1), so we have to install this required version from source.

$ sudo apt-get install libogg-dev
$ wget https://launchpadlibrarian.net/59154526/celt_0.5.1.3.orig.tar.gz
$ tar xf celt_0.5.1.3.orig.tar.gz
$ cd celt-0.5.1.3
$ ./configure --prefix=/usr
$ make && sudo make install

All packages we need to install are following:

$ sudo apt-get install build-essential autoconf libtool intltool libspice-protocol-dev libgtk2.0-dev libssl-dev libpulse-dev gobject-introspection libgirepository1.0-dev libjpeg8-dev pulseaudio

Compile and install source

This step, we are ready to compile and install Spice client.

A accelerated X driver for Raspberry Pi are being developped, and a test version has been released. The driver takes advantage of hardware acceleration for display. So spice client could run much smoother.

But by default, spice-gtk uses cairo as display backend, which does not use hardware acceleration. In order to use hardware acceleration, we must configure “–with-gtk=2.0”.

$ tar xf spice-gtk-0.14.tar.bz2
$ cd spice-gtk-0.14
$ ./configure --prefix=/usr --disable-maintainer-mode --disable-static --enable-introspection --without-python --without-sasl --disable-polkit --disable-vala --enable-smartcard=no --with-gtk=2.0
$ make
$ sudo make install

Ok, if no errors occur, we have completed the compilation and installation, it’s time to enjoy it.

Done, connect to server

Now you can connect to your VM using following command:

$ spicy -h spice_server -p port

That’s all, enjoy it!

Drag And Drop support in QEMU/Spice

2012年11月25日 4 条评论

好久没有写 Blog 了, 最近的业余时间我在给 Spice 社区挖一个坑, 让QEMU/Spice 的虚拟机也支持文件拖拽, 简单的讲, 就是通过鼠标拖拽文件/文件夹, 使得可以在 client 和 guest 之间传输文件, 这个功能 Vmware 和 Virtualbox 已经有了.

我决定给 spice 也加上这个功能, 目前的状态是:

  1. 只支持 client -> guest 的拖拽
  2. 代码基本实现了, 正在 merge 到 upstream. 目前正在和上游的 maintainer 交流一些细节方面的问题, 得到了很多 Developper 的支持.

相信不久这个功能就可以在正式版中用了.

分类: QEMU, Spice 标签: ,

Spice 分析(3) – 编写 Spice Client

2012年9月20日 4 条评论

spice-gtk 的帮助下, spice client 的编写非常简单. 以致于我在做 Spice Server 的测试的时候, 顺手写了一个简单的 spice client.

把下面的一些核心部分做一个剖析:

static void channel_new(SpiceSession *s, SpiceChannel *c, gpointer *data);
 
/* 创建一个 Spice session */
spice_session = spice_session_new();
 
/* 设置 Spice 的地址和端口 */
g_object_set(spice_session, "host", host, NULL);
g_object_set(spice_session, "port", port, NULL);
 
/* 设置当 Spice Channel 建立之后的 callback, 也就是说这个时候可以
 * 获取 Spice Gtk 建立的 Spice Widget, 包括 Spice Window 等 */
g_signal_connect(spice_session, "channel-new",
                 G_CALLBACK(channel_new), NULL);
 
/* 最后调用这个 API, 连接 Server 就可以了 */
spice_session_connect(spice_session);
 
static void channel_new(SpiceSession *s, SpiceChannel *c, gpointer *data)
{
    int id = 0;
 
    /* 获取通道 ID */
    g_object_get(c, "channel-id", &id, NULL);
 
    if (SPICE_IS_DISPLAY_CHANNEL(c)) {
        /* 对 Display 通道, 获取 spice window, 然后把它加入我们的容器(主窗口,
         * VBox 等), 这里的 main_window 是我用 gtk_window_new() 创建的主窗口 */
        spice_display = spice_display_new(s, id);
        gtk_container_add(GTK_CONTAINER(main_window), GTK_WIDGET(spice_display));
        gtk_widget_show_all(main_window);
    }
 
}

事实上, 核心代码不超过 10 行, 如此的简单, 当然更多的鼠标, 键盘事件, USB 重定向 等, 只要自己写 signal 的 callback 就可以了.

完整的代码在 我的github 上:

$ git clone git://github.com/mathslinux/qemu-tools
$ cd qemu-tools/spice-tools/
$ make
$ ./spice-client

分类: QEMU, Spice 标签: ,

Spice 分析(2) – Spice Server 架构分析

2012年9月17日 没有评论

概述

Spice 是一个 Virtual Device Interfaces(VDI) 的库, 它以 libspice 库的形式提供给 VDI 后端, 这个后端可以是 QEMU, 或者 Xorg 等.

架构模型

根据上面提到的 VDI 的概念, 它是一个非常复杂的模型, 包括无数的组件: 显示模块, 输入输出模块, 各种 guest agent模块, 所以需要一个精心设计 的架构来使得各个模块流畅的运行.

其实处理 VDI 的这种编程模型, 有两种比较流行的架构模型

事件驱动架构

在一个 main loop 里面通过 select 或者 poll 各种句柄, 来对 用户的事件进行处理. 典型的比如大部分基于 gtk 程序.

并行处理架构

开辟多个进程/线程分别处理不同的模块.

Spice server 严格说来属于上面的事件驱动架构, 即它是通过抛出一系列的文件句柄给 调用者, 然后在 mainloop(甚至 几乎所有 mainloop 都抛出给caller) 上轮询这些 句柄, 实现他的架构的. 这个架构写过 GTK 或者用过 libevent 的人应该不会陌生.

SS 通过提供接口的注册函数(spice_server_add_interface()), 使得 caller 可以把想要处理的模块注册到 SS 系统中, 比如想要实现声音的播放, 需要注册 SpicePlaybackInterface 实例到 SS 系统中, 当不想用这个模块了, 可以用 spice_server_remove_interface() 将它从 SS 中移除.

在所有的模块中, 其中有一个接口(Core interface)比较特殊, 这个接口是在 SS 初始化的时候被注册的. 这个接口主要用于监听 Spice client 的连接请求, 管理 channels 的注册和移除等.

初始化

SS 提供了一个 API spice_server_init() 来初始化 SS. 它的原型如下:

/** 
 * 初始化 SS
 * 
 * @param s SS 的实例, 用 spice_server_new() 创建
 * @param core 前面提到的 Core interface
 * 
 * @return 
 */
int spice_server_init(SpiceServer *s, SpiceCoreInterface *core);

其中 SpiceCoreInterface 这个 API 的核心. 下面重点介绍:

struct SpiceBaseInterface {
    /* 接口类型 */
    const char *type;
    /* 接口描述 */
    const char *description;
    /* 接口的主版本号, 初始化这个接口的时候会判断用户传递的值和 SS 允许的
     是否相等, 不相等初始化失败 */
    uint32_t major_version;
    /* 接口的次版本号, 初始化的时候比较用户传递的和 SS 允许的大小, 大于
     SS 允许的则初始化失败 */
    uint32_t minor_version;
};
 
struct SpiceCoreInterface {
    /* 接口信息, 见上面的分析 */
    SpiceBaseInterface base;
 
    /* 添加一个定时器, 旧版本的 SS 会用这个 API 自动创建一个 timer, 新版本要求
     * caller 传递 timer 给这个 API, 这个 timer 主要用来在超时的时候调用
     * callback, 超时时间是由下面的 timer_start() 设置的 */
    SpiceTimer *(*timer_add)(SpiceTimerFunc func, void *opaque);
 
    /* 设置超时时间, 在 ms 毫秒之后调用 callback */
    void (*timer_start)(SpiceTimer *timer, uint32_t ms);
 
    /* 取消上面设置的超时时间 */
    void (*timer_cancel)(SpiceTimer *timer);
 
    /* 移除 timer */
    void (*timer_remove)(SpiceTimer *timer);
 
    /* 添加一个 watch, 前面说过, SS 是事件驱动模型的, SS 会在内部创建事件
     * 源(客户端连接),把这些事件的句柄通过文件的形式做成接口, caller
     * 实现这些接口, 比如 用 select 轮询这些句柄. */
    SpiceWatch *(*watch_add)(int fd, int event_mask, SpiceWatchFunc func, void *opaque);
    /* 修改文件句柄的读写属性 */
    void (*watch_update_mask)(SpiceWatch *watch, int event_mask);
    /* 移除对该文件句柄的g监听 */
    void (*watch_remove)(SpiceWatch *watch);
 
    /* 当有一些特殊的事件发生时, 会调用这个 API, 比如新客户端连接, 断开等,
     * caller 可以根据自己的需求做一些操作, 比如 QEMU 会创建一个 QMP 消息
     * 等 */
    void (*channel_event)(int event, SpiceChannelEventInfo *info);
};

注册接口

上面说过, 通过注册接口的函数, 可以把想要处理的模块添加到 SS 系统中, 这个 API 就是:

int spice_server_add_interface(SpiceServer *s, SpiceBaseInstance *sin);

以注册键盘为例:

static void kbd_push_key(SpiceKbdInstance *sin, uint8_t frag)
{
}
 
static uint8_t kbd_get_leds(SpiceKbdInstance *sin)
{
}
 
static const SpiceKbdInterface kbd_interface = {
    .base.type          = SPICE_INTERFACE_KEYBOARD,
    .base.description   = "keyboard",
    .base.major_version = SPICE_INTERFACE_KEYBOARD_MAJOR,
    .base.minor_version = SPICE_INTERFACE_KEYBOARD_MINOR,
    /* Client的键盘输入 */
    .push_scan_freg     = kbd_push_key,
    /* Client查询 led 的状态 */
    .get_leds           = kbd_get_leds,
};
 
SpiceKbdInstance *kbd;
kbd = g_malloc0(sizeof(*kbd));
kbd->base.sif = &kbd_interface.base;
/* 调用 API 注册 */
spice_server_add_interface(spice_server, &kbd->base);

分类: QEMU, Spice 标签: ,

Spice 分析(1) – 构建 Spice 开发环境

2012年9月17日 没有评论

Spice 基本的组件包括:

spice-protocol
Spice 协议, 全是以头文件的形式提供的, 这个头文件 expose 给外部用到spice的相关程序, e.g. QEMU, spice-gtk
spice-common
提供了一些公共的模块, 比如内存分配的 API, ssl 的 API 等, 是给 spice-server 和 spice-gtk 共同使用的.
spice-server
以库的形式提供接口, 一方面用 spice-protocol 和 client 通信, 一方面, hypervise 调用其提供的接口提供 VDI 的能力, 如果是做客户端开发, 这个组件是不需要的.
spice-gtk
用 gtk 封装的一系列客户端与 spice-server 通信的 API, 包括 连接 spice 实例, 传递鼠标键盘事件等等, 如果是做 hypervise 端的开发, 比如 只使用 QEMU/KVM, 那么不需要这个组件.

clone 项目仓库

在 git 仓库里, spice-common 作为 spice 和 spice-gtk 的一个子项目存在, spice-protocol 又是在 spice-common 里的一个子项目, 所以只需要 clone spice 和 spice-gtk 就可以了.

$ git clone git://git.freedesktop.org/git/spice/spice
$ git clone git://git.freedesktop.org/git/spice/spice-gtk

编译安装

$ cd spice && ./autogen.sh --prefix=/usr && make -j5 && sudo make install
$ cd spice-gtk && ./autogen.sh --prefix=/usr && make -j5 && sudo make install

当然可以在编译的时候做一些更深的定制, 比如我的:

$ cd spice && ./autogen.sh --prefix=/usr --build=x86_64-pc-linux-gnu \
--host=x86_64-pc-linux-gnu --mandir=/usr/share/man --infodir=/usr/share/info \
--datadir=/usr/share --sysconfdir=/etc --localstatedir=/var/lib \
--libdir=/usr/lib64 --disable-dependency-tracking --disable-static \
--disable-tunnel --enable-client --enable-gui --without-sasl \
--disable-static-linkage && make -j5 && sudo make install
 
$ cd spice-gtk && ./autogen.sh --prefix=/usr --build=x86_64-pc-linux-gnu \
--host=x86_64-pc-linux-gnu --mandir=/usr/share/man --infodir=/usr/share/info \
--datadir=/usr/share --sysconfdir=/etc --localstatedir=/var/lib \
--libdir=/usr/lib64 --disable-dependency-tracking --disable-maintainer-mode \
--disable-static --enable-introspection --with-audio=pulse --without-python \
--without-sasl --enable-polkit --disable-vala --with-gtk=3.0 --enable-werror \
--enable-usbredir && make -j5 && sudo make install

分类: QEMU, Spice 标签: ,