12 [[nodiscard]]
const char* glsl_type(uint32_t components)
noexcept
27 [[nodiscard]] std::string glsl_zero(uint32_t components)
29 return components == 1 ?
"0.0f" : std::string(glsl_type(components)) +
"(0.0f)";
32 [[nodiscard]] std::string word(uint32_t n)
34 return std::to_string(n) +
"u";
41 case FieldTarget::POSITION:
43 case FieldTarget::COLOR:
45 case FieldTarget::NORMAL:
47 case FieldTarget::TANGENT:
49 case FieldTarget::SCALAR:
74 [[nodiscard]] std::optional<Kakshya::DataModality>
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;
91 return DataModality::TEXTURE_COORDS_2D;
104 case DataModality::VERTEX_POSITIONS_3D:
105 case DataModality::VERTEX_COLORS_RGB:
106 case DataModality::VERTEX_NORMALS_3D:
107 case DataModality::VERTEX_TANGENTS_3D:
109 case DataModality::TEXTURE_COORDS_2D:
111 case DataModality::SCALAR_F32:
118 constexpr std::array<const char*, 3> k_swizzle {
".x",
".y",
".z" };
123 : m_layout(
std::move(layout))
124 , m_field_config(config)
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.",
187std::optional<std::pair<uint32_t, uint32_t>>
190 const auto modality = target_modality(target);
191 if (!modality.has_value())
196 return a.component_modality == *modality;
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);
210 return std::make_pair(it->offset_in_vertex / 4, modality_components(*modality));
220 "GpuFieldOperator::bind: operator is inert, its layout stride was "
221 "rejected at construction");
225 if (!any_flag(target)) {
227 "GpuFieldOperator::bind: empty target mask");
231 if (!source.valid()) {
233 "GpuFieldOperator::bind: field '{}' has no usable shader half. A "
234 "DualField lambda must carry a trailing return type.",
240 if (!has_flag(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));
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);
261 const auto clash = std::ranges::find_if(
m_bindings,
263 return b.source.name == source.name &&
b.source.body != source.body;
268 "GpuFieldOperator::bind: a different field named '{}' is already "
269 "bound. Emitted function names must be unique; rename one field.",
282 std::optional<uint32_t> cluster)
287 .components = components,
288 .temporal = temporal,
295 std::optional<uint32_t> cluster)
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),
313 std::optional<uint32_t> cluster)
315 if (target != FieldTarget::SCALAR) {
317 "GpuFieldOperator::bind: a scalar field binds only to SCALAR, got "
319 static_cast<uint16_t
>(target));
330 std::optional<uint32_t> cluster)
332 if (target != FieldTarget::UV) {
334 "GpuFieldOperator::bind: a two-component field binds only to UV, got "
336 static_cast<uint16_t
>(target));
347 std::optional<uint32_t> cluster)
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),
365 std::optional<uint32_t> cluster)
367 if (target != FieldTarget::SCALAR) {
369 "GpuFieldOperator::bind: a scalar field binds only to SCALAR, got "
371 static_cast<uint16_t
>(target));
382 std::optional<uint32_t> cluster)
384 if (target != FieldTarget::UV) {
386 "GpuFieldOperator::bind: a two-component field binds only to UV, got "
388 static_cast<uint16_t
>(target));
401 b.targets &= ~target;
404 [](
const Binding&
b) {
return !any_flag(
b.targets); });
438 "GpuFieldOperator: unknown parameter '{}' ({})", param,
value);
444 [](
const Binding&
b) {
return b.cluster.has_value(); });
449 if (query ==
"binding_count")
450 return static_cast<double>(
m_bindings.size());
451 if (query ==
"stride_words")
465 if (!position.has_value()) {
467 "GpuFieldOperator: layout carries no position attribute. Fields are "
468 "functions of position and cannot be evaluated without one.");
490 std::vector<std::string_view> emitted;
492 const auto seen = std::ranges::find(emitted,
b.source.name);
493 if (seen != emitted.end())
495 emitted.push_back(
b.source.name);
496 assemble.
function(
b.source.return_type,
b.source.name,
497 b.source.params,
b.source.body);
500 const uint32_t pw = position->first;
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";
509 body +=
" uint my_cluster = cluster_id[first_vertex + i];\n";
513 const bool touched = std::ranges::any_of(
m_bindings,
514 [t](
const Binding&
b) {
return has_flag(
b.targets, t); });
519 if (!slot.has_value())
522 const auto [w, components] = *slot;
523 const std::string acc = std::string(
"acc_") + target_name(t);
525 body +=
"\n " + std::string(glsl_type(components)) +
" " + acc
526 +
" = " + glsl_zero(components) +
";\n";
529 if (!has_flag(
b.targets, t))
532 const std::string call = std::string(
b.source.name)
533 + (
b.temporal ?
"(p, time)" :
"(p)");
535 if (
b.cluster.has_value()) {
536 body +=
" if (my_cluster == " + word(*
b.cluster) +
") { "
537 + acc +
" += " + call +
"; }\n";
539 body +=
" " + acc +
" += " + call +
";\n";
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";
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";
#define MF_ERROR(comp, ctx,...)
#define MF_RT_TRACE(comp, ctx,...)
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.
uint32_t m_vertex_binding
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.
SpatialFieldConfig m_field_config
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.
std::vector< Binding > m_bindings
uint32_t m_workgroup_size
Kakshya::VertexLayout m_layout
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.
Assemble & start_binding(uint32_t n)
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.
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.
Parsed representation of a stringified field lambda.
An authored expression of position and time, shader-only.
One target and the field driving it.
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.