Nano-X圖形引擎分析及其優化

劉崢嶸   [email protected]

MicroWindows是一個開放源碼的嵌入式GUI軟件,目的是把圖形視窗環境引入到運行Linux的小型設備和平臺上。作爲X Window系統的替代品,MicroWindows可以用更少的RAM和文件存儲空間(100KB~600KB)提供相似的功能,允許設計者輕鬆加入各種顯示設備、鼠標、觸摸屏和鍵盤等;可移植性非常好,可用C語言實現;支持Intel 16位/32位CPU、MIPS R4000以及基於ARM內核的處理器芯片。由於和微軟的windows註冊商標存在衝突,從2005年月起,MicroWindows改名爲Nano-X。

       作爲一個嵌入式的GUI,Nano-X因其體積小,定製性好的優點而在嵌入式領域得到了廣泛的應用。但同時,一些專業開發者和競爭對手也對它的圖形引擎提出了諸多批評,認爲它過於原始,算法過於低效。下面就對Nano-X的圖形引擎進行分析,並試圖對之進行優化。

       Nano-X採用分層次的設計方法,在底層提供對屏幕、鼠標、觸摸屏和鍵盤的驅動,在程序能訪問實際的硬件設備和其它用戶定製設備。在中間層 有一個可移植圖形引擎,提供繪製線程、區域填充、繪製多邊形、裁減和使用顏色模式的方法。在頂層實現多種API以適應不同的應用環境。目前, MicroWindows中使用兩種流行的圖形編程接口:Microsoft Windows Win32/WinCE圖形顯示接口(GDI)和Xlib接口。前者應用於所有的Windows CE和Win32應用程序;後者就像Nano-X,應用於所有Linux X插件集的最底層,這樣可讓Linux圖形程序員X接口開發圖形應用程序。

       顯然Nano-X窗口刷新速度取決於兩個因素,一個是準備顯示的時間,也就是生成要顯示的數據的時間,GUI要運算出窗口的哪部分需要刷新重畫,它們的位置是什麼,這就是屬於中間層的圖形引擎層的工作。另一個是顯示的時間,也就是把放在緩衝區(內存)的顯示數據搬移到顯存中的時間,這部分的時間與硬件設備的外部總線訪問時間有關,如果在顯存中是連續的,還可以使用DMA的方式,這屬於驅動層的內容。

       對於Nano-X的圖形引擎層,它被API層調用(GrXXX()),提供繪製線程、區域填充、繪製多邊形、裁減和使用顏色模式的方法。由於驅動程只提供畫點、畫橫線、畫豎線和填充矩形的方法,所以它首先要把API層一些比較複雜的繪圖調用進行分化,以便能夠調用驅動層的一些比較原始的方法去實現,比如把畫矩形的函數分解爲四次畫邊的調用。而另一方面,驅動層還不能直接在窗口上畫,它必須判斷要畫的圖形是否已經超過窗口的大小,還有,要畫的窗口是否已經被別的窗口覆蓋,如果被覆蓋,就要把被覆蓋的部分從本窗口中剪切出去,這就牽涉到窗口的剪切問題了,如果有窗口的多重覆蓋,就要對每一個可能覆蓋的窗口進行檢查,這也是圖形引擎層比較複雜的原因了。

       首先要說明,所有的API調用函數中,都是必須要指明要畫的窗口的(另外也可以指明在pixelmap上畫),也就是要提供窗口的ID。下面看看MicroWindows中記錄窗口信息的數據結構:

typedef struct {

GR_WINDOW_ID wid;           /* window id (or 0 if no such window) */

GR_WINDOW_ID parent;        /* parent window id */

GR_WINDOW_ID child;          /* first child window id (or 0) */

GR_WINDOW_ID sibling;              /* next sibling window id (or 0) */

GR_BOOL inputonly;        /* TRUE if window is input only */

GR_BOOL mapped;          /* TRUE if window is mapped */

GR_COUNT unmapcount;        /* reasons why window is unmapped */

GR_COORD x;                /* absolute x position of window */

GR_COORD y;                /* absolute y position of window */

GR_SIZE width;        /* width of window */

GR_SIZE height;              /* height of window */

GR_SIZE bordersize;        /* size of border */

GR_COLOR bordercolor;         /* color of border */

GR_COLOR background;         /* background color */

GR_EVENT_MASK eventmask;       /* current event mask for this client */

GR_WM_PROPS props;          /* window properties */

GR_CURSOR_ID cursor;         /* cursor id*/

unsigned long processid;    /* process id of owner*/

} GR_WINDOW_INFO;

其中x,y,width,height分別是左上角的絕對座標和寬度、高度

parent是這個窗口的父窗口,除了根窗口,每一個窗口都有且只有父窗口。

Child是這個窗口的第一個子窗口,一個窗口可以有多個子窗口。

Sibling是這個窗口的兄弟窗口,他們同一個父窗口。

Inputonly表明這個這個窗口只是用於輸入。

Mapped表明這個窗口是否已經被顯示出來。

Unmapcount記錄窗口被隱藏的次數。

Bordercolor,bordercolor窗口邊的寬度和邊的顏色

Background窗口的背景色

Eventmask是一堆事件宏定義的或,表明這個窗口可以處理這些事件。

Props窗口的屬性,裏面有些內容和前面的重疊

Cursor這個窗口所用鼠標的ID號,各個窗口可以自己定義自己的鼠標形狀

Processed運行這個窗口的進程ID

 

對於窗口的覆蓋關係,MicroWindows是這樣規定的:

1) 子窗口可以覆蓋父窗口,而父窗口不能覆蓋子窗口

2) 在兄弟窗口之間,他們的父窗口的child指針指向的窗口優先級最高,也就是說,它覆蓋它的任意兄弟窗口,然後它的sibling指向的窗口優先級其次,通過sibling指針依次遞減。

3) 子窗口的顯示區域不能超出它的父窗口的顯示區域。

下面是一個各個窗口的關係圖:

 

根據我們前面說的窗口覆蓋的優先級關係,它們優先級的次序與左子樹優先的中序遍歷順序是一樣的。

對於任何一個API級的函數GrXXX(),它裏面的參數都要指定要畫的窗口ID,以及所用圖形上下文GC的ID。在這些API函數裏面,在調用圖形引擎層的GdXXX()真正的畫之前,都要先調用GsPrepareDrawing(id, gc, &dp)作準備。其實這個函數就是對要畫的窗口作剪切。它首先根據ID找到窗口,如果找不到再看是否ID爲pixelmap的ID,如果都不是則報錯。對於在pixelmap上畫圖形,用得很少,原理也差不多,這裏不作討論。

對於窗口,首先看窗口是否是當前的窗口,也就是看窗口是否是上一次已經做過剪切算法的窗口並且gc也沒有改變,如果是,直接返回記錄窗口的數據結構即可,否則就要作剪切了。作剪切的函數是在GsSetClipWindow()中實現的。對於用戶區的偏移量不是(0,0)的特殊情況(一般是(0,0)),在調用GsSetClipWindow()之前還要作偏移運算。

在GsSetClipWindow()(新版本這個函數調用的是在srvclip2.c裏)中,實現對一個窗口的剪切。由於一個窗口被覆蓋的情況相當的複雜,所以這個算法也是比較複雜的。

首先,一個子窗口的顯示區域不能超出父窗口的顯示區域,這就要把這個窗口與它的各級父窗口進行比較,得到經過父窗口剪切後的可顯示區域,這是通過以下代碼實現的:

       x = wp->x;

       y = wp->y;

       width = wp->width;

       height = wp->height;

       /*

       * First walk upwards through all parent windows,

       * and restrict the visible part of this window to the part

       * that shows through all of those parent windows.

       */

       pwp = wp;

       while (pwp != rootwp) {

              pwp = pwp->parent;

 

              diff = pwp->x - x;

              if (diff > 0) {

                     width -= diff;

                     x = pwp->x;

              }

 

              diff = (pwp->x + pwp->width) - (x + width);

              if (diff < 0)

                     width += diff;

 

              diff = pwp->y - y;

              if (diff > 0) {

                     height -= diff;

                     y = pwp->y;

              }

 

              diff = (pwp->y + pwp->height) - (y + height);

              if (diff < 0)

                     height += diff;

       }

這個顯示區域肯定是一個矩形,或者由於整個窗口超出父窗口的顯示區域而爲NULL。如果顯示區域爲NULL,則調用       GdSetClipRegion(clipwp->psd, NULL);然後直接返回。GdSetClipRegion()對第二個參數爲NULL的處理是調用GdAllocRegion()重新分配一個區域(這樣clipregion->numRects 肯定等於 0,clipresult = FALSE,表明不可顯示)。

如果顯示區域不爲NULL,就要進入下一步的剪切,根據前面的分析可以知道,一個窗口可以被比它優先級高的各個窗口覆蓋,覆蓋它的窗口有兩種:一種是它自己的各級子窗口,另一種是排在它前面的兄弟窗口以及它各級父窗口的優先級高的兄弟窗口,至於它父窗口的兄弟窗口的子窗口,由於不會超出它父窗口的兄弟窗口的範圍,所以雖然優先級比它高,但是不用考慮。

如何記錄被剪切過後的區域呢,剪切過後是一個不規則的形狀,但是由於都是一個被矩形被另一個矩形剪切,可以把剪切過後的形狀劃分爲多個矩形,並且用一個全局變量來記錄劃分過後的各個矩形,這個全局變量就是clipregion,它的數據結構爲:

typedef struct {

       int    size;        /* malloc'd # of rectangles*/

       int    numRects;      /* # rectangles in use*/

       int    type;             /* region type*/

       MWRECT *rects;         /* rectangle array*/

       MWRECT      extents;   /* bounding box of region*/

} MWCLIPREGION;

它的numRects記錄分解過後的矩形個數,而各個矩形的信息記錄在rects指向的數組中。

在剪切過程中,它依次檢查優先級比它高的各個窗口,如果與它有相交的則調用GdSubtractRegion()函數進行剪切,並把剪切過後的的形狀分解爲一個個的矩形保存在clipregion中。

需要注意的是,在GdSubtractRegion()的剪切中,是橫向優先的,比如如下圖所示:

 

窗口W1被W2覆蓋,覆蓋過後的綠色區域會被分解爲圖2 的R1和R2矩形

 

 

這樣,在整個剪切完成後,就可以得到一個矩形數組,這裏面保存着當前窗口可以畫的矩形區域的信息。

在當前窗口剪切完成後,就可以調用圖形引擎裏面的對應函數GdXXX()進行繪圖操作了。

由於窗口被剪切,所要畫的圖形可能有一部分被剪切掉,不能畫出來,另外沒有被剪切掉的部分則可以畫出來。在MicroWindows中,對一個圖形的顯示是逐點判斷顯示的,也就是把一個圖形分解爲一個個的象素點,對每一個象素點,調用GdClipPoint(psd, x, y)進行判斷,如果這個點在前面clipregion記錄的任意一個矩形內,則表明是可以顯示的,就可以調用驅動層的psd->drawpixel()函數進行寫屏操作。另外,如果clipregion-> numRects小於或者等於0,表明這個窗口沒有被剪切,那麼只要象素點落在屏幕範圍內,就是可以顯示的。否則,這一點就是被剪切掉了,直接返回。

在GdClipPoint()中,用全局變量clipresult 來記錄是否可顯示,若clipresult等於TRUE;則可以顯示,否則不能顯示。如果能顯示,則clipminx,clipminy ,clipmaxx ,clipmaxy 來記錄包含這個象素點的矩形的位置,否則用來記錄去掉這一點的半邊的的矩形。這樣可以緩存當前矩形。

在MicroWindows中,還提供了一個判斷一個矩形區域是否包含在剪切後的矩形數組內的函數GdClipArea(),如果參數定義的矩形完全包含在矩形數組中的任意一個矩形內,就返回CLIP_VISIBLE,如果不與任何一個矩形有重疊的區域,那麼就返回CLIP_INVISIBLE,如果與矩形數組中的矩形有重疊區域,但是不能包含在裏面,就返回CLIP_PARTIAL。

這個函數在窗口剪切中的作用是,如果返回CLIP_VISIBLE,表明要畫的部分完全暴露的,可以直接畫,不用再利用GdClipPoint()逐點判斷的畫。如果是CLIP_INVISIBLE,表明要畫的部分被完全覆蓋,可以不用再畫,直接返回即可。如果是CLIP_PARTIAL,那麼就要調用GdClipPoint()函數,逐點的畫了。

優化

在實際的應用中發現,只要編程時稍加註意,真正的需要剪切的情況比較少,可是,在MicroWindows中不加區別的爲所有的畫圖函數都進行逐點的剪切運算,所以在窗口切換和背景刷新時,如果CPU的運算能力比較低(低於50mips時),就會看見明顯的刷屏現象。從以上分析可以看出,MicroWindows的窗口剪切算法效率是比較低的,有可以改進的地方。

首先是MicroWindows雖然提供了GdClipArea()函數,但是在圖形引擎層的畫線、畫圖像等GdXXX()函數中都沒有用。

所以優化的第一步是在GdXXX()函數中使用GdClipArea()進行判斷,如果返回CLIP_VISIBLE,可以調用驅動層的psd->drawXXX()直接畫,可以大大的提高畫線、畫圖像和填充窗口背景的速度。而且對於在VRAM中連續寫值的情況,還可以使用DMA傳送顯示數據,可以進一步減小顯示時間。

但是使用上面的方法取得的優化效果是有限的,它只能在窗口沒有被其他窗口覆蓋的情況下才能進行優化。而對於如下的情況:

 

窗口W1被W2(藍色)覆蓋,如果要在黃色區域進行繪圖操作,由於被剪切後的矩形數組是橫向的,結果沒有一個矩形能夠完全包含它,如果調用GdClipArea(),結果會返回CLIP_PARTIAL,這樣即使黃色區域沒有被別的窗口覆蓋,也要調用GdCliipPoint()進行逐點的畫。而以上這種情況是經常碰到的,特別是在使用控件的時候(控件就是一個窗口)。

對它優化的辦法是,在GsSetClipWindow()進行剪切時,另外定義一個矩形數組,記錄所有覆蓋當前窗口的窗口的位置和尺寸,再用要畫的區域分別與它們相比較,看是否有重疊的,如果沒有,表明即使當前窗口被其他窗口覆蓋,但是要畫的區域沒有被覆蓋,可以如CLIP_VISIBLE一樣直接畫。如果要畫的區域與任意一個記錄的矩形區域有重疊的話,就只能進行逐點的畫了。

根據我的研究發現,事實上,可以對需要剪切的的情況進行進一步的優化。我們用一個矩形把要畫的圖形包含起來,用來表示要畫的區域。前面說過,在窗口剪切後的可畫區域會用一個矩形數組表示。可以用這個矩形數組再對要畫的矩形區域進行剪切,把要畫的矩形區域剪切成一個個的小的矩形的集合,其中每個矩形都包含在前面說到的可畫的矩形數組中。這樣,每個小矩形中的圖形都是可以直接畫的,不用逐點判斷,就可以完全不用GdCliipPoint()。

比如,如下圖所示:

 

窗口W1被W2覆蓋,我們想在W1中的W3區域大小的範圍畫圖。W1被W2剪切後生成的矩形數組爲R1,R2,R3,R4。如下圖所示:

 

我們可以用這個數組對要畫的區域W3進行再剪切,R1和R2會與要畫的區域重疊,對要畫區域進行剪切後的圖形爲下圖所示:

 

圖中的T1和T2就是剪切後的可以直接畫的圖形,不用再判斷,這樣可以進一步提高顯示速度。

由於在窗口顯示的大多數情況下,都是屬於前兩可以優化的情況,經過實驗證明,通過優化後,MicroWindows的顯示速度有較爲明顯的提高,在我們的EPSON C<?XML:NAMESPACE PREFIX = ST1 />33L05平臺上(約爲50mips)顯示速度效果良好。

 

參考文獻:

1.《The Nano-X Window System Architecture》Greg Haerr,http://www.microwindows.org

2《嵌入式Linux系統下Microwindows的應用》吳升豔 嶽春生 胡 冰,單片機及嵌入式系統應用

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章