MayaFlux 1.0.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 bool parametric)
284{
285 m_bindings.push_back({
286 .targets = target,
287 .source = source,
288 .components = components,
289 .temporal = temporal,
290 .cluster = cluster,
291 .parametric = parametric,
292 });
293 invalidate();
294}
295
297 std::optional<uint32_t> cluster)
298{
299 if (any_flag(target & ~Kinesis::k_vector_targets)) {
301 "GpuFieldOperator::bind: mask {:#x} reaches bits {:#x} that a "
302 "three-component field cannot drive. Nothing was bound.",
303 static_cast<uint16_t>(target),
304 static_cast<uint16_t>(target & ~Kinesis::k_vector_targets));
305 return;
306 }
307
308 if (!accept(target, field.source, 3))
309 return;
310
311 store(target, field.source, 3, false, cluster);
312}
313
315 std::optional<uint32_t> cluster)
316{
317 if (target != FieldTarget::SCALAR) {
319 "GpuFieldOperator::bind: a scalar field binds only to SCALAR, got "
320 "mask {:#x}",
321 static_cast<uint16_t>(target));
322 return;
323 }
324
325 if (!accept(target, field.source, 1))
326 return;
327
328 store(target, field.source, 1, false, cluster);
329}
330
332 std::optional<uint32_t> cluster)
333{
334 if (target != FieldTarget::UV) {
336 "GpuFieldOperator::bind: a two-component field binds only to UV, got "
337 "mask {:#x}",
338 static_cast<uint16_t>(target));
339 return;
340 }
341
342 if (!accept(target, field.source, 2))
343 return;
344
345 store(target, field.source, 2, false, cluster);
346}
347
349 std::optional<uint32_t> cluster)
350{
351 if (any_flag(target & ~Kinesis::k_vector_targets)) {
353 "GpuFieldOperator::bind: mask {:#x} reaches bits {:#x} that a "
354 "three-component field cannot drive. Nothing was bound.",
355 static_cast<uint16_t>(target),
356 static_cast<uint16_t>(target & ~Kinesis::k_vector_targets));
357 return;
358 }
359
360 if (!accept(target, field.source, 3))
361 return;
362
363 store(target, field.source, 3, true, cluster);
364}
365
367 std::optional<uint32_t> cluster)
368{
369 if (target != FieldTarget::SCALAR) {
371 "GpuFieldOperator::bind: a scalar field binds only to SCALAR, got "
372 "mask {:#x}",
373 static_cast<uint16_t>(target));
374 return;
375 }
376
377 if (!accept(target, field.source, 1))
378 return;
379
380 store(target, field.source, 1, true, cluster);
381}
382
384 std::optional<uint32_t> cluster)
385{
386 if (target != FieldTarget::UV) {
388 "GpuFieldOperator::bind: a two-component field binds only to UV, got "
389 "mask {:#x}",
390 static_cast<uint16_t>(target));
391 return;
392 }
393
394 if (!accept(target, field.source, 2))
395 return;
396
397 store(target, field.source, 2, true, cluster);
398}
399
401 std::optional<uint32_t> cluster)
402{
403 if (any_flag(target & ~Kinesis::k_vector_targets)) {
405 "GpuFieldOperator::bind: mask {:#x} reaches bits {:#x} that a "
406 "three-component field cannot drive. Nothing was bound.",
407 static_cast<uint16_t>(target),
408 static_cast<uint16_t>(target & ~Kinesis::k_vector_targets));
409 return;
410 }
411
412 if (!accept(target, field.source, 3))
413 return;
414
415 store(target, field.source, 3, true, cluster, true);
416}
417
419 std::optional<uint32_t> cluster)
420{
421 if (target != FieldTarget::SCALAR) {
423 "GpuFieldOperator::bind: a scalar field binds only to SCALAR, got "
424 "mask {:#x}",
425 static_cast<uint16_t>(target));
426 return;
427 }
428
429 if (!accept(target, field.source, 1))
430 return;
431
432 store(target, field.source, 1, true, cluster, true);
433}
434
436 std::optional<uint32_t> cluster)
437{
438 if (target != FieldTarget::UV) {
440 "GpuFieldOperator::bind: a two-component field binds only to UV, got "
441 "mask {:#x}",
442 static_cast<uint16_t>(target));
443 return;
444 }
445
446 if (!accept(target, field.source, 2))
447 return;
448
449 store(target, field.source, 2, true, cluster, true);
450}
451
453{
454 return std::ranges::any_of(m_bindings,
455 [](const Binding& b) { return b.parametric; });
456}
457
459{
460 for (auto& b : m_bindings)
461 b.targets &= ~target;
462
463 std::erase_if(m_bindings,
464 [](const Binding& b) { return !any_flag(b.targets); });
465
466 invalidate();
467}
468
470{
471 if (m_vertex_binding == binding)
472 return;
473 m_vertex_binding = binding;
474 invalidate();
475}
476
478{
479 if (x == 0 || m_workgroup_size == x)
480 return;
482 invalidate();
483}
484
486{
487 m_spec_cache.reset();
488 ++m_revision;
489}
490
492{
493}
494
495void GpuFieldOperator::set_parameter(std::string_view param, double value)
496{
498 "GpuFieldOperator: unknown parameter '{}' ({})", param, value);
499}
500
502{
503 return std::ranges::any_of(m_bindings,
504 [](const Binding& b) { return b.cluster.has_value(); });
505}
506
507std::optional<double> GpuFieldOperator::query_state(std::string_view query) const
508{
509 if (query == "binding_count")
510 return static_cast<double>(m_bindings.size());
511 if (query == "stride_words")
512 return static_cast<double>(m_stride_words);
513 return std::nullopt;
514}
515
516std::optional<Portal::Graphics::ShaderSpec> GpuFieldOperator::build_spec() const
517{
518 if (m_spec_cache.has_value())
519 return m_spec_cache;
520
521 if (m_bindings.empty())
522 return std::nullopt;
523
525 if (!position.has_value()) {
527 "GpuFieldOperator: layout carries no position attribute. Fields are "
528 "functions of position and cannot be evaluated without one.");
529 return std::nullopt;
530 }
531
532 const bool needs_cluster = needs_cluster_id();
533
538
539 if (needs_cluster) {
540 assemble.ssbo("cluster_id", Portal::Graphics::BindingDirection::Input,
542 }
543
544 assemble.pc("first_vertex", Kakshya::GpuDataFormat::UINT32)
545 .pc("vertex_count", Kakshya::GpuDataFormat::UINT32)
546 .pc("stride_words", Kakshya::GpuDataFormat::UINT32)
549
550 if (uses_params()) {
551 assemble.pc("params", Kakshya::GpuDataFormat::VEC4_F32);
552 }
553
554 assemble.workgroup(m_workgroup_size);
555
556 std::vector<std::string_view> emitted;
557 for (const auto& b : m_bindings) {
558 const auto seen = std::ranges::find(emitted, b.source.name);
559 if (seen != emitted.end())
560 continue;
561 emitted.push_back(b.source.name);
562 assemble.function(b.source.return_type, b.source.name,
563 b.source.params, b.source.body);
564 }
565
566 const uint32_t pw = position->first;
567 std::string body;
568 body += " if (i >= vertex_count) { return; }\n";
569 body += " uint b = (first_vertex + i) * stride_words;\n";
570 body += " vec3 p = (geometry * vec4(vertices[b + " + word(pw)
571 + "], vertices[b + " + word(pw + 1)
572 + "], vertices[b + " + word(pw + 2) + "], 1.0f)).xyz;\n";
573
574 if (needs_cluster) {
575 body += " uint my_cluster = cluster_id[first_vertex + i];\n";
576 }
577
579 const bool touched = std::ranges::any_of(m_bindings,
580 [t](const Binding& b) { return has_flag(b.targets, t); });
581 if (!touched)
582 continue;
583
584 const auto slot = resolve_target(t);
585 if (!slot.has_value())
586 continue;
587
588 const auto [w, components] = *slot;
589 const std::string acc = std::string("acc_") + target_name(t);
590
591 body += "\n " + std::string(glsl_type(components)) + " " + acc
592 + " = " + glsl_zero(components) + ";\n";
593
594 for (const auto& b : m_bindings) {
595 if (!has_flag(b.targets, t))
596 continue;
597
598 const char* arguments = "(p)";
599 if (b.parametric) {
600 arguments = "(p, time, params)";
601 } else if (b.temporal) {
602 arguments = "(p, time)";
603 }
604 const std::string call = std::string(b.source.name) + arguments;
605
606 if (b.cluster.has_value()) {
607 body += " if (my_cluster == " + word(*b.cluster) + ") { "
608 + acc + " += " + call + "; }\n";
609 } else {
610 body += " " + acc + " += " + call + ";\n";
611 }
612 }
613
615 body += " " + acc + " = inverse(mat3(geometry)) * " + acc + ";\n";
616 } else if (t == Kinesis::FieldTarget::NORMAL) {
617 body += " " + acc + " = transpose(mat3(geometry)) * " + acc + ";\n";
618 }
619
621 const std::string len = std::string("len_") + target_name(t);
622 body += " float " + len + " = length(" + acc + ");\n";
623 body += " " + acc + " = " + len + " > 1e-6f ? " + acc + " / "
624 + len + " : " + acc + ";\n";
625 }
626
627 const char* assign = target_accumulates(t) ? " += " : " = ";
628 for (uint32_t k = 0; k < components; ++k) {
629 body += " vertices[b + " + word(w + k) + "]" + assign
630 + acc + (components == 1 ? "" : k_swizzle[k]) + ";\n";
631 }
632 }
633
634 assemble.kernel(Portal::Graphics::KernelSource { .body = std::move(body) });
635
636 m_spec_cache = assemble.build();
637 return m_spec_cache;
638}
639
640} // namespace MayaFlux::Nodes::Network
#define MF_ERROR(comp, ctx,...)
#define MF_RT_TRACE(comp, ctx,...)
std::optional< glm::vec3 > position
Definition Decoder.cpp:93
bool size
float rate
size_t a
size_t b
glm::vec3 value
size_t count
uint32_t pw
float threshold
void invalidate()
Clear the cached spec and bump revision().
void set_workgroup_size(uint32_t x)
Workgroup size along x.
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.
bool uses_params() const
Whether any current binding is parametric, so the spec carries the block.
void store(FieldTarget, const Kinesis::FieldSource &, uint32_t, bool, std::optional< uint32_t > cluster, bool parametric=false)
Store a binding after validation.
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:132
@ VEC4_F32
glm::vec4 (32-bit components)
@ FLOAT32
32-bit float (standard GPU)
@ MAT4_F32
glm::mat4 (column-major), not a sampled image format
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:77
Parsed representation of a stringified field lambda.
An authored expression of position, time and a parameter block, shader-only.
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.