MayaFlux 0.5.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
SpatialHashProcessor.hpp
Go to the documentation of this file.
1#pragma once
2
4
6class GpuFieldOperator;
7}
8
9namespace MayaFlux::Buffers {
10
11class NetworkGeometryBuffer;
12
13/**
14 * @struct SpatialHashConfig
15 * @brief Uniform grid parameters shared by every stage of the hash build.
16 *
17 * Cell size is ordinarily the owning ParticleNetwork's interaction radius:
18 * a neighbour query at that radius only needs to visit the 27 cells around
19 * the query particle's own cell. grid_min/grid_dims describe an axis-aligned
20 * box of grid_dims.x * grid_dims.y * grid_dims.z cells covering the
21 * network's bounds; a particle outside the box clamps to the nearest edge
22 * cell rather than being dropped.
23 *
24 * particle_count, stride_words and position_word_offset describe the vertex
25 * record layout the same way GpuFieldOperator does: word offset of the
26 * position attribute within one record, and the record stride in words.
27 * Fixed at construction, matching NetworkGeometryBuffer::declare_state's own
28 * documented limitation: none of this tracks later growth of the network.
29 */
31 glm::vec3 grid_min;
32 glm::uvec3 grid_dims;
33 float cell_size;
35 uint32_t stride_words;
37
38 /** @brief Total cell count, grid_dims.x * grid_dims.y * grid_dims.z. */
39 [[nodiscard]] uint32_t cell_count() const
40 {
41 return grid_dims.x * grid_dims.y * grid_dims.z;
42 }
43
44 /**
45 * @brief Build hash grid parameters from a NetworkGeometryBuffer's
46 * network and its primary operator.
47 * @param buffer Buffer whose network supplies particle_count, vertex
48 * layout and bounds. Reads the layout from the network's own
49 * GraphicsOperator (e.g. PhysicsOperator::get_vertex_layout),
50 * not from the buffer's cached copy: that copy is written by
51 * NetworkGeometryProcessor during upload and unset before the
52 * first cycle, whereas the operator's own layout is valid the
53 * moment create_operator<PhysicsOperator>() (or equivalent)
54 * returns. Callable synchronously right after setup, with no
55 * need to wait for a processing cycle.
56 * @param cell_size Grid cell size. Ordinarily PhysicsOperator's own
57 * interaction radius when the network is physics-driven; there
58 * is no generic source for this, since not every ParticleNetwork
59 * operator has an equivalent concept, so the caller supplies it
60 * directly.
61 * @return Populated config, or nullopt when the buffer's network is
62 * neither a ParticleNetwork nor a PointCloudNetwork, its primary
63 * operator is not a GraphicsOperator (or none is set), that
64 * operator's layout carries no word-aligned position attribute,
65 * or cell_size is not positive.
66 */
67 [[nodiscard]] static std::optional<SpatialHashConfig> from_network(
68 const std::shared_ptr<NetworkGeometryBuffer>& buffer, float cell_size);
69
70 /**
71 * @brief Declare the four state fields the hash build stages read and
72 * write, sized from this config.
73 * @param buffer Buffer to declare on. Fields already present under
74 * these names are left untouched (NetworkGeometryBuffer::declare_state
75 * rejects the duplicate and logs, matching its own documented
76 * behaviour).
77 *
78 * Declares hash_cell_count/hash_cell_start/hash_cell_cursor at
79 * cell_count() elements each, and hash_particle_index at particle_count
80 * elements. Call once before attaching HashClearProcessor/
81 * HashCountProcessor/HashScanProcessor/HashScatterProcessor to the
82 * buffer's chain. All four fields are single-slot: every stage fully
83 * overwrites the ones it owns each cycle rather than reading a previous
84 * cycle's value, so none of them need a second ping-pong slot.
85 *
86 * Also declares hash_cluster_id, one uint32 per particle, at
87 * particle_count elements. Unlike the four fields above it is written
88 * by no dispatch at all: NetworkGeometryBuffer uploads it once, at
89 * wiring time, from PhysicsOperator::get_collections(), since cluster
90 * membership is fixed the moment a collection is added and never
91 * changes cycle to cycle. Declared here rather than by MutationConfig
92 * because it is consumed by any hash-based neighbour query, not only
93 * the claim protocol: HashDensityColorProcessor reads it too, and a
94 * caller can enable density colouring with no absorb_radius at all, in
95 * which case MutationConfig::declare_fields never runs. Every entry is
96 * 0 when the operator carries at most one collection, so every
97 * cluster-aware guard built on this field is a no-op for the ordinary
98 * single-population case.
99 */
100 void declare_fields(const std::shared_ptr<NetworkGeometryBuffer>& buffer) const;
101};
102
103/**
104 * @struct GridPushConstants
105 * @brief Uniform-grid push-constant block shared by the hash count/scatter and
106 * population-spawn kernels. Field order is the push-constant declaration
107 * order; do not rearrange.
108 */
111 uint32_t stride_words;
117 uint32_t dim_x;
118 uint32_t dim_y;
119 uint32_t dim_z;
120};
121
122/** @brief Populate a GridPushConstants from grid config. */
124{
125 return GridPushConstants {
127 .stride_words = c.stride_words,
128 .position_offset = c.position_word_offset,
129 .grid_min_x = c.grid_min.x,
130 .grid_min_y = c.grid_min.y,
131 .grid_min_z = c.grid_min.z,
132 .cell_size = c.cell_size,
133 .dim_x = c.grid_dims.x,
134 .dim_y = c.grid_dims.y,
135 .dim_z = c.grid_dims.z,
136 };
137}
138
139/**
140 * @class NetworkStateFieldProcessor
141 * @brief ComputeProcessor operating on named state fields of a
142 * NetworkGeometryBuffer, plus optionally the buffer's own primary
143 * vertex storage.
144 *
145 * Mirrors VolumeFieldProcessor's relationship to VolumeGridBuffer: every
146 * binding lives at a private set 0, descriptors are written directly
147 * through ShaderFoundry rather than bind_buffer, and the write happens in
148 * processing_function so a resize between cycles (which can reassign the
149 * primary vertex buffer's handle) is picked up before execute_shader binds
150 * the set into the command buffer.
151 *
152 * Unlike VolumeFieldProcessor there is no shared lattice to size dispatch
153 * from and no per-field double-buffering to track here: every field a
154 * concrete stage below declares is single-slot, fully overwritten each
155 * cycle by whichever stage owns it. Dispatch sizing and push constant
156 * content are each concrete stage's own responsibility.
157 */
158class MAYAFLUX_API NetworkStateFieldProcessor : public ComputeProcessor {
159public:
160 /**
161 * @struct FieldBinding
162 * @brief One shader binding and where it draws from.
163 *
164 * No read/write distinction: every field this class or its subclasses
165 * declare is single-slot (NetworkGeometryBuffer::declare_state with
166 * double_buffered=false), and for a single-slot field read_state_handle
167 * and write_state_handle resolve to the same handle.
168 */
170 std::string name; ///< Shader binding name, as declared in ShaderConfig.
171 uint32_t binding; ///< Binding index within set 0.
172 std::string field; ///< State field name, or empty for the buffer's own vertex storage.
173 };
174
175 /** @brief The attached buffer, or null if attachment failed validation. */
176 [[nodiscard]] const std::shared_ptr<NetworkGeometryBuffer>& get_network_buffer() const { return m_buffer; }
177
178protected:
179 /**
180 * @brief Construct from a generated ShaderSpec.
181 * @param bindings Binding table. Entries are registered into
182 * m_config.bindings in order, all at set 0.
183 * @param spec ShaderSpec implementing the stage.
184 */
185 NetworkStateFieldProcessor(std::vector<FieldBinding> bindings, const Portal::Graphics::ShaderSpec& spec);
186
187 /**
188 * @brief Cache and validate the buffer, then call on_buffer_ready.
189 * @param buffer The attached buffer, expected to be a NetworkGeometryBuffer.
190 *
191 * On any validation failure the cached buffer is reset, which makes
192 * on_before_execute reject every subsequent cycle.
193 */
194 void on_attach(const std::shared_ptr<Buffer>& buffer) override;
195
196 /** @brief Write every binding in the table for the current buffer state. */
197 void on_descriptors_created() override;
198
199 /**
200 * @brief Reject buffers that are not the validated NetworkGeometryBuffer.
201 */
202 bool on_before_execute(Portal::Graphics::CommandBufferID cmd_id, const std::shared_ptr<VKBuffer>& buffer) override;
203
204 /**
205 * @brief Rewrite field descriptors, then run the shader.
206 *
207 * The primary vertex buffer's handle can change on resize, so bindings
208 * are rewritten every cycle rather than once, the same reasoning
209 * VolumeFieldProcessor applies to fields that may have swapped.
210 */
211 void processing_function(const std::shared_ptr<Buffer>& buffer) override;
212
213 /**
214 * @brief Hook for subclass setup, called at the end of on_attach.
215 *
216 * The buffer is non-null and validated when this runs.
217 */
218 virtual void on_buffer_ready() { }
219
220 /**
221 * @brief Configure manual dispatch for one thread per element, and
222 * stage the given push constant data.
223 * @tparam T Trivially copyable push constant struct.
224 * @param element_count Elements the dispatch must cover. Group count is
225 * ceil(element_count / workgroup_x), floored at 1, reading
226 * workgroup_x from the compiled spec rather than a repeated
227 * literal, matching VertexFieldProcessor::calculate_dispatch_size.
228 * @param params Push constant data to stage for the next dispatch.
229 *
230 * For the common shape most stages share: element_count threads total,
231 * the compiled workgroup size per group, one dispatch per cycle.
232 * HashScanProcessor's fixed single-invocation dispatch doesn't fit this
233 * shape and configures itself directly instead.
234 */
235 template <typename T>
236 void dispatch_one_thread_per(uint32_t element_count, const T& params)
237 {
238 const uint32_t local_x = std::max(1U, get_dispatch_config().workgroup_x);
239 const uint32_t groups = std::max(1U, (element_count + local_x - 1) / local_x);
240 set_manual_dispatch(groups, 1, 1);
241 set_push_constant_data(params);
242 }
243
244 /**
245 * @brief Base on_before_execute guard, then a revision-gated tuning reload.
246 * @param current_revision The owning operator's revision() sampled now.
247 * @param built_revision Revision the push constants were last built for;
248 * overwritten with current_revision when reload runs.
249 * @param reload Called only on a revision change; must re-read the tuning
250 * fields into the Params struct and call set_push_constant_data.
251 * @return False when the base guard rejects the cycle.
252 */
253 template <typename F>
256 const std::shared_ptr<VKBuffer>& buffer,
257 uint64_t current_revision,
258 uint64_t& built_revision,
259 F&& reload)
260 {
261 if (!NetworkStateFieldProcessor::on_before_execute(cmd_id, buffer)) {
262 return false;
263 }
264 if (current_revision != built_revision) {
265 std::forward<F>(reload)();
266 built_revision = current_revision;
267 }
268 return true;
269 }
270
271private:
272 /** @brief Register the binding table into m_config.bindings. */
273 void register_bindings();
274
275 /**
276 * @brief Check every named state field exists on the attached buffer.
277 * @return True if the cached buffer satisfies the binding table.
278 */
279 bool validate_fields();
280
281 /** @brief Issue descriptor writes for every entry in the binding table. */
282 void write_field_descriptors();
283
284 std::vector<FieldBinding> m_bindings;
285 std::shared_ptr<NetworkGeometryBuffer> m_buffer;
286};
287
288/**
289 * @class HashClearProcessor
290 * @brief Zeros the per-cell particle histogram before HashCountProcessor
291 * accumulates into it.
292 *
293 * Only "hash_cell_count" needs an explicit zero: hash_cell_start and
294 * hash_cell_cursor are fully overwritten by HashScanProcessor, and
295 * hash_particle_index is fully overwritten by HashScatterProcessor. Neither
296 * is read-modify-write, so neither carries stale data forward.
297 */
299public:
300 explicit HashClearProcessor(const SpatialHashConfig& config);
301
302protected:
303 void on_buffer_ready() override;
304
305private:
306 struct Params {
308 };
309
311};
312
313/**
314 * @class HashCountProcessor
315 * @brief Buckets each particle into its grid cell and accumulates a
316 * per-cell histogram via atomicAdd.
317 *
318 * One thread per particle. hash_cell_count must already be zeroed by
319 * HashClearProcessor this cycle.
320 */
322public:
323 /**
324 * @param gate_alive When true, binds mutation_alive and skips a
325 * particle before counting it into any cell, so a population
326 * with reserve capacity (see SpatialFieldConfig::reserve_fraction)
327 * never lets a dead or not-yet-spawned slot occupy a cell. Must
328 * agree with whatever HashScatterProcessor in the same chain
329 * uses: count and scatter disagreeing about which particles
330 * exist corrupts every cell's prefix-sum offset, not just the
331 * affected particle's own. False (the default) emits exactly
332 * today's shader, no new binding, no new branch.
333 */
334 explicit HashCountProcessor(const SpatialHashConfig& config, bool gate_alive = false);
335
336protected:
337 void on_buffer_ready() override;
338
339private:
341};
342
343/**
344 * @class HashScanProcessor
345 * @brief Sequential exclusive prefix sum over the per-cell histogram.
346 *
347 * Single invocation (workgroup and dispatch both {1,1,1}) looping over
348 * every cell. Not parallel: a workgroup-shared-memory scan is bounded to one
349 * workgroup's element count, which would cap grid resolution; this has no
350 * such ceiling. Cell counts in the thousands cost microseconds sequentially.
351 *
352 * Writes both hash_cell_start (the offset each cell's particles begin at
353 * in hash_particle_index) and hash_cell_cursor (seeded to the same values,
354 * the atomic write cursor HashScatterProcessor advances).
355 */
357public:
358 explicit HashScanProcessor(const SpatialHashConfig& config);
359
360protected:
361 void on_buffer_ready() override;
362
363private:
364 struct Params {
366 };
367
369};
370
371/**
372 * @class HashScatterProcessor
373 * @brief Writes each particle's index into hash_particle_index at the slot
374 * its cell's atomic cursor claims.
375 *
376 * One thread per particle, same cell math as HashCountProcessor. After this
377 * stage, hash_particle_index[hash_cell_start[c] .. +hash_cell_count[c]) is
378 * the index list of every particle in cell c.
379 */
381public:
382 /**
383 * @param gate_alive See HashCountProcessor's own doc: must agree with
384 * whatever HashCountProcessor in the same chain uses.
385 */
386 explicit HashScatterProcessor(const SpatialHashConfig& config, bool gate_alive = false);
387
388protected:
389 void on_buffer_ready() override;
390
391private:
393};
394
395/**
396 * @class HashDensityColorProcessor
397 * @brief Colours each particle by local neighbour count, read from the
398 * completed hash: proof that the hash build is actually usable for
399 * a neighbour query, not just self-consistent.
400 *
401 * One thread per particle. Walks the query particle's own cell plus its 26
402 * neighbours (27 total, clamped at grid edges), and for each candidate in
403 * hash_particle_index[hash_cell_start[c] .. +hash_cell_count[c]) checks the
404 * real distance rather than trusting cell membership alone, since a cell is
405 * a cube and two points in adjacent cells can be closer than two points in
406 * the same one. Neighbour count maps to a cool-to-warm colour ramp written
407 * into the vertex record's own colour attribute.
408 *
409 * At tens of thousands of particles this is O(n * 27 * average occupancy)
410 * rather than the O(n^2) of PhysicsOperator::apply_spatial_interactions, in
411 * one GPU dispatch. Must run after HashScatterProcessor.
412 *
413 * Reads density_saturation_count from the owning GpuFieldOperator
414 * fresh whenever its revision() changes (checked in on_before_execute,
415 * the same hook VertexFieldProcessor::sync_revision() uses), so
416 * GpuFieldOperator::set_density_saturation_count() takes effect on
417 * this processor's next dispatch with no rebuild.
418 *
419 * A candidate j is skipped when hash_cluster_id[j] differs from i's own
420 * cluster_id, unless SpatialFieldConfig::cross_cluster is true: by default
421 * a particle's density reading only ever counts neighbours from its own
422 * PhysicsOperator collection, the same boundary ClaimProcessor enforces, so
423 * one population's density never reads warm just because a denser, unrelated
424 * population happens to occupy the same cells. Read fresh alongside
425 * density_saturation_count on every revision change.
426 */
428public:
429 /**
430 * @param config Grid parameters, shared with the build stages that
431 * must already have populated hash_cell_start/hash_cell_count/
432 * hash_particle_index this cycle.
433 * @param particle_op Owning operator. Colour word offset is resolved
434 * from its vertex layout (DataModality::VERTEX_COLORS_RGB) at
435 * construction; throws std::invalid_argument when the layout
436 * carries no colour attribute. density_saturation_count is
437 * re-read from this operator's config whenever revision()
438 * changes.
439 */
441 const SpatialHashConfig& config,
442 std::shared_ptr<Nodes::Network::GpuFieldOperator> particle_op);
443
444protected:
445 void on_buffer_ready() override;
446
447 bool on_before_execute(
449 const std::shared_ptr<VKBuffer>& buffer) override;
450
451private:
452 struct Params {
454 uint32_t stride_words;
456 uint32_t color_offset;
461 uint32_t dim_x;
462 uint32_t dim_y;
463 uint32_t dim_z;
466 };
467
469 std::shared_ptr<Nodes::Network::GpuFieldOperator> m_particle_op;
471};
472
473} // namespace MayaFlux::Buffers
Specialized ShaderProcessor for Compute Pipelines.
Zeros the per-cell particle histogram before HashCountProcessor accumulates into it.
Buckets each particle into its grid cell and accumulates a per-cell histogram via atomicAdd.
std::shared_ptr< Nodes::Network::GpuFieldOperator > m_particle_op
Colours each particle by local neighbour count, read from the completed hash: proof that the hash bui...
Sequential exclusive prefix sum over the per-cell histogram.
Writes each particle's index into hash_particle_index at the slot its cell's atomic cursor claims.
std::shared_ptr< NetworkGeometryBuffer > m_buffer
virtual void on_buffer_ready()
Hook for subclass setup, called at the end of on_attach.
void dispatch_one_thread_per(uint32_t element_count, const T &params)
Configure manual dispatch for one thread per element, and stage the given push constant data.
bool guard_and_resync(Portal::Graphics::CommandBufferID cmd_id, const std::shared_ptr< VKBuffer > &buffer, uint64_t current_revision, uint64_t &built_revision, F &&reload)
Base on_before_execute guard, then a revision-gated tuning reload.
const std::shared_ptr< NetworkGeometryBuffer > & get_network_buffer() const
The attached buffer, or null if attachment failed validation.
ComputeProcessor operating on named state fields of a NetworkGeometryBuffer, plus optionally the buff...
GridPushConstants make_grid_push_constants(const SpatialHashConfig &c)
Populate a GridPushConstants from grid config.
Uniform-grid push-constant block shared by the hash count/scatter and population-spawn kernels.
std::string name
Shader binding name, as declared in ShaderConfig.
std::string field
State field name, or empty for the buffer's own vertex storage.
static std::optional< SpatialHashConfig > from_network(const std::shared_ptr< NetworkGeometryBuffer > &buffer, float cell_size)
Build hash grid parameters from a NetworkGeometryBuffer's network and its primary operator.
void declare_fields(const std::shared_ptr< NetworkGeometryBuffer > &buffer) const
Declare the four state fields the hash build stages read and write, sized from this config.
uint32_t cell_count() const
Total cell count, grid_dims.x * grid_dims.y * grid_dims.z.
Uniform grid parameters shared by every stage of the hash build.
Complete declarative description of a generated compute shader.