SPICE源码分析(九):光标通道(Cursor Channel)实现分析
·
本文分析SPICE光标通道的实现,包括光标图像处理、位置更新和缓存优化机制。
背景与设计目标
在远程桌面场景中,鼠标光标的显示是用户体验的关键因素。SPICE将光标处理从显示通道中分离出来,形成独立的Cursor Channel,这样做有几个重要原因:
- 独立更新频率:光标移动频繁,但图像变化少,分离后可以独立优化
- 低延迟要求:光标响应对延迟敏感,独立通道便于优先处理
- 带宽优化:光标图像可以独立缓存,避免重复传输
整体架构

核心数据结构
CursorChannel 类
CursorChannel继承自CommonGraphicsChannel,管理光标状态和客户端连接。
// cursor-channel.h
// CursorChannel是光标通道的核心类
// 继承自CommonGraphicsChannel,与DisplayChannel共享图形通道基础设施
struct CursorChannel final: public CommonGraphicsChannel
{
// ===== 光标状态 =====
red::shared_ptr<RedCursorPipeItem> item; // 当前光标项
// 保存最新的光标图像
// 新客户端连接时发送此项
bool cursor_visible = true; // 光标可见性
// false时客户端隐藏光标
SpicePoint16 cursor_position; // 光标位置(x, y)
// 16位坐标,支持65535分辨率
uint16_t cursor_trail_length; // 光标轨迹长度
uint16_t cursor_trail_frequency; // 光标轨迹频率
// Windows光标轨迹特效支持
// ===== 鼠标模式 =====
uint32_t mouse_mode = SPICE_MOUSE_MODE_SERVER;
// SERVER模式:服务器控制光标位置
// CLIENT模式:客户端本地控制,减少延迟
// ===== 核心方法 =====
void process_cmd(red::shared_ptr<const RedCursorCmd> &&cursor_cmd);
void set_mouse_mode(uint32_t mode);
void reset();
void do_init();
void on_connect(RedClient *client, RedStream *stream,
int migration, RedChannelCapabilities *caps) override;
};
类继承关系:
详见下方流程图。
CursorChannelClient 类
每个连接的客户端对应一个CursorChannelClient实例,管理客户端特定的光标缓存。
// cursor-channel-client.h
// 管理单个客户端的光标通道连接
class CursorChannelClient final: public CommonGraphicsChannelClient
{
// ===== 光标缓存管理 =====
// 每个客户端维护独立的缓存
// 避免重复发送相同的光标图像
RedCacheItem* cache_find(uint64_t id); // 查找缓存
int cache_add(uint64_t id, size_t size); // 添加缓存
void reset_cursor_cache(); // 重置缓存
// ===== 消息发送 =====
void send_item(RedPipeItem *pipe_item) override;
// 根据PipeItem类型选择序列化方法
// ===== 迁移支持 =====
void migrate() override;
void on_disconnect() override;
// ===== 私有数据 =====
red::unique_link<CursorChannelClientPrivate> priv;
// 包含:缓存哈希表、统计信息等
};
客户端缓存设计:
每个客户端维护独立的光标缓存,使用哈希表存储 cursor_id → RedCacheItem 映射,并通过LRU双向链表管理缓存淘汰,最大支持256个缓存条目。
RedCursorCmd 命令结构
光标命令从QXL设备传入,描述光标的各种操作。
// red-parse-qxl.h
// QXL光标命令的解析结果
struct RedCursorCmd {
// ===== 命令类型 =====
uint8_t type;
// QXL_CURSOR_SET: 设置光标形状和位置
// QXL_CURSOR_MOVE: 仅移动位置
// QXL_CURSOR_HIDE: 隐藏光标
// QXL_CURSOR_TRAIL: 设置轨迹效果
// ===== 命令数据(联合体) =====
union {
// ----- SET命令 -----
struct {
SpicePoint16 position; // 初始位置
uint8_t visible; // 是否可见
SpiceCursor shape; // 光标形状数据
// shape包含:
// - header: 宽、高、热点、unique ID
// - data: 像素数据
} set;
// ----- TRAIL命令 -----
struct {
uint16_t length; // 轨迹长度
uint16_t frequency; // 更新频率
} trail;
// ----- MOVE命令 -----
SpicePoint16 position; // 新位置
} u;
// ===== 资源管理 =====
QXLReleaseInfoExt release_info_ext;
// 命令处理完成后释放Guest资源
};
光标图像处理流程
命令处理主流程
// cursor-channel.cpp
// 处理来自QXL设备的光标命令
void CursorChannel::process_cmd(red::shared_ptr<const RedCursorCmd> &&cursor_cmd)
{
// ===== 创建管道项 =====
// 将命令封装为PipeItem,用于异步发送
auto cursor_pipe_item = red::make_shared<RedCursorPipeItem>(cursor_cmd);
// ===== 根据命令类型更新状态 =====
switch (cursor_cmd->type) {
case QXL_CURSOR_SET:
// ===== 设置新光标 =====
// 更新可见性
cursor_visible = !!cursor_cmd->u.set.visible;
// 保存当前光标项
// 新客户端连接时会收到此光标
item = cursor_pipe_item;
break;
case QXL_CURSOR_MOVE:
// ===== 移动光标 =====
// 显示光标(如果之前隐藏)
cursor_visible = true;
// 更新位置
cursor_position = cursor_cmd->u.position;
break;
case QXL_CURSOR_HIDE:
// ===== 隐藏光标 =====
cursor_visible = false;
break;
case QXL_CURSOR_TRAIL:
// ===== 设置轨迹效果 =====
cursor_trail_length = cursor_cmd->u.trail.length;
cursor_trail_frequency = cursor_cmd->u.trail.frequency;
break;
}
// ===== 决定是否发送到客户端 =====
// 条件判断考虑鼠标模式
if (is_connected() &&
(mouse_mode == SPICE_MOUSE_MODE_SERVER // 服务器模式:发送所有
|| cursor_cmd->type != QXL_CURSOR_MOVE // 非MOVE命令:始终发送
|| cursor_show)) { // 光标刚显示:发送位置
pipes_add(cursor_pipe_item);
}
}
命令处理流程图:

光标图像缓存机制
SPICE使用unique ID来缓存光标图像,避免重复传输相同的光标。
// cursor-channel.cpp
// 填充光标数据,处理缓存逻辑
static void cursor_fill(CursorChannelClient *ccc,
RedCursorPipeItem *cursor,
SpiceCursor *red_cursor,
SpiceMarshaller *m)
{
// ===== 复制光标头信息 =====
*red_cursor = cursor_cmd->u.set.shape;
// ===== 缓存处理 =====
if (red_cursor->header.unique) {
// 光标有unique ID,可以缓存
// ===== 检查缓存命中 =====
if (ccc->cache_find(red_cursor->header.unique)) {
// 缓存命中!设置标志,不发送图像数据
red_cursor->flags |= SPICE_CURSOR_FLAGS_FROM_CACHE;
return;
}
// ===== 缓存未命中,尝试添加 =====
if (ccc->cache_add(red_cursor->header.unique, 1)) {
// 添加成功,告诉客户端缓存此光标
red_cursor->flags |= SPICE_CURSOR_FLAGS_CACHE_ME;
}
}
// ===== 发送图像数据 =====
if (red_cursor->data_size) {
SpiceMarshaller *m2 = spice_marshaller_get_submarshaller(m);
cursor->add_to_marshaller(m2, red_cursor->data, red_cursor->data_size);
}
}
缓存工作流程:
缓存流程说明:
- 检查光标是否有unique ID
- 如果有,通过
cache_find()查找缓存 - 缓存命中:设置
FROM_CACHE标志,不发送图像数据(客户端使用本地缓存) - 缓存未命中:添加到缓存,设置
CACHE_ME标志,发送完整图像数据
消息序列化与发送
// cursor-channel.cpp
// 发送管道项到客户端
void CursorChannelClient::send_item(RedPipeItem *pipe_item)
{
SpiceMarshaller *m = get_marshaller();
switch (pipe_item->type) {
case RED_PIPE_ITEM_TYPE_CURSOR:
// ===== 发送光标命令 =====
red_marshall_cursor(this, m,
static_cast<RedCursorPipeItem*>(pipe_item));
break;
case RED_PIPE_ITEM_TYPE_INVAL_ONE:
// ===== 发送缓存失效(单个) =====
red_marshall_inval(this, m,
static_cast<RedCachePipeItem*>(pipe_item));
break;
case RED_PIPE_ITEM_TYPE_CURSOR_INIT:
// ===== 发送初始化消息 =====
// 新客户端连接时发送
reset_cursor_cache(); // 重置缓存
red_marshall_cursor_init(this, m);
break;
case RED_PIPE_ITEM_TYPE_INVAL_CURSOR_CACHE:
// ===== 发送缓存全部失效 =====
reset_cursor_cache();
init_send_data(SPICE_MSG_CURSOR_INVAL_ALL);
break;
}
begin_send_message();
}
消息类型对照:
| QXL命令类型 | SPICE消息类型 | 说明 |
|---|---|---|
| QXL_CURSOR_SET | SPICE_MSG_CURSOR_SET | 设置光标形状 |
| QXL_CURSOR_MOVE | SPICE_MSG_CURSOR_MOVE | 移动光标位置 |
| QXL_CURSOR_HIDE | SPICE_MSG_CURSOR_HIDE | 隐藏光标 |
| QXL_CURSOR_TRAIL | SPICE_MSG_CURSOR_TRAIL | 轨迹效果 |
| - | SPICE_MSG_CURSOR_INIT | 初始化 |
| - | SPICE_MSG_CURSOR_INVAL_ALL | 清空缓存 |
鼠标模式与光标位置
双鼠标模式
SPICE支持两种鼠标模式,影响光标位置的传输方式:
// cursor-channel.cpp
// 设置鼠标模式
void CursorChannel::set_mouse_mode(uint32_t mode)
{
mouse_mode = mode;
// 模式改变可能影响后续MOVE命令是否发送
}
模式对比:
| 特性 | SERVER模式 | CLIENT模式 |
|---|---|---|
| 光标位置 | 服务器发送 | 客户端本地 |
| MOVE命令 | 发送所有 | 通常不发送 |
| 延迟 | 较高 | 很低 |
| 适用场景 | 需要精确同步 | 日常使用 |
- SERVER模式:服务器发送所有MOVE命令,客户端显示服务器指定的光标位置
- CLIENT模式:服务器不发送MOVE命令,客户端本地计算光标位置,延迟更低
位置更新逻辑
// cursor-channel.cpp
case QXL_CURSOR_MOVE:
// ===== 显示状态跟踪 =====
cursor_show = !cursor_visible; // 如果之前隐藏,标记需要显示
cursor_visible = true; // 现在可见
// ===== 更新位置 =====
cursor_position = cursor_cmd->u.position;
break;
// ===== 发送决策 =====
// 在CLIENT模式下,MOVE命令通常不发送
// 除非光标从隐藏变为显示(cursor_show为true)
if (mouse_mode == SPICE_MOUSE_MODE_SERVER
|| cursor_cmd->type != QXL_CURSOR_MOVE
|| cursor_show) {
pipes_add(cursor_pipe_item);
}
客户端连接处理
新连接初始化
// cursor-channel.cpp
// 处理新客户端连接
void CursorChannel::on_connect(RedClient *client, RedStream *stream,
int migration, RedChannelCapabilities *caps)
{
// ===== 创建客户端实例 =====
auto ccc = cursor_channel_client_new(this, client, stream,
migration, caps);
// ===== 发送初始化消息 =====
// 包含当前光标状态
ccc->pipe_add_type(RED_PIPE_ITEM_TYPE_CURSOR_INIT);
// ===== 发送当前光标 =====
// 如果有保存的光标项
if (item) {
ccc->pipe_add(item);
}
}
// 初始化消息序列化
static void red_marshall_cursor_init(CursorChannelClient *ccc,
SpiceMarshaller *m)
{
CursorChannel *channel = ccc->get_channel();
ccc->init_send_data(SPICE_MSG_CURSOR_INIT);
SpiceMsgCursorInit msg;
msg.visible = channel->cursor_visible;
msg.position = channel->cursor_position;
msg.trail_length = channel->cursor_trail_length;
msg.trail_frequency = channel->cursor_trail_frequency;
spice_marshall_msg_cursor_init(m, &msg);
}
连接初始化流程:
新客户端连接时:
- 创建
CursorChannelClient实例 - 发送
CURSOR_INIT消息(包含当前光标状态) - 如果有保存的光标项,发送
CURSOR_SET消息
迁移支持
VM迁移时需要保持光标状态一致:
// cursor-channel-client.cpp
void CursorChannelClient::migrate()
{
// ===== 发送迁移数据 =====
// 包含缓存状态等
pipe_add_type(RED_PIPE_ITEM_TYPE_MIGRATE_DATA);
}
// 迁移数据包含:
// 1. 光标缓存ID列表
// 2. 当前光标状态
// 3. 可见性、位置等
性能优化
缓存效率
典型工作负载下的缓存命中率:
- 常用光标(箭头、手型、等待):>95%命中率
- 文本编辑光标:>90%命中率
- 动态/自定义光标:首次未命中,后续命中
带宽节省效果:
- 32x32 ARGB光标:4KB/次
- 64x64 ARGB光标:16KB/次
- 缓存命中时:约10字节(仅ID)
模式选择建议
| 场景 | 推荐模式 | 原因 |
|---|---|---|
| 日常办公 | CLIENT模式 | 低延迟体验 |
| 远程游戏 | CLIENT模式 + 相对鼠标 | 响应速度要求高 |
| 精确绘图 | SERVER模式 | 需要位置同步 |
| 多客户端共享 | SERVER模式 | 保持一致性 |
与DisplayChannel的关系
CursorChannel和DisplayChannel都继承自CommonGraphicsChannel,共享迁移支持和基础设施:
| 通道 | 职责 |
|---|---|
| DisplayChannel | 屏幕内容、图形命令、图像压缩、视频流 |
| CursorChannel | 光标图像、光标位置、光标缓存、轨迹效果 |
两个通道由同一个Worker线程处理,使用同一个QXL设备。
总结
| 特性 | 实现 |
|---|---|
| 独立通道 | 光标与显示分离,可独立优化 |
| 缓存机制 | unique ID缓存,减少重复传输 |
| 双模式支持 | SERVER/CLIENT模式适应不同场景 |
| 低延迟 | CLIENT模式本地处理光标 |
| 迁移支持 | 缓存状态随VM迁移 |
光标通道的设计体现了SPICE的性能优化理念:
- 分离关注点:光标与显示独立处理
- 缓存优化:相同光标只传输一次
- 模式适配:根据场景选择最优方案
- 无缝迁移:保持用户体验连续性
更多推荐


所有评论(0)