MayaFlux 1.0.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
RelaxationGridBuffer.hpp
Go to the documentation of this file.
1#pragma once
2
6
7namespace MayaFlux::Buffers {
8
9class RelaxationStepProcessor;
10class RelaxationEmitProcessor;
11class RenderProcessor;
12
13/**
14 * @class RelaxationGridBuffer
15 * @brief GPU-resident, double-buffered SSBO state for synchronous
16 * local-rule systems evaluated over a fixed 2D topology: cellular
17 * automata, Jacobi/Gauss-Seidel relaxation, discrete diffusion, and
18 * any rule where every cell updates from its own and neighbors'
19 * previous-generation state.
20 *
21 * Grid state lives entirely as two raw Vulkan handle pairs inside
22 * VKBufferResources::back_buffers, allocated once at construction. No
23 * VKBuffer object represents the grid state at any point; descriptor
24 * writes for these handles are issued directly against ShaderFoundry by
25 * RelaxationStepProcessor and RelaxationEmitProcessor, bypassing
26 * ShaderProcessor::bind_buffer entirely, since that call requires a
27 * VKBuffer wrapper this design does not create.
28 *
29 * The VKBuffer base itself (this object) owns only the emitted vertex
30 * output (Usage::VERTEX), written each generation by
31 * RelaxationEmitProcessor from the front generation's state, then drawn
32 * directly by RenderProcessor as ordinary untextured point geometry.
33 *
34 * Chain order:
35 * default — RelaxationStepProcessor (rule shader: state front -> back)
36 * flat[0] — RelaxationEmitProcessor (emit shader: front state -> vertices)
37 * final — RenderProcessor
38 *
39 * Snapshot delivery uses a BroadcastSource<std::vector<uint8_t>>, owned by
40 * this buffer and exposed via snapshot_source(). RelaxationStepProcessor
41 * signals it directly once a requested download completes; there is no
42 * intermediate queue or poll. Subscribe via Kriya::on_signal() or
43 * on_signal_matching() at whatever cadence the caller chooses — the class
44 * imposes none.
45 *
46 * Usage:
47 * @code
48 * auto grid = std::make_shared<RelaxationGridBuffer>(
49 * 256, 256, sizeof(uint32_t), "ca_rule.comp", "ca_emit.comp");
50 * grid->setup_processors(ProcessingToken::GRAPHICS_BACKEND);
51 * grid->setup_rendering({ .target_window = window });
52 * @endcode
53 */
54class MAYAFLUX_API RelaxationGridBuffer : public VKBuffer {
55public:
56 /**
57 * @brief Either a shader file path or a generated ShaderSpec, resolved at
58 * construction time by setup_processors().
59 */
60 using ShaderSource = std::variant<std::string, Portal::Graphics::ShaderSpec>;
61
62 /**
63 * @struct GridConfig
64 * @brief Arrangement of a relaxation grid: extent, the update rule, the
65 * emit stage that draws it, and their constants.
66 *
67 * Carries what varies between grids. What does not vary is fixed by the
68 * processors: stage order (rule, then emit, then render), the leading
69 * push constant prefixes, and the dispatch tiling. A rule shader
70 * declares GridExtent first and uses a 16x16 local size. An emit shader
71 * declares EmitParams first, uses a 256 local size, and writes one packed
72 * Kakshya::Vertex per cell.
73 *
74 * Names no rule. Any ShaderSource meeting those contracts is a rule,
75 * generated or hand-written.
76 *
77 * Structural: width, height, and every Stage::state and Stage::shader.
78 * They decide buffer sizes and which processors are built, so they are
79 * fixed at construction. Stage::constants initialize trailing push
80 * constant words during setup_processors. They establish that footprint
81 * for path shaders, while generated specs declare it themselves. Every
82 * word in the resulting block remains live-tunable through
83 * RelaxationStepProcessor::set_constant and
84 * RelaxationEmitProcessor::set_constant. extent and point_size are
85 * live-tunable through RelaxationEmitProcessor::set_extent and
86 * set_point_size. Float feeds remain available through
87 * ShaderProcessor::feed at an explicit byte offset.
88 */
89 struct GridConfig {
90 /** @brief One 4-byte push constant word, in the scalar types a shader block can hold. */
91 using Constant = std::variant<uint32_t, int32_t, float>;
92
93 /**
94 * @struct Stage
95 * @brief One compute stage of the grid, rule or emit.
96 */
97 struct Stage {
98 std::optional<Kakshya::GpuDataFormat> state; ///< Format of one cell as this stage reads it. Nullopt derives it from the ShaderSpec binding ("state_in" for a rule, "cell_state" for an emit). Required when shader is a path.
99 ShaderSource shader; ///< Hand-written shader path or generated ShaderSpec.
100 std::vector<Constant> constants; ///< Initial words after the stage's fixed prefix (GridExtent for a rule, EmitParams for an emit), in shader declaration order. Required to establish trailing words for path shaders.
101 };
102
103 uint32_t width; ///< Grid width in cells. Required.
104 uint32_t height; ///< Grid height in cells. Required.
105 Stage rule; ///< Required. Reads "state_in", writes "state_out".
106 Stage emit; ///< Required. Reads "cell_state", writes packed vertices.
107 float extent { 1.0F }; ///< NDC half-span the grid occupies.
108 float point_size { 2.0F }; ///< Per-point size written to the vertex scalar field.
109 };
110
111 /**
112 * @brief Construct an unregistered double-buffered grid buffer.
113 * @param width Grid width in cells.
114 * @param height Grid height in cells.
115 * @param cell_stride_bytes Size in bytes of one cell's state.
116 * @param rule_source Either a path to a hand-written rule shader (for
117 * rules needing branchy multi-state logic, e.g. Conway, Wireworld)
118 * or a generated ShaderSpec (e.g. RelaxationSpecs::jacobi_diffusion()).
119 * @param emit_source Either a path to a hand-written emit shader, or a
120 * generated ShaderSpec that meets the packed vertex contract.
121 *
122 * rule_source and emit_source are independent: any pairing meeting their
123 * respective contracts is supported. Processors are constructed from
124 * whichever alternative each holds in setup_processors().
125 */
127 uint32_t width,
128 uint32_t height,
129 size_t cell_stride_bytes,
130 ShaderSource rule_source,
131 ShaderSource emit_source);
132
133 /**
134 * @brief Construct from a GridConfig.
135 * @param config Grid arrangement.
136 * @throws std::invalid_argument When a path stage declares no state,
137 * when rule and emit disagree on the cell format, or when the
138 * format is a three component vector, whose std430 array stride
139 * (16 bytes) differs from gpu_data_format_bytes.
140 *
141 * Derives the cell stride from the stage format and delegates to the
142 * positional constructor.
143 */
144 explicit RelaxationGridBuffer(const GridConfig& config);
145
146 /**
147 * @brief Destructor.
148 *
149 * Raw handles in m_resources.back_buffers are released by the backend
150 * during buffer service teardown, consistent with how VKBuffer's own
151 * primary buffer/memory are released; this class performs no manual
152 * Vulkan destruction itself.
153 */
154 ~RelaxationGridBuffer() override = default;
155
156 /**
157 * @brief Create and attach RelaxationStepProcessor (default processor)
158 * and RelaxationEmitProcessor (flat processor), and set the
159 * per-cell vertex layout on this buffer's own storage.
160 * @param token Processing domain (typically GRAPHICS_BACKEND).
161 */
162 void setup_processors(ProcessingToken token) override;
163
164 /**
165 * @brief Attach a RenderProcessor drawing the emitted vertex output as
166 * ordinary untextured point geometry.
167 * @param config Render target; vertex/fragment shaders default to the
168 * standard untextured pair unless overridden. Topology
169 * is always forced to POINT_LIST regardless of what is
170 * supplied in @p config.
171 */
172 void setup_rendering(const RenderConfig& config);
173
174 /** @brief Grid width in cells. */
175 [[nodiscard]] uint32_t get_grid_width() const { return m_width; }
176
177 /** @brief Grid height in cells. */
178 [[nodiscard]] uint32_t get_grid_height() const { return m_height; }
179
180 /** @brief Total cell count, width * height. */
181 [[nodiscard]] uint32_t get_cell_count() const { return m_width * m_height; }
182
183 /** @brief Total byte size of one generation's state buffer. */
184 [[nodiscard]] size_t get_state_bytes() const
185 {
186 return static_cast<size_t>(m_width) * m_height * m_cell_stride_bytes;
187 }
188
189 /**
190 * @brief Size in bytes of one cell's state, fixed at construction.
191 *
192 * A caller reinterpreting request_snapshot()/snapshot_source()'s raw
193 * bytes needs this to know how to slice them into individual cells:
194 * get_state_bytes() alone gives only the total, and state format
195 * genuinely varies by rule (a single float, a uint32 automaton state,
196 * a vec2 reaction-diffusion pair, or anything else a rule shader
197 * defines), so nothing here can infer it.
198 */
199 [[nodiscard]] size_t get_cell_stride_bytes() const { return m_cell_stride_bytes; }
200
201 /**
202 * @brief Write initial state directly into the current front generation.
203 * @param data Pointer to cell_count * cell_stride_bytes of state data.
204 * @param size Byte count; must equal get_state_bytes().
205 *
206 * Direct host-visible memcpy into back_buffers[front_index()], mirroring
207 * VKBuffer::set_data's own host-visible write path. Valid only before the
208 * buffer's processing chain has run at least once, or between generations
209 * where overwriting the current front state is intended (e.g. a runtime
210 * reset). Does not touch the back generation or trigger a swap.
211 */
212 void seed_state(const void* data, size_t size);
213
214 /**
215 * @brief Index into m_resources.back_buffers currently holding the
216 * most recently completed generation's state.
217 */
218 [[nodiscard]] uint32_t front_index() const { return m_front_is_a ? 0 : 1; }
219
220 /**
221 * @brief Index into m_resources.back_buffers currently serving as the
222 * write target for the next rule dispatch.
223 */
224 [[nodiscard]] uint32_t back_index() const { return m_front_is_a ? 1 : 0; }
225
226 /**
227 * @brief Request a snapshot of the next completed generation.
228 *
229 * Wait-free. Served by RelaxationStepProcessor after the next cycle that
230 * dispatches, once that dispatch has completed: a device-to-host copy of
231 * the newly-front state buffer is signalled on snapshot_source().
232 */
233 void request_snapshot() { m_snapshot_requested.store(true, std::memory_order_release); }
234
235 /**
236 * @brief BroadcastSource of completed generation snapshots.
237 *
238 * Signaled by RelaxationStepProcessor exactly once per fulfilled
239 * request_snapshot() call, at the point the device-to-host download
240 * actually completes — not polled, not tied to any particular pacing.
241 * Subscribe via Kriya::on_signal()/on_signal_matching() at whatever
242 * cadence suits the caller; the class itself imposes none.
243 */
244 [[nodiscard]] std::shared_ptr<Vruta::BroadcastSource<std::vector<uint8_t>>> snapshot_source() const
245 {
246 return m_snapshot_source;
247 }
248
249 /** @brief The attached rule-stage processor, valid after setup_processors(). */
250 [[nodiscard]] std::shared_ptr<RelaxationStepProcessor> step_processor() const { return m_step_processor; }
251
252 /** @brief The attached emit-stage processor, valid after setup_processors(). */
253 [[nodiscard]] std::shared_ptr<RelaxationEmitProcessor> emit_processor() const { return m_emit_processor; }
254
255 /**
256 * @brief The rule source this buffer was constructed with: either a
257 * hand-written shader path or a generated ShaderSpec.
258 *
259 * For a caller building descriptive metadata (a rule name/identity
260 * tag) about a capture rather than for driving the pipeline itself,
261 * which goes through setup_processors() instead.
262 */
263 [[nodiscard]] const ShaderSource& rule_source() const { return m_rule_source; }
264
265 /** @brief The emit source this buffer was constructed with. See rule_source(). */
266 [[nodiscard]] const ShaderSource& emit_source() const { return m_emit_source; }
267
268private:
271
272 [[nodiscard]] bool consume_snapshot_request()
273 {
274 return m_snapshot_requested.exchange(false, std::memory_order_acq_rel);
275 }
276
277 inline void swap_generation() { m_front_is_a = !m_front_is_a; }
278
279 /** @brief Signaled by RelaxationStepProcessor when a requested snapshot completes. */
280 std::shared_ptr<Vruta::BroadcastSource<std::vector<uint8_t>>> m_snapshot_source {
281 std::make_shared<Vruta::BroadcastSource<std::vector<uint8_t>>>()
282 };
283
284 uint32_t m_width; ///< Grid width in cells.
285 uint32_t m_height; ///< Grid height in cells.
286 size_t m_cell_stride_bytes; ///< Size in bytes of one cell's state, fixed at construction.
287 std::vector<GridConfig::Constant> m_rule_constants; ///< Rule constants from GridConfig, written at setup_processors.
288 std::vector<GridConfig::Constant> m_emit_constants; ///< Emit constants from GridConfig, written at setup_processors.
289 float m_extent { 1.0F }; ///< Emit extent applied at setup_processors.
290 float m_point_size { 2.0F }; ///< Emit point size applied at setup_processors.
291
292 ShaderSource m_rule_source; ///< Either a path to a hand-written rule shader or a generated ShaderSpec.
293 ShaderSource m_emit_source; ///< Either a path to a hand-written emit shader or a generated ShaderSpec.
294
295 bool m_front_is_a { true }; ///< True when back_buffers[0] holds the current front generation.
296 std::atomic<bool> m_snapshot_requested { false }; ///< Set by request_snapshot(), cleared by consume_snapshot_request().
297
298 std::shared_ptr<RelaxationStepProcessor> m_step_processor; ///< Rule stage, retained for parameter access.
299 std::shared_ptr<RelaxationEmitProcessor> m_emit_processor; ///< Emit stage, retained for parameter access.
300};
301
302} // namespace MayaFlux::Buffers
bool size
uint32_t width
uint32_t height
ComputeProcessor that reads the current front generation's raw state buffer from a RelaxationGridBuff...
~RelaxationGridBuffer() override=default
Destructor.
const ShaderSource & rule_source() const
The rule source this buffer was constructed with: either a hand-written shader path or a generated Sh...
std::vector< GridConfig::Constant > m_rule_constants
Rule constants from GridConfig, written at setup_processors.
std::vector< GridConfig::Constant > m_emit_constants
Emit constants from GridConfig, written at setup_processors.
std::shared_ptr< RelaxationEmitProcessor > m_emit_processor
Emit stage, retained for parameter access.
ShaderSource m_emit_source
Either a path to a hand-written emit shader or a generated ShaderSpec.
std::shared_ptr< RelaxationEmitProcessor > emit_processor() const
The attached emit-stage processor, valid after setup_processors().
uint32_t get_cell_count() const
Total cell count, width * height.
uint32_t get_grid_width() const
Grid width in cells.
std::shared_ptr< RelaxationStepProcessor > m_step_processor
Rule stage, retained for parameter access.
size_t get_state_bytes() const
Total byte size of one generation's state buffer.
size_t get_cell_stride_bytes() const
Size in bytes of one cell's state, fixed at construction.
size_t m_cell_stride_bytes
Size in bytes of one cell's state, fixed at construction.
std::shared_ptr< RelaxationStepProcessor > step_processor() const
The attached rule-stage processor, valid after setup_processors().
ShaderSource m_rule_source
Either a path to a hand-written rule shader or a generated ShaderSpec.
std::variant< std::string, Portal::Graphics::ShaderSpec > ShaderSource
Either a shader file path or a generated ShaderSpec, resolved at construction time by setup_processor...
std::shared_ptr< Vruta::BroadcastSource< std::vector< uint8_t > > > snapshot_source() const
BroadcastSource of completed generation snapshots.
uint32_t back_index() const
Index into m_resources.back_buffers currently serving as the write target for the next rule dispatch.
uint32_t front_index() const
Index into m_resources.back_buffers currently holding the most recently completed generation's state.
uint32_t get_grid_height() const
Grid height in cells.
void request_snapshot()
Request a snapshot of the next completed generation.
const ShaderSource & emit_source() const
The emit source this buffer was constructed with.
GPU-resident, double-buffered SSBO state for synchronous local-rule systems evaluated over a fixed 2D...
ComputeProcessor driving one generation step of a RelaxationGridBuffer per dispatch.
Vulkan-backed buffer wrapper used in processing chains.
Definition VKBuffer.hpp:76
ProcessingToken
Bitfield enum defining processing characteristics and backend requirements for buffer operations.
std::optional< Kakshya::GpuDataFormat > state
Format of one cell as this stage reads it. Nullopt derives it from the ShaderSpec binding ("state_in"...
std::vector< Constant > constants
Initial words after the stage's fixed prefix (GridExtent for a rule, EmitParams for an emit),...
ShaderSource shader
Hand-written shader path or generated ShaderSpec.
Stage emit
Required. Reads "cell_state", writes packed vertices.
Stage rule
Required. Reads "state_in", writes "state_out".
std::variant< uint32_t, int32_t, float > Constant
One 4-byte push constant word, in the scalar types a shader block can hold.
Arrangement of a relaxation grid: extent, the update rule, the emit stage that draws it,...
Unified rendering configuration for graphics buffers.