MayaFlux 0.5.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
ShaderSpec.hpp
Go to the documentation of this file.
1#pragma once
2
4
6
7/**
8 * @enum BindingDirection
9 * @brief Data flow direction for a shader binding slot.
10 */
11enum class BindingDirection : uint8_t {
12 Input,
13 Output,
14 InOut,
15};
16
17/**
18 * @enum KernelTemplate
19 * @brief Structural shape of the generated compute kernel.
20 *
21 * Determines thread dispatch math, binding structure, and loop shape.
22 */
23enum class KernelTemplate : uint8_t {
24 Elementwise, ///< f(x[i]) -> y[i]; one thread per element
25 Reduction, ///< f(x[0..n]) -> scalar; shared-memory tree reduction
26 Stencil, ///< f(x[i-k..i+k]) -> y[i]; neighbourhood reads, radius in PC
27 GeometryEmit, ///< Writes into vertex SSBO with atomic counter
28 BitonicSort, ///< Bitonic sort network; one thread per element
29 Convolve2D, ///< 2D separable or non-separable convolution; kernel weights in SSBO, radius in PC
30 Scan, ///< Inclusive prefix scan over one InOut SSBO, double-buffered
31 ///< Hillis-Steele in shared memory. Op selects the combine function.
32};
33
34/**
35 * @struct KernelSource
36 * @brief Parsed representation of a user-supplied kernel lambda.
37 *
38 * Produced by the MF_KERNEL macro, which stringifies the lambda text
39 * at the call site. The parameter names must match binding and push
40 * constant field names declared on the enclosing ShaderSpec in
41 * declaration order: SSBO bindings first, then PC fields, then
42 * optionally a uint32_t index parameter named i.
43 *
44 * The body is extracted verbatim between the outermost braces and
45 * injected into the generated GLSL compute shader by the emitter.
46 *
47 * @code
48 * auto spec = ShaderSpec::Assemble{}
49 * .ssbo("sig", BindingDirection::InOut, Kakshya::GpuDataFormat::FLOAT32)
50 * .pc("gain")
51 * .kernel(MF_KERNEL([](float* sig, float gain, uint32_t i) {
52 * sig[i] = sig[i] * gain + sin(float(i) * 0.01f);
53 * }))
54 * .build();
55 * @endcode
56 */
58 std::string raw;
59 std::vector<std::string> param_names;
60 std::string body;
61
62 /**
63 * @brief Parse parameter names and body from a stringified lambda.
64 *
65 * Extracts identifiers from the parameter list by stripping type
66 * tokens (everything up to and including the last space or * before
67 * the identifier). Extracts body as the text between the first { and
68 * the matching closing }.
69 *
70 * @param stringified Raw text produced by MF_KERNEL.
71 * @return Populated KernelSource.
72 */
73 [[nodiscard]] static KernelSource parse(std::string_view stringified)
74 {
75 KernelSource ks;
76 ks.raw = std::string(stringified);
77
78 ///< Locate parameter list: between first '(' after '[' and its matching ')'
79 const auto bracket = stringified.find('[');
80 const auto paren_open = stringified.find('(', bracket);
81 if (paren_open == std::string_view::npos)
82 return ks;
83
84 std::size_t depth = 1;
85 std::size_t paren_close = paren_open + 1;
86 while (paren_close < stringified.size() && depth > 0) {
87 if (stringified[paren_close] == '(') {
88 ++depth;
89 } else if (stringified[paren_close] == ')') {
90 --depth;
91 }
92 ++paren_close;
93 }
94 --paren_close; ///< points at the closing ')'
95
96 const auto params_text = stringified.substr(paren_open + 1, paren_close - paren_open - 1);
97
98 ///< Split on ',' and extract the last token (the identifier) from each param
99 auto extract_name = [](std::string_view param) -> std::string {
100 ///< strip trailing whitespace
101 auto end = param.find_last_not_of(" \t\n\r");
102 if (end == std::string_view::npos)
103 return {};
104 param = param.substr(0, end + 1);
105 ///< find last space or * - identifier follows
106 auto sep = param.find_last_of(" *\t");
107 if (sep == std::string_view::npos)
108 return std::string(param);
109 return std::string(param.substr(sep + 1));
110 };
111
112 std::size_t start = 0;
113 while (start < params_text.size()) {
114 ///< find next comma not inside <> or ()
115 int angle = 0, paren = 0;
116 std::size_t pos = start;
117 while (pos < params_text.size()) {
118 char c = params_text[pos];
119 if (c == '<') {
120 ++angle;
121 } else if (c == '>') {
122 --angle;
123 } else if (c == '(') {
124 ++paren;
125 } else if (c == ')') {
126 --paren;
127 } else if (c == ',' && angle == 0 && paren == 0) {
128 break;
129 }
130 ++pos;
131 }
132 auto name = extract_name(params_text.substr(start, pos - start));
133 if (!name.empty())
134 ks.param_names.push_back(std::move(name));
135 start = (pos < params_text.size()) ? pos + 1 : pos;
136 }
137
138 ///< Locate body: between first '{' after ')' and its matching '}'
139 const auto brace_open = stringified.find('{', paren_close);
140 if (brace_open == std::string_view::npos)
141 return ks;
142
143 depth = 1;
144 std::size_t brace_close = brace_open + 1;
145 while (brace_close < stringified.size() && depth > 0) {
146 if (stringified[brace_close] == '{') {
147 ++depth;
148 } else if (stringified[brace_close] == '}') {
149 --depth;
150 }
151 ++brace_close;
152 }
153 --brace_close;
154
155 ks.body = std::string(stringified.substr(brace_open + 1, brace_close - brace_open - 1));
156 return ks;
157 }
158};
159
160/**
161 * @enum KernelOp
162 * @brief Named operation the SPIR-V emitter knows how to lower to opcodes.
163 *
164 * Each value maps to a fixed, unambiguous instruction sequence. No string
165 * parsing or expression compilation is involved.
166 *
167 * Naming convention: operands are listed in application order.
168 * PC fields are consumed in declaration order from ShaderSpec::pc_fields.
169 * SSBO operands are the InOut/Output bindings in declaration order.
170 *
171 * Single-SSBO elementwise — arithmetic (sig[i] = f(sig[i], pc...)):
172 * Scale sig[i] = sig[i] * pc[0]
173 * ScaleOffset sig[i] = sig[i] * pc[0] + pc[1]
174 * Fma sig[i] = sig[i] * pc[0] + pc[1] (fused, single instruction)
175 * Offset sig[i] = sig[i] + pc[0]
176 * Clip sig[i] = clamp(sig[i], pc[0], pc[1])
177 * Abs sig[i] = abs(sig[i])
178 * Negate sig[i] = -sig[i]
179 * Floor sig[i] = floor(sig[i])
180 * Ceil sig[i] = ceil(sig[i])
181 * Round sig[i] = round(sig[i])
182 * Trunc sig[i] = trunc(sig[i])
183 * Fract sig[i] = fract(sig[i])
184 * Sqrt sig[i] = sqrt(sig[i])
185 * InverseSqrt sig[i] = 1/sqrt(sig[i])
186 *
187 * Single-SSBO elementwise — transcendental (sig[i] = f(sig[i])):
188 * Sin sig[i] = sin(sig[i])
189 * Cos sig[i] = cos(sig[i])
190 * Tan sig[i] = tan(sig[i])
191 * Asin sig[i] = asin(sig[i])
192 * Acos sig[i] = acos(sig[i])
193 * Atan sig[i] = atan(sig[i])
194 * Sinh sig[i] = sinh(sig[i])
195 * Cosh sig[i] = cosh(sig[i])
196 * Tanh sig[i] = tanh(sig[i])
197 * Exp sig[i] = e^sig[i]
198 * Exp2 sig[i] = 2^sig[i]
199 * Log sig[i] = ln(sig[i])
200 * Log2 sig[i] = log2(sig[i])
201 *
202 * Two-SSBO elementwise (out[i] = f(a[i], b[i])):
203 * Add out[i] = a[i] + b[i]
204 * Multiply out[i] = a[i] * b[i]
205 * Mix out[i] = a[i] + (b[i] - a[i]) * pc[0]
206 * Sub out[i] = a[i] - b[i]
207 * Pow out[i] = pow(a[i], b[i])
208 * Atan2 out[i] = atan(a[i], b[i])
209 * Min out[i] = min(a[i], b[i])
210 * MaxTwo out[i] = max(a[i], b[i])
211 * Step out[i] = step(a[i], b[i]) edge=a, x=b
212 *
213 * Two-SSBO + one PC:
214 * SmoothStep out[i] = smoothstep(a[i], b[i], pc[0])
215 *
216 * Image body elementwise (out = f(pixel, pc)):
217 * CompareGE out[ch] = pixel[ch] >= pc[0] ? 1.0 : 0.0
218 * CompareGEPreserve out[ch] = pixel[ch] >= pc[0] ? pc[1] : pixel[ch]
219 * ChannelDot out = dot(pixel.rgba, pc[0..3]) broadcast to all channels
220 * ChannelReplicate out = pixel[pc_channel_index].xxxx (single channel to all)
221 *
222 * Reduction operations (one InOut SSBO, shared memory):
223 * Sum accumulate + into shared[lid], tree reduce
224 * Max accumulate max into shared[lid], tree reduce
225 * IndexScale sig[i] = float(i) * sig[i]
226 */
227enum class KernelOp : uint8_t {
228 // arithmetic
229 Scale,
231 Fma,
232 Offset,
233 Clip,
234 Abs,
235 Negate,
236 Floor,
237 Ceil,
238 Round,
239 Trunc,
240 Fract,
241 Sqrt,
243 // transcendental
244 Sin,
245 Cos,
246 Tan,
247 Asin,
248 Acos,
249 Atan,
250 Sinh,
251 Cosh,
252 Tanh,
253 Exp,
254 Exp2,
255 Log,
256 Log2,
257 // two-SSBO
258 Add,
259 Multiply,
260 Mix,
261 Sub,
262 Pow,
263 Atan2,
264 Min,
265 MaxTwo,
266 Step,
268 // reduction
269 Sum,
270 Max,
271 ScanSum, ///< Inclusive prefix sum: shared[i] = sum(x[0..i])
272 MaxIndex, ///< Reduction variant: finds max value AND its index.
273 ///< First InOut SSBO holds the values (overwritten with the
274 ///< max in slot 0). Second InOut SSBO (UINT32) receives the
275 ///< winning index in slot 0.
276 IndexScale, ///< out[i] = float(i) * a[i]. The only op that uses the
277 ///< invocation index itself as an arithmetic operand rather
278 ///< than purely for addressing.
279
280 CompareGE, ///< out[ch] = pixel[ch] >= pc[0] ? 1.0 : 0.0
281 CompareGEPreserve, ///< out[ch] = pixel[ch] >= pc[0] ? pc[1] : pixel[ch]
282 ChannelDot, ///< out = dot(pixel.rgba, pc[0..3]) broadcast to all channels
283 ChannelReplicate, ///< out = pixel[pc_channel_index].xxxx (single channel to all)
284
285 /**
286 * @brief Stencil-only: blends a cell toward a scaled neighbor sum by a
287 * rate factor: out = center + pc[N] * (sum * pc[N+1] - center),
288 * where N is the next available push-constant index after any
289 * fields the template itself has already declared (e.g. width/
290 * height for Stencil). pc[N] is the blend rate (0 = unchanged,
291 * 1 = fully adopt the scaled neighbor value); pc[N+1] is the
292 * neighbor-sum scale, typically 1/neighbor_count to average
293 * (0.125 for an 8-neighbor Moore stencil). Covers Jacobi-style
294 * diffusion and any other rate-blended local-average relaxation.
295 * Float element formats only.
296 */
298};
299
300/**
301 * @struct BindingSlot
302 * @brief Declaration of one SSBO or image binding in a generated shader.
303 */
312
313/**
314 * @struct PushConstantField
315 * @brief One field in the generated push constant block.
316 */
321
322/**
323 * @struct FunctionDef
324 * @brief A standalone function emitted before main() in the generated GLSL.
325 *
326 * Body text is injected verbatim between the braces of the emitted signature.
327 * Consumed by the GLSL kernel path only: the SPIR-V assembly emitter has no
328 * function-call lowering and ignores this list.
329 */
331 std::string return_type;
332 std::string name;
333 std::string params;
334 std::string body;
335};
336
337/**
338 * @struct ShaderSpec
339 * @brief Complete declarative description of a generated compute shader.
340 *
341 * Pure value type. Consumed by ShaderFoundry::load_shader(const ShaderSpec&)
342 * which emits SPIR-V assembly via VKShaderModule::emit_spirv_asm() and
343 * assembles it via VKShaderModule::create_from_spirv_asm(). No shaderc
344 * or GLSL toolchain is involved.
345 *
346 * The op field selects a named operation the emitter knows how to lower
347 * to SPIR-V opcodes deterministically. PC fields are consumed in
348 * declaration order as operands.
349 *
350 * @code
351 * auto spec = ShaderSpec::Assemble{}
352 * .ssbo("sig", BindingDirection::InOut, Kakshya::GpuDataFormat::FLOAT32)
353 * .pc("gain")
354 * .pc("offset")
355 * .op(KernelOp::ScaleOffset)
356 * .build();
357 * @endcode
358 */
362 std::vector<BindingSlot> bindings;
363 std::vector<PushConstantField> pc_fields;
364 std::array<uint32_t, 3> workgroup_size { 256, 1, 1 };
365 uint32_t push_constant_bytes { 0 };
366 std::vector<FunctionDef> functions; ///< Emitted before main(). GLSL path only.
367 std::optional<KernelSource> kernel; ///< When set, KernelOp is ignored.
368
369 /**
370 * @class Assemble
371 * @brief Fluent assembler producing a ShaderSpec.
372 *
373 * Binding indices are assigned in declaration order starting at 1.
374 */
375 class Assemble {
376 public:
377 Assemble() = default;
378
379 /** @brief Set the kernel template. Defaults to Elementwise. */
381 {
382 m_tmpl = t;
383 return *this;
384 }
385
386 /**
387 * @brief Set the named operation the emitter will lower to SPIR-V.
388 * @param o KernelOp value.
389 */
391 {
392 m_op = o;
393 return *this;
394 }
395
396 Assemble& start_set(uint32_t s)
397 {
398 m_set = s;
399 return *this;
400 }
401
403 {
404 m_next_binding = n;
405 return *this;
406 }
407
408 /**
409 * @brief Declare an SSBO binding.
410 */
413 {
414 m_bindings.push_back({
415 .name = std::move(name),
416 .direction = direction,
417 .format = format,
418 .modality = modality,
419 .set = m_set,
420 .binding_index = m_next_binding++,
421 });
422 return *this;
423 }
424
425 /**
426 * @brief Declare a sampled image binding (sampler2D).
427 */
428 Assemble& texture(std::string name)
429 {
430 m_bindings.push_back({
431 .name = std::move(name),
432 .direction = BindingDirection::Input,
435 .binding_index = m_next_binding++,
436 });
437 return *this;
438 }
439
440 /**
441 * @brief Declare a storage image binding (image2D).
442 */
444 std::string name,
446 {
447 m_bindings.push_back({
448 .name = std::move(name),
449 .direction = direction,
452 .binding_index = m_next_binding++,
453 });
454 return *this;
455 }
456
457 /**
458 * @brief Declare a push constant field with explicit format.
459 */
461 {
462 m_pc_fields.push_back({ .name = std::move(name), .format = format });
463 return *this;
464 }
465
466 /** @brief Declare a float push constant field. */
467 Assemble& pc(std::string name)
468 {
469 return pc(std::move(name), Kakshya::GpuDataFormat::FLOAT32);
470 }
471
472 /** @brief Override workgroup size. Defaults to {256, 1, 1}. */
473 Assemble& workgroup(uint32_t x, uint32_t y = 1, uint32_t z = 1)
474 {
475 m_workgroup = { x, y, z };
476 return *this;
477 }
478
479 /**
480 * @brief Declare a function emitted before main() in the generated GLSL.
481 * @param return_type GLSL return type spelling.
482 * @param name Function name, callable from the kernel body.
483 * @param params Parameter list verbatim, including types ("vec3 p").
484 * @param body Function body text without the enclosing braces.
485 *
486 * Requires a kernel supplied via MF_KERNEL. Names are emitted as given;
487 * two functions sharing a name produce a GLSL redefinition error rather
488 * than a silent override.
489 */
491 std::string return_type,
492 std::string name,
493 std::string params,
494 std::string body)
495 {
496 m_functions.push_back({ .return_type = std::move(return_type),
497 .name = std::move(name),
498 .params = std::move(params),
499 .body = std::move(body) });
500 return *this;
501 }
502
503 /**
504 * @brief Supply a user kernel via MF_KERNEL. When set, KernelOp is ignored.
505 * @param ks KernelSource produced by MF_KERNEL. If its param_names is
506 * empty, build() fills it from the declared bindings, then the
507 * push constant fields, then a trailing "i".
508 */
510 {
511 m_kernel = std::move(ks);
512 return *this;
513 }
514
515 /** @brief Finalise and return the ShaderSpec. */
516 [[nodiscard]] ShaderSpec build()
517 {
518 uint32_t pc_bytes = 0;
519 for (const auto& f : m_pc_fields)
520 pc_bytes += Kakshya::gpu_data_format_bytes(f.format);
521
522 if (m_kernel.has_value() && m_kernel->param_names.empty()) {
523 auto& names = m_kernel->param_names;
524 names.reserve(m_bindings.size() + m_pc_fields.size() + 1);
525 for (const auto& b : m_bindings)
526 names.push_back(b.name);
527 for (const auto& f : m_pc_fields)
528 names.push_back(f.name);
529 names.emplace_back("i");
530 }
531
532 return ShaderSpec {
533 .tmpl = m_tmpl,
534 .op = m_op,
535 .bindings = std::move(m_bindings),
536 .pc_fields = std::move(m_pc_fields),
537 .workgroup_size = m_workgroup,
538 .push_constant_bytes = pc_bytes,
539 .functions = std::move(m_functions),
540 .kernel = std::move(m_kernel),
541 };
542 }
543
544 private:
547 std::vector<BindingSlot> m_bindings;
548 std::vector<PushConstantField> m_pc_fields;
549 std::array<uint32_t, 3> m_workgroup { 256, 1, 1 };
550 uint32_t m_set { 0 };
551 uint32_t m_next_binding { 0 };
552 std::vector<FunctionDef> m_functions;
553 std::optional<KernelSource> m_kernel;
554 };
555};
556
557/**
558 * @brief Stringifies a kernel lambda for use with ShaderSpec::Assemble::kernel().
559 *
560 * The lambda is never evaluated. Its text is captured at the call site and
561 * parsed into parameter names and a body by KernelSource::parse(). Parameter
562 * names must match the binding and push constant field names declared on the
563 * enclosing ShaderSpec in declaration order.
564 *
565 * @code
566 * .kernel(MF_KERNEL([](float* sig, float gain, uint32_t i) {
567 * sig[i] *= gain;
568 * }))
569 * @endcode
570 */
571#define MF_KERNEL(lambda) \
572 MayaFlux::Portal::Graphics::KernelSource::parse(#lambda)
573
574} // namespace MayaFlux::Portal::Graphics
size_t b
std::string name
Definition VKDevice.cpp:143
uint32_t depth
Assemble & op(KernelOp o)
Set the named operation the emitter will lower to SPIR-V.
Assemble & function(std::string return_type, std::string name, std::string params, std::string body)
Declare a function emitted before main() in the generated GLSL.
ShaderSpec build()
Finalise and return the ShaderSpec.
Assemble & pc(std::string name, Kakshya::GpuDataFormat format)
Declare a push constant field with explicit format.
Assemble & ssbo(std::string name, BindingDirection direction, Kakshya::GpuDataFormat format, Kakshya::DataModality modality=Kakshya::DataModality::SCALAR_F32)
Declare an SSBO binding.
Assemble & kernel(KernelSource ks)
Supply a user kernel via MF_KERNEL.
Assemble & pc(std::string name)
Declare a float push constant field.
std::vector< PushConstantField > m_pc_fields
Assemble & workgroup(uint32_t x, uint32_t y=1, uint32_t z=1)
Override workgroup size.
Assemble & tmpl(KernelTemplate t)
Set the kernel template.
Assemble & texture(std::string name)
Declare a sampled image binding (sampler2D).
Assemble & storage_image(std::string name, BindingDirection direction=BindingDirection::Output)
Declare a storage image binding (image2D).
Fluent assembler producing a ShaderSpec.
size_t gpu_data_format_bytes(GpuDataFormat fmt) noexcept
Byte size of one element of a GpuDataFormat.
Definition NDData.cpp:9
DataModality
Data modality types for cross-modal analysis.
Definition NDData.hpp:164
@ SCALAR_F32
Single-channel float data.
@ IMAGE_2D
2D image (grayscale or single channel)
GpuDataFormat
GPU data formats with explicit precision levels.
Definition NDData.hpp:25
KernelTemplate
Structural shape of the generated compute kernel.
@ Scan
Inclusive prefix scan over one InOut SSBO, double-buffered Hillis-Steele in shared memory.
@ Convolve2D
2D separable or non-separable convolution; kernel weights in SSBO, radius in PC
@ Reduction
f(x[0..n]) -> scalar; shared-memory tree reduction
@ Elementwise
f(x[i]) -> y[i]; one thread per element
@ Stencil
f(x[i-k..i+k]) -> y[i]; neighbourhood reads, radius in PC
@ GeometryEmit
Writes into vertex SSBO with atomic counter.
@ BitonicSort
Bitonic sort network; one thread per element.
BindingDirection
Data flow direction for a shader binding slot.
KernelOp
Named operation the SPIR-V emitter knows how to lower to opcodes.
@ CompareGE
out[ch] = pixel[ch] >= pc[0] ? 1.0 : 0.0
@ ChannelDot
out = dot(pixel.rgba, pc[0..3]) broadcast to all channels
@ WeightedBlend
Stencil-only: blends a cell toward a scaled neighbor sum by a rate factor: out = center + pc[N] * (su...
@ ScanSum
Inclusive prefix sum: shared[i] = sum(x[0..i])
@ IndexScale
out[i] = float(i) * a[i].
@ CompareGEPreserve
out[ch] = pixel[ch] >= pc[0] ? pc[1] : pixel[ch]
@ MaxIndex
Reduction variant: finds max value AND its index.
@ ChannelReplicate
out = pixel[pc_channel_index].xxxx (single channel to all)
Declaration of one SSBO or image binding in a generated shader.
A standalone function emitted before main() in the generated GLSL.
static KernelSource parse(std::string_view stringified)
Parse parameter names and body from a stringified lambda.
std::vector< std::string > param_names
Parsed representation of a user-supplied kernel lambda.
One field in the generated push constant block.
std::vector< PushConstantField > pc_fields
std::vector< BindingSlot > bindings
std::array< uint32_t, 3 > workgroup_size
std::optional< KernelSource > kernel
When set, KernelOp is ignored.
std::vector< FunctionDef > functions
Emitted before main(). GLSL path only.
Complete declarative description of a generated compute shader.