2017-01-31 3 views
0

Je veux mettre en œuvre une simulation de fluide. Quelque chose comme this. L'algorithme n'est pas important. Le problème important est que si nous devions l'implémenter dans un pixel shader, cela devrait être fait en plusieurs passes.Toute technique pour convertir un shader de pixel multi-passes pour calculer le shader?

Le problème avec la technique que j'avais l'habitude de faire est la performance est très mauvaise. J'expliquerai un aperçu de ce qui se passe et de la technique utilisée pour résoudre le calcul en un passage, puis l'information de synchronisation.

Vue d'ensemble:

Nous avons un terrain et nous voulons la pluie au-dessus et de voir le débit d'eau. Nous avons des données sur les textures 1024x1024. Nous avons la hauteur du terrain et la quantité d'eau dans chaque point. C'est une simulation itérative. L'itération 1 obtient les textures de terrain et d'eau en entrée, calcule puis écrit les résultats sur les textures de terrain et d'eau. L'itération 2 s'exécute et modifie encore un peu les textures. Après des centaines d'itérations, nous avons quelque chose comme ceci:

enter image description here

Dans chaque itération ces étapes se produisent:

  1. Fetch terrain et la hauteur de l'eau.
  2. Calculer le débit.
  3. Écrire la valeur de flux dans la mémoire partagée.
  4. Mémoire de groupe de synchronisation
  5. Lecture Valeur de flux de la mémoire partagée pour ce thread et les threads à gauche, à droite, en haut et en bas du thread en cours.
  6. Calculer une nouvelle valeur pour l'eau en fonction des valeurs de débit lues à l'étape précédente.
  7. Ecrivez les résultats aux textures Terrain et Eau.

Donc, fondamentalement, nous récupérons les données, faire calculate1, mis calculate1 résultats à mémoire partagée, la synchronisation, rapportez de mémoire partagée pour thread courant et les voisins, faire calculate2, et écrire des résultats.

Ceci est un motif clair qui se produit dans un très large éventail de problèmes de traitement d'image. La solution classique serait un shader multi-passes, mais je l'ai fait en un seul passage pour réduire la bande passante.

Technique:

J'ai utilisé la technique expliquée dans Practical Rendering and Computation with Direct3D 11 chapitre 12. Supposons que nous voulons que chaque groupe de fil à 16x16x1 threads. Mais parce que le second calcul a aussi besoin de voisins, nous plaçons les pixels dans chaque direction. ce qui signifie que nous aurons des groupes de discussions 18x18x1. A cause de ce remplissage, nous aurons des voisins valides dans le second calcul. Voici une photo montrant le rembourrage. Les fils jaunes sont ceux qui doivent être calculés et les rouges sont en rembourrage. Ils font partie du groupe de threads, mais nous les utilisons uniquement pour le traitement intermédiaire et ne les sauvegardons pas dans les textures. S'il vous plaît noter que dans cette image le groupe avec remplissage est de 10x10x1 mais notre groupe de discussions est 18x18x1.

enter image description here

Les pistes de processus et retourne un résultat correct. Le seul problème est la performance.

Durée: Sur le système avec Geforce GT 710 je lance la simulation avec 10000 itérations.

  • La simulation complète et correcte prend 60 secondes.
  • Si je ne remplis pas les bordures et n'utilise pas de groupes de fils 16x16x1, le temps sera de 40 secondes. De toute évidence, les résultats sont erronés.
  • Si je n'utilise pas de mémoire partagée et que j'aligne le deuxième calcul avec des valeurs factices, le temps sera de 19 secondes. Les résultats sont bien sûr erronés.

Questions:

  1. Est-ce la meilleure technique pour résoudre ce problème? Si nous calculons à la place dans deux noyaux différents, ce serait plus rapide. 2x19 < 60.
  2. Pourquoi la mémoire partagée de groupe est trop sacrément lente?

Voici le code du shader de calcul. C'est la version correcte qui prend 60 secondes:

#pragma kernel CSMain 


Texture2D<float> _waterAddTex; 
Texture2D<float4> _flowTex; 

RWTexture2D<float4> _watNormTex; 
RWTexture2D<float4> _flowOutTex; 

RWTexture2D<float> terrainFieldX; 
RWTexture2D<float> terrainFieldY; 
RWTexture2D<float> waterField; 


SamplerState _LinearClamp; 
SamplerState _LinearRepeat; 

#define _gpSize 16 
#define _padGPSize 18 

groupshared float4 f4shared[_padGPSize * _padGPSize]; 

float _timeStep, _resolution, _groupCount, _pixelMeter, _watAddStrength, watDamping, watOutConstantParam, _evaporation; 
int _addWater, _computeWaterNormals; 
float2 _rainUV; 
bool _usePrevOutflow,_useStava; 

float terrHeight(float2 texData) { 
    return dot(texData, identity2); 
} 

[numthreads(_padGPSize, _padGPSize, 1)] 
void CSMain(int2 groupID : SV_GroupID, uint2 dispatchIdx : SV_DispatchThreadID, uint2 padThreadID : SV_GroupThreadID) 
{ 

    int2 id = groupID * _gpSize + padThreadID - 1; 
    int gsmID = padThreadID.x + _padGPSize * padThreadID.y; 
    float2 uv = (id + 0.5)/_resolution; 

    bool outOfGroupBound = (padThreadID.x == 0 || padThreadID.y == 0 || padThreadID.x == _padGPSize - 1 
     || padThreadID.y == _padGPSize - 1) ? true : false; 

    // -------------FETCH------------- 

    float2 cenTer, lTer, rTer, tTer, bTer; 
    sampleUavNei(terrainFieldX,terrainFieldY, id, cenTer, lTer, rTer, tTer, bTer); 

    float cenWat, lWat, rWat, tWat, bWat; 
    sampleUavNei(waterField, id, cenWat, lWat, rWat, tWat, bWat); 

    // -------------Calculate 1------------- 

    float cenTerHei = terrHeight(cenTer); 
    float cenTotHei = cenWat + cenTerHei; 
    float4 neisTerHei = float4(terrHeight(lTer), terrHeight(rTer), terrHeight(tTer), terrHeight(bTer)); 
    float4 neisWat = float4(lWat, rWat, tWat, bWat); 
    float4 neisTotHei = neisWat + neisTerHei; 
    float4 neisTotHeiDiff = cenTotHei - neisTotHei; 


    float4 prevOutflow = _usePrevOutflow? _flowTex.SampleLevel(_LinearClamp, uv, 0):float4(0,0,0,0); 
    float4 watOutflow; 
      float4 flowFac = min(abs(neisTotHeiDiff), (cenWat + neisWat) * 0.5f); 
      flowFac = min(1, flowFac); 
      watOutflow = max(watDamping* prevOutflow + watOutConstantParam * neisTotHeiDiff * flowFac, 0); 
      float outWatFac = cenWat/max(dot(watOutflow, identity4) * _timeStep, 0.001f); 
      outWatFac = min(outWatFac, 1); 
      watOutflow *= outWatFac; 

    // -------------groupshared memory------------- 
    f4shared[gsmID] = watOutflow; 

    GroupMemoryBarrierWithGroupSync(); 

    float4 cenFlow = f4shared[gsmID]; 
    float4 lFlow = f4shared[gsmID - 1]; 
    float4 rFlow = f4shared[gsmID + 1]; 
    float4 tFlow = f4shared[gsmID + _padGPSize]; 
    float4 bFlow = f4shared[gsmID - _padGPSize]; 

    //float4 cenFlow = 0; 
    //float4 lFlow = 0; 
    //float4 rFlow = 0; 
    //float4 tFlow = 0; 
    //float4 bFlow = 0; 

    // -------------Calculate 2------------- 

    if (!outOfGroupBound) { 
      float watDiff = _timeStep *((lFlow.y + rFlow.x + tFlow.w + bFlow.z) - dot(cenFlow, identity4)); 
      cenWat = cenWat + watDiff - _evaporation; 
      cenWat = max(cenWat, 0); 
    } 

    // -------------End of calculation------------- 

    //Water Addition 
    if (_addWater) 
     cenWat += _timeStep * _watAddStrength * _waterAddTex.SampleLevel(_LinearRepeat, uv + _rainUV, 0); 

    if (_computeWaterNormals) 
     _watNormTex[id] = float4(0, 1, 0, 0); 

    // -------------Write results------------- 

    if (!outOfGroupBound) { 
     _flowOutTex[id] = cenFlow; 
     waterField[id] = cenWat; 
    } 

} 

Répondre

0

OK J'ai fait une terrible erreur. Pour tester les performances de la mémoire partagée, j'ai fait ce qui suit:

// -------------groupshared memory------------- 
f4shared[gsmID] = watOutflow; 

GroupMemoryBarrierWithGroupSync(); 

float4 cenFlow = f4shared[gsmID]; 
float4 lFlow = f4shared[gsmID - 1]; 
float4 rFlow = f4shared[gsmID + 1]; 
float4 tFlow = f4shared[gsmID + _padGPSize]; 
float4 bFlow = f4shared[gsmID - _padGPSize]; 

//float4 cenFlow = 0; 
//float4 lFlow = 0; 
//float4 rFlow = 0; 
//float4 tFlow = 0; 
//float4 bFlow = 0; 

Vous voyez qu'il y a quelques variables après la synchronisation. J'ai testé avec les cinq premières instructions qui initialisent les variables pour corriger les valeurs. Ensuite, pour tester sans grouphare j'ai commenté les cinq déclarations et décommenté les cinq ci-dessous qui initialisent les variables à zéro. Voici comment cela est devenu:

// -------------groupshared memory------------- 
//f4shared[gsmID] = watOutflow; 

//GroupMemoryBarrierWithGroupSync(); 

//float4 cenFlow = f4shared[gsmID]; 
//float4 lFlow = f4shared[gsmID - 1]; 
//float4 rFlow = f4shared[gsmID + 1]; 
//float4 tFlow = f4shared[gsmID + _padGPSize]; 
//float4 bFlow = f4shared[gsmID - _padGPSize]; 

float4 cenFlow = 0; 
float4 lFlow = 0; 
float4 rFlow = 0; 
float4 tFlow = 0; 
float4 bFlow = 0; 

Ensuite, le processus a été beaucoup plus rapide. La seule chose que je n'ai pas remarquée était que je n'utilisais plus les calculs précédents et que le compilateur les optimisait. Après avoir implémenté plus de l'algorithme, j'ai réalisé que la mémoire partagée fonctionne bien et prend un peu de temps comparé à la lecture de textures.

Après tout, j'ai réussi à faire l'algorithme en 1 passe. C'est 2,5 fois plus rapide que l'implémentation originale et la technique décrite dans les questions fonctionne gracieusement dans mon cas.