MayaFlux 0.5.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
PrimitiveMill.cpp
Go to the documentation of this file.
1#include "PrimitiveMill.hpp"
2
3#include "ShaderFoundry.hpp"
4#include "ShaderSpec.hpp"
5
10
12
13using Buffers::VKBuffer;
14
15namespace {
16
17 constexpr uint32_t WORKGROUP = 256;
18 constexpr uint32_t ABSENT = 0xFFFFFFFFU;
19
20 /** @brief Words per DrawRun in the run table: topology, offset, count. */
21 constexpr size_t RUN_WORDS = 3;
22
23 /**
24 * @struct MillOffsets
25 * @brief Word offsets into one source vertex, resolved from its layout.
26 */
27 struct MillOffsets {
28 uint32_t stride_words { 0 };
29 uint32_t position { ABSENT };
30 uint32_t scalar { ABSENT };
31 uint32_t tangent { ABSENT };
32 uint32_t uv { ABSENT };
33 };
34
35 /**
36 * @brief Resolve @p layout into word offsets.
37 * @return false when the stride is not word aligned or no position
38 * attribute is addressable, which makes the layout unmillable.
39 */
40 bool offsets_from(const Kakshya::VertexLayout& layout, MillOffsets& out)
41 {
42 if (layout.stride_bytes == 0 || layout.stride_bytes % sizeof(uint32_t) != 0) {
43 return false;
44 }
45
46 const auto position = layout.find_word_offset(Kakshya::DataModality::VERTEX_POSITIONS_3D);
47 if (!position) {
48 return false;
49 }
50
51 out.stride_words = layout.stride_bytes / static_cast<uint32_t>(sizeof(uint32_t));
52 out.position = *position;
53 out.scalar = layout.find_word_offset(Kakshya::DataModality::SCALAR_F32).value_or(ABSENT);
54 out.tangent = layout.find_word_offset(Kakshya::DataModality::VERTEX_TANGENTS_3D).value_or(ABSENT);
55 out.uv = layout.find_word_offset(Kakshya::DataModality::TEXTURE_COORDS_2D).value_or(ABSENT);
56
57 return true;
58 }
59
60 /**
61 * @struct MillPC
62 * @brief Push constants for the mill kernel.
63 *
64 * Field order and widths must match build_mill_spec()'s pc() declaration
65 * order, which is how ShaderSpec assigns offsets.
66 */
67 struct MillPC {
68 uint32_t total;
69 uint32_t run_count;
70 uint32_t stride_words;
72 uint32_t scalar_offset;
73 uint32_t uv_offset;
74 uint32_t mode;
75 uint32_t synth_uv;
79 float eye_x;
80 float eye_y;
81 float eye_z;
82 };
83
84 static_assert(sizeof(MillPC) == 14 * sizeof(uint32_t),
85 "MillPC must be tightly packed 4-byte fields matching build_mill_spec's pc() list");
86
87 void add_helpers(ShaderSpec::Assemble& assemble)
88 {
89 assemble.function("uint", "find_run", "uint g, uint n",
90 " uint lo = 0u;\n"
91 " uint hi = n;\n"
92 " while (lo + 1u < hi) {\n"
93 " uint mid = (lo + hi) >> 1u;\n"
94 " if (prefix[mid] <= g) { lo = mid; } else { hi = mid; }\n"
95 " }\n"
96 " return lo;\n");
97
98 assemble.function("vec3", "read_pos", "uint v, uint sw, uint po",
99 " uint b = v * sw + po;\n"
100 " return vec3(uintBitsToFloat(src[b]), uintBitsToFloat(src[b + 1u]), "
101 "uintBitsToFloat(src[b + 2u]));\n");
102
103 assemble.function("float", "read_extent", "uint v, uint sw, uint so, float fb",
104 " if (so == 0xffffffffu) { return fb; }\n"
105 " return uintBitsToFloat(src[v * sw + so]);\n");
106
107 assemble.function("void", "copy_vertex", "uint d, uint s, uint sw",
108 " uint db = d * sw;\n"
109 " uint sb = s * sw;\n"
110 " for (uint k = 0u; k < sw; ++k) { dst[db + k] = src[sb + k]; }\n");
111
112 assemble.function("void", "write_pos", "uint d, uint sw, uint po, vec3 p",
113 " uint b = d * sw + po;\n"
114 " dst[b] = floatBitsToUint(p.x);\n"
115 " dst[b + 1u] = floatBitsToUint(p.y);\n"
116 " dst[b + 2u] = floatBitsToUint(p.z);\n");
117
118 assemble.function("void", "write_uv", "uint d, uint sw, uint uo, vec2 t",
119 " if (uo == 0xffffffffu) { return; }\n"
120 " uint b = d * sw + uo;\n"
121 " dst[b] = floatBitsToUint(t.x);\n"
122 " dst[b + 1u] = floatBitsToUint(t.y);\n");
123
124 assemble.function("vec3", "view_normal", "vec3 p, vec3 eye",
125 " vec3 n = eye - p;\n"
126 " float l = length(n);\n"
127 " return l < 1e-8 ? vec3(0.0, 0.0, 1.0) : n / l;\n");
128
129 /**
130 * Direction arriving at vertex s from the nearest preceding vertex at a
131 * different position, within [lo, s]. Skipping coincident vertices lets
132 * one path serve both a true strip and a strip emitted as duplicated
133 * pairs. Zero when s starts the run.
134 */
135 assemble.function("vec3", "dir_in", "uint s, uint lo, uint sw, uint po",
136 " vec3 p = read_pos(s, sw, po);\n"
137 " uint c = s;\n"
138 " for (uint k = 0u; k < 4u; ++k) {\n"
139 " if (c <= lo) { break; }\n"
140 " c = c - 1u;\n"
141 " vec3 q = read_pos(c, sw, po);\n"
142 " if (length(p - q) > 1e-6) { return normalize(p - q); }\n"
143 " }\n"
144 " return vec3(0.0);\n");
145
146 /** Direction leaving vertex s toward the next distinct vertex below hi. */
147 assemble.function("vec3", "dir_out", "uint s, uint hi, uint sw, uint po",
148 " vec3 p = read_pos(s, sw, po);\n"
149 " uint c = s;\n"
150 " for (uint k = 0u; k < 4u; ++k) {\n"
151 " c = c + 1u;\n"
152 " if (c >= hi) { break; }\n"
153 " vec3 q = read_pos(c, sw, po);\n"
154 " if (length(q - p) > 1e-6) { return normalize(q - p); }\n"
155 " }\n"
156 " return vec3(0.0);\n");
157
158 /**
159 * Width at vertex s, averaged over every vertex sharing its position
160 * and the nearest distinct vertex on each side.
161 *
162 * A producer that emits each interior sample twice can give the two
163 * copies different thickness, which makes the ribbon step at a point
164 * where it should be continuous: one quad ends at one width and the
165 * next begins at another. Averaging over position rather than over
166 * vertex index makes both copies agree, and folding in the neighbours
167 * damps per-sample jitter that reads as serration once the ribbon is
168 * more than a pixel wide.
169 */
170 assemble.function("float", "extent_at",
171 "uint s, uint lo, uint hi, uint sw, uint po, uint so, float fb",
172 " vec3 p = read_pos(s, sw, po);\n"
173 " float sum = read_extent(s, sw, so, fb);\n"
174 " float cnt = 1.0;\n"
175 " uint c = s;\n"
176 " for (uint k = 0u; k < 4u; ++k) {\n"
177 " if (c <= lo) { break; }\n"
178 " c = c - 1u;\n"
179 " sum += read_extent(c, sw, so, fb);\n"
180 " cnt += 1.0;\n"
181 " if (length(read_pos(c, sw, po) - p) > 1e-6) { break; }\n"
182 " }\n"
183 " c = s;\n"
184 " for (uint k = 0u; k < 4u; ++k) {\n"
185 " c = c + 1u;\n"
186 " if (c >= hi) { break; }\n"
187 " sum += read_extent(c, sw, so, fb);\n"
188 " cnt += 1.0;\n"
189 " if (length(read_pos(c, sw, po) - p) > 1e-6) { break; }\n"
190 " }\n"
191 " return sum / cnt;\n");
192
193 /**
194 * Offset from vertex s to the ribbon edge, mitred.
195 *
196 * The side vector follows the bisector of the segments meeting at s,
197 * so both quads sharing s place their corners identically and the
198 * ribbon stays continuous. Scaling by the reciprocal of the bisector's
199 * projection onto the segment normal keeps the width constant through
200 * the turn; the clamp is the usual miter limit, past which a very sharp
201 * corner would otherwise throw the corner out to infinity.
202 */
203 assemble.function("vec3", "offset_from",
204 "vec3 p, vec3 din, vec3 dout, vec3 seg, vec3 eye, float half_w",
205 " vec3 vn = view_normal(p, eye);\n"
206 " vec3 ns = cross(seg, vn);\n"
207 " float nsl = length(ns);\n"
208 " if (nsl < 1e-6) { return vec3(0.0); }\n"
209 " ns = ns / nsl;\n"
210 " vec3 t = din + dout;\n"
211 " float tl = length(t);\n"
212 " if (tl < 1e-6) { return ns * half_w; }\n"
213 " vec3 nm = cross(t / tl, vn);\n"
214 " float nml = length(nm);\n"
215 " if (nml < 1e-6) { return ns * half_w; }\n"
216 " nm = nm / nml;\n"
217 " float proj = dot(nm, ns);\n"
218 " if (abs(proj) < 0.25) { return ns * half_w; }\n"
219 " return nm * (half_w / proj);\n");
220
221 /**
222 * Direction of the segment preceding a LINE_LIST pair that starts at
223 * @p s, or zero when the previous pair ends somewhere else.
224 *
225 * Pairs that meet at a shared position are a polyline written as
226 * disconnected segments, which is what a path producer emits when it
227 * duplicates each interior sample. Pairs that do not meet are genuinely
228 * separate edges and must not be joined.
229 */
230 assemble.function("vec3", "pair_dir_in", "uint s, uint lo, uint sw, uint po",
231 " if (s < lo + 2u) { return vec3(0.0); }\n"
232 " vec3 p = read_pos(s, sw, po);\n"
233 " vec3 b = read_pos(s - 1u, sw, po);\n"
234 " if (length(b - p) > 1e-6) { return vec3(0.0); }\n"
235 " vec3 a = read_pos(s - 2u, sw, po);\n"
236 " vec3 d = b - a;\n"
237 " float l = length(d);\n"
238 " return l < 1e-6 ? vec3(0.0) : d / l;\n");
239
240 /** Direction of the segment following a LINE_LIST pair ending at s. */
241 assemble.function("vec3", "pair_dir_out", "uint s, uint hi, uint sw, uint po",
242 " if (s + 2u >= hi) { return vec3(0.0); }\n"
243 " vec3 p = read_pos(s, sw, po);\n"
244 " vec3 a = read_pos(s + 1u, sw, po);\n"
245 " if (length(a - p) > 1e-6) { return vec3(0.0); }\n"
246 " vec3 b = read_pos(s + 2u, sw, po);\n"
247 " vec3 d = b - a;\n"
248 " float l = length(d);\n"
249 " return l < 1e-6 ? vec3(0.0) : d / l;\n");
250
251 /** Width at s averaged with a coincident neighbour at @p o, if any. */
252 assemble.function("float", "pair_extent",
253 "uint s, uint o, uint lo, uint hi, uint sw, uint po, uint so, float fb",
254 " float e = read_extent(s, sw, so, fb);\n"
255 " if (o < lo || o >= hi) { return e; }\n"
256 " if (length(read_pos(o, sw, po) - read_pos(s, sw, po)) > 1e-6) { return e; }\n"
257 " return 0.5 * (e + read_extent(o, sw, so, fb));\n");
258 }
259
260 /**
261 * @brief Kernel milling one output vertex per invocation.
262 *
263 * src and dst are declared UINT32 so an attribute copy is bit-exact
264 * whatever the attribute's real type; only position and texture coordinate
265 * words are reinterpreted. Topology codes follow PrimitiveTopology's
266 * declaration order.
267 *
268 * A zero-length segment collapses all six corners onto one position and
269 * skips the attribute copy: the triangles have no area, so nothing
270 * interpolates them and the untouched words are never read. That is the
271 * common case for a producer emitting a strip as duplicated vertex pairs,
272 * where every second segment is a seam.
273 */
274 ShaderSpec build_mill_spec()
275 {
276 ShaderSpec::Assemble assemble;
277 assemble
283 .pc("run_count", Kakshya::GpuDataFormat::UINT32)
284 .pc("stride_words", Kakshya::GpuDataFormat::UINT32)
285 .pc("position_offset", Kakshya::GpuDataFormat::UINT32)
286 .pc("scalar_offset", Kakshya::GpuDataFormat::UINT32)
287 .pc("uv_offset", Kakshya::GpuDataFormat::UINT32)
289 .pc("synth_uv", Kakshya::GpuDataFormat::UINT32)
290 .pc("width_scale", Kakshya::GpuDataFormat::FLOAT32)
291 .pc("point_scale", Kakshya::GpuDataFormat::FLOAT32)
292 .pc("fallback_extent", Kakshya::GpuDataFormat::FLOAT32)
296 .workgroup(WORKGROUP);
297
298 add_helpers(assemble);
299
300 std::string body;
301 body += " if (i >= total) { return; }\n";
302 body += " uint sw = stride_words;\n";
303 body += " uint po = position_offset;\n";
304 body += " vec3 eye = vec3(eye_x, eye_y, eye_z);\n";
305 body += " uint r = find_run(i, run_count);\n";
306 body += " uint rb = r * 3u;\n";
307 body += " uint topo = runs[rb];\n";
308 body += " uint voff = runs[rb + 1u];\n";
309 body += " uint local = i - prefix[r];\n";
310
311 body += " if (topo == 3u) {\n";
312 body += " copy_vertex(i, voff + local, sw);\n";
313 body += " return;\n";
314 body += " }\n";
315
316 body += " if (topo == 4u || topo == 5u) {\n";
317 body += " uint tri = local / 3u;\n";
318 body += " uint c = local - tri * 3u;\n";
319 body += " uint a; uint b1; uint b2;\n";
320 body += " if (topo == 5u) { a = 0u; b1 = tri + 1u; b2 = tri + 2u; }\n";
321 body += " else {\n";
322 body += " a = tri; b1 = tri + 1u; b2 = tri + 2u;\n";
323 body += " if ((tri & 1u) == 1u) { uint t = a; a = b1; b1 = t; }\n";
324 body += " }\n";
325 body += " uint pick = c == 0u ? a : (c == 1u ? b1 : b2);\n";
326 body += " copy_vertex(i, voff + pick, sw);\n";
327 body += " return;\n";
328 body += " }\n";
329
330 body += " uint corner = local % 6u;\n";
331 body += " uint quad = local / 6u;\n";
332 body += " vec2 uv;\n";
333
334 body += " if (topo == 0u) {\n";
335 body += " uint s = voff + quad;\n";
336 body += " vec3 p = read_pos(s, sw, po);\n";
337 body += " float h = read_extent(s, sw, scalar_offset, fallback_extent)\n";
338 body += " * point_scale * 0.5;\n";
339 body += " vec3 rx; vec3 ry;\n";
340 body += " if (mode == 1u) {\n";
341 body += " rx = vec3(h, 0.0, 0.0);\n";
342 body += " ry = vec3(0.0, h, 0.0);\n";
343 body += " } else {\n";
344 body += " vec3 n = view_normal(p, eye);\n";
345 body += " vec3 up = abs(n.y) < 0.99 ? vec3(0.0, 1.0, 0.0) : vec3(1.0, 0.0, 0.0);\n";
346 body += " rx = normalize(cross(up, n)) * h;\n";
347 body += " ry = normalize(cross(n, rx)) * h;\n";
348 body += " }\n";
349 body += " vec3 o;\n";
350 body += " if (corner == 0u) { o = -rx - ry; uv = vec2(10.0, 10.0); }\n";
351 body += " else if (corner == 1u) { o = rx - ry; uv = vec2(11.0, 10.0); }\n";
352 body += " else if (corner == 2u) { o = rx + ry; uv = vec2(11.0, 11.0); }\n";
353 body += " else if (corner == 3u) { o = -rx - ry; uv = vec2(10.0, 10.0); }\n";
354 body += " else if (corner == 4u) { o = rx + ry; uv = vec2(11.0, 11.0); }\n";
355 body += " else { o = -rx + ry; uv = vec2(10.0, 11.0); }\n";
356 body += " copy_vertex(i, s, sw);\n";
357 body += " write_pos(i, sw, po, p + o);\n";
358 body += " if (synth_uv == 1u) { write_uv(i, sw, uv_offset, uv); }\n";
359 body += " return;\n";
360 body += " }\n";
361
362 body += " uint s0 = topo == 1u ? voff + quad * 2u : voff + quad;\n";
363 body += " uint s1 = s0 + 1u;\n";
364 body += " vec3 p0 = read_pos(s0, sw, po);\n";
365 body += " vec3 p1 = read_pos(s1, sw, po);\n";
366 body += " vec3 d = p1 - p0;\n";
367 body += " if (length(d) < 1e-6) {\n";
368 body += " write_pos(i, sw, po, p0);\n";
369 body += " return;\n";
370 body += " }\n";
371 body += " float dl = length(d);\n";
372 body += " vec3 dn = d / dl;\n";
373 body += " float h0 = read_extent(s0, sw, scalar_offset, fallback_extent)\n";
374 body += " * width_scale * 0.5;\n";
375 body += " float h1 = read_extent(s1, sw, scalar_offset, fallback_extent)\n";
376 body += " * width_scale * 0.5;\n";
377 body += " vec3 e0; vec3 e1;\n";
378 body += " if (mode == 1u) {\n";
379 body += " vec3 sv = vec3(-dn.y, dn.x, 0.0);\n";
380 body += " e0 = sv * h0;\n";
381 body += " e1 = sv * h1;\n";
382 body += " } else {\n";
383 body += " uint lo = voff;\n";
384 body += " uint hi = voff + runs[rb + 2u];\n";
385 body += " vec3 din; vec3 dout;\n";
386 body += " if (topo == 2u) {\n";
387 body += " din = dir_in(s0, lo, sw, po);\n";
388 body += " dout = dir_out(s1, hi, sw, po);\n";
389 body += " h0 = extent_at(s0, lo, hi, sw, po, scalar_offset, fallback_extent)\n";
390 body += " * width_scale * 0.5;\n";
391 body += " h1 = extent_at(s1, lo, hi, sw, po, scalar_offset, fallback_extent)\n";
392 body += " * width_scale * 0.5;\n";
393 body += " } else {\n";
394 body += " din = pair_dir_in(s0, lo, sw, po);\n";
395 body += " dout = pair_dir_out(s1, hi, sw, po);\n";
396 body += " h0 = pair_extent(s0, s0 - 1u, lo, hi, sw, po, scalar_offset, fallback_extent)\n";
397 body += " * width_scale * 0.5;\n";
398 body += " h1 = pair_extent(s1, s1 + 1u, lo, hi, sw, po, scalar_offset, fallback_extent)\n";
399 body += " * width_scale * 0.5;\n";
400 body += " }\n";
401 body += " e0 = offset_from(p0, din, dn, dn, eye, h0);\n";
402 body += " e1 = offset_from(p1, dn, dout, dn, eye, h1);\n";
403 body += " }\n";
404 body += " vec3 pos; uint pick;\n";
405 body += " if (corner == 0u) { pos = p0 - e0; pick = s0; uv = vec2(0.0, 1.0); }\n";
406 body += " else if (corner == 1u) { pos = p0 + e0; pick = s0; uv = vec2(0.0, 0.0); }\n";
407 body += " else if (corner == 2u) { pos = p1 + e1; pick = s1; uv = vec2(1.0, 0.0); }\n";
408 body += " else if (corner == 3u) { pos = p0 - e0; pick = s0; uv = vec2(0.0, 1.0); }\n";
409 body += " else if (corner == 4u) { pos = p1 + e1; pick = s1; uv = vec2(1.0, 0.0); }\n";
410 body += " else { pos = p1 - e1; pick = s1; uv = vec2(1.0, 1.0); }\n";
411 body += " copy_vertex(i, pick, sw);\n";
412 body += " write_pos(i, sw, po, pos);\n";
413 body += " if (synth_uv == 1u) { write_uv(i, sw, uv_offset, uv); }\n";
414
415 assemble.kernel(KernelSource { .body = std::move(body) });
416
417 return assemble.build();
418 }
419
420 MillPC make_pc(
421 const MillSpec& spec,
422 const MillView& view,
423 const MillOffsets& off,
424 uint32_t total,
425 size_t run_count)
426 {
427 return MillPC {
428 .total = total,
429 .run_count = static_cast<uint32_t>(run_count),
430 .stride_words = off.stride_words,
431 .position_offset = off.position,
432 .scalar_offset = spec.use_vertex_extent ? off.scalar : ABSENT,
433 .uv_offset = off.uv,
434 .mode = spec.ribbon == MillSpec::Ribbon::WorldPlane ? 1U : 0U,
435 .synth_uv = spec.synthesize_uv ? 1U : 0U,
436 .width_scale = spec.width_scale,
437 .point_scale = spec.point_scale,
439 .eye_x = view.eye.x,
440 .eye_y = view.eye.y,
441 .eye_z = view.eye.z
442 };
443 }
444
445 /** @brief Exclusive prefix of milled counts, with the total appended. */
446 void build_prefix(std::span<const DrawRun> runs, std::vector<uint32_t>& prefix)
447 {
448 prefix.assign(runs.size() + 1, 0U);
449 for (size_t r = 0; r < runs.size(); ++r) {
450 prefix[r + 1] = prefix[r]
451 + triangle_vertex_count(runs[r].topology, runs[r].vertex_count);
452 }
453 }
454
455} // namespace
456
457PrimitiveMill::PrimitiveMill(MillSpec spec, uint32_t output_ring)
458 : m_spec(spec)
459 , m_output_ring(std::max(output_ring, 1U))
460{
461}
462
467
468std::shared_ptr<VKBuffer> PrimitiveMill::output() const
469{
470 if (m_outputs.empty()) {
471 return nullptr;
472 }
473 return m_outputs[m_output_slot];
474}
475
476uint32_t PrimitiveMill::milled_vertex_count(std::span<const DrawRun> runs)
477{
478 uint32_t total = 0;
479 for (const auto& run : runs) {
480 total += triangle_vertex_count(run.topology, run.vertex_count);
481 }
482 return total;
483}
484
486{
488 return true;
489 }
490
491 const auto spec = build_mill_spec();
492 m_push_constant_size = spec.push_constant_bytes;
493
494 if (m_push_constant_size != sizeof(MillPC)) {
496 "PrimitiveMill: kernel expects {} push constant bytes but MillPC is {}",
497 m_push_constant_size, sizeof(MillPC));
498 return false;
499 }
500
501 auto& foundry = get_shader_foundry();
502 m_shader = foundry.load_shader(spec);
503 if (m_shader == INVALID_SHADER) {
505 "PrimitiveMill: kernel failed to compile");
506 return false;
507 }
508
509 auto& press = get_compute_press();
510 m_pipeline = press.create_pipeline_auto(m_shader, m_push_constant_size);
513 "PrimitiveMill: pipeline creation failed");
514 return false;
515 }
516
517 m_sets = press.allocate_pipeline_descriptors(m_pipeline);
518 if (m_sets.empty()) {
520 "PrimitiveMill: descriptor allocation failed");
521 return false;
522 }
523
524 return true;
525}
526
528 const std::shared_ptr<VKBuffer>& source,
529 const Kakshya::VertexLayout& layout,
530 uint32_t total,
531 size_t run_count)
532{
535 if (!svc) {
537 "PrimitiveMill: BufferService unavailable");
538 return false;
539 }
540
541 if (total > m_output_capacity || m_outputs.size() != m_output_ring) {
542 const auto grown = static_cast<uint32_t>(static_cast<float>(total) * 1.5F);
543
544 auto milled_layout = layout;
545 milled_layout.vertex_count = total;
546
547 if (!m_outputs.empty()) {
549 }
550
551 m_outputs.assign(m_output_ring, nullptr);
552 for (auto& slot : m_outputs) {
553 slot = std::make_shared<VKBuffer>(
554 static_cast<size_t>(grown) * layout.stride_bytes,
555 VKBuffer::Usage::VERTEX,
556 source->get_modality());
557 slot->set_vertex_layout(milled_layout);
558 svc->initialize_buffer(slot);
559 }
560
561 m_output_capacity = grown;
562 m_output_slot = 0;
563 m_descriptors_written = false;
564 m_bound_source.reset();
565 } else {
566 for (auto& slot : m_outputs) {
567 auto milled_layout = *slot->get_vertex_layout();
568 milled_layout.vertex_count = total;
569 slot->set_vertex_layout(milled_layout);
570 }
571 }
572
573 const auto run_bytes = std::max<size_t>(run_count * RUN_WORDS * sizeof(uint32_t), sizeof(uint32_t));
574 if (!m_run_buf || m_run_buf->get_size_bytes() < run_bytes) {
575 m_run_buf = std::make_shared<VKBuffer>(
576 run_bytes, VKBuffer::Usage::HOST_STORAGE, Kakshya::DataModality::UNKNOWN);
577 svc->initialize_buffer(m_run_buf);
578 m_bound_source.reset();
579 }
580
581 const auto prefix_bytes = std::max<size_t>(m_prefix.size() * sizeof(uint32_t), sizeof(uint32_t));
582 if (!m_prefix_buf || m_prefix_buf->get_size_bytes() < prefix_bytes) {
583 m_prefix_buf = std::make_shared<VKBuffer>(
584 prefix_bytes, VKBuffer::Usage::HOST_STORAGE, Kakshya::DataModality::UNKNOWN);
585 svc->initialize_buffer(m_prefix_buf);
586 m_bound_source.reset();
587 }
588
589 return !m_outputs.empty() && m_run_buf && m_prefix_buf;
590}
591
592void PrimitiveMill::write_descriptors(const std::shared_ptr<VKBuffer>& source)
593{
594 if (m_descriptors_written && m_bound_source.lock() == source
596 return;
597 }
598
599 auto& foundry = get_shader_foundry();
600 const auto set = m_sets.front();
601
602 const auto bind = [&](uint32_t binding, const std::shared_ptr<VKBuffer>& buf) {
603 foundry.update_descriptor_buffer(
604 set, binding, vk::DescriptorType::eStorageBuffer,
605 buf->get_buffer(), 0, buf->get_size_bytes());
606 };
607
608 bind(0, source);
609 bind(1, m_run_buf);
610 bind(2, m_prefix_buf);
611 bind(3, m_outputs[m_output_slot]);
612
613 m_bound_source = source;
616}
617
619{
621 return;
622 }
623
624 auto& foundry = get_shader_foundry();
625 foundry.wait_for_fence(m_pending_fence);
626 foundry.release_fence(m_pending_fence);
628}
629
631 const std::shared_ptr<VKBuffer>& source,
632 std::span<const DrawRun> runs,
633 const MillView& view)
634{
636
637 m_milled_count = 0;
638
639 if (!source || runs.empty()) {
640 return 0;
641 }
642
643 const auto layout = source->get_vertex_layout();
644 if (!layout.has_value()) {
645 return 0;
646 }
647
648 MillOffsets off;
649 if (!offsets_from(*layout, off)) {
651 "PrimitiveMill: layout is not millable, stride {} needs word alignment "
652 "and an addressable position attribute",
653 layout->stride_bytes);
654 return 0;
655 }
656
657 const uint32_t source_vertices = layout->vertex_count;
658 for (const auto& run : runs) {
659 if (run.vertex_count == 0) {
660 continue;
661 }
662 if (run.vertex_offset > source_vertices
663 || run.vertex_count > source_vertices - run.vertex_offset) {
665 "PrimitiveMill: run [{}, {}) exceeds the source's {} vertices",
666 run.vertex_offset, run.vertex_offset + run.vertex_count, source_vertices);
667 return 0;
668 }
669 }
670
671 build_prefix(runs, m_prefix);
672 const uint32_t total = m_prefix.back();
673 if (total == 0) {
674 return 0;
675 }
676
677 if (!ensure_kernel() || !ensure_buffers(source, *layout, total, runs.size())) {
678 return 0;
679 }
680
681 m_output_slot = (m_output_slot + 1) % m_outputs.size();
682
683 auto* run_ptr = static_cast<uint32_t*>(m_run_buf->get_mapped_ptr());
684 auto* prefix_ptr = static_cast<uint32_t*>(m_prefix_buf->get_mapped_ptr());
685 if (!run_ptr || !prefix_ptr) {
687 "PrimitiveMill: run or prefix buffer is not host mapped");
688 return 0;
689 }
690
691 for (size_t r = 0; r < runs.size(); ++r) {
692 run_ptr[r * RUN_WORDS + 0] = static_cast<uint32_t>(runs[r].topology);
693 run_ptr[r * RUN_WORDS + 1] = runs[r].vertex_offset;
694 run_ptr[r * RUN_WORDS + 2] = runs[r].vertex_count;
695 }
696 std::memcpy(prefix_ptr, m_prefix.data(), m_prefix.size() * sizeof(uint32_t));
697
698 write_descriptors(source);
699
700 const auto pc = make_pc(m_spec, view, off, total, runs.size());
701
702 auto& foundry = get_shader_foundry();
703 auto& press = get_compute_press();
704
705 const auto destination = m_outputs[m_output_slot]->get_buffer();
706
707 auto cmd_id = foundry.begin_commands(ShaderFoundry::CommandBufferType::GRAPHICS);
708
709 foundry.buffer_barrier(
710 cmd_id,
711 destination,
712 vk::AccessFlagBits::eVertexAttributeRead,
713 vk::AccessFlagBits::eShaderWrite,
714 vk::PipelineStageFlagBits::eVertexInput,
715 vk::PipelineStageFlagBits::eComputeShader);
716
717 press.bind_all(cmd_id, m_pipeline, m_sets, &pc, sizeof(MillPC));
718 press.dispatch(cmd_id, (total + WORKGROUP - 1) / WORKGROUP, 1, 1);
719
720 foundry.buffer_barrier(
721 cmd_id,
722 destination,
723 vk::AccessFlagBits::eShaderWrite,
724 vk::AccessFlagBits::eVertexAttributeRead,
725 vk::PipelineStageFlagBits::eComputeShader,
726 vk::PipelineStageFlagBits::eVertexInput);
727
728 m_pending_fence = foundry.submit_async(cmd_id);
731 "PrimitiveMill: dispatch submission failed");
732 return 0;
733 }
734
736
738 "PrimitiveMill: milled {} runs into {} vertices", runs.size(), total);
739
740 return total;
741}
742
744{
746
747 auto& foundry = get_shader_foundry();
748 auto& press = get_compute_press();
749
751 press.destroy_pipeline(m_pipeline);
753 }
754
755 if (m_shader != INVALID_SHADER) {
756 foundry.destroy_shader(m_shader);
758 }
759
760 m_sets.clear();
761 m_bound_source.reset();
762 m_bound_slot = 0;
763 m_descriptors_written = false;
764 m_outputs.clear();
765 m_output_slot = 0;
766 m_run_buf.reset();
767 m_prefix_buf.reset();
768 m_prefix.clear();
770 m_milled_count = 0;
772}
773
774// ===========================================================================
775// Host path
776// ===========================================================================
777
778namespace {
779
780 /** @brief Reads and writes one vertex record as words, mirroring the kernel. */
781 struct HostRecords {
782 const uint32_t* src;
783 uint32_t* dst;
784 uint32_t stride_words;
785
786 [[nodiscard]] glm::vec3 read_pos(uint32_t v, uint32_t po) const
787 {
788 const uint32_t* w = src + static_cast<size_t>(v) * stride_words + po;
789 glm::vec3 p;
790 std::memcpy(&p.x, w, sizeof(float));
791 std::memcpy(&p.y, w + 1, sizeof(float));
792 std::memcpy(&p.z, w + 2, sizeof(float));
793 return p;
794 }
795
796 [[nodiscard]] float read_extent(uint32_t v, uint32_t so, float fb) const
797 {
798 if (so == ABSENT) {
799 return fb;
800 }
801 float f = 0.0F;
802 std::memcpy(&f, src + static_cast<size_t>(v) * stride_words + so, sizeof(float));
803 return f;
804 }
805
806 void copy_vertex(uint32_t d, uint32_t s) const
807 {
808 std::memcpy(
809 dst + static_cast<size_t>(d) * stride_words,
810 src + static_cast<size_t>(s) * stride_words,
811 static_cast<size_t>(stride_words) * sizeof(uint32_t));
812 }
813
814 void write_pos(uint32_t d, uint32_t po, const glm::vec3& p) const
815 {
816 uint32_t* w = dst + static_cast<size_t>(d) * stride_words + po;
817 std::memcpy(w, &p.x, sizeof(float));
818 std::memcpy(w + 1, &p.y, sizeof(float));
819 std::memcpy(w + 2, &p.z, sizeof(float));
820 }
821
822 void write_uv(uint32_t d, uint32_t uo, const glm::vec2& t) const
823 {
824 if (uo == ABSENT) {
825 return;
826 }
827 uint32_t* w = dst + static_cast<size_t>(d) * stride_words + uo;
828 std::memcpy(w, &t.x, sizeof(float));
829 std::memcpy(w + 1, &t.y, sizeof(float));
830 }
831 };
832
833 glm::vec3 host_view_normal(const glm::vec3& p, const glm::vec3& eye)
834 {
835 const glm::vec3 n = eye - p;
836 const float l = glm::length(n);
837 return l < 1e-8F ? glm::vec3(0.0F, 0.0F, 1.0F) : n / l;
838 }
839
840 glm::vec3 host_side_at(
841 const glm::vec3& p,
842 const glm::vec3& seg,
843 const glm::vec3& eye,
844 float half_w,
845 bool world_plane)
846 {
847 glm::vec3 dir = seg;
848 const float dl = glm::length(dir);
849 if (dl < 1e-8F) {
850 return glm::vec3(0.0F);
851 }
852 dir /= dl;
853
854 glm::vec3 s = world_plane
855 ? glm::vec3(-dir.y, dir.x, 0.0F)
856 : glm::cross(dir, host_view_normal(p, eye));
857
858 const float sl = glm::length(s);
859 return (sl < 1e-6F ? glm::vec3(0.0F, 1.0F, 0.0F) : s / sl) * half_w;
860 }
861
862} // namespace
863
865 std::span<const uint8_t> src,
866 const Kakshya::VertexLayout& layout,
867 std::span<const DrawRun> runs,
868 const MillSpec& spec,
869 const MillView& view,
870 std::vector<uint8_t>& dst)
871{
872 auto result = layout;
873 result.vertex_count = 0;
874 dst.clear();
875
876 MillOffsets off;
877 if (!offsets_from(layout, off)) {
878 return result;
879 }
880
881 std::vector<uint32_t> prefix;
882 build_prefix(runs, prefix);
883 const uint32_t total = prefix.empty() ? 0U : prefix.back();
884 if (total == 0) {
885 return result;
886 }
887
888 dst.assign(static_cast<size_t>(total) * layout.stride_bytes, 0);
889 result.vertex_count = total;
890
891 const HostRecords rec {
892 .src = reinterpret_cast<const uint32_t*>(src.data()),
893 .dst = reinterpret_cast<uint32_t*>(dst.data()),
894 .stride_words = off.stride_words
895 };
896
897 const bool world_plane = spec.ribbon == MillSpec::Ribbon::WorldPlane;
898 const uint32_t scalar_offset = spec.use_vertex_extent ? off.scalar : ABSENT;
899
900 for (size_t r = 0; r < runs.size(); ++r) {
901 const auto& run = runs[r];
902 const uint32_t emitted = prefix[r + 1] - prefix[r];
903 const uint32_t base = prefix[r];
904 const uint32_t voff = run.vertex_offset;
905
906 for (uint32_t local = 0; local < emitted; ++local) {
907 const uint32_t out = base + local;
908
909 if (run.topology == PrimitiveTopology::TRIANGLE_LIST) {
910 rec.copy_vertex(out, voff + local);
911 continue;
912 }
913
915 || run.topology == PrimitiveTopology::TRIANGLE_FAN) {
916 const uint32_t tri = local / 3U;
917 const uint32_t c = local - tri * 3U;
918 uint32_t a = 0;
919 uint32_t b1 = tri + 1U;
920 uint32_t b2 = tri + 2U;
921 if (run.topology == PrimitiveTopology::TRIANGLE_STRIP) {
922 a = tri;
923 if ((tri & 1U) == 1U) {
924 std::swap(a, b1);
925 }
926 }
927 const uint32_t pick = c == 0U ? a : (c == 1U ? b1 : b2);
928 rec.copy_vertex(out, voff + pick);
929 continue;
930 }
931
932 const uint32_t corner = local % 6U;
933 const uint32_t quad = local / 6U;
934
935 if (run.topology == PrimitiveTopology::POINT_LIST) {
936 const uint32_t s = voff + quad;
937 const glm::vec3 p = rec.read_pos(s, off.position);
938 const float h = rec.read_extent(s, scalar_offset, spec.fallback_extent)
939 * spec.point_scale * 0.5F;
940
941 glm::vec3 rx;
942 glm::vec3 ry;
943 if (world_plane) {
944 rx = glm::vec3(h, 0.0F, 0.0F);
945 ry = glm::vec3(0.0F, h, 0.0F);
946 } else {
947 const glm::vec3 n = host_view_normal(p, view.eye);
948 const glm::vec3 up = std::abs(n.y) < 0.99F
949 ? glm::vec3(0.0F, 1.0F, 0.0F)
950 : glm::vec3(1.0F, 0.0F, 0.0F);
951 rx = glm::normalize(glm::cross(up, n)) * h;
952 ry = glm::normalize(glm::cross(n, rx)) * h;
953 }
954
955 glm::vec3 o;
956 glm::vec2 uv;
957 switch (corner) {
958 case 1:
959 o = rx - ry;
960 uv = { 1.0F, 0.0F };
961 break;
962 case 2:
963 case 4:
964 o = rx + ry;
965 uv = { 1.0F, 1.0F };
966 break;
967 case 5:
968 o = -rx + ry;
969 uv = { 0.0F, 1.0F };
970 break;
971 default:
972 o = -rx - ry;
973 uv = { 0.0F, 0.0F };
974 break;
975 }
976
977 rec.copy_vertex(out, s);
978 rec.write_pos(out, off.position, p + o);
979 if (spec.synthesize_uv) {
980 rec.write_uv(out, off.uv, uv);
981 }
982 continue;
983 }
984
985 const uint32_t s0 = run.topology == PrimitiveTopology::LINE_LIST
986 ? voff + quad * 2U
987 : voff + quad;
988 const uint32_t s1 = s0 + 1U;
989
990 const glm::vec3 p0 = rec.read_pos(s0, off.position);
991 const glm::vec3 p1 = rec.read_pos(s1, off.position);
992 const glm::vec3 d = p1 - p0;
993
994 if (glm::length(d) < 1e-6F) {
995 rec.write_pos(out, off.position, p0);
996 continue;
997 }
998
999 const float h0 = rec.read_extent(s0, scalar_offset, spec.fallback_extent)
1000 * spec.width_scale * 0.5F;
1001 const float h1 = rec.read_extent(s1, scalar_offset, spec.fallback_extent)
1002 * spec.width_scale * 0.5F;
1003
1004 const glm::vec3 e0 = host_side_at(
1005 p0, d, view.eye, h0, world_plane);
1006 const glm::vec3 e1 = host_side_at(
1007 p1, d, view.eye, h1, world_plane);
1008
1009 glm::vec3 pos;
1010 uint32_t pick = s0;
1011 glm::vec2 uv;
1012 switch (corner) {
1013 case 1:
1014 pos = p0 + e0;
1015 uv = { 0.0F, 0.0F };
1016 break;
1017 case 2:
1018 case 4:
1019 pos = p1 + e1;
1020 pick = s1;
1021 uv = { 1.0F, 0.0F };
1022 break;
1023 case 5:
1024 pos = p1 - e1;
1025 pick = s1;
1026 uv = { 1.0F, 1.0F };
1027 break;
1028 default:
1029 pos = p0 - e0;
1030 uv = { 0.0F, 1.0F };
1031 break;
1032 }
1033
1034 rec.copy_vertex(out, pick);
1035 rec.write_pos(out, off.position, pos);
1036 if (spec.synthesize_uv) {
1037 rec.write_uv(out, off.uv, uv);
1038 }
1039 }
1040 }
1041
1042 return result;
1043}
1044
1045} // namespace MayaFlux::Portal::Graphics
#define MF_ERROR(comp, ctx,...)
#define MF_RT_ERROR(comp, ctx,...)
#define MF_RT_TRACE(comp, ctx,...)
uint32_t h
Definition InkPress.cpp:29
uint32_t stride_words
uint32_t * dst
uint32_t position_offset
float fallback_extent
uint32_t synth_uv
const uint32_t * src
float point_scale
uint32_t total
uint32_t mode
uint32_t tangent
uint32_t scalar
uint32_t run_count
uint32_t uv_offset
float width_scale
float eye_z
float eye_y
float eye_x
uint32_t position
uint32_t scalar_offset
uint32_t uv
size_t a
glm::ivec3 max
std::vector< float > * out
PrimitiveMill(MillSpec spec={}, uint32_t output_ring=2)
size_t m_bound_slot
Ring slot the descriptor set currently points at, for invalidation.
std::shared_ptr< Buffers::VKBuffer > output() const
The milled triangles: the ring slot the last mill() wrote.
static uint32_t milled_vertex_count(std::span< const DrawRun > runs)
Vertices runs would mill to.
FenceID m_pending_fence
Outstanding dispatch, resolved at the start of the next mill().
std::weak_ptr< Buffers::VKBuffer > m_bound_source
Source the descriptor set currently points at, for invalidation.
bool ensure_buffers(const std::shared_ptr< Buffers::VKBuffer > &source, const Kakshya::VertexLayout &layout, uint32_t total, size_t run_count)
Grows the destination ring, run and prefix buffers to fit.
std::shared_ptr< Buffers::VKBuffer > m_prefix_buf
void release()
Destroy the kernel, pipeline, descriptor sets and buffers.
std::shared_ptr< Buffers::VKBuffer > m_run_buf
std::vector< DescriptorSetID > m_sets
bool ensure_kernel()
Compiles the kernel and allocates its descriptor sets, once.
std::vector< std::shared_ptr< Buffers::VKBuffer > > m_outputs
Milled buffers rotated between dispatches, all at m_output_capacity.
uint32_t mill(const std::shared_ptr< Buffers::VKBuffer > &source, std::span< const DrawRun > runs, const MillView &view)
Mill runs out of source into the owned buffer.
void resolve_pending()
Wait on and reclaim the previous dispatch, if one is outstanding.
void write_descriptors(const std::shared_ptr< Buffers::VKBuffer > &source)
Points the descriptor set at the current buffer set, on any change of source or ring slot.
vk::Queue get_graphics_queue() const
Get Vulkan graphics queue.
Interface * get_service()
Query for a backend service.
static BackendRegistry & instance()
Get the global registry instance.
void run()
Definition main.cpp:22
@ Rendering
GPU rendering operations (graphics pipeline, frame rendering)
@ Portal
High-level user-facing API layer.
@ UNKNOWN
Unknown or undefined modality.
@ SCALAR_F32
Single-channel float data.
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.
MAYAFLUX_API ShaderFoundry & get_shader_foundry()
Get the global shader compiler instance.
constexpr ComputePipelineID INVALID_COMPUTE_PIPELINE
constexpr uint32_t triangle_vertex_count(PrimitiveTopology topology, uint32_t n) noexcept
Vertices a span of n at topology yields once reduced to a TRIANGLE_LIST, with points becoming quads a...
MAYAFLUX_API ComputePress & get_compute_press()
uint32_t stride_bytes
Total bytes per vertex (stride in Vulkan terms) e.g., 3 floats (position) + 3 floats (normal) = 24 by...
uint32_t vertex_count
Total number of vertices in this buffer.
Complete description of vertex data layout in a buffer.
@ 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.
Backend buffer management service interface.