Autor: | Wojciech Muła |
---|---|
Dodany: | 31.08.2001 |
Aktualizacja: | 2.06.2008 |
Obecnie większość bibliotek graficznych wewnętrznie używa obrazów 32bpp (BPP — Bits Per Pixel), dopiero podczas wyświetlania są one konwertowane do odpowiedniego formatu, zależnego od sprzętu (trybu karty graficznej).
Poniżej przedstawiono formaty rozważanych pikseli.
Oto struktury opisujące piksele w języku C:
typedef unsigned char byte; // 8 bitów typedef unsigned short int word; // 16 bitów typedef unsigned int dword; // 16 bitów typedef struct { word b: 5; word g: 5; word r: 5; word _: 1; // nieużywany } pixel15bpp; typedef struct { word b: 5; word g: 6; word r: 5; } pixel16bpp; typedef struct { byte b,g,r; } pixel24bpp; typedef struct { byte b,g,r; byte _; // nieużywany } pixel24bpp;
Nadmienię tylko, że piksele 24bpp nie są stosowane — po prostu nie sprawdziły się. Oszczędność jednego bajtu nie rekompensowała skomplikowania kodu i opóźnień związanych z brakiem wyrównania.
Jest to chyba jedna z najlepszych metod do implementacji na x86. Polega na prekalkulowaniu wartości pikseli. Bynajmniej nie tworzone są tablice dla wszystkich pikseli — proszę wyobrazić sobie rozmiar takiej tablicy dla 24bpp — całe 16MB i te problemy z cache.
Piksel źródłowy jest dzielony na bajty, tak więc dla pikseli 24/32bpp potrzebne będą trzy, a dla pikseli 15/16bpp dwie tablice 256-elementowe.
Ograniczę się tylko do konwersji 16bpp/32bpp, inne konwersje Czytelnik z łatwością sam napisze.
dword pix16to32bpp_lookup[2][256]; // rozmiar 256*4*2 = 2kB word pix32to16bpp_lookup[3][256]; // rozmiar 256*2*3 = 1.5kB void fill_pix16to32bpp() { for (int i=0; i<256; i++) { // |gggbbbbb| -- młodszy bajt piksela 16bpp // |00000000|000ggg00|bbbbb000| -- rozkład bitów w pikselu 32bpp dword blu = i & 0x1f; dword grn1 = i & 0x1e; pix16to32bpp_lookup[0][i] = (blu << 3) | (grn1 << 5); // |rrrrrggg| -- starszy bajt piksela 16bpp // |rrrrr000|ggg00000|00000000| dword red = i & 0xf8; dword grn2 = i & 0x07; pix16to32bpp_lookup[1][i] = (red << 16) | (grn2 << 13); } } void fill_pix32to16bpp() { for (int i=0; i<256; i++) { word blu = i >> 3; word grn = i >> 2; word red = i >> 3; pix32to16bpp_lookup[0][i] = blu; pix32to16bpp_lookup[1][i] = grn << 5; pix32to16bpp_lookup[2][i] = red << 11; } } pixel32bpp pix16to32bpp(pixel16bpp SRC) { word src = *(word*)&SRC; dword res = pix16to32bpp_lookup[0][src & 0xff] | pix16to32bpp_lookup[1][src >> 8]; return *(pixel32bpp*)&res; } pixel16bpp pix32to316pp(pixel32bpp SRC) { dword src = *(dword)&SRC; word res = pix32to16bpp_lookup[0][(src >> 0) & 0xff] | pix32to16bpp_lookup[1][(src >> 8) & 0xff] | pix32to16bpp_lookup[2][(src >> 16) & 0xff]; return *(pixel16bpp*)&res; }
Podczas takich konwersji część bitów piksela źródłowego jest tracona, są to najmłodsze bity każdej ze składowych; oto przykład w C.
pixel16bpp convert32_to_16bpp(pixel32bpp src) { pixel16bpp dst; dst.r = src.r >> 3; // 8-5 dst.g = src.g >> 2; // 8-6 dst.b = src.b >> 3; // 8-5 return p; }
Pokażę funkcję dla konwersji 32bpp na 16bpp, konwersja na 15bpp będzie wymagać nieznacznych zmian w kodzie. Użycie rozkazu pmaddwd wyeliminowało co najmniej dwa przesłania, jedno przesunięcie bitowe i operację logiczną or.
; wto 29 paź 2002 16:43:22 CET segment .data mask_rg dd 0x001f001f, 0x001f001f mask_b dd 0x00001f00, 0x00001f00 shift dd 0x00800001, 0x00800001 ; 1, 1<<11, 1, 1<<11 segment .text ; esi -> wskaźnik na obraz 32bpp conv_32bpp_to_16bpp: movq mm0, [esi] ; mm0 = |0000 0000|rrrr rrrr|gggg gggg|bbbb bbbb|... psrld mm0, 3 ; mm0 = |0000 0000|000r rrrr|rrrg gggg|gggb bbbb|... movq mm1, mm0 pand mm0, [mask_rb] ; mm0 = |0000 0000|000r rrrr|0000 0000|000b bbbb|... pand mm1, [mask_g] ; mm1 = |0000 0000|0000 0000|000g gggg|g000 0000|... pmaddwd mm0, [shift] ; mm0 = |0000 0000|000b bbbb| ; |rrrr r000|0000 0000| + psrld mm1, 2 ; mm1 = |0000 0000|0000 0000|0000 0ggg|ggg0 0000|... por mm0, mm1 ; mm0 = |0000 0000|0000 0000|rrrr rggg|gggb bbbb|... ; mm0 = |pix1| 0 |pix0| 0 | ret
; wejście: eax - piksel 32bpp ; wyjście: ax - piksel 15bpp ; eax = |xxxx xxxx|rrrr rrrr|gggg gggg|bbbb bbbb| and eax, 00f8f8f8 ; eax = |0000 0000|rrrr r000|gggg g000|bbbb b000| shr eax, 3 ; eax = |0000 0000|000r rrrr|000g gggg|000b bbbb| shl al , 3 ; eax = |0000 0000|000r rrrr|000g gggg|bbbb b000| shl ax , 3 ; eax = |0000 0000|000r rrrr|gggg gbbb|bb00 0000| shr eax, 6 ; eax = |0000 0000|0000 0000|0rrr rrgg|gggb bbbb|
; wejście: eax - piksel 32bpp ; wyjście: ax - piksel 16bpp ; eax = |xxxx xxxx|rrrr rrrr|gggg gggg|bbbb bbbb| and eax, 00f8fcf8 ; eax = |xxxx xxxx|rrrr r000|gggg gg00|bbbb b000| shr ah, 2 ; eax = |xxxx xxxx|rrrr r000|00gg gggg|bbbb b000| shr eax, 3 ; eax = |xxxx xxxx|000r rrrr|0000 0ggg|gggb bbbb| shl ax, 5 ; eax = |xxxx xxxx|000r rrrr|gggg ggbb|bbb0 0000| shr eax, 5 ; eax = |xxxx xxxx|0000 0000|grrr rggg|gggb bbbb|
; wejście: ax - piksel 16bpp ; wyjście: ax - piksel 15bpp ; niszczy: bl ; ax = |rrrr rggg|gggb bbbb| mov bl, al and bl, 00011111b and al, 11011111b ; ax = |rrrr rggg|gg0b bbbb| add al, bl ; ax = |rrrr rggg|ggbb bbb0| shr ax, 1
Podczas konwersji brakuje części najmłodszych bitów, są one wypełniane zerami; oto przykład w C.
pixel32bpp convert(pixel15bpp src) { pixel32bpp dst = {src.r << 3, src.g << 3, src.b << 3, 0}; return dst; }
; wejście: ax - piksel 15bpp ; wyjście: eax - piksel 32bpp ; eax = |xxxx xxxx|xxxx xxxx|0rrr rrgg|gggb bbbb| shr eax, 6 ; eax = |xxxx xxxx|xx0r rrrr|gggg gbbb|bb00 0000| shr ax, 3 ; eax = |xxxx xxxx|xx0r rrrr|000g gggg|bbbb b000| shr al, 3 ; eax = |xxxx xxxx|xx0r rrrr|000g gggg|000b bbbb| shl eax, 3 ; eax = |xxxx xxxx|rrrr r000|gg0g g000|bbbb b000|
segment .data mask_rb dw 0b0111110000011111, 0x7c1f, 0x7c1f, 0x7c1f mask_g dw 0b0000001111100000, 0x03e0, 0x03e0, 0x03e0 shifts dw 0b0000001000001000, 0x0208, 0x0208, 0x0208 ; 1<<9 | 1<<3 mask_rb32bpp dd 0x00f800f8, 0x00f800f8 segment .text ; wejście: ; esi -> wskaźnik na 2 piksele 15bpp ; wyjście: ; mm0 = 2 piksele 32bpp ; niszczy: ; mm1, mm2, mm3, mm4 conv_15bpp_to_32bpp: pxor mm1, mm1 movq mm0, [esi] ; mm0 = | 0 | 0 |pix1|pix0| punpcklwd mm0, mm1 ; mm0 = | 0 |pix1| 0 |pix0| movq mm1, mm0 pand mm0, [mask_rb] ; mm0 = |00000000 00000000|0rrrrr00 000bbbbb|.. pand mm1, [mask_g] ; mm1 = |00000000 00000000|000000gg ggg00000|.. pmaddwd mm0, [shift] ; mm0 = |00000000 000000rr|rrr00000 bbbbb000| + ; |00000000 rrrrr000|00bbbbb0 00000000| = ; |00000000 rrrrrxxx|xxxxxxx0 bbbbb000|.. pslld mm1, 6 ; mm1 = |00000000 00000000|ggggg000 00000000| pand mm0, [mask_rb32] ; mm0 = |00000000 rrrrr000|00000000 bbbbb000| por mm0, mm1 ret
Zamiast „suchego” kodu w C pokażę praktyczną realizację tablicowania w asemblerze.
W jeden pętli przetwarzane będą po dwa piksele, przede wszystkim po to by zminimalizować liczbę odwołań do pamięci.
segment .data lookup_lo times 256 dd 0 ; tablice są wypełniane "gdzieś tam" lookup_hi times 256 dd 0 ; esi -> wskaźnik na 2 wejściowe piksele 16bpp ; edi -> wskaźnik na 2 wyjściowe piksele 32bpp conv_16bpp_to_32bpp: mov eax, [esi] ; ładowane są 2 piksele movzx ecx, al ; młodsze movzx edx, ah ; i starsze słowo pierwszego piksela bswap eax movzx ebx, ah, ; młodsze and eax, 0xff ; i starsze słowo drugiego piksela mov ebx, [lookup_lo + ebx*4] mov ecx, [lookup_lo + ecx*4] or ebx, [lookup_hi + edx*4] or ecx, [lookup_hi + eax*4] mov [edi+0], ebx mov [edi+8], ecx ret
; wejście: ax - piksel 16bpp ; wyjście: eax - piksel 32bpp ; eax = |xxxx xxxx|xxxx xxxx|rrrr rggg|gggb bbbb| shl eax, 5 ; eax = |xxxx xxxx|xxxr rrrr|gggg ggbb|bbb0 0000| shr ax, 5 ; eax = |xxxx xxxx|xxxr rrrr|0000 0ggg|gggb bbbb| shr eax, 3 ; eax = |xxxx xxxx|rrrr r000|00gg gggg|bbbb b000| shl ah, 2 ; eax = |xxxx xxxx|rrrr r000|gggg gg00|bbbb b000|
; czw 24 paź 2002 18:59:18 CEST segment .data mask_r db 0b1111100000000000, 0xf800, 0xf800, 0xf800 mask_g db 0b0000011111100000, 0x07e0, 0x07e0, 0x07e0 mask_b db 0b0000000000011111, 0x0x1e, 0x0x1e, 0x0x1e segment .text ; esi -> wskaźnik do obrazu 16bpp conv_16bpp_to_32bpp: movq mm0, [esi] ; mm0 = |pix3|pix2|pix1|pix0| movq mm1, mm0 movq mm2, mm0 pand mm0, [mask_r] ; mm0 = |rrrrr000 00000000|... pand mm1, [mask_g] ; mm1 = |00000ggg ggg00000|... pand mm2, [mask_b] ; mm2 = |00000000 000bbbbb|... psrlw mm0, 8 ; mm0 = |00000000 rrrrr000|... psllw mm1, 5 ; mm1 = |gggggg00 00000000|... psllw mm2, 3 ; mm2 = |00000000 bbbbb000|... por mm1, mm2 ; mm1 = |gggggg00 bbbbb000|... movq mm2, mm1 punpcklwd mm1, mm0 ; mm1 = |00000000 rrrrr000|gggggg00 bbbbb000|... punpckhwd mm2, mm0 ; mm2 = |00000000 rrrrr000|gggggg00 bbbbb000|... ret
Po optymalizacji.
; czw 24 paź 2002 19:19:35 CEST ; pią 25 paź 2002 17:41:44 CEST segment .data mask_r dw 0b0000000011111000, 0x00f8, 0x00f8, 0x00f8 mask_b dw 0b0000011111100000, 0x07e0, 0x07e0, 0x07e0 mask_g dw 0b0000000011111000, 0x00f8, 0x00f8, 0x00f8 segment .text conv_16bpp_to_32bpp: movq mm0, [esi] ; 1 movq mm3, [edi] ; - movq mm1, mm0 ; 2 movq mm2, mm0 ; - movq mm4, mm3 ; 3 movq mm5, mm3 ; - psrlw mm0, 8 ; 4 pand mm2, [mask_b] ; - psllw mm1, 5 ; 5 pand mm0, [mask_r] ; - psllw mm2, 3 ; 6 pand mm1, [mask_g] ; - psrlw mm3, 8 ; 7 por mm1, mm2 ; - psllw mm4, 5 ; 8 pand mm3, [mask_r] ; - psllw mm5, 3 ; 9 movq mm2, mm1 ; - punpcklwd mm1, mm0 ; 10 pand mm4, [mask_g] ; - punpckhwd mm2, mm0 ; 11 pand mm5, [mask_b] ; - por mm4, mm5 ; 13 movq mm5, mm4 ; 14 punpcklwd mm4, mm3 ; 15 punpckhwd mm5, mm3 ; 16 -- 16 cykli/8 pikseli (2 cykle/piksel) ret
Kod jest łatwy do przerobienia na MMX/SSE.
; wejście: ax - piksel 15bpp ; wyjście: ax - piksel 16bpp ; niszczy: bx ; ax = |0rrr rrgg|gggb bbbb| mov bx, ax and ax, 7fe0h ; ax = |0rrr rrgg|ggg0 0000| and bx, 001fh ; bx = |0000 0000|000b bbbb| lea eax, [eax*2 + ebx]
Ta wersja jest o wiele szybsza niż przedstawiony wyżej algorytm; kod jest równie łatwy do przerobienia na MMX/SSE.
; wejście: ax - piksel 15bpp ; wyjście: ax - piksel 16bpp ; niszczy: bx ; ax = |0rrr rrgg|gggb bbbb| mov bx, ax and ax, 7fe0h ; ax = |0rrr rrgg|ggg0 0000| add ax, bx ; bx = |rrrr rggg|gg0b bbbb|
Zobacz w osobnym artykule