這是我在嘗試最佳化 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 << 24); // 讓 Alpha 部分是不透明的,防止顯示透明色讓大家以為介面看不到
for (int i = 0; i < mapLength / 4; i++)
{
var p = (int*) shmaddr;
p[i] = color;
}
還請不要在意這裡的設定純色使用的是 for 迴圈方法,本文這裡只是一個演示程式碼,大家開森就好
向共享記憶體寫入資料之後,即可透過 XShmPutImage 通知 Server 端,程式碼如下
XShmPutImage(display, drawable: handle, gc, (XImage*) shmImage, src_x: 0, src_y: 0, dst_x: 0, dst_y: 0, (uint) width, (uint) height, send_event: true);
XFlush(display);
此 XShmPutImage 的引數和使用方法和 XPutImage 非常相同。只是其最後一個引數 send_event 表示的是在 Server 端完成讀取共享記憶體的資料之後,是否要發一條 Event 給到 Client 端,讓 Client 端可以在 XNextEvent 讀取到
為什麼會需要 Server 端讀取之後傳送 Event 事件給到 Client 端?這是 Lib C 共享記憶體的一個設計問題,共享記憶體的讀取是不帶通知的,即生產端和消費端之間的寫入和讀取完成是沒有帶通知的,需要透過第三方方式進行通知。在 XShm Extension 擴充套件裡面,生產端在 Client 端寫入資料之後,透過 XShmPutImage 通知到 Server 進行消費,這也就是為什麼 XShmPutImage 執行速度非常快,耗時接近 0 毫秒的原因。當 Server 端消費完成,即讀取完成共享記憶體的資料之後,就透過傳送 Event 事件給到 Client 端,讓 Client 可以決定是否複用共享記憶體空間
如果在呼叫 XShmPutImage 之後,不等 Server 端回覆的讀取完成 XShmCompletionEvent 事件,就繼續向共享記憶體寫入資料呢?此時將會發現 Server 端讀取到錯誤的資訊,這是不合理的程式碼邏輯
以上就是使用 XShm Extension 擴充套件方法,使用共享記憶體傳送傳輸介面畫面圖片資料
我將所有的核心程式碼組裝起來,合起來的程式碼如下
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}");
XMatchVisualInfo(display, screen, depth: 32, klass: 4, out var info);
var visual = info.visual;
const int ZPixmap = 2;
var xShmSegmentInfo = new XShmSegmentInfo();
var shmImage = (XImage*) XShmCreateImage(display, visual, 32, ZPixmap, IntPtr.Zero, &xShmSegmentInfo,
(uint) width, (uint) height);
var shmgetResult = shmget(IPC_PRIVATE, mapLength, IPC_CREAT | 0777);
Console.WriteLine($"shmgetResult={shmgetResult:X}");
xShmSegmentInfo.shmid = shmgetResult;
var shmaddr = shmat(shmgetResult, IntPtr.Zero, 0);
Console.WriteLine($"shmaddr={shmaddr:X}");
xShmSegmentInfo.shmaddr = (char*) shmaddr.ToPointer();
shmImage->data = shmaddr;
XShmAttach(display, &xShmSegmentInfo);
XFlush(display);
var gc = XCreateGC(display, handle, 0, 0);
// 以下為繪製介面
// 模擬繪製介面
var color = Random.Shared.Next();
color = (color | 0xFF << 24);
for (int i = 0; i < mapLength / 4; i++)
{
var p = (int*) shmaddr;
p[i] = color;
}
// 推送圖片
XShmPutImage(display, drawable: handle, gc, (XImage*) shmImage, src_x: 0, src_y: 0, dst_x: 0, dst_y: 0, (uint) width, (uint) height, send_event: true);
XFlush(display);
在實際的程式碼裡面,推送圖片 XShmPutImage 等邏輯是寫到 Expose 事件裡面的,完全的程式碼如下
XInitThreads();
var display = XOpenDisplay(IntPtr.Zero);
var screen = XDefaultScreen(display);
var rootWindow = XDefaultRootWindow(display);
XMatchVisualInfo(display, screen, depth: 32, klass: 4, out var info);
var visual = info.visual;
var xDisplayWidth = XDisplayWidth(display, screen);
var xDisplayHeight = XDisplayHeight(display, screen);
var width = xDisplayWidth;
var height = xDisplayHeight;
var valueMask =
//SetWindowValuemask.BackPixmap
0
| SetWindowValuemask.BackPixel
| SetWindowValuemask.BorderPixel
| SetWindowValuemask.BitGravity
| SetWindowValuemask.WinGravity
| SetWindowValuemask.BackingStore
| SetWindowValuemask.ColorMap
//| SetWindowValuemask.OverrideRedirect
;
var xSetWindowAttributes = new XSetWindowAttributes
{
backing_store = 1,
bit_gravity = Gravity.NorthWestGravity,
win_gravity = Gravity.NorthWestGravity,
//override_redirect = true, // 設定視窗的override_redirect屬性為True,以避免視窗管理器的干預
colormap = XCreateColormap(display, rootWindow, visual, 0),
border_pixel = 0,
background_pixel = 0,
};
var handle = XCreateWindow(display, rootWindow, 0, 0, width, height, 5,
32,
(int) CreateWindowArgs.InputOutput,
visual,
(nuint) valueMask, ref xSetWindowAttributes);
XEventMask ignoredMask = XEventMask.SubstructureRedirectMask | XEventMask.ResizeRedirectMask |
XEventMask.PointerMotionHintMask;
var mask = new IntPtr(0xffffff ^ (int) ignoredMask);
XSelectInput(display, handle, mask);
XMapWindow(display, handle);
XFlush(display);
var mapLength = width * 4 * height;
//Console.WriteLine($"Length = {mapLength}");
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}");
const int ZPixmap = 2;
var xShmSegmentInfo = new XShmSegmentInfo();
var shmImage = (XImage*) XShmCreateImage(display, visual, 32, ZPixmap, IntPtr.Zero, &xShmSegmentInfo,
(uint) width, (uint) height);
Console.WriteLine($"XShmCreateImage = {(IntPtr) shmImage:X} xShmSegmentInfo={xShmSegmentInfo}");
var shmgetResult = shmget(IPC_PRIVATE, mapLength, IPC_CREAT | 0777);
Console.WriteLine($"shmgetResult={shmgetResult:X}");
xShmSegmentInfo.shmid = shmgetResult;
var shmaddr = shmat(shmgetResult, IntPtr.Zero, 0);
Console.WriteLine($"shmaddr={shmaddr:X}");
xShmSegmentInfo.shmaddr = (char*) shmaddr.ToPointer();
shmImage->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,
}
};
// [Xlib Programming Manual: Expose Events](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 << 24);
for (int i = 0; i < mapLength / 4; i++)
{
var p = (int*) shmaddr;
p[i] = color;
}
stopwatch.Restart();
XShmPutImage(display, drawable: handle, gc, (XImage*) shmImage, src_x: 0, src_y: 0, dst_x: 0, dst_y: 0, (uint) width, (uint) height, send_event: true);
XFlush(display);
stopwatch.Stop();
}
}
本文程式碼放在 github 和 gitee 上,可以使用如下命令列拉取程式碼。我整個程式碼倉庫比較龐大,使用以下命令列可以進行部分拉取,拉取速度比較快
先建立一個空資料夾,接著使用命令列 cd 命令進入此空資料夾,在命令列裡面輸入以下程式碼,即可獲取到本文的程式碼
git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 2557bcf18d1aecedd57b6c8c8bc0d74ed8251977
以上使用的是國內的 gitee 的源,如果 gitee 不能訪問,請替換為 github 的源。請在命令列繼續輸入以下程式碼,將 gitee 源換成 github 源進行拉取程式碼。如果依然拉取不到程式碼,可以發郵件向我要程式碼
git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin 2557bcf18d1aecedd57b6c8c8bc0d74ed8251977
獲取程式碼之後,進入 X11/WercawchallwarnefeWhedurcachay 資料夾,即可獲取到原始碼
更多技術部落格,請參閱 部落格導航
XCopyArea/XPutImage which faster?
c++ - Perfomance of (XGetImage + XPutImage) VS XCopyArea VS (XShmGetImage + XShmPutImage) VS GTK+ - Stack Overflow
MIT-SHM—The MIT Shared Memory Extension How the shared memory extension works
善用 XShm Extension 加速貼圖
c++ - How can I use XCopyArea with colors other than blue in Xlib? - Stack Overflow
c - How to use XShmGetImage and XShmPutImage - Stack Overflow
Why use XSync in X11FramebufferSurface.Blit · AvaloniaUI/Avalonia · Discussion #16690
Linux程序間通訊(六):共享記憶體 shmget()、shmat()、shmdt()、shmctl() - 52php - 部落格園