Оптимизация обхода BVH в трассировщике лучей - PullRequest
3 голосов
/ 15 апреля 2020

В моем процессоре трассировки лучей (ну, в общем, трассировщик пути) большая часть времени, затрачиваемого на ЦП, приходится на функцию обхода BVH. Согласно моему профайлеру, 75% времени, затрачиваемого на трассировку лучей, расходуется на эту функцию и функции, которые она вызывает, а 35% времени уходит на саму функцию. Остальные 40% находятся в различных тестах пересечения, которые он вызывает.

По сути, код выполняет обход DFS через все ограничивающие рамки и треугольники, с которыми он пересекается. Он использует статически распределенный массив в стеке для хранения исследуемых узлов (BVHSTACKSIZE имеет значение 32, ему никогда не требуется большая часть пространства), поэтому память не выделяется динамически. Однако мне кажется сумасшедшим, что 35% времени здесь проводится. Я потратил некоторое время на оптимизацию кода, и в настоящее время он работает быстрее всех, но я все еще являюсь самым большим узким местом в моей программе.

У кого-нибудь есть советы по оптимизации этого кода еще больше? ? У меня уже есть приличный алгоритм построения BVH, поэтому я не думаю, что я бы ускорился, если бы использовал другой BVH. У кого-нибудь есть советы о том, как лучше всего выполнять построчное профилирование для Ma c?

. Для справки, этот код в примере сцены занимает от <1 микросекунды до 40 микросекунд в зависимости от числа пересечений, и в то время как l oop выполняется от 1 до ~ 400 итераций (также в зависимости от количества пересечений). </p>

Спасибо!

bool BVHAccel::intersect(Ray& ray) const {
  bool hit = false;

  BVHNode* to_intersect[BVHSTACKSIZE];
  int head = 0;
  to_intersect[head++] = root;

  while (head != 0) {
    assert(head < BVHSTACKSIZE);
    BVHNode* cur = to_intersect[--head];

    if (cur->bb.intersect(ray)) { // Does not modify the ray
      if (cur->isLeaf()) {
        for (const auto& primitive : cur->primitives) {
          hit |= primitive->intersect(ray); // Modifies the ray!
        }
      } else {
        to_intersect[head++] = cur->r;
        to_intersect[head++] = cur->l;
      }
    }
  }

  return hit;
}

bool BBox::intersect(const Ray& r) const {
  double txmin = (min.x - r.o.x) * r.inv_d.x;
  double txmax = (max.x - r.o.x) * r.inv_d.x;
  double tymin = (min.y - r.o.y) * r.inv_d.y;
  double tymax = (max.y - r.o.y) * r.inv_d.y;
  double tzmin = (min.z - r.o.z) * r.inv_d.z;
  double tzmax = (max.z - r.o.z) * r.inv_d.z;

  ascending(txmin, txmax);
  ascending(tymin, tymax);
  ascending(tzmin, tzmax);

  double t0 = std::max(txmin, std::max(tymin, tzmin));
  double t1 = std::min(txmax, std::min(tymax, tzmax));

  if (t1 < t0 || t0 > r.max_t || t1 < r.min_t) {
    return false;
  }

  return true;
}

void ascending(double& a, double& b) {
  if (a > b) {
    std::swap(a, b);
  }
}

Ответы [ 3 ]

1 голос
/ 22 апреля 2020

Я вижу три улучшения, которые вы могли бы сделать.

Первая большая проблема (которая сложная) заключается в том, что у вас есть много условных веток в вашем коде, которые наверняка замедлят ваш процессор, поскольку он не может хорошо предсказать путь к коду (что также верно при компиляции). Например, я вижу, что вы сначала пересекаете, а затем проверяете, является ли узел листом, затем выполняете пересечение со всеми простыми числами. Не могли бы вы сначала проверить, если это лист, а затем сделать правильное пересечение? Это немного уменьшило бы ветвление.

Во-вторых, какова структура памяти вашего BVH? Не могли бы вы оптимизировать его, чтобы сделать его дружественным для вас? Вы могли бы попытаться посмотреть на количество пропусков кэша, которое происходит во время вашего обхода, что было бы хорошим показателем того, имеет ли ваша память правильную структуру или нет. Хотя это и не связано напрямую, теперь приятно, что ваша платформа и базовое оборудование. Я рекомендую прочитать this .

Наконец, и именно здесь вы окажете наибольшее влияние на производительность, используйте SSE / AVX! С помощью небольшого рефакторинга в вашем коде пересечения вы можете пересекать четыре ограничивающие рамки одновременно и, следовательно, иметь хороший импульс в вашем приложении. Вы могли бы взглянуть на то, что делает embree (intel tracer), особенно в математической библиотеке.

Кроме того, я только что увидел, что вы используете double. Есть ли причина для этого? Наш pathtracer вообще не использует double, так как на самом деле не существует случаев, когда вам нужна такая точность для рендеринга.

Надеюсь, это поможет!

РЕДАКТИРОВАТЬ: я сделал sse версия вашего пересечения bbox, если вы хотите попробовать это. Он частично основан на нашем коде, но я не совсем уверен, сработает ли он, вы должны протестировать его и протестировать!

#include <xmmintrin.h>
#include <emmintrin.h>
#include <smmintrin.h>

#include <cmath>
#include <limits>

constexpr float pos_inf = std::numeric_limits<float>::max();
constexpr float neg_inf = std::numeric_limits<float>::min();

size_t __bsf(size_t v)
{
  size_t r = 0; asm ("bsf %1,%0" : "=r"(r) : "r"(v));
  return r;
}

__m128 mini(const __m128 a, const __m128 b)
{
  return _mm_castsi128_ps(_mm_min_epi32(_mm_castps_si128(a),_mm_castps_si128(b)));
}

__m128 maxi(const __m128 a, const __m128 b)
{
  return _mm_castsi128_ps(_mm_max_epi32(_mm_castps_si128(a),_mm_castps_si128(b)));
}

__m128 abs(const __m128 a)
{
  return _mm_andnot_ps(_mm_set1_ps(-0.0f), a);
}

__m128 select(const __m128 mask, const __m128 t, const __m128 f)
{ 
  return _mm_blendv_ps(f, t, mask); 
}

template<size_t i0, size_t i1, size_t i2, size_t i3>
__m128 shuffle(const __m128 b)
{
  return _mm_castsi128_ps(_mm_shuffle_epi32(_mm_castps_si128(b), _MM_SHUFFLE(i3, i2, i1, i0)));
}

__m128 min(const __m128 a, const __m128 b) { return _mm_min_ps(a, b); }

__m128 max(const __m128 a, const __m128 b) { return _mm_max_ps(a, b); }

__m128 vreduce_min(const __m128 v)
{ 
  __m128 h = min(shuffle<1,0,3,2>(v),v);
  return min(shuffle<2,3,0,1>(h),h);
}

__m128 vreduce_max(const __m128 v)
{ 
  __m128 h = max(shuffle<1,0,3,2>(v),v);
  return max(shuffle<2,3,0,1>(h),h);
}

size_t select_min(__m128 valid, __m128 v)
{
  const __m128 a = select(valid, v, _mm_set_ps1(pos_inf));
  return __bsf(_mm_movemask_ps(_mm_and_ps(valid, (a == vreduce_min(a)))));
}

size_t select_max(const __m128 valid, const __m128 v)
{ 
  const __m128 a = select(valid, v, _mm_set_ps1(neg_inf));
  return __bsf(_mm_movemask_ps(_mm_and_ps(valid, (a == vreduce_max(a)))));
}

struct Ray
{
  vec3 o, inv_d;

  float min_t, max_t;
};

struct BBox
{
  vec3 min, max;

  bool intersect(const Ray& r) const;
};

bool BBox::intersect(const Ray& r) const
{
  const __m128 lowerSlab = _mm_mul_ps(_mm_sub_ps(max.m128, r.o.m128), r.inv_d.m128);
  const __m128 upperSlab = _mm_mul_ps(_mm_sub_ps(min.m128, r.o.m128), r.inv_d.m128);

  __m128 tmin = mini(lowerSlab, upperSlab);
  __m128 tmax = maxi(lowerSlab, upperSlab);

  reinterpret_cast<float*>(&tmin)[3] = r.min_t;
  reinterpret_cast<float*>(&tmax)[3] = r.max_t;

  const __m128 maskmin = _mm_castsi128_ps(_mm_cmpeq_epi32(tmin, tmin));
  const __m128 maskmax = _mm_castsi128_ps(_mm_cmpeq_epi32(tmax, tmax));

  const float tNear = abs(tmin[select_max(maskmin, tmin)]); // select the max non NaN value and ensure the result is positive using abs
  const float tFar  =     tmax[select_min(maskmax, tmax)]; // select the min non NaN value
  return tNear <= tFar;
}
1 голос
/ 24 апреля 2020

Я думаю, что есть алгоритмы c улучшений, которые вы можете сделать, прежде чем вы начнете думать о своем оборудовании и выжимать из него каждый байт и каждый цикл.

То, что я вас интересую, является первым ударил по твоему лучу. Вы не делаете накопления от нескольких попаданий вдоль луча, верно? (как будто примитивы, где полупрозрачный или что-то). Да?

Итак - если все вышесказанное верно, я вижу проблему с вашим ордером обхода. Если я правильно читаю ваш код, вы безоговорочно проходите сначала cur->l, а затем cur->r. Это хорошо, если ваш луч ударил в левую рамку первым. Но - это может происходить с другой стороны вашей сцены - тогда вы выполняете намного больше пересечений, чем необходимо, по существу, перемещаясь от одного к другому вдоль вашего луча.

Вместо этого вы хотите перемещаться спереди назад вдоль вашего луча, надеясь, что вы столкнетесь с чем-то раньше, что позволит вам пропустить большинство дальнейших тестов обхода.

К счастью, расстояние до вашей ограничительной рамки вычисляется уже в функции пересечения, и вы можете легко проверить, какая из них пересекается первым. Ему просто нужно вернуть float расстояние до пересечения, а не bool (и, например, + бесконечность при неудаче). И вам нужно вызвать его, прежде чем помещать вещи в ваш стек to_intersect.

Итак, в псевдокоде я хотел бы пройти примерно так:

while stack not empty:
    cur = pop from top of stack;
    //we already know that we want to enter this node!
    if cur is leaf:
        intersect primitives
    else:
        t_left = intersect bbox of cur->l
        t_right = intersect bbox of cur->r
        if both intersected:
            if t_left < t_right:
                push cur->r, cur->l in that order (so that cur->l will be on top)
            else:
                push cur->l, cur->r in that order (so that cur->r will be on top)
        else if one intersected:
            push only that one
        else:
            push nothing

Как только вы реализуете это, вы будете вероятно, обратите внимание, что нужно сделать еще одно улучшение. Условие «мы уже знаем, что хотим войти в этот узел» не обязательно выполняется, если вы нажали какой-то примитив между pu sh и pop, и ваш ray.max_t был уменьшен. Чтобы учесть это, вы можете сохранить пару (t_enter, node) в вашем стеке. Затем, когда вы поп, вы перепроверяете t_enter с вашим текущим ray.max_t. Это может сэкономить вам до h проверок обхода, где h - высота вашего дерева.

Возможная ловушка - вы, вероятно, уже знаете это, но на всякий случай - просто потому, что вы переходите от передней части к задней, не означает что вы можете прекратить свой обход, как только вы найдете свой первый удар. Узлы BVH могут перекрываться. Вот почему сравнение между t_enter и ray.max_t - это путь к go.

1 голос
/ 21 апреля 2020

Кажется, что с вашим кодом есть хотя бы одна проблема. Создание копии primitive может быть дорогостоящей операцией.

bool BVHAccel::intersect(Ray ray) const {
  bool hit = false;

  BVHNode* to_intersect[BVHSTACKSIZE];
  int head = 0;
  to_intersect[head++] = root;

  while (head != 0) {
    assert(head < BVHSTACKSIZE);
    BVHNode* cur = to_intersect[--head];

    if (cur->bb.intersect(ray)) { // Does not modify the ray
      if (cur->isLeaf()) {
        for (const auto& primitive : cur->primitives) { // this code made a copy of primitives on every call!
          hit |= primitive->intersect(ray); // Modifies the ray!
        }
      } else {
        to_intersect[head++] = cur->r;
        to_intersect[head++] = cur->l;
      }
    }
  }

  return hit;
}

Почему необходимо изменить копию луча?

Редактировать 1: Можно ли предположить, что BVHNode выглядит следующим образом это?

constexpr auto BVHSTACKSIZE = 32;

struct Primitive;

struct BVHNode {
    std::vector<Primitive> primitives;
    AABB        bb;   
    BVHNode*    r = nullptr;
    BVHNode*    l = nullptr;

    bool isLeaf() const { return r == nullptr && l == nullptr; }
};
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...