peclet.voro 1.0.0
Device-native moving-particle Voronoi dynamics
Loading...
Searching...
No Matches
tessellator.hpp
Go to the documentation of this file.
1
26#ifndef PECLET_VORO_TESSELLATOR_HPP
27#define PECLET_VORO_TESSELLATOR_HPP
28
29#include <array>
30#include <cmath>
31#include <cstdint>
32#include <cstdio>
33#include <cstdlib>
34#include <Kokkos_Core.hpp>
35#include <string>
36#include <type_traits>
37#include <vector>
38
39#include "morton/morton.hpp" // suite spatial-index primitive (after Kokkos_Core: MORTON_HD->KOKKOS_FUNCTION)
40#include "peclet/core/common/view.hpp"
42#include "peclet/voro/sdf.hpp"
43#include "peclet/voro/tess_grid.hpp" // counting-sort grid + worklist (kWlOffBias, morton3, TessGrid)
45
46namespace peclet::voro {
47
50 kOk = 0,
52 kEmpty = 2,
54 // Power only: a candidate's radical offset d = ½(|r|²+w_i−w_j) went ≤ 0, i.e. the seed lies
55 // outside its own cell w.r.t. that neighbour. The ConvexCell foot-point half-space {n·x ≤ n·n}
56 // always contains the seed, so it cannot represent such a plane; the cut is skipped and the cell
57 // flagged (never silently mis-clipped). Full support needs a generalized (origin-excluding)
58 // half-space representation — a follow-up to this weighted build.
59 kNegOffset = 8
60};
61
62template <class Real>
65 Kokkos::View<int*, peclet::core::MemSpace> status; // per-cell StatusBit mask
66};
67
77template <class Real, bool Weighted, class Sdf, bool TrackAdj = false>
79 using MemSpace = peclet::core::MemSpace;
80 static constexpr int kMaxP = 64; // plane cap (overflow -> kOverflow)
81 static constexpr int kMaxT = 112; // dual-triangle (vertex) cap
82 static constexpr int MAXF_TMP = 50; // max facets one cell may publish
84 using PlanePolicy = std::conditional_t<Weighted, Power, Voronoi>; // radical vs bisector planes
85
86 // Grid-sorted inputs (read).
87 Kokkos::View<int*, MemSpace> binned;
88 Kokkos::View<Real*, MemSpace> posSorted;
89 Kokkos::View<Real*, MemSpace> wSorted;
90 Kokkos::View<gid_t*, MemSpace> gidSorted;
91 Kokkos::View<int*, MemSpace> cellStart;
92 // Per-sub-position worklist (both backends): for the wlS^3 sub-region a seed lands in, the
93 // block offsets (packed (dx,dy,dz), kWlOffBias) sorted by nearest-corner dist^2 (wlRmin,
94 // absolute). The Voronoi gather walks the presorted list and breaks once wlRmin exceeds the
95 // security radius — a table lookup, no per-block geometry. ConvexCell is lean enough that
96 // the worklist runs on the GPU too (its geometry pass is cheap, so no occupancy penalty).
97 Kokkos::View<int*, MemSpace> wlOff;
98 Kokkos::View<Real*, MemSpace> wlRmin;
99 // Published outputs (write).
100 Kokkos::View<int*, MemSpace> status;
101 Kokkos::View<Real*, MemSpace> cellVol;
102 Kokkos::View<int*, MemSpace> facetCount;
103 Kokkos::View<int*, MemSpace> cellFacetBase;
104 Kokkos::View<int*, MemSpace> oNbr;
105 Kokkos::View<Real*, MemSpace> oArea, oDV, oConn;
106 Kokkos::View<int*, MemSpace> facetCursor;
107 // Grid scalars.
108 Real icx, icy, icz, Lx, Ly, Lz, minCsz;
109 Real wMaxAll; // global max weight (Power only) — bounds the weight-aware worklist reach
112 size_t facetCap;
113 Sdf sdf;
114 // Optional Part-II (moving-points) outputs — emitted only when the views are sized (else left
115 // empty so existing callers are unaffected): the compact resident TOPOLOGY store (np/nt +
116 // pnbr/packed-triangles at kMaxP/kMaxT strides, written from the final clipped cell) and the
117 // per-cell CANDIDATE (skin) list = the neighbour ids this cell examined (within the worklist's
118 // security reach), capped at candCap. Together they let a later step re-evaluate geometry
119 // (ConvexCell::reevalGeometry) and locally repair off the skin list without re-gathering. See
120 // peclet/voro/docs/voronoi_dynamic_update_study.md.
121 Kokkos::View<int*, MemSpace> oNp, oNt, oTopoPnbr;
122 Kokkos::View<unsigned*, MemSpace> oTri;
123 Kokkos::View<unsigned char*, MemSpace>
124 oPoke4; // N*kMaxT*4 local-cert plane set (3 edge-opposite +
125 // 4th-face), emitted only when TrackAdj && emitTopo
126 Kokkos::View<int*, MemSpace> oCand, oCandCnt;
129
131 KOKKOS_INLINE_FUNCTION void relVec(int q, Real pix, Real piy, Real piz, Real pv[3]) const {
132 Real rx = posSorted(3 * q + 0) - pix, ry = posSorted(3 * q + 1) - piy,
133 rz = posSorted(3 * q + 2) - piz;
134 const Real Lxh = Real(0.5) * Lx, Lyh = Real(0.5) * Ly, Lzh = Real(0.5) * Lz;
135 pv[0] = rx > Lxh ? rx - Lx : (rx < -Lxh ? rx + Lx : rx);
136 pv[1] = ry > Lyh ? ry - Ly : (ry < -Lyh ? ry + Ly : ry);
137 pv[2] = rz > Lzh ? rz - Lz : (rz < -Lzh ? rz + Lz : rz);
138 }
139
141 KOKKOS_INLINE_FUNCTION int gridCell(int rgx, int rgy, int rgz) const {
142 int gx = rgx < 0 ? rgx + dimx : (rgx >= dimx ? rgx - dimx : rgx);
143 int gy = rgy < 0 ? rgy + dimy : (rgy >= dimy ? rgy - dimy : rgy);
144 int gz = rgz < 0 ? rgz + dimz : (rgz >= dimz ? rgz - dimz : rgz);
145 return useMorton ? morton3(gx, gy, gz) : (gx + gy * dimx + gz * dimx * dimy);
146 }
147
149 KOKKOS_INLINE_FUNCTION void homeCell(Real pix, Real piy, Real piz, int& cx, int& cy,
150 int& cz) const {
151 cx = ((((int)Kokkos::floor(pix * icx)) % dimx) + dimx) % dimx;
152 cy = ((((int)Kokkos::floor(piy * icy)) % dimy) + dimy) % dimy;
153 cz = ((((int)Kokkos::floor(piz * icz)) % dimz) + dimz) % dimz;
154 }
155
157 KOKKOS_INLINE_FUNCTION int subBase(Real pix, Real piy, Real piz) const {
158 const Real fxi = pix * icx, fyi = piy * icy, fzi = piz * icz;
159 int sx = (int)((fxi - Kokkos::floor(fxi)) * Real(wlS));
160 int sy = (int)((fyi - Kokkos::floor(fyi)) * Real(wlS));
161 int sz = (int)((fzi - Kokkos::floor(fzi)) * Real(wlS));
162 sx = sx < 0 ? 0 : (sx >= wlS ? wlS - 1 : sx);
163 sy = sy < 0 ? 0 : (sy >= wlS ? wlS - 1 : sy);
164 sz = sz < 0 ? 0 : (sz >= wlS ? wlS - 1 : sz);
165 return (sx + wlS * (sy + wlS * sz)) * nOff;
166 }
167
169 KOKKOS_INLINE_FUNCTION int worklistCell(int base, int g, int cx, int cy, int cz) const {
170 const int packed = wlOff(base + g);
171 const int rgx = cx + ((packed & 0xFF) - kWlOffBias);
172 const int rgy = cy + (((packed >> 8) & 0xFF) - kWlOffBias);
173 const int rgz = cz + (((packed >> 16) & 0xFF) - kWlOffBias);
174 return gridCell(rgx, rgy, rgz);
175 }
176
180 KOKKOS_INLINE_FUNCTION void finishCell(Cell& c, int i, Real pix, Real piy, Real piz, Real covSq,
181 Real wSelf = Real(0)) const {
182 if (c.overflow) {
183 status(i) = kOverflow;
184 facetCount(i) = 0;
185 cellFacetBase(i) = 0;
186 cellVol(i) = Real(0);
187 return;
188 }
189 // Complete iff the gathered coverage inscribes past the reachability radius. Voronoi:
190 // blockReachSq = 4·rSqMax (unchanged). Power: the larger weight-aware reach, so a cell whose
191 // heavy far neighbour lies beyond the coverage is correctly flagged kIncomplete.
192 const bool incomplete =
193 !(covSq > PlanePolicy::template blockReachSq<Real>(c.maxVertexRsq(), wSelf, wMaxAll));
194 (void)sdf;
195 if constexpr (!std::is_same_v<Sdf, NoSdf>) {
196 const Real seedW[3] = {pix, piy, piz};
197 clipCellAgainstSdf<Real, kMaxP, kMaxT>(c, seedW, sdf);
198 }
199 int st = kOk;
200 if (c.overflow)
201 st |= kOverflow;
202 const bool empty = c.empty();
203 if (empty)
204 st |= kEmpty;
205 if (incomplete)
206 st |= kIncomplete;
207 cellVol(i) = (empty || c.overflow) ? Real(0) : c.volumePerVertex();
208
209 // Part-II: persist the final (post-SDF-clip) topology so a later step can re-eval/repair
210 // without a rebuild.
211 if (emitTopo && !empty && !c.overflow) {
212 oNp(i) = c.np;
213 oNt(i) = c.nt;
214 for (int k = 0; k < c.np; ++k)
215 oTopoPnbr[(size_t)i * kMaxP + k] = c.pnbr[k];
216 for (int t = 0; t < c.nt; ++t)
217 oTri[(size_t)i * kMaxT + t] = (unsigned)c.t0[t] | ((unsigned)c.t1[t] << 8) |
218 ((unsigned)c.t2[t] << 16) | ((c.alive[t] ? 1u : 0u) << 24);
219 if constexpr (TrackAdj) // derive the local-cert plane set from the clip-maintained adj (no
220 // findSharing)
221 c.computePoke4(&oPoke4[(size_t)i * kMaxT * 4]);
222 }
223
224 // Collect this cell's live faces (a plane with >=3 incident live triangles), then reserve
225 // a contiguous CSR range with one atomic and write the per-facet geometry straight in.
226 int faces[MAXF_TMP];
227 int nf = 0;
228 if (!empty && !c.overflow) {
229 for (int k = 0; k < c.np; ++k) {
230 int cnt = 0;
231 for (int t = 0; t < c.nt; ++t)
232 if (c.alive[t] && (c.t0[t] == k || c.t1[t] == k || c.t2[t] == k))
233 ++cnt;
234 if (cnt < 3)
235 continue; // not a polygon face
236 if (nf >= MAXF_TMP) {
237 st |= kOverflow;
238 break;
239 }
240 faces[nf++] = k;
241 }
242 }
243 status(i) = st;
244 const int base = Kokkos::atomic_fetch_add(&facetCursor(0), nf);
245 if ((size_t)base + (size_t)nf > facetCap) {
246 status(i) |= kOverflow;
247 facetCount(i) = 0;
248 cellFacetBase(i) = 0;
249 return;
250 }
251 facetCount(i) = nf;
252 cellFacetBase(i) = base;
253 for (int idx = 0; idx < nf; ++idx) {
254 const int k = faces[idx];
255 Real area[3] = {0, 0, 0}, dv[3] = {0, 0, 0}, conn[3];
256 conn[0] = Real(2) * c.n[k][0];
257 conn[1] = Real(2) * c.n[k][1];
258 conn[2] = Real(2) * c.n[k][2];
259 if (withForceGeom)
260 c.facetGeometry(k, area, dv, conn); // area vector + dV + connector
261 oNbr((size_t)base + idx) = c.pnbr[k];
262 const size_t o = ((size_t)base + idx) * 3;
263 for (int cc = 0; cc < 3; ++cc) {
264 oArea(o + cc) = area[cc];
265 oDV(o + cc) = dv[cc];
266 oConn(o + cc) = conn[cc];
267 }
268 }
269 }
270
273 KOKKOS_INLINE_FUNCTION void buildCell(int pi) const {
274 // Voronoi (Weighted=false) uses the bisector plane + security-radius early-out. Power
275 // (Weighted=true) uses the radical plane offset ½(|r|²+w_self−w_nbr) — which can be negative
276 // (the seed can lie OUTSIDE its own cell) so the bisector security certificate is invalid.
277 // P1 keeps the trivially-correct APPLY-ALL path for Power (every worklist candidate, no
278 // early-out); the weight-aware security gate is P2.
279 const int i = binned(pi);
280 const Real pix = posSorted(3 * pi + 0), piy = posSorted(3 * pi + 1),
281 piz = posSorted(3 * pi + 2);
282 int cx, cy, cz;
283 homeCell(pix, piy, piz, cx, cy, cz);
284 const int base = subBase(pix, piy, piz);
285 Cell c;
286 c.initBox(Lx, Ly, Lz);
287 Real wSelf = Real(0);
288 if constexpr (Weighted) wSelf = wSorted(pi);
289 bool buried = false; // Power: some neighbour dominates the seed at its own location (d≤0)
290
291 // The worklist is sorted by nearest-corner dist², so once wlRmin exceeds the reachability radius
292 // every remaining block is too far to cut — break. Voronoi keeps the exact bisector certificate
293 // (secR2 = 2·rSqMax, break at wlRmin > 2·secR2 = 4·rSqMax) so the unweighted path stays
294 // byte-identical. Power uses the weight-aware reach (blockReachSq) with the global max weight.
295 Real secR2 = Real(2) * c.maxVertexRsq(); // Voronoi
296 Real reachSq = PlanePolicy::template blockReachSq<Real>(c.maxVertexRsq(), wSelf, wMaxAll); // Pow
297 int ncRec = 0; // Part-II: count of recorded candidate (skin) ids for this cell
298 for (int g = 0; g < nOff && !c.overflow && !buried; ++g) {
299 if constexpr (Weighted) {
300 if (wlRmin(base + g) > reachSq) break;
301 } else {
302 if (wlRmin(base + g) > Real(2) * secR2) break; // sorted ⇒ rest are farther; cell closed
303 }
304 const int gc = worklistCell(base, g, cx, cy, cz);
305 for (int q = cellStart(gc); q < cellStart(gc + 1) && !c.overflow && !buried; ++q) {
306 if (q == pi)
307 continue;
308 if (haveGid && gidSorted(q) == gidSorted(pi))
309 continue;
310 // record the examined neighbour as a skin candidate (within the worklist's security reach),
311 // capped.
312 if (emitCand && ncRec < candCap)
313 oCand[(size_t)i * candCap + ncRec++] = binned(q);
314 Real pv[3];
315 relVec(q, pix, piy, piz, pv);
316 Real wNbr = Real(0);
317 if constexpr (Weighted) wNbr = wSorted(q);
318 const Real off = PlanePolicy::template offsetFromRel<Real>(pv, wSelf, wNbr);
319 if constexpr (Weighted) {
320 // Power: d = ½(|r|²+w_i−w_j) ≤ 0 means neighbour j has lower power than i at the seed's
321 // own location — the seed is not in its own power cell, so cell i is BURIED (empty). The
322 // foot-point half-space cannot represent this plane anyway; empty the cell and stop.
323 if (off <= Real(0)) {
324 buried = true;
325 break;
326 }
327 // Conservative cull: r·v ≤ |r|·√rSqMax, so |r|²·rSqMax ≤ d² ⇒ the plane cannot cut.
328 const Real rho = pv[0] * pv[0] + pv[1] * pv[1] + pv[2] * pv[2];
329 if (rho * c.maxVertexRsq() <= off * off)
330 continue; // provably beyond reach
331 if (c.clip(pv, off, binned(q)))
332 reachSq = PlanePolicy::template blockReachSq<Real>(c.maxVertexRsq(), wSelf, wMaxAll);
333 } else {
334 if (off >= secR2)
335 continue; // beyond the radius: cannot cut (bisector certificate)
336 if (c.clip(pv, off, binned(q)))
337 secR2 = Real(2) * c.maxVertexRsq();
338 }
339 }
340 }
341 if constexpr (Weighted) {
342 if (buried) { // buried power cell: emit as empty (kEmpty), like an in-solid SDF seed
343 status(i) = kEmpty;
344 facetCount(i) = 0;
345 cellFacetBase(i) = 0;
346 cellVol(i) = Real(0);
347 return;
348 }
349 }
350 if (emitCand)
351 oCandCnt(i) = ncRec;
352 // Completeness uses the conservative inscribed-sphere coverage (sw·minCsz)², the same
353 // criterion the legacy gather used: complete iff (sw·minCsz)² > 4·rSqMax.
354 const Real covSq = Real(sw) * minCsz * Real(sw) * minCsz;
355 finishCell(c, i, pix, piy, piz, covSq, wSelf);
356 }
357};
358
380template <class Real, bool Weighted, class Sdf = NoSdf>
382 const Kokkos::View<Real*, peclet::core::MemSpace>& posFlat,
383 const Kokkos::View<Real*, peclet::core::MemSpace>& weight, int N, const Real L[3], int sw = 4,
384 int densityCount = -1, Kokkos::View<long*, peclet::core::MemSpace> gid = {}, Sdf sdf = {},
385 bool withForceGeom = true, int nBuild = -1,
386 Kokkos::View<int*, peclet::core::MemSpace> outNp = {},
387 Kokkos::View<int*, peclet::core::MemSpace> outNt = {},
388 Kokkos::View<int*, peclet::core::MemSpace> outPnbr = {},
389 Kokkos::View<unsigned*, peclet::core::MemSpace> outTri = {},
390 Kokkos::View<int*, peclet::core::MemSpace> outCand = {},
391 Kokkos::View<int*, peclet::core::MemSpace> outCandCnt = {}, int candCap = 0,
392 WorklistCache<Real>* wlc = nullptr) {
393 using peclet::core::MemSpace;
394 using Exec = peclet::core::ExecSpace;
395 // Part-II optional outputs (see CellBuilder): emit the resident topology store / candidate skin
396 // list only when the caller supplies sized views (so existing callers, passing none, are
397 // byte-for-byte unaffected).
398 const bool emitTopo = outNp.extent(0) == static_cast<size_t>(N);
399 const bool emitCand = outCand.extent(0) > 0 && candCap > 0;
400 // Optional global ids: skip a candidate sharing the cell's own id (its periodic
401 // self-image, which can wrap exactly onto the seed -> a degenerate zero-distance
402 // cut). Mirrors the legacy processNbrs `itr->id == m_id` guard. In the
403 // single-domain case (gid empty) only the same local index is skipped.
404 const bool haveGid = gid.extent(0) == static_cast<size_t>(N);
405 // Seeds with original index >= nBuildEff are candidate-only (their cell is skipped).
406 const int nBuildEff = (nBuild >= 0 && nBuild < N) ? nBuild : N;
407
408 const bool prof = std::getenv("PECLET_VORO_PROFILE") != nullptr;
409 Kokkos::Timer ptimer;
410 double tGrid = 0, tBuild = 0, tCsr = 0;
411
412 // Counting-sort grid + presorted worklist. Factored into buildTessGrid so the SAME grid backs
413 // both this cold build and the moving-point subset gather (device/subset_gather.hpp); pure code
414 // motion, so the cold-build output is byte-for-byte unchanged.
415 auto grid = buildTessGrid<Real, Weighted>(posFlat, weight, N, L, sw, densityCount, gid, wlc);
416 const Real Lx = grid.Lx, Ly = grid.Ly, Lz = grid.Lz;
417 const Real icx = grid.icx, icy = grid.icy, icz = grid.icz, minCsz = grid.minCsz;
418 const int dimx = grid.dimx, dimy = grid.dimy, dimz = grid.dimz;
419 const int nOff = grid.nOff, wlS = grid.wlS;
420 const bool useMorton = grid.useMorton;
421
422 if (prof) {
423 Kokkos::fence();
424 tGrid = ptimer.seconds();
425 ptimer.reset();
426 }
427
428 // --- pass 1: build each cell, record volume / facet count / status and the
429 // per-cell facet records (neighbour id + area vector) into a temp slab ---
430 Kokkos::View<Real*, MemSpace> cellVol("cellVol", N);
431 Kokkos::View<int*, MemSpace> facetCount("facetCount", N);
432 Kokkos::View<int*, MemSpace> status("status", N);
433 Kokkos::View<int*, MemSpace> cellFacetBase("cellFacetBase", N); // per-cell facet base
434 // Facet over-buffer + atomic cursor: the build packs each cell's facets directly into
435 // a single contiguous CSR-layout buffer via atomic_fetch_add (one-pass "fusion") — no
436 // fixed-stride temp slab, no exclusive scan, and the connecting vector is the cut's own
437 // plane vector (no minimal-image recompute). The buffer is over-allocated to a tight
438 // facet cap; the used prefix [0,nFacets) is copied to a compact view at the end.
439 // Per-cell facet cap: the most facets one Voronoi/Power cell may publish (a single
440 // cell rarely exceeds ~40 faces); bounds the per-cell `faces[]` stack array.
441 constexpr int MAXF_TMP = 50;
442 // Global over-buffer capacity: the published CSR holds the *sum* of all cells'
443 // facets ≈ N × mean-faces-per-cell (~15.5 for random Poisson–Voronoi). Sizing it at
444 // N × MAXF_TMP over-allocated ~3× and OOM'd the GPU at large N (≈15 GB at N=4M). A
445 // mean-facet estimate with headroom (N×18, ~16% over the aggregate mean) is ample —
446 // the *sum* has negligible relative variance — and the atomic overflow guard below
447 // flags the rare cell whose reservation would exceed it instead of writing past the
448 // end. (The interleaved copy-and-free in the pack keeps peak memory near this size.)
449 constexpr size_t kMeanFacets = 18;
450 const size_t facetCap = (size_t)N * kMeanFacets;
451 using Kokkos::view_alloc;
452 using Kokkos::WithoutInitializing;
453 Kokkos::View<int*, MemSpace> oNbr(view_alloc(std::string("oNbr"), WithoutInitializing), facetCap);
454 Kokkos::View<Real*, MemSpace> oArea(view_alloc(std::string("oArea"), WithoutInitializing),
455 facetCap * 3);
456 Kokkos::View<Real*, MemSpace> oDV(view_alloc(std::string("oDV"), WithoutInitializing),
457 facetCap * 3);
458 Kokkos::View<Real*, MemSpace> oConn(view_alloc(std::string("oConn"), WithoutInitializing),
459 facetCap * 3);
460 Kokkos::View<int*, MemSpace> facetCursor("facetCursor", 1); // zero-initialised
461 if (prof)
462 std::fprintf(stderr, "[worklist] sw=%d nOff=%d\n", sw, nOff);
463
464 // Build each cell: one thread per cell (RangePolicy), the compact ConvexCell in the
465 // per-thread frame, the cut applied on the fly. Same path on every backend (ConvexCell is
466 // lean enough that the worklist + geometry run well on the GPU without a team variant).
467 Kokkos::View<unsigned char*, MemSpace>
468 noPoke4; // cold build does not emit the cert plane set (TrackAdj=false)
469
470 // Global max weight bounds the weight-aware worklist reach (Power only; 0 for Voronoi).
471 Real wMaxAll = Real(0);
472 if constexpr (Weighted) {
473 if (weight.extent(0) == static_cast<size_t>(N)) {
474 Kokkos::parallel_reduce(
475 "wmax", Kokkos::RangePolicy<Exec>(0, N),
476 KOKKOS_LAMBDA(int idx, Real& m) { m = weight(idx) > m ? weight(idx) : m; },
477 Kokkos::Max<Real>(wMaxAll));
478 }
479 }
480
481 CellBuilder<Real, Weighted, Sdf> op{grid.binned,
482 grid.posSorted,
483 grid.wSorted,
484 grid.gidSorted,
485 grid.cellStart,
486 grid.wlOff,
487 grid.wlRmin,
488 status,
489 cellVol,
490 facetCount,
491 cellFacetBase,
492 oNbr,
493 oArea,
494 oDV,
495 oConn,
496 facetCursor,
497 icx,
498 icy,
499 icz,
500 Lx,
501 Ly,
502 Lz,
503 minCsz,
504 wMaxAll,
505 dimx,
506 dimy,
507 dimz,
508 sw,
509 nOff,
510 wlS,
511 useMorton,
512 haveGid,
513 withForceGeom,
514 facetCap,
515 sdf,
516 outNp,
517 outNt,
518 outPnbr,
519 outTri,
520 noPoke4,
521 outCand,
522 outCandCnt,
523 emitTopo,
524 emitCand,
525 candCap};
526 const int nBuildL = nBuildEff;
527 auto binnedV0 = grid.binned;
528 Kokkos::parallel_for(
529 "tess.build", Kokkos::RangePolicy<Exec>(0, N), KOKKOS_LAMBDA(const int pi) {
530 if (binnedV0(pi) >= nBuildL)
531 return; // candidate-only seed: skip its cell
532 op.buildCell(pi);
533 });
534
535 if (prof) {
536 Kokkos::fence();
537 tBuild = ptimer.seconds();
538 ptimer.reset();
539 }
540
541 // --- pack the over-buffer prefix [0,nFacets) into a compact CSR ---
542 // The atomic packing already produced a gapless CSR (in cell-finish order, with each
543 // cell's facets contiguous at cellFacetBase(i)); we only copy its used prefix into a
544 // right-sized view (a contiguous read+write — no exclusive scan, no strided gather,
545 // no minimal-image recompute that the old temp->CSR compaction paid).
546 int nFacetsRaw = 0;
547 Kokkos::deep_copy(nFacetsRaw, Kokkos::subview(facetCursor, 0));
548 // Clamp to capacity: if the over-buffer overflowed (rare; flagged per-cell above),
549 // the cursor ran past the end and only [0,facetCap) holds valid, indexable facets.
550 const int nFacets = (size_t)nFacetsRaw > facetCap ? (int)facetCap : nFacetsRaw;
551
552 TessellationView<Real> view;
553 view.cellFacetOffset = cellFacetBase;
554 view.cellFacetCount = facetCount;
555 view.cellVolume = cellVol;
556 view.cellSeedId =
557 Kokkos::View<gid_t*, MemSpace>(view_alloc(std::string("cellSeedId"), WithoutInitializing), N);
558 {
559 auto vSeed = view.cellSeedId;
560 Kokkos::parallel_for(
561 "tess.seedId", Kokkos::RangePolicy<Exec>(0, N),
562 KOKKOS_LAMBDA(const int i) { vSeed(i) = (gid_t)i; });
563 }
564
565 // Pack each over-buffer component into a right-sized published view, then free the
566 // source immediately. Copying-and-freeing one array at a time holds peak memory at
567 // ~(over-buffer + one compact array) instead of (over-buffer + full compact CSR) —
568 // the latter OOM'd the GPU at large N. numFacets() == nFacets, so the CSR contract is
569 // intact (consumers see correctly sized views). The fence before each free ensures the
570 // copy completed before the source is released.
571 view.facetNeighbor = Kokkos::View<gid_t*, MemSpace>(
572 view_alloc(std::string("facetNeighbor"), WithoutInitializing), nFacets);
573 {
574 auto v = view.facetNeighbor;
575 auto src = oNbr;
576 Kokkos::parallel_for(
577 "tess.packNbr", Kokkos::RangePolicy<Exec>(0, nFacets),
578 KOKKOS_LAMBDA(const int g) { v(g) = (gid_t)src(g); });
579 }
580 Kokkos::fence();
581 oNbr = Kokkos::View<int*, MemSpace>();
582
583 view.facetArea = Kokkos::View<Real*, MemSpace>(
584 view_alloc(std::string("facetArea"), WithoutInitializing), (size_t)nFacets * 3);
585 {
586 auto v = view.facetArea;
587 auto src = oArea;
588 Kokkos::parallel_for(
589 "tess.packArea", Kokkos::RangePolicy<Exec>(0, nFacets), KOKKOS_LAMBDA(const int g) {
590 for (int cc = 0; cc < 3; ++cc)
591 v(3 * g + cc) = src(3 * (size_t)g + cc);
592 });
593 }
594 Kokkos::fence();
595 oArea = Kokkos::View<Real*, MemSpace>();
596
597 view.facetConnect = Kokkos::View<Real*, MemSpace>(
598 view_alloc(std::string("facetConnect"), WithoutInitializing), (size_t)nFacets * 3);
599 {
600 auto v = view.facetConnect;
601 auto src = oDV;
602 Kokkos::parallel_for(
603 "tess.packDV", Kokkos::RangePolicy<Exec>(0, nFacets), KOKKOS_LAMBDA(const int g) {
604 for (int cc = 0; cc < 3; ++cc)
605 v(3 * g + cc) = src(3 * (size_t)g + cc);
606 });
607 }
608 Kokkos::fence();
609 oDV = Kokkos::View<Real*, MemSpace>();
610
611 view.facetConnVec = Kokkos::View<Real*, MemSpace>(
612 view_alloc(std::string("facetConnVec"), WithoutInitializing), (size_t)nFacets * 3);
613 {
614 auto v = view.facetConnVec;
615 auto src = oConn;
616 Kokkos::parallel_for(
617 "tess.packConn", Kokkos::RangePolicy<Exec>(0, nFacets), KOKKOS_LAMBDA(const int g) {
618 for (int cc = 0; cc < 3; ++cc)
619 v(3 * g + cc) = src(3 * (size_t)g + cc);
620 });
621 }
622 Kokkos::fence();
623 oConn = Kokkos::View<Real*, MemSpace>();
624
625 if (prof) {
626 tCsr = ptimer.seconds();
627 std::fprintf(stderr, "[tess N=%d] grid=%.4fs build=%.4fs csr=%.4fs (build %.0f%%)\n", N, tGrid,
628 tBuild, tCsr, 100.0 * tBuild / (tGrid + tBuild + tCsr));
629 // Max published facets/cell — the empirical bound for sizing a shrunk shared cell
630 // (vertices ~ 2·facets - 4). Reduce over the per-cell facet count.
631 int maxF = 0;
632 auto fc = facetCount;
633 Kokkos::parallel_reduce(
634 "tess.maxFacets", Kokkos::RangePolicy<Exec>(0, N),
635 KOKKOS_LAMBDA(const int c, int& m) { m = fc(c) > m ? fc(c) : m; }, Kokkos::Max<int>(maxF));
636 std::fprintf(stderr, "[tess N=%d] maxFacets/cell=%d (=> maxVerts ~ %d; CAP=%d)\n", N, maxF,
638 }
639
640 TessellatorResult<Real> res;
641 res.view = view;
642 res.status = status;
643 return res;
644}
645
646} // namespace peclet::voro
647
648#endif // PECLET_VORO_TESSELLATOR_HPP
Definition convex_cell.hpp:40
std::int32_t gid_t
Definition tessellation_view.hpp:41
StatusBit
Per-cell status bits written by the build pass.
Definition tessellator.hpp:49
@ kOverflow
Definition tessellator.hpp:51
@ kNegOffset
Definition tessellator.hpp:59
@ kOk
Definition tessellator.hpp:50
@ kIncomplete
Definition tessellator.hpp:53
@ kEmpty
Definition tessellator.hpp:52
constexpr int kWlOffBias
Definition tess_grid.hpp:37
TessellatorResult< Real > buildTessellation(const Kokkos::View< Real *, peclet::core::MemSpace > &posFlat, const Kokkos::View< Real *, peclet::core::MemSpace > &weight, int N, const Real L[3], int sw=4, int densityCount=-1, Kokkos::View< long *, peclet::core::MemSpace > gid={}, Sdf sdf={}, bool withForceGeom=true, int nBuild=-1, Kokkos::View< int *, peclet::core::MemSpace > outNp={}, Kokkos::View< int *, peclet::core::MemSpace > outNt={}, Kokkos::View< int *, peclet::core::MemSpace > outPnbr={}, Kokkos::View< unsigned *, peclet::core::MemSpace > outTri={}, Kokkos::View< int *, peclet::core::MemSpace > outCand={}, Kokkos::View< int *, peclet::core::MemSpace > outCandCnt={}, int candCap=0, WorklistCache< Real > *wlc=nullptr)
Definition tessellator.hpp:381
KOKKOS_INLINE_FUNCTION int morton3(int x, int y, int z)
Definition tess_grid.hpp:45
Definition tessellator.hpp:78
Kokkos::View< int *, MemSpace > facetCursor
Definition tessellator.hpp:106
bool useMorton
Definition tessellator.hpp:111
Kokkos::View< int *, MemSpace > status
Definition tessellator.hpp:100
Kokkos::View< Real *, MemSpace > cellVol
Definition tessellator.hpp:101
Kokkos::View< Real *, MemSpace > wSorted
Definition tessellator.hpp:89
bool emitTopo
Definition tessellator.hpp:127
static constexpr int MAXF_TMP
Definition tessellator.hpp:82
Kokkos::View< unsigned char *, MemSpace > oPoke4
Definition tessellator.hpp:124
KOKKOS_INLINE_FUNCTION int subBase(Real pix, Real piy, Real piz) const
Flat base offset into the worklist tables for the wlS^3 sub-region the seed lands in.
Definition tessellator.hpp:157
int candCap
Definition tessellator.hpp:128
static constexpr int kMaxT
Definition tessellator.hpp:81
Kokkos::View< int *, MemSpace > oCandCnt
Definition tessellator.hpp:126
KOKKOS_INLINE_FUNCTION void buildCell(int pi) const
Definition tessellator.hpp:273
static constexpr int kMaxP
Definition tessellator.hpp:80
Real icx
Definition tessellator.hpp:108
int sw
Definition tessellator.hpp:110
int dimy
Definition tessellator.hpp:110
Kokkos::View< int *, MemSpace > oNbr
Definition tessellator.hpp:104
bool emitCand
Definition tessellator.hpp:127
bool haveGid
Definition tessellator.hpp:111
KOKKOS_INLINE_FUNCTION int gridCell(int rgx, int rgy, int rgz) const
Linear/Morton index of the grid cell at raw (periodic-unwrapped) offset.
Definition tessellator.hpp:141
Kokkos::View< gid_t *, MemSpace > gidSorted
Definition tessellator.hpp:90
int dimx
Definition tessellator.hpp:110
Real Ly
Definition tessellator.hpp:108
Real Lx
Definition tessellator.hpp:108
Real wMaxAll
Definition tessellator.hpp:109
int wlS
Definition tessellator.hpp:110
Kokkos::View< unsigned *, MemSpace > oTri
Definition tessellator.hpp:122
Real Lz
Definition tessellator.hpp:108
std::conditional_t< Weighted, Power, Voronoi > PlanePolicy
Definition tessellator.hpp:84
int nOff
Definition tessellator.hpp:110
Kokkos::View< int *, MemSpace > oNt
Definition tessellator.hpp:121
Sdf sdf
Definition tessellator.hpp:113
Kokkos::View< int *, MemSpace > wlOff
Definition tessellator.hpp:97
Kokkos::View< int *, MemSpace > binned
Definition tessellator.hpp:87
peclet::core::MemSpace MemSpace
Definition tessellator.hpp:79
Kokkos::View< Real *, MemSpace > oArea
Definition tessellator.hpp:105
bool withForceGeom
Definition tessellator.hpp:111
int dimz
Definition tessellator.hpp:110
KOKKOS_INLINE_FUNCTION void relVec(int q, Real pix, Real piy, Real piz, Real pv[3]) const
Minimal-image relative vector from the seed at (pix,piy,piz) to sorted seed q.
Definition tessellator.hpp:131
Kokkos::View< int *, MemSpace > cellStart
Definition tessellator.hpp:91
Kokkos::View< int *, MemSpace > cellFacetBase
Definition tessellator.hpp:103
Kokkos::View< int *, MemSpace > facetCount
Definition tessellator.hpp:102
size_t facetCap
Definition tessellator.hpp:112
Kokkos::View< int *, MemSpace > oTopoPnbr
Definition tessellator.hpp:121
Real icy
Definition tessellator.hpp:108
Kokkos::View< Real *, MemSpace > oConn
Definition tessellator.hpp:105
KOKKOS_INLINE_FUNCTION void finishCell(Cell &c, int i, Real pix, Real piy, Real piz, Real covSq, Real wSelf=Real(0)) const
Definition tessellator.hpp:180
Kokkos::View< Real *, MemSpace > oDV
Definition tessellator.hpp:105
Kokkos::View< int *, MemSpace > oNp
Definition tessellator.hpp:121
Kokkos::View< Real *, MemSpace > wlRmin
Definition tessellator.hpp:98
Real minCsz
Definition tessellator.hpp:108
Real icz
Definition tessellator.hpp:108
Kokkos::View< int *, MemSpace > oCand
Definition tessellator.hpp:126
Kokkos::View< Real *, MemSpace > posSorted
Definition tessellator.hpp:88
KOKKOS_INLINE_FUNCTION int worklistCell(int base, int g, int cx, int cy, int cz) const
Decode worklist entry g (relative to base) into a grid-cell index.
Definition tessellator.hpp:169
KOKKOS_INLINE_FUNCTION void homeCell(Real pix, Real piy, Real piz, int &cx, int &cy, int &cz) const
Home grid cell index (cx,cy,cz) of seed (pix,piy,piz).
Definition tessellator.hpp:149
Definition convex_cell.hpp:139
KOKKOS_INLINE_FUNCTION Real volumePerVertex() const
Definition convex_cell.hpp:1036
unsigned char t1[MAXT]
Definition convex_cell.hpp:150
KOKKOS_INLINE_FUNCTION void initBox(Real L0, Real L1, Real L2)
Definition convex_cell.hpp:173
bool overflow
set if MAXP/MAXT exceeded -> cell invalid, caller falls back
Definition convex_cell.hpp:154
int nt
triangle high-water mark
Definition convex_cell.hpp:153
Real n[MAXP][3]
Definition convex_cell.hpp:144
KOKKOS_INLINE_FUNCTION bool empty() const
Definition convex_cell.hpp:775
int pnbr[MAXP]
neighbour seed id per plane (<0 => bounding box)
Definition convex_cell.hpp:148
KOKKOS_INLINE_FUNCTION Real maxVertexRsq() const
Largest squared dual-vertex radius over live triangles (drives the security radius).
Definition convex_cell.hpp:490
KOKKOS_INLINE_FUNCTION bool clip(const Real pdir[3], Real d, int nbr)
Definition convex_cell.hpp:584
unsigned char t2[MAXT]
triangle = triple of plane indices
Definition convex_cell.hpp:150
int np
number of planes
Definition convex_cell.hpp:149
unsigned char t0[MAXT]
Definition convex_cell.hpp:150
KOKKOS_INLINE_FUNCTION void computePoke4(unsigned char *out) const
Definition convex_cell.hpp:391
bool alive[MAXT]
triangle live flag
Definition convex_cell.hpp:152
KOKKOS_INLINE_FUNCTION bool facetGeometry(int k, Real areaVec[3], Real dv[3], Real conn[3]) const
Definition convex_cell.hpp:1633
Definition tessellation_view.hpp:111
Definition tessellator.hpp:63
Kokkos::View< int *, peclet::core::MemSpace > status
Definition tessellator.hpp:65
TessellationView< Real > view
Definition tessellator.hpp:64
Published, read-only device data layer for the tessellation (migration §2.1, §3).