MayaFlux 0.5.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 (16 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 *
51 * time is seconds since the processor was constructed, and it is the only
52 * varying input a field has. MF_FIELD bakes every literal into the shader text,
53 * and the producing operator re-uploads identical records each cycle, so a
54 * field of position alone evaluates to the same result forever and draws a
55 * still image. Motion comes from a field that takes time as a second argument.
56 *
57 * @code
58 * auto op = net->get_operator_chain()->emplace<GpuFieldOperator>(
59 * Kakshya::VertexLayout::for_points());
60 * op->bind(FieldTarget::POSITION | FieldTarget::NORMAL, Fields::swirl);
61 *
62 * auto proc = std::make_shared<VertexFieldProcessor>(op);
63 * chain->add_postprocessor(proc, self);
64 * @endcode
65 *
66 * Cluster-scoped bindings (GpuFieldOperator::bind's cluster argument) need a
67 * per-vertex cluster id at dispatch time. When the operator reports
68 * needs_cluster_id(), on_attach resolves the target buffer as a
69 * NetworkGeometryBuffer and calls its ensure_cluster_ids(), which declares
70 * and populates hash_cluster_id from whatever the network's own primary
71 * GraphicsOperator reports (all zero unless that operator overrides
72 * GraphicsOperator::build_cluster_ids(), which today only PhysicsOperator
73 * does) if nothing else -- NetworkGeometryBuffer's own hash wiring, most
74 * commonly -- has already done so. This is not particle-specific: a
75 * cluster-scoped bind() against a PointCloudNetwork or any other
76 * GraphicsOperator-backed network resolves through the same base-class
77 * default rather than silently assuming PhysicsOperator. An operator that
78 * never uses cluster-scoped bind() pays none of this: needs_cluster_id() is
79 * false, nothing here runs, and build_spec()
80 * emits exactly the shader it always has.
81 */
82class MAYAFLUX_API VertexFieldProcessor : public ComputeProcessor {
83public:
84 /**
85 * @param field_operator Operator supplying the generated spec. Must already
86 * carry at least one binding and a layout with a position attribute,
87 * since the shader is compiled during construction. Throws
88 * std::invalid_argument when build_spec() yields nothing.
89 */
90 explicit VertexFieldProcessor(
91 std::shared_ptr<Nodes::Network::GpuFieldOperator> field_operator);
92
93 ~VertexFieldProcessor() override = default;
94
95 /**
96 * @brief Restrict the dispatch to a slice of the buffer.
97 * @param first_vertex Index of the first record to touch.
98 * @param vertex_count Number of records.
99 *
100 * Records are counted in the operator's layout stride, not bytes. Setting a
101 * count of zero suppresses the dispatch without detaching the processor.
102 */
103 void set_vertex_range(uint32_t first_vertex, uint32_t vertex_count);
104
105 /**
106 * @brief Drop an explicit range and cover the whole buffer again.
107 */
108 void clear_vertex_range();
109
110 /**
111 * @brief The operator this processor dispatches for.
112 */
113 [[nodiscard]] std::shared_ptr<Nodes::Network::GpuFieldOperator>
115 {
116 return m_operator;
117 }
118
119protected:
120 void on_attach(const std::shared_ptr<Buffer>& buffer) override;
121
122 /**
123 * @brief Rebuild on a stale revision, resolve the range, write push constants.
124 * @return False to suppress the dispatch when the range is empty or the
125 * operator no longer produces a spec.
126 */
127 bool on_before_execute(
129 const std::shared_ptr<VKBuffer>& buffer) override;
130
131 /**
132 * @brief One thread per record in the resolved range.
133 *
134 * The generated kernel guards its own tail, so rounding up is safe.
135 */
136 std::array<uint32_t, 3> calculate_dispatch_size(
137 const std::shared_ptr<VKBuffer>& buffer) override;
138
139 /**
140 * @brief Write the cluster_id descriptor when the operator needs one.
141 *
142 * A no-op call when m_needs_cluster_id is false. hash_cluster_id's
143 * underlying allocation is fixed-size for the buffer's whole lifetime
144 * (NetworkGeometryBuffer::declare_state never resizes a declared state
145 * field the way the primary vertex buffer resizes), so a single write
146 * here each time descriptor sets are (re)created is sufficient; unlike
147 * NetworkStateFieldProcessor's own fields, nothing here needs rewriting
148 * every cycle for resize-safety.
149 */
150 void on_descriptors_created() override;
151
152private:
153 /**
154 * @struct RangeParams
155 * @brief Push constant block matching the prefix GpuFieldOperator declares.
156 */
157 struct RangeParams {
158 uint32_t first_vertex {};
159 uint32_t vertex_count {};
160 uint32_t stride_words {};
161 float time {};
162 };
163
164 std::shared_ptr<Nodes::Network::GpuFieldOperator> m_operator;
165
167 uint32_t m_explicit_first {};
168 uint32_t m_explicit_count {};
169 bool m_range_set {};
170
171 uint64_t m_built_revision {};
172
173 std::chrono::steady_clock::time_point m_epoch { std::chrono::steady_clock::now() };
174
175 std::shared_ptr<NetworkGeometryBuffer> m_network_buffer;
176 bool m_needs_cluster_id {};
177
178 /**
179 * @brief Records the buffer can hold at the operator's stride.
180 */
181 [[nodiscard]] uint32_t buffer_capacity(const std::shared_ptr<VKBuffer>& buffer) const;
182
183 /**
184 * @brief Seconds since construction, the value bound to the time parameter.
185 */
186 [[nodiscard]] float elapsed() const;
187
188 /**
189 * @brief Recompile and rebuild the pipeline when the operator has changed.
190 * @return False when the operator no longer yields a spec.
191 */
192 bool sync_revision();
193
194 /**
195 * @brief Ensure the backing hash_cluster_id state field exists, when
196 * m_needs_cluster_id is true.
197 * @return False when cluster scoping is needed but the attached buffer
198 * is not a NetworkGeometryBuffer, or NetworkGeometryBuffer::
199 * ensure_cluster_ids() itself fails (no primary GraphicsOperator,
200 * or it reports zero points). True when m_needs_cluster_id is
201 * false: nothing to do.
202 *
203 * Delegates the actual declare/derive/upload to
204 * NetworkGeometryBuffer::ensure_cluster_ids(), which is itself
205 * has_state-guarded, so calling this again after a revision rebuild is
206 * cheap and whichever caller (this processor, or NetworkGeometryBuffer's
207 * own hash wiring) reaches it first does the real work.
208 */
209 bool ensure_cluster_binding();
210};
211
212} // namespace MayaFlux::Buffers
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 matching the prefix GpuFieldOperator declares.