Poglavlje 08: GPU Arhitektura -- Osnove

Poglavlje 08: GPU Arhitektura -- Osnove


Zasto ovo poglavlje menja sve

Zamislite sledece: imate scenu u Unreal Engine-u koja radi na 15 FPS. Otvorite profajler, vidite da je GPU usko grlo, i sada gledate u brdo brojeva koji vam nista ne govore. Znate da treba nesto da optimizujete, ali sta tacno? I zasto bas to?

Ovo poglavlje je odgovor na to pitanje.

Vecina tutoriala o optimizaciji vam kaze sta da radite -- smanjite rezoluciju tekstura, koristite LOD, izbegavajte prozirne materijale. Ali retko ko objasni zasto to funkcionise. A bez razumevanja "zasto", vi samo slepo pratite pravila koja ne razumete, i cim naidjete na situaciju koja nije pokrivena tim pravilima, ostajete bez odgovora.

GPU arhitektura je taj "zasto".

Kada zaista razumete kako GPU fizicki radi -- kako izvrsava instrukcije, kako pristupa memoriji, kako se nosi sa grananjem u kodu -- onda vam optimizacija prestaje da bude crna magija. Postaje logicna, predvidljiva, skoro ocigledno.

U ovom poglavlju cemo proci kroz:

Svaki od ovih koncepata direktno utice na to kako pisete materijale, kako postavljate scene, i kako donosite odluke o performansama. Ovo nije akademsko znanje -- ovo je znanje koje cete koristiti svakodnevno.

Krenimo.


8.1 CPU vs GPU -- Dva Potpuno Razlicita Pristupa

Analogija koja menja perspektivu

Zamislite da gradite zid od cigle.

CPU pristup: Imate 8 visoko kvalifikovanih zidara. Svaki od njih je majstor -- moze da cita nacrte, donosi odluke na licu mesta, prilagodjava se nepredvidjenim situacijama, radi kompleksne proracune. Ako naidje na problem -- na primer, cev koja prolazi kroz zid -- on zastane, razmisli, prilagodi plan, i nastavi. Svaki zidar radi nezavisno, na svom delu posla, i svaki moze da radi potpuno razlicit zadatak.

GPU pristup: Imate 10.000 radnika. Svaki od njih zna da uradi jednu stvar -- stavi ciglu na odredjeno mesto. Ne donose odluke, ne citaju nacrte, ne prilagodjavaju se. Ali kada im kazete "stavite ciglu na svoju poziciju" -- svih 10.000 to uradi istovremeno. Jedan red cigle -- gotov za sekund.

Ovo je fundamentalna razlika. CPU je dizajniran za slozenost -- mali broj jezgara, ali svako jezgro je neverovatno mocno i fleksibilno. GPU je dizajniran za paralelizam -- ogroman broj jednostavnih procesora koji rade isti posao na razlicitim podacima.

Brojke koje ilustruju razliku

Pogledajmo tipican moderan CPU i GPU iz 2024/2025. godine:

Karakteristika CPU (npr. Ryzen 9 7950X) GPU (npr. RTX 4080)
Broj jezgara 16 (32 thread-a) 9.728 CUDA jezgara
Takt (clock speed) 4.5 - 5.7 GHz 2.2 - 2.5 GHz
L1 Cache po jezgru 32 KB data + 32 KB instruction ~128 KB (shared memory + L1 po SM)
L2 Cache 16 MB ~4 MB
L3 Cache 64 MB Nema zasebnog L3
Kontrola toka Kompleksna (branch prediction, speculative execution, out-of-order) Minimalna
Kontekst prebacivanje Skupo (stotine ciklusa) Gotovo besplatno (1 ciklus)

Pogledajte te brojke pazljivo. CPU ima 16 jezgara koja rade na skoro 6 GHz. GPU ima skoro 10.000 jezgara koja rade na samo 2.5 GHz. Ali svako GPU jezgro je dramaticno jednostavnije.

CPU jezgro ima ogroman deo silicijuma posvecen kontroli -- branch prediction (predvidjanje grananja), speculative execution (spekulativno izvrsavanje), out-of-order execution (izvrsavanje van redosleda), veliki keshevi. Sve to postoji da bi jedno jezgro moglo da radi sto brze na jednom thread-u.

GPU jezgro nema nista od toga. Nema branch prediction. Nema speculative execution. Ima minimalan kes. Umesto toga, sav taj silicijum je iskoriscen za -- vise jezgara.

Zasto je GPU pristup savrsen za grafiku

Razmislite o tome sta rendering zapravo radi:

  1. Vertex processing: Imate 100.000 vertex-a. Na svaki treba primeniti istu transformaciju (World-View-Projection matricu). To je isti posao na razlicitim podacima.

  2. Rasterizacija: Imate milione piksela. Za svaki treba odrediti koji trougao ga pokriva. Opet -- isti posao, razliciti podaci.

  3. Pixel/Fragment shading: Za svaki piksel treba izracunati boju -- primeniti iste teksture, iste jednacine osvetljenja. Isti posao, razliciti podaci.

Vidite obrazac? Rendering je masovno paralelan problem. Imate milione nezavisnih elemenata (verteksa, piksela) na koje treba primeniti istu operaciju. Ovo je bukvalno ono za sta je GPU dizajniran.

CPU bi za ovo bio uzasan. 16 jezgara, ma koliko mocnih, ne moze da obradi 2 miliona piksela 60 puta u sekundi (to je 120 miliona piksela u sekundi za Full HD). Ali 10.000 GPU jezgara moze -- jer svako jezgro obradjuje svoj piksel, sva istovremeno.

Gde CPU i dalje vlada

Bitno je razumeti da GPU nije bolji od CPU-a -- on je drugaciji. Za zadatke koji zahtevaju kompleksnu logiku, donosenje odluka, rad sa nepredvidivim podacima -- CPU je daleko bolji.

Na primer, game logic u Unreal Engine-u: AI donosenje odluka, fizika kolizije, stablo ponasanja (Behavior Tree), animacioni state machine -- sve to radi na CPU-u, jer svaki objekat moze zahtevati potpuno drugaciji tok izvrsavanja.

Ovo je razlog zasto optimizacija igara uvek zahteva razumevanje obe strane. Ponekad je CPU bottleneck (Poglavlje 12 -- CPU optimizacija), ponekad GPU (ovo poglavlje i naredno). Ali da biste znali gde je problem, morate razumeti kako oba rade.


8.2 SIMD/SIMT -- Jedno Naredjenje, Hiljade Radnika

Single Instruction, Multiple Data

SIMD (Single Instruction, Multiple Data) je koncept koji je stariji od GPU-a -- postoji i u CPU-ima (SSE, AVX instrukcije). Ali GPU ga je doveo do ekstrema.

Ideja je prosta: umesto da svaki procesor dobije svoju instrukciju, jedan kontroler instrukcija salje istu instrukciju svim procesorima istovremeno. Svaki procesor tu instrukciju primenjuje na svoje podatke.

Zamislite vojnu paradu. Komandir kaze "korak levo" i svih 1.000 vojnika istovremeno koraci levo. Komandir ne mora da svakom vojniku pojedinacno kaze sta da radi -- jedna komanda, hiljadu izvrsenja.

U GPU terminologiji:

// Ovo je shader kod koji se izvrsava na GPU-u
float4 PixelShader(float2 uv : TEXCOORD) : SV_Target
{
    float4 color = Texture.Sample(Sampler, uv);  // 1. Dohvati teksturu
    color.rgb *= LightIntensity;                   // 2. Pomnoži sa svetlom
    color.rgb = pow(color.rgb, 1.0/2.2);          // 3. Gamma korekcija
    return color;                                  // 4. Vrati boju
}

Ovaj shader se izvrsava za svaki piksel na ekranu. Ali GPU ne izvrsava svaki piksel pojedinacno -- on uzme grupu piksela (recimo 32) i kaze:

  1. "Svi dohvatite teksturu sa svojih UV koordinata"
  2. "Svi pomnozite svoju boju sa intenzitetom svetla"
  3. "Svi primenite gamma korekciju na svoju boju"
  4. "Svi vratite svoju boju"

Cetiri komande -- 32 piksela obradjena. Ista cetiri komande -- sledecih 32 piksela. I tako dalje, dok svi pikseli ne budu gotovi.

SIMT -- GPU-ova varijacija

GPU zapravo koristi malo modifikovan model koji se zove SIMT (Single Instruction, Multiple Threads). Razlika izmedju SIMD i SIMT je suptilna ali vazna:

Razlika je u tome sto SIMT model izgleda kao da svaka nit radi nezavisno -- vi pisete shader kao da obradjujete samo jedan piksel. GPU hardver je taj koji "ispod haube" grupise te niti i izvrsava ih paralelno. Ovo je ogromna prednost za programera, jer ne morate razmisljati o vektorizaciji.

Ali -- i ovo je kljucno -- ta apstrakcija ima svoju cenu. Zato sto se niti izvrsavaju u grupama, ako razlicite niti u grupi treba da rade razlicite stvari (na primer, razlicite grane if naredbe), nastaje problem. O tome cemo detaljno u sledecem odeljku.

Kako ovo izgleda u praksi za Unreal Engine

Kada u Unreal Engine-u napravite Material i primenite ga na mesh, Unreal prevodi vas Material Graph u HLSL shader kod. Taj kod se kompajlira u GPU instrukcije. Kada se mesh renderuje, GPU pokrece taj shader za svaki piksel koji mesh pokriva na ekranu.

Recimo da imate mesh koji pokriva 500.000 piksela. GPU ce kreirati 500.000 niti -- po jednu za svaki piksel. Te niti se grupisu u warp-ove (NVIDIA) ili wavefront-e (AMD) od 32 ili 64 niti, i izvrsavaju se paralelno.

Na RTX 4080 sa 9.728 CUDA jezgara, GPU moze da izvrsava stotine warp-ova istovremeno. To znaci da tih 500.000 piksela moze biti obradjen veoma brzo -- ali samo ako sve niti rade isti posao.


8.3 Warp-ovi i Wavefront-i -- Lockstep Izvrsavanje

Sta je Warp?

Warp je grupa niti (thread-ova) koje se izvrsavaju u lockstep-u -- to jest, sve niti u grupi izvrsavaju istu instrukciju u istom takt ciklusu. Ovo je najmanji nivo paralelizma na GPU-u.

GPU proizvodjac Naziv Velicina
NVIDIA Warp 32 niti
AMD Wavefront (Wavefront64) 64 niti (ili 32 kod RDNA arhitekture -- Wavefront32)
Intel (Arc) EU thread 8/16 niti

Zamislite warp kao grupu vojnika koji marsiraju u formaciji. Svi moraju da idu istim korakom, istom brzinom, u istom smeru. Ako jedan vojnik treba da skrene levo dok ostali idu pravo -- svi moraju da zastanu, sacekaju da on skrene levo, pa onda nastave pravo. Ili obrnuto.

Ovo je fundamentalno ogranicenje GPU-a i jedan od najvaznijih koncepata za razumevanje performansi.

Branch Divergence -- Kada niti odu razlicitim putem

Evo klasicnog primera divergencije u shader-u:

if (uv.x > 0.5)
{
    // Put A: kompleksna kalkulacija
    color = ExpensiveCalculation(uv);
}
else
{
    // Put B: jednostavna kalkulacija
    color = SimpleCalculation(uv);
}

Zamislite da imate warp od 32 niti. 16 niti obradjuje piksele gde je uv.x > 0.5 (Put A), a 16 niti obradjuje piksele gde je uv.x <= 0.5 (Put B).

Sta GPU radi u ovom slucaju?

  1. Prvo, svih 32 niti ulazi u if blok (Put A). Ali samo 16 niti zaista izvrsava kod -- ostalih 16 je maskirano (deaktivirano). One trose takt cikluse ali ne rade nista korisno.

  2. Zatim, svih 32 niti ulazi u else blok (Put B). Sada tih drugih 16 niti radi, a prvih 16 je maskirano.

Rezultat? GPU izvrsava oba puta za ceo warp. Umesto da se izvrsava samo jedan put (sto bi bilo idealno), izvrsavaju se oba, i efektivna iskoriscenost je samo 50%.

Ovo je branch divergence (divergencija grananja) i to je jedan od najvecih ubica performansi na GPU-u.

Koliko je divergencija zaista skupa?

Hajde da napravimo racunicu:

Ali to je najgori slucaj. Ako svi pikseli u jednom warp-u idu istim putem -- nema divergencije. Problem nastaje samo kada se niti unutar istog warp-a razilaze.

Ovo je razlog zasto lokalna koherentnost (susedni pikseli idu istim putem) pomaze performansama. Pikseli koji su blizu jedan drugom na ekranu obicno zavrsavaju u istom warp-u, pa ako svi pikseli u jednom regionu biraju isti put -- nema divergencije.

Prakticne posledice za Unreal Engine materijale

U Unreal Engine materijalu, svaki if node, svaki Switch, svaki Custom node sa branch-om moze da izazove divergenciju.

Klasican primer je kvalitetni LOD u materijalu:

// LOSA PRAKSA: branch divergence na granici prelaza
if (DistanceToCamera < 1000.0)
{
    // Visok kvalitet: 4 texture sample-a, kompleksan lighting
    color = HighQualityShading(uv);
}
else
{
    // Nizak kvalitet: 1 texture sample, jednostavan lighting
    color = LowQualityShading(uv);
}

Na granici od 1000 unita, neki pikseli u istom warp-u ce biti blize, neki dalje. To znaci divergenciju. I umesto da daleki pikseli profitiraju od jeftinijeg shadera, oni placaju cenu oba shadera.

Bolje resenje je koristiti lerp (linearna interpolacija) umesto if:

// BOLJA PRAKSA: bez divergencije
float blend = saturate((DistanceToCamera - 900.0) / 200.0);
float4 highQ = HighQualityShading(uv);
float4 lowQ = LowQualityShading(uv);
color = lerp(highQ, lowQ, blend);

Da, ovo uvek racuna oba kvaliteta, sto zvuci gore. Ali nema divergencije, pa su sve niti u warp-u zauzete -- i cesto je ovo brze nego if sa divergencijom, posebno na velikim povrsinama gde je granica prelaza izrazena.

Naravno, ako mozete da garantujete da ceo mesh koristi samo jedan put (na primer, ceo mesh je daleko od kamere), onda if moze da pomogne jer ce svi warp-ovi ici istim putem i GPU nece morati da izvrsava oba puta. Pogledajte Poglavlje 14 (Materijali i Shader Optimizacija) za vise detalja o tome kako donositi ovu odluku.

Ugnjezdjena divergencija -- umnozavanje problema

Divergencija postaje eksponencijalno gora sa ugnezdenim grananjima:

if (condition1)          // 50% niti -> A, 50% -> B
{
    if (condition2)      // Unutar A: 50% -> AA, 50% -> AB
    {
        // Put AA
    }
    else
    {
        // Put AB
    }
}
else
{
    if (condition3)      // Unutar B: 50% -> BA, 50% -> BB
    {
        // Put BA
    }
    else
    {
        // Put BB
    }
}

U najgorem slucaju, GPU mora da prodje kroz sve cetiri puta, a svaka nit radi korisno u samo jednom od njih. Iskoriscenost: 25%. Tri cetvrtine GPU-ovog vremena je bacaceno.

Ovo je razlog zasto kompleksni materijali sa mnogo uslova mogu biti izuzetno skupi -- ne samo zato sto imaju vise instrukcija, vec zato sto divergencija dramaticno smanjuje efektivnu iskoriscenost hardvera.


8.4 Occupancy -- Sto Vise Posla, Bolje Performanse

Paradoks GPU-a

Evo neceg sto na prvi pogled ne zvuci logicno: GPU radi brze kada ima vise posla. Dajte mu premalo posla, i on ce zapravo raditi sporije nego sto bi mogao.

Zasto? Odgovor lezi u konceptu koji se zove occupancy (zauzetost).

Sta je Occupancy?

Occupancy je odnos izmedju broja aktivnih warp-ova na jednom Streaming Multiprocessor-u (SM) i maksimalnog broja warp-ova koje taj SM moze da podrzi.

Occupancy = Aktivni warp-ovi / Maksimalni warp-ovi

Na primer, ako jedan SM moze da podrzi do 48 warp-ova, a trenutno ima 24 aktivna warp-a, occupancy je 50%.

Zasto je Occupancy vazan?

Da bismo ovo razumeli, moramo prvo razumeti jedan kljucan fakt: pristup memoriji na GPU-u je spor. Kada warp treba da procita teksturu iz VRAM-a, to moze da traje stotine takt ciklusa. Sta GPU radi dok ceka?

Ako ima samo jedan warp -- nista. Samo ceka. Svi procesori sede besposleni dok podaci ne stignu iz memorije.

Ali ako ima 48 warp-ova -- GPU jednostavno prebaci na drugi warp koji ima podatke spremne za obradu. Dok warp #1 ceka na teksturu, GPU izvrsava instrukcije na warp-u #2. Kada warp #2 zatrazi memoriju, prebaci na warp #3. I tako dalje.

Ovo je kao restoran sa jednim kuvarom. Ako kuvar ima samo jedno jelo u pripremi, svaki put kad stavi nesto u rernu mora da ceka 30 minuta ne radeci nista. Ali ako ima 10 jela u raznim fazama pripreme -- dok jedno jelo ceka u rerni, on sece povrce za drugo, servira trece, priprema sos za cetvrto. Nikad ne stoji besposleni.

Sta ogranicava Occupancy?

Svaki SM ima ogranicene resurse koje warp-ovi dele:

  1. Registri (Register File): Svaki SM ima odredjen broj registara (na primer, 65.536 na modernim NVIDIA GPU-ima). Svaka nit u warp-u zauzima odredjen broj registara u zavisnosti od kompleksnosti shader-a. Sto je shader kompleksniji, vise registara koristi, manje warp-ova staje na SM.

  2. Deljena memorija (Shared Memory): Svaki SM ima ogranicenu kolicinu deljene memorije (obicno 48-100 KB). Compute shader-i koji koriste shared memory mogu da smanje occupancy.

  3. Maksimalan broj warp-ova: Postoji hardversko ogranicenje na broj warp-ova po SM-u, cak i ako ima dovoljno registara i shared memory.

  4. Maksimalan broj niti po bloku: Za compute shader-e, postoji ogranicenje na velicinu thread group-a.

Evo prakticnog primera:

SM ima:
- 65.536 registara
- Maksimalno 48 warp-ova (1.536 niti)

Shader A koristi 32 registra po niti:
- 32 registra × 32 niti/warp × 48 warp-ova = 49.152 registara
- 49.152 < 65.536, dakle moze svih 48 warp-ova
- Occupancy: 100%

Shader B koristi 64 registra po niti:
- 64 registra × 32 niti/warp × 48 warp-ova = 98.304 registara
- 98.304 > 65.536! Ne moze svih 48!
- Maksimalno: 65.536 / (64 × 32) = 32 warp-ova
- Occupancy: 32/48 = 66.7%

Shader C koristi 128 registara po niti:
- Maksimalno: 65.536 / (128 × 32) = 16 warp-ova
- Occupancy: 16/48 = 33.3%

Vidite trend? Sto je shader kompleksniji (koristi vise registara), to je occupancy nizi, i GPU ima manje warp-ova izmedju kojih moze da prebacuje, sto znaci da manje efikasno skriva latenciju memorije.

Da li je 100% Occupancy uvek cilj?

Ne. I ovo je cest nesporazum.

Occupancy je vazan za latency-bound shader-e -- one koji provode mnogo vremena cekajuci podatke iz memorije. Za te shader-e, veci occupancy znaci bolje skrivanje latencije.

Ali za compute-bound shader-e (ALU-bound) -- one koji vecinu vremena provode racunajuci, a ne cekajuci memoriju -- visok occupancy moze zapravo skoditi performansama. Zasto? Zato sto veci broj warp-ova znaci manje registara po niti, sto moze dovesti do register spilling -- kada shader nema dovoljno registara, GPU mora da privremeno cuva podatke u sporijem memorijskom prostoru.

Generalno pravilo:

U Unreal Engine kontekstu, vi ne kontrolisete occupancy direktno. Ali kontrolisete kompleksnost vasih materijala, sto direktno utice na broj registara po niti, sto utice na occupancy. Kompleksniji materijal = vise registara = nizi occupancy = (potencijalno) losije performanse, cak i ako sam shader nije mnogo sporiji po instrukciji.

Poglavlje 14 (Materijali i Shader Optimizacija) detaljnije pokriva kako proceniti i kontrolisati kompleksnost materijala.


8.5 Latency Hiding -- Genijalnost GPU Dizajna

Problem: Memorija je spora

Evo jednog neprijatnog fakta iz sveta racunarstva: memorija je mnogo sporija od procesora. I ova razlika se samo povecava iz generacije u generaciju.

Na modernom GPU-u:

Operacija Latencija (takt ciklusi)
Aritmeticka operacija (sabiranje, mnozenje) 4-6 ciklusa
Pristup shared memory / L1 cache 20-30 ciklusa
Pristup L2 cache 100-200 ciklusa
Pristup VRAM (texture fetch, buffer read) 400-800 ciklusa

Procitajte to ponovo. Pristup VRAM-u je 100-200 puta sporiji od aritmeticke operacije. To znaci da za svaki Texture.Sample() poziv u vasem shader-u, GPU bi mogao da uradi 100-200 matematickih operacija u istom vremenu.

Kako CPU resava ovaj problem

CPU koristi veliku hijerarhiju kes memorije: L1 (mali, brz), L2 (srednji), L3 (veliki, sporiji). Kada CPU zatrazi podatak iz memorije, a taj podatak je u L1 kesu -- odgovor stize za 3-4 ciklusa. Ako je u L2 -- 10 ciklusa. L3 -- 30 ciklusa. Ako podatak nije ni u jednom kesu (cache miss), CPU mora da ceka stotine ciklusa na RAM.

Dodatno, CPU koristi i spekulativno izvrsavanje (speculative execution) -- nastavlja da izvrsava instrukcije "na slepo" dok ceka podatke, nadajuci se da ce predvidjanje biti tacno. Ako bude pogresno, baca taj rad i pocinje ispocetka.

Sve ovo zahteva ogroman deo silicijuma (fizickog prostora na cipu) -- i upravo zato CPU ima samo 8-16 jezgara. Ostatak cipa je kes i kontrolna logika.

Kako GPU resava ovaj problem -- potpuno drugacije

GPU nema velike kes memorije. Nema spekulativno izvrsavanje. Umesto toga, koristi strategiju koja je genijalana u svojoj jednostavnosti:

"Dok jedni cekaju, drugi rade."

Evo kako to funkcionise korak po korak:

  1. GPU ima, recimo, 48 warp-ova na jednom SM-u
  2. Warp #1 pocinje da se izvrsava: color = Texture.Sample(...) -- trazi podatak iz VRAM-a
  3. Podatak nece stici 400+ ciklusa. GPU ne ceka. Umesto toga, prebacuje na Warp #2
  4. Warp #2 se izvrsava: normal = NormalMap.Sample(...) -- i on trazi memoriju
  5. GPU prebacuje na Warp #3, #4, #5...
  6. U medjuvremenu, podaci za Warp #1 stizu! GPU se vraca na Warp #1 i nastavlja izvrsavanje

Zamislite to ovako: imate 48 lonaca na stednjaku. Dok se supa u loncu #1 kuva, vi mesate sos u loncu #2, secete povrce za lonac #3, dodajete zacine u lonac #4. Nikad ne stojite i gledate u lonac cekajuci da provri -- uvek imate nesto drugo da radite.

Kljuc je u tome sto prebacivanje izmedju warp-ova na GPU-u je besplatno (ili gotovo besplatno -- 1 takt ciklus). Ovo je radikalno drugacije od CPU-a, gde prebacivanje konteksta kosta stotine ciklusa.

Zasto je prebacivanje besplatno? Zato sto svaki warp ima svoje registre koji su vec alocirani na SM-u. Nema potrebe da se bilo sta cuva ili ucitava -- svi podaci su vec tu, GPU samo pocinje da cita instrukcije iz drugog programa counter-a.

Matematika skrivanja latencije

Hajde da napravimo konkretnu racunicu:

Pretpostavke:
- Latencija VRAM-a: 400 ciklusa
- Shader izvrsava 20 instrukcija izmedju dva memory fetch-a
- Svaka instrukcija traje ~4 ciklusa
- Dakle, korisno vreme izmedju fetch-ova: 20 × 4 = 80 ciklusa
- Vreme cekanja: 400 - 80 = 320 ciklusa besposlenog rada (per warp)

Za potpuno skrivanje latencije:
- Potrebno je dovoljno warp-ova da popune 400 ciklusa
- Svaki warp daje 80 ciklusa korisnog rada
- 400 / 80 = 5 warp-ova minimum

Sa 5 aktivnih warp-ova, dok warp #1 ceka, GPU izvrsava #2, #3, #4, #5,
i kada se vrati na #1, podaci su tu.

Sa samo 2 warp-a: 2 × 80 = 160 ciklusa korisnog rada
Praznog hoda: 400 - 160 = 240 ciklusa -- GPU stoji besposlen 60% vremena!

Ovaj proracun pokazuje zasto je occupancy vazan: vise warp-ova = bolje skrivanje latencije = manja besposlica = bolje performanse.

Implikacije za Unreal Engine

Ovo objasnjava nekoliko optimizacionih "pravila" koja se cesto citiraju ali retko objasne:

  1. Zasto je bolje imati nesto vise matematike nego dodatni texture fetch: Jedan texture fetch kosta 400+ ciklusa latencije. Dodavanje 20-30 matematickih operacija kosta 80-120 ciklusa. Ako mozete da izracunate neku vrednost umesto da je procitate iz teksture -- cesto je to brze, jer ne dodajete memorijsku latenciju.

  2. Zasto je kompleksnost materijala vazna cak i kad shader nije "spor": Kompleksniji shader koristi vise registara, smanjuje occupancy, i time smanjuje sposobnost GPU-a da skrije latenciju. Shader mozda sam po sebi nije sporiji, ali zbog nizeg occupancy-ja, GPU provodi vise vremena cekajuci.

  3. Zasto je overdraw posebno skup (detalji u Poglavlju 10 -- Overdraw i Fill Rate): Svaki preklopljeni piksel trazi iste teksture ponovo, dodajuci memorijsku latenciju. I svaki takav piksel zauzima warp slot koji bi mogao raditi na vidljivom pikselu.


8.6 VRAM -- Memorija Koja Hrani GPU

Sta je VRAM?

VRAM (Video RAM) je memorija koja se nalazi fizicki na grafickoj kartici, direktno povezana sa GPU-om putem brze memorijske magistrale. Za razliku od sistemskog RAM-a koji komunicira sa CPU-om preko PCIe ili memory bus-a, VRAM je dizajniran za ogroman propusni opseg (bandwidth) koji GPU zahteva.

Zamislite sistemski RAM kao autoput sa 4 trake koji vodi od skladista do fabrike. VRAM je kao autoput sa 256 ili 384 traka koji vodi direktno od skladista do proizvodne linije -- neuporedivo vise materijala moze da stigne u istom vremenskom periodu.

VRAM vs Sistemski RAM

Karakteristika Sistemski RAM (DDR5) VRAM (GDDR6X) VRAM (HBM3)
Tipican kapacitet 16-64 GB 8-24 GB 24-80 GB
Bandwidth 50-80 GB/s 500-1000 GB/s 2000-3000+ GB/s
Latencija ~80-100 ns ~100-150 ns ~100 ns
Sirina magistrale 128-bit (dual channel) 256-384 bit 4096-6144 bit
Cena po GB Niska Visoka Veoma visoka

Pogledajte bandwidth brojke. Tipican GPU ima 10-15 puta veci bandwidth od sistemskog RAM-a. I cak taj enormni bandwidth je cesto nedovoljan za sve sto GPU treba da uradi.

Sta se cuva u VRAM-u?

U kontekstu Unreal Engine renderinga, VRAM sadrzi:

  1. Teksture: Ovo je obicno najveci korisnik VRAM-a. Svaka tekstura u sceni -- diffuse, normal map, roughness, metallic, emissive -- sve to zivi u VRAM-u. Mip-map-ovi takodje (Poglavlje 13 -- Teksture i Streaming).

  2. Vertex i Index Buffer-i: Geometrija svakog mesh-a koji se renderuje -- pozicije verteksa, normale, UV koordinate, indeksi trouglova.

  3. Framebuffer: Sama slika koja se renderuje. Za Full HD sa HDR (16-bit float, 4 kanala), to je: 1920 × 1080 × 8 bajtova = ~16 MB po render target-u. Sa G-Buffer-om (deferred rendering) koji ima 4-6 render target-a, to je 64-96 MB samo za jedan frame.

  4. Depth Buffer (Z-Buffer): Informacija o dubini za svaki piksel. Obicno 32-bit float: 1920 × 1080 × 4 = ~8 MB.

  5. Constant Buffer-i: Parametri shader-a -- matrice transformacije, parametri materijala, svetlosne informacije.

  6. Structured Buffer-i: Instancing podatci, GPU-driven rendering podatci, Nanite podatci.

  7. Compute Buffer-i: Privremeni buffer-i za post-processing, Lumen podatci, particle simulation.

Za tipicnu Unreal Engine 5 scenu na 1080p, upotreba VRAM-a moze izgledati ovako:

Teksture:           2-6 GB   (zavisi od broja i rezolucije)
Geometry buffer-i:  200-800 MB
G-Buffer:           ~100 MB
Depth buffer:       ~8 MB
Shadow maps:        200-500 MB
Lumen podatci:      300-800 MB
Nanite podatci:     200-500 MB
Post-process:       100-300 MB
Ostalo:             200-500 MB
--------------------------------------
UKUPNO:             3-9 GB (tipicno)

Ove brojke se dramaticno povecavaju na visim rezolucijama. Na 4K (3840 x 2160), framebuffer-i i render target-i su 4 puta veci. Na 1440p ultrawide, slicno.

Memorijska hijerarhija na GPU-u

Bas kao sto CPU ima hijerarhiju L1 → L2 → L3 → RAM, GPU ima svoju memorijsku hijerarhiju:

    ┌─────────────────────────────────┐
    │         Registri (per nit)       │  Najbrži, najmanja kapaciteta
    │         ~256 registara/nit       │  Latencija: 0-1 ciklus
    ├─────────────────────────────────┤
    │    Shared Memory / L1 Cache      │  Deljeno u okviru SM-a
    │         48-100 KB po SM          │  Latencija: ~20-30 ciklusa
    ├─────────────────────────────────┤
    │           L2 Cache               │  Deljeno za ceo GPU
    │            4-6 MB               │  Latencija: ~100-200 ciklusa
    ├─────────────────────────────────┤
    │             VRAM                 │  Glavni memorijski prostor
    │           8-24 GB               │  Latencija: ~400-800 ciklusa
    └─────────────────────────────────┘

Svaki nivo je veci ali sporiji od prethodnog:

Registri: Najbrza memorija na GPU-u. Svaka nit ima svoje privatne registre -- na modernim NVIDIA GPU-ima, do 255 registara po niti. Pristup je prakticki instantan (0-1 ciklus). Ali kapacitet je mali -- svaki registar je 32-bit (4 bajta), dakle svaka nit ima oko 1 KB privatne memorije. Ovo je gde shader cuva privremene promenljive (float4 color, float3 normal, itd.).

Shared Memory / L1 Cache: Ovo je mali ali brz memorijski prostor (48-100 KB) koji dele sve niti u okviru jednog SM-a. Latencija je oko 20-30 ciklusa. Shared memory je posebno vazan za compute shader-e gde niti trebaju da dele podatke -- na primer, pri racunanju blur efekta, susedni pikseli trebaju pristup istim podacima. U kontekstu rendering shader-a, ovaj prostor se uglavnom koristi kao L1 cache za texture fetch-ove.

L2 Cache: Ovo je veci kes (4-6 MB) koji dele svi SM-ovi na celom GPU-u. Latencija je 100-200 ciklusa. Kada vise SM-ova pristupa istim teksturama (sto je cesto u renderingu), L2 cache znacajno smanjuje pritisak na VRAM bandwidth.

VRAM: Glavni memorijski prostor. Ogroman kapacitet (8-24 GB) ali latencija od 400-800 ciklusa. Ovde zive sve teksture, buffer-i, framebuffer-i.

Texture Cache i zasto je lokalizacija bitna

GPU ima specijalizovan texture cache koji koristi cinjenicu da susedni pikseli obicno citaju susedne texel-e iz teksture. Ovo se zove prostorna lokalizacija (spatial locality).

Teksture u VRAM-u su organizovane u tile-ove (blokove) umesto linearno -- to jest, texel-i koji su blizu na 2D teksturi su blizu i u memoriji. Kada GPU ucita jedan texel, automatski ucitava i okolne texel-e u cache, jer je velika verovatnoca da ce biti potrebni za susedne piksele.

Ovo je razlog zasto tri-linear i anisotropno filtriranje tekstura nije dramaticno skuplje od point filtriranja -- susedni texel-i su verovatno vec u cache-u.

Ali ovo takodje objasnjava zasto neke operacije ubijaju texture cache performanse:


8.7 Memory Bandwidth -- Najcesci Bottleneck

Sta je Memory Bandwidth?

Memory bandwidth (propusni opseg memorije) je kolicina podataka koju GPU moze da procita ili zapise u VRAM u jednoj sekundi. Ovo je jedan od najvaznijih specifikacija GPU-a, a istovremeno jedan od najcesce zanemarenih.

Zamislite VRAM kao ogromno skladiste, a bandwidth kao broj kamiona koji mogu da voze izmedju skladista i fabrike u isto vreme. Mozete imati ogromno skladiste, ali ako imate samo 2 kamiona -- fabrika ce cesce cekati na materijal nego sto ce raditi.

Moderni GPU-ovi imaju impresivan bandwidth:

GPU Memory Bandwidth
RTX 3060 360 GB/s
RTX 4070 504 GB/s
RTX 4080 717 GB/s
RTX 4090 1.008 GB/s
RX 7900 XTX 960 GB/s

Zvuci mnogo, zar ne? Ali hajde da izracunamo koliko je zapravo potrebno.

Koliko bandwidth-a trosi rendering?

Uzmimo primer: renderujemo Full HD (1920x1080) na 60 FPS sa deferred rendering-om.

G-Buffer citanje/pisanje po frame-u:

Lighting pass (citanje G-Buffer-a + pisanje scene color):

Texture fetch-ovi:

Shadow maps:

Post-processing:

Screen-space efekti (SSAO, SSR):

Ukupno po frame-u (gruba procena):

G-Buffer:        ~33 MB
Lighting:        ~50 MB
Teksture:        ~96 MB
Shadows:         ~50 MB
Post-process:    ~200 MB
Screen-space:    ~50 MB
Ostalo:          ~50 MB
---------------------------------
UKUPNO:          ~529 MB po frame-u

Na 60 FPS: 529 MB × 60 = ~31.7 GB/s

To je samo za Full HD! Na 4K, vecina ovih brojki se ucetvorostruci:

Na 4K (3840×2160) na 60 FPS: ~127 GB/s

A ovo je konzervativna procena! Sa Lumen-om, Nanite-om, VSM-om (Virtual Shadow Maps), TSR-om, i svim ostalim UE5 feature-ima, realan zahtev moze biti 2-3x veci.

Na RTX 4070 sa 504 GB/s bandwidth-a, rendering na 4K sa svim UE5 feature-ima aktiviranim moze zaista postati bandwidth-limited.

Bandwidth i overdraw

Overdraw -- kada se isti piksel renderuje vise puta jer ga pokrivaju vise objekata -- direktno umnozava bandwidth zahteve. Ako svaki piksel u proseku ima overdraw od 2x, potreban bandwidth se dupla.

Ovo je jedan od razloga zasto je depth prepass (Early-Z) vazan: ako GPU zna unapred koji objekti su ispred, ne mora da obradjuje (i samim tim trosi bandwidth za) piksele koji ce svakako biti prekriveni. Vise o ovome u Poglavlju 09 (Rendering Pipeline) i Poglavlju 10 (Overdraw i Fill Rate).

Kompresija -- GPU-ov odgovor na bandwidth krizu

Moderni GPU-ovi imaju hardversku kompresiju framebuffer-a koja moze znacajno smanjiti bandwidth zahteve. Ovo se zove Delta Color Compression (NVIDIA) ili Delta Color Compression / Infinity Cache (AMD).

Ideja je da se framebuffer podaci kompresuju pre nego sto se zapisu u VRAM, i dekompresuju kada se citaju. Kompresija je lossless (bez gubitka) i radi dobro na "glatkim" regionima slike gde se susedni pikseli malo razlikuju.

Ali kompresija ne radi dobro na regionima sa mnogo detalja, ostrih ivica, ili buke (noise). U tim slucajevima, kompresija moze potpuno da otkaze i GPU pada na pun, nekompresovani bandwidth.

Ovo je jos jedan razlog zasto su neke rendering tehnike efikasnije od drugih -- one koje proizvode "glatke" izlaze bolje koriste bandwidth kompresiju.


8.8 Compute Units i Streaming Multiprocessors

Gradivni blokovi GPU-a

Do sada smo pricali o GPU-u kao monolitnoj masini. Ali GPU je zapravo sastavljen od mnogo manjih, relativno nezavisnih jedinica. NVIDIA ih naziva Streaming Multiprocessors (SM), AMD ih naziva Compute Units (CU), a Intel ih naziva Execution Units (EU) grupisane u Slices.

Zamislite GPU kao veliku fabriku. Fabrika se ne sastoji od jedne ogromne proizvodne linije -- umesto toga, ima mnogo manjih radionica (SM-ova/CU-ova), svaka sa sopstvenom opremom i radnicima. Direktor fabrike (GPU scheduler) rasporedjuje posao po radionicama.

Sta je unutar jednog SM-a (NVIDIA arhitektura kao primer)?

Pogledajmo strukturu jednog SM-a na modernoj NVIDIA arhitekturi (Ada Lovelace, RTX 40xx serija):

┌──────────────────────────────────────────────────────────┐
│                   Streaming Multiprocessor                │
│                                                          │
│  ┌───────────────┐  ┌───────────────┐                    │
│  │  Warp         │  │  Warp         │                    │
│  │  Scheduler #1 │  │  Scheduler #2 │  ... (#3, #4)      │
│  └───────┬───────┘  └───────┬───────┘                    │
│          │                  │                             │
│  ┌───────┴──────────────────┴───────┐                    │
│  │                                   │                    │
│  │   FP32 Jedinice (CUDA Cores)     │                    │
│  │   128 komada po SM-u             │                    │
│  │                                   │                    │
│  │   INT32 Jedinice                 │                    │
│  │   64 komada po SM-u              │                    │
│  │                                   │                    │
│  │   FP64 Jedinice (opciono)        │                    │
│  │   2 komada (gaming GPU-ovi)      │                    │
│  │                                   │                    │
│  │   SFU (Special Function Units)   │                    │
│  │   sin, cos, sqrt, log, exp       │                    │
│  │   16 komada                      │                    │
│  │                                   │                    │
│  │   LD/ST Jedinice (Load/Store)    │                    │
│  │   32 komada                      │                    │
│  │                                   │                    │
│  │   Tensor Cores                   │                    │
│  │   4 komada (za AI/ML)            │                    │
│  │                                   │                    │
│  │   RT Cores                       │                    │
│  │   1 komad (za ray tracing)       │                    │
│  │                                   │                    │
│  └───────────────────────────────────┘                    │
│                                                          │
│  ┌───────────────────────────────────┐                    │
│  │   Register File: 65.536 × 32-bit │                    │
│  │   = 256 KB                        │                    │
│  └───────────────────────────────────┘                    │
│                                                          │
│  ┌───────────────────────────────────┐                    │
│  │   Shared Memory / L1 Cache        │                    │
│  │   128 KB (konfigurisano)          │                    │
│  └───────────────────────────────────┘                    │
│                                                          │
│  ┌───────────────────────────────────┐                    │
│  │   Texture Units: 4                │                    │
│  │   (texture fetch + filtriranje)   │                    │
│  └───────────────────────────────────┘                    │
│                                                          │
│  Maks warp-ova: 48 (= 1.536 niti)                       │
│  Maks thread blokova: 16-32                              │
└──────────────────────────────────────────────────────────┘

Hajde da razjasnimo svaku komponentu:

CUDA Cores (FP32 ALU-ovi)

Ovo su "radni konji" GPU-a -- aritmeticko-logicke jedinice (ALU) koje izvrsavaju osnovne matematicke operacije na floating-point brojevima: sabiranje, oduzimanje, mnozenje, fused multiply-add (FMA). FMA je posebno vazan za grafiku jer se a × b + c operacija pojavljuje svuda -- u transformacijama matrica, racunanju osvetljenja, interpolaciji boja.

Na Ada Lovelace arhitekturi, svaki SM ima 128 FP32 jedinica. RTX 4080 ima 76 SM-ova, dakle ukupno 128 × 76 = 9.728 CUDA jezgara. RTX 4090 ima 128 SM-ova = 16.384 CUDA jezgara.

Bitna napomena: kada NVIDIA kaze "CUDA jezgra", to je pojedinacna FP32 ALU. To nije isto sto i CPU jezgro -- jedno CPU jezgro je znatno mocnije od jednog CUDA jezgra. Ne uporedjujte ove brojke direktno.

INT32 Jedinice

Izvrsavaju celobrojne (integer) operacije. Koriste se za proracune adresa, indeksiranje, bitske operacije. Na modernim arhitekturama, INT32 jedinice mogu da rade paralelno sa FP32 jedinicama, sto povecava efektivnu propusnost.

SFU (Special Function Units)

Ove jedinice izvrsavaju transendetalne matematicke funkcije: sin(), cos(), sqrt(), log(), exp(), pow(). Ove funkcije su mnogo kompleksnije od prostog sabiranja ili mnozenja i zahtevaju specijalizovan hardver.

Svaki SM ima samo 16 SFU-ova (u poredjenju sa 128 FP32 jedinica). To znaci da su ove operacije sporije -- GPU moze da izvrsi jednu SFU operaciju za svake 8 FP32 operacije.

Ovo je prakticno vazno: u vasim Unreal Engine materijalima, operacije poput sin(), cos(), pow() su skuplje nego add() ili multiply(). Ako imate materijal sa mnogo trigonometrijskih funkcija (na primer, za animaciju vode), imajte na umu da SFU moze postati bottleneck.

Texture Units

Svaki SM ima 4 teksturne jedinice (TMU -- Texture Mapping Units). Ove jedinice su specijalizovane za:

  1. Adresni proracun: Konvertovanje UV koordinata u memorijsku adresu u VRAM-u
  2. Filtriranje: Bilinearno, trilinearno, anizotropno filtriranje (interpolacija izmedju texel-a)
  3. Dekompresija: On-the-fly dekompresija kompresovanih tekstura (BC/DXT formati)

Texture Unit prima UV koordinatu, odlazi u VRAM (ili cache) po podatke, primenjuje filtriranje, i vraca rezultujucu boju. Ceo proces traje mnogo takt ciklusa (zbog memorijske latencije), ali pipeline-ovanjem, svaki TMU moze da vraca po jedan rezultat svakog ciklusa (throughput od 1 texel/ciklus po TMU).

Sa 4 TMU-a po SM-u i 76 SM-ova na RTX 4080, to je 304 texture fetch-a po ciklusu. Na 2.5 GHz, to je teorijski ~760 giga-texel-a po sekundi (GTexel/s). Naravno, memorijski bandwidth ogranicava stvarne performanse daleko ispod ovog teorijskog maksimuma.

Load/Store Jedinice

Ove jedinice se bave citanjem i pisanjem podataka u/iz memorije (VRAM, shared memory, registri). Svaki SM ima 32 LD/ST jedinica, sto znaci da moze da izda 32 memorijske operacije po ciklusu.

Tensor Cores

Ovo su specijalizovane jedinice za matricno mnozenje, originalno dizajnirane za AI/ML trening. U kontekstu igara, NVIDIA ih koristi za DLSS (Deep Learning Super Sampling) -- AI-bazirano upscaling resenje. Unreal Engine 5 podrzava DLSS kroz plugin.

Tensor Core-ovi nisu direktno relevantni za tradicionalni rendering, ali DLSS je izuzetno vazan za performanse (Poglavlje 17 -- Upscaling Tehnologije).

RT Cores (Ray Tracing Cores)

Specijalizovane jedinice za akceleraciju ray tracing-a -- konkretno, za BVH (Bounding Volume Hierarchy) traversal i ray-triangle intersection testove. Ove operacije su srce ray tracing-a i bez hardverske akceleracije bi bile previse spore za real-time upotrebu.

Lumen u Unreal Engine 5 koristi hardware ray tracing (kada je dostupan) za globalnu iluminaciju i refleksije. Bez RT Core-ova, Lumen koristi software ray tracing ili screen-space fallback-ove, sto je manje precizno (vise o ovome u buducem poglavlju o Lumen-u).

Register File -- Skriveni heroj

Register file je mozda najvaznija komponenta SM-a sa stanovista performansi, a gotovo nikad se ne pominje u marketinskim materijalima.

Svaki SM ima 65.536 registara od 32 bita = 256 KB register file. Ovo je ogromno u poredjenju sa CPU-om (koji ima obicno 16-32 opstenamenska registra po jezgru).

Zasto toliko registara? Zato sto svi warp-ovi na SM-u dele ovaj register file. Ako imate 48 warp-ova × 32 niti/warp = 1.536 niti, svaka nit dobija 65.536 / 1.536 = ~42 registra. Ako shader koristi vise registara, manje warp-ova moze da bude aktivno (smanjuje se occupancy).

Ovo je direktna veza izmedju kompleksnosti vaseg materijala u Unreal Engine-u i GPU performansi:

Jednostavan materijal (20 registara/nit):
  → 48 warp-ova × 32 niti × 20 reg = 30.720 registara (od 65.536)
  → Occupancy: 100%
  → Odlicno skrivanje latencije

Kompleksan materijal (80 registara/nit):
  → 65.536 / (80 × 32) = 25 warp-ova
  → Occupancy: 25/48 = 52%
  → Pristojno skrivanje latencije

Veoma kompleksan materijal (200 registara/nit):
  → 65.536 / (200 × 32) = 10 warp-ova
  → Occupancy: 10/48 = 20.8%
  → Slabo skrivanje latencije → moguc bottleneck

Kako GPU raspodjeljuje posao medju SM-ovima

GPU ima globalni scheduler (rasporedivac) koji deli posao na grupe i rasporedjuje ih po SM-ovima. Za rendering:

  1. Vertex processing: GPU deli vertekse na grupe od 32 (warp size). Svaka grupa se salje na jedan SM. SM izvrsava vertex shader na svim verteksima u grupi istovremeno.

  2. Rasterizacija: Specijalizovan hardver (ROP-ovi -- Raster Operators) pretvara trouglove u fragmente (piksele). Ovaj proces nije programabilan.

  3. Pixel/Fragment processing: Fragmenti se grupisu u warp-ove od 32 i salju na SM-ove. Svaki SM izvrsava pixel shader na svojoj grupi piksela.

Vazna stvar: GPU ne garantuje redosled izvrsavanja izmedju SM-ova. SM #5 moze da zavrsi pre SM #1. Ovo je razlog zasto shader-i ne mogu da komuniciraju izmedju razlicitih piksela (sem kroz mehanizme kao sto su atomske operacije ili barrier-i u compute shader-ima).


8.9 Unified Shader Arhitektura -- Kako Logicki Pipeline Mapira na Fizicki Hardver

Stari nacin: Fiksne shader jedinice

U starijim GPU-ima (pre 2007. godine, pre NVIDIA G80 / ATI R600), vertex shader-i i pixel shader-i su se izvrsavali na fizicki odvojenim jedinicama:

STARA ARHITEKTURA (pre-unified):

Verteksi  →  [Vertex Shader Jedinice]  →  Rasterizer  →  [Pixel Shader Jedinice]  →  Framebuffer
                 (fiksni broj)                              (fiksni broj)

Problem sa ovim pristupom je ocigledan: ako scena ima mnogo geometrije ali jednostavne materijale, vertex shader jedinice su preopterecene dok pixel shader jedinice sede besposlene. Obrnuto, ako imate malo geometrije ali kompleksne materijale, pixel shader jedinice su preopterecene.

U oba slucaja, deo GPU-a je neiskoriscen. To je kao da imate fabriku gde se u jednom delu cigle prave, a u drugom farbaju -- i ne mozete da prebacite radnike iz farbanja u pravljenje cigli cak i kad farbari nemaju posla.

Novi nacin: Unified Shaders (od 2007. do danas)

Moderna GPU arhitektura ima unificirane shader jedinice -- isti fizicki hardver moze da izvrsava vertex shadere, pixel shadere, geometry shadere, tessellation shadere, compute shadere, mesh shadere, ray tracing shadere. Sve.

MODERNA ARHITEKTURA (unified):

                    ┌─────────────────────┐
Verteksi ─────────→ │                     │
                    │                     │
Pikseli  ─────────→ │   Unified Shader    │ ─────→ Framebuffer
                    │   Pool (SM-ovi)     │
Compute  ─────────→ │                     │
                    │                     │
Ray Trace ────────→ │                     │
                    └─────────────────────┘

Sada, GPU dinamicki rasporedjuje SM-ove prema potrebi. Ako scena ima mnogo geometrije -- vise SM-ova radi na vertex shader-ima. Ako ima malo geometrije ali kompleksne materijale -- vise SM-ova radi na pixel shader-ima. Iskoriscenost hardvera je dramaticno bolja.

Kako rendering pipeline radi na unified arhitekturi

Pogledajmo korak po korak kako jedan frame prolazi kroz GPU na unified arhitekturi:

Korak 1: Command Processing

CPU (kroz Unreal Engine RHI -- Rendering Hardware Interface) salje komande GPU-u kroz Command Buffer. Svaka komanda kaze GPU-u sta da radi: "izvrsi draw call sa ovim vertex buffer-om, ovim shader-om, ovim teksturama."

GPU-ov command processor cita ove komande i rasporedjuje posao.

Korak 2: Vertex Processing (Input Assembly + Vertex Shader)

GPU-ov Input Assembler cita vertex podatke iz vertex buffer-a i index buffer-a u VRAM-u. Verteksi se grupisu u warp-ove od 32 i salju na dostupne SM-ove.

SM izvrsava vertex shader na svakom verteksu: primenjuje World-View-Projection transformaciju, racuna per-vertex podatke (normale, tangente, UV koordinate za sledeci stage).

Korak 3: Primitive Assembly i Culling

Transformisani verteksi se sklapaju u primitive (trouglove). U ovoj fazi, GPU moze da izvrsi hardverski culling:

Sa Nanite-om u UE5, ovaj proces je fundamentalno drugaciji -- Nanite ima sopstveni software rasterizer koji radi mnogo agresivniji culling (detaljno u Poglavlju o Nanite-u).

Korak 4: Rasterizacija

Rasterizer je fiksna funkcija (nije programabilan) koja pretvara trouglove u fragmente. Za svaki trougao, rasterizer odredjuje koji pikseli na ekranu su pokriveni tim trouglom.

Rasterizer radi u blokovima od 2×2 piksela koji se zovu quad. Ovo je vazno -- cak i ako trougao pokriva samo 1 piksel, GPU obradjuje ceo quad od 4 piksela (3 piksela ce biti maskirani i nece pisati u framebuffer, ali ce trositi compute resurse). O tome vise u odeljku o prakticnim implikacijama.

Korak 5: Early-Z Test (opciono)

Pre nego sto se pokrene pixel shader, GPU moze da proveri depth buffer: ako je piksel iza vec renderovanog piksela, nema smisla pokretati shader za njega. Ovo je ogromna usteda -- pixel shader je obicno najskuplji deo pipeline-a.

Ali Early-Z ne radi uvek. Ako pixel shader menja dubinu (depth write) ili koristi discard/clip (za alpha testing), GPU mora da pokrene shader pre nego sto zna da li piksel treba da se nacrta. U ovim slucajevima, Early-Z je iskljucen za taj draw call.

Ovo je razlog zasto su materijali sa maskiranjem (Masked materials) u Unreal Engine-u skuplji od opaque materijala -- oni koriste clip() za prozirnost, sto iskljucuje Early-Z.

Korak 6: Pixel/Fragment Shading

Fragmenti se grupisu u warp-ove i salju na SM-ove. SM izvrsava pixel shader -- dohvata teksture, racuna osvetljenje, primenjuje efekte materijala.

Ovo je obicno najskuplji deo pipeline-a, jer:

Korak 7: Output Merger (ROP-ovi)

Konacno, Raster Operations (ROP) jedinice izvrsavaju:

ROP-ovi su fiksna funkcija i obicno nisu bottleneck, osim u slucajevima teskog blending-a (transparentni objekti, particles).

Async Compute -- Simultano izvrsavanje razlicitih tipova posla

Moderna GPU arhitektura podrzava asinhrono izvrsavanje -- GPU moze istovremeno da izvrsava graficki posao (rendering) i compute posao (compute shader-i) na razlicitim SM-ovima.

Ovo je korisno jer rendering pipeline ima faze gde ne koristi sve SM-ove (na primer, tokom rasterizacije, pixel shader SM-ovi cekaju). Async compute dozvoljava da se u tim "rupama" izvrsava drugi posao -- na primer, post-processing ili particle simulacija.

Unreal Engine 5 koristi async compute za razlicite efekte: Lumen compute, SSAO, volumetric fog, i druge. Ovo je razlog zasto ponekad dodavanje compute-heavy efekta ne kosta onoliko FPS-a koliko biste ocekivali -- on se parcijalno izvrsava u vremenu kada bi GPU inace bio besposlen.


8.10 Prakticne Implikacije za Optimizaciju u UE5

Sada dolazimo do najvaznijeg dela ovog poglavlja -- kako sve sto smo naucili direktno utice na performanse u Unreal Engine 5. Ovo je deo gde se teorija pretvara u praksu.

Problem malih trouglova (Small Triangle Problem)

Ovo je mozda najvaznija prakticna posledica GPU arhitekture za game developere.

Secate se da smo rekli da rasterizer radi u quad-ovima od 2×2 piksela? Evo sta to znaci u praksi:

Zamislite trougao koji pokriva samo 1 piksel na ekranu. Rasterizer kreira quad od 2×2 = 4 piksela. Pixel shader se pokrece za sva 4 piksela, ali samo 1 ce zapravo pisati u framebuffer. Iskoriscenost: 25%.

Sada zamislite mesh koji je toliko daleko od kamere da svaki trougao pokriva samo 1-2 piksela. Ako mesh ima 10.000 trouglova, GPU ce pokrenuti pixel shader za 40.000 piksela, ali ce korisno obraditi samo 10.000-20.000. Efektivno, trosimo 2-4x vise GPU resursa nego sto je potrebno.

Ali problem je zapravo jos gori od toga.

Svaki warp na GPU-u sadrzi 32 niti (piksela). Ako trougao pokriva samo par piksela, warp ce imati mnogo maskiranih (neaktivnih) niti. U ekstremnom slucaju, trougao koji pokriva 1 piksel ce zauzeti warp od 32 niti ali samo 1 nit ce raditi korisno -- iskoriscenost od 3.1%.

A setite se jos jedne stvari: za svaki takav trougao, GPU mora da izvrsi vertex shader na 3 verteksa, primitive assembly, rasterizaciju -- sav taj overhead za svega 1-2 piksela rezultata.

Ovo je eksplicitno razlog zasto Nanite postoji u Unreal Engine 5. Nanite-ov software rasterizer efikasno obradjuje sitne trouglove jer:

Za mesheve koji ne koriste Nanite, trebate se pobrinuti da trouglovi na tipicanom rastojanju posmatranja budu dovoljno veliki -- minimum 8-16 piksela po trouglu je pravilo palca. LOD sistem (Poglavlje 11) je vaš alat za ovo.

Zasto su texture fetch-ovi skupi

Svaki Texture.Sample() poziv u shader-u pokrece lanac dogadjaja:

  1. GPU izracunava UV adresu
  2. Salje zahtev za podatke iz VRAM-a (ili, sa srecom, cache-a)
  3. Ceka 400-800 ciklusa na odgovor (ako nije u cache-u)
  4. Prima podatke, primenjuje filtriranje
  5. Vraca rezultat

Jedan texture fetch nije problem -- GPU skriva latenciju prebacivanjem na druge warp-ove. Ali svaki dodatni fetch:

Tipican Unreal Engine material moze imati:

Svaki dodatni fetch iznad 4-5 pocinje da bude primetljivo skup. Ovo je razlog zasto se preporucuje:

Vise o optimizaciji tekstura u Poglavlju 13 (Teksture i Streaming) i Poglavlju 14 (Materijali i Shader Optimizacija).

Zasto branching boli -- prakticni primeri

Objasnili smo branch divergence u teoriji, ali evo konkretnih Unreal Engine primera:

Primer 1: Materijal sa uslovnim slojevima

// SKUP materijal -- divergencija na granicama
if (HeightBlend > 0.5)
{
    Fetch snow textures (3 fetcha)
    Calculate snow lighting
}
else
{
    Fetch rock textures (3 fetcha)
    Calculate rock lighting
}

Na granicama izmedju snega i stene, pikseli u istom warp-u ce se razilaziti. GPU izvrsava obe grane. To je 6 texture fetch-ova umesto 3 -- dvostruka cena.

Primer 2: Switch na Material Instance parametar

// POTENCIJALNO SKUP materijal
switch (MaterialType)
{
    case 0: MetalShading(); break;
    case 1: FabricShading(); break;
    case 2: SkinShading(); break;
    case 3: HairShading(); break;
}

Ako razliciti delovi mesh-a koriste razlicite tipove materijala, divergencija je neizbezna. Bolje resenje je da svaki tip bude zaseban materijal na zasebnom mesh section-u (jer razliciti draw call-ovi ne dele warp-ove).

Primer 3: Proceduralni efekti sa uslovima

// Proceduralni materijal za teren
if (slope > 45_degrees)
    ApplyCliffMaterial();
else if (height > 2000)
    ApplySnowMaterial();
else if (moisture > 0.7)
    ApplyGrassMaterial();
else
    ApplyDirtMaterial();

Ovo je ugnjezdena divergencija. U najgorem slucaju (na preseku sva cetiri tipa terena), GPU izvrsava sve cetiri grane. Koristite lerp sa tezinskim mapama umesto if naredbi.

Fill Rate i Overdraw

Fill rate je brzina kojom GPU moze da "popuni" piksele na ekranu -- to jest, koliko piksela po sekundi moze da obradi kroz pixel shader i zapise u framebuffer.

Fill rate je ogranicen sa:

Overdraw -- kada isti piksel bude obradjivan vise puta -- direktno smanjuje efektivni fill rate. Ako imate overdraw od 3x, potrebno vam je 3x veci fill rate da biste odrzali isti framerate.

Najgori krivci za overdraw u Unreal Engine-u su:

  1. Transparentni objekti: Ne mogu da koriste depth buffer za eliminaciju prekrivih piksela (ne mogu da pisu u Z-buffer jer su prozirni)
  2. Particle sistemi: Mnogo overlapping quad-ova
  3. Vegetacija: Mnogo overlapping listova (leaf cards)
  4. Nesortirani opaque objekti: Ako se daleki objekti renderuju pre bliskih, daleki pikseli prolaze kroz ceo shader pa ih bliski pikseli prepisuju

Za (4), Unreal Engine koristi depth prepass i front-to-back sortiranje da minimizira overdraw za opaque objekte. Ali za (1), (2), i (3), overdraw je teze izbecci i zahteva pazljivu optimizaciju (Poglavlje 10 -- Overdraw i Fill Rate).

Shader Complexity i registarski pritisak

Kompleksnost shader-a utice na performanse na vise nacina:

  1. Direktno: Vise instrukcija = vise takt ciklusa za izvrsavanje
  2. Indirektno (registri): Kompleksniji shader koristi vise registara → manji occupancy → losije skrivanje latencije
  3. Indirektno (instruction cache): Duzi shader moze da ne stane u instruction cache SM-a, sto dovodi do dodatnog ucitavanja instrukcija iz memorije

U Unreal Engine Material Editor-u, postoji "Shader Complexity" vizualizacioni mod koji boja piksele prema broju instrukcija u shader-u. Ovo je koristan ali nepotpun alat -- ne uzima u obzir registarski pritisak niti memory bandwidth. Piksel moze biti "zeleni" (malo instrukcija) ali opet skup ako ima mnogo texture fetch-ova.

Za detaljniju analizu shader performansi, koristite GPU profajlere (RenderDoc, NVIDIA Nsight Graphics, AMD Radeon GPU Profiler). Vise o profilisanju u Poglavlju 18 (Profajliranje i Analiza).

Compute Shader Bottleneck-ovi

Compute shader-i (koje UE5 intenzivno koristi za Lumen, Nanite, VSM, post-processing) imaju svoje specificne bottleneck-ove:

  1. Thread Group Size: Ako thread group ima manje niti od warp size-a (32 za NVIDIA), neke CUDA jezgra ce biti neiskoriscena. Thread group treba da bude umnozak warp size-a.

  2. Shared Memory: Compute shader-i koji koriste puno shared memory smanjuju occupancy. Ali shared memory je cesto neophodan za efikasan rad (komunikacija izmedju niti).

  3. Barrier Synchronization: GroupMemoryBarrier() zahteva da sve niti u grupi stignu do iste tacke pre nego sto nastave. Ako neke niti zavrse ranije, one cekaju -- sto je cist gubitak.

  4. Atomske operacije: InterlockedAdd(), InterlockedMax() i slicne atomske operacije su serializovane -- ako vise niti pokusa da pristupi istoj memorijskoj lokaciji, one se izvrsavaju jedna po jedna. Ovo moze dramaticno da uspori shader.

Wave/Warp Intrinsics -- napredne optimizacije

Moderna GPU-ovi i shader modeli (SM 6.0+) podrzavaju wave intrinsics -- operacije koje rade na nivou celog warp-a umesto pojedinacne niti. Na primer:

Ove operacije su mnogo brze od alternativa koje koriste shared memory i barrier-e. Unreal Engine 5 ih koristi interno, i napredni korisnici ih mogu koristiti u Custom HLSL node-ovima. Ali ovo je vec territory za iskusne graficke programere.

Async Compute u praksi

Kako async compute pomaze UE5 performansama?

Tipican rendering frame u UE5 izgleda otprilike ovako vremenski:

Bez async compute:
[Depth Prepass][G-Buffer][Lighting][Post-Process][UI]
GPU: ███████████████████████████████████████████████
     (sve sekvencijalno)

Sa async compute:
Graphics:  [Depth Prepass][G-Buffer][Lighting]........[Post-Process][UI]
Compute:   .............[SSAO][Volumetric Fog][Lumen]..............
GPU:       ███████████████████████████████████████████████
           (compute popunjava "rupe" u graphics pipeline-u)

U idealnom slucaju, compute posao se izvrsava besplatno jer koristi SM-ove koji bi inace bili neiskorisceni tokom grafickog posla. U praksi, uvek postoji neko overhead jer compute i graphics dele memorijski bandwidth i L2 cache, ali korist je cesto znacajna.


Sumarni Pregled: Kljucni Termini

Pojam Znacenje
GPU Graphics Processing Unit -- procesor specijalizovan za masovno paralelnu obradu, sadrzi hiljade jednostavnih jezgara
SIMD Single Instruction Multiple Data -- ista instrukcija se primenjuje na vise podataka istovremeno
SIMT Single Instruction Multiple Threads -- GPU varijanta SIMD-a gde svaka nit ima svoj kontekst ali se izvrsava u lockstep-u
Warp Grupa od 32 niti (NVIDIA) koje se izvrsavaju istovremeno na istoj instrukciji
Wavefront AMD-ov ekvivalent warp-a, tradicionalno 64 niti (32 na RDNA)
Branch Divergence Situacija kada niti u istom warp-u idu razlicitim putevima grananja, primoravajuci GPU da izvrsava sve puteve sekvencijalno
Occupancy Odnos aktivnih warp-ova prema maksimalno mogucem broju na jednom SM-u; veci occupancy = bolje skrivanje latencije
Latency Hiding GPU-ova strategija prebacivanja izmedju warp-ova dok jedni cekaju na memoriju, kako bi se sakrila spora memorija
VRAM Video RAM -- memorija na grafickoj kartici sa visokim bandwidth-om, gde se cuvaju teksture, buffer-i, framebuffer
Memory Bandwidth Kolicina podataka koju GPU moze da procita/zapise iz/u VRAM po sekundi; cest bottleneck
SM (Streaming Multiprocessor) NVIDIA-in naziv za osnovnu gradivnu jedinicu GPU-a koja sadrzi ALU-ove, registre, kes, texture unite
CU (Compute Unit) AMD-ov ekvivalent SM-a
CUDA Core Pojedinacna FP32 ALU jedinica na NVIDIA GPU-u; ne mesati sa CPU jezgrom
SFU Special Function Unit -- specijalizovana jedinica za transcendentalne funkcije (sin, cos, sqrt, itd.)
TMU Texture Mapping Unit -- specijalizovana jedinica za dohvatanje i filtriranje tekstura
ROP Raster Operations Unit -- jedinica za finalno pisanje u framebuffer (depth test, blending)
Register File Brza memorija na SM-u, podeljena izmedju svih aktivnih niti; vise registara po niti = manji occupancy
Shared Memory Mali brzi memorijski prostor (48-128 KB) deljen izmedju niti na istom SM-u
Register Spilling Kada shader koristi vise registara nego sto je dostupno, GPU preliva podatke u sporiju memoriju
Unified Shader Architecture Dizajn gde isti fizicki hardver izvrsava sve tipove shader-a (vertex, pixel, compute, itd.)
Quad Blok od 2x2 piksela -- minimalna jedinica rasterizacije; razlog zasto mali trouglovi imaju losiju efikasnost
Early-Z Optimizacija gde GPU odbacuje piksele koji ce svakako biti prekriveni, pre pokretanja pixel shader-a
Async Compute Mogucnost istovremenog izvrsavanja grafickih i compute operacija na istom GPU-u
Fill Rate Brzina kojom GPU moze da obradi i zapise piksele u framebuffer
Overdraw Visestruko renderovanje istog piksela zbog preklapanja objekata
Tensor Core Specijalizovana jedinica za matricno mnozenje (AI/ML, DLSS)
RT Core Specijalizovana jedinica za hardverski ray tracing (BVH traversal, ray-triangle intersection)

Povezivanje sa ostalim poglavljima

Znanje iz ovog poglavlja je temelj za razumevanje svega sto sledi:


Dalje citanje:

  • "A trip through the Graphics Pipeline" -- Fabian Giesen (2011, ali i dalje relevantan, detaljno objasnjava svaki korak GPU pipeline-a)
  • "GPU Gems" serija -- NVIDIA (besplatno dostupna online na developer.nvidia.com, prakticni primeri GPU programiranja)
  • "Real-Time Rendering, 4th Edition" -- Akenine-Moller, Haines, Hoffman (sveobuhvatna knjiga o real-time rendering tehnikama)
  • "Life of a Triangle" -- NVIDIA (blog post koji objasnjava put jednog trougla kroz GPU pipeline)
  • "RDNA Architecture" -- AMD (white paper o AMD GPU arhitekturi)
  • "GPU Architecture and Shader Programming" -- GDC prezentacije (dostupne na GDC Vault-u, godisnje azurirane)
  • "Nsight Graphics Documentation" -- NVIDIA (prakticno znanje o GPU profilisanju)
  • "Unreal Engine 5 Rendering Internals" -- Epic Games dokumentacija i Unreal Fest prezentacije