MayaFlux 0.5.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
GpuFieldOperator.cpp
Go to the documentation of this file.
2
4
6
7namespace {
8
9 /**
10 * @brief GLSL type spelling for a component count.
11 */
12 [[nodiscard]] const char* glsl_type(uint32_t components) noexcept
13 {
14 switch (components) {
15 case 1:
16 return "float";
17 case 2:
18 return "vec2";
19 default:
20 return "vec3";
21 }
22 }
23
24 /**
25 * @brief Zero literal for a component count.
26 */
27 [[nodiscard]] std::string glsl_zero(uint32_t components)
28 {
29 return components == 1 ? "0.0f" : std::string(glsl_type(components)) + "(0.0f)";
30 }
31
32 [[nodiscard]] std::string word(uint32_t n)
33 {
34 return std::to_string(n) + "u";
35 }
36
37 [[nodiscard]] const char* target_name(Kinesis::FieldTarget t) noexcept
38 {
40 switch (t) {
41 case FieldTarget::POSITION:
42 return "position";
43 case FieldTarget::COLOR:
44 return "color";
45 case FieldTarget::NORMAL:
46 return "normal";
47 case FieldTarget::TANGENT:
48 return "tangent";
49 case FieldTarget::SCALAR:
50 return "scalar";
51 case FieldTarget::UV:
52 return "uv";
53 default:
54 return "unknown";
55 }
56 }
57
58 /**
59 * @brief Whether a target accumulates onto its current value or replaces it.
60 *
61 * Matches FieldOperator: a position field is a displacement added to the
62 * vertex, every other target is written outright. FieldMode does not enter
63 * into it, because mode there selects whether reference data is restored
64 * before evaluation, and this operator holds no reference data.
65 */
66 [[nodiscard]] bool target_accumulates(Kinesis::FieldTarget t) noexcept
67 {
69 }
70
71 /**
72 * @brief Vertex attribute modality a single-bit target addresses.
73 */
74 [[nodiscard]] std::optional<Kakshya::DataModality>
75 target_modality(Kinesis::FieldTarget t) noexcept
76 {
79 switch (t) {
80 case FieldTarget::POSITION:
81 return DataModality::VERTEX_POSITIONS_3D;
82 case FieldTarget::COLOR:
83 return DataModality::VERTEX_COLORS_RGB;
84 case FieldTarget::NORMAL:
85 return DataModality::VERTEX_NORMALS_3D;
86 case FieldTarget::TANGENT:
87 return DataModality::VERTEX_TANGENTS_3D;
88 case FieldTarget::SCALAR:
89 return DataModality::SCALAR_F32;
90 case FieldTarget::UV:
91 return DataModality::TEXTURE_COORDS_2D;
92 default:
93 return std::nullopt;
94 }
95 }
96
97 /**
98 * @brief Component count implied by an attribute modality.
99 */
100 [[nodiscard]] uint32_t modality_components(Kakshya::DataModality m) noexcept
101 {
103 switch (m) {
104 case DataModality::VERTEX_POSITIONS_3D:
105 case DataModality::VERTEX_COLORS_RGB:
106 case DataModality::VERTEX_NORMALS_3D:
107 case DataModality::VERTEX_TANGENTS_3D:
108 return 3;
109 case DataModality::TEXTURE_COORDS_2D:
110 return 2;
111 case DataModality::SCALAR_F32:
112 return 1;
113 default:
114 return 0;
115 }
116 }
117
118 constexpr std::array<const char*, 3> k_swizzle { ".x", ".y", ".z" };
119
120} // namespace
121
123 : m_layout(std::move(layout))
124 , m_field_config(config)
125{
126 if (m_layout.stride_bytes == 0 || m_layout.stride_bytes % 4 != 0) {
128 "GpuFieldOperator: stride {} is not a nonzero multiple of 4. The "
129 "emitted shader addresses the record as a flat float array and "
130 "cannot describe this layout. The operator is inert: bind() will "
131 "reject everything and build_spec() returns nullopt.",
133 return;
134 }
135
137}
138
144
146{
148 invalidate();
149}
150
156
162
168
174
176{
178 invalidate();
179}
180
186
187std::optional<std::pair<uint32_t, uint32_t>>
189{
190 const auto modality = target_modality(target);
191 if (!modality.has_value())
192 return std::nullopt;
193
194 const auto it = std::ranges::find_if(m_layout.attributes,
195 [&modality](const Kakshya::VertexAttributeLayout& a) {
196 return a.component_modality == *modality;
197 });
198
199 if (it == m_layout.attributes.end())
200 return std::nullopt;
201
202 if (it->offset_in_vertex % 4 != 0) {
204 "GpuFieldOperator: attribute '{}' sits at byte offset {}, which is "
205 "not word-aligned and cannot be addressed as a float index.",
206 it->name, it->offset_in_vertex);
207 return std::nullopt;
208 }
209
210 return std::make_pair(it->offset_in_vertex / 4, modality_components(*modality));
211}
212
214 FieldTarget target,
215 const Kinesis::FieldSource& source,
216 uint32_t components)
217{
218 if (m_stride_words == 0) {
220 "GpuFieldOperator::bind: operator is inert, its layout stride was "
221 "rejected at construction");
222 return false;
223 }
224
225 if (!any_flag(target)) {
227 "GpuFieldOperator::bind: empty target mask");
228 return false;
229 }
230
231 if (!source.valid()) {
233 "GpuFieldOperator::bind: field '{}' has no usable shader half. A "
234 "DualField lambda must carry a trailing return type.",
235 source.name);
236 return false;
237 }
238
240 if (!has_flag(target, bit))
241 continue;
242
243 const auto slot = resolve_target(bit);
244 if (!slot.has_value()) {
246 "GpuFieldOperator::bind: target bit {:#x} is not present in the "
247 "supplied vertex layout. Nothing was bound.",
248 static_cast<uint16_t>(bit));
249 return false;
250 }
251
252 if (slot->second != components) {
254 "GpuFieldOperator::bind: target bit {:#x} takes {} components, "
255 "field '{}' produces {}. Nothing was bound.",
256 static_cast<uint16_t>(bit), slot->second, source.name, components);
257 return false;
258 }
259 }
260
261 const auto clash = std::ranges::find_if(m_bindings,
262 [&source](const Binding& b) {
263 return b.source.name == source.name && b.source.body != source.body;
264 });
265
266 if (clash != m_bindings.end()) {
268 "GpuFieldOperator::bind: a different field named '{}' is already "
269 "bound. Emitted function names must be unique; rename one field.",
270 source.name);
271 return false;
272 }
273
274 return true;
275}
276
278 FieldTarget target,
279 const Kinesis::FieldSource& source,
280 uint32_t components,
281 bool temporal,
282 std::optional<uint32_t> cluster)
283{
284 m_bindings.push_back({
285 .targets = target,
286 .source = source,
287 .components = components,
288 .temporal = temporal,
289 .cluster = cluster,
290 });
291 invalidate();
292}
293
295 std::optional<uint32_t> cluster)
296{
297 if (any_flag(target & ~Kinesis::k_vector_targets)) {
299 "GpuFieldOperator::bind: mask {:#x} reaches bits {:#x} that a "
300 "three-component field cannot drive. Nothing was bound.",
301 static_cast<uint16_t>(target),
302 static_cast<uint16_t>(target & ~Kinesis::k_vector_targets));
303 return;
304 }
305
306 if (!accept(target, field.source, 3))
307 return;
308
309 store(target, field.source, 3, false, cluster);
310}
311
313 std::optional<uint32_t> cluster)
314{
315 if (target != FieldTarget::SCALAR) {
317 "GpuFieldOperator::bind: a scalar field binds only to SCALAR, got "
318 "mask {:#x}",
319 static_cast<uint16_t>(target));
320 return;
321 }
322
323 if (!accept(target, field.source, 1))
324 return;
325
326 store(target, field.source, 1, false, cluster);
327}
328
330 std::optional<uint32_t> cluster)
331{
332 if (target != FieldTarget::UV) {
334 "GpuFieldOperator::bind: a two-component field binds only to UV, got "
335 "mask {:#x}",
336 static_cast<uint16_t>(target));
337 return;
338 }
339
340 if (!accept(target, field.source, 2))
341 return;
342
343 store(target, field.source, 2, false, cluster);
344}
345
347 std::optional<uint32_t> cluster)
348{
349 if (any_flag(target & ~Kinesis::k_vector_targets)) {
351 "GpuFieldOperator::bind: mask {:#x} reaches bits {:#x} that a "
352 "three-component field cannot drive. Nothing was bound.",
353 static_cast<uint16_t>(target),
354 static_cast<uint16_t>(target & ~Kinesis::k_vector_targets));
355 return;
356 }
357
358 if (!accept(target, field.source, 3))
359 return;
360
361 store(target, field.source, 3, true, cluster);
362}
363
365 std::optional<uint32_t> cluster)
366{
367 if (target != FieldTarget::SCALAR) {
369 "GpuFieldOperator::bind: a scalar field binds only to SCALAR, got "
370 "mask {:#x}",
371 static_cast<uint16_t>(target));
372 return;
373 }
374
375 if (!accept(target, field.source, 1))
376 return;
377
378 store(target, field.source, 1, true, cluster);
379}
380
382 std::optional<uint32_t> cluster)
383{
384 if (target != FieldTarget::UV) {
386 "GpuFieldOperator::bind: a two-component field binds only to UV, got "
387 "mask {:#x}",
388 static_cast<uint16_t>(target));
389 return;
390 }
391
392 if (!accept(target, field.source, 2))
393 return;
394
395 store(target, field.source, 2, true, cluster);
396}
397
399{
400 for (auto& b : m_bindings)
401 b.targets &= ~target;
402
403 std::erase_if(m_bindings,
404 [](const Binding& b) { return !any_flag(b.targets); });
405
406 invalidate();
407}
408
410{
411 if (m_vertex_binding == binding)
412 return;
413 m_vertex_binding = binding;
414 invalidate();
415}
416
418{
419 if (x == 0 || m_workgroup_size == x)
420 return;
422 invalidate();
423}
424
426{
427 m_spec_cache.reset();
428 ++m_revision;
429}
430
432{
433}
434
435void GpuFieldOperator::set_parameter(std::string_view param, double value)
436{
438 "GpuFieldOperator: unknown parameter '{}' ({})", param, value);
439}
440
442{
443 return std::ranges::any_of(m_bindings,
444 [](const Binding& b) { return b.cluster.has_value(); });
445}
446
447std::optional<double> GpuFieldOperator::query_state(std::string_view query) const
448{
449 if (query == "binding_count")
450 return static_cast<double>(m_bindings.size());
451 if (query == "stride_words")
452 return static_cast<double>(m_stride_words);
453 return std::nullopt;
454}
455
456std::optional<Portal::Graphics::ShaderSpec> GpuFieldOperator::build_spec() const
457{
458 if (m_spec_cache.has_value())
459 return m_spec_cache;
460
461 if (m_bindings.empty())
462 return std::nullopt;
463
464 const auto position = resolve_target(Kinesis::FieldTarget::POSITION);
465 if (!position.has_value()) {
467 "GpuFieldOperator: layout carries no position attribute. Fields are "
468 "functions of position and cannot be evaluated without one.");
469 return std::nullopt;
470 }
471
472 const bool needs_cluster = needs_cluster_id();
473
478
479 if (needs_cluster) {
480 assemble.ssbo("cluster_id", Portal::Graphics::BindingDirection::Input,
482 }
483
484 assemble.pc("first_vertex", Kakshya::GpuDataFormat::UINT32)
485 .pc("vertex_count", Kakshya::GpuDataFormat::UINT32)
486 .pc("stride_words", Kakshya::GpuDataFormat::UINT32)
489
490 std::vector<std::string_view> emitted;
491 for (const auto& b : m_bindings) {
492 const auto seen = std::ranges::find(emitted, b.source.name);
493 if (seen != emitted.end())
494 continue;
495 emitted.push_back(b.source.name);
496 assemble.function(b.source.return_type, b.source.name,
497 b.source.params, b.source.body);
498 }
499
500 const uint32_t pw = position->first;
501 std::string body;
502 body += " if (i >= vertex_count) { return; }\n";
503 body += " uint b = (first_vertex + i) * stride_words;\n";
504 body += " vec3 p = vec3(vertices[b + " + word(pw)
505 + "], vertices[b + " + word(pw + 1)
506 + "], vertices[b + " + word(pw + 2) + "]);\n";
507
508 if (needs_cluster) {
509 body += " uint my_cluster = cluster_id[first_vertex + i];\n";
510 }
511
513 const bool touched = std::ranges::any_of(m_bindings,
514 [t](const Binding& b) { return has_flag(b.targets, t); });
515 if (!touched)
516 continue;
517
518 const auto slot = resolve_target(t);
519 if (!slot.has_value())
520 continue;
521
522 const auto [w, components] = *slot;
523 const std::string acc = std::string("acc_") + target_name(t);
524
525 body += "\n " + std::string(glsl_type(components)) + " " + acc
526 + " = " + glsl_zero(components) + ";\n";
527
528 for (const auto& b : m_bindings) {
529 if (!has_flag(b.targets, t))
530 continue;
531
532 const std::string call = std::string(b.source.name)
533 + (b.temporal ? "(p, time)" : "(p)");
534
535 if (b.cluster.has_value()) {
536 body += " if (my_cluster == " + word(*b.cluster) + ") { "
537 + acc + " += " + call + "; }\n";
538 } else {
539 body += " " + acc + " += " + call + ";\n";
540 }
541 }
542
544 const std::string len = std::string("len_") + target_name(t);
545 body += " float " + len + " = length(" + acc + ");\n";
546 body += " " + acc + " = " + len + " > 1e-6f ? " + acc + " / "
547 + len + " : " + acc + ";\n";
548 }
549
550 const char* assign = target_accumulates(t) ? " += " : " = ";
551 for (uint32_t k = 0; k < components; ++k) {
552 body += " vertices[b + " + word(w + k) + "]" + assign
553 + acc + (components == 1 ? "" : k_swizzle[k]) + ";\n";
554 }
555 }
556
557 assemble.kernel(Portal::Graphics::KernelSource { .body = std::move(body) });
558
559 m_spec_cache = assemble.build();
560 return m_spec_cache;
561}
562
563} // namespace MayaFlux::Nodes::Network
#define MF_ERROR(comp, ctx,...)
#define MF_RT_TRACE(comp, ctx,...)
float rate
size_t a
size_t b
size_t count
float value
float threshold
float k
void invalidate()
Clear the cached spec and bump revision().
void set_workgroup_size(uint32_t x)
Workgroup size along x.
void store(FieldTarget, const Kinesis::FieldSource &, uint32_t, bool, std::optional< uint32_t > cluster)
Store a binding after validation.
void set_swallow_dim_factor(float factor)
Live-update ClaimSwallowProcessor's absorbed-particle dimming.
std::optional< Portal::Graphics::ShaderSpec > m_spec_cache
bool accept(FieldTarget target, const Kinesis::FieldSource &source, uint32_t components)
Shared validation for the three bind overloads.
void bind(FieldTarget target, const Kinesis::DualVectorField &field, std::optional< uint32_t > cluster=std::nullopt)
Bind a three-component field to one or more vec3 targets.
void set_swallow_max_size(float size)
Live-update ClaimSwallowProcessor's survivor size ceiling.
GpuFieldOperator(Kakshya::VertexLayout layout, SpatialFieldConfig config={})
std::optional< Portal::Graphics::ShaderSpec > build_spec() const
Assemble the compute spec for the current bindings.
std::optional< double > query_state(std::string_view query) const override
Query operator internal state.
void process(float dt) override
No per-cycle work.
void set_density_saturation_count(float count)
Live-update HashDensityColorProcessor's saturation point.
void set_spawn_density_threshold(float threshold)
Live-update PopulationSpawnProcessor's spawn density threshold.
void set_swallow_base_size(float size)
Live-update ClaimSwallowProcessor's base survivor size.
void set_cross_cluster(bool enabled)
Live-update whether claims and density see across cluster boundaries.
void set_swallow_growth_rate(float rate)
Live-update ClaimSwallowProcessor's per-swallow size increase.
void set_parameter(std::string_view param, double value) override
No settable parameters.
void set_vertex_binding(uint32_t binding)
Descriptor binding index the vertex SSBO occupies.
std::optional< std::pair< uint32_t, uint32_t > > resolve_target(FieldTarget target) const
Locate a target's word offset and component count in the layout.
void unbind(FieldTarget target)
Clear the given targets.
bool needs_cluster_id() const
Whether build_spec() needs a per-vertex cluster id this cycle.
void set_capture_growth(float growth)
Live-update ClaimProcessor's size-to-capture-radius scaling.
Assemble & function(std::string return_type, std::string name, std::string params, std::string body)
Declare a function emitted before main() in the generated GLSL.
ShaderSpec build()
Finalise and return the ShaderSpec.
Assemble & pc(std::string name, Kakshya::GpuDataFormat format)
Declare a push constant field with explicit format.
Assemble & ssbo(std::string name, BindingDirection direction, Kakshya::GpuDataFormat format, Kakshya::DataModality modality=Kakshya::DataModality::SCALAR_F32)
Declare an SSBO binding.
Assemble & kernel(KernelSource ks)
Supply a user kernel via MF_KERNEL.
Assemble & workgroup(uint32_t x, uint32_t y=1, uint32_t z=1)
Override workgroup size.
Fluent assembler producing a ShaderSpec.
@ NodeProcessing
Node graph processing (Nodes::NodeGraphManager)
@ Nodes
DSP Generator and Filter Nodes, graph pipeline, node management.
DataModality
Data modality types for cross-modal analysis.
Definition NDData.hpp:164
FieldTarget
What a Tendency drives when applied to a vertex record.
constexpr FieldTarget k_vector_targets
Targets that accept a three-component field.
constexpr std::array< FieldTarget, 6 > k_field_targets
Single-bit targets known to this version, in vertex layout order.
Kinesis::FieldTarget FieldTarget
Semantic description of a single vertex attribute.
uint32_t stride_bytes
Total bytes per vertex (stride in Vulkan terms) e.g., 3 floats (position) + 3 floats (normal) = 24 by...
std::vector< VertexAttributeLayout > attributes
All attributes that make up one vertex Ordered by shader location (0, 1, 2, ...)
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.
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...
float swallow_growth_rate
ClaimSwallowProcessor: point-size increase per particle a survivor swallowed this cycle.
float swallow_base_size
ClaimSwallowProcessor: a survivor's point size the cycle it has swallowed nothing.
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...
Parsed representation of a user-supplied kernel lambda.