dotnet X11 簡單使用 MIT-SHM 共享記憶體推送圖片

lindexi發表於2024-08-21

這是我在嘗試最佳化 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();
        }
    }

本文程式碼放在 githubgitee 上,可以使用如下命令列拉取程式碼。我整個程式碼倉庫比較龐大,使用以下命令列可以進行部分拉取,拉取速度比較快

先建立一個空資料夾,接著使用命令列 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 - 部落格園

相關文章