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,
288 .components = components,
289 .temporal = temporal,
291 .parametric = parametric,
297 std::optional<uint32_t> cluster)
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),
315 std::optional<uint32_t> cluster)
317 if (target != FieldTarget::SCALAR) {
319 "GpuFieldOperator::bind: a scalar field binds only to SCALAR, got "
321 static_cast<uint16_t
>(target));
332 std::optional<uint32_t> cluster)
334 if (target != FieldTarget::UV) {
336 "GpuFieldOperator::bind: a two-component field binds only to UV, got "
338 static_cast<uint16_t
>(target));
349 std::optional<uint32_t> cluster)
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),
367 std::optional<uint32_t> cluster)
369 if (target != FieldTarget::SCALAR) {
371 "GpuFieldOperator::bind: a scalar field binds only to SCALAR, got "
373 static_cast<uint16_t
>(target));
384 std::optional<uint32_t> cluster)
386 if (target != FieldTarget::UV) {
388 "GpuFieldOperator::bind: a two-component field binds only to UV, got "
390 static_cast<uint16_t
>(target));
401 std::optional<uint32_t> cluster)
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),
415 store(target, field.
source, 3,
true, cluster,
true);
419 std::optional<uint32_t> cluster)
421 if (target != FieldTarget::SCALAR) {
423 "GpuFieldOperator::bind: a scalar field binds only to SCALAR, got "
425 static_cast<uint16_t
>(target));
432 store(target, field.
source, 1,
true, cluster,
true);
436 std::optional<uint32_t> cluster)
438 if (target != FieldTarget::UV) {
440 "GpuFieldOperator::bind: a two-component field binds only to UV, got "
442 static_cast<uint16_t
>(target));
449 store(target, field.
source, 2,
true, cluster,
true);
455 [](
const Binding&
b) {
return b.parametric; });
461 b.targets &= ~target;
464 [](
const Binding&
b) {
return !any_flag(
b.targets); });
498 "GpuFieldOperator: unknown parameter '{}' ({})", param,
value);
504 [](
const Binding&
b) {
return b.cluster.has_value(); });
509 if (query ==
"binding_count")
510 return static_cast<double>(
m_bindings.size());
511 if (query ==
"stride_words")
527 "GpuFieldOperator: layout carries no position attribute. Fields are "
528 "functions of position and cannot be evaluated without one.");
556 std::vector<std::string_view> emitted;
558 const auto seen = std::ranges::find(emitted,
b.source.name);
559 if (seen != emitted.end())
561 emitted.push_back(
b.source.name);
562 assemble.
function(
b.source.return_type,
b.source.name,
563 b.source.params,
b.source.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";
575 body +=
" uint my_cluster = cluster_id[first_vertex + i];\n";
579 const bool touched = std::ranges::any_of(
m_bindings,
580 [t](
const Binding&
b) {
return has_flag(
b.targets, t); });
585 if (!slot.has_value())
588 const auto [w, components] = *slot;
589 const std::string acc = std::string(
"acc_") + target_name(t);
591 body +=
"\n " + std::string(glsl_type(components)) +
" " + acc
592 +
" = " + glsl_zero(components) +
";\n";
595 if (!has_flag(
b.targets, t))
598 const char* arguments =
"(p)";
600 arguments =
"(p, time, params)";
601 }
else if (
b.temporal) {
602 arguments =
"(p, time)";
604 const std::string call = std::string(
b.source.name) + arguments;
606 if (
b.cluster.has_value()) {
607 body +=
" if (my_cluster == " + word(*
b.cluster) +
") { "
608 + acc +
" += " + call +
"; }\n";
610 body +=
" " + acc +
" += " + call +
";\n";
615 body +=
" " + acc +
" = inverse(mat3(geometry)) * " + acc +
";\n";
617 body +=
" " + acc +
" = transpose(mat3(geometry)) * " + acc +
";\n";
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";
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";
#define MF_ERROR(comp, ctx,...)
#define MF_RT_TRACE(comp, ctx,...)
std::optional< glm::vec3 > position
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.
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.
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.
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.
@ 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.
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.
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.