Track
/
x86-64 Assembly
x86-64 Assembly
/
Esercizi
/
Swizzle dei canali
Swizzle dei canali

Swizzle dei canali

Esercizio di apprendimento

Introduzione

SIMD: operazioni tra lane

Finora ogni operazione SIMD ha agito lane per lane. Il valore nella lane i del risultato viene calcolato sulla lane i degli input.

Tuttavia, ci sono molte situazioni in cui spostare valori tra le lane è necessario o desiderabile. Per esempio, le lane potrebbero essere in disordine per il calcolo che dobbiamo eseguire.

Esistono molte istruzioni SIMD che spostano dati tra le lane in modi diversi.

Shuffle

Gli shuffle riordinano i byte o le lane di un registro, prendendo ogni lane di destinazione da una lane sorgente che può trovarsi ovunque. Possono selezionare la stessa lane per destinazioni diverse, il che li rende capaci anche di fare broadcast di valori.

Le istruzioni di shuffle si comportano in modo diverso a seconda della loro dimensione e del loro dominio di esecuzione.

Selezionare le lane con un immediato

L'istruzione pshufd riorganizza le quattro lane a 32 bit della sua sorgente nella destinazione.

La selezione è un immediato di 8 bit, letto come quattro campi da 2 bit, uno per ogni lane di destinazione. Ogni campo seleziona quale delle quattro lane sorgente copiare in quella lane di destinazione:

campo indice della lane
00 0
01 1
10 2
11 3

La posizione del campo nell'immediato, letta da destra a sinistra, indica dove viene inserita la lane selezionata. Una lane sorgente può essere selezionata più di una volta, ed è così che una singola lane viene trasmessa in broadcast su tutto il registro:

; 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
Shuffle di lane in virgola mobile

Esistono due istruzioni di shuffle per i numeri in virgola mobile, che seguono la stessa sintassi generale: shuf + p + il suffisso di dimensione (s oppure d).

Anche queste usano un immediato per selezionare le lane. shufps usa la stessa codifica a campi da 2 bit degli shuffle su interi visti prima. Tuttavia, poiché in un operando a 128 bit ci sono solo 2 lane a 64 bit, shufpd usa solo una codifica a campi da 1 bit.

Queste istruzioni differiscono dalle loro controparti intere perché prendono le lane da due operandi invece che da uno:

  • La metà bassa del risultato proviene dall'operando di destinazione.
  • La metà alta proviene dall'operando sorgente.
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]}

Poiché entrambi gli operandi contribuiscono al risultato, possono essere usate per intrecciare due vettori in un solo passaggio. Se sorgente e destinazione sono lo stesso registro, ogni lane viene presa da quel registro.

Lo shuffle di byte

pshufb è lo shuffle più generale. Anche se pshufb e pshufd hanno nomi molto simili, che differiscono solo nel suffisso di dimensione, eseguono operazioni molto diverse.

Innanzitutto, mentre pshufd usa un immediato per selezionare le posizioni delle lane, pshufb usa un vettore di controllo nell'operando sorgente. Questo vettore di controllo è un registro xmm o un operando in memoria di 16 byte.

Per ognuna delle 16 lane di destinazione, i quattro bit bassi del vettore di controllo forniscono un indice di byte sorgente, da 0 a 15. Se la lane i del vettore di controllo contiene l'indice di byte sorgente j, allora la lane j-th della destinazione verrà spostata nella lane i-th.

Questo mette in evidenza una seconda differenza tra le due istruzioni. Mentre pshufd prende le lane da riordinare da un operando sorgente diverso, pshufb esegue lo shuffle sul posto, nella destinazione.

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

pshufb è persino più flessibile di così: può azzerare qualsiasi lane. Se il bit più alto è impostato in una lane i del vettore di controllo, allora la lane i della destinazione viene azzerata. Questo rende pshufb sia una permutazione arbitraria di byte sia una cancellazione selettiva, in un'unica istruzione.

Poiché pshufb agisce a livello di singolo byte, può anche essere usata per riordinare lane di dimensioni diverse, raggruppando lane adiacenti. Per esempio, può essere usata per riordinare i dword:

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}

Intrecciare le lane

Le istruzioni unpack prendono lane da due operandi e le intrecciano, alternando tra i due. Esistono varianti intere e in virgola mobile e seguono in gran parte la stessa struttura sintattica generale, con due differenze:

  1. C'è un suffisso l o h che indica se agisce sulla metà bassa (l) o sulla metà alta (h) di ciascun operando.
  2. Le varianti intere hanno due suffissi di dimensione, il secondo rappresenta il doppio del primo. Per esempio, bw o qdq (il suffisso di dimensione per 16 byte è dq, come in movdqu).
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]}

Restringere con la saturazione

Le istruzioni pack vanno nella direzione opposta, combinando le lane di due operandi in lane di larghezza dimezzata nella destinazione.

Queste istruzioni non hanno un prefisso p. A parte questo, la sintassi combina elementi che abbiamo già visto:

  • l'operazione eseguita, pack.
  • una s o una u per indicare se i valori di output sono con segno o senza segno, rispettivamente.
  • una s per saturante, come nell'aritmetica saturante vista in un concetto precedente.
  • due suffissi di dimensione, il secondo indica la metà della larghezza del primo (il comportamento opposto rispetto a unpck).

Poiché l'operazione pack restringe, non ha bisogno di un suffisso l o h:

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)

Queste istruzioni sono saturanti, cioè i valori di output vengono limitati all'intervallo più stretto. Operano da dword a word e da word a byte. Non esiste una variante da qword a dword.

Nota che l'input viene sempre interpretato come con segno. Il tipo, con segno o senza segno, riguarda l'output. Indica l'intervallo in cui limitare. Per esempio, packsswb limita all'intervallo di un byte con segno, cioè [-128, 127].

Le lane basse del risultato provengono dall'operando di destinazione e quelle alte dalla sorgente:

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]}

Non esiste un equivalente in virgola mobile per queste istruzioni.

Spostare singole lane

Finora abbiamo sempre spostato i dati tra un registro SIMD e un registro di uso generale usando movq/movd. Queste istruzioni possono scrivere o leggere solo la lane bassa di un registro SIMD e, quando scrivono, azzerano tutte le altre lane.

Esistono istruzioni che fanno lo stesso per qualsiasi lane, non solo la prima, lasciando intatte le altre:

  • le istruzioni insert scrivono un elemento da un registro di uso generale, o dalla memoria, in una lane scelta tramite un immediato.
  • le istruzioni extract leggono da una lane e la portano in un registro di uso generale.

Entrambe seguono la sintassi delle istruzioni intere: p + insr/extr + il suffisso di dimensione (b, w, d o q).

In entrambi i casi, il registro di uso generale è di solito largo 32 bit. Solo pinsrq e pextrq richiedono un operando a 64 bit. Un operando in memoria ha sempre la dimensione dell'operazione: 8 bit per pinsrb/pextrb, 16 bit per pinsrw/pextrw e così via.

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

Nota che queste istruzioni, proprio come movd e movq, spostano semplicemente byte grezzi. Ciò significa che possono essere usate anche per valori in virgola mobile memorizzati in memoria o in un registro di uso generale.

Istruzioni

Scrivi i cicli più interni di una pipeline software per le immagini, lo stadio che prepara le texture e compone i livelli prima che arrivino sullo schermo. La pipeline lavora sui pixel un blocco alla volta, applicando la stessa operazione all'intero blocco.

Un pixel è composto da quattro canali da 1 byte: rosso, verde, blu e alpha, in quest'ordine (RGBA). Un blocco è di 4 pixel, quindi 16 byte in totale.

Hai cinque attività.

Ricevi gli operandi tramite indirizzi di memoria e scrivi il risultato tramite un indirizzo di risultato. Tutti gli indirizzi di memoria in questo esercizio sono allineati a 16 byte.

Note

I calcoli in questo esercizio dovrebbero essere eseguiti usando istruzioni SIMD, non operazioni scalari.

1. Converti un'immagine nell'ordine di visualizzazione

Le texture sono memorizzate in RGBA, ma il framebuffer in cui disegna questa pipeline si aspetta ogni pixel nell'ordine BGRA: i canali rosso e blu scambiati, i canali verde e alpha lasciati al loro posto. Un'immagine arriva come una sequenza di blocchi e ogni blocco viene convertito allo stesso modo.

Implementa la funzione to_display_order, che converte un'intera immagine da RGBA a BGRA, un blocco alla volta. Dovresti definire la maschera di controllo per riordinare i canali come una costante impacchettata in memoria e riutilizzarla per ogni blocco.

Questa funzione prende come argomenti, in quest'ordine:

  • result: indirizzo di memoria di un buffer in cui vengono scritti i blocchi convertiti, 16 byte per blocco.
  • pixels: indirizzo di memoria dei blocchi di origine, 4 pixel per blocco, ogni pixel di 4 byte in ordine RGBA.
  • block_count: il numero di blocchi, sempre maggiore di 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}

Questa funzione non restituisce alcun valore.

2. Riempi una regione con un solo colore

Per cancellare una regione o dipingere una fascia uniforme, la pipeline scrive un unico colore su ogni pixel della regione.

Implementa la funzione fill_region, che riempie una regione di block_count blocchi con copie di un unico colore.

Questa funzione prende come argomenti, in quest'ordine:

  • result: indirizzo di memoria di un buffer in cui vengono scritti i blocchi riempiti, 16 byte per blocco.
  • color: indirizzo di memoria di un pixel, 4 byte in ordine RGBA.
  • block_count: il numero di blocchi da riempire, sempre maggiore di 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}

Questa funzione non restituisce alcun valore.

3. Intreccia due scanline

Due livelli a canale singolo devono essere uniti in un unico buffer con i loro campioni interlacciati. Il risultato alterna un campione dal primo livello, poi uno dal secondo.

Implementa la funzione weave_scanlines, che intreccia due righe di 16 campioni ciascuna in un'unica riga di 32 campioni.

Questa funzione prende come argomenti, in quest'ordine:

  • result: indirizzo di memoria di un buffer in cui vengono scritti i 32 campioni interlacciati.
  • first: indirizzo di memoria della prima riga, 16 campioni, ciascuno un valore a 8 bit.
  • second: indirizzo di memoria della seconda riga, 16 campioni, ciascuno un valore a 8 bit.
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}

Questa funzione non restituisce alcun valore.

4. Impacchetta una riga schiarita

Una passata di luminosità riscala ogni campione con una precisione di lavoro a 16 bit, così che un campione troppo luminoso possa superare 255 e una differenza possa scendere sotto 0. Lo stadio finale restringe quei valori di lavoro a campioni a 8 bit per la visualizzazione, portando a 0 tutto ciò che è sotto 0 e a 255 tutto ciò che è sopra 255.

Implementa la funzione pack_samples, che restringe due gruppi di 8 valori di lavoro in un'unica riga di 16 campioni, in ordine.

Questa funzione prende come argomenti, in quest'ordine:

  • result: indirizzo di memoria di un buffer in cui vengono scritti i 16 campioni limitati, ciascuno un valore a 8 bit.
  • first: indirizzo di memoria dei primi 8 valori di lavoro, ciascuno un intero con segno a 16 bit.
  • second: indirizzo di memoria degli 8 valori di lavoro successivi, ciascuno un intero con segno a 16 bit.
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}

Questa funzione non restituisce alcun valore.

5. Separa le coordinate in x e y

I dati di texture e vertici spesso arrivano interlacciati, con x e y di ogni punto impacchettati insieme. Tuttavia, spesso è necessario tenerli separati per un'elaborazione efficiente: tutti i valori x in un vettore, tutti i valori y in un altro.

Implementa la funzione split_coordinates, che separa quattro punti (x, y) interlacciati in un vettore di coordinate x e un vettore di coordinate y.

Questa funzione prende come argomenti, in quest'ordine:

  • xs: indirizzo di memoria di un buffer in cui vengono scritte le 4 coordinate x, 4 numeri in virgola mobile a 32 bit.
  • ys: indirizzo di memoria di un buffer in cui vengono scritte le 4 coordinate y, 4 numeri in virgola mobile a 32 bit.
  • first: indirizzo di memoria dei primi due punti, 4 numeri in virgola mobile a 32 bit, come {x0, y0, x1, y1}.
  • second: indirizzo di memoria dei due punti successivi, 4 numeri in virgola mobile a 32 bit, come {x2, y2, x3, y3}.
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}

Questa funzione non restituisce alcun valore.

Modifica tramite GitHub Il link si apre in una nuova finestra o scheda
x86-64 Assembly Exercism

Vuoi iniziare Swizzle dei canali?

Iscriviti a Exercism per imparare e padroneggiare x86-64 Assembly con 22 concetti130 esercizi e il mentoring di persone reali, tutto gratis.