MayaFlux 0.5.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
SpatialHashProcessor.cpp
Go to the documentation of this file.
2
5
10
14
15namespace MayaFlux::Buffers {
16
17std::optional<SpatialHashConfig> SpatialHashConfig::from_network(
18 const std::shared_ptr<NetworkGeometryBuffer>& buffer, float cell_size)
19{
20 if (cell_size <= 0.0F) {
22 "SpatialHashConfig::from_network: cell_size {} is not positive", cell_size);
23 return std::nullopt;
24 }
25
26 const auto network = buffer->get_network();
27
29 if (auto particle_net = std::dynamic_pointer_cast<Nodes::Network::ParticleNetwork>(network)) {
30 bounds = particle_net->get_bounds();
31 } else if (auto cloud_net = std::dynamic_pointer_cast<Nodes::Network::PointCloudNetwork>(network)) {
32 bounds = cloud_net->get_bounds();
33 } else {
35 "SpatialHashConfig::from_network: buffer's network is neither a ParticleNetwork "
36 "nor a PointCloudNetwork");
37 return std::nullopt;
38 }
39
40 auto* graphics_op = dynamic_cast<Nodes::Network::GraphicsOperator*>(network->get_operator());
41 if (!graphics_op) {
43 "SpatialHashConfig::from_network: network's primary operator is not a "
44 "GraphicsOperator, or none is set. create_operator<PhysicsOperator>() (or "
45 "equivalent) must run before this.");
46 return std::nullopt;
47 }
48
49 const auto layout = graphics_op->get_vertex_layout();
50
51 if (layout.stride_bytes == 0 || layout.stride_bytes % 4 != 0) {
53 "SpatialHashConfig::from_network: stride {} is not a nonzero multiple of 4",
54 layout.stride_bytes);
55 return std::nullopt;
56 }
57
58 const auto position_offset = layout.find_word_offset(Kakshya::DataModality::VERTEX_POSITIONS_3D);
59 if (!position_offset.has_value()) {
61 "SpatialHashConfig::from_network: vertex layout carries no word-aligned "
62 "position attribute");
63 return std::nullopt;
64 }
65
66 const glm::vec3 extent = bounds.max - bounds.min;
67
68 const glm::uvec3 dims {
69 std::max(1U, static_cast<uint32_t>(std::ceil(extent.x / cell_size))),
70 std::max(1U, static_cast<uint32_t>(std::ceil(extent.y / cell_size))),
71 std::max(1U, static_cast<uint32_t>(std::ceil(extent.z / cell_size))),
72 };
73
74 return SpatialHashConfig {
75 .grid_min = bounds.min,
76 .grid_dims = dims,
77 .cell_size = cell_size,
78 .particle_count = static_cast<uint32_t>(graphics_op->get_vertex_count()),
79 .stride_words = layout.stride_bytes / 4,
80 .position_word_offset = *position_offset,
81 };
82}
83
84void SpatialHashConfig::declare_fields(const std::shared_ptr<NetworkGeometryBuffer>& buffer) const
85{
86 const uint32_t cells = cell_count();
87 buffer->declare_state("hash_cell_count", cells, sizeof(uint32_t), false);
88 buffer->declare_state("hash_cell_start", cells, sizeof(uint32_t), false);
89 buffer->declare_state("hash_cell_cursor", cells, sizeof(uint32_t), false);
90 buffer->declare_state("hash_particle_index", particle_count, sizeof(uint32_t), false);
91
92 if (!buffer->has_state("hash_cluster_id")) {
93 buffer->declare_state("hash_cluster_id", particle_count, sizeof(uint32_t), false);
94 }
95}
96
97//=============================================================================
98// NetworkStateFieldProcessor
99//=============================================================================
100
102 std::vector<FieldBinding> bindings, const Portal::Graphics::ShaderSpec& spec)
103 : ComputeProcessor(spec)
104 , m_bindings(std::move(bindings))
105{
107}
108
110{
111 for (const auto& entry : m_bindings) {
112 m_config.bindings[entry.name]
113 = ShaderBinding(0, entry.binding, vk::DescriptorType::eStorageBuffer);
114 }
115}
116
118{
119 return std::ranges::all_of(m_bindings, [this](const FieldBinding& entry) {
120 if (entry.field.empty()) {
121 return true;
122 }
123 if (!m_buffer->has_state(entry.field)) {
124 MF_ERROR(Journal::Component::Buffers, Journal::Context::BufferProcessing,
125 "NetworkStateFieldProcessor: buffer has no state field '{}' for binding '{}'",
126 entry.field, entry.name);
127 return false;
128 }
129 return true;
130 });
131}
132
133void NetworkStateFieldProcessor::on_attach(const std::shared_ptr<Buffer>& buffer)
134{
136
137 m_buffer = std::dynamic_pointer_cast<NetworkGeometryBuffer>(buffer);
138 if (!m_buffer) {
139 return;
140 }
141
142 if (!validate_fields()) {
143 m_buffer.reset();
144 return;
145 }
146
148}
149
151{
152 if (!m_buffer || m_descriptor_set_ids.empty()) {
153 return;
154 }
155
156 auto& foundry = Portal::Graphics::get_shader_foundry();
157
158 for (const auto& entry : m_bindings) {
159 vk::Buffer handle {};
160 size_t bytes = 0;
161
162 if (entry.field.empty()) {
163 handle = m_buffer->get_buffer();
164 bytes = m_buffer->get_size_bytes();
165 } else {
166 handle = m_buffer->read_state_handle(entry.field);
167 bytes = m_buffer->get_state_bytes(entry.field);
168 }
169
170 foundry.update_descriptor_buffer(
171 m_descriptor_set_ids[0], entry.binding, vk::DescriptorType::eStorageBuffer,
172 handle, 0, bytes);
173 }
174}
175
180
181void NetworkStateFieldProcessor::processing_function(const std::shared_ptr<Buffer>& buffer)
182{
185 }
186
188}
189
192 const std::shared_ptr<VKBuffer>& buffer)
193{
194 return m_buffer && std::dynamic_pointer_cast<NetworkGeometryBuffer>(buffer) != nullptr;
195}
196
197//=============================================================================
198// Shared cell-index function, emitted identically into any spec that needs it
199//=============================================================================
200
201namespace {
202
206
207 /**
208 * @brief Emit the shared "which cell does this position fall in" function.
209 *
210 * Takes grid_min/cell_size/dims as arguments rather than reading them from
211 * push-constant-aliased locals: prelude functions are emitted above main()
212 * and cannot see main()'s locals, only pc.<field> directly, which would
213 * make this function shader-specific. Passing them as arguments keeps it
214 * identical text in both HashCountProcessor and HashScatterProcessor.
215 */
216 void add_cell_of_function(ShaderSpec::Assemble& assemble)
217 {
218 std::string body;
219 body += " ivec3 c = ivec3(floor((p - grid_min) / cell_size));\n";
220 body += " ivec3 d = ivec3(dims) - ivec3(1);\n";
221 body += " c = clamp(c, ivec3(0), d);\n";
222 body += " return uint(c.x) + uint(c.y) * dims.x + uint(c.z) * dims.x * dims.y;\n";
223
224 assemble.function("uint", "cell_of",
225 "vec3 p, vec3 grid_min, float cell_size, uvec3 dims", std::move(body));
226 }
227
228} // namespace
229
230//=============================================================================
231// HashClearProcessor
232//=============================================================================
233
234namespace {
235
236 ShaderSpec build_clear_spec()
237 {
238 ShaderSpec::Assemble assemble;
239 assemble
240 .ssbo("cell_count", BindingDirection::Output, Kakshya::GpuDataFormat::UINT32)
241 .pc("cell_count_total", Kakshya::GpuDataFormat::UINT32)
242 .workgroup(256);
243
244 std::string body;
245 body += " if (i < cell_count_total) {\n";
246 body += " cell_count[i] = 0u;\n";
247 body += " }\n";
248
249 assemble.kernel(KernelSource { .body = std::move(body) });
250
251 return assemble.build();
252 }
253
254} // namespace
255
258 { FieldBinding { .name = "cell_count", .binding = 0, .field = "hash_cell_count" } },
259 build_clear_spec())
260 , m_params { .cell_count_total = config.cell_count() }
261{
262}
263
268
269//=============================================================================
270// HashCountProcessor
271//=============================================================================
272
273namespace {
274
275 ShaderSpec build_count_spec(bool gate_alive)
276 {
277 ShaderSpec::Assemble assemble;
278 assemble
279 .ssbo("vertices", BindingDirection::Input, Kakshya::GpuDataFormat::FLOAT32)
280 .ssbo("cell_count", BindingDirection::InOut, Kakshya::GpuDataFormat::UINT32);
281
282 if (gate_alive) {
283 assemble.ssbo("alive", BindingDirection::Input, Kakshya::GpuDataFormat::UINT32);
284 }
285
286 assemble
287 .pc("particle_count", Kakshya::GpuDataFormat::UINT32)
288 .pc("stride_words", Kakshya::GpuDataFormat::UINT32)
289 .pc("position_offset", Kakshya::GpuDataFormat::UINT32)
290 .pc("grid_min_x", Kakshya::GpuDataFormat::FLOAT32)
291 .pc("grid_min_y", Kakshya::GpuDataFormat::FLOAT32)
292 .pc("grid_min_z", Kakshya::GpuDataFormat::FLOAT32)
293 .pc("cell_size", Kakshya::GpuDataFormat::FLOAT32)
297 .workgroup(256);
298
299 add_cell_of_function(assemble);
300
301 std::string body;
302 body += " if (i >= particle_count) { return; }\n";
303 if (gate_alive) {
304 body += " if (alive[i] == 0u) { return; }\n";
305 }
306 body += " uint b = i * stride_words;\n";
307 body += " vec3 p = vec3(vertices[b + position_offset], "
308 "vertices[b + position_offset + 1u], vertices[b + position_offset + 2u]);\n";
309 body += " vec3 gmin = vec3(grid_min_x, grid_min_y, grid_min_z);\n";
310 body += " uvec3 dims = uvec3(dim_x, dim_y, dim_z);\n";
311 body += " uint cell = cell_of(p, gmin, cell_size, dims);\n";
312 body += " atomicAdd(cell_count[cell], 1u);\n";
313
314 assemble.kernel(KernelSource { .body = std::move(body) });
315
316 return assemble.build();
317 }
318
319 std::vector<NetworkStateFieldProcessor::FieldBinding> count_bindings(bool gate_alive)
320 {
321 std::vector<NetworkStateFieldProcessor::FieldBinding> bindings {
322 { .name = "vertices", .binding = 0, .field = {} },
323 { .name = "cell_count", .binding = 1, .field = "hash_cell_count" },
324 };
325 if (gate_alive) {
326 bindings.push_back({ .name = "alive", .binding = 2, .field = "mutation_alive" });
327 }
328 return bindings;
329 }
330
331} // namespace
332
334 : NetworkStateFieldProcessor(count_bindings(gate_alive), build_count_spec(gate_alive))
335 , m_params(make_grid_push_constants(config))
336{
337}
338
343
344//=============================================================================
345// HashScanProcessor
346//=============================================================================
347
348namespace {
349
350 ShaderSpec build_scan_spec()
351 {
352 ShaderSpec::Assemble assemble;
353 assemble
354 .ssbo("cell_count", BindingDirection::Input, Kakshya::GpuDataFormat::UINT32)
355 .ssbo("cell_start", BindingDirection::Output, Kakshya::GpuDataFormat::UINT32)
356 .ssbo("cell_cursor", BindingDirection::Output, Kakshya::GpuDataFormat::UINT32)
357 .pc("cell_count_total", Kakshya::GpuDataFormat::UINT32)
358 .workgroup(1);
359
360 std::string body;
361 body += " if (i == 0u) {\n";
362 body += " uint running = 0u;\n";
363 body += " for (uint c = 0u; c < cell_count_total; c = c + 1u) {\n";
364 body += " cell_start[c] = running;\n";
365 body += " cell_cursor[c] = running;\n";
366 body += " running = running + cell_count[c];\n";
367 body += " }\n";
368 body += " }\n";
369
370 assemble.kernel(KernelSource { .body = std::move(body) });
371
372 return assemble.build();
373 }
374
375} // namespace
376
379 { FieldBinding { .name = "cell_count", .binding = 0, .field = "hash_cell_count" },
380 FieldBinding { .name = "cell_start", .binding = 1, .field = "hash_cell_start" },
381 FieldBinding { .name = "cell_cursor", .binding = 2, .field = "hash_cell_cursor" } },
382 build_scan_spec())
383 , m_params { .cell_count_total = config.cell_count() }
384{
385}
386
392
393//=============================================================================
394// HashScatterProcessor
395//=============================================================================
396
397namespace {
398
399 ShaderSpec build_scatter_spec(bool gate_alive)
400 {
401 ShaderSpec::Assemble assemble;
402 assemble
403 .ssbo("vertices", BindingDirection::Input, Kakshya::GpuDataFormat::FLOAT32)
404 .ssbo("cell_cursor", BindingDirection::InOut, Kakshya::GpuDataFormat::UINT32)
405 .ssbo("particle_index", BindingDirection::Output, Kakshya::GpuDataFormat::UINT32);
406
407 if (gate_alive) {
408 assemble.ssbo("alive", BindingDirection::Input, Kakshya::GpuDataFormat::UINT32);
409 }
410
411 assemble
412 .pc("particle_count", Kakshya::GpuDataFormat::UINT32)
413 .pc("stride_words", Kakshya::GpuDataFormat::UINT32)
414 .pc("position_offset", Kakshya::GpuDataFormat::UINT32)
415 .pc("grid_min_x", Kakshya::GpuDataFormat::FLOAT32)
416 .pc("grid_min_y", Kakshya::GpuDataFormat::FLOAT32)
417 .pc("grid_min_z", Kakshya::GpuDataFormat::FLOAT32)
418 .pc("cell_size", Kakshya::GpuDataFormat::FLOAT32)
422 .workgroup(256);
423
424 add_cell_of_function(assemble);
425
426 std::string body;
427 body += " if (i >= particle_count) { return; }\n";
428 if (gate_alive) {
429 body += " if (alive[i] == 0u) { return; }\n";
430 }
431 body += " uint b = i * stride_words;\n";
432 body += " vec3 p = vec3(vertices[b + position_offset], "
433 "vertices[b + position_offset + 1u], vertices[b + position_offset + 2u]);\n";
434 body += " vec3 gmin = vec3(grid_min_x, grid_min_y, grid_min_z);\n";
435 body += " uvec3 dims = uvec3(dim_x, dim_y, dim_z);\n";
436 body += " uint cell = cell_of(p, gmin, cell_size, dims);\n";
437 body += " uint slot = atomicAdd(cell_cursor[cell], 1u);\n";
438 body += " particle_index[slot] = i;\n";
439
440 assemble.kernel(KernelSource { .body = std::move(body) });
441
442 return assemble.build();
443 }
444
445 std::vector<NetworkStateFieldProcessor::FieldBinding> scatter_bindings(bool gate_alive)
446 {
447 std::vector<NetworkStateFieldProcessor::FieldBinding> bindings {
448 { .name = "vertices", .binding = 0, .field = {} },
449 { .name = "cell_cursor", .binding = 1, .field = "hash_cell_cursor" },
450 { .name = "particle_index", .binding = 2, .field = "hash_particle_index" },
451 };
452 if (gate_alive) {
453 bindings.push_back({ .name = "alive", .binding = 3, .field = "mutation_alive" });
454 }
455 return bindings;
456 }
457
458} // namespace
459
461 : NetworkStateFieldProcessor(scatter_bindings(gate_alive), build_scatter_spec(gate_alive))
462 , m_params(make_grid_push_constants(config))
463{
464}
465
470
471//=============================================================================
472// HashDensityColorProcessor
473//=============================================================================
474
475namespace {
476
477 ShaderSpec build_density_spec()
478 {
479 ShaderSpec::Assemble assemble;
480 assemble
481 .ssbo("vertices", BindingDirection::InOut, Kakshya::GpuDataFormat::FLOAT32)
482 .ssbo("cell_start", BindingDirection::Input, Kakshya::GpuDataFormat::UINT32)
483 .ssbo("cell_count", BindingDirection::Input, Kakshya::GpuDataFormat::UINT32)
484 .ssbo("particle_index", BindingDirection::Input, Kakshya::GpuDataFormat::UINT32)
485 .ssbo("cluster_id", BindingDirection::Input, Kakshya::GpuDataFormat::UINT32)
486 .pc("particle_count", Kakshya::GpuDataFormat::UINT32)
487 .pc("stride_words", Kakshya::GpuDataFormat::UINT32)
488 .pc("position_offset", Kakshya::GpuDataFormat::UINT32)
489 .pc("color_offset", Kakshya::GpuDataFormat::UINT32)
490 .pc("grid_min_x", Kakshya::GpuDataFormat::FLOAT32)
491 .pc("grid_min_y", Kakshya::GpuDataFormat::FLOAT32)
492 .pc("grid_min_z", Kakshya::GpuDataFormat::FLOAT32)
493 .pc("cell_size", Kakshya::GpuDataFormat::FLOAT32)
497 .pc("density_saturation_count", Kakshya::GpuDataFormat::FLOAT32)
498 .pc("cross_cluster", Kakshya::GpuDataFormat::UINT32)
499 .workgroup(256);
500
501 std::string body;
502 body += " if (i >= particle_count) { return; }\n";
503 body += " uint b = i * stride_words;\n";
504 body += " vec3 p = vec3(vertices[b + position_offset], "
505 "vertices[b + position_offset + 1u], vertices[b + position_offset + 2u]);\n";
506 body += " uint my_cluster = cluster_id[i];\n";
507 body += "\n";
508 body += " uint neighbor_count = 0u;\n";
510 .cluster_scoped = true,
511 .on_hit = "neighbor_count = neighbor_count + 1u;",
512 });
513 body += "\n";
514 body += " float density = clamp(float(neighbor_count) / density_saturation_count, 0.0, 1.0);\n";
515 body += " vec3 ember = vec3(0.03, 0.0, 0.06);\n";
516 body += " vec3 fire = vec3(0.95, 0.25, 0.02);\n";
517 body += " vec3 white_hot = vec3(1.0, 0.95, 0.7);\n";
518 body += " vec3 col = density < 0.5\n";
519 body += " ? mix(ember, fire, density * 2.0)\n";
520 body += " : mix(fire, white_hot, (density - 0.5) * 2.0);\n";
521 body += " vertices[b + color_offset] = col.x;\n";
522 body += " vertices[b + color_offset + 1u] = col.y;\n";
523 body += " vertices[b + color_offset + 2u] = col.z;\n";
524
525 assemble.kernel(KernelSource { .body = std::move(body) });
526
527 return assemble.build();
528 }
529
530 /**
531 * @brief Resolve the vertex record's colour word offset or fail loudly.
532 *
533 * A free function rather than inline in the member-initialiser list:
534 * the constructor needs the result before it can build m_params, and a
535 * throwing helper called there is the same shape
536 * VertexFieldProcessor::require_spec already uses for a precondition
537 * that must hold before construction can proceed at all.
538 */
539 uint32_t require_color_offset(
540 const std::shared_ptr<Nodes::Network::GpuFieldOperator>& particle_op)
541 {
542 if (!particle_op) {
543 error<std::invalid_argument>(
546 std::source_location::current(),
547 "HashDensityColorProcessor: null particle operator");
548 }
549
550 auto offset = particle_op->get_layout().find_word_offset(Kakshya::DataModality::VERTEX_COLORS_RGB);
551 if (!offset.has_value()) {
552 error<std::invalid_argument>(
555 std::source_location::current(),
556 "HashDensityColorProcessor: vertex layout carries no word-aligned colour attribute");
557 }
558
559 return *offset;
560 }
561
562} // namespace
563
565 const SpatialHashConfig& config,
566 std::shared_ptr<Nodes::Network::GpuFieldOperator> particle_op)
568 { FieldBinding { .name = "vertices", .binding = 0, .field = {} },
569 FieldBinding { .name = "cell_start", .binding = 1, .field = "hash_cell_start" },
570 FieldBinding { .name = "cell_count", .binding = 2, .field = "hash_cell_count" },
571 FieldBinding { .name = "particle_index", .binding = 3, .field = "hash_particle_index" },
572 FieldBinding { .name = "cluster_id", .binding = 4, .field = "hash_cluster_id" } },
573 build_density_spec())
574 , m_params {
575 .particle_count = config.particle_count,
576 .stride_words = config.stride_words,
577 .position_offset = config.position_word_offset,
578 .color_offset = require_color_offset(particle_op),
579 .grid_min_x = config.grid_min.x,
580 .grid_min_y = config.grid_min.y,
581 .grid_min_z = config.grid_min.z,
582 .cell_size = config.cell_size,
583 .dim_x = config.grid_dims.x,
584 .dim_y = config.grid_dims.y,
585 .dim_z = config.grid_dims.z,
586 .density_saturation_count = particle_op->get_field_config().density_saturation_count,
587 .cross_cluster = particle_op->get_field_config().cross_cluster ? 1U : 0U,
588 }
589 , m_particle_op(std::move(particle_op))
590 , m_built_revision(m_particle_op->revision())
591{
592}
593
598
601 const std::shared_ptr<VKBuffer>& buffer)
602{
603 return guard_and_resync(cmd_id, buffer, m_particle_op->revision(), m_built_revision, [this] {
604 const auto& pconfig = m_particle_op->get_field_config();
605 m_params.density_saturation_count = pconfig.density_saturation_count;
606 m_params.cross_cluster = pconfig.cross_cluster ? 1U : 0U;
607 set_push_constant_data(m_params);
608 });
609}
610
611} // namespace MayaFlux::Buffers
#define MF_ERROR(comp, ctx,...)
Core::GlobalNetworkConfig network
Definition Config.cpp:39
float offset
virtual void on_attach(const std::shared_ptr< Buffer > &)
Called when this processor is attached to a buffer.
virtual void processing_function(const std::shared_ptr< Buffer > &buffer)=0
The core processing function that must be implemented by derived classes.
void set_manual_dispatch(uint32_t x, uint32_t y=1, uint32_t z=1)
Set manual dispatch group counts.
Specialized ShaderProcessor for Compute Pipelines.
void on_buffer_ready() override
Hook for subclass setup, called at the end of on_attach.
HashClearProcessor(const SpatialHashConfig &config)
void on_buffer_ready() override
Hook for subclass setup, called at the end of on_attach.
HashCountProcessor(const SpatialHashConfig &config, bool gate_alive=false)
std::shared_ptr< Nodes::Network::GpuFieldOperator > m_particle_op
bool on_before_execute(Portal::Graphics::CommandBufferID cmd_id, const std::shared_ptr< VKBuffer > &buffer) override
Called before each process callback.
void on_buffer_ready() override
Hook for subclass setup, called at the end of on_attach.
HashDensityColorProcessor(const SpatialHashConfig &config, std::shared_ptr< Nodes::Network::GpuFieldOperator > particle_op)
HashScanProcessor(const SpatialHashConfig &config)
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.
HashScatterProcessor(const SpatialHashConfig &config, bool gate_alive=false)
void write_field_descriptors()
Issue descriptor writes for every entry in the binding table.
std::shared_ptr< NetworkGeometryBuffer > m_buffer
void register_bindings()
Register the binding table into m_config.bindings.
bool on_before_execute(Portal::Graphics::CommandBufferID cmd_id, const std::shared_ptr< VKBuffer > &buffer) override
Reject buffers that are not the validated NetworkGeometryBuffer.
virtual void on_buffer_ready()
Hook for subclass setup, called at the end of on_attach.
void on_descriptors_created() override
Write every binding in the table for the current buffer state.
bool validate_fields()
Check every named state field exists on the attached buffer.
void on_attach(const std::shared_ptr< Buffer > &buffer) override
Cache and validate the buffer, then call on_buffer_ready.
void dispatch_one_thread_per(uint32_t element_count, const T &params)
Configure manual dispatch for one thread per element, and stage the given push constant data.
NetworkStateFieldProcessor(std::vector< FieldBinding > bindings, const Portal::Graphics::ShaderSpec &spec)
Construct from a generated ShaderSpec.
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.
ComputeProcessor operating on named state fields of a NetworkGeometryBuffer, plus optionally the buff...
void set_push_constant_data(const T &data)
Update push constant data (type-safe)
bool are_descriptors_ready() const
Check if descriptors are initialized.
std::vector< Portal::Graphics::DescriptorSetID > m_descriptor_set_ids
Operator that produces GPU-renderable geometry.
void append_neighbour_walk(std::string &body, const NeighbourWalk &walk)
Append the spatial-hash neighbour-gather loop nest to a kernel body.
GridPushConstants make_grid_push_constants(const SpatialHashConfig &c)
Populate a GridPushConstants from grid config.
@ 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.
std::string name
Shader binding name, as declared in ShaderConfig.
std::string field
State field name, or empty for the buffer's own vertex storage.
Describes how a VKBuffer binds to a shader descriptor.
std::unordered_map< std::string, ShaderBinding > bindings
static std::optional< SpatialHashConfig > from_network(const std::shared_ptr< NetworkGeometryBuffer > &buffer, float cell_size)
Build hash grid parameters from a NetworkGeometryBuffer's network and its primary operator.
void declare_fields(const std::shared_ptr< NetworkGeometryBuffer > &buffer) const
Declare the four state fields the hash build stages read and write, sized from this config.
uint32_t cell_count() const
Total cell count, grid_dims.x * grid_dims.y * grid_dims.z.
Uniform grid parameters shared by every stage of the hash build.
Spatial domain for vertex generation.
Parsed representation of a user-supplied kernel lambda.
std::optional< KernelSource > kernel
When set, KernelOp is ignored.
Complete declarative description of a generated compute shader.