Friday, December 25, 2009

Android with 3D Effects

年終了。

最近幾個月不知道為什麼,沒辦法專心工作。利用上 ptt 跟看漫畫的空檔,把說很久的特效弄出可以 demo 的樣子



感謝 Erin 的幫忙,這段影片展示了 Cube 跟 Fire 特效。 WindowManagerService 允許的地方都可以接特效,但現在只有接在程式啟動時。

目前的進展可以在 gitorious 上取得。不過很顯然地,還有很多東西要弄。

Sunday, November 29, 2009

Fast bit counting

最近 mesa3d-dev mailing list 有一個討論串在討論 bit counting,也就是在算一個 unsigned int 裡有幾個 bit 是 1。

假設 unsigned int 有 32 bits 寬,最簡單的算法就是一個迴圈跑 32 次,一個一個 bit 去看。想當然這不是有效率的算法。事實上,只要做簡單的修改,就可以讓迴圈跑的次數降到跟 bit 為 1 的 bit 數相同。換句話說,如果 unsigned int 裡只有 3 個 bit 為 1,迴圈只要跑三次就可以。不過像這麼基礎的運算,一定有不少人下過功夫在找最快的算法。一個問題,工程師會去尋找更好的方法;但是身為鄉民,我們感興趣的只有最好的方法,而且我們從不自己找!很幸運地,在最開頭提到的討論串裡頭,就有人提出一個號稱最快的算法。

討論串中提到的方法經過簡化可以寫成

int bitcount(unsigned int v)
{
v = v - ((v >> 1) & 0x55555555);
v = (v & 0x33333333) + ((v >> 2) & 0x33333333);
v = (v + (v >> 4)) & 0x0f0f0f0f;
return (v * 0x01010101) >> 24;
}


在深入去看這段程式碼前,最好可以用抽像點的方式看它想做的事



上圖中,v0 是使用者的輸入。展開成二進位表示法,每個 bit 的值不是 1 就是 0。如果有一個方法可以讓所有的 bit 變成兩個兩個一組 (v1)、4 個 4 個一組 (v2)、8 個 8 個一組 (v3)、一直到 32 個 bit 自己成一組 (v5),那 v5 的值剛好就是我們要的結果。從 v4 看起,假設我們已經有 v4 了,那麼應該不難發現
v5 = (v4 & 0xffff) + (v4 >> 16);


以此類推,很快就可以發現我們需要的是

int bitcount2(unsigned int v)
{
v = (v & 0x55555555) + ((v >> 1) & 0x55555555); /* v1 */
v = (v & 0x33333333) + ((v >> 2) & 0x33333333); /* v2 */
v = (v & 0x0f0f0f0f) + ((v >> 4) & 0x0f0f0f0f); /* v3 */
v = (v & 0x00ff00ff) + ((v >> 8) & 0x00ff00ff); /* v4 */
v = (v & 0x0000ffff) + ((v >> 16) & 0x0000ffff); /* v5 */
return v;
}


這時再回過頭去看 bitcount,可以注意到雖然算法不完全相同,但它前三行在做的也是求 v1、v2 與 v3。但它接下來不是求 v4,而是把 v3 乘上 0x01010101。透過四則運算,這個乘法可以改寫成加法

v = (v3 << 24) + (v3 << 16) + (v3 << 8) + v3


把 v3 的展開式代入,會發現



將這個結果向右 shift 24 bits 得到的也是 v5。這就是 bitcount 的做法。

如果把寫死的 magic number 換成適當型別的除法與補數運算,這個 bitcount 函數對 32/64/128-bits 寬的 unsigned int 都可以適用。寬度的限制是來自於 v4' 的第一個小括號,它不能大於或等於 2^8。不過如果 bitcount 求到 v4 再來做乘法,這個限制也可以被放寬。

Thursday, June 25, 2009

一千零一夜之 GEM Object

這個系列是讀書筆記,作者可能沒有跟主題有關的開發經驗。


GEM object 是一種 buffer,一種可以在 A 程式被配置,在 B 程式被使用的 buffer。當 userspace 產生一個 GEM object 時,以 i915 為例,它會呼叫 drm_gem_object_alloc。從這個函數的頭幾行

struct drm_gem_object *obj;

BUG_ON((size & (PAGE_SIZE - 1)) != 0);

obj = kcalloc(1, sizeof(*obj), GFP_KERNEL);

obj->dev = dev;
obj->filp = shmem_file_setup("drm mm object", size, VM_NORESERVE);
if (IS_ERR(obj->filp)) {
kfree(obj);
return NULL;
}


可以看出,它只是一塊 shared memory。就這個方式來看,GEM object 其實不是新的東西。

但這塊 buffer 除了 A、B 程式會用到外,它還另外有一個使用者,這個使用者是 GPU。之所以把 shared memory 包裝成 GEM object 最主要的原因是,kernel 需要同步 CPU 和 GPU 對這塊 buffer 的存取。很明顯地,如果讓 CPU 與 GPU 同時寫入同一塊記憶體,出來的結果一定無法預期。在 GEM object 裡,處理同步的方法是引入 domain 的機制。所有人在存取 GEM object 前,必須先把它的 domain 設好。如果是 GPU 要讀取或寫入,那就需要先把 read domain 或 write domain 設成 GPU;相對的,如果是 CPU 要讀取或寫入,則需要把 read/write domain 設成 CPU。DRM 保證的是,只要正確地指定 domain,存取到的資料也會是正確的。

為了解 domain 跟同步的關係,我們可以看一下當我們把 read domain 指定為 CPU 時,kernel 會幫我們做什麼處理。這部份完整的程式碼可以在 i915_gem_object_set_to_cpu_domain 看到。

首先,kernel 要確定之前所下的 GPU 指令已經全部執行完畢,而且 GPU cache 要 flush 掉

i915_gem_object_flush_gpu_write_domain(obj);
ret = i915_gem_object_wait_rendering(obj);


GPU 的運作方式是會依序執行某個指定的 ring buffer 裡面的指令,而開發者要做的是把指令放到這個 buffer。為了達到目的,kernel 會在 ring buffer 最後加上 flush 跟 interrupt 指令,並且開始等待中斷。我們可以想像,當 kernel 收到中斷時,也就表示之前的指令都已經執行,包含它自己加上的 flush 指令。

事實上,CPU 除了直接存取 shared memory 外,也可以從 AGP aperture 去存取,這在 i915 裡稱做 GTT domain。所以 kernel 也要確定這邊沒有資料被 cache 住

i915_gem_object_flush_gtt_write_domain(obj);


這些動作可能造成記憶體上的資料被改變,而,這時候 CPU 還不知道這件事。所以只要再確保 CPU 可以知道這些變動

i915_gem_object_set_to_full_cpu_read_domain(obj);
if ((obj->read_domains & I915_GEM_DOMAIN_CPU) == 0) {
i915_gem_clflush_object(obj);
obj->read_domains |= I915_GEM_DOMAIN_CPU;
}


我們就可以說 GEM object 已經被移到 CPU read domain。

GEM object 是 shared memory 的特性,讓 X server 可以直接把 buffer 傳給 client 做 DRI。它同時也可以當做到 OpenGL 裡頭各種 buffer object 的 storage。除了 Intel 外,在 2.6.31 也可以看到初步的 ATI Radeon KMS 支援。雖然從使用者的經驗來看,似乎只是進入另一個黑暗時期。但是在領袖的帶領下,我們好像也可以看到光了!

Wednesday, June 24, 2009

一千零一夜之 Kernel and Firmware

這個系列是讀書筆記,作者可能沒有跟主題有關的開發經驗。

Driver 需要 firmware 時會呼叫 request_firmware,這個函數會在 /sys 底下產生適當的裝置讓 userspace 上傳 firmware 給 driver。它會等到 firmware 上傳完成 (或發生錯誤) 才回傳。除了建立適當的裝置,它也會送一個 uevent,內容類似

KERNEL[1245809267.000297] add      /devices/pci0000:00/0000:00:1c.2/0000:02:00.0/firmware/0000:02:00.0 (firmware)
UDEV_LOG=3
ACTION=add
DEVPATH=/devices/pci0000:00/0000:00:1c.2/0000:02:00.0/firmware/0000:02:00.0
SUBSYSTEM=firmware
FIRMWARE=iwlwifi-3945-2.ucode
SEQNUM=1226


uevent 事件通常是 udev 在處理,在遇到 firmware 事件,也就是 SUBSYSTEM 是 firmware 的事件時,udev 預設會執行 /lib/udev/firmware.agent (在 Debian 上) 來上傳 firmware。簡化過的 firmware.agent 其實只做了

echo 1 > /sys/$DEVPATH/loading # 準備上傳
cat $firmwaredir/$FIRMWARE > /sys/$DEVPATH/data
echo 0 > /sys/$DEVPATH/loading # 上傳完畢


注意到上面大寫的變數內容都是 uevent 提供,像是 $DEVPATH 就是 kernel 剛產生用來接受 firmware 上傳的虛擬裝置。

這是 firmware 擺在 filesystem 上的情形。當 CONFIG_FIRMWARE_IN_KERNEL 打開時,如果 driver 被編進 kernel,firmware 也會一起被編進去。在 request_firmware 中有一段

for (builtin = __start_builtin_fw; builtin != __end_builtin_fw;
builtin++) {
if (strcmp(name, builtin->name))
continue;
dev_info(device, "firmware: using built-in firmware %s\n",
name);
firmware->size = builtin->size;
firmware->data = builtin->data;
return 0;
}


會先檢查 driver 要求的 firmware 是否有內建。這邊的 __start_builtin_fw 與 __end_builtin_fw 是 link time 產生的 symbol,所以不會在任何 C code 找到它們的定義。在 vmlinux.lds (或 include/asm-generic/vmlinux.lds.h) 可以看到,它們的值會是所有 .builtin_fw section 內容的開始位址與結束位址。

.builtin_fw 的內容要再找到 firmware/Makefile。在那份 Makefile 可以看到 kbuild 會為每一個要內建的 firmware 產生一份 assembly code。這些 assembly code 編出來的 object code 會跟 kernel link 在一起,達成 firmware 內建的目的。從 assembly code 可以看出,這些 object code 的內容會是 firmware 本身,以及一個放在 .builtin_fw section 的資料結構,給出 firmware 的檔名、位置、還有大小。

這邊看到內建 firmware 的做法其實是 kernel 常見的手法之一。寫 driver 最開始會加的 module_init(entry_point) 是另一個例子。這些 macro 會產生 initcall,而所有的 initcall 在 link time 也是用類似的方式被收集。在電腦剛開機進 userspace 前它們會依照被收集的順序依序被執行。這邊可以看到一個要注意的小地方。在每個 subsystem (rtc, scsi, ...) 的 Makefile 中,核心的部份必須放在 driver 之前。

Tuesday, June 9, 2009

Android Wave

上篇文章提到的程式碼已上線,有興趣的朋友可以到 Android Eee PC 專案網頁下載。

在做 clean build 的空檔,請 Jeremy 幫忙錄了一段影片,以證明加速是真的 XD



不過錄到後半,我頭暈得差點連瀏覽器 icon 在哪都找不到,所以請注意不要靠太近看。再來兩天要參加 FreedomHEC Taipei 2009,順便休息一下。

Thursday, June 4, 2009

Android 3D acceleration on x86

過去一、二個月的文章都是圍繞著 android x86 與 mesa/dri 在打轉,原因無它,因為這陣子在做的事恰好就是 Android 在 Eee PC 上的 3D 加速。因為 android 跟 mesa 都是第一次接觸,所以確實費了不少工夫在認識它們。

Android 在起來的時候,會試著載入 /system/lib 底下的 libagl.so 與 libhgl.so。前者是 OpenGL ES 與 EGL 的純軟體實作,source code 在 frameworks/base/opengl/libagl/ 底下;後者則與硬體相關,目前沒有開放的實作。所以我做的事基本上就是寫一份 libhgl.so。

之前在 DRI Overview 有提到,只要有適當的 loader,DRI driver 可以在 X 以外的平台使用。目前我的 libhgl.so 實作是基於 eagle,並只支援 intel graphics chipsets。因為 i915_dri.so 實作了 OpenGL,而不是 OpenGL ES,所以定點數與部份 ES 才有的 extension 沒有辦法支援。這個問題最快的解決方式應該是改用 Gallium 搭配 OpenGL ES state tracker,這也是接下來要評估的。

對 Android 本身,主要則有三個部份需要修改。一是 EGLDisplaySurface 要用 kernel modesetting 的實作取代,同時 kernel 也要上 2.6.29;二是 GPUHardware 要改用 i915 的 GEM object 實作;三是 Surface 要能認得 GEM object。

目前整個 Android 系統可以正常運作。SurfaceFlinger 或一些 3D 軟體像是 Android-GL 也可以正確地被加速。不過目前的實作比較接近於驗證這個方式的可行性。程式碼整理好後會放在 Android Eee PC,到時有興趣的開發者都可以一同參與。

Friday, May 15, 2009

Android Window System

在上星期 0xlab 的討論會中,我對 Android 的視窗系統做了簡短的分享,投影片可以在這邊取得。簡報內容主要在介紹 SurfaceManager,同時也附上簡短的範例程式,示範利用 Android 的 native library 取得可用來繪圖的記憶體。

之前的文章提到 Android 跟 cairo 的結合,也是利用一樣的方法,麻煩的地方反而是在於 cairo 的編譯。我當時是照著 cairo (與 pixman) 的 Makefile.am 寫一份 Android.mk,非常的苦。jserv 後來提到了 agcc,或許可以拿來與既有程式的 autotools 結合。

不過苦也有苦的好處,因為苦慣了就不會害怕。不小心還會弄出怪怪的東西



花了一陣子的時間在認識 Android,慢慢地也比較能掌握。希望下篇文章開始,本小站可以脫離嘴炮,向上提升。

Wednesday, April 29, 2009

一千零一夜之 eagle

這個系列是讀書筆記,作者可能沒有跟主題有關的開發經驗。

之前的文章提到過,只要有適當的 loader,DRI driver 也可以用在 X server 以外的平台,eagle 就是一個例子。eagle 提供開發者接近於 EGL 的 API,並且可以運作在支援 DRI2 的 X server 與支援 GEM 的 intel drm 之上。這篇文章主要想討論後者。

eagle 與視窗系統無關的實作,行數落在大約 800 行左右。intel drm 的支援也只在 250 行左右。對它使用到的技術有些許了解後,只要花一個晚上就可以讀完,算是學習 linux 3D 實作不錯的途徑。也因為它的簡單,閱讀過程如果有難以理解的部份,也可以馬上修改跑跑看。如果這兩部份看完後想要再看 DRI2 的部份,eagle 也可以讓人滿足。

intel drm 的支援在 eagle 中稱為 backend,以 EagleBackend 表示。Backend 與視窗系統相關,要提供的主要功能有

  • 建立 EGLSurface

  • 提供 DRI driver 需要的 GEM buffer

  • 更新畫好的內容到螢幕上



intel drm backend 建立 EGLSurface 的函數是 intelCreateSurfaceForName。這邊的 name 型別為 uint32_t,指的是 GEM buffer 的 identifier。如果沒給的話,backend 會產生一塊新的 buffer。client 可以要求 double buffering,這時 surface 的 backBuffer 會設為 EGL_TRUE。

intelGetBuffers 負責提供 DRI driver 需要的 GEM buffer,像是 front buffer,back buffer 或 depth buffer 等。這邊要特別注意,在 double buffering 的情形,也就是 backBuffer 設為 EGL_TRUE 的時候,即便 DRI driver 要求了 front buffer,intelGetBuffers 還是會產生新的 buffer 回傳給 DRI driver,而不是回傳 intelCreateSurfaceForName 時給定或產生的 front buffer。這個新產生的 buffer 也被稱為 fake front buffer。

最後看到 intelSwapBuffers,也就是 eglSwapBuffers 會呼叫的函數。可以看到,當不是 double buffering 時,這個函數會直接 return,因為 DRI driver 本來就是畫在真正的 front buffer 上。若是 double buffering 的話,intelSwapBuffers 會把 DRI driver 以為的 (fake) front buffer 複製到真正的 front buffer 上。

Backend 大致上就處理這些事情。eagle 的核心則負責提供接近 EGL 的 API。它同時也是 DRI loader,選定適當的 EagleBackend 並載入適當的 DRI driver。除了選擇與 dlopen 適當的 .so 檔外,核心其餘的程式碼大多在處理 EGL 的型別與 DRI 型別的對映。這個部份很基礎,也是所有想寫 loader 的開發者都要熟悉的東西。這邊的程式碼比較直接,有興趣的人可以自行閱讀。

在初次研讀 eagle 的過程中,我最感興趣的其實是這篇文章沒提到的 glapi。這是 mesa 或 mesa 自動產生的程式碼,其中後者佔了絕大多數。在許多功用中,其中一項功用是提供 OpenGL (ES) 的 global symbols,讓 link 到 libeagle.so 的程式可以呼叫 gl* 等函數。這可以算是 mesa build system 的一部份,對熟悉 linux 3D 的組成有相當的幫助。

Friday, April 17, 2009

一千零一夜之 Android Binder

這個系列是讀書筆記,作者可能沒有跟主題有關的開發經驗。

Binder 是 android 大量使用的 IPC 機制。當使用者在 launcher (home) 按了某個 App 時,這個動作會經由 ActivityManager 向 zygote 發出請求,並從 zygote fork 出新的 process 執行被選擇的 App。這個過程大致上有 4 個 process 參與:home、system server、zygote 和新產生的 appA。更仔細去看的話,

  1. system server 向 home 送出 touchscreen 事件

  2. home 向 system server 送出 start activity 事件

  3. system server 向 zygote 送出 appA 的啟動參數

  4. appA 向 system server 送出 attach 事件


除了第 3 項以外,其它跨 process 的溝通都是採用 binder。Zygote 是 android 上少數幾個聽 unix socket 的程式。驗證這個說法最簡單的方法是去看 process 的 memory maps 還有 opened fd,這個技巧在之前有用過,這邊暫且略過。

一個 IPC 機制的效能很大一部份取決於它資料交換的能力,從這點去切入是不錯的途徑。所有參與 binder 的 process 都會在一開始向 /dev/binder 裝置 mmap 一塊 read-only 的記憶體,大小在幾個 MB 左右。A process 傳送資料給 B process 的做法是,由 A 對 /dev/binder 做 ioctl 將資料先送給 binder driver。依資料大小,binder driver 動態配置足夠的 physical memory 存放來自 A 的資料,並在 B 一開始 mmap 得到的 address space 裡尋找一段足夠大且未使用的 address area。找到後,binder driver 會修改 B 的 page table,讓這段 address area 映射到剛剛動態配置的 physical memory。如此一來,B 可以直接存取 A 送過來的資料;等到 B 不需要這些資料時,再由 B 負責把這塊記憶體還回去給系統。這段實作可以參改 binder driver 的 binder_update_page_range 函數。

Process 對 binder 的操作只透過 ioctl 做 buffer 的讀寫。Buffer 的內容是一系列的 (command, argument) 組。每個 command 為 4 bytes,argument 的長度視 command 而定,可能是固定的,也可能是變動的。當 A 跟 B 交換資料時,通常會由一方送出 BC_TRANSACTION command,再由另一方回覆 BC_REPLY command。Transaction/reply 的 argument 除了一段 header 外,其餘的資料部份會使用上一段討論的方式做交換。資料的長度跟內容由 A、B 另行約定,可能是整數、浮點數或字串等。除了上述的基本資料型別外,還有三種用 struct flat_binder_object 表示的特殊型別:BINDER、HANDLE、FD。FD 型別讓 A、B 可以交換 file descriptor,也就是說,A 可以打開某個檔案,把 fd 傳給 B,讓 B 去讀寫。或者說,也是更重要的,surface manager 可以準備一塊 surface,把 surface 的 fd (一塊 ashmem 記憶體) 傳給一個 app,讓 app 可以在上面作畫。有了這層理解,我們對 android 可以有更自由的發揮,例如,可以不改一行 cairo 的程式碼,就利用 cairo 與 c++ 寫出一個會動的時鐘:



回到正題,我們知道 binder 還有 BINDER 與 HANDLE 這兩個特殊型別。它們的功用可以用 service port 跟 connection 做類比。就像 httpd 可以開 80 port 提供服務,client 可以跟它建立 connection 使用服務一樣,在 android 上頭,WindowManagerService 可能建立了一個 BINDER,提供了視窗操作的服務。其它程式可能會建立這個服務的 HANDLE。在 binder 之上,android 提供了 aidl 讓開發者可以把 remote service 當做 local object 操作,所以開發者可以直接呼叫 mWindowManager.setRotation 來旋轉螢幕,而幾乎可以不用知道 BINDER 跟 HANDLE 的存在。

當一個 process 收到 transaction 的時候,如果 binder driver 發現 process 沒有任何 thread 已經準備好等著要處理,等到下次有 thread 來處理時,它會收到 BR_SPAWN_LOOPER 的訊息,userspace 也會多產生一個 thread 專門對 /dev/binder 做 I/O,確保下次有 transaction 的時候,有比較高的機會是已經有 thread 準備好了。提個數字,在 android sdk 1.5 preview 的 emulator 剛開好機時,在我的機器上,Phone App 會有 11 個 thread,而其中有 6 個 thread 是跑在同一個迴圈裡等著對 /dev/binder 做 I/O。這項機制相信是為了縮短應用程式的反應時間。

Binder 再探討下去還有許多東西可以討論,例如 android native library 沒有 aidl 可以用,它如何建立操作 binder 的物件?或者 native code 裡面常常看到的 sp<>、wp<> 跟 binder 的 reference counting 關係為何?對 binder 的功能有初步認識後,再去理解這些實作會顯得直覺許多。

Thursday, April 9, 2009

Android with cursor

一般 PC 上面沒有 touchscreen 只有滑鼠,為了方便做事,晚上試著幫 Android 加上 cursor:



暫時是做在 WindowManagerService 裡面,髒髒的 patch 可以在這邊取得。目前 drag 有點問題,而且是 software cursor,歡迎有興趣的網友幫忙改進 :)

Monday, April 6, 2009

一千零一夜之 DRI2 Overview

這個系列是讀書筆記,作者可能沒有跟主題有關的開發經驗。

Linux 的圖形加速一直是個問題,不過這個問題在最近幾版的 kernel 隱約可以看到一些希望。從 2.6.28 的 GEM memory manager 到 2.6.29 的 kernel modesetting,由 Intel 與 RedHat 的開發者所提出的加速架構已經進入 kernel,雖然目前採用這些架構的只有 Intel 自身的驅動程式,但其它繪圖晶片的 open source 驅動程式勢必也得跟隨此架構。在 Intel 與 AMD 相繼釋出 datasheet 後,或許有天所有的使用者都可以享用這些成果,不再依賴私有的驅動程式。

去年年中 GEM 公佈後,許多架構迅速翻新,DRI2 最後也環繞著 GEM 在設計。DRI (Direct Rendering Infrastructure) 是各家 open source 驅動程式所依循的架構,雖然處處都可以看見這個名詞,但是要了解它背後的實作卻不太容易。困難的地方在於 DRI 的組成份子很多,而且大部份來頭都不小。以 Intel 的 945GM 為例,在 kernel 有 drm.ko 與 i915.ko 兩個模組,對映到 userspace,有 drm 提供的libdrm.so 與 libdrm_intel.so;在 xserver 有 libglx.so 與 libdri2.so 這兩個 extension,以及 xf86-video-intel 提供的 intel_drv.so;最後在 mesa 也有 libGL.so 以及 i915_dri.so。習慣上,intel_drv.so 被稱作 2D driver 或 DDX driver,i915_dri.so 則是 3D driver 或 DRI driver。從一個 3D 程式被執行時到顯示在螢幕上,參與這個看似普通的過程背後就是上面這些檔案。

DRI 最重要的內涵在於,一個 X client 可以向 X server 取得視窗的 buffer(s),並利用硬體加速在上面直接繪圖,不再透過 X server。Kernel 在 DRI 扮演的主要角色是讓多個 process 可以共享 (GEM) buffer,還有接受與執行 process 傳來的硬體指令。這邊困難的地方之一在於 CPU 跟 GPU 都有各自的 MMU 與 cache,所以有許多同步的問題要處理。

libGL.so 在 DRI 架構裡扮演了 loader 的角色,它跑在 client 這邊。就像 OpenGL 與平台無關一樣,DRI driver 也與平台無關。loader 一方面載入 DRI driver,一方面又提供適當的 hook 讓 DRI driver 呼叫,並實作了平台相關的部份。所以在 DRI driver 真的要用到 buffer 的時候,它會呼叫 loader 的 getBuffers 函數取得視窗的 buffer 與大小。libglx.so 跟 libdri2.so 實作了 GLX 與 DRI2 這兩個 X extensions。在 X 的平台上,對於 DRI driver 的要求,libGL.so 會利用 GLX 與 DRI2 協定取得所需的資訊,並傳回給 DRI driver。

一個快速驗證上面說法的方法是直接檢視系統的 /usr/lib/libGL.so 跟 /usr/lib/dri/i915_dri.so。libGL.so 只是 loader,檔案大小應該要比 i915_dri.so 小不少;而 i915_dri.so 與平台無關,所以 ldd 應該不會看到 X11 的函式庫。如果再進一步想,那是不是只要提供適當的 loader,DRI driver 也能用在 X 以外的平台?答案是肯定的。

在 compiz 剛出來的時候,有很多名詞跟著它一起冒出來。像是 XGL、GLX_EXT_texture_from_pixmap、還有 AIGLX 等。在 libGL.so 無法成功跟 X server 溝通使用 DRI 時,client 的 3D 繪圖指令會改經由 GLX 傳給 server 處理,這被稱作 indirect rendering。在早先,libglx.so 會利用純軟體的方式執行這些 3D 指令,所以 indirect rendering 跟 software rendering 是等價的。這個情形在 AIGLX 引入後有很大的改變。在 AIGLX 下,libglx.so 除了提供 GLX extension 實作外,它同時也是一個 DRI loader,可以利用 DRI driver 進行 3D 加速。因為 AIGLX,indirect rendering 不再直接被視為慢、無法接受,雖然它還是多了 client 到 server 的 overhead。AIGLX 即是 Accelerated Indirect GLX 的縮寫,非常直接。

因為我直接從 DRI2 開始看,無法比較它跟 DRI1 的差異。在 DRI2 作者 Kristian Høgsberg 的 blog 可以找到一些關鍵字。Kristian Høgsberg 同時也是之前文章提到的 Wayland 的作者。

Monday, March 23, 2009

Android on x86

因為之後的需要,這兩天開始看起 android,尤其是在 x86 的情境。不過手邊沒有硬體可以用,打算先用模擬器測試。可能是 cupcake 在我動工的前兩天 merge 到 master 時的部份變更,雖然很快就編好 image,可以進到 console,但是 UI 起不來。又多花了一天時間才順利完工。



在 android 的 Get source 頁面有編譯 ARM 版本的方式。關於 android on x86,最重要的參考資料則是 這個討論串,利用 google 也可以找到更詳細的步驟。前面提到的 cupcake merge 同時造成了 opencore 的編譯問題,解法一樣要去看最開頭提到的 merge 到 master 討論串。不過我在 Debian testing (AMD64) 上做的時候,還需要先找到 build/core/combo/linux-x86.mk 的這兩行
$(combo_target)GLOBAL_CFLAGS := $($(combo_target)GLOBAL_CFLAGS) -m32
$(combo_target)GLOBAL_LDFLAGS := $($(combo_target)GLOBAL_LDFLAGS) -m32

把 -m32 參數拿掉,然後再多安裝 {gcc,g++}-multilib 套件。前者是說不要把 host binary 預設編成 32bit,後者則是讓某些在 Android.mk 又加上 -m32 的模組可以編譯成功。

一開始我是用 VirtualBox 2.1.4,不過因為 UI 起不來,想要用 adb 來除錯,所以後來改用 QEMU 0.10.0 (x86 版),並利用它的 -redir 功能讓 host 可以 adb 進去模擬出來的機器。給 QEMU 用的 kernel 有幾件事要注意一下:為方便起見,最好把
CONFIG_NE2K_PCI=y
CONFIG_FB_VESA=y
CONFIG_FRAMEBUFFER_CONSOLE=y

直接編進 kernel,並設定 vga=xxx。如果 kernel 跑到一半停住,並且在倒數幾行出現
MP-BIOS bug: 8254 timer not connected to IO-APIC

訊息,可以在 cmdline 加上 noapic 參數解決。

前面一直提到的 UI 問題可以參考這個 dirty patch 做修改。這樣應該就完成了。

Sunday, March 1, 2009

編譯 Wayland

Wayland 是一個極精簡的 display server。它是由 Kristian Høgsberg 在工作之餘所進行的實驗性計畫。與 X server 不同,Wayland client 要負責所有的繪圖動作,server 只處理最後的合成與顯示。這邊的繪圖動作還包含視窗邊框,這在 X 的世界裡是由 window manager 完成。

client 必須把它想顯示的畫面畫在 GEM buffer 上。GEM buffer 是 kernel 在管理的資源,它在不同的時間點可能會存在於不同的地方,例如它可能在系統記憶體或者是顯示卡的記憶體上。client 想要繪圖時可以利用 mmap 取得 GEM buffer 的指標,直接對它進行操作。client 與 server 的資料交換,圖形以外的部份是透過 unix socket;圖形部份因為使用 GEM buffer,server 可以直接取得。server 最後再透過 compositor 把 client 的畫面合成並顯示到螢幕上。

Wayland 還在早期的階段,這邊提到的編譯方法可能很快就不適用 (或被簡化)。編譯 Wayland 之所以困難,主要原因在於它用到的 git repository 不好找。先讓我們找到 DRM 並且安裝起來
$ git clone git://anongit.freedesktop.org/git/mesa/drm
$ cd drm
$ ./autogen.sh --prefix=/opt/gfx
$ make
$ make install
$ export PKG_CONFIG_PATH=/opt/gfx/lib/pkgconfig
$ export LD_LIBRARY_PATH=/opt/gfx/lib


再來安裝 Mesa 跟 DRI driver (這邊選用 i915)
$ git clone git://anongit.freedesktop.org/git/mesa/mesa
$ cd mesa
$ git remote add krh git://people.freedesktop.org/~krh/mesa
$ git fetch krh
$ git checkout -b eagle krh/eagle
$ ./autogen.sh --prefix=/opt/gfx --with-dri-drivers=i915 \
--disable-gallium --disable-glw --disable-glut --disable-glu \
--without-demos
$ make
$ make install


Wayland 會用到 udev 136 之後提供的 libudev,雖然這邊只安裝 libudev,但要注意它可能會覆蓋系統上本來的檔案
$ git clone git://git.kernel.org/pub/scm/linux/hotplug/udev.git
$ cd udev
$ ./autogen.sh
$ cd udev/lib
$ make
$ make install


Wayland 目前的 compositor 是採用 eagle 這個 EGL 實作
$ git clone git://anongit.freedesktop.org/~krh/eagle
$ cd eagle
$ autoreconf -vif
$ ./configure --prefix=/opt/gfx
$ make
$ make install
$ export EAGLE_DRIVER_PATH=/opt/gfx/lib/dri


大部份的 example client 用到了 cairo-drm,這是一個直接以 GEM buffer 為 surface backend 的 cairo 分支
$ git clone git://anongit.freedesktop.org/git/cairo
$ cd cairo
$ git remote add ickle git://anongit.freedesktop.org/~ickle/cairo
$ git fetch ickle
$ git checkout -b drm ickle/drm
$ ./autogen.sh --prefix=/opt/gfx --disable-xlib
$ make
$ make install


最後,Wayland
$ git clone git://people.freedesktop.org/~krh/wayland.git
$ cd wayland
$ autoreconf -vif
$ ./configure --prefix=/opt/gfx
$ make
$ make install


Wayland 需要 kernel modesetting 的支援 (linux kernel >= 2.6.29)。再克服這一關,一個晚上折騰的結果

Friday, February 27, 2009

objdump 反組譯

objdump 反組譯是一個很實用的功能,但有時候我們拿到的不是執行檔,而是 binary image 或者是自行從記憶體 dump 下來的內容。在這種情況,要手動給 objdump 足夠的資訊,它才能完成它的工作。事實上,它需要的只是簡單的:
$ arm-angstrom-linux-gnueabi-objdump -b binary -m arm -D bootrom.bin

或者也可以先利用 objcopy 將 binary image 轉成 ELF 格式:
$ arm-angstrom-linux-gnueabi-objcopy -I binary -O elf32-littlearm -B arm --rename-section .data=.text bootrom.bin bootrom.elf

這樣也可以照本來的習慣去使用 objdump。

Friday, January 30, 2009

一千零一夜之 Console I/O

這個系列是讀書筆記,作者可能沒有跟主題有關的開發經驗。

當一個程式被執行時,它的 stdin/stdout/stderr 會從父程序繼承。如果沒有特別指定,它們通常會指向同一個諸如 /dev/ttyX 或 /dev/pts/X 的 TTY device。很難不去好奇,當我們 getchar() 或 printf() 時,對一個 TTY device 的 I/O,在 kernel 裡頭會發生什麼事?



口語裡的 console,指的通常是圖中 VT。VT 是 virtual terminal 的縮寫,對 userspace 來說,它就像是一部真實存在著的終端機;在 kernel 裡頭,VT(的功能之一)是一個 TTY driver,它是所有 /dev/ttyX 裝置的驅動程式。對 TTY device 的 I/O 動作,會經由 TTY core 送給 TTY driver。資料往來於 TTY 與 TTY driver 的過程中,或者說圖中連結 TTY 與 VT 的線,被稱做 TTY ldisc,TTY line discipline。

之前的文章中我們看到鍵盤向 TTY 送出的資料。來自於鍵盤的輸入,可能是特別的按鍵組合,目的並不是讓 userspace 去取得這些輸入。例如,我們會敲下 Ctrl-C 來中斷目前的程式;Ctrl-Z 來暫停目前的程式。又或者,我們不希望輸入的密碼會出現在螢幕上。這些需求都在 line discipline 中被滿足。因為所有的 I/O 都會經由 line discipline,於是它可以在使用者輸入特定字元時,對目前的程式送出 SIGINT 或 SIGTSTP;它也可以選擇是否要把使用者的輸入再 echo 回 VT。從這邊也可以看到,line discipline 不只可以對資料做緩衝與處理,它還被允計針對特定的輸入做特定的行為。當我們從 VT 換到實體的 COM port 時,且當與 COM port 連結的裝置不是終端機,而是數據機時,line discipline 甚至可以拒絕所有來自 userspace 的 I/O,轉而從 PPP network device 與 userspace 做資料交換。實際上,TTY device 只是一個 transport layer,可以跟它連結的裝置千變萬化。line discipline 於是也被設計成可以抽換;當從終端機換成數據機時,line discipline 也從 N_TTY 換成 N_PPP。

跟來自鍵盤的輸入一樣,使用者對 TTY device 寫入的資料,會經過 line discipline,VT,再進入螢幕。在 drivers/char/n_tty.c 的 do_output_char 函數中
switch (c) {
...
case '\r':
if (O_ONOCR(tty) && tty->column == 0)
return 0;
if (O_OCRNL(tty)) {
c = '\n';
if (O_ONLRET(tty))
tty->canon_column = tty->column = 0;
break;
}
tty->canon_column = tty->column = 0;
break;
case '\t':
spaces = 8 - (tty->column & 7);
if (O_TABDLY(tty) == XTABS) {
if (space <>column += spaces;
tty->ops->write(tty, " ", spaces);
return spaces;
}
tty->column += spaces;
break;
...
}

可以看到 line discipline 會對特殊字元,像是換行符號或 tab 等做排版。更多關於 N_TTY 這個 line discipline 的功能可以參考 stty(1)。

VT 的架構與實體的 terminal 與 host computer 架構是對應的。要把現在手上正在上網的電腦想像成它是 terminal 與 host computer 的組成可能不是那麼容易。一個相對常見、具體的情境,是用一條線把手上的電腦與 wifi router 或手機的 UART console 連結。上圖中的 Computer B 可以理解成手上的電腦,Computer A 則是跑著 linux 的嵌入式系統。

Tuesday, January 27, 2009

一千零一夜之 keyboard in console

這個系列是讀書筆記,作者可能沒有跟主題有關的開發經驗。

按下或放開按鍵的時候,鍵盤會產生一段長度不等的 scancode,這段 scancode 會被 keyboard driver (像是 drivers/input/keyboard/atkbd.c) 處理,轉換成類型為 EV_MSC 與 EV_KEY 的 input event。EV_MSC 事件的內容是 scancode 本身,與硬體直接相關且不容易處理,除非有特殊需求,通常我們比較關心的是 EV_KEY input event。想要處理這些事件的其它子系統,可以向 input layer 註冊 input handler。例如 evdev 會註冊一個 input handler 讓 userspace 可以從 /dev/input/eventX 直接取得鍵盤事件。kernel 的 console driver 也會向 input layer 註冊一個 handler 來取得鍵盤事件,這是我們這次想要比較深入了解的項目。

console 跟鍵盤的交互作用可以在 drivers/input/char/keyboard.c 看到。在 console 底下,鍵盤有四種模式,

#define VC_XLATE 0 /* translate keycodes using keymap */
#define VC_MEDIUMRAW 1 /* medium raw (keycode) mode */
#define VC_RAW 2 /* raw (scancode) mode */
#define VC_UNICODE 3 /* Unicode mode */

可以利用 kbd_mode 命令來切換。前面有提到 keyboard driver 會送出 EV_MSC 與 EV_KEY 兩種 input event,其中前者送出的 scancode 被用來實作 VC_RAW 模式;後者送出的 keycode,則可以用來實作 VC_MEDIUMRAW 模式。剩下的兩種模式都是 keycode 的再加工。

scancode 到 keycode 的轉換透過了 keyboard driver 定義的 keycode table 完成。在 atkbd.c 裡,如果忽略掉其它細節,它只是簡單的

keycode = atkbd->keycode[code];

一行程式碼。有些多媒體鍵盤會有一些按鍵的 scancode 不在預設的 keycode table 裡,當這些按鍵被按下或放開的時候,kernel 會印出類似

atkbd.c: Unknown key pressed (translated set 2, code 0x1e on isa0060/serio0).
atkbd.c: Use 'setkeycodes 1e <keycode>' to make it known.

的訊息。依照這段訊息提示的方法去做

# setkeycodes <scancode> <keycode>

就可以新增一筆對映的資料。setkeycodes 與 getkeycodes 命令即是用來設定或取得目前的 keycode table。值得注意的是,大部份的 keycode 有對應的 symbolic name,例如 keycode 30 的 symbolic name 是 KEY_A,keycode 48 的是 KEY_B,keycode 46 的是 KEY_C 等 (見 /usr/include/linux/input.h)。這些 symbolic name 是照美式鍵盤定義而來,拿到其它語言的鍵盤上並沒有意義。

console 下 keyboard 預設的模式是 VC_XLATE 或 VC_UNICODE。從 input layer 拿到 keycode 後,console 會呼叫 kbd_event 函數做處理。在這兩個模式下,keycode 會被轉換成 keysym

key_map = key_maps[shift_final];
...
keysym = key_map[keycode];

然後 queue 到 console buffer。stdin 接到 console 的程式即是由這邊取得使用者的輸入。這個動作會經過 line discipline,不過我們這邊不去討論。

一個 keysym 是兩個 byte,分別可以由 KTYP(keysym) 跟 KVAL(keysym) 取得它的 type 與 value。當 type 小於 0xf0 時,整個 keysym 代表一個 UCS2 的 codepoint,keysym 會被轉成 UTF-8 放進 console buffer;當 type >= 0xf0 時,與 type 相關的 handler 被呼叫,其 prototype 為

typedef void (k_handler_fn)(struct vc_data *vc, unsigned char value,
char up_flag);

其中 value 就是剛剛看到的 KVAL(keysym)。有些 handler 會把處理過的 value 寫入 console buffer,有些則不會,這樣設計的目的以例子來看最容易理解。我們知道 shift 按著的時候,所有的字母大小寫會互換。當 shift 的按下時,鍵盤控制器送出中斷,keyboard driver 收到後把 scancode 讀出並轉成 keycode,送出 input event。console 收到 event 後利用 keymap 把 keycode 轉成 keysym。shift 的 keysym type 是 0xf7,value 是 0x00,送到對應的 handler 中

if (up_flag) {
if (shift_down[value])
shift_down[value]--;
} else
shift_down[value]++;

if (shift_down[value])
shift_state |= (1 << value);
else
shift_state &= ~(1 << value);

可以發現 shift_state 的第一個 bit 被設為 1。shift_state 是 keyboard.c 裡的 file-scope 變數,當任何一個按鍵再被按下時,kbd_event 裡這一段程式碼

shift_final = (shift_state | kbd->slockstate) ^ kbd->lockstate;
key_map = key_maps[shift_final];

選取了不同的 keymap,導致或許不一樣的 keysym 被送出,本來是小寫的英文字母也因而變成大寫。

在 kernel 內部實作,console 使用的是 UCS2 編碼。不管是使用者列印到 console 的字串,或者是 keysym 的 value 都會先重新編碼成 UCS2 再做進一步處理。在 VC_UNICODE 模式底下,UCS2 會以 UTF-8 形式被送進 console buffer;在 VC_XLATE 模式,則會嘗試再編碼回原來的 value 送到 console buffer。

相對於 keymap 複雜的機制,它的操作卻非常簡單。在 userspace 可以利用 loadkeys 與 dumpkeys 命令來操作 keymaps

# loadkeys de # 使用德式鍵盤 layout
# loadkeys us # 使用美式鍵盤 layout

進一步的資訊可以參考 keymaps(5)。

這篇文章我們看到了鍵盤在 console 的運作。如果換到 X(org) server,情形有點不同,而且也因驅動程式而異。例如 xf86-input-keyboard 雖然是從 console 取得鍵盤事件,但它會把 console 設為 VC_RAW 模式,自行處理 scancode。而 xf86-input-evdev 則是直接從 /dev/input/eventX 取得鍵盤事件。以後有機會也可以看看 X(org) 下的鍵盤運作。