18 buffer->declare_state(
"mutation_claimed_by",
hash.
particle_count,
sizeof(uint32_t),
false);
19 buffer->declare_state(
"mutation_swallow_count",
hash.
particle_count,
sizeof(uint32_t),
false);
20 buffer->declare_state(
"mutation_claim_events", 1,
sizeof(uint32_t),
false);
21 buffer->declare_state(
"mutation_accreted_mass",
hash.
particle_count,
sizeof(
float),
false);
37 std::pair<uint32_t, uint32_t> require_color_and_size_offset(
38 const std::shared_ptr<Nodes::Network::GpuFieldOperator>& particle_op)
41 error<std::invalid_argument>(
44 std::source_location::current(),
45 "require_color_and_size_offset: null particle operator");
48 const auto& layout = particle_op->get_layout();
51 const auto size_attr = std::ranges::find_if(layout.attributes,
52 [](
const auto& attr) { return attr.name ==
"size"; });
53 const bool have_size = size_attr != layout.attributes.end() && size_attr->offset_in_vertex % 4 == 0;
55 if (!color_offset.has_value() || !have_size) {
56 error<std::invalid_argument>(
59 std::source_location::current(),
60 "require_color_and_size_offset: vertex layout is missing a colour or "
61 "word-aligned size attribute");
64 return { *color_offset, size_attr->offset_in_vertex / 4 };
75 ShaderSpec build_claim_init_spec()
77 ShaderSpec::Assemble assemble;
86 body +=
" if (i >= particle_count) { return; }\n";
87 body +=
" claimed_by[i] = i;\n";
88 body +=
" swallow_count[i] = 0u;\n";
89 body +=
" if (i == 0u) { claim_events[0] = 0u; }\n";
91 assemble.kernel(KernelSource { .body = std::move(body) });
93 return assemble.build();
100 {
FieldBinding { .
name =
"claimed_by", .binding = 0, .field =
"mutation_claimed_by" },
101 FieldBinding { .
name =
"swallow_count", .binding = 1, .field =
"mutation_swallow_count" },
102 FieldBinding { .
name =
"claim_events", .binding = 2, .field =
"mutation_claim_events" } },
103 build_claim_init_spec())
104 , m_params { .particle_count = config.hash.particle_count }
119 ShaderSpec build_claim_spec(
bool gate_alive)
121 ShaderSpec::Assemble assemble;
153 body +=
" if (i >= particle_count) { return; }\n";
155 body +=
" if (alive[i] == 0u) { return; }\n";
157 body +=
" uint b = i * stride_words;\n";
158 body +=
" vec3 p = vec3(vertices[b + position_offset], "
159 "vertices[b + position_offset + 1u], vertices[b + position_offset + 2u]);\n";
160 body +=
" float capture_radius = max(absorb_radius, "
161 "pow(max(accreted_mass[i], 0.0001), 1.0 / 3.0) * capture_growth);\n";
162 body +=
" uint my_cluster = cluster_id[i];\n";
164 body +=
" int reach = clamp(int(ceil(capture_radius / cell_size)), 1, 8);\n";
167 .skip_self =
"j <= i",
168 .cluster_scoped =
true,
169 .radius =
"capture_radius",
170 .on_hit =
"atomicMin(claimed_by[j], i);\natomicAdd(claim_events[0], 1u);",
173 assemble.kernel(KernelSource { .body = std::move(body) });
175 return assemble.build();
178 std::vector<NetworkStateFieldProcessor::FieldBinding> claim_bindings(
bool gate_alive)
180 std::vector<NetworkStateFieldProcessor::FieldBinding> bindings {
181 { .name =
"vertices", .binding = 0, .field = {} },
182 { .name =
"cell_start", .binding = 1, .field =
"hash_cell_start" },
183 { .name =
"cell_count", .binding = 2, .field =
"hash_cell_count" },
184 { .name =
"particle_index", .binding = 3, .field =
"hash_particle_index" },
185 { .name =
"claimed_by", .binding = 4, .field =
"mutation_claimed_by" },
186 { .name =
"claim_events", .binding = 5, .field =
"mutation_claim_events" },
187 { .name =
"accreted_mass", .binding = 6, .field =
"mutation_accreted_mass" },
188 { .name =
"cluster_id", .binding = 7, .field =
"hash_cluster_id" },
191 bindings.push_back({ .name =
"alive", .binding = 8, .field =
"mutation_alive" });
200 std::shared_ptr<Nodes::Network::GpuFieldOperator> particle_op,
204 .particle_count = config.hash.particle_count,
205 .stride_words = config.hash.stride_words,
206 .position_offset = config.hash.position_word_offset,
207 .absorb_radius = config.absorb_radius,
208 .capture_growth = particle_op->get_field_config().capture_growth,
209 .grid_min_x = config.hash.grid_min.x,
210 .grid_min_y = config.hash.grid_min.y,
211 .grid_min_z = config.hash.grid_min.z,
212 .cell_size = config.hash.cell_size,
213 .dim_x = config.hash.grid_dims.x,
214 .dim_y = config.hash.grid_dims.y,
215 .dim_z = config.hash.grid_dims.z,
216 .cross_cluster = particle_op->get_field_config().cross_cluster ? 1U : 0U,
218 , m_particle_op(
std::move(particle_op))
219 , m_built_revision(m_particle_op->revision())
230 const std::shared_ptr<VKBuffer>& buffer)
233 const auto& pconfig = m_particle_op->get_field_config();
234 m_params.capture_growth = pconfig.capture_growth;
235 m_params.cross_cluster = pconfig.cross_cluster ? 1U : 0U;
236 set_push_constant_data(m_params);
246 ShaderSpec build_claim_flatten_spec()
248 ShaderSpec::Assemble assemble;
255 body +=
" if (i >= particle_count) { return; }\n";
256 body +=
" claimed_by[i] = claimed_by[claimed_by[i]];\n";
258 assemble.kernel(KernelSource { .body = std::move(body) });
260 return assemble.build();
270 uint32_t rounds_to_flatten(uint32_t n)
273 while ((1U <<
k) < n) {
276 return std::max(1U,
k);
283 {
FieldBinding { .
name =
"claimed_by", .binding = 0, .field =
"mutation_claimed_by" } },
284 build_claim_flatten_spec())
285 , m_params { .particle_count = config.hash.particle_count }
297 const std::shared_ptr<VKBuffer>& ,
304 vk::AccessFlagBits::eShaderWrite,
305 vk::AccessFlagBits::eShaderRead | vk::AccessFlagBits::eShaderWrite,
306 vk::PipelineStageFlagBits::eComputeShader,
307 vk::PipelineStageFlagBits::eComputeShader);
316 ShaderSpec build_claim_accumulate_spec(
bool gate_alive,
bool transfer_on_claim)
318 ShaderSpec::Assemble assemble;
329 if (transfer_on_claim) {
339 body +=
" if (i >= particle_count) { return; }\n";
341 body +=
" if (alive[i] == 0u) {\n";
342 body +=
" vertices[i * stride_words + size_offset] = 0.0;\n";
343 body +=
" return;\n";
346 body +=
" uint root = claimed_by[i];\n";
347 body +=
" if (root != i) {\n";
348 if (transfer_on_claim) {
349 body +=
" if (cluster_id[root] != cluster_id[i]) { return; }\n";
351 body +=
" atomicAdd(swallow_count[root], 1u);\n";
353 body +=
" alive[i] = 0u;\n";
354 body +=
" vertices[i * stride_words + size_offset] = 0.0;\n";
358 assemble.kernel(KernelSource { .body = std::move(body) });
360 return assemble.build();
363 std::vector<NetworkStateFieldProcessor::FieldBinding> claim_accumulate_bindings(
364 bool gate_alive,
bool transfer_on_claim)
366 std::vector<NetworkStateFieldProcessor::FieldBinding> bindings {
367 { .name =
"claimed_by", .binding = 0, .field =
"mutation_claimed_by" },
368 { .name =
"swallow_count", .binding = 1, .field =
"mutation_swallow_count" },
372 bindings.push_back({ .name =
"alive", .binding = next++, .field =
"mutation_alive" });
373 bindings.push_back({ .name =
"vertices", .binding = next++, .field = {} });
375 if (transfer_on_claim) {
376 bindings.push_back({ .name =
"cluster_id", .binding = next++, .field =
"hash_cluster_id" });
387 const std::shared_ptr<Nodes::Network::GpuFieldOperator>& particle_op,
388 bool transfer_on_claim)
390 claim_accumulate_bindings(live_count != 0, transfer_on_claim),
391 build_claim_accumulate_spec(live_count != 0, transfer_on_claim))
393 .particle_count = config.hash.particle_count,
394 .stride_words = config.hash.stride_words,
395 .size_offset = live_count != 0 ? require_color_and_size_offset(particle_op).second : 0U,
397 , m_physics_op(physics_op)
398 , m_live_count(live_count != 0 ? live_count : config.hash.particle_count)
416 uint32_t claim_events = 0;
418 network_buffer->read_state_slot(
"mutation_claim_events"),
421 if (claim_events == 0u) {
426 network_buffer->read_state_slot(
"mutation_claimed_by"),
436 if (!accreted_mass.empty()) {
439 network_buffer->write_state_slot(
"mutation_accreted_mass"),
440 accreted_mass.data(),
441 accreted_mass.size() *
sizeof(
float),
445 std::copy_n(accreted_mass.begin(),
449 network_buffer->write_state_slot(
"mutation_accreted_mass"),
463 ShaderSpec build_claim_swallow_spec(
bool transfer_on_claim)
465 ShaderSpec::Assemble assemble;
471 if (transfer_on_claim) {
488 body +=
" if (i >= particle_count) { return; }\n";
489 body +=
" uint root = claimed_by[i];\n";
490 if (transfer_on_claim) {
491 body +=
" if (root != i && cluster_id[root] != cluster_id[i]) { return; }\n";
493 body +=
" uint b = i * stride_words;\n";
494 body +=
" uint rb = root * stride_words;\n";
496 body +=
" float count = float(swallow_count[root]);\n";
497 body +=
" float size = clamp(base_size + count * growth_rate, base_size, max_size);\n";
498 body +=
" float t = clamp((size - base_size) / max(max_size - base_size, 0.001), 0.0, 1.0);\n";
499 body +=
" vec3 ember = vec3(0.03, 0.0, 0.06);\n";
500 body +=
" vec3 fire = vec3(0.95, 0.25, 0.02);\n";
501 body +=
" vec3 white_hot = vec3(1.0, 0.95, 0.7);\n";
502 body +=
" vec3 heat = t < 0.5\n";
503 body +=
" ? mix(ember, fire, t * 2.0)\n";
504 body +=
" : mix(fire, white_hot, (t - 0.5) * 2.0);\n";
506 body +=
" if (root == i) {\n";
507 body +=
" vertices[b + size_offset] = size;\n";
508 body +=
" vertices[b + color_offset] = heat.x;\n";
509 body +=
" vertices[b + color_offset + 1u] = heat.y;\n";
510 body +=
" vertices[b + color_offset + 2u] = heat.z;\n";
511 body +=
" return;\n";
514 body +=
" vec3 root_pos = vec3(vertices[rb + position_offset], "
515 "vertices[rb + position_offset + 1u], vertices[rb + position_offset + 2u]);\n";
516 body +=
" vertices[b + position_offset] = root_pos.x;\n";
517 body +=
" vertices[b + position_offset + 1u] = root_pos.y;\n";
518 body +=
" vertices[b + position_offset + 2u] = root_pos.z;\n";
520 body +=
" vec3 dim_col = heat * dim_factor;\n";
521 body +=
" vertices[b + color_offset] = dim_col.x;\n";
522 body +=
" vertices[b + color_offset + 1u] = dim_col.y;\n";
523 body +=
" vertices[b + color_offset + 2u] = dim_col.z;\n";
525 assemble.kernel(KernelSource { .body = std::move(body) });
527 return assemble.build();
530 std::vector<NetworkStateFieldProcessor::FieldBinding> claim_swallow_bindings(
bool transfer_on_claim)
532 std::vector<NetworkStateFieldProcessor::FieldBinding> bindings {
533 { .name =
"vertices", .binding = 0, .field = {} },
534 { .name =
"claimed_by", .binding = 1, .field =
"mutation_claimed_by" },
535 { .name =
"swallow_count", .binding = 2, .field =
"mutation_swallow_count" },
537 if (transfer_on_claim) {
538 bindings.push_back({ .name =
"cluster_id", .binding = 3, .field =
"hash_cluster_id" });
547 std::shared_ptr<Nodes::Network::GpuFieldOperator> particle_op,
548 bool transfer_on_claim)
550 claim_swallow_bindings(transfer_on_claim),
551 build_claim_swallow_spec(transfer_on_claim))
553 .particle_count = config.hash.particle_count,
554 .stride_words = config.hash.stride_words,
555 .position_offset = config.hash.position_word_offset,
556 .color_offset = require_color_and_size_offset(particle_op).first,
557 .size_offset = require_color_and_size_offset(particle_op).second,
558 .base_size = particle_op->get_field_config().swallow_base_size,
559 .growth_rate = particle_op->get_field_config().swallow_growth_rate,
560 .max_size = particle_op->get_field_config().swallow_max_size,
561 .dim_factor = particle_op->get_field_config().swallow_dim_factor,
563 , m_particle_op(
std::move(particle_op))
564 , m_built_revision(m_particle_op->revision())
575 const std::shared_ptr<VKBuffer>& buffer)
578 const auto& pconfig = m_particle_op->get_field_config();
579 m_params.base_size = pconfig.swallow_base_size;
580 m_params.growth_rate = pconfig.swallow_growth_rate;
581 m_params.max_size = pconfig.swallow_max_size;
582 m_params.dim_factor = pconfig.swallow_dim_factor;
583 set_push_constant_data(m_params);
593 ShaderSpec build_claim_transfer_spec()
595 ShaderSpec::Assemble assemble;
603 body +=
" if (i >= particle_count) { return; }\n";
604 body +=
" uint root = claimed_by[i];\n";
605 body +=
" if (root == i) { return; }\n";
606 body +=
" if (cluster_id[root] == cluster_id[i]) { return; }\n";
607 body +=
" cluster_id[i] = cluster_id[root];\n";
609 assemble.kernel(KernelSource { .body = std::move(body) });
611 return assemble.build();
618 {
FieldBinding { .
name =
"claimed_by", .binding = 0, .field =
"mutation_claimed_by" },
619 FieldBinding { .
name =
"cluster_id", .binding = 1, .field =
"hash_cluster_id" } },
620 build_claim_transfer_spec())
621 , m_params { .particle_count = config.hash.particle_count }
std::vector< uint32_t > m_claimed_by_readback
std::shared_ptr< VKBuffer > m_readback_staging
Nodes::Network::PhysicsOperator * m_physics_op
void processing_function(const std::shared_ptr< Buffer > &buffer) override
Dispatch as usual, then conditionally read back the result.
std::shared_ptr< VKBuffer > m_upload_staging
void on_buffer_ready() override
Hook for subclass setup, called at the end of on_attach.
std::vector< float > m_accreted_mass_padded
ClaimAccumulateProcessor(const MutationConfig &config, Nodes::Network::PhysicsOperator *physics_op, uint32_t live_count=0, const std::shared_ptr< Nodes::Network::GpuFieldOperator > &particle_op=nullptr, bool transfer_on_claim=false)
ClaimFlattenProcessor(const MutationConfig &config)
void on_iteration_barrier(Portal::Graphics::CommandBufferID cmd_id, const std::shared_ptr< VKBuffer > &buffer, uint32_t index) override
Called after each iteration except the last.
void on_buffer_ready() override
Hook for subclass setup, called at the end of on_attach.
void on_buffer_ready() override
Hook for subclass setup, called at the end of on_attach.
ClaimInitProcessor(const MutationConfig &config)
ClaimProcessor(const MutationConfig &config, std::shared_ptr< Nodes::Network::GpuFieldOperator > particle_op, bool gate_alive=false)
std::shared_ptr< Nodes::Network::GpuFieldOperator > m_particle_op
uint64_t m_built_revision
bool on_before_execute(Portal::Graphics::CommandBufferID cmd_id, const std::shared_ptr< VKBuffer > &buffer) override
Re-sync capture_growth from particle_op when its revision changes.
void on_buffer_ready() override
Hook for subclass setup, called at the end of on_attach.
std::shared_ptr< Nodes::Network::GpuFieldOperator > m_particle_op
void on_buffer_ready() override
Hook for subclass setup, called at the end of on_attach.
ClaimSwallowProcessor(const MutationConfig &config, std::shared_ptr< Nodes::Network::GpuFieldOperator > particle_op, bool transfer_on_claim=false)
bool on_before_execute(Portal::Graphics::CommandBufferID cmd_id, const std::shared_ptr< VKBuffer > &buffer) override
Called before each process callback.
uint64_t m_built_revision
void on_buffer_ready() override
Hook for subclass setup, called at the end of on_attach.
ClaimTransferProcessor(const MutationConfig &config)
void set_iteration_count(uint32_t count)
Set how many dispatches are recorded per execute cycle.
void dispatch_one_thread_per(uint32_t element_count, const T ¶ms)
Configure manual dispatch for one thread per element, and stage the given push constant data.
bool guard_and_resync(Portal::Graphics::CommandBufferID cmd_id, const std::shared_ptr< VKBuffer > &buffer, uint64_t current_revision, uint64_t &built_revision, F &&reload)
Base on_before_execute guard, then a revision-gated tuning reload.
void processing_function(const std::shared_ptr< Buffer > &buffer) override
Rewrite field descriptors, then run the shader.
const std::shared_ptr< NetworkGeometryBuffer > & get_network_buffer() const
The attached buffer, or null if attachment failed validation.
ComputeProcessor operating on named state fields of a NetworkGeometryBuffer, plus optionally the buff...
std::span< const float > get_accreted_mass_span()
Every particle's currently accreted mass, in global index order.
void sync_bonds_from_claims(std::span< const uint32_t > claimed_by)
Adopt this cycle's GPU claim/absorption clustering as bonds.
void clear_bonds()
Drop every adopted bond.
N-body physics simulation with point rendering.
void buffer_barrier(CommandBufferID cmd_id, vk::Buffer buffer, vk::AccessFlags src_access, vk::AccessFlags dst_access, vk::PipelineStageFlags src_stage, vk::PipelineStageFlags dst_stage)
Insert buffer memory barrier.
void append_neighbour_walk(std::string &body, const NeighbourWalk &walk)
Append the spatial-hash neighbour-gather loop nest to a kernel body.
void download_back_buffer(const VKBufferResources::GenerationSlot &slot, void *data, size_t size, std::shared_ptr< VKBuffer > &staging)
Download a raw back_buffers slot to host memory.
void upload_back_buffer(const VKBufferResources::GenerationSlot &slot, const void *data, size_t size, std::shared_ptr< VKBuffer > &staging)
Upload host memory into a raw back_buffers slot.
@ BufferProcessing
Buffer processing (Buffers::BufferManager, processing chains)
@ Buffers
Buffers, Managers, processors and processing chains.
MAYAFLUX_API ShaderFoundry & get_shader_foundry()
Get the global shader compiler instance.
BindingDirection
Data flow direction for a shader binding slot.
void declare_fields(const std::shared_ptr< NetworkGeometryBuffer > &buffer) const
Declare the state fields the claim pipeline reads and writes.
Parameters for the deterministic pairwise claim protocol built on top of a completed spatial hash.
std::string name
Shader binding name, as declared in ShaderConfig.
One shader binding and where it draws from.
Parsed representation of a user-supplied kernel lambda.
Complete declarative description of a generated compute shader.