MayaFlux 0.5.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
VolumeGridBuffer.hpp
Go to the documentation of this file.
1#pragma once
2
5
7
8namespace MayaFlux::Buffers {
9
10class VolumeSurfaceProcessor;
11class SDFMeshProcessor;
12class RenderProcessor;
13class VolumeGridBuffer;
14class AdvectProcessor;
15class BuoyancyProcessor;
16class WallProcessor;
17class DivergenceProcessor;
18class DiffuseProcessor;
19class PressureProcessor;
20class SolenoidalProcessor;
21
22/**
23 * @struct ScalarRef
24 * @brief A scalar field's name, issued by the volume that owns it.
25 *
26 * Exists so a field is spelled once, at declaration, rather than at every
27 * site that addresses it. A name typed twice is two independent literals
28 * that must agree; a mismatch surfaces as a stage that runs and does
29 * nothing, which is the hardest failure in this subsystem to diagnose.
30 *
31 * Converts implicitly to the field name, so a ref passes anywhere a
32 * processor expects a string. The type distinction bites where a
33 * signature demands one kind: a parameter taking ScalarRef cannot be
34 * handed a vector field.
35 *
36 * Carries a weak reference to its issuer so a ref from one volume used
37 * against another can be rejected rather than silently resolving to a
38 * same-named field.
39 */
40struct ScalarRef {
41 std::string name;
42 std::weak_ptr<VolumeGridBuffer> owner;
43
44 static constexpr size_t stride = sizeof(float);
45
46 operator const std::string&() const { return name; }
47
48 /**
49 * @brief Whether this ref was issued by the given volume.
50 * @param volume Volume to test against.
51 */
52 [[nodiscard]] bool issued_by(const VolumeGridBuffer* volume) const
53 {
54 auto o = owner.lock();
55 return o && o.get() == volume;
56 }
57};
58
59/**
60 * @struct VectorRef
61 * @brief A vector field's name, issued by the volume that owns it.
62 *
63 * As ScalarRef, for fields of glm::vec4 stride. The fourth component is
64 * carried through by every current stage and read by none.
65 */
66struct VectorRef {
67 std::string name;
68 std::weak_ptr<VolumeGridBuffer> owner;
69
70 static constexpr size_t stride = sizeof(glm::vec4);
71
72 operator const std::string&() const { return name; }
73
74 /**
75 * @brief Whether this ref was issued by the given volume.
76 * @param volume Volume to test against.
77 */
78 [[nodiscard]] bool issued_by(const VolumeGridBuffer* volume) const
79 {
80 auto o = owner.lock();
81 return o && o.get() == volume;
82 }
83};
84
85/**
86 * @class VolumeGridBuffer
87 * @brief GPU-resident state for multi-field simulations evaluated over a
88 * fixed 3D topology: incompressible fluid, gas and smoke, reaction
89 * systems, and any process where several quantities co-evolve over
90 * the same cell lattice.
91 *
92 * Where RelaxationGridBuffer carries one field over a 2D topology and
93 * advances it with one rule dispatch per cycle, this carries N named
94 * fields over a 3D topology and advances them with a chain of processors,
95 * each responsible for one stage. A velocity field, a pressure field, a
96 * divergence scratch field and one or more carried scalars are the
97 * ordinary case; nothing in the class knows what any of them mean.
98 *
99 * All field storage lives as raw Vulkan handle pairs inside
100 * VKBufferResources::back_buffers, allocated once at construction as
101 * device-local with transfer source and destination usage. Fields are
102 * addressed by name; each declares its own component stride and whether
103 * it needs a second slot for ping-pong. Single-slot fields resolve read
104 * and write to the same handle, which suits scratch quantities
105 * recomputed from nothing each stage.
106 *
107 * No VKBuffer object represents any field. Processors write descriptors
108 * for these handles directly against ShaderFoundry, as
109 * RelaxationStepProcessor does, resolving them through read_handle() and
110 * write_handle().
111 *
112 * Chain layout:
113 * default - VolumeSurfaceProcessor, resampling the surfaced field
114 * flat[0] - SDFMeshProcessor, extracting the isosurface
115 * flat[1..] - simulation stages, added by the caller at any point
116 * final - RenderProcessor
117 *
118 * Extraction occupies the default and first flat slots so that
119 * simulation stages appended by the caller always follow it. The surface
120 * therefore reflects the previous cycle's field rather than the current
121 * one, which at frame rate is not observable.
122 *
123 * The VKBuffer base owns no vertex output. Extraction is a separate
124 * concern: attach SDFMeshProcessor against a scalar field for an
125 * isosurface, or a bespoke processor for raymarching or debug points.
126 * The buffer emits nothing on its own.
127 *
128 * Usage:
129 * @code
130 * auto vol = std::make_shared<VolumeGridBuffer>(
131 * 64, 64, 64,
132 * { { "velocity", sizeof(glm::vec4) },
133 * { "pressure", sizeof(float) },
134 * { "divergence", sizeof(float), false },
135 * { "density", sizeof(float) } },
136 * Kinesis::AABB3D { { -1, -1, -1 }, { 1, 1, 1 } });
137 *
138 * vol->seed("density", Kinesis::SpatialField { ... });
139 * vol->setup_processors(ProcessingToken::GRAPHICS_BACKEND);
140 * @endcode
141 */
142class MAYAFLUX_API VolumeGridBuffer : public VKBuffer {
143public:
144 /**
145 * @struct FieldDecl
146 * @brief One field's declaration within a volume.
147 */
148 struct FieldDecl {
149 std::string name; ///< Lookup key, unique within the volume.
150 size_t stride_bytes; ///< Bytes per cell.
151 bool double_buffered = true; ///< False resolves read and write to one slot.
152 };
153
154 /**
155 * @struct SurfaceConfig
156 * @brief Isosurface extraction parameters for a scalar field.
157 *
158 * Supplied at construction because the extraction resolution
159 * determines this buffer's own vertex storage size: mc_emit allocates
160 * slots by atomicAdd without a capacity check, so storage is sized to
161 * the worst case of fifteen vertices per voxel. At 48 cubed that is
162 * roughly 95 MB, at 64 cubed roughly 225 MB.
163 */
165 std::string field_name; ///< Scalar field surfaced. Stride must be sizeof(float).
166 glm::uvec3 resolution; ///< Extraction cell count per axis, over the volume's own bounds.
167 float threshold; ///< Field value the surface is placed at.
168 };
169
170 /**
171 * @struct FlowConfig
172 * @brief Parameters for the incompressible flow stage arrangement.
173 *
174 * Carries what varies between simulations. What does not vary, the
175 * stage ordering, is fixed by setup_flow: buoyancy must precede the
176 * projection or the solve removes the divergence it injects, the wall
177 * condition must follow every stage that writes velocity, and carried
178 * scalars must be advected by the projected velocity rather than the
179 * raw one. Those are correctness constraints, not preferences, and
180 * they are the part callers get wrong.
181 */
182 struct FlowConfig {
183 /**
184 * @struct Carried
185 * @brief A scalar transported by the velocity field.
186 */
187 struct Carried {
188 ScalarRef field; ///< Field advected.
189 float dissipation { 1.0F }; ///< Per-cycle multiplier. One conserves.
190 };
191
192 /**
193 * @struct Buoyancy
194 * @brief Body force accumulated into velocity from two scalars.
195 */
196 struct Buoyancy {
197 ScalarRef temperature; ///< Drives motion along direction.
198 ScalarRef density; ///< Drives motion against it.
199 glm::vec3 direction { 0.0F, 1.0F, 0.0F }; ///< Axis and magnitude.
200 float temperature_gain { 1.0F };
201 float density_gain { 0.0F };
202 float ambient { 0.0F }; ///< Temperature at which the rise term vanishes.
203 };
204
205 VectorRef velocity; ///< Required.
206 ScalarRef divergence; ///< Required. Single-slot is correct.
207 ScalarRef pressure; ///< Required.
208 VectorRef scratch; ///< Required only when viscosity is above zero.
209
210 float time_step { 1.0F / 60.0F }; ///< Applied to every stage that integrates.
211 float viscosity { 0.0F }; ///< Zero omits the diffusion stage entirely.
212 uint32_t jacobi_iterations { 32 };
213 bool walls { true }; ///< Free-slip on the six faces, twice per cycle.
214
215 std::vector<Carried> carried;
216 std::optional<Buoyancy> buoyancy;
217 };
218
219 /**
220 * @struct FlowStages
221 * @brief Every stage setup_flow built, in no particular order.
222 *
223 * Returned rather than stored so each remains reachable for retuning,
224 * feeding, or inspection. Members are null where the config omitted
225 * the corresponding stage.
226 */
227 struct FlowStages {
228 std::shared_ptr<AdvectProcessor> self_advect;
229 std::shared_ptr<BuoyancyProcessor> buoyancy;
230 std::shared_ptr<DiffuseProcessor> diffuse;
231 std::shared_ptr<WallProcessor> wall_advected;
232 std::shared_ptr<DivergenceProcessor> divergence;
233 std::shared_ptr<PressureProcessor> pressure;
234 std::shared_ptr<SolenoidalProcessor> solenoidal;
235 std::shared_ptr<WallProcessor> wall_projected;
236 std::vector<std::shared_ptr<AdvectProcessor>> carriers; ///< Parallel to config.carried.
237 };
238
239 /**
240 * @brief Construct an unregistered multi-field volume.
241 * @param lattice Discretized extent every field is stored over.
242 * @param fields Field declarations. Duplicated names are rejected with
243 * an error and the later declaration discarded.
244 * @param surface Optional isosurface extraction parameters.
245 */
247 Kinesis::Lattice3D lattice,
248 std::initializer_list<FieldDecl> fields,
249 std::optional<SurfaceConfig> surface = std::nullopt);
250
251 /**
252 * @brief Construct from a runtime-built field list.
253 * @param lattice Discretized extent every field is stored over.
254 * @param fields Field declarations.
255 * @param surface Optional isosurface extraction parameters.
256 */
258 Kinesis::Lattice3D lattice,
259 std::vector<FieldDecl> fields,
260 std::optional<SurfaceConfig> surface = std::nullopt);
261
262 /**
263 * @brief Construct a volume with no fields, to be declared after.
264 * @param lattice Discretized extent every field is stored over.
265 * @param surface Optional isosurface extraction parameters. Its field
266 * name resolves at setup_processors, so it may name a field
267 * declared after construction.
268 *
269 * Extraction storage is sized here because it depends only on the
270 * extraction resolution, so the SurfaceConfig cannot move later.
271 */
272 explicit VolumeGridBuffer(
273 Kinesis::Lattice3D lattice,
274 std::optional<SurfaceConfig> surface = std::nullopt);
275
276 /**
277 * @brief Destructor.
278 *
279 * Raw handles in m_resources.back_buffers are released by the backend
280 * during buffer service teardown, as with RelaxationGridBuffer. This
281 * class performs no manual Vulkan destruction.
282 */
283 ~VolumeGridBuffer() override;
284
285 /**
286 * @brief Establish the processing chain without attaching any stage.
287 * @param token Processing domain, typically GRAPHICS_BACKEND.
288 *
289 * The volume declares no default processor and no stages of its own.
290 * Simulation identity is the sequence of processors the caller adds,
291 * not a property of this class.
292 *
293 * When a SurfaceConfig was supplied at construction, two stages are
294 * appended: the field-to-corner-grid resample and the marching cubes
295 * extraction writing into this buffer's own vertex storage. Simulation
296 * stages added before this call keep their position ahead of them.
297 */
298 void setup_processors(ProcessingToken token) override;
299
300 /**
301 * @brief Attach a RenderProcessor drawing the extracted surface.
302 * @param config Render target. Vertex and fragment shaders default to
303 * the untextured triangle pair; topology is forced to
304 * TRIANGLE_LIST.
305 *
306 * Requires a SurfaceConfig at construction. Without one this buffer
307 * has no vertex storage and nothing to draw.
308 */
309 void setup_rendering(const RenderConfig& config);
310
311 /**
312 * @brief Build and append the incompressible flow stages.
313 * @param config Fields and parameters. Every ref must have been issued
314 * by this volume.
315 * @return The stages built, all already in the chain.
316 *
317 * Appends in the order: self-advection, buoyancy, diffusion, wall,
318 * divergence, pressure, solenoidal, wall, then one advection per
319 * carried scalar. Carriers run last so they are transported by the
320 * projected velocity.
321 *
322 * Stages added to the chain before this call keep their position
323 * ahead of it, which is where an influx stage belongs.
324 */
325 FlowStages setup_flow(const FlowConfig& config);
326
327 /** @brief The surface extraction stage, valid after setup_rendering. */
328 [[nodiscard]] std::shared_ptr<VolumeSurfaceProcessor> surface_processor() const { return m_surface_processor; }
329
330 /** @brief The marching cubes stage, valid after setup_rendering. */
331 [[nodiscard]] std::shared_ptr<SDFMeshProcessor> mesh_processor() const { return m_mesh_processor; }
332
333 /** @brief The lattice every field is discretized over. */
334 [[nodiscard]] const Kinesis::Lattice3D& get_lattice() const { return m_lattice; }
335
336 /** @brief Cell count along X. */
337 [[nodiscard]] uint32_t get_width() const { return m_lattice.resolution.x; }
338
339 /** @brief Cell count along Y. */
340 [[nodiscard]] uint32_t get_height() const { return m_lattice.resolution.y; }
341
342 /** @brief Cell count along Z. */
343 [[nodiscard]] uint32_t get_depth() const { return m_lattice.resolution.z; }
344
345 /** @brief Total cell count. */
346 [[nodiscard]] uint32_t get_cell_count() const { return static_cast<uint32_t>(m_lattice.cell_count()); }
347
348 /** @brief World-space extent the lattice covers. */
349 [[nodiscard]] const Kinesis::AABB3D& get_bounds() const { return m_lattice.bounds; }
350
351 /** @brief World-space size of one cell along each axis. */
352 [[nodiscard]] glm::vec3 get_cell_size() const { return m_lattice.cell_size(); }
353
354 /** @brief Whether a field of this name was declared. */
355 [[nodiscard]] bool has_field(const std::string& name) const;
356
357 /** @brief Byte size of one slot of the named field, or 0 if undeclared. */
358 [[nodiscard]] size_t get_field_bytes(const std::string& name) const;
359
360 /** @brief Names of every declared field, in declaration order. */
361 [[nodiscard]] std::vector<std::string> get_field_names() const;
362
363 /**
364 * @brief Handle a stage should read the named field from.
365 * @param name Field name.
366 * @return Vulkan buffer handle, or nullptr if undeclared.
367 */
368 [[nodiscard]] vk::Buffer read_handle(const std::string& name) const;
369
370 /**
371 * @brief Handle a stage should write the named field to.
372 * @param name Field name.
373 * @return Vulkan buffer handle, or nullptr if undeclared.
374 *
375 * Equals read_handle() for single-slot fields.
376 */
377 [[nodiscard]] vk::Buffer write_handle(const std::string& name) const;
378
379 /**
380 * @brief Exchange read and write slots for the named field.
381 * @param name Field name. No effect on single-slot fields.
382 *
383 * Called by whichever stage last wrote the field, after its dispatch,
384 * so the next stage reading it observes the new values. A stage that
385 * writes a field it also reads must swap; a stage writing a scratch
386 * field consumed immediately after need not.
387 */
388 void swap_field(const std::string& name);
389
390 /**
391 * @brief Declare a double-buffered scalar field.
392 * @param name Lookup key, unique within this volume.
393 * @return Ref naming the field, or a ref with an empty name if the
394 * declaration was rejected.
395 */
396 ScalarRef declare_scalar(std::string name);
397
398 /**
399 * @brief Declare a single-slot scalar field.
400 * @param name Lookup key, unique within this volume.
401 * @return Ref naming the field, or a ref with an empty name if the
402 * declaration was rejected.
403 *
404 * Read and write resolve to one handle, which suits a quantity
405 * recomputed from other fields every cycle and never carried across
406 * cycles: divergence is the ordinary case. A stage that reads a
407 * neighbourhood of a field it also writes cannot use one of these,
408 * and VolumeFieldProcessor rejects that arrangement at attach.
409 */
410 ScalarRef declare_scratch(std::string name);
411
412 /**
413 * @brief Declare a double-buffered vector field.
414 * @param name Lookup key, unique within this volume.
415 * @return Ref naming the field, or a ref with an empty name if the
416 * declaration was rejected.
417 *
418 * Vector fields are always double-buffered. The single-slot case
419 * saves one slot, a megabyte at 64 cubed, and every vector field in
420 * use is either advected or diffused, both of which require two.
421 */
422 VectorRef declare_vector(std::string name);
423
424 /**
425 * @brief Write initial values into a scalar field from a Kinesis field.
426 * @param name Field name. Must have stride sizeof(float).
427 * @param field Sampled at each cell centre in world space.
428 */
429 void seed(const std::string& name, const Kinesis::SpatialField& field);
430
431 /**
432 * @brief Write initial values into a vector field from a Kinesis field.
433 * @param name Field name. Must have stride sizeof(glm::vec4).
434 * @param field Sampled at each cell centre in world space. The fourth
435 * component of every cell is zeroed.
436 */
437 void seed(const std::string& name, const Kinesis::VectorField& field);
438
439 /**
440 * @brief Write initial values into a field from raw host memory.
441 * @param name Field name.
442 * @param data Pointer to at least @p size bytes.
443 * @param size Byte count; must equal get_field_bytes(name).
444 *
445 * Writes into the current read slot. For double-buffered fields the
446 * write slot is left untouched, which is correct when the first stage
447 * to touch the field reads before it writes.
448 */
449 void seed_raw(const std::string& name, const void* data, size_t size);
450
451 /**
452 * @brief Add sampled values into a scalar field.
453 * @param name Field name. Must have stride sizeof(float).
454 * @param field Sampled at each cell centre in world space and added
455 * to whatever the field already holds.
456 *
457 * Blocking. Reads the current read slot to host memory, adds, and
458 * writes back, so it costs a full round trip at the lattice's byte
459 * size. Intended for authored injection on a coarse clock, not as a
460 * chain stage.
461 */
462 void accumulate(const std::string& name, const Kinesis::SpatialField& field);
463
464 /**
465 * @brief Add sampled values into a vector field.
466 * @param name Field name. Must have stride sizeof(glm::vec4).
467 * @param field Sampled at each cell centre in world space and added
468 * to the first three components. The fourth is left as found.
469 */
470 void accumulate(const std::string& name, const Kinesis::VectorField& field);
471
472 /**
473 * @brief Copy the current read slot of a field to host memory.
474 * @param name Field name.
475 * @param data Destination pointer, at least get_field_bytes(name) bytes.
476 * @param size Byte count; must equal get_field_bytes(name).
477 *
478 * Blocking. Records a fenced device-to-host copy and waits on it from
479 * the calling thread. Not for per-frame use on the graphics thread.
480 */
481 void read_field(const std::string& name, void* data, size_t size);
482
483private:
484 struct Field {
486 uint32_t slot_a;
487 uint32_t slot_b;
489 };
490
491 /**
492 * @brief Populate m_fields from declarations.
493 * @param decls Field declarations in declaration order.
494 */
495 void allocate_fields(const std::vector<FieldDecl>& decls);
496
497 /**
498 * @brief Allocate one field's slots and register it.
499 * @param name Lookup key.
500 * @param stride_bytes Bytes per cell.
501 * @param double_buffered Whether to allocate a second slot.
502 * @return True if the field was registered.
503 */
504 bool allocate_field(const std::string& name, size_t stride_bytes, bool double_buffered);
505
506 /**
507 * @brief Resolve a field by name.
508 * @param name Field name.
509 * @param context Caller identifier used in the error path.
510 * @return Pointer to the field, or nullptr with an error logged.
511 */
512 [[nodiscard]] const Field* find_field(const std::string& name, const char* context) const;
513
515
516 std::vector<std::string> m_field_order;
517 std::unordered_map<std::string, Field> m_fields;
518 std::optional<SurfaceConfig> m_surface;
519
520 std::shared_ptr<VKBuffer> m_transfer_staging; ///< Reused across seed and read calls.
521 std::shared_ptr<VolumeSurfaceProcessor> m_surface_processor;
522 std::shared_ptr<SDFMeshProcessor> m_mesh_processor;
523 std::shared_ptr<VKBuffer> m_counter_buf; ///< Atomic vertex counter for the extraction stage.
524
525 TransferHandle m_pending_transfer; ///< In-flight seed upload, resolved before the next transfer.
526
527 /**
528 * @brief Bytes of vertex storage the extraction resolution requires.
529 * @param surface Extraction parameters, or nullopt for no extraction.
530 * @return Worst-case vertex bytes, or 1 when no surface is configured.
531 */
532 static size_t surface_storage_bytes(const std::optional<SurfaceConfig>& surface);
533};
534
535} // namespace MayaFlux::Buffers
float density_gain
float ambient
float temperature_gain
float time_step
Vulkan-backed buffer wrapper used in processing chains.
Definition VKBuffer.hpp:76
uint32_t get_width() const
Cell count along X.
const Kinesis::Lattice3D & get_lattice() const
The lattice every field is discretized over.
std::unordered_map< std::string, Field > m_fields
void accumulate(const std::string &name, const Kinesis::VectorField &field)
Add sampled values into a vector field.
TransferHandle m_pending_transfer
In-flight seed upload, resolved before the next transfer.
uint32_t get_cell_count() const
Total cell count.
uint32_t get_depth() const
Cell count along Z.
const Kinesis::AABB3D & get_bounds() const
World-space extent the lattice covers.
glm::vec3 get_cell_size() const
World-space size of one cell along each axis.
std::optional< SurfaceConfig > m_surface
std::shared_ptr< SDFMeshProcessor > mesh_processor() const
The marching cubes stage, valid after setup_rendering.
std::shared_ptr< VolumeSurfaceProcessor > surface_processor() const
The surface extraction stage, valid after setup_rendering.
std::shared_ptr< VKBuffer > m_transfer_staging
Reused across seed and read calls.
std::shared_ptr< VolumeSurfaceProcessor > m_surface_processor
std::vector< std::string > m_field_order
void seed(const std::string &name, const Kinesis::VectorField &field)
Write initial values into a vector field from a Kinesis field.
std::shared_ptr< VKBuffer > m_counter_buf
Atomic vertex counter for the extraction stage.
std::shared_ptr< SDFMeshProcessor > m_mesh_processor
uint32_t get_height() const
Cell count along Y.
GPU-resident state for multi-field simulations evaluated over a fixed 3D topology: incompressible flu...
std::shared_ptr< void > TransferHandle
Opaque handle to an in-flight transfer.
ProcessingToken
Bitfield enum defining processing characteristics and backend requirements for buffer operations.
std::weak_ptr< VolumeGridBuffer > owner
static constexpr size_t stride
bool issued_by(const VolumeGridBuffer *volume) const
Whether this ref was issued by the given volume.
A scalar field's name, issued by the volume that owns it.
static constexpr size_t stride
bool issued_by(const VolumeGridBuffer *volume) const
Whether this ref was issued by the given volume.
std::weak_ptr< VolumeGridBuffer > owner
A vector field's name, issued by the volume that owns it.
std::string name
Lookup key, unique within the volume.
One field's declaration within a volume.
Body force accumulated into velocity from two scalars.
A scalar transported by the velocity field.
VectorRef scratch
Required only when viscosity is above zero.
ScalarRef divergence
Required. Single-slot is correct.
Parameters for the incompressible flow stage arrangement.
std::shared_ptr< AdvectProcessor > self_advect
std::shared_ptr< PressureProcessor > pressure
std::shared_ptr< WallProcessor > wall_projected
std::vector< std::shared_ptr< AdvectProcessor > > carriers
Parallel to config.carried.
std::shared_ptr< DiffuseProcessor > diffuse
std::shared_ptr< DivergenceProcessor > divergence
std::shared_ptr< SolenoidalProcessor > solenoidal
std::shared_ptr< BuoyancyProcessor > buoyancy
Every stage setup_flow built, in no particular order.
std::string field_name
Scalar field surfaced. Stride must be sizeof(float).
glm::uvec3 resolution
Extraction cell count per axis, over the volume's own bounds.
float threshold
Field value the surface is placed at.
Isosurface extraction parameters for a scalar field.
Axis-aligned bounding box in 3D world space.
Definition Bounds.hpp:143
A regular subdivision of an AABB3D into a cell count per axis.
Definition Lattice.hpp:25
Typed, composable, stateless callable from domain D to range R.
Definition Tendency.hpp:22
Unified rendering configuration for graphics buffers.