Measured on an RTX 5080, one worker (nsys), 16M EIGER rotation set, per frame: spot-finding kernels 757 -> ~383 us. Output is bit-identical: integer sums and a deterministic candidate/spot order throughout. - flag_strong reads the image only where a strong pixel can be: the plain ring pass notes each 32-pixel word's largest value, a geometry table gives the rings each word touches, and a word below the lowest of their thresholds is skipped. One thread per word writes the whole word, so the ring buffer needs no clearing or atomics. 128 -> 12 us. - analyze_pixel with candidates uses 16-row tiles (only tiles near a candidate are worked, and each warp's serial chain of rows is short) and issues the entering/leaving row loads before the window scans; the window count is summed in 32 bits. 254 -> 72 us. The dense path keeps its tiling. - analyze_candidates reads its column window with unconditional, unrolled loads; through value_at each load waited for its previous-pass bit and the row before. 74 -> 34 us. - scatter_bits emits a block's range with the whole warp (prefix sum over the words' bit counts, same order). 32 -> 6 us. - finish_components split into label (one block), sum (all blocks, integer atomics) and filter/compact (one block); a component's sums no longer walk the list from its first member to its last. 26 -> 12 us. Tried and not kept (slower on sm_120): warp-aggregated ring sums and a low-value histogram fast path in reduce_rings_shared, a precomputed tile-needed map, unrolling analyze_pixel's row loop. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
420 lines
21 KiB
Plaintext
420 lines
21 KiB
Plaintext
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
// Sparse connected-component labelling following the design the ACTS/traccc project arrived at for
|
|
// sparse silicon-detector clusterization (backward-neighbour graph over a sorted hit list, then a
|
|
// parallel union-find), which is itself the GPU counterpart of the SparseCCL that
|
|
// StrongPixelSet.cpp adapts on the host.
|
|
// https://github.com/acts-project/traccc
|
|
// (c) 2021-2025 CERN for the benefit of the ACTS project
|
|
// Mozilla Public License Version 2.0
|
|
// The kernels below are this project's own - the algorithm is traccc's. Cited in
|
|
// docs/ACKNOWLEDGEMENT.md: P. Gessinger et al., "traccc: GPU track reconstruction library for HEP
|
|
// experiments" (2025), arXiv:2505.22822.
|
|
|
|
#include <climits>
|
|
|
|
#include "SpotExtractorGPU.h"
|
|
#include "../../common/JFJochException.h"
|
|
|
|
namespace {
|
|
|
|
inline void cuda_err(cudaError_t val) {
|
|
if (val != cudaSuccess)
|
|
throw JFJochException(JFJochExceptionCategory::GPUCUDAError, cudaGetErrorString(val));
|
|
}
|
|
|
|
constexpr int THREADS = 256;
|
|
constexpr int FINISH_THREADS = 1024;
|
|
|
|
// --- compaction: packed bit buffer -> strong-pixel list, sorted by flat index -------------------
|
|
// Each block owns a CONTIGUOUS range of words. Pass 1 counts its strong bits, a single-block scan
|
|
// turns the counts into offsets, and pass 2 walks the same range in increasing order and writes at
|
|
// that offset. No atomics anywhere, which is what keeps the output sorted.
|
|
__global__ void count_bits(const uint32_t *__restrict__ strong, const uint32_t *__restrict__ res_mask,
|
|
uint32_t *__restrict__ block_count, size_t nwords) {
|
|
const size_t per_block = (nwords + gridDim.x - 1) / gridDim.x;
|
|
const size_t w0 = static_cast<size_t>(blockIdx.x) * per_block;
|
|
const size_t w1 = min(w0 + per_block, nwords);
|
|
uint32_t local = 0;
|
|
for (size_t w = w0 + threadIdx.x; w < w1; w += blockDim.x)
|
|
local += __popc(strong[w] & ~res_mask[w]);
|
|
|
|
__shared__ uint32_t block_total;
|
|
if (threadIdx.x == 0) block_total = 0;
|
|
__syncthreads();
|
|
atomicAdd(&block_total, local);
|
|
__syncthreads();
|
|
if (threadIdx.x == 0) block_count[blockIdx.x] = block_total;
|
|
}
|
|
|
|
__global__ void scan_block_counts(const uint32_t *__restrict__ in, uint32_t *__restrict__ out,
|
|
uint32_t *__restrict__ total, int n) {
|
|
__shared__ uint32_t shared[FINISH_THREADS];
|
|
const int t = threadIdx.x, nthreads = blockDim.x;
|
|
const int chunk = (n + nthreads - 1) / nthreads;
|
|
const int lo = min(t * chunk, n), hi = min(lo + chunk, n);
|
|
uint32_t sum = 0;
|
|
for (int i = lo; i < hi; i++) sum += in[i];
|
|
shared[t] = sum;
|
|
__syncthreads();
|
|
for (int d = 1; d < nthreads; d <<= 1) {
|
|
const uint32_t v = (t >= d) ? shared[t - d] : 0u;
|
|
__syncthreads();
|
|
shared[t] += v;
|
|
__syncthreads();
|
|
}
|
|
uint32_t acc = shared[t] - sum;
|
|
for (int i = lo; i < hi; i++) { out[i] = acc; acc += in[i]; }
|
|
if (t == nthreads - 1) *total = shared[t];
|
|
}
|
|
|
|
// One warp per block emits its range, 32 words at a time: each lane takes a word, a warp prefix sum
|
|
// over the words' bit counts gives every lane where its pixels go, and the lane writes them in
|
|
// increasing order - so the list comes out in exactly the order a single thread walking the range
|
|
// would write it. Strong pixels are ~1e-4 of the image, so most passes find nothing at all; doing the
|
|
// walk on one thread left the other 31 idle and the reads of the range one after another.
|
|
__global__ void scatter_bits(const uint32_t *__restrict__ strong, const uint32_t *__restrict__ res_mask,
|
|
const uint32_t *__restrict__ block_offset, const int32_t *__restrict__ image,
|
|
uint32_t *__restrict__ out_index, int32_t *__restrict__ out_value,
|
|
size_t nwords, uint32_t capacity) {
|
|
const int lane = threadIdx.x % 32;
|
|
const size_t per_block = (nwords + gridDim.x - 1) / gridDim.x;
|
|
const size_t w0 = static_cast<size_t>(blockIdx.x) * per_block;
|
|
const size_t w1 = min(w0 + per_block, nwords);
|
|
uint32_t pos = block_offset[blockIdx.x];
|
|
for (size_t base = w0; base < w1; base += 32) {
|
|
const size_t w = base + lane;
|
|
uint32_t word = (w < w1) ? (strong[w] & ~res_mask[w]) : 0;
|
|
const uint32_t n = __popc(word);
|
|
uint32_t before = n; // inclusive prefix sum of n over the lanes
|
|
for (int offset = 1; offset < 32; offset *= 2) {
|
|
const uint32_t t = __shfl_up_sync(0xffffffff, before, offset);
|
|
if (lane >= offset) before += t;
|
|
}
|
|
const uint32_t total = __shfl_sync(0xffffffff, before, 31);
|
|
uint32_t p = pos + before - n;
|
|
while (word != 0) {
|
|
const uint32_t flat = static_cast<uint32_t>(w * 32 + (__ffs(word) - 1));
|
|
word &= word - 1;
|
|
if (p < capacity) {
|
|
out_index[p] = flat;
|
|
out_value[p] = image[flat];
|
|
}
|
|
++p;
|
|
}
|
|
pos += total;
|
|
}
|
|
}
|
|
|
|
// --- connected components ----------------------------------------------------------------------
|
|
__device__ __forceinline__ int lower_bound_device(const uint32_t *a, int n, uint32_t key) {
|
|
int lo = 0, hi = n;
|
|
while (lo < hi) {
|
|
const int mid = (lo + hi) >> 1;
|
|
if (a[mid] < key) lo = mid + 1; else hi = mid;
|
|
}
|
|
return lo;
|
|
}
|
|
|
|
// Walk to the root, halving the path on the way. The plain store is safe: a parent only ever
|
|
// decreases and the grandparent is always still an ancestor, so a concurrent writer can only make
|
|
// the chain shorter. Without halving, a long thin feature - a diffraction ring is exactly one -
|
|
// builds a parent chain as long as the feature itself and every later merge walks all of it.
|
|
__device__ __forceinline__ uint32_t find_root(uint32_t *parent, uint32_t a) {
|
|
uint32_t p = parent[a];
|
|
while (p != a) {
|
|
const uint32_t gp = parent[p];
|
|
if (gp == p) return p;
|
|
parent[a] = gp;
|
|
a = gp;
|
|
p = parent[a];
|
|
}
|
|
return a;
|
|
}
|
|
|
|
// Lock-free union. Terminates because max(a,b) strictly decreases; converges on the component's
|
|
// LOWEST index as its root, which is what the host's make_union does too.
|
|
__device__ __forceinline__ void merge_roots(uint32_t *parent, uint32_t a, uint32_t b) {
|
|
a = find_root(parent, a);
|
|
b = find_root(parent, b);
|
|
while (a != b) {
|
|
if (a < b) { const uint32_t t = a; a = b; b = t; }
|
|
const uint32_t old = atomicMin(&parent[a], b);
|
|
if (old == a) return; // a was a root and now points at b: done
|
|
a = find_root(parent, old); // someone re-parented a; carry on from its root
|
|
b = find_root(parent, b);
|
|
}
|
|
}
|
|
|
|
__global__ void init_parent(uint32_t *__restrict__ parent, const uint32_t *__restrict__ nstrong,
|
|
uint32_t capacity) {
|
|
const int n = static_cast<int>(min(*nstrong, capacity));
|
|
for (int i = blockIdx.x * blockDim.x + threadIdx.x; i < n; i += blockDim.x * gridDim.x)
|
|
parent[i] = i;
|
|
}
|
|
|
|
// The list is sorted by flat index, so a pixel's 8-neighbours that come EARLIER in it are exactly
|
|
// four: (line, col-1), (line-1, col-1), (line-1, col) and (line-1, col+1). Each is one binary
|
|
// search away, which is what makes the sparse formulation cheap.
|
|
__global__ void union_neighbours(const uint32_t *__restrict__ index, uint32_t *__restrict__ parent,
|
|
const uint32_t *__restrict__ nstrong, uint32_t capacity, int width) {
|
|
const int n = static_cast<int>(min(*nstrong, capacity));
|
|
if (static_cast<uint32_t>(n) >= capacity) return;
|
|
for (int i = blockIdx.x * blockDim.x + threadIdx.x; i < n; i += blockDim.x * gridDim.x) {
|
|
const uint32_t flat = index[i];
|
|
const uint32_t col = flat % width;
|
|
const uint32_t line = flat / width;
|
|
uint32_t candidate[4];
|
|
int ncandidate = 0;
|
|
if (col > 0) candidate[ncandidate++] = flat - 1;
|
|
if (line > 0 && col > 0) candidate[ncandidate++] = flat - width - 1;
|
|
if (line > 0) candidate[ncandidate++] = flat - width;
|
|
if (line > 0 && col + 1 < static_cast<uint32_t>(width)) candidate[ncandidate++] = flat - width + 1;
|
|
for (int k = 0; k < ncandidate; k++) {
|
|
const int j = lower_bound_device(index, i, candidate[k]);
|
|
if (j < i && index[j] == candidate[k]) merge_roots(parent, static_cast<uint32_t>(i), static_cast<uint32_t>(j));
|
|
}
|
|
}
|
|
}
|
|
|
|
__global__ void resolve_roots(uint32_t *__restrict__ parent, uint32_t *__restrict__ root,
|
|
const uint32_t *__restrict__ nstrong, uint32_t capacity) {
|
|
const int n = static_cast<int>(min(*nstrong, capacity));
|
|
for (int i = blockIdx.x * blockDim.x + threadIdx.x; i < n; i += blockDim.x * gridDim.x)
|
|
root[i] = find_root(parent, static_cast<uint32_t>(i));
|
|
}
|
|
|
|
// Everything after the labelling, in three launches and still one host synchronisation: hand out
|
|
// labels and clear each component's sums (one block), add every pixel to its component (all blocks),
|
|
// then filter by max-pix and shape and compact (one block) - each step order-preserving or made of
|
|
// integer sums, so the result does not depend on the order anything runs in.
|
|
__global__ void label_components(const uint32_t *__restrict__ root, uint32_t *__restrict__ label,
|
|
SpotExtractorGPUSpot *__restrict__ scratch, uint32_t *__restrict__ nlabel_out,
|
|
uint32_t *__restrict__ nout, const uint32_t *__restrict__ nstrong,
|
|
uint32_t capacity) {
|
|
const int n = static_cast<int>(min(*nstrong, capacity));
|
|
if (threadIdx.x == 0) {
|
|
*nout = 0;
|
|
*nlabel_out = 0;
|
|
}
|
|
__syncthreads();
|
|
// The same give-up the host makes at StrongPixelLimit - except that here the count is known
|
|
// before a single pixel has been written anywhere, so the frame costs nothing to reject.
|
|
if (n == 0 || static_cast<uint32_t>(n) >= capacity) return;
|
|
|
|
__shared__ uint32_t shared[FINISH_THREADS];
|
|
const int t = threadIdx.x, nthreads = blockDim.x;
|
|
const int chunk = (n + nthreads - 1) / nthreads;
|
|
const int lo = min(t * chunk, n), hi = min(lo + chunk, n);
|
|
|
|
// Labels, by a prefix sum over the roots in ascending order - the order the host's second scan
|
|
// hands them out in, which is what makes the spot ORDER identical.
|
|
uint32_t nroot = 0;
|
|
for (int i = lo; i < hi; i++) nroot += (root[i] == static_cast<uint32_t>(i)) ? 1u : 0u;
|
|
shared[t] = nroot;
|
|
__syncthreads();
|
|
for (int d = 1; d < nthreads; d <<= 1) {
|
|
const uint32_t v = (t >= d) ? shared[t - d] : 0u;
|
|
__syncthreads();
|
|
shared[t] += v;
|
|
__syncthreads();
|
|
}
|
|
uint32_t next_label = shared[t] - nroot;
|
|
for (int i = lo; i < hi; i++)
|
|
if (root[i] == static_cast<uint32_t>(i)) label[i] = next_label++;
|
|
const int nlabel = static_cast<int>(shared[nthreads - 1]);
|
|
if (t == 0) *nlabel_out = nlabel;
|
|
|
|
for (int l = t; l < nlabel; l += nthreads) {
|
|
SpotExtractorGPUSpot &s = scratch[l];
|
|
s.x = 0;
|
|
s.y = 0;
|
|
s.photons = 0;
|
|
s.max_photons = LLONG_MIN;
|
|
s.pixel_count = 0;
|
|
s.bbox_side = 0;
|
|
}
|
|
}
|
|
|
|
// The bounding box of each component (x, y = lowest and highest column, z, w = lowest and highest
|
|
// line), kept aside from the sums while they are being accumulated.
|
|
__global__ void init_boxes(int4 *__restrict__ box, const uint32_t *__restrict__ nlabel) {
|
|
const int n = static_cast<int>(*nlabel);
|
|
for (int l = blockIdx.x * blockDim.x + threadIdx.x; l < n; l += blockDim.x * gridDim.x)
|
|
box[l] = make_int4(INT_MAX, INT_MIN, INT_MAX, INT_MIN);
|
|
}
|
|
|
|
// Every strong pixel into its component: the member count, the sums exactly as DiffractionSpot::AddPixel
|
|
// forms them (integers, so the order the atomics arrive in does not matter and host and device agree by
|
|
// construction) and the bounding box.
|
|
__global__ void sum_components(const uint32_t *__restrict__ index, const int32_t *__restrict__ value,
|
|
const uint32_t *__restrict__ root, const uint32_t *__restrict__ label,
|
|
SpotExtractorGPUSpot *__restrict__ scratch, int4 *__restrict__ box,
|
|
const uint32_t *__restrict__ nstrong, uint32_t capacity, int width) {
|
|
const int n = static_cast<int>(min(*nstrong, capacity));
|
|
if (static_cast<uint32_t>(n) >= capacity) return;
|
|
for (int i = blockIdx.x * blockDim.x + threadIdx.x; i < n; i += blockDim.x * gridDim.x) {
|
|
const uint32_t l = label[root[i]];
|
|
const long long counts = value[i];
|
|
const int col = static_cast<int>(index[i] % width), line = static_cast<int>(index[i] / width);
|
|
SpotExtractorGPUSpot &s = scratch[l];
|
|
atomicAdd(reinterpret_cast<unsigned long long *>(&s.x), static_cast<unsigned long long>(col * counts));
|
|
atomicAdd(reinterpret_cast<unsigned long long *>(&s.y), static_cast<unsigned long long>(line * counts));
|
|
atomicAdd(reinterpret_cast<unsigned long long *>(&s.photons), static_cast<unsigned long long>(counts));
|
|
atomicMax(reinterpret_cast<long long *>(&s.max_photons), counts);
|
|
atomicAdd(&s.pixel_count, 1);
|
|
atomicMin(&box[l].x, col);
|
|
atomicMax(&box[l].y, col);
|
|
atomicMin(&box[l].z, line);
|
|
atomicMax(&box[l].w, line);
|
|
}
|
|
}
|
|
|
|
// Size and shape filter, compacted by a prefix sum so the surviving spots keep their order. The test
|
|
// is SpotShapeAccepted written out - the constants come in as arguments rather than being included
|
|
// here, so there is exactly one definition of them.
|
|
__global__ void filter_components(SpotExtractorGPUSpot *__restrict__ scratch, const int4 *__restrict__ box,
|
|
SpotExtractorGPUSpot *__restrict__ out, uint32_t *__restrict__ nout,
|
|
const uint32_t *__restrict__ nlabel_in,
|
|
int max_pix, int shape_free_pix, int min_fill_percent) {
|
|
const int nlabel = static_cast<int>(*nlabel_in);
|
|
if (nlabel == 0) return;
|
|
|
|
__shared__ uint32_t shared[FINISH_THREADS];
|
|
const int t = threadIdx.x, nthreads = blockDim.x;
|
|
auto keep = [&](const SpotExtractorGPUSpot &s) {
|
|
if (s.pixel_count > max_pix) return false;
|
|
if (s.pixel_count <= shape_free_pix) return true;
|
|
return static_cast<long long>(s.pixel_count) * 100
|
|
>= static_cast<long long>(min_fill_percent) * s.bbox_side * s.bbox_side;
|
|
};
|
|
const int label_chunk = (nlabel + nthreads - 1) / nthreads;
|
|
const int label_lo = min(t * label_chunk, nlabel), label_hi = min(label_lo + label_chunk, nlabel);
|
|
uint32_t nkeep = 0;
|
|
for (int i = label_lo; i < label_hi; i++) {
|
|
scratch[i].bbox_side = max(box[i].y - box[i].x, box[i].w - box[i].z) + 1;
|
|
nkeep += keep(scratch[i]) ? 1u : 0u;
|
|
}
|
|
shared[t] = nkeep;
|
|
__syncthreads();
|
|
for (int d = 1; d < nthreads; d <<= 1) {
|
|
const uint32_t v = (t >= d) ? shared[t - d] : 0u;
|
|
__syncthreads();
|
|
shared[t] += v;
|
|
__syncthreads();
|
|
}
|
|
uint32_t pos = shared[t] - nkeep;
|
|
for (int i = label_lo; i < label_hi; i++)
|
|
if (keep(scratch[i])) out[pos++] = scratch[i];
|
|
if (t == nthreads - 1) *nout = shared[t];
|
|
}
|
|
|
|
} // namespace
|
|
|
|
SpotExtractorGPU::SpotExtractorGPU(int32_t in_width, int32_t in_height, std::shared_ptr<CudaStream> in_stream)
|
|
: stream(std::move(in_stream)),
|
|
width(in_width),
|
|
nwords((static_cast<size_t>(in_width) * in_height + 31) / 32),
|
|
max_strong(StrongPixelLimit(static_cast<size_t>(in_width) * in_height)),
|
|
gpu_res_mask(nwords),
|
|
gpu_nstrong(1),
|
|
gpu_index(max_strong),
|
|
gpu_value(max_strong),
|
|
gpu_parent(max_strong),
|
|
gpu_root(max_strong),
|
|
gpu_label(max_strong),
|
|
gpu_box(max_strong),
|
|
gpu_nlabel(1),
|
|
gpu_spot(max_strong),
|
|
gpu_spot_out(max_strong),
|
|
gpu_nspot(1),
|
|
host_nstrong(1),
|
|
host_nspot(1),
|
|
host_spot(SPOT_PREFIX) {
|
|
// One block per few hundred words: enough blocks to fill the device, few enough that the serial
|
|
// emission inside a block stays short even when a whole detector row lights up.
|
|
compact_blocks = static_cast<int>((nwords + 255) / 256);
|
|
if (compact_blocks > 4096) compact_blocks = 4096;
|
|
if (compact_blocks < 1) compact_blocks = 1;
|
|
gpu_block_count = CudaDevicePtr<uint32_t>(compact_blocks);
|
|
gpu_block_offset = CudaDevicePtr<uint32_t>(compact_blocks);
|
|
|
|
// Nothing excluded except the padding bits of the last word - the same starting point as
|
|
// ImageSpotFinder's own mask, so the two agree even when no resolution mask is ever set.
|
|
std::vector<uint32_t> mask(nwords, 0);
|
|
const size_t npixel = static_cast<size_t>(in_width) * in_height;
|
|
if (npixel % 32 != 0)
|
|
mask.back() = ~((1u << (npixel % 32)) - 1u);
|
|
// On this engine's stream, then synchronised - the default stream is non-blocking, so a NULL-stream
|
|
// copy is not ordered against the kernels that read this mask.
|
|
cuda_err(cudaMemcpyAsync(gpu_res_mask, mask.data(), nwords * sizeof(uint32_t),
|
|
cudaMemcpyHostToDevice, *stream));
|
|
cuda_err(cudaStreamSynchronize(*stream));
|
|
}
|
|
|
|
void SpotExtractorGPU::SetResolutionMask(const std::vector<uint32_t> &packed_mask) {
|
|
if (packed_mask.size() != nwords)
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
|
"SpotExtractorGPU::SetResolutionMask: mask size mismatch");
|
|
cuda_err(cudaMemcpyAsync(gpu_res_mask, packed_mask.data(), nwords * sizeof(uint32_t),
|
|
cudaMemcpyHostToDevice, *stream));
|
|
cuda_err(cudaStreamSynchronize(*stream));
|
|
}
|
|
|
|
void SpotExtractorGPU::Extract(const uint32_t *gpu_strong, const int32_t *gpu_image,
|
|
const SpotFindingSettings &settings, std::vector<DiffractionSpot> &spots) {
|
|
const int max_pix = static_cast<int>(settings.max_pix_per_spot);
|
|
|
|
count_bits<<<compact_blocks, THREADS, 0, *stream>>>(gpu_strong, gpu_res_mask, gpu_block_count, nwords);
|
|
cuda_err(cudaGetLastError());
|
|
scan_block_counts<<<1, FINISH_THREADS, 0, *stream>>>(gpu_block_count, gpu_block_offset, gpu_nstrong,
|
|
compact_blocks);
|
|
cuda_err(cudaGetLastError());
|
|
scatter_bits<<<compact_blocks, 32, 0, *stream>>>(gpu_strong, gpu_res_mask, gpu_block_offset, gpu_image,
|
|
gpu_index, gpu_value, nwords, max_strong);
|
|
cuda_err(cudaGetLastError());
|
|
// Fixed grids reading the strong-pixel count from device memory: the host never learns it, so it
|
|
// never has to synchronise in the middle of the frame.
|
|
init_parent<<<512, THREADS, 0, *stream>>>(gpu_parent, gpu_nstrong, max_strong);
|
|
cuda_err(cudaGetLastError());
|
|
union_neighbours<<<512, THREADS, 0, *stream>>>(gpu_index, gpu_parent, gpu_nstrong, max_strong, width);
|
|
cuda_err(cudaGetLastError());
|
|
resolve_roots<<<512, THREADS, 0, *stream>>>(gpu_parent, gpu_root, gpu_nstrong, max_strong);
|
|
cuda_err(cudaGetLastError());
|
|
label_components<<<1, FINISH_THREADS, 0, *stream>>>(gpu_root, gpu_label, gpu_spot, gpu_nlabel, gpu_nspot,
|
|
gpu_nstrong, max_strong);
|
|
cuda_err(cudaGetLastError());
|
|
init_boxes<<<512, THREADS, 0, *stream>>>(gpu_box, gpu_nlabel);
|
|
cuda_err(cudaGetLastError());
|
|
sum_components<<<512, THREADS, 0, *stream>>>(gpu_index, gpu_value, gpu_root, gpu_label, gpu_spot, gpu_box,
|
|
gpu_nstrong, max_strong, width);
|
|
cuda_err(cudaGetLastError());
|
|
filter_components<<<1, FINISH_THREADS, 0, *stream>>>(gpu_spot, gpu_box, gpu_spot_out, gpu_nspot, gpu_nlabel,
|
|
max_pix, static_cast<int>(SPOT_SHAPE_FREE_PIXELS),
|
|
static_cast<int>(SPOT_MIN_FILL_PERCENT));
|
|
cuda_err(cudaGetLastError());
|
|
// Rides along with the spot count on the frame's one synchronisation, so knowing how many strong
|
|
// pixels there were costs nothing.
|
|
cuda_err(cudaMemcpyAsync(host_nstrong, gpu_nstrong, sizeof(uint32_t), cudaMemcpyDeviceToHost, *stream));
|
|
cuda_err(cudaMemcpyAsync(host_nspot, gpu_nspot, sizeof(uint32_t), cudaMemcpyDeviceToHost, *stream));
|
|
cuda_err(cudaMemcpyAsync(host_spot, gpu_spot_out, SPOT_PREFIX * sizeof(SpotExtractorGPUSpot),
|
|
cudaMemcpyDeviceToHost, *stream));
|
|
cuda_err(cudaStreamSynchronize(*stream)); // the only synchronisation in the frame
|
|
|
|
const uint32_t nspot = *host_nspot.get();
|
|
const SpotExtractorGPUSpot *s = host_spot.get();
|
|
if (nspot > SPOT_PREFIX) {
|
|
overflow_spot.resize(nspot);
|
|
cuda_err(cudaMemcpyAsync(overflow_spot.data(), gpu_spot_out, nspot * sizeof(SpotExtractorGPUSpot),
|
|
cudaMemcpyDeviceToHost, *stream));
|
|
cuda_err(cudaStreamSynchronize(*stream));
|
|
s = overflow_spot.data();
|
|
}
|
|
|
|
spots.clear();
|
|
spots.reserve(nspot);
|
|
for (uint32_t i = 0; i < nspot; i++)
|
|
spots.emplace_back(s[i].x, s[i].y, s[i].pixel_count, s[i].photons, s[i].max_photons);
|
|
}
|