MayaFlux 0.5.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
GpuFieldOperator.hpp
Go to the documentation of this file.
1#pragma once
2
3#include "NetworkOperator.hpp"
4
9
11
13
14/**
15 * @struct SpatialFieldConfig
16 * @brief Declarative configuration for the GPU-side spatial-relational work a
17 * GpuFieldOperator can drive beyond plain field bindings: uniform-grid
18 * spatial hashing, neighbour-density colouring, the deterministic
19 * claim/absorption protocol, and opt-in population dynamics.
20 *
21 * Domain-neutral. Every stage this selects operates on the vertex records a
22 * GraphicsOperator produces, addressed through that operator's VertexLayout,
23 * with no dependency on what kind of network or primary operator sits behind
24 * them: a PhysicsOperator on a ParticleNetwork is one source, a
25 * TopologyOperator or PathOperator on a PointCloudNetwork is another. A
26 * handful of fields draw on concepts only a PhysicsOperator supplies
27 * (accreted mass, collection boundaries) and degrade to a documented no-op
28 * when none is present; each such field says so in its own entry.
29 *
30 * Filled once, at construction, with designated initializers: the same
31 * shape as VolumeGridBuffer::FlowConfig, a plain value handed over once
32 * rather than a sequence of enable_x() calls.
33 *
34 * Two kinds of field here, deliberately not distinguished by type but
35 * documented per-field: cell_size/absorb_radius/cosmetic_swallow/density_color
36 * are structural, as they decide *which* processors the geometry buffer
37 * constructs in the first place, so changing them after wiring would mean
38 * tearing down and rebuilding part of the chain, and there is no live path
39 * for that yet. Every other field is a tuning value read by a processor
40 * that exists regardless of its value; those do have a live path, through
41 * GpuFieldOperator's own setters (see each field's own doc for which one).
42 */
44 /**
45 * Spatial hash cell size. Nullopt derives it from the network's primary
46 * operator when that operator is a PhysicsOperator (its
47 * get_interaction_radius()); any other primary operator has no
48 * equivalent concept and must set this explicitly. Only consulted when
49 * absorb_radius or density_color below actually needs the hash built.
50 * Structural: fixed at construction.
51 */
52 std::optional<float> cell_size;
53
54 /**
55 * Enables the deterministic claim/absorption pipeline at this distance.
56 * Nullopt (the default) disables mutation entirely. Implies the
57 * spatial hash is built regardless of density_color. Structural: fixed
58 * at construction.
59 */
60 std::optional<float> absorb_radius;
61
62 /**
63 * Scales a claimant's effective capture radius by the cube root of its
64 * own currently accreted mass (mutation_accreted_mass, uploaded fresh
65 * each cycle from PhysicsOperator::get_accreted_mass_span when a
66 * PhysicsOperator drives the network): max(absorb_radius,
67 * cbrt(accreted_mass) * capture_growth). 0 (the default) makes every
68 * point claim at the fixed absorb_radius regardless of accreted mass.
69 * With no PhysicsOperator in the chain nothing maintains the accreted
70 * mass tally, so this stays inert whatever its value: see
71 * ClaimAccumulateProcessor.
72 *
73 * A fixed absorb_radius alone caps how much territory any body can ever
74 * sweep, however much mass it already holds, so growth plateaus as soon
75 * as that fixed neighbourhood is exhausted; scaling capture radius with
76 * mass instead makes accretion self-reinforcing. Cube root specifically,
77 * not mass directly: captured-mass rate scales with capture_radius
78 * cubed (a 3D volume query), so a radius proportional to mass directly
79 * gives a growth rate proportional to mass cubed, which reaches
80 * infinite mass in finite time for ANY nonzero capture_growth -- not a
81 * gradual curve, an instantaneous one-cycle explosion at a delay this
82 * value controls. Cube-rooting mass first (the same real-world
83 * reasoning PhysicsOperator::apply_bond_forces already uses for
84 * physical spread) keeps the growth rate proportional to mass itself:
85 * ordinary exponential growth, with a genuine, live-tunable doubling
86 * time rather than a hidden singularity. Live-tunable via
87 * set_capture_growth().
88 */
89 float capture_growth { 0.0F };
90
91 /**
92 * Whether ClaimSwallowProcessor's cosmetic pass (position snap onto
93 * root, size/colour from this cycle's swallow_count) is added to the
94 * chain. Default true: existing absorb_radius behaviour is unchanged.
95 *
96 * Set false when a caller wants claim resolution purely for its CPU
97 * side effect, PhysicsOperator::sync_bonds_from_claims (bond force and
98 * the persistent accreted-mass tally it maintains), without the GPU
99 * cosmetic pass fighting it: swallow_count resets every cycle (see
100 * ClaimInitProcessor), so it can only ever represent this instant's
101 * local cluster size, never true accumulation over time. A caller after
102 * real, permanent growth reads PhysicsOperator's accreted mass instead
103 * and drives size/colour from that on the CPU side, and doesn't want
104 * ClaimSwallowProcessor overwriting the same vertices with its own,
105 * necessarily transient, numbers. ClaimInit/Claim/Flatten/Accumulate
106 * still run regardless: they are what ClaimAccumulateProcessor's
107 * readback needs to populate PhysicsOperator's bonds at all. Structural:
108 * fixed at construction.
109 */
110 bool cosmetic_swallow { true };
111
112 /**
113 * Enables neighbour-density colouring (ember-to-white-hot ramp).
114 * Structural: fixed at construction.
115 */
116 bool density_color { false };
117
118 /**
119 * HashDensityColorProcessor: neighbour count at which the density ramp
120 * saturates to fully warm. Lower values make sparser clusters read as
121 * dense; live-tunable via set_density_saturation_count().
122 */
124
125 /**
126 * ClaimSwallowProcessor: a survivor's point size the cycle it has
127 * swallowed nothing. Live-tunable via set_swallow_base_size().
128 */
129 float swallow_base_size { 10.0F };
130
131 /**
132 * ClaimSwallowProcessor: point-size increase per particle a survivor
133 * swallowed this cycle. Live-tunable via set_swallow_growth_rate().
134 */
135 float swallow_growth_rate { 0.6F };
136
137 /**
138 * ClaimSwallowProcessor: point-size ceiling regardless of swallow
139 * count, so one runaway cluster can't dominate the screen.
140 * Live-tunable via set_swallow_max_size().
141 */
142 float swallow_max_size { 70.0F };
143
144 /**
145 * ClaimSwallowProcessor: brightness multiplier applied to an absorbed
146 * particle's cluster colour (0 invisible, 1 as bright as the
147 * survivor). Live-tunable via set_swallow_dim_factor().
148 */
149 float swallow_dim_factor { 0.08F };
150
151 /**
152 * Whether ClaimProcessor and HashDensityColorProcessor may see across
153 * cluster boundaries. False (the default) means a point in one cluster
154 * can never claim, be claimed by, or be counted as a density neighbour
155 * of a point in another: every hash-based neighbour query they run is
156 * scoped to hash_cluster_id[i], the per-vertex field
157 * NetworkGeometryBuffer::ensure_cluster_ids() derives from whatever
158 * GraphicsOperator::build_cluster_ids() returns (a PhysicsOperator tags
159 * one cluster per collection, a TopologyOperator or PathOperator one per
160 * set; every entry 0, and this guard a no-op, when the operator holds a
161 * single set). True restores the single global neighbourhood both
162 * processors used before hash_cluster_id existed: every point in the
163 * buffer is a candidate for every other, regardless of cluster.
164 * Live-tunable via set_cross_cluster().
165 */
166 bool cross_cluster { false };
167
168 /**
169 * Fraction of extra reserve capacity the geometry buffer allocates
170 * beyond the live point count, for PopulationSpawnProcessor to claim:
171 * reserve_count = ceil(live_count * reserve_fraction), and the
172 * vertex buffer plus every hash/claim/mutation state field is sized to
173 * live_count + reserve_count from the moment this operator is wired,
174 * never resized again afterward. 0 (the default) allocates no reserve
175 * and wires none of PopulationInitProcessor/PopulationSpawnProcessor/the
176 * alive-gating on HashCountProcessor/HashScatterProcessor/ClaimProcessor/
177 * ClaimAccumulateProcessor at all -- population dynamics is entirely
178 * opt-in and costs nothing unset.
179 *
180 * Requires absorb_radius to also be set: destruction is destroy-on-
181 * absorption, reusing mutation_claimed_by, so there is nothing for this
182 * to destroy without claims running. A nonzero reserve_fraction with no
183 * absorb_radius is treated as unset and logged.
184 *
185 * What spawns and dies here is deliberately not reflected to
186 * PhysicsOperator: see PopulationConfig's own doc for the full
187 * reasoning. Structural: fixed at construction, like absorb_radius
188 * itself, since it changes which processors get built.
189 */
190 float reserve_fraction { 0.0F };
191
192 /**
193 * PopulationSpawnProcessor: real neighbour count (same 27-cell query
194 * HashDensityColorProcessor performs) a live point must clear before
195 * it spawns a copy of itself into a fresh reserve slot. Only consulted
196 * when reserve_fraction enables population dynamics at all.
197 * Live-tunable via set_spawn_density_threshold().
198 */
199 float spawn_density_threshold { 30.0F };
200
201 /**
202 * Whether a claim that crosses a cluster boundary moves the absorbed
203 * vertex into the claimant's cluster (a "hop"/transfer) instead of
204 * destroying it (population dynamics) or cosmetically swallowing it.
205 * False (the default) leaves absorption purely destructive/cosmetic.
206 *
207 * Requires absorb_radius: there is no claim to reinterpret otherwise, and
208 * a value with absorb_radius unset is treated as unset and logged. Also
209 * needs cross_cluster on to do anything, since ClaimProcessor never lets a
210 * claim cross a cluster boundary while it is off; that being live-tunable,
211 * this simply stays inert (no cross-cluster claim exists to transform)
212 * rather than erroring, until cross_cluster is switched on. Needs more
213 * than one cluster, like every cluster-aware field here.
214 *
215 * The hop is a single write to hash_cluster_id[i], the one place cluster
216 * membership is ever reassigned at runtime. Unlike destroy-on-absorption
217 * it does not need re-asserting each cycle, since nothing re-uploads
218 * hash_cluster_id after wiring. Claims resolve by atomicMin (lowest index
219 * wins), so absorption flows toward the lowest global vertex index in
220 * contact and cluster boundaries migrate monotonically toward low-index
221 * territory rather than oscillating. GPU-local and rebuilt from
222 * build_cluster_ids() on the next reseed, same as population dynamics.
223 * Structural: fixed at construction, since it changes which processors
224 * (ClaimTransferProcessor, and the cluster-aware branches of
225 * ClaimAccumulateProcessor/ClaimSwallowProcessor) get built.
226 */
227 bool transfer_on_claim { false };
228};
229
230/**
231 * @class GpuFieldOperator
232 * @brief Chain operator that declares Tendency field deformation as a compute
233 * shader rather than evaluating it on the CPU.
234 *
235 * Holds no vertex data and no Vulkan objects. It owns bindings and emits a
236 * ShaderSpec; a ComputeProcessor in the buffer chain owns the dispatch. This is
237 * a NetworkOperator rather than a GraphicsOperator, so
238 * NetworkGeometryProcessor's dynamic_cast fails and the operator contributes an
239 * empty render slice without any participates_in_rendering bookkeeping.
240 *
241 * Vertex-agnostic. Addressing is derived from the supplied VertexLayout, whose
242 * attribute offsets are identical across for_points, for_lines, for_meshes and
243 * for_raw, so one operator serves any of them. Pass for_raw(stride) for
244 * pre-packed data that fits no struct.
245 *
246 * Bound fields are DualFields: the same authored text that FieldOperator would
247 * consume through .cpu is emitted here through .source as a GLSL function. A
248 * field can be moved between the two operators without being rewritten.
249 *
250 * Field domain is always the vertex position read from the record, matching
251 * Tendency<glm::vec3, R>. FieldMode::ABSOLUTE is therefore only meaningful when
252 * the target is not POSITION: writing an absolute position would require a
253 * reference copy of the original vertices, which this operator does not
254 * allocate. bind() rejects that combination.
255
256 * Consumers compile a pipeline from build_spec() and hold it. revision()
257 * increments whenever that spec changes, so a consumer records the revision it
258 * built against and rebuilds when it differs. Rebinding is authoring-time work
259 * and is not synchronised against a running process_batch, matching
260 * OperatorChain's contract.
261 *
262 * @code
263 * namespace MayaFlux::Fields {
264 * using namespace MayaFlux::ShaderCompat;
265 * const auto swirl = MF_FIELD(swirl, [](vec3 p) -> vec3 {
266 * return cross(vec3(0.0f, 1.0f, 0.0f), p) * 2.0f;
267 * });
268 * }
269 *
270 * auto op = net->get_operator_chain()->emplace<GpuFieldOperator>(
271 * Kakshya::VertexLayout::for_points());
272 * op->bind(FieldTarget::POSITION | FieldTarget::NORMAL, Fields::swirl);
273 * @endcode
274 */
275class MAYAFLUX_API GpuFieldOperator : public NetworkOperator {
276public:
277 /**
278 * @param layout Vertex layout describing the record the shader will write.
279 * stride_bytes and every bound attribute's offset_in_vertex
280 * must be divisible by 4; construction fails loudly otherwise.
281 * @param config Spatial-field configuration read by NetworkGeometryBuffer's
282 * wiring: which of the spatial-hash / claim / density /
283 * population stages to build, plus their live-tunable values.
284 * Defaulted empty, meaning field displacement only.
285 */
286 explicit GpuFieldOperator(Kakshya::VertexLayout layout, SpatialFieldConfig config = {});
287
288 ~GpuFieldOperator() override = default;
289
290 // -------------------------------------------------------------------------
291 // Field binding
292 // -------------------------------------------------------------------------
293
294 /**
295 * @brief Bind a three-component field to one or more vec3 targets.
296 * @param target Mask over POSITION, COLOR, NORMAL and TANGENT.
297 * @param field Dual-source field with a usable shader half.
298 * @param cluster Nullopt (the default) applies the field to every vertex
299 * regardless of which cluster it belongs to, the
300 * only behaviour that existed before this parameter. A value
301 * restricts the field to vertices whose cluster id equals it: see
302 * needs_cluster_id() for what that costs a consumer that never
303 * uses this.
304 *
305 * Binding nothing and returning is the response to an empty mask, a bit the
306 * layout does not carry, a component-count mismatch on any bit, a field
307 * whose shader half failed to parse, or a function name already emitted
308 * with a different body. Validation covers the whole mask before anything
309 * is stored, so a partly-valid mask binds nothing rather than part.
310 *
311 * Several fields may drive the same target. They sum before the write,
312 * matching FieldOperator, and NORMAL and TANGENT normalise after the sum.
313 * A cluster-scoped field only contributes to that sum for vertices in its
314 * own cluster; an unscoped field sharing the target still contributes to
315 * every vertex regardless.
316 */
317 void bind(FieldTarget target, const Kinesis::DualVectorField& field,
318 std::optional<uint32_t> cluster = std::nullopt);
319
320 /**
321 * @brief Bind a scalar field. Target must be exactly SCALAR.
322 * @param cluster See the DualVectorField overload's own doc.
323 */
324 void bind(FieldTarget target, const Kinesis::DualSpatialField& field,
325 std::optional<uint32_t> cluster = std::nullopt);
326
327 /**
328 * @brief Bind a two-component field. Target must be exactly UV.
329 * @param cluster See the DualVectorField overload's own doc.
330 */
331 void bind(FieldTarget target, const Kinesis::DualUVField& field,
332 std::optional<uint32_t> cluster = std::nullopt);
333
334 /**
335 * @brief Bind a three-component field of position and time.
336 * @param target Mask over POSITION, COLOR, NORMAL and TANGENT.
337 * @param field Temporal field with a usable shader half.
338 * @param cluster See the DualVectorField overload's own doc.
339 *
340 * Validation matches the DualField overload. The emitted function takes a
341 * second float argument supplied from the time push constant.
342 */
343 void bind(FieldTarget target, const Kinesis::TemporalVectorField& field,
344 std::optional<uint32_t> cluster = std::nullopt);
345
346 /**
347 * @brief Bind a scalar field of position and time. Target must be SCALAR.
348 * @param cluster See the DualVectorField overload's own doc.
349 */
351 std::optional<uint32_t> cluster = std::nullopt);
352
353 /**
354 * @brief Bind a two-component field of position and time. Target must be UV.
355 * @param cluster See the DualVectorField overload's own doc.
356 */
357 void bind(FieldTarget target, const Kinesis::TemporalUVField& field,
358 std::optional<uint32_t> cluster = std::nullopt);
359
360 /**
361 * @brief Clear the given targets.
362 *
363 * Removes each bit in the mask from every binding that carries it. A
364 * binding left with no targets is dropped; one that still drives another
365 * target survives. Cluster scope plays no part in the match: unbind(t)
366 * removes every binding driving t, scoped or not.
367 */
368 void unbind(FieldTarget target);
369
370 /**
371 * @brief Number of bound fields. One binding may drive several targets.
372 */
373 [[nodiscard]] size_t binding_count() const { return m_bindings.size(); }
374
375 /**
376 * @brief Whether build_spec() needs a per-vertex cluster id this cycle.
377 * @return True only if at least one current binding was given a cluster
378 * argument. False for every operator that has never called a
379 * cluster-scoped bind() overload -- the ordinary case -- in
380 * which build_spec() emits exactly the shader it always has,
381 * with no cluster_id binding, no extra SSBO read, and no extra
382 * branch anywhere in the kernel. VertexFieldProcessor checks
383 * this to decide whether it needs to resolve and bind a
384 * hash_cluster_id state field at all.
385 */
386 [[nodiscard]] bool needs_cluster_id() const;
387
388 /**
389 * @brief Descriptor binding index the vertex SSBO occupies.
390 *
391 * Must match what the owning processor pushes. Default 0.
392 */
393 void set_vertex_binding(uint32_t binding);
394
395 /**
396 * @brief Get the descriptor binding index the vertex SSBO occupies.
397 */
398 [[nodiscard]] uint32_t get_vertex_binding() const noexcept { return m_vertex_binding; }
399
400 /**
401 * @brief Workgroup size along x. Default 256.
402 */
403 void set_workgroup_size(uint32_t x);
404
405 /**
406 * @brief Monotonic counter incremented whenever build_spec()'s result changes.
407 *
408 * A consumer that compiles a pipeline from build_spec() records the
409 * revision it built against and rebuilds when it differs. A bool would not
410 * survive two consumers, since the first to observe it would clear it.
411 */
412 [[nodiscard]] uint64_t revision() const noexcept { return m_revision; }
413
414 // -------------------------------------------------------------------------
415 // Spatial-field configuration
416 // -------------------------------------------------------------------------
417
418 /**
419 * @brief The current SpatialFieldConfig: structural fields (which stages
420 * exist) and live-tunable values alike.
421 *
422 * Read by NetworkGeometryBuffer at wiring time to decide which stages to
423 * build, and re-read by HashDensityColorProcessor / ClaimProcessor /
424 * ClaimSwallowProcessor / PopulationSpawnProcessor whenever revision()
425 * changes, so the setters below take effect on the next dispatch with no
426 * rebuild.
427 */
428 [[nodiscard]] const SpatialFieldConfig& get_field_config() const { return m_field_config; }
429
430 /** @brief Live-update HashDensityColorProcessor's saturation point. */
431 void set_density_saturation_count(float count);
432
433 /** @brief Live-update ClaimProcessor's size-to-capture-radius scaling. */
434 void set_capture_growth(float growth);
435
436 /** @brief Live-update ClaimSwallowProcessor's base survivor size. */
437 void set_swallow_base_size(float size);
438
439 /** @brief Live-update ClaimSwallowProcessor's per-swallow size increase. */
440 void set_swallow_growth_rate(float rate);
441
442 /** @brief Live-update ClaimSwallowProcessor's survivor size ceiling. */
443 void set_swallow_max_size(float size);
444
445 /** @brief Live-update ClaimSwallowProcessor's absorbed-particle dimming. */
446 void set_swallow_dim_factor(float factor);
447
448 /** @brief Live-update whether claims and density see across cluster boundaries. */
449 void set_cross_cluster(bool enabled);
450
451 /** @brief Live-update PopulationSpawnProcessor's spawn density threshold. */
452 void set_spawn_density_threshold(float threshold);
453
454 // -------------------------------------------------------------------------
455 // Shader
456 // -------------------------------------------------------------------------
457
458 /**
459 * @brief Assemble the compute spec for the current bindings.
460 *
461 * Emits one GLSL function per distinct bound field, then a kernel that
462 * guards against the dispatch tail, reads the position once, and writes
463 * every touched target in a single pass. Fields sharing a target sum
464 * before the write; NORMAL and TANGENT normalise after the sum. POSITION
465 * accumulates onto the existing vertex, every other target is assigned,
466 * matching FieldOperator.
467 *
468 * The kernel addresses a range rather than the whole buffer, since a
469 * NetworkGeometryBuffer aggregates one slice per producing operator. The
470 * range and the record stride arrive as push constants written by the
471 * processor, so neither the caller nor the field author offsets anything,
472 * and a layout or slice change needs no recompile. Attribute offsets are
473 * baked, since those describe the record the spec was built against.
474 *
475 * Cached until a bind, unbind or configuration change invalidates it.
476 * Returns nullopt when nothing is bound or the layout carries no position
477 * attribute.
478 *
479 * A cluster_id SSBO binding and the read that feeds it are emitted only
480 * when needs_cluster_id() is true: an operator with no cluster-scoped
481 * binding gets exactly the shader it always has, unchanged.
482 */
483 [[nodiscard]] std::optional<Portal::Graphics::ShaderSpec> build_spec() const;
484
485 /**
486 * @brief Layout the emitted shader addresses.
487 */
488 [[nodiscard]] const Kakshya::VertexLayout& get_layout() const { return m_layout; }
489
490 // -------------------------------------------------------------------------
491 // NetworkOperator interface
492 // -------------------------------------------------------------------------
493
494 /**
495 * @brief No per-cycle work.
496 *
497 * The operator holds only declarations. Dispatch, push constants and the
498 * vertex range all belong to the VertexFieldProcessor that consumes
499 * build_spec(). Present because NetworkOperator requires it.
500 */
501 void process(float dt) override;
502
503 /**
504 * @brief No settable parameters.
505 *
506 * dt is owned by process() and rewritten every cycle, so accepting it here
507 * would silently discard the value. When mapped parameters reach chain
508 * operators, they land as new push constant fields rather than as writes to
509 * this one.
510 */
511 void set_parameter(std::string_view param, double value) override;
512
513 [[nodiscard]] std::optional<double> query_state(std::string_view query) const override;
514
515 [[nodiscard]] std::string_view get_type_name() const override
516 {
517 return "GpuFieldOperator";
518 }
519
520 /**
521 * @brief True once at least one field is bound: the VertexFieldProcessor
522 * this operator drives writes pos += f in place, so the geometry
523 * producer must keep restoring the reference vertices every frame.
524 */
525 [[nodiscard]] bool demands_full_vertex_restore() const override
526 {
527 return binding_count() > 0;
528 }
529
530protected:
531 /**
532 * @brief Clear the cached spec and bump revision().
533 *
534 * Used by this class's own SpatialFieldConfig setters. Protected rather
535 * than private so a subclass with additional config surface can signal a
536 * change through the same revision a consumer already checks, rather than
537 * needing a second, parallel change-notification mechanism.
538 */
539 void invalidate();
540
541private:
542 /**
543 * @struct Binding
544 * @brief One target and the field driving it.
545 */
546 struct Binding {
549 uint32_t components;
551 std::optional<uint32_t> cluster;
552 };
553
556 uint32_t m_stride_words {};
557 uint32_t m_vertex_binding {};
558 uint32_t m_workgroup_size { 256 };
559 std::vector<Binding> m_bindings;
560 uint64_t m_revision {};
561
562 mutable std::optional<Portal::Graphics::ShaderSpec> m_spec_cache;
563
564 /**
565 * @brief Locate a target's word offset and component count in the layout.
566 * @return Nullopt when the layout carries no attribute of that modality.
567 */
568 [[nodiscard]] std::optional<std::pair<uint32_t, uint32_t>>
569 resolve_target(FieldTarget target) const;
570
571 /**
572 * @brief Shared validation for the three bind overloads.
573 */
574 bool accept(FieldTarget target, const Kinesis::FieldSource& source,
575 uint32_t components);
576
577 /**
578 * @brief Store a binding after validation.
579 */
580 void store(FieldTarget, const Kinesis::FieldSource&, uint32_t, bool,
581 std::optional<uint32_t> cluster);
582};
583
584} // namespace MayaFlux::Nodes::Network
float rate
size_t count
float value
float threshold
void bind(FieldTarget target, const Kinesis::DualUVField &field, std::optional< uint32_t > cluster=std::nullopt)
Bind a two-component field.
void bind(FieldTarget target, const Kinesis::TemporalSpatialField &field, std::optional< uint32_t > cluster=std::nullopt)
Bind a scalar field of position and time.
std::optional< Portal::Graphics::ShaderSpec > m_spec_cache
void bind(FieldTarget target, const Kinesis::TemporalUVField &field, std::optional< uint32_t > cluster=std::nullopt)
Bind a two-component field of position and time.
const SpatialFieldConfig & get_field_config() const
The current SpatialFieldConfig: structural fields (which stages exist) and live-tunable values alike.
void bind(FieldTarget target, const Kinesis::DualSpatialField &field, std::optional< uint32_t > cluster=std::nullopt)
Bind a scalar field.
std::string_view get_type_name() const override
Type name for introspection.
size_t binding_count() const
Number of bound fields.
uint64_t revision() const noexcept
Monotonic counter incremented whenever build_spec()'s result changes.
uint32_t get_vertex_binding() const noexcept
Get the descriptor binding index the vertex SSBO occupies.
bool demands_full_vertex_restore() const override
True once at least one field is bound: the VertexFieldProcessor this operator drives writes pos += f ...
const Kakshya::VertexLayout & get_layout() const
Layout the emitted shader addresses.
Chain operator that declares Tendency field deformation as a compute shader rather than evaluating it...
Domain-agnostic interpretive lens for network processing.
FieldTarget
What a Tendency drives when applied to a vertex record.
Kinesis::FieldTarget FieldTarget
std::shared_ptr< T > store(std::shared_ptr< T > obj)
Transfer ownership of an existing object to the persistent store for process lifetime.
Definition Persist.hpp:28
Complete description of vertex data layout in a buffer.
One authored expression carried as both a host callable and shader text.
Definition DualField.hpp:69
Parsed representation of a stringified field lambda.
An authored expression of position and time, shader-only.
std::optional< float > absorb_radius
Enables the deterministic claim/absorption pipeline at this distance.
std::optional< float > cell_size
Spatial hash cell size.
float swallow_max_size
ClaimSwallowProcessor: point-size ceiling regardless of swallow count, so one runaway cluster can't d...
float density_saturation_count
HashDensityColorProcessor: neighbour count at which the density ramp saturates to fully warm.
bool cross_cluster
Whether ClaimProcessor and HashDensityColorProcessor may see across cluster boundaries.
float spawn_density_threshold
PopulationSpawnProcessor: real neighbour count (same 27-cell query HashDensityColorProcessor performs...
bool cosmetic_swallow
Whether ClaimSwallowProcessor's cosmetic pass (position snap onto root, size/colour from this cycle's...
float swallow_growth_rate
ClaimSwallowProcessor: point-size increase per particle a survivor swallowed this cycle.
float reserve_fraction
Fraction of extra reserve capacity the geometry buffer allocates beyond the live point count,...
float swallow_base_size
ClaimSwallowProcessor: a survivor's point size the cycle it has swallowed nothing.
bool density_color
Enables neighbour-density colouring (ember-to-white-hot ramp).
bool transfer_on_claim
Whether a claim that crosses a cluster boundary moves the absorbed vertex into the claimant's cluster...
float capture_growth
Scales a claimant's effective capture radius by the cube root of its own currently accreted mass (mut...
float swallow_dim_factor
ClaimSwallowProcessor: brightness multiplier applied to an absorbed particle's cluster colour (0 invi...
Declarative configuration for the GPU-side spatial-relational work a GpuFieldOperator can drive beyon...