Check alle échte Black Friday-deals Ook zo moe van nepaanbiedingen? Wij laten alleen échte deals zien
Toon posts:

[OpenGL] efficient VBO streamen

Pagina: 1
Acties:

Verwijderd

Topicstarter
Ik hobby een particle engine in elkaar. Momenteel is het uploaden van de data naar de GPU een redelijke bottleneck.

Mijn particle struct welke naar de GPU wordt verzonden bestaat uit de volgende datastructuur:
C++:
1
2
3
4
5
6
    struct Particle
    {
        float x, y, z;
        unsigned char s, t; // uv coord
        unsigned char rotation; // rotatie 0-1 geschaald
    }; // = 15 bytes per stuk

Mijn GPU ondersteunt geen geometry shader. Dus ik genereer 4 van deze particles waarbij de positie identiek is, en de rotatie telkens 90° afwijkt. Op de GPU genereert hij vervolgens de uiteindelijke coördinaten met wat sin() en cos(). Hierdoor hoeft de CPU bij het genereren van de quads niet veel meer te doen dan wat data kopiëren. Wat overigens best zwaar is. ( ~50ms per frame @ 1mil. particles ). Dit kan natuurlijk asynchroon gebeuren, al is dat momenteel niet het geval.

We maken een testcase met 1 mil. particles. Een particle is 4*15 bytes, dus ~57MiB data totaal. We tekenen alle particles met een enkele glDrawArrays( GL_QUADS, 0, _itemCount ); call. Ik weet dat GL_QUADS officieel deprecated is. Maar als ik ooit een GPU tegen kom waar GL_QUADS niet werkt... dan wordt het hoog tijd om over te stappen op de geometry shader. De test voeren we uit met de CPU op een fixed kloksnelheid ( 2.0Ghz ) met het window op lage resolutie, zodat we zeker weten dat de GPU geen bottleneck is. Bij het testen bleef de GPU utilization onder de 10%.
setup 1
We uploaden de bewuste VBO met een enkele:
C++:
1
    glBufferData(GL_ARRAY_BUFFER, sizeof( data[0] ) * _itemCount, data.data(), GL_STREAM_DRAW);


Dit levert 16-18 fps op. De profiler toont dat het uploaden van de data (de effectieve glBufferData call) 10 ms duurt. Dit is alleen zo als de groote van het buffer elke frame hetzelfde is. Wanneer het aantal items elke frame met één stijgt keldert de framerate naar 5 fps. De profiler toont dat de call naar glBufferData dan tussen de 190 en 200ms duurt.

Ergo: glBufferData is baggertraag als het formaat van het VBO regelmatig verandert.
setup 2
We alloceren met glBufferData() een buffer wat N keer zo groot is als vereist. Daarna kopiëren we elke frame de data in dat buffer met glBufferSubData(). Bij deze testcase gebruiken we N = 1.2f

C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
    if ( _itemCount > _bufferSize )
    {
        // upsize buffer when buffer is too small;
        resizeBuffer( _itemCount * 1.2 );
    }
    else if ( ( _itemCount * 1.4 ) < _bufferSize )
    {
        // downsize buffer when buffer is too large
        resizeBuffer( _itemCount * 1.2 );
    }

    // upload data
    glBindBuffer(GL_ARRAY_BUFFER, _vbo);
    glBufferSubData( GL_ARRAY_BUFFER, 0, sizeof( data[0] ) * _itemCount, data.data() );

Waar resizeBuffer een enkele glBufferData( GL_ARRAY_BUFFER, sizeof( Particle ) * newSize, 0, GL_STREAM_DRAW ) call doet.

Dit levert 14-15 fps op als het buffer elke frame dezelfde grootte blijft. Het uploaden duurt telkens 25ms (al kan je in de profiler goed zien dat hij van die 25ms, eigenlijk maar 9ms echt aan het uploadden is. Het uploaden stallt om een of andere reden). De framerate komt dus in de buurt van de complete buffer vernieuwing, maar de tijd die de render thread bezig is is een stuk groter. Volgens mij is er sprake van een stall omdat de profiler dit tevoorschijn tovert als ik alleen de bewuste bufferSubData call selecteer:
Afbeeldingslocatie: http://tweakers.net/ext/f/tb4cxx3DbVjsoeZSik82STum/full.png

Als de grootte van het buffer elke frame verandert levert dat 10-11 fps op. Bij profilen verandert er niets tov. de vorige testcase. Het tijdsverlies zal hem dan wel zitten in de aspecten van OpenGL die assynchroon gebeuren.




Concrete vraag: weet iemand snellere manieren om VBO's te 'streamen'? Bij setup #2 heb ik ook nog double buffering met geprobeerd, maar dat maakte geen enkel verschil. De GPU heeft geen enkele moeite om 1 mil. particles te renderen op postzegel formaat, maar de CPU of memory bus bottleneckt gewoon hard.

Ik vermoed dat het grootste probleem de stall bij het gebruiken van bufferSubData is. Maar de mogelijkheden die ik online kan vinden om het buffer te 'invalideren' hebben geen effect op de performance. Ik doel hierbij met name op het gebruiken van glBufferData met een nullptr als data ptr.

psuedo code

Render loop:
code:
1
2
3
4
5
6
7
8
while( 1 )
{
  wacht op signal van main thread..
  render particles
  upload particle VBO
  signal main thread dat de render thread klaar is met alle resources
  glfinish
}


main loop:
code:
1
2
3
4
5
6
7
while( 1 )
{
  convert particles naar juiste OpenGL formaat
  signal render thread
  update particles
  wacht op signal van render thread
}

specs

windows 8
i7-2820QM sandy bridge
Intel HD graphics 3000, OpenGL 3.1
Ik heb ook een nvidia NVS 4200 beschikbaar, maar die levert bij mijn testcases altijd lagere performance. Sowieso wordt nvidia optimus niet blij van buffersubdata... ik heb een half jaar terug een factor 7x memory overhead gemeten tussen nvidia optimus aan en uit bij het gebruik van bufferSubData.

  • Zoijar
  • Registratie: September 2001
  • Niet online

Zoijar

Because he doesn't row...

Ik memory maps ze vaak met deze bits:

C++:
1
2
3
    flags |= GL_MAP_WRITE_BIT;
    flags |= GL_MAP_INVALIDATE_BUFFER_BIT;
    m_map = glMapBufferRange(target, 0 /* offset */, size, flags);


Misschien dat dat helpt. STATIC_DRAW was ook altijd sneller voor mij:

C++:
1
2
3
4
5
6
7
8
9
10
            m_upload_vbos.push_back(
                //  FS::GLBuffers::create(GLBuffers::PIXEL_UNPACK, m_bufsz, GLBuffers::STREAM_DRAW, 0)
                    // try to force it on the GPU...
                    // This seems to matter a lot; why is opengl buffer handling so difficult
                    FS::GLBuffers::create(GLBuffers::ARRAY, m_bufsz, GLBuffers::STATIC_DRAW, 0)
            );
            // for some reason this is very important. If you fail to unbind the created buffer here
            // it will be twice as slow (I can only guess why; perhaps the stream_draw hint gets lost)
//          FS::GLBuffers::disable(GL_PIXEL_UNPACK_BUFFER);
            // done implicitly now by GLBuffers


C++:
1
2
3
4
5
6
7
8
9
            m_timer_upload->start();
            {
                FS::MapPtr map = FS::GLBuffers::map(m_upload_vbos[write_slot->slot_nr], GLBuffers::PIXEL_UNPACK, GLBuffers::WRITE_ONLY);
                char* dst = (char*)map->pointer();
                memcpy(dst, read_slot->ptr, m_bufsz);
            }
            FS::GLBuffers::disable(GLBuffers::PIXEL_UNPACK);

            m_timer_upload->stop();


Dit is vrij oude code (begin 2009), maar ik herinner me dat ik toendertijd weken ben bezig geweest om (async) buffer uploads zo snel mogelijk te krijgen. Misschien dat je er iets tussen ziet dat helpt. Misschien ook niet... :) Ik zou in iedergeval beginnen met een static_draw memory map.

Wat je namelijk wil (denk ik), is heel zeker zijn dat je buffer alleen op de GPU staat, en er een rechtstreekse DMA transfer naartoe gaat. Wat je niet wil is dat je driver hem eerst nog een keer buffert in driver space en dan pas gaat uploaden als hij dat nodig acht.

Overigens kreeg ik toen op een GTX260 maximaal 28.1 MB in 11.6 ms async geupload (GPU was ondertussen druk aan het renderen). Dus misschien is het gewoon wel zo traag. Kaarten zijn wel *veel* sneller nu, dus dat moet je makkelijk verslaan, maar data upload is een gigantische bottleneck. Tijd om je particles te updaten met een compute shader of OpenCL :)

[ Voor 19% gewijzigd door Zoijar op 15-10-2013 20:30 ]


Verwijderd

Topicstarter
Zoijar schreef op dinsdag 15 oktober 2013 @ 20:14:
Misschien dat dat helpt. STATIC_DRAW was ook altijd sneller voor mij:
Dat lijkt bij mijn intel niet veel effect te hebben.

Ik heb ook nog even met glMapBuffer zitten spelen. Dat wordt er niet sneller op. Maar die lelijke CPU stall die er in zat is weg, of in elk geval niet meer zichtbaar in de profier.

Verder ben ik erachter waarom zowel de GPU usage als CPU usage zo laag blijven. Ik gebruik glFinish na elke frame. Dat blijkt de boel enorm te vertragen. Ik had glFinish er oorspronkelijk tussen gezet omdat je zonder glFinish een heel stroperig effect kreeg op interacties met de aansturing van de wereld. Alsof er een stuk of 5 frames in de queue stonden die nog moesten worden afgehandeld. Bij eerdere tests maakte glFinish wel of niet geen verschil in de framerate. Vergeet niet dat ik op een intel graphics zit met 0 latency.

Zonder de glFinish() call kom ik nu op de volgende gegevens, cursief gemarkeerde scores zijn CPU bottlenecked.

Met wat CPU verslinders die voor deze test niet belangrijk zijn uitgeschakeld, buffer elke frame identiek.
53 FPS voor bufferData( static_draw ).
53 FPS voor bufferData( stream_draw ).

33 FPS voor bufferSubData( static_draw ).
52 FPS voor bufferSubData( static_draw ) als ik elke frame een bufferData( null ) tussendoor gooi.
30 FPS voor bufferSubData( stream_draw ).
53 FPS voor bufferSubData( stream_draw ) als ik elke frame een bufferData( null ) tussendoor gooi.

30 FPS voor mapBuffer() met static_draw.
52 FPS voor mapBuffer() met static_draw als ik elke frame een bufferData( null ) tussendoor gooi.
30 FPS voor mapBuffer() met stream_draw.
55 FPS met mapBuffer() met stream_draw als ik elke frame een bufferData( null ) tussendoor gooi.

En ook wanneer het buffer niet elke frame identiek is. Let op, niet vergelijkbaar met vorige gegevens vanwege een van de CPU verslinders die ik bij deze test moet aanzetten.

6 FPS voor bufferData( static_draw ).
6 FPS voor bufferData( stream_draw ).

15 FPS voor bufferSubData( static_draw ).
15 FPS voor bufferSubData( static_draw ) als ik elke frame een bufferData( null ) tussendoor gooi.
15 FPS voor bufferSubData( stream_draw ).
15 FPS voor bufferSubData( stream_draw ) als ik elke frame een bufferData( null ) tussendoor gooi.

14 FPS voor mapBuffer() met static_draw.
14 FPS voor mapBuffer() met static_draw als ik elke frame een bufferData( null ) tussendoor gooi.
14 FPS voor mapBuffer() met stream_draw.
14 FPS met mapBuffer() met stream_draw als ik elke frame een bufferData( null ) tussendoor gooi.


CPU bottleneck galore dus Het genereren van de quads op de CPU, wat nu synchroon gebeurt, is simpelweg traag...

Als ik wat preciezer test zie ik geen verschil tussen static en stream draw en doet bufferSubData 12-13ms per upload call, en mapBuffer 14-15ms. In termen van transfer speed is dat 5GB/s, wat ik geen gek getal vind. Ik heb wel eens minder gemeten op iets wat bottleneckte op bandwidth... Mijn nvidia doet 16-17ms voor SubData en 14-15ms voor mapBuffer(), bufferData() is net als op de intel baggertraag.

Maar: als ik bij glBufferData() voordat de test begint eerst bufferData() uitvoer met meer ruimte dan ik nodig heb, dan is de vreemde dip op de intel kaart voorbij. Nvidia negeert mijn 'hint'.

Het moraal van dit verhaal, als ik de resultaten goed interpreteer, is dat BufferData(), BufferSubData() en MapBuffer() assynchrone functies zijn die voor een flinke bak performanceverlies zorgen als je er glFlush achteraan gooit. Verder moet je glBufferData() zo veel mogelijk vermijden als er een kans bestaat dat de VBO regelmatig groter wordt.

Volgens mij is consistent <15ms snel zat voor 1 mil. particles. Als je ziet dat de CPU al veel eerder bottleneckt qua geometry generatie... Maar als iemand nog ideeën heeft om het sneller te krijgen met OpenGL 3.1, hoor ik het graag.