GPU:er ljuger om dina värden: Perspektivkorrigering, floating point och en bugg som tog veckor att hitta
Alla tre vertexar har värdet 238.0. Fragment-shadern läser 237. Buggen dök bara upp på vissa spelares hårdvara och aldrig i level-editorn. Reddit-tråden (312 poäng) avslöjar hur GPU:ers perspektivkorrekta interpolation kan sabotera dina shader-beräkningar.
En spelutvecklare publicerade en buggrapport på sin blogg som fick 312 poäng på r/programming. Sandrutor i spelet Blackshift ritade skuggor fel — men bara på vissa spelares hårdvara, och aldrig i level-editorn. Felet tog veckor att hitta. Fixen var en enda rad kod. Och anledningen avslöjar något om GPU:er som många utvecklare aldrig tänkt på.
Det här handlar inte om ett obskyrt spel. Det handlar om hur alla GPU:er interpolerar data — och varför det kan gå snett även när du gör allt rätt.
Buggen: Sandskuggor som bara var fel ibland
I Blackshift har varje sandruta en 8-bitars bitmask som styr var skuggor ritas vid rutans kanter. En bit per riktning, åtta riktningar totalt. Värdet passas genom GPU-pipelinen som en float (grafikbiblioteket bgfx stödjer bara floats i instansdata) och castas tillbaka till int i fragment-shadern.
På utvecklarens egen maskin: perfekt. I level-editorn: perfekt. På vissa spelares GPU:er: helt fel skuggmönster. Värdet 238 tolkades som237 — en helt annan bitmask.
Perspective-correct interpolation: Det dolda steget
När du skickar ett varying-värde genom vertex-shadern till fragment-shadern gör GPU:n något som heter perspektivkorrekt interpolation. Även om alla tre hörnen i en triangel har exakt samma värde — säg 238.0 — så passas det inte bara rakt igenom.
GPU:n dividerar med djupet för varje fragment, interpolerar, och multiplicerar tillbaka. Det är nödvändigt för att texturer och färger ska se korrekta ut i 3D-perspektiv. Men det introducerar flyttalsfel. Ett värde som borde vara 238.0 kan bli 237.9999999.
// Fragment shader: den ursprungliga koden
int adjacency = int(v_adjacency); // v_adjacency = 237.9999999
// int() trunkerar → 237, inte 238
// Helt fel bitmask → fel skuggorOch här är det kontraintuitiva: identiska värden på alla tre vertexar garanterar inte identiska värden på alla fragment. Det är den tekniska insikten som Reddit-tråden cirkulerade kring.
Varför level-editorn aldrig visade felet
Level-editorn använder ortografisk projektion — ingen djupkorrigering behövs. I ortografiskt läge är alla fragment på samma djup, så GPU:n hoppar över perspektivkorrigeringen helt. Värdet passas genom oförändrat.
Det är därför buggen aldrig dök upp under utveckling. Och det är därför den bara påverkade vissa spelare — olika GPU-tillverkare har olika flyttalsprecision i sina interpolatorer.
Fixen: En halv float
Lösningen är elegant i sin enkelhet:
// Vertex shader: lägg till 0.5 innan float-konvertering
v_adjacency = float(adjacency) + 0.5;
// Fragment shader: trunkering landar nu rätt
int adj = int(v_adjacency);
// 238.5 → jitter → 238.4999 eller 238.5001 → int() → 238 ✓Genom att lägga till 0.5 hamnar värdet mitt emellan två heltal. Oavsett åt vilket håll GPU:ns interpolation jittrar, landar trunkeringen alltid på rätt heltal. Det är samma princip som att avrunda i stället för att trunkera.
Community-reaktioner: ”It was floating point”
Reddit-tråden gav flera värdefulla tekniska perspektiv:
Gör aldrig diskontinuerliga beräkningar på floats
Användaren u/gurebu (113 poäng) påpekade att även utan perspektivkorrigering är grundläggande barycentrisk interpolation oprecis. Om u + v + w = 1.0 så är u*255 + v*255 + w*255 inte garanterat att vara 255.0 i flyttal. Distributiva lagen gäller inte för IEEE 754.
FMA ändrar allt
En kommentar från u/dukey lyfte att GPU-drivrutiner kan kompilera samma shader-kod på olika sätt. fma(A, B, C) ger ett annat resultat än (A * B) + C — och vilken variant som används beror på GPU-tillverkaren. Det är därför buggen bara dök upp på viss hårdvara.
Använd flat-interpolation
I modern GLSL (3.0+) kan du markera en varying som flat, vilket stänger av interpolation helt:
// GLSL 3.0+
flat out int v_adjacency; // Ingen interpolation
// Alla fragment får exakt vertexvärdet
// Men i GLSL 1.2 (äldre hårdvara) finns inte flat-kvalificeraren
// → Du sitter fast med float + avrundningSpelutvecklaren Robin Allen förklarade att han riktar sig mot GLSL 1.2 för kompatibilitet med äldre hårdvara, där flat inte finns.
Tre lärdomar för shader-utvecklare
Det här är inte ett edge case. Det är en grundläggande egenskap hos GPU-pipelinen som påverkar alla som arbetar med grafik.
- Trunkera aldrig float till int i en shader utan avrundning. Lägg till
0.5innanint(), eller användround()om din GLSL-version stödjer det. - Testa på flera GPU:er. Nvidias interpolator beter sig annorlunda än AMDs som beter sig annorlunda än Intels integrerade grafik. Om du bara testar på din egen maskin ser du bara ditt eget hörn av problemet.
- Använd
flatnär du kan. Om ett värde inte ska interpoleras — heltal, bitmasks, enums, ID:n — markera det somflat. Det är gratis prestanda och eliminerar hela felklassen.
Kodexempel: Så reproducerar du felet
Här är ett minimalt WebGL-exempel som demonstrerar problemet. Skapa en triangel där alla tre vertexar har samma varying-värde, och observera att fragmenten kan få olika värden:
// Vertex shader
attribute vec3 a_position;
varying float v_value;
void main() {
v_value = 238.0; // Samma värde på alla vertexar
gl_Position = projectionMatrix * modelViewMatrix * vec4(a_position, 1.0);
}
// Fragment shader
varying float v_value;
void main() {
int value = int(v_value);
// På vissa GPU:er: value = 237 (inte 238!)
// Fix: avrunda istället för att trunkera
int value_fixed = int(v_value + 0.5);
// Alltid 238, oavsett GPU
// Visualisera skillnaden
if (value != value_fixed) {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // Röd = bugg
} else {
gl_FragColor = vec4(0.0, 1.0, 0.0, 1.0); // Grön = OK
}
}Varför det spelar roll bortom spel
GPU-interpolation påverkar inte bara spelutvecklare. Alla som arbetar med WebGL, compute shaders, eller GPU-accelererad databehandling kan stöta på samma klass av problem. Visualiseringsverktyg, vetenskapliga simuleringar, och till och med ML-inferens på GPU kan ge olika resultat på olika hårdvara av exakt samma anledning.
Flyttal är deterministiska på en given hårdvara, men inte portabla mellan hårdvaror. Det är en distinktion som är lätt att glömma när allt fungerar på din egen maskin.
Summering
En till synes trivial bugg — fel skuggor på sandrutor — avslöjade en fundamental egenskap hos GPU:er: perspektivkorrekt interpolation kan ändra värden även när alla vertexar har identiska data. Fixen (+ 0.5) är en rad kod. Förståelsen bakom den är värd betydligt mer.
Källor och vidare läsning
- Robin Allen — Blackshift Sand Bug
Originalbloggposten med fullständig buggrapport och teknisk analys.
foon.uk/blackshift-sand-bug/ - r/programming-diskussion (312 poäng, 48 kommentarer)
Community-diskussion med tekniska insikter om barycentrisk interpolation, FMA och GPU-skillnader.
reddit.com/r/programming/comments/1sk9bhg/i_learned_something_about_gpus_today/ - OpenGL Specification — Perspective-Correct Interpolation
Sektion 14.6 i OpenGL 4.6-specifikationen beskriver interpolation av varying-variabler.
registry.khronos.org/OpenGL/specs/gl/glspec46.core.pdf