目前现状
目前spice对qxl接口的云视频方案支持最好,对于扩展多屏的方案在qemu的启动配置参数可以完成,libvirt 的多屏参数配置如下:
<video>
<model type='qxl' vram='9126' heads='2'/>
</video>
通过指定heads 的数量就可以确定spice云桌面的扩展屏的数量。
spice的流方案是利用虚拟机上的spice-streaming-agent推流转发到spice server,然后通过display channel 发送到客户端,流方案的libvirt配置如下:
<channel type='spiceport'>
<source channel='org.spice-space.stream.0'/>
<target type='virtio' name='org.spice-space.stream.0'/>
<address type='virtio-serial' controller='0' bus='0' port='2'/>
</channel>
配置一个spiceport 的虚拟串口可以产生一个云桌面窗口,但是对于扩展多屏,目前不支持这种配置。需要进一步探索流模式下的多屏方案。
spice stream 多屏方案探索
多stream-channel 方案
方案
stream 的流程如下图所示:
一个spiceport就对应一个streamdevice, 一个streamdevice就对应一个streamChannel, 相对应的客户端创建一个显示窗口。基于以上思路,可以创建两个spiceport,stream-agent 底层抓屏后,拆分成两个屏幕, 再通过两个spiceport 推出去,这样客户端就会产生两个窗口,形成多屏。
实现上是通过修改libvirt的配置如下:
<channel type='spiceport'>
<source channel='org.spice-space.stream.0'/>
<target type='virtio' name='org.spice-space.stream.0'/>
<address type='virtio-serial' controller='0' bus='0' port='2'/>
</channel>
<channel type='spiceport'>
<source channel='org.spice-space.stream.1'/>
<target type='virtio' name='org.spice-space.stream.1'/>
<address type='virtio-serial' controller='0' bus='0' port='3'/>
</channel>
问题
目前这种方案实现上最容易,不需要修改任何代码,最终客户端可以出现双屏,但是在客户端上鼠标只能在主屏上生效,在扩展屏上鼠标不生效。 产生这种问题的原因是因为,两个流的显示id 都是0
,这样没办法区分两个屏幕,底层的vdagent只会处理display id 为0 的消息,这样第一个流的窗口会生效,第二个不生效。
单spice stream 多monitor的方案
多个流的方案会导致在inputchannel(鼠标输入出现覆盖),可以创建单个流多个monitor的方案,一个monitor 在客户端产生一个窗口对对应一个显示id, 这样各个窗口的显示id不会重复。方案流程如下所示:
依据moitor_config 就可以产生多个屏幕,monitor_config的配置参数如下:
typedef struct SpiceHead {
uint32_t monitor_id; //标识monitor
uint32_t surface_id; //对于扩展屏的方式只有一个surface 为0
uint32_t width; //该monitor对应显示图像的宽
uint32_t height; //该monitor 对应显示图像的高
uint32_t x; //在surface 中的起始位置
uint32_t y; //在surface位置
uint32_t flags;
} SpiceHead;
typedef struct SpiceMsgDisplayMonitorsConfig {
uint16_t count;
uint16_t max_allowed;
SpiceHead heads[0];
} SpiceMsgDisplayMonitorsConfig;
qxl模式下通过读取libvirt多屏配置文件后就会在spice-server 中生成monitor_config, 所以qxl 方式下多屏可以直接生成。但是sream 方式需要额外生成, 目前spice-server 系统中的moitor_config 是直接写成了固定的方式,参考spice_server 项目stream-channel.cpp的marshall_monitors_config 函数:
void
StreamChannelClient::marshall_monitors_config(StreamChannel *channel, SpiceMarshaller *m)
{
struct {
SpiceMsgDisplayMonitorsConfig config;
SpiceHead head;
} msg = {
{ 1, 1, },
{
// monitor ID. These IDs are allocated per channel starting from 0
0,
PRIMARY_SURFACE_ID,
channel->width, channel->height,
0, 0,
0 // flags
}
};
init_send_data(SPICE_MSG_DISPLAY_MONITORS_CONFIG);
spice_marshall_msg_display_monitors_config(m, &msg.config);
}
以上配置中monitor_config 配置固定成了单屏方式, 显示的是整个屏幕,通过发送SPICE_MSG_DISPLAY_MONITORS_CONFIG信号通知client monitor_config 配置。
spice-server 代码中修改支持多屏
在marshall_monitors_config 直接修改成两屏模式(水平方式),代码如下:
void
StreamChannelClient::marshall_monitors_config(StreamChannel *channel, SpiceMarshaller *m)
{
struct {
SpiceMsgDisplayMonitorsConfig config;
SpiceHead head[2];
} msg = {
{ 2, 2, },
{
// monitor ID. These IDs are allocated per channel starting from 0
0,
PRIMARY_SURFACE_ID,
channel->width/2, channel->height,
0, 0,
0 // flags
},
{
// monitor ID. These IDs are allocated per channel starting from 0
0,
PRIMARY_SURFACE_ID,
channel->width、2, channel->height,
channel->width/2, 0,
0 // flags
}
};
init_send_data(SPICE_MSG_DISPLAY_MONITORS_CONFIG);
spice_marshall_msg_display_monitors_config(m, &msg.config);
}
通过以上的方式修改后,在spicy 客户端显示中出现出了两个屏幕,但是第二个屏幕屏幕是黑的,通过日志分析问题出现在客户端。
客户端创建流模式流程
以gsteamer的模式为例,调用关系如下:
- display_handle_stream_create 是客户端接收到服务端信号SPICE_MSG_DISPLAY_STREAM_CREATE完成创建。
- hand_pipeline_to_widget 的左右是确定显示方式,代码如下:
G_GNUC_INTERNAL
gboolean hand_pipeline_to_widget(display_stream *st, GstPipeline *pipeline)
{
gboolean res = false;
if (st->surface->streaming_mode) {
g_signal_emit(st->channel, signals[SPICE_DISPLAY_OVERLAY], 0,
pipeline, &res);
}
return res;
}
目前流模式默认通过发送widget窗口发送SPICE_DISPLAY_OVERLAY采用GstVideoOverlay interface进行显示,这种显示方式是直接讲pipeline和窗口绑定进行显示。这样的方式在多屏的模式下,流直接绑定到了0号窗口显示。
- 修改的方式不采用GstVideoOverlay interface进行显示,而是直接采用gtstream 解码出图片后,将图片的不同区域显示在不同的窗口下。修改方式如下:
G_GNUC_INTERNAL
gboolean hand_pipeline_to_widget(display_stream *st, GstPipeline *pipeline)
{
gboolean res = false;
return res;
}
修改之后出现了双屏显示正常。
双屏流显示流程
客户端调用关系如图所示:
这里是两个线程, 工作刘晨如下:
- display_channel 接收到SPICE_MSG_DISPLAY_STREAM_DATA 之后就调用流数据处理函数
display_handle_stream_data, 然后将frame 送入解码队列中。 - 解码线程从队列中得到数据frame 后进行解码,然后定时调用display_frame 进行显示画图
- stream_display_frame 显示画图后就发信号通知widget进行显示,这部分的代码如下:
G_GNUC_INTERNAL
void stream_display_frame(display_stream *st, SpiceFrame *frame,
uint32_t width, uint32_t height, int stride, uint8_t *data)
{
if (stride == SPICE_UNKNOWN_STRIDE) {
stride = width * sizeof(uint32_t);
}
if (!(st->flags & SPICE_STREAM_FLAGS_TOP_DOWN)) {
data += stride * (height - 1);
stride = -stride;
}
st->surface->canvas->ops->put_image(st->surface->canvas,
&frame->dest, data,
width, height, stride,
st->have_region ? &st->region : NULL);
if (st->surface->primary) {
g_signal_emit(st->channel, signals[SPICE_DISPLAY_INVALIDATE], 0,
frame->dest.left, frame->dest.top,
frame->dest.right - frame->dest.left,
frame->dest.bottom - frame->dest.top);
}
}
鼠标显示异常的问题
通过上面的修改双屏显示问题解决,第二个屏幕上的鼠标可以操作(有cursor显示,可以点击鼠标), 但是第二个屏幕上的鼠标操作,控制的还是第一个屏幕上的内容。
检查客户端鼠标操作
检查调试客户端的代码,在鼠标操作的时候,原生获取屏幕显示id的函数如下:
static gint get_display_id(SpiceDisplay *display)
{
SpiceDisplayPrivate *d = display->priv;
/* supported monitor_id only with display channel #0 */
if (d->channel_id == 0 && d->monitor_id >= 0)
return d->monitor_id;
g_return_val_if_fail(d->monitor_id <= 0, -1);
return d->channel_id;
}
从上面的代码可以分析得到, 该多屏模式只针对channel_id 为0的场景,而对于channel_id > 0 的场景会存在问题,修改
为如下代码:
static gint get_display_id(SpiceDisplay *display)
{
SpiceDisplayPrivate *d = display->priv;
/* supported monitor_id only with display channel #0 */
if (d->monitor_id >= 0)
return d->monitor_id;
g_return_val_if_fail(d->monitor_id <= 0, -1);
return d->channel_id;
}
修改完成之后,日志分析中disply_id = 0 和 display_id 的坐标位置(widget中的坐标位置)都发送到了server, 但是鼠标问题还是存在。
服务端鼠标位置的处理
检查服务端的鼠标位置的处理流程,参考spice-server端inputs-channel 处理代码:
case SPICE_MSGC_INPUTS_MOUSE_POSITION: {
auto pos = static_cast<SpiceMsgcMousePosition *>(message);
SpiceTabletInstance *tablet = inputs_channel->tablet;
on_mouse_motion();
if (reds_get_mouse_mode(reds) != SPICE_MOUSE_MODE_CLIENT) {
break;
}
spice_assert((reds_config_get_agent_mouse(reds) && reds_has_vdagent(reds)) || tablet);
if (!reds_config_get_agent_mouse(reds) || !reds_has_vdagent(reds)) {
SpiceTabletInterface *sif;
sif = SPICE_UPCAST(SpiceTabletInterface, tablet->base.sif);
sif->position(tablet, pos->x, pos->y, RED_MOUSE_STATE_TO_LOCAL(pos->buttons_state)); #服务端模式
break;
}
//采用vdagent 辅助,完成客户端模式,客户端模式下客户窗口传送的是鼠标的相对位置和形状
VDAgentMouseState *mouse_state = &inputs_channel->mouse_state;
mouse_state->x = pos->x;
mouse_state->y = pos->y;
mouse_state->buttons = RED_MOUSE_BUTTON_STATE_TO_AGENT(pos->buttons_state);
mouse_state->display_id = pos->display_id;
reds_handle_agent_mouse_event(reds, mouse_state);
break;
}
从以上代码可以分析出当设置为客户端模式下的时候,鼠标地址会通过虚拟串口下发到vdagent 底层进行处理,需要进一步分析vdagent 端的代码。
vdagent 鼠标位置处理
vdagent 中鼠标处理的调用关系如下:
mouse_postion_module 主要是完成了坐标点的转换,代码如下:
mouse->x += screen_info->x;
mouse->y += screen_info->y;
screen_info 是通过lookup_screen_info获取的当前的屏幕的信息, screen_info 的信息内容如下:
struct vdagentd_guest_xorg_resolution {
int width; //屏幕显示的宽度
int height; //屏幕显示的高度
int x; //屏幕的起始x
int y; //屏幕的起始y
int display_id; //屏幕显示的id
};
从打印的日志分析目前display_id =1时候screen_info 的x, y 都是0, 这样导致了坐标转换的时候没有映射成真实的坐标地址,x=0, y=0 时候计算的坐标会位置会映射到display_id = 0 的屏幕上,导致坐标不对。
lookup_screen_info 的代码内容如下:
static struct vdagentd_guest_xorg_resolution* lookup_screen_info(struct vdagentd_uinput *uinput, int display_id)
{
int i;
for (i = 0; i < uinput->screen_count; i++) {
if (uinput->screen_info[i].display_id == display_id) {
return &uinput->screen_info[i];
}
}
syslog(LOG_WARNING, "Unable to find output index for display id %d", display_id);
return NULL;
}
从以上代码可以分析得到,屏幕信息都存储在uinput 中, 而uinput 是一个全局的缓存,接下来需要分析整个全局的写入的位置。
uinput屏幕信息写入
阅读代码,最终定位信息的写入是在do_agent_xorg_resolution 中完成,核心代码如下:
agent_data->screen_info = g_memdup2(data, header->size);
agent_data->width = header->arg1;
agent_data->height = header->arg2;
agent_data->screen_count = n;
梳理整个协议调用关系如下:
通过调试整个协议流程通路是正常的,而通过分析协议和代码,spice_vagent 模块负责读取本地系统x11的多屏信息,
定位出最终是在vdagent_x11_get_resolutions 这个函数实现了屏幕信息关系的读取,该函数生成多屏信息有两种模式
- xrandr 模式
- xinerama 模式
在vdagent 初始化时候两种模式都支持,但是实际的代码逻辑是存在xrandr模式就不会执行xinerama, 而xinerama方式对于
多屏信息处理是正常的,xrandr 处理的只能去读系统存在几个虚拟显示设备,没有多屏关系, 代码示意如下:
GArray *vdagent_x11_get_resolutions(struct vdagent_x11 *x11, gboolean update,
int *width, int *height, int *system_screen_count)
{
GArray *res_array = g_array_new(FALSE, FALSE, sizeof(struct vdagentd_guest_xorg_resolution));
int i, screen_count = 0;
*width = *height = 0;
if (x11->has_xrandr) {
.......
} else if (x11->has_xinerama) {
.......
}
所以修改以下代码逻辑,先执行xinerama 模式,如果xinerama模式不支持,再运行xrandr模式。
GArray *vdagent_x11_get_resolutions(struct vdagent_x11 *x11, gboolean update,
int *width, int *height, int *system_screen_count)
{
GArray *res_array = g_array_new(FALSE, FALSE, sizeof(struct vdagentd_guest_xorg_resolution));
int i, screen_count = 0;
*width = *height = 0;
if (x11->has_xinerama) {
.......
} else if (x11->has_xrandr) {
.......
}
xinerama获取屏幕信息的代码如下:
XineramaScreenInfo *screen_info = NULL;
screen_info = XineramaQueryScreens(x11->display, &screen_count);
通过以上的代码可以获取一个执行screen 信息表的指针,信息表中每个元素的信息如下:
typedef struct {
int screen_number; //屏幕索引
short x_org; //起始位置x
short y_org; //起始位置y
short width; //宽度
short height; //高度
} XineramaScreenInfo;
通过上面的修改解决了双屏显示,并且双屏的坐标都正常,但是目前存在的问题有:
- 双屏配置被写死在了server代码中
- 没有办法通过配置的方式,配置几个屏幕,屏幕之间的关系(目前代码上是配置固定的左右屏)
需要进一步完善方案,要动态配置有以下两种方案:
- 通过配置文件动态配置,在启动生效
- 通过stream_agent 发送配置信息
第一种方案,当前的配置文件,没有该配置项,二次开发需要修改qemu,代价高。
第二种方案,只需要修改stream_agent 和 spice_server 即可。
分析spice_steam_agent的整个通信协议过程如下:
以上是当前版本spice_stream_agent 和 spice_server 之间通信的整个协议流程。
spice 多屏上报处理
stream_agent修改
在不改变协议的条件下,可以在STREAM_TYPE_DEVICE_DISPLAY_INFO 中加入,屏幕信息, 添加的代码如下所示
static std::vector<uint8_t> getXrandrMonitorConfig()
{
Display *display = XOpenDisplay(NULL);
Window window = DefaultRootWindow(display);
int monitors = 0;
XRRMonitorInfo* info = XRRGetMonitors(display, window, True, &monitors);
std::vector<uint8_t> out(sizeof(MonitorConfigsInfo) + monitors * sizeof(MonitorHead));
uint8_t *pBase = out.data();
MonitorConfigsInfo *monitor_info = (MonitorConfigsInfo *)(pBase );
monitor_info->monitor_count = monitors;
for (int i = 0; i < monitors; i++)
{
monitor_info->head[i].x = info[i].x;
monitor_info->head[i].y = info[i].y;
monitor_info->head[i].width = info[i].width;
monitor_info->head[i].height = info[i].height;
monitor_info->head[i].monitor_id = i;
monitor_info->head[i].primary = info[i].primary;
}
XRRFreeMonitors(info);
XFree(display);
return out;
}
修改DeviceDisplayInfoMessage消息的发送的数据格式,参考如下代码:
class DeviceDisplayInfoMessage : public OutboundMessage<StreamMsgDeviceDisplayInfo, DeviceDisplayInfoMessage, STREAM_TYPE_DEVICE_DISPLAY_INFO>
{
public:
DeviceDisplayInfoMessage(const DeviceDisplayInfo &info) : OutboundMessage(info) {}
DeviceDisplayInfoMessage(const std::vector<uint8_t> &info) : OutboundMessage(info) {} // 新增
static size_t size(const DeviceDisplayInfo &info)
{
return sizeof(PayloadType) +
std::min(info.device_address.length(), static_cast<size_t>(max_device_address_len)) +
1;
}
static size_t size(const std::vector<uint8_t> &info) // 新增
{
return sizeof(PayloadType) + info.size();
}
void write_message_body(StreamPort &stream_port, const DeviceDisplayInfo &info)
{
std::string device_address = info.device_address;
if (device_address.length() > max_device_address_len) {
syslog(LOG_WARNING,
"device address of stream id %u is longer than %u bytes, trimming.",
info.stream_id, max_device_address_len);
device_address = device_address.substr(0, max_device_address_len);
}
StreamMsgDeviceDisplayInfo strm_msg_info{};
strm_msg_info.stream_id = info.stream_id;
strm_msg_info.device_display_id = info.device_display_id;
strm_msg_info.device_address_len = device_address.length() + 1;
stream_port.write(&strm_msg_info, sizeof(strm_msg_info));
stream_port.write(device_address.c_str(), device_address.length() + 1);
}
void write_message_body(StreamPort &stream_port, const std::vector<uint8_t> &info_vec)// 新增
{
if (info_vec.size() > max_device_address_len )
{
syslog(LOG_WARNING, "system monitor info message size %d is more than the setting capacity %d", info_vec.size(), max_device_address_len);
}
StreamMsgDeviceDisplayInfo strm_msg_info{};
strm_msg_info.stream_id = 17;
strm_msg_info.device_address_len = info_vec.size() ;
strm_msg_info.device_display_id = strm_msg_info.stream_id + strm_msg_info.device_address_len;
stream_port.write(&strm_msg_info, sizeof(strm_msg_info));
stream_port.write(info_vec.data(), info_vec.size());
}
}
然后在stream agent 在读取STREAM_TYPE_START_STOP之前调用如下函数:
auto config_vec = getXrandrMonitorConfig();
stream_port.send<DeviceDisplayInfoMessage>(config_vec);
spice_server修改
在StreamChannel类中增加存储变量stream_monitors_config_vec用来保存保存上报的信息,代码如下:
inline void set_config_stream_monitor_head(std::vector<StreamMonitorHead> &heads)
{
stream_monitors_config_vec = heads;
}
inline std::vector<StreamMonitorHead> get_config_stream_monitor_head()
{
return stream_monitors_config_vec;
}
在red-stream-device.cpp 的handle_msg_device_display_info 函数中增加,解析配置的模块
MonitorConfigsInfo * config_info = (MonitorConfigsInfo * )(display_info_msg->device_address);
uint32_t moinitor_count = GUINT32_FROM_LE(config_info->monitor_count);
g_info("report monitor num is %d", moinitor_count);
std::vector<StreamMonitorHead> heads(moinitor_count);
for(int i = 0; i < moinitor_count; i++)
{
heads[i].monitor_id = config_info->head[i].monitor_id;
heads[i].primary = config_info->head[i].primary;
heads[i].x = GUINT32_FROM_LE(config_info->head[i].x);
heads[i].y = GUINT32_FROM_LE(config_info->head[i].y);
heads[i].width = GUINT32_FROM_LE(config_info->head[i].width);
heads[i].height = GUINT32_FROM_LE(config_info->head[i].height);
}
if (stream_channel)
{
stream_channel->set_config_stream_monitor_head(heads); //保存配置
}
在stream-channel.cpp 中的marshall_monitors_config 模块修改,增加读取缓存配置上报的功能,代码如下:
std::vector<StreamMonitorHead> stream_monitors_config_vec = channel->get_config_stream_monitor_head();
if (stream_monitors_config_vec.size() == 0)
{
spice_warning("stream_monitors_config_vec size is 0, use default config");
stream_monitors_config_vec ={{0, 0, 0, 0, channel->width, channel->height}};
}
int heads_size = sizeof(SpiceHead) *stream_monitors_config_vec.size();
auto msg = static_cast<SpiceMsgDisplayMonitorsConfig *>(
g_malloc0(sizeof(SpiceMsgDisplayMonitorsConfig) + heads_size));
msg->count = stream_monitors_config_vec.size();
msg->max_allowed = stream_monitors_config_vec.size();
for (int i = 0; i < msg->count; i++)
{
msg->heads[i].monitor_id = stream_monitors_config_vec[i].monitor_id;
msg->heads[i].surface_id = 0;
msg->heads[i].width = stream_monitors_config_vec[i].width;
msg->heads[i].height = stream_monitors_config_vec[i].height;
msg->heads[i].x = stream_monitors_config_vec[i].x;
msg->heads[i].y = stream_monitors_config_vec[i].y;
msg->heads[i].flags = 0;
}
完成以上功能后,就可以通过在虚拟机上配置xrandr 完成上报配置到客户端。