MayaFlux 0.5.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
PrimitiveMill.hpp
Go to the documentation of this file.
1#pragma once
2
3#include "ComputePress.hpp"
4#include "GraphicsUtils.hpp"
5
7
8namespace MayaFlux::Buffers {
9class VKBuffer;
10}
11
13
14/**
15 * @struct MillSpec
16 * @brief How a PrimitiveMill shapes spans. Stable across dispatches.
17 */
18struct MillSpec {
19 /**
20 * @enum Ribbon
21 * @brief Plane a line span's ribbon and a point span's quad are built in.
22 *
23 * Neither mode can hold a ribbon at a constant pixel width, which requires
24 * expanding after the vertex shader has projected. That remains a geometry
25 * shader's job and is deliberately not represented here.
26 */
27 enum class Ribbon : uint8_t {
28 WorldFacing, ///< Turned to face the viewpoint, in world space.
29 WorldPlane ///< Held in the XY plane, viewpoint ignored.
30 };
31
33
34 /**
35 * @brief Take ribbon width and point size from each vertex's own scalar
36 * (LineVertex::thickness, PointVertex::size) rather than
37 * fallback_extent.
38 *
39 * A geometry shader expanding in NDC made per-vertex variation nearly
40 * invisible at a pixel or two of width. Milling in world space makes the
41 * same variation an order of magnitude more prominent, so thickness authored
42 * as noise reads as serration rather than texture.
43 */
44 bool use_vertex_extent { true };
45
46 /**
47 * @brief World units of total ribbon width per unit of a vertex's scalar.
48 *
49 * Those scalars carry pixel-era values sized for setLineWidth, so this maps
50 * them into world space; a caller whose scalars are already world-scale sets
51 * it to 1. Scene dependent, since width does not track screen space.
52 */
53 float width_scale { 0.05F };
54
55 /** @brief World units per unit of a vertex's own size, on point spans. */
56 float point_scale { 0.004F };
57
58 /** @brief Extent used when the layout carries no scalar attribute. */
59 float fallback_extent { 2.0F };
60
61 /**
62 * @brief Write 0..1 across each ribbon and quad rather than copying source.
63 *
64 * Lets a fragment shader run a gradient, dash, or edge falloff along a
65 * ribbon. Ignored when the layout carries no texture coordinates.
66 *
67 * A point corner's UV lands in [MILL_POINT_UV_MARKER, +1] instead of
68 * [0,1] (data/shaders/include/mill_shape.glsl) -- the only way a shared
69 * fragment shader tells a point quad from a ribbon or passthrough
70 * triangle. Use mill_is_point()/mill_point_local_uv() rather than
71 * re-deriving the threshold.
72 */
73 bool synthesize_uv { true };
74};
75
76/**
77 * @struct MillView
78 * @brief Per-dispatch viewpoint. Consumed only by MillSpec::Ribbon::WorldFacing.
79 */
80struct MillView {
81 glm::vec3 eye {};
82};
83
84/**
85 * @class PrimitiveMill
86 * @brief Mills spans of mixed primitive topology into one TRIANGLE_LIST vertex
87 * buffer on the GPU.
88 *
89 * One job. Given a source vertex buffer, its layout, and a list of DrawRun
90 * spans, it produces a single triangle buffer carrying that same vertex layout,
91 * so every span can be drawn by one non-indexed draw at one topology. Points
92 * become quads, line segments become ribbons, triangle spans reduce to lists.
93 * Positions are rewritten and texture coordinates optionally synthesised; every
94 * other attribute is copied bit-exact from the source vertex its corner belongs
95 * to, so a vertex shader written against the source layout consumes the result
96 * unchanged.
97 *
98 * Owns its kernel, pipeline, descriptor sets and destination buffers, and
99 * dispatches through ComputePress directly. It is a dispatch driver in the
100 * manner of Yantra's GPU executor, not a BufferProcessor: no processing token,
101 * no attach, no chain membership, no per-cycle driver. Milling is demand driven
102 * by whoever is about to draw, so a chain position would fix when it runs
103 * relative to the geometry it consumes.
104 *
105 * The output is a transient draw source, not authored geometry. It is
106 * regenerated whenever the spans or the viewpoint change, its vertices have no
107 * correspondence back to the source, and it is not an export or readback
108 * surface. The intended caller is a render processor resolving its geometry
109 * immediately before recording.
110 *
111 * ## Known gaps
112 *
113 * Ribbon quality is not yet good enough for drawing work. Joins are mitred, so
114 * consecutive segments share their corners, but the result still shows visible
115 * unevenness along a curve. The remaining causes are not isolated: candidates
116 * are the miter limit falling back to the plain segment normal at moderate
117 * angles, width being resolved per vertex rather than along arc length, and the
118 * absence of any round join or cap.
119 *
120 * Width is fixed in world units. A geometry shader expanding after projection
121 * holds a constant pixel width at any camera distance; this cannot, so a ribbon
122 * thins on screen as the camera pulls back. Matching a screen-space width would
123 * require expanding after the vertex shader, which a compute prepass cannot do.
124 *
125 * mill_on_host has diverged and is no longer an oracle for the kernel. It
126 * implements the unjoined form only: no miter, no width averaging across shared
127 * positions. Treat it as a reference for span indexing and expansion counts,
128 * not for ribbon geometry, until it is brought back into step.
129 *
130 * A producer emitting a polyline as duplicated vertex pairs costs twice the
131 * milled vertices it needs, since every second segment is a zero-length seam
132 * that collapses. Detected and skipped, but still budgeted for.
133 */
134class MAYAFLUX_API PrimitiveMill {
135public:
136 /**
137 * @param spec Shaping parameters.
138 * @param output_ring Milled buffers to rotate between dispatches, at least
139 * one.
140 *
141 * The ring is a latency knob, not a correctness one: the barriers mill()
142 * records hold at any depth, and a depth of one merely lets the leading one
143 * stall each dispatch behind the previous frame's reads. Each extra slot
144 * costs a full copy of the milled geometry, tens of megabytes for dense line
145 * work.
146 */
147 explicit PrimitiveMill(MillSpec spec = {}, uint32_t output_ring = 2);
149
150 PrimitiveMill(const PrimitiveMill&) = delete;
154
155 /**
156 * @brief Vertices @p runs would mill to. No dispatch, no allocation.
157 *
158 * Closed form over the spans, which is why nothing here needs an atomic
159 * counter, a readback, or a frame of latency to learn its own output size.
160 */
161 [[nodiscard]] static uint32_t milled_vertex_count(std::span<const DrawRun> runs);
162
163 /**
164 * @brief Mill @p runs out of @p source into the owned buffer.
165 * @param source Vertex buffer the spans index into. Must carry a vertex
166 * layout whose stride is a multiple of four bytes and which has a
167 * word aligned position attribute.
168 * @param runs Spans to mill, in output order.
169 * @param view Viewpoint, consumed only by Ribbon::WorldFacing.
170 * @return Vertices written, or 0 when there was nothing to mill or the
171 * source could not be used.
172 *
173 * Does not wait on its own dispatch. It resolves the previous call's
174 * submission first, then submits this one and returns. output() is
175 * immediately valid to *record* a draw against; it is not valid to read from
176 * the host until a later mill() or release() has resolved the fence.
177 *
178 * Dispatched on the graphics queue, not a compute one, which is what makes
179 * the recorded barriers mean anything: submission order spans vkQueueSubmit
180 * calls to one queue, so the trailing barrier orders this write before the
181 * draw submitted afterwards and the leading one orders the previous frame's
182 * vertex fetch before this write. Neither reaches across queues, and the
183 * destination is SharingMode::eExclusive, so a dedicated compute queue would
184 * need a semaphore and a queue family ownership transfer instead.
185 *
186 * Waiting on the previous dispatch is not optional: the run and prefix
187 * tables are host visible and rewritten here, so the prior dispatch must
188 * have finished reading them.
189 *
190 * The destination grows to fit and never shrinks.
191 */
192 uint32_t mill(
193 const std::shared_ptr<Buffers::VKBuffer>& source,
194 std::span<const DrawRun> runs,
195 const MillView& view);
196
197 /**
198 * @brief The milled triangles: the ring slot the last mill() wrote.
199 * @return Null before the first successful mill().
200 *
201 * Re-read after every mill(), never cached across one: a rotating ring
202 * hands back a different buffer each dispatch.
203 */
204 [[nodiscard]] std::shared_ptr<Buffers::VKBuffer> output() const;
205
206 /** @brief Vertices written by the last mill(). */
207 [[nodiscard]] uint32_t milled_count() const { return m_milled_count; }
208
209 [[nodiscard]] const MillSpec& spec() const { return m_spec; }
210
211 /** @brief Replace the spec. Takes effect on the next mill(). */
212 void set_spec(const MillSpec& spec) { m_spec = spec; }
213
214 /** @brief Destroy the kernel, pipeline, descriptor sets and buffers. */
215 void release();
216
217private:
218 /** @brief Compiles the kernel and allocates its descriptor sets, once. */
219 bool ensure_kernel();
220
221 /**
222 * @brief Grows the destination ring, run and prefix buffers to fit.
223 *
224 * Every slot is replaced at once, so one capacity covers them all. Doing so
225 * frees buffers a recorded draw may still name, hence the graphics queue
226 * drain first, and hence the 1.5x headroom: geometry a caller is actively
227 * adding to would otherwise re-grow, and stall, on nearly every cycle.
228 */
229 bool ensure_buffers(
230 const std::shared_ptr<Buffers::VKBuffer>& source,
231 const Kakshya::VertexLayout& layout,
232 uint32_t total,
233 size_t run_count);
234
235 /**
236 * @brief Points the descriptor set at the current buffer set, on any change
237 * of source or ring slot.
238 *
239 * Rewriting a shared set is safe here because resolve_pending() has already
240 * retired the only submission that reads it, and the draw takes the milled
241 * buffer as vertex input rather than through this set.
242 */
243 void write_descriptors(const std::shared_ptr<Buffers::VKBuffer>& source);
244
245 /**
246 * @brief Wait on and reclaim the previous dispatch, if one is outstanding.
247 *
248 * Cheap in steady state: the work was submitted a frame earlier and has
249 * normally completed. Releases the fence and its command buffer.
250 */
251 void resolve_pending();
252
254
257 std::vector<DescriptorSetID> m_sets;
258 size_t m_push_constant_size { 0 };
259
260 /// Milled buffers rotated between dispatches, all at m_output_capacity.
261 std::vector<std::shared_ptr<Buffers::VKBuffer>> m_outputs;
262 std::shared_ptr<Buffers::VKBuffer> m_run_buf;
263 std::shared_ptr<Buffers::VKBuffer> m_prefix_buf;
264
265 /// Source the descriptor set currently points at, for invalidation.
266 std::weak_ptr<Buffers::VKBuffer> m_bound_source;
267
268 std::vector<uint32_t> m_prefix;
269
270 uint32_t m_output_ring { 2 };
271 size_t m_output_slot { 0 };
272
273 /// Ring slot the descriptor set currently points at, for invalidation.
274 size_t m_bound_slot { 0 };
275 bool m_descriptors_written { false };
276
277 uint32_t m_milled_count { 0 };
278 uint32_t m_output_capacity { 0 };
279
280 /// Outstanding dispatch, resolved at the start of the next mill().
281 FenceID m_pending_fence { INVALID_FENCE };
282};
283
284/**
285 * @brief Host equivalent of PrimitiveMill::mill, over raw bytes.
286 * @param src Source vertex bytes. Must be four byte aligned.
287 * @param layout Describes @p src. The result carries the same attributes.
288 * @param runs Spans to mill, in output order.
289 * @param spec Shaping parameters, as for the device path.
290 * @param view Viewpoint, as for the device path.
291 * @param dst Resized to hold the result.
292 * @return Layout describing @p dst, which is @p layout with a new vertex_count.
293 *
294 * Exists so the kernel has an oracle: identical spans, spec and view must
295 * produce identical bytes on both paths. Secondarily a route for a caller with
296 * no compute queue available. Not a general geometry utility, and not intended
297 * for per-frame use at scale.
298 */
300 std::span<const uint8_t> src,
301 const Kakshya::VertexLayout& layout,
302 std::span<const DrawRun> runs,
303 const MillSpec& spec,
304 const MillView& view,
305 std::vector<uint8_t>& dst);
306
307} // namespace MayaFlux::Portal::Graphics
uint32_t * dst
const uint32_t * src
uint32_t total
uint32_t run_count
std::shared_ptr< Core::VKImage > output
PrimitiveMill(PrimitiveMill &&)=delete
PrimitiveMill & operator=(PrimitiveMill &&)=delete
uint32_t milled_count() const
Vertices written by the last mill().
std::weak_ptr< Buffers::VKBuffer > m_bound_source
Source the descriptor set currently points at, for invalidation.
PrimitiveMill(const PrimitiveMill &)=delete
std::shared_ptr< Buffers::VKBuffer > m_prefix_buf
std::shared_ptr< Buffers::VKBuffer > m_run_buf
void set_spec(const MillSpec &spec)
Replace the spec.
std::vector< DescriptorSetID > m_sets
std::vector< std::shared_ptr< Buffers::VKBuffer > > m_outputs
Milled buffers rotated between dispatches, all at m_output_capacity.
PrimitiveMill & operator=(const PrimitiveMill &)=delete
Mills spans of mixed primitive topology into one TRIANGLE_LIST vertex buffer on the GPU.
constexpr ShaderID INVALID_SHADER
constexpr FenceID INVALID_FENCE
Kakshya::VertexLayout mill_on_host(std::span< const uint8_t > src, const Kakshya::VertexLayout &layout, std::span< const DrawRun > runs, const MillSpec &spec, const MillView &view, std::vector< uint8_t > &dst)
Host equivalent of PrimitiveMill::mill, over raw bytes.
constexpr ComputePipelineID INVALID_COMPUTE_PIPELINE
Complete description of vertex data layout in a buffer.
Ribbon
Plane a line span's ribbon and a point span's quad are built in.
@ WorldFacing
Turned to face the viewpoint, in world space.
@ WorldPlane
Held in the XY plane, viewpoint ignored.
bool synthesize_uv
Write 0..1 across each ribbon and quad rather than copying source.
float fallback_extent
Extent used when the layout carries no scalar attribute.
float width_scale
World units of total ribbon width per unit of a vertex's scalar.
bool use_vertex_extent
Take ribbon width and point size from each vertex's own scalar (LineVertex::thickness,...
float point_scale
World units per unit of a vertex's own size, on point spans.
How a PrimitiveMill shapes spans.