Skip to content

File point_octree.cpp

File List > gui > point_octree.cpp

Go to the documentation of this file

#include "gui/point_octree.hpp"

#include <omp.h>

#include <chrono>
#include <thread>

namespace {

struct BuildJob {
  PointOctreeNode* node = nullptr;
  size_t begin = 0;
  size_t end = 0;
};

constexpr size_t PARALLEL_BUILD_MIN_POINTS = 64'000;

void partition_range(PointOctreeNode& node, OctreePointVector& points, size_t begin, size_t end,
                     std::array<size_t, 9>& child_ends) {
  // Count buckets.
  std::array<size_t, 8> bucket_counts{};
  for (size_t i = begin; i < end; ++i) {
    ++bucket_counts[static_cast<size_t>(node.child_index(points[i]))];
  }
  // Compute bucket boundaries.
  child_ends[0] = begin;
  for (int i = 0; i < 8; ++i) {
    child_ends[static_cast<size_t>(i + 1)] =
        child_ends[static_cast<size_t>(i)] + bucket_counts[static_cast<size_t>(i)];
  }
  // In-place 8-way partition: walk each bucket's final range and swap
  // misplaced elements into their correct buckets. Each swap fixes one
  // element, so at most N swaps total. No temp buffer needed.
  std::array<size_t, 8> write_pos = {child_ends[0], child_ends[1], child_ends[2], child_ends[3],
                                     child_ends[4], child_ends[5], child_ends[6], child_ends[7]};
  for (int b = 0; b < 8; ++b) {
    for (size_t i = write_pos[static_cast<size_t>(b)]; i < child_ends[static_cast<size_t>(b + 1)];
         ++i) {
      int c;
      while ((c = node.child_index(points[i])) != b) {
        std::swap(points[i], points[write_pos[static_cast<size_t>(c)]]);
        ++write_pos[static_cast<size_t>(c)];
      }
    }
  }
}

void split_subtree_job(PointOctreeNode& node, size_t begin, size_t end, OctreePointVector& points,
                       std::vector<BuildJob>& out_jobs) {
  // Shuffle is deferred to shuffle_leaves() after build — per-level shuffles
  // are redundant since children will be shuffled again at lower levels.
  node.begin_index = 0;
  node.end_index = 0;

  std::array<size_t, 9> child_ends{};
  partition_range(node, points, begin, end, child_ends);

  for (int i = 0; i < 8; ++i) {
    const size_t child_begin = child_ends[static_cast<size_t>(i)];
    const size_t child_end = child_ends[static_cast<size_t>(i + 1)];
    if (child_begin == child_end) {
      continue;
    }
    node.ensure_child(i);
    out_jobs.push_back({node.children[static_cast<size_t>(i)].get(), child_begin, child_end});
  }
}

void build_subtree_sequential(PointOctreeNode& node, size_t begin, size_t end,
                              OctreePointVector& points, std::atomic<size_t>& processed,
                              const std::atomic<bool>* cancel) {
  if (end <= begin || (cancel && cancel->load(std::memory_order_relaxed))) {
    return;
  }

  const size_t count = end - begin;
  if (count <= PointOctreeNode::MAX_POINTS || node.depth >= PointOctreeNode::MAX_DEPTH) {
    node.begin_index = begin;
    node.end_index = end;
    processed.fetch_add(count, std::memory_order_relaxed);
    return;
  }

  std::vector<BuildJob> child_jobs;
  child_jobs.reserve(8);
  split_subtree_job(node, begin, end, points, child_jobs);
  for (auto& child_job : child_jobs) {
    build_subtree_sequential(*child_job.node, child_job.begin, child_job.end, points, processed,
                             cancel);
  }
}

}  // namespace

void PointOctree::insert_batch(OctreePointVector&& points,
                               const std::function<void(size_t, size_t)>& progress,
                               const std::atomic<bool>* cancel) {
  m_points = std::move(points);
  m_total_points = m_points.size();
  if (m_points.empty() || !m_root) {
    return;
  }

  std::atomic<size_t> processed{0};
  const size_t total = m_total_points;

  // Expand the root into roughly target_jobs independent subtrees so the build
  // below parallelises well. This phase is fast; only the build phase reports
  // progress (via the poll thread driven by the processed atomic).
  std::vector<BuildJob> jobs;
  jobs.reserve(256);
  jobs.push_back({m_root.get(), 0, m_total_points});

  const size_t target_jobs = static_cast<size_t>(std::max(1, omp_get_max_threads())) * 4;
  size_t expand_idx = 0;
  while (expand_idx < jobs.size() && jobs.size() < target_jobs) {
    if (cancel && cancel->load(std::memory_order_relaxed)) {
      return;
    }
    BuildJob& job = jobs[expand_idx];
    if (job.end <= job.begin || job.end - job.begin <= PARALLEL_BUILD_MIN_POINTS ||
        job.node->depth >= PointOctreeNode::MAX_DEPTH ||
        job.end - job.begin <= PointOctreeNode::MAX_POINTS) {
      ++expand_idx;
      continue;
    }
    BuildJob split_job = job;
    jobs[expand_idx] = std::move(jobs.back());
    jobs.pop_back();
    split_subtree_job(*split_job.node, split_job.begin, split_job.end, m_points, jobs);
  }

  std::atomic<bool> build_done{false};
  std::thread progress_thread;
  if (progress) {
    progress_thread = std::thread([&]() {
      while (!build_done.load(std::memory_order_acquire)) {
        progress(processed.load(std::memory_order_relaxed), total);
        std::this_thread::sleep_for(std::chrono::milliseconds(50));
      }
    });
  }

#pragma omp parallel for schedule(dynamic)
  for (ptrdiff_t job_idx = 0; job_idx < static_cast<ptrdiff_t>(jobs.size()); ++job_idx) {
    BuildJob job = jobs[static_cast<size_t>(job_idx)];
    if (job.end <= job.begin) {
      continue;
    }
    build_subtree_sequential(*job.node, job.begin, job.end, m_points, processed, cancel);
  }

  build_done.store(true, std::memory_order_release);
  if (progress_thread.joinable()) {
    progress_thread.join();
  }
  if (progress && !(cancel && cancel->load(std::memory_order_relaxed))) {
    progress(total, total);
  }
}