Csatorna-swizzle

Csatorna-swizzle

Tanulófeladat

Bevezetés

SIMD: sávok közötti műveletek

Minden eddigi SIMD-művelet sávonként működött. Az eredmény i sávjában lévő érték a bemenetek i sávjából számítódik ki.

Sok olyan helyzet van azonban, amikor szükséges vagy kívánatos az értékek mozgatása a sávok között. Előfordulhat például, hogy a sávok nincsenek a szükséges számításhoz megfelelő sorrendben.

Számos olyan SIMD-utasítás létezik, amely különböző módon mozgat adatot a sávok között.

Keverés

Egy keverés átrendezi egy regiszter bájtjait vagy sávjait, és minden cél sávot egy tetszőleges helyen lévő forrás sávból vesz. Ugyanazt a sávot több különböző célhoz is kiválaszthatják, így értékek szórására is képesek.

A keverő utasítások méretüktől és végrehajtási tartományuktól függően eltérően viselkednek.

Sávok kiválasztása azonnali értékkel

A pshufd utasítás átrendezi a forrás négy 32 bites sávját a célba.

A kiválasztás egy 8 bites azonnali érték, amelyet négy 2 bites mezőként olvasunk be, mindegyiket egy cél sávhoz. Minden mező kijelöli, hogy a négy forrás sáv közül melyiket másoljuk az adott cél sávba:

mező sáv indexe
00 0
01 1
10 2
11 3

A mező helye az azonnali értékben, jobbról balra olvasva, megmutatja, hová kerül a kiválasztott sáv. Egy forrás sáv többször is kiválasztható, így egyetlen sáv szórható ki az egész regiszterbe:

; The bit fields are read right-to-left
pshufd xmm1, xmm0, 0b00_01_10_11 ; reverse: lane 0 takes source 0b11 (3), lane 1 takes source 0b10 (2), and so on
pshufd xmm3, xmm2, 0b00_00_00_00 ; broadcast source lane 0 into all four lanes
Lebegőpontos sávok keverése

Két lebegőpontos keverő utasítás létezik, amelyek ugyanazt az általános szintaxist követik: shuf + p + a méret utótagja (vagy s, vagy d).

Ezek is azonnali értékkel választják ki a sávokat. A shufps ugyanazt a 2 bites mezőkódolást használja, mint a korábbi egész típusú keverések. Mivel azonban egy 128 bites operandusban csak 2 darab 64 bites sáv van, a shufpd csak 1 bites mezőkódolást használ.

Ezek az utasítások abban különböznek egész típusú megfelelőiktől, hogy a sávokat két operandusból veszik, nem egyből:

  • Az eredmény alsó fele a céloperandusból származik.
  • A felső fele a forrásoperandusból származik.
shufps xmm0, xmm1, 0b11_10_01_00 ; xmm0 = {xmm0[0], xmm0[1], xmm1[2], xmm1[3]}
shufps xmm2, xmm3, 0b00_00_00_00 ; xmm2 = {xmm2[0], xmm2[0], xmm3[0], xmm3[0]}
shufpd xmm4, xmm5, 0b0_1         ; xmm4 = {xmm4[1], xmm5[0]}

Mivel mindkét operandus táplálja az eredményt, egy lépésben összefűzhetünk velük két vektort. Ha a forrás és a cél ugyanaz a regiszter, minden sáv abból származik.

A bájtok keverése

A pshufb a legáltalánosabb keverés. Bár a pshufb és a pshufd neve nagyon hasonló, és csak a méret utótagjában tér el, egészen különböző műveleteket végeznek.

Először is, míg a pshufd azonnali értékkel választja ki a sávok pozícióját, a pshufb egy vezérlővektort használ a forrásoperandusban. Ez a vezérlővektor egy xmm regiszter vagy egy 16 bájtos memóriaoperandus.

A 16 cél sáv mindegyikénél a vezérlővektor alsó négy bitje adja egy forrásbájt indexét, 0-tól 15-ig. Ha a vezérlővektor i sávja a j forrásbájt-indexet tartalmazza, akkor a cél j-th sávja az i-th sávba kerül.

Ez rávilágít a két utasítás közötti második különbségre. Míg a pshufd egy másik forrásoperandusból veszi a keverendő sávokat, a pshufb a célban helyben végzi el a keverést.

section .rodata
align 16
reverse: db 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0

section .text
fn:
  pshufb xmm0, [rel reverse]
  ; xmm0[0] = xmm0[reverse[0]] (xmm0[15])
  ; xmm0[1] = xmm0[reverse[1]] (xmm0[14])
  ; ...
  ; in the end, the bytes in xmm0 are reversed

A pshufb ennél is rugalmasabb: bármelyik sávot törölheti. Ha a vezérlővektor i sávjában a legfelső bit be van állítva, akkor a cél i sávja nullázódik. Így a pshufb egyetlen utasításban egyszerre végez tetszőleges bájtpermutációt és szelektív nullázást.

Mivel a pshufb bájt szinten működik, különböző méretű sávok keverésére is használható, ha a szomszédos sávokat csoportosítjuk. Például dwordok keverésére is használható:

section .rodata
align 16
input: dd 13, 25, 37, 49 ; each number takes 4 bytes
reverse_dwords: db 12, 13, 14, 15, 8, 9, 10, 11, 4, 5, 6, 7, 0, 1, 2, 3 ; aligned too, since input spans 16 bytes

section .text
fn:
  movdqa xmm0, [rel input] ; xmm0 = {13, 25, 37, 49}
  pshufb xmm0, [rel reverse_dwords]
  ; xmm0 = {49, 37, 25, 13}

Sávok összefűzése

Az unpack utasítások két operandusból vesznek sávokat, és összefonják őket, felváltva a kettő között. Léteznek egész és lebegőpontos változataik, és nagyrészt ugyanazt az általános szintaxist követik, két eltéréssel:

  1. Van egy l vagy h utótag, amely jelzi, hogy az operandusok alsó (l) vagy felső (h) felén működik-e.
  2. Az egész típusú változatok két méretutótagot kapnak, a második az első kétszeresét jelöli. Például bw vagy qdq (a 16 bájt méretutótagja dq, mint a movdqu esetében).
punpcklwd xmm0, xmm1 ; p + unpck + l + wd
; this interleaves the first 4 words of xmm0 and xmm1 into 4 dwords
; for each dword in the result, the first word is taken from xmm0 and the second, from xmm1
; xmm0 = {xmm0[0], xmm1[0], xmm0[1], xmm1[1], xmm0[2], xmm1[2], xmm0[3], xmm1[3]}

unpckhps xmm2, xmm3 ; unpck + h + p + s
; this interleaves the high 32-bit floats of xmm2 and xmm3
; xmm2 = {xmm2[2], xmm3[2], xmm2[3], xmm3[3]}

Szűkítés telítéssel

A pack utasítások az ellenkező irányba haladnak: két operandus sávjait a cél félszélességű sávjaiba kombinálják.

Ezek az utasítások nem kapnak p előtagot. Azon kívül a szintaxis olyan elemeket kombinál, amelyeket már láttunk:

  • az elvégzett művelet, pack.
  • egy s vagy u annak jelzésére, hogy a kimeneti értékek előjelesek vagy előjel nélküliek.
  • egy s a telítésre, ahogy a korábbi fogalomban látott telítéses aritmetikában.
  • két méretutótag, a második az első felét jelöli (ellentétes viselkedés az unpck-hoz képest).

Mivel a pack művelet szűkítő, nincs szüksége l vagy h utótagra:

packssdw xmm0, xmm1 ; pack + s (for signed) + s (for saturating) + dw (dword to word)
packuswb xmm2, xmm3 ; pack + u (for unsigned) + s (for saturating) + wb (word to byte)

Ezek az utasítások telítő műveletek, azaz a kimeneti értékek a szűkebb tartományba lesznek szorítva. Dwordból wordbe és wordből bájtba működnek. Nincs qwordból dwordbe változat.

Vegyük észre, hogy a bemenet mindig előjelesként értelmeződik. A típus, akár előjeles, akár előjel nélküli, a kimenetre vonatkozik. Ez jelöli a leszorítás tartományát. Például a packsswb egy előjeles bájt tartományára szorítja az értéket, azaz [-128, 127]-re.

Az eredmény alsó sávjai a céloperandusból, a felső sávjai a forrásoperandusból származnak:

packusdw xmm0, xmm1   ; 8 words, each clamped to 0..65535
; xmm0 = {xmm0[0], xmm0[1], xmm0[2], xmm0[3], xmm1[0], xmm1[1], xmm1[2], xmm1[3]}

Ezeknek az utasításoknak nincs lebegőpontos megfelelőjük.

Egyes sávok mozgatása

Eddig mindig a movq/movd utasításokkal mozgattunk adatot SIMD-regiszter és általános célú regiszter között. Ezek az utasítások csak a SIMD-regiszter alsó sávjába írhatnak, illetve abból olvashatnak, és íráskor az összes többi sávot törlik.

Vannak olyan utasítások, amelyek ugyanezt bármelyik sávra elvégzik, nem csak az elsőre, és a többi sávot érintetlenül hagyják:

  • a beszúró utasítások egy elemet írnak egy általános célú regiszterből vagy memóriából egy azonnali értékkel kiválasztott sávba.
  • a kinyerő utasítások egy sávból olvasnak ki egy általános célú regiszterbe.

Mindkettő az egész típusú szintaxist követi: p + insr/extr + a méret utótagja (b, w, d vagy q).

Mindkét esetben az általános célú regiszter általában 32 bites. Csak a pinsrq és a pextrq igényel 64 bites operandust. A memóriaoperandus mérete mindig a művelet méretével egyezik: 8 bites a pinsrb/pextrb, 16 bites a pinsrw/pextrw esetében, és így tovább.

pinsrb xmm0, eax, 5        ; replace byte 5 of xmm0 with the low byte of eax
pextrb byte [rdx], xmm0, 5 ; copy byte 5 of xmm0 into the memory location indicated by rdx

Ne feledjük, hogy ezek az utasítások, akárcsak a movd és a movq, egyszerűen nyers bájtokat mozgatnak. Ez azt jelenti, hogy memóriában vagy általános célú regiszterben tárolt lebegőpontos értékekhez is használhatók.

Utasítások

Egy szoftveres képi feldolgozó pipeline belső ciklusait írod meg, azt a szakaszt, amely előkészíti a textúrákat és egyesíti a rétegeket, mielőtt azok a képernyőre kerülnének. A pipeline egyszerre egy blokkot dolgoz fel, ugyanazt a műveletet alkalmazva a teljes blokkra.

Egy pixel négy 1 bájtos csatornából áll: vörös, zöld, kék és alfa, ebben a sorrendben (RGBA). Egy blokk 4 pixel, azaz összesen 16 bájt.

Öt részfeladat vár rád.

Az operandusokat memóriacímeken keresztül kapod meg, az eredményt pedig egy eredménycímre írod. Ebben a feladatban minden memóriacím 16 bájtos határra igazított.

Note

Az ebben a feladatban végzett számításokat SIMD utasításokkal kell elvégezni, nem skaláris műveletekkel.

1. Kép átalakítása megjelenítési sorrendbe

A textúrák RGBA formában tárolódnak, de a framebuffer, amelybe ez a pipeline rajzol, BGRA sorrendben várja a pixeleket: a vörös és a kék csatorna felcserélve, a zöld és az alfa csatorna a helyén marad. A kép blokkok sorozataként érkezik, és minden blokk ugyanúgy alakul át.

Valósítsd meg a to_display_order függvényt, amely egy teljes képet RGBA-ból BGRA-ba alakít át, egyszerre egy blokkot. A csatornák átrendezését vezérlő maszkot egy csomagolt konstansként érdemes definiálnod a memóriában, és minden blokknál újra felhasználnod.

A függvény argumentumai, ebben a sorrendben:

  • result: egy puffer memóriacíme, ahová az átalakított blokkok kerülnek, blokkonként 16 bájt.
  • pixels: a forrásblokkok memóriacíme, blokkonként 4 pixel, minden pixel 4 bájt RGBA sorrendben.
  • block_count: a blokkok száma, mindig nagyobb, mint 0.
pixels      = {200, 64, 32, 255, 10, 20, 30, 40, 0, 0, 0, 255, 12, 34, 56, 78,
               1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} // 2 blocks
block_count = 2
result      = {32, 64, 200, 255, 30, 20, 10, 40, 0, 0, 0, 255, 56, 34, 12, 78,
               3, 2, 1, 4, 7, 6, 5, 8, 11, 10, 9, 12, 15, 14, 13, 16}

A függvénynek nincs visszatérési értéke.

2. Terület kitöltése egy színnel

Egy terület törléséhez vagy egy egyszínű sáv kifestéséhez a pipeline ugyanazt a színt írja a terület minden pixelére.

Valósítsd meg a fill_region függvényt, amely block_count blokkból álló területet tölt ki egyetlen szín másolataival.

A függvény argumentumai, ebben a sorrendben:

  • result: egy puffer memóriacíme, ahová a kitöltött blokkok kerülnek, blokkonként 16 bájt.
  • color: egy pixel memóriacíme, 4 bájt RGBA sorrendben.
  • block_count: a kitöltendő blokkok száma, mindig nagyobb, mint 0.
color       = {18, 52, 86, 120}
block_count = 2
result      = {18, 52, 86, 120, 18, 52, 86, 120, 18, 52, 86, 120, 18, 52, 86, 120,
               18, 52, 86, 120, 18, 52, 86, 120, 18, 52, 86, 120, 18, 52, 86, 120}

A függvénynek nincs visszatérési értéke.

3. Két scanline összefésülése

Két egycsatornás réteget kell egyetlen pufferbe összefésülni úgy, hogy a mintáik egymásba fonódjanak. Az eredményben felváltva következik egy minta az első rétegből, majd egy a másodikból.

Valósítsd meg a weave_scanlines függvényt, amely két, egyenként 16 mintából álló sort fésül össze egyetlen 32 mintás sorrá.

A függvény argumentumai, ebben a sorrendben:

  • result: egy puffer memóriacíme, ahová a 32 összefésült minta kerül.
  • first: az első sor memóriacíme, 16 minta, mindegyik 8 bites érték.
  • second: a második sor memóriacíme, 16 minta, mindegyik 8 bites érték.
first  = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
second = {100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115}
result = {0, 100, 1, 101, 2, 102, 3, 103, 4, 104, 5, 105, 6, 106, 7, 107,
          8, 108, 9, 109, 10, 110, 11, 111, 12, 112, 13, 113, 14, 114, 15, 115}

A függvénynek nincs visszatérési értéke.

4. Felfényesített sor összecsomagolása

A fényerőt növelő szakasz minden mintát 16 bites munkapontossággal skáláz, így előfordulhat, hogy egy túl fényes minta meghaladja a 255-öt, egy különbség pedig 0 alá csökken. A végső szakasz ezeket a munkapontosságú értékeket 8 bites mintákká szűkíti a megjelenítéshez: a 0 alatti értékeket 0-ra, a 255 felettieket 255-re vágja.

Valósítsd meg a pack_samples függvényt, amely két, egyenként 8 munkapontosságú értékből álló csoportot szűkít egyetlen 16 mintás sorrá, sorrendben.

A függvény argumentumai, ebben a sorrendben:

  • result: egy puffer memóriacíme, ahová a 16 levágott minta kerül, mindegyik 8 bites érték.
  • first: az első 8 munkapontosságú érték memóriacíme, mindegyik 16 bites előjeles egész szám.
  • second: a következő 8 munkapontosságú érték memóriacíme, mindegyik 16 bites előjeles egész szám.
first  = {300, -5, 128, 255, 0, 400, 64, 200}
second = {255, 256, -1, 100, 50, 1000, 7, 0}
result = {255, 0, 128, 255, 0, 255, 64, 200,   255, 255, 0, 100, 50, 255, 7, 0}

A függvénynek nincs visszatérési értéke.

5. Koordináták szétválasztása x-re és y-ra

A textúra- és csúcsadatok gyakran egymásba fonva érkeznek, minden pont x és y értéke együtt csomagolva. A hatékony feldolgozáshoz viszont gyakran szét kell választani őket: az összes x érték az egyik vektorban, az összes y érték a másikban.

Valósítsd meg a split_coordinates függvényt, amely négy egymásba font (x, y) pontot választ szét egy x koordinátákból álló és egy y koordinátákból álló vektorra.

A függvény argumentumai, ebben a sorrendben:

  • xs: egy puffer memóriacíme, ahová a 4 x koordináta kerül, 4 darab 32 bites lebegőpontos szám.
  • ys: egy puffer memóriacíme, ahová a 4 y koordináta kerül, 4 darab 32 bites lebegőpontos szám.
  • first: az első két pont memóriacíme, 4 darab 32 bites lebegőpontos szám, {x0, y0, x1, y1} formában.
  • second: a következő két pont memóriacíme, 4 darab 32 bites lebegőpontos szám, {x2, y2, x3, y3} formában.
first  = {0.0, 0.5, 1.0, 1.5} // {x0, y0, x1, y1}
second = {2.0, 2.5, 3.0, 3.5} // {x2, y2, x3, y3}
xs     = {0.0, 1.0, 2.0, 3.0} // {x0, x1, x2, x3}
ys     = {0.5, 1.5, 2.5, 3.5} // {y0, y1, y2, y3}

A függvénynek nincs visszatérési értéke.

Szerkesztés GitHubon A hivatkozás új ablakban vagy lapon nyílik meg
x86-64 Assembly Exercism

Készen állsz elkezdeni a(z) Csatorna-swizzle feladatot?

Iratkozz fel az Exercism-re, hogy megtanuld és elsajátítsd a(z) x86-64 Assembly nyelvet 22 fogalom130 feladat segítségével, valódi emberi mentorálással, mindez ingyen.