MayaFlux 1.0.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
VertexFieldProcessor.hpp
Go to the documentation of this file.
1#pragma once
2
4
5#include <chrono>
6
8
9namespace MayaFlux::Buffers {
10
11class NetworkGeometryBuffer;
12
13/**
14 * @class VertexFieldProcessor
15 * @brief Compute pass that applies a GpuFieldOperator's generated shader to a
16 * range of vertex records in the attached buffer.
17 *
18 * The general case of UVFieldProcessor. Where that one hardcodes four
19 * projections writing two fixed byte offsets, this one dispatches whatever
20 * GpuFieldOperator::build_spec() emitted: arbitrary Tendency fields written to
21 * arbitrary attributes of a layout the operator was constructed against.
22 *
23 * Runs after the producing processor has uploaded vertex data and before
24 * RenderProcessor draws, so it belongs in the chain as a postprocessor. The
25 * attached VKBuffer is bound as an SSBO under the name "vertices"; Usage::VERTEX
26 * already carries eShaderStorageBuffer, so no separate allocation exists and
27 * nothing crosses the bus.
28 *
29 * A NetworkGeometryBuffer aggregates one vertex slice per producing operator,
30 * so this processor addresses a range rather than the whole buffer.
31 * set_vertex_range() supplies it; left unset, the range covers every record the
32 * buffer can hold at the operator's stride, which is correct only when a single
33 * producer owns the buffer.
34 *
35 * The dispatch is unconditional. Whatever uploaded the records overwrote last
36 * cycle's result, so the pass must re-run every cycle to stay visible. If the
37 * producing processor gains a dirty gate, this one follows the same flag rather
38 * than inventing its own: the operator holds no data and has nothing to be
39 * dirty about.
40 *
41 * Rebinding a field at runtime changes the operator's revision. This processor
42 * notices before the next dispatch and rebuilds its pipeline from the new spec,
43 * which is what makes fields editable from Lila without tearing down the chain.
44 *
45 * Push constant block, written here and never by the caller (80 bytes):
46 * offset 0 uint first_vertex
47 * offset 4 uint vertex_count
48 * offset 8 uint stride_words
49 * offset 12 float time
50 * offset 16 mat4 transform
51 *
52 * When the operator carries parametric fields, the block grows to 96 bytes:
53 * offset 80 vec4 params, from GpuFieldOperator::get_params()
54 *
55 * transform is the geometry transform of the buffer's render processor, or
56 * identity when it has none. Fields are evaluated at transform * position, so
57 * they see the buffer as it is drawn, and vector results are carried back
58 * through the inverse before they are stored.
59 *
60 * time is seconds since the processor was constructed, and it is the only
61 * varying input a field has. MF_FIELD bakes every literal into the shader text,
62 * and the producing operator re-uploads identical records each cycle, so a
63 * field of position alone evaluates to the same result forever and draws a
64 * still image. Motion comes from a field that takes time as a second argument.
65 *
66 * @code
67 * auto op = net->get_operator_chain()->emplace<GpuFieldOperator>(
68 * Kakshya::VertexLayout::for_points());
69 * op->bind(FieldTarget::POSITION | FieldTarget::NORMAL, Fields::swirl);
70 *
71 * auto proc = std::make_shared<VertexFieldProcessor>(op);
72 * chain->add_postprocessor(proc, self);
73 * @endcode
74 *
75 * Cluster-scoped bindings (GpuFieldOperator::bind's cluster argument) need a
76 * per-vertex cluster id at dispatch time. When the operator reports
77 * needs_cluster_id(), on_attach resolves the target buffer as a
78 * NetworkGeometryBuffer and calls its ensure_cluster_ids(), which declares
79 * and populates hash_cluster_id from whatever the network's own primary
80 * GraphicsOperator reports (all zero unless that operator overrides
81 * GraphicsOperator::build_cluster_ids(), which today only PhysicsOperator
82 * does) if nothing else -- NetworkGeometryBuffer's own hash wiring, most
83 * commonly -- has already done so. This is not particle-specific: a
84 * cluster-scoped bind() against a PointCloudNetwork or any other
85 * GraphicsOperator-backed network resolves through the same base-class
86 * default rather than silently assuming PhysicsOperator. An operator that
87 * never uses cluster-scoped bind() pays none of this: needs_cluster_id() is
88 * false, nothing here runs, and build_spec()
89 * emits exactly the shader it always has.
90 */
91class MAYAFLUX_API VertexFieldProcessor : public ComputeProcessor {
92public:
93 /**
94 * @param field_operator Operator supplying the generated spec. Must already
95 * carry at least one binding and a layout with a position attribute,
96 * since the shader is compiled during construction. Throws
97 * std::invalid_argument when build_spec() yields nothing.
98 */
99 explicit VertexFieldProcessor(
100 std::shared_ptr<Nodes::Network::GpuFieldOperator> field_operator);
101
102 ~VertexFieldProcessor() override = default;
103
104 /**
105 * @brief Restrict the dispatch to a slice of the buffer.
106 * @param first_vertex Index of the first record to touch.
107 * @param vertex_count Number of records.
108 *
109 * Records are counted in the operator's layout stride, not bytes. Setting a
110 * count of zero suppresses the dispatch without detaching the processor.
111 */
112 void set_vertex_range(uint32_t first_vertex, uint32_t vertex_count);
113
114 /**
115 * @brief Drop an explicit range and cover the whole buffer again.
116 */
117 void clear_vertex_range();
118
119 /**
120 * @brief The operator this processor dispatches for.
121 */
122 [[nodiscard]] std::shared_ptr<Nodes::Network::GpuFieldOperator>
124 {
125 return m_operator;
126 }
127
128protected:
129 void on_attach(const std::shared_ptr<Buffer>& buffer) override;
130
131 /**
132 * @brief Rebuild on a stale revision, resolve the range, write push constants.
133 * @return False to suppress the dispatch when the range is empty or the
134 * operator no longer produces a spec.
135 */
136 bool on_before_execute(
138 const std::shared_ptr<VKBuffer>& buffer) override;
139
140 /**
141 * @brief One thread per record in the resolved range.
142 *
143 * The generated kernel guards its own tail, so rounding up is safe.
144 */
145 std::array<uint32_t, 3> calculate_dispatch_size(
146 const std::shared_ptr<VKBuffer>& buffer) override;
147
148 /**
149 * @brief Write the cluster_id descriptor when the operator needs one.
150 *
151 * A no-op call when m_needs_cluster_id is false. hash_cluster_id's
152 * underlying allocation is fixed-size for the buffer's whole lifetime
153 * (NetworkGeometryBuffer::declare_state never resizes a declared state
154 * field the way the primary vertex buffer resizes), so a single write
155 * here each time descriptor sets are (re)created is sufficient; unlike
156 * NetworkStateFieldProcessor's own fields, nothing here needs rewriting
157 * every cycle for resize-safety.
158 */
159 void on_descriptors_created() override;
160
161private:
162 /**
163 * @struct RangeParams
164 * @brief Push constant block matching the prefix GpuFieldOperator declares.
165 */
166 struct RangeParams {
167 uint32_t first_vertex {};
168 uint32_t vertex_count {};
169 uint32_t stride_words {};
170 float time {};
171 glm::mat4 transform { 1.0F };
172 };
173
174 /**
175 * @struct ParamsBlock
176 * @brief Push constant block when the operator carries parametric fields.
177 */
178 struct ParamsBlock {
180 std::array<float, 4> params;
181 };
182
183 static_assert(sizeof(RangeParams) == 80);
184 static_assert(sizeof(ParamsBlock) == 96);
185
186 std::shared_ptr<Nodes::Network::GpuFieldOperator> m_operator;
187
189 uint32_t m_explicit_first {};
190 uint32_t m_explicit_count {};
191 bool m_range_set {};
192
193 uint64_t m_built_revision {};
194
195 std::chrono::steady_clock::time_point m_epoch { std::chrono::steady_clock::now() };
196
197 std::shared_ptr<NetworkGeometryBuffer> m_network_buffer;
198 bool m_needs_cluster_id {};
199
200 /**
201 * @brief Records the buffer can hold at the operator's stride.
202 */
203 [[nodiscard]] uint32_t buffer_capacity(const std::shared_ptr<VKBuffer>& buffer) const;
204
205 /**
206 * @brief Seconds since construction, the value bound to the time parameter.
207 */
208 [[nodiscard]] float elapsed() const;
209
210 /**
211 * @brief Recompile and rebuild the pipeline when the operator has changed.
212 * @return False when the operator no longer yields a spec.
213 */
214 bool sync_revision();
215
216 /**
217 * @brief Ensure the backing hash_cluster_id state field exists, when
218 * m_needs_cluster_id is true.
219 * @return False when cluster scoping is needed but the attached buffer
220 * is not a NetworkGeometryBuffer, or NetworkGeometryBuffer::
221 * ensure_cluster_ids() itself fails (no primary GraphicsOperator,
222 * or it reports zero points). True when m_needs_cluster_id is
223 * false: nothing to do.
224 *
225 * Delegates the actual declare/derive/upload to
226 * NetworkGeometryBuffer::ensure_cluster_ids(), which is itself
227 * has_state-guarded, so calling this again after a revision rebuild is
228 * cheap and whichever caller (this processor, or NetworkGeometryBuffer's
229 * own hash wiring) reaches it first does the real work.
230 */
231 bool ensure_cluster_binding();
232};
233
234} // namespace MayaFlux::Buffers
std::shared_ptr< NetworkGeometryBuffer > buffer
uint32_t stride_words
Specialized ShaderProcessor for Compute Pipelines.
std::shared_ptr< Nodes::Network::GpuFieldOperator > get_field_operator() const
The operator this processor dispatches for.
std::shared_ptr< NetworkGeometryBuffer > m_network_buffer
std::shared_ptr< Nodes::Network::GpuFieldOperator > m_operator
Compute pass that applies a GpuFieldOperator's generated shader to a range of vertex records in the a...
Push constant block when the operator carries parametric fields.
Push constant block matching the prefix GpuFieldOperator declares.