OpenVDB 13.0.1
Loading...
Searching...
No Matches
MaskPrefixSum.h
Go to the documentation of this file.
1// Copyright Contributors to the OpenVDB Project
2// SPDX-License-Identifier: Apache-2.0
3
4/*!
5 \file nanovdb/util/MaskPrefixSum.h
6
7 \author Efty Sifakis
8
9 \date March 23, 2026
10
11 \brief Bit-parallel inclusive prefix-sum over a NanoVDB Mask<3>.
12
13 \details Computes a 512-entry uint16_t table where entry i holds the
14 inclusive prefix popcount of the leaf's valueMask up to and including
15 voxel i. That is:
16
17 offsets[i] = number of active voxels at positions 0..i (inclusive)
18
19 The function is intentionally CPU-only; a GPU equivalent would look
20 fundamentally different and is not provided here.
21*/
22
23#ifndef NANOVDB_UTIL_MASKPREFIXSUM_H_HAS_BEEN_INCLUDED
24#define NANOVDB_UTIL_MASKPREFIXSUM_H_HAS_BEEN_INCLUDED
25
26#include <nanovdb/NanoVDB.h>
27
28namespace nanovdb {
29
30namespace util {
31
32namespace {
33
34/// @brief Union giving byte-accessible view of a uint64_t.
35union QWord {
36 uint64_t ui64;
37 uint8_t ui8[8];
38};
39
40/// @brief Broadcast constant: LSB of each byte in a uint64_t.
41static constexpr uint64_t kSpread = UINT64_C(0x0101010101010101);
42
43/// @brief Transpose a single byte-row of an 8x8 bit matrix into a byte-column.
44///
45/// @details Treats the low 8 bits of \p src as the first row of an 8x8 bit
46/// matrix and returns the result of transposing it: bit z of the input byte
47/// becomes the LSB of output byte z. Equivalently:
48///
49/// output.ui8[z] = (src >> z) & 1 for z = 0..7
50///
51/// Equivalent to _pdep_u64(src & 0xFF, kSpread) on x86. Implemented via
52/// portable arithmetic so that the constituent shift/mask ops vectorize
53/// across the y-loop in buildMaskPrefixSums; _pdep_u64 has no SIMD equivalent.
54///
55/// Two stages:
56/// Stage 1: scatter bit-pairs (b1b0, b3b2, b5b4, b7b6) into 16-bit lanes.
57/// Stage 2: space the odd bit of each pair into its own byte lane.
58///
59/// @param src Any uint64_t; only the low 8 bits are used.
60/// @return uint64_t with bit z of (src & 0xFF) in byte z.
61inline uint64_t transposeByteRow(uint64_t src)
62{
63 uint64_t v = src & 0xFFu;
64 // Stage 1: scatter bit-pairs (b1b0, b3b2, b5b4, b7b6) into 16-bit lanes.
65 v = (v | (v << 14) | (v << 28) | (v << 42)) & UINT64_C(0x0003000300030003);
66 // Stage 2: space the odd bit of each pair into its own byte lane.
67 v = (v | (v << 7)) & UINT64_C(0x0101010101010101);
68 return v;
69}
70
71} // anonymous namespace
72
73// --------------------------> buildMaskPrefixSums <------------------------------------------
74
75/// @brief Compute the 512-entry inclusive prefix-sum table for a NanoVDB
76/// Mask<3> leaf, optionally over the inverted mask.
77///
78/// @details
79/// When @p Invert is false (the default), each entry offsets[i] equals the
80/// number of active (set) voxels at linearised positions 0..i (inclusive).
81/// When @p Invert is true, offsets[i] equals the number of inactive (unset)
82/// voxels at positions 0..i (inclusive) -- equivalently, the result of
83/// running the algorithm on the bitwise complement of the mask.
84///
85/// The algorithm is a three-pass bit-parallel scan that operates entirely
86/// on 64 uint64_t values (the 8x8 workspace data[x][y]) without any
87/// hardware popcount instruction.
88///
89/// @tparam Invert If true, count inactive (0) bits instead of active (1) bits.
90/// @param mask The 8x8x8 leaf value mask (512 bits, 8 uint64_t words).
91/// @param prefixSum Packed cross-word exclusive offsets: 7 x 9-bit fields,
92/// field x-1 = total active voxels in words 0..x-1, for
93/// x = 1..7. This is the value stored in the leaf's
94/// mPrefixSum member. Always the original (non-inverted)
95/// field; the function adjusts it internally when Invert=true.
96/// @param offsets Output array of 512 uint16_t values. offsets[i] holds
97/// the inclusive prefix popcount (of active or inactive bits)
98/// at voxel i.
99template <bool Invert = false>
101 const Mask<3>& mask,
102 uint64_t prefixSum,
103 uint16_t offsets[512])
104{
105 const uint64_t *maskWords = mask.words();
106
107 alignas(64) QWord data[8][8];
108
109 // ------------------------------------------------------------------
110 // Step 1: Indicator fill.
111 //
112 // data[x][y].ui8[z] = 1 if bit (y*8+z) of maskWords[x] is set, else 0.
113 //
114 // transposeByteRow(maskWords[x] >> (y*8)) extracts byte y of word x and
115 // places bit z of that byte into byte z of the result. The y-loop
116 // is independent for fixed x and vectorizable via #pragma omp simd.
117 // ------------------------------------------------------------------
118 for (int x = 0; x < 8; x++) {
119 const uint64_t word = Invert ? ~maskWords[x] : maskWords[x];
120 #pragma omp simd
121 for (int y = 0; y < 8; y++)
122 data[x][y].ui64 = transposeByteRow(word >> (y * 8));
123 }
124
125 // ------------------------------------------------------------------
126 // Step 2: Z-pass -- Hillis-Steele inclusive prefix sum over z.
127 //
128 // Three shift-and-add steps perform a length-8 inclusive scan within
129 // each byte lane independently. No inter-byte carry occurs: values
130 // enter as 0/1 and reach at most 8.
131 //
132 // After: data[x][y].ui8[z] = \sum_{z'=0..z} indicator(x, y, z')
133 // i.e. the partial popcount over z-positions 0..z within row (x, y).
134 // data[x][y].ui8[7] = full popcount of row (x, y).
135 // ------------------------------------------------------------------
136 for (int x = 0; x < 8; x++) {
137 #pragma omp simd
138 for (int y = 0; y < 8; y++) {
139 uint64_t& zRow = data[x][y].ui64;
140 zRow += zRow << 8;
141 zRow += zRow << 16;
142 zRow += zRow << 32;
143 }
144 }
145
146 // ------------------------------------------------------------------
147 // Step 3: Y-pass -- accumulate preceding-row popcounts.
148 //
149 // Extract full-row popcounts (byte 7 of each post-Z-pass word) into
150 // byte 0, run an exclusive prefix scan over y (sequential in y,
151 // independent across x), then broadcast each row offset to all byte
152 // lanes via * kSpread and add to data[x][y].
153 //
154 // After: data[x][y].ui8[z] = (\sum_{y'<y} popcount(row y' of word x))
155 // + (\sum_{z'=0..z} indicator(x, y, z'))
156 // = inclusive prefix sum within word x at position y*8+z.
157 // ------------------------------------------------------------------
158
159 // extract full-row popcounts into byte 0
160 alignas(64) QWord shifts[8][8];
161 for (int x = 0; x < 8; x++) {
162 #pragma omp simd
163 for (int y = 0; y < 8; y++)
164 shifts[x][y].ui64 = data[x][y].ui64 >> 56; // = data[x][y].ui8[7]
165 }
166
167 // exclusive y-prefix scan (sequential over y, independent over x)
168 alignas(64) QWord rowOffset[8][8];
169 for (int x = 0; x < 8; x++)
170 rowOffset[x][0].ui64 = 0;
171 for (int y = 1; y < 8; y++)
172 for (int x = 0; x < 8; x++)
173 rowOffset[x][y].ui64 = rowOffset[x][y-1].ui64 + shifts[x][y-1].ui64;
174
175 // broadcast row offsets to all byte lanes and add
176 for (int x = 0; x < 8; x++) {
177 #pragma omp simd
178 for (int y = 0; y < 8; y++)
179 data[x][y].ui64 += rowOffset[x][y].ui64 * kSpread;
180 }
181
182 // ------------------------------------------------------------------
183 // Step 4: Zero-extend bytes to uint16_t in linear index order.
184 //
185 // data[x][y].ui8[z] -> offsets[x*64 + y*8 + z]
186 //
187 // Values are at most 64 (at most 64 active bits per 64-bit word),
188 // so zero-extending to uint16_t is always safe. The output is already
189 // in the correct linear order -- no reordering is required.
190 // ------------------------------------------------------------------
191 for (int x = 0; x < 8; x++)
192 for (int y = 0; y < 8; y++)
193 for (int z = 0; z < 8; z++)
194 offsets[x*64 + y*8 + z] = data[x][y].ui8[z];
195
196 // ------------------------------------------------------------------
197 // Step 5: Add cross-word offsets decoded from prefixSum.
198 //
199 // Unpack prefixSum into xOffset[x]: the exclusive cumulative popcount
200 // up to word x. Each of the 64 uint16_t entries in slice x is
201 // incremented by xOffset[x].
202 // ------------------------------------------------------------------
203 uint16_t xOffset[8];
204 xOffset[0] = 0;
205 for (int x = 1; x < 8; x++) {
206 const uint16_t ones = static_cast<uint16_t>((prefixSum >> (9*(x-1))) & 0x1FFu);
207 if constexpr (Invert)
208 xOffset[x] = static_cast<uint16_t>(64 * x) - ones;
209 else
210 xOffset[x] = ones;
211 }
212
213 for (int x = 0; x < 8; x++) {
214 uint16_t *p = offsets + x * 64;
215 for (int i = 0; i < 64; i++)
216 p[i] += xOffset[x];
217 }
218} // util::buildMaskPrefixSums
219
220} // namespace util
221
222} // namespace nanovdb
223
224#endif // NANOVDB_UTIL_MASKPREFIXSUM_H_HAS_BEEN_INCLUDED
Implements a light-weight self-contained VDB data-structure in a single file! In other words,...
Bit-mask to encode active states and facilitate sequential iterators and a fast codec for I/O compres...
Definition NanoVDB.h:1068
__hostdev__ uint64_t * words()
Return a pointer to the list of words of the bit mask.
Definition NanoVDB.h:1192
Definition ForEach.h:29
void buildMaskPrefixSums(const Mask< 3 > &mask, uint64_t prefixSum, uint16_t offsets[512])
Compute the 512-entry inclusive prefix-sum table for a NanoVDB Mask<3> leaf, optionally over the inve...
Definition MaskPrefixSum.h:100
T prefixSum(std::vector< T > &vec, bool threaded=true, OpT op=OpT())
Computes inclusive prefix sum of a vector.
Definition PrefixSum.h:74
Definition GridHandle.h:27