dotnet X11 简单使用 MIT-SHM 共享内存推送图片
这是我在尝试优化 Avalonia 在 Linux 上的低端设备的渲染性能时所研究的方式,本文将告诉大家如何简单使用 XShmPutImage 等 X11 的 XShm Extension 扩展方法,通过共享内存的方式推送图片众所周知,在 X11 里面有经典的 Client-Server 模型。客户端程序是属于 Client 角色,需要将渲染界面作为图片推送到 Server 端进行在屏幕上呈现。推送的方法可以是 XPutImage 方式,也可以是本文介绍的 X11 的 XShm Extension 的 XShmPutImage 方式
上文的 XShm 是 X Shared Memory 的缩写,核心使用的是 libc 提供的 shmget 共享内存方法作为 X 的扩展。对于 XPutImage 的优势在于,如果是 Client 和 Server 都在本机的情况下,可以减少 Client 通过 X 协议传输图片到 Server 的耗时。利用 XShmPutImage 可以实现共享内存的共享,减少传输的耗时,提升渲染性能,降低渲染延迟
相关 Avalonia 链接: https://github.com/AvaloniaUI/Avalonia/discussions/16690#discussioncomment-10359540
经过我在兆芯的 ZHAOXIN KaiXian KX-U6780A 的 CPU 上的实际测试,使用 XPutImage 推送界面大图能够耗时 10 多毫秒,而使用 XShmPutImage 耗时约 0 毫秒
为什么 XShmPutImage 能够如此明显提升 XPutImage 的耗时,减少推送图片的延迟?其实 XShmPutImage 里面只是做一个通知,准确来说啥都没有做。调用 XShmPutImage 时会在 XServer 端慢慢执行渲染相关逻辑,在下文的对 XShmPutImage 的 send_event 方法参数介绍时将会重新聊到这一点
从 XPutImage 换成 XShmPutImage 只是减少传输的影响,对于界面渲染与合成器部分没有优化。实际减少的耗时有限,上文的实际测试的耗时影响,仅仅只是 XShmPutImage 对于 XPutImage 的耗时相同阶段上,被 XShmPutImage 给延后在其他模块了,总耗时减少上可能只有 1-2 毫秒
接下来将和大家演示如何在 X11 里面简单使用 XShm Extension 扩展方法推送图片渲染
本文使用的很多 X11 的 PInvoke 代码是从 CPF 和 Avalonia 里面抄的,大家可以在本文末尾找到本文所有代码的下载方法
前置的 X11 相关知识博客,请参阅 博客导航
尽管在上个世纪就能找到 XShm 相关文档,但是在实际使用之前,推荐还是判断一下当前设备的 XShm 情况,判断代码如下
var status = XShmQueryExtension(display);
if (status == 0)
{
Console.WriteLine("XShmQueryExtension failed");
}
status = XShmQueryVersion(display, out var major, out var minor, out var pixmaps);
Console.WriteLine($"XShmQueryVersion: {status} major={major} minor={minor} pixmaps={pixmaps}");如果以上两个函数任意一个 status 为 0 则代表失败,当前设备不能使用 XShm 扩展
在创建图片信息之前,需要先获取对应的色深的 visual 指针,本文设置尝试获取的是 32 色的,代码如下
XMatchVisualInfo(display, screen, depth: 32, klass: 4, out var info);
var visual = info.visual;使用 XShmCreateImage 方法创建 XImage 对象,于此同时注册 XShmSegmentInfo 信息。在 LibC 共享内存里面,共享内存的工作依赖 shmget 创建一个共享内存标识和 shmat 通过共享内存标识获取一段内存地址。这两个信息,共享内存标识和当前进程的共享内存地址信息需要存放给到 XShmSegmentInfo 信息里面,让 X 底层工作,详细请参阅 Linux进程间通信(六):共享内存 shmget()、shmat()、shmdt()、shmctl() - 52php - 博客园
const int ZPixmap = 2;
var xShmSegmentInfo = new XShmSegmentInfo();
var shmImage = (XImage*) XShmCreateImage(display, visual, 32, ZPixmap, IntPtr.Zero, &xShmSegmentInfo,
(uint) width, (uint) height);上面代码的 ZPixmap 格式请参阅
dotnet 理解 X11 的 24 位或 32 位色深窗口
如此即可创建一个颜色深度为 32 位色深的 XImage 指针
如上文所述,使用 shmget 创建一个共享内存标识符,代码如下
var mapLength = width * 4 * height; // 这里的 4 表示的是一个像素使用 4 个 byte 组成,即 ARGB 一个颜色分量一个 byte 大小
var shmgetResult = shmget(IPC_PRIVATE, mapLength, IPC_CREAT | 0777);详细关于 shmget 函数的介绍,还请参阅 Linux进程间通信(六):共享内存 shmget()、shmat()、shmdt()、shmctl() - 52php - 博客园
上面代码没有列举出来的 IPC_PRIVATE 和 IPC_CREAT 是两个常量,定义如下
// #define IPC_CREAT 01000 /* create key if key does not exist */
// #define IPC_PRIVATE ((key_t) 0) /* private key */
public const int IPC_CREAT = 01000;
public const int IPC_PRIVATE = 0;更详细的代码可以在本文末尾找到本文所有代码的下载方法
以上代码获取到的 shmgetResult 局部变量就是共享内存标识,需要将其放入到 XShmSegmentInfo 的 shmid 字段里面,且依据此变量调用 Lib C 的 shmat 获取内存地址,代码如下
var shmgetResult = shmget(IPC_PRIVATE, mapLength, IPC_CREAT | 0777);
xShmSegmentInfo.shmid = shmgetResult;
var shmaddr = shmat(shmgetResult, IntPtr.Zero, 0);获取到的共享内存地址 shmaddr 需要同样也放入到 XShmSegmentInfo 的字段进行存放,也用于 XImage 的 data 指针赋值,代码如下
xShmSegmentInfo.shmaddr = (char*) shmaddr.ToPointer();
shmImage->data = shmaddr;以上逻辑都在 Client 端执行的,现在 Server 端还不知道信息,此时通过 XShmAttach 方法即可将其关联,让 Server 端也能知道 XImage 对应的共享内存信息,包括 shm id 共享内存标识和 shm addr 共享内存地址信息
XShmAttach(display, &xShmSegmentInfo);
XFlush(display);上述代码的 XFlush 非必须,只是在本演示代码里面,期望立刻将此信息推送过去而已。慢点推送不会造成问题,也不会导致延迟
通过上文方法,现在就完成了 XShm Extension 扩展的初始化逻辑。接下来即可尝试在此共享内存里面写入数据,通知给到 Server 端在界面显示即可
以下代码演示写入一个测试界面的画面到共享内存里面,代码如下,以下将绘制一个随机颜色作为纯色界面
// 模拟绘制界面 var color = Random.Shared.Next(); color = (color | 0xFF data = shmaddr; XShmAttach(display, &xShmSegmentInfo);
XFlush(display); var gc = XCreateGC(display, handle, 0, 0); // 以下为绘制界面 // 模拟绘制界面 var color = Random.Shared.Next(); color = (color | 0xFF data = shmaddr; XShmAttach(display, &xShmSegmentInfo);
XFlush(display); var gc = XCreateGC(display, handle, 0, 0); XFlush(display); Task.Run(() => { var newDisplay = XOpenDisplay(IntPtr.Zero); while (true) { Console.ReadLine(); var xEvent = new XEvent { ExposeEvent = { type = XEventName.Expose, send_event = true, window = handle, count = 1, display = newDisplay, x = 0, y = 0, width = width, height = height, } }; // (https://tronche.com/gui/x/xlib/events/exposure/expose.html ) XLib.XSendEvent(newDisplay, handle, propagate: false, new IntPtr((int) (EventMask.ExposureMask)), ref xEvent); XFlush(newDisplay); } XCloseDisplay(newDisplay); }); var stopwatch = new Stopwatch(); while (true) { var xNextEvent = XNextEvent(display, out var @event); if (xNextEvent != 0) { break; } if (@event.type == XEventName.Expose) { // 模拟绘制界面 var color = Random.Shared.Next(); color = (color | 0xFF
页:
[1]