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/**
287 * @struct BindingSlot
288 * @brief Declaration of one SSBO or image binding in a generated shader.
289 */
298
299/**
300 * @struct PushConstantField
301 * @brief One field in the generated push constant block.
302 */
307
308/**
309 * @struct ShaderSpec
310 * @brief Complete declarative description of a generated compute shader.
311 *
312 * Pure value type. Consumed by ShaderFoundry::load_shader(const ShaderSpec&)
313 * which emits SPIR-V assembly via VKShaderModule::emit_spirv_asm() and
314 * assembles it via VKShaderModule::create_from_spirv_asm(). No shaderc
315 * or GLSL toolchain is involved.
316 *
317 * The op field selects a named operation the emitter knows how to lower
318 * to SPIR-V opcodes deterministically. PC fields are consumed in
319 * declaration order as operands.
320 *
321 * @code
322 * auto spec = ShaderSpec::Assemble{}
323 * .ssbo("sig", BindingDirection::InOut, Kakshya::GpuDataFormat::FLOAT32)
324 * .pc("gain")
325 * .pc("offset")
326 * .op(KernelOp::ScaleOffset)
327 * .build();
328 * @endcode
329 */
333 std::vector<BindingSlot> bindings;
334 std::vector<PushConstantField> pc_fields;
335 std::array<uint32_t, 3> workgroup_size { 256, 1, 1 };
336 uint32_t push_constant_bytes { 0 };
337 std::optional<KernelSource> kernel; ///< When set, KernelOp is ignored.
338
339 /**
340 * @class Assemble
341 * @brief Fluent assembler producing a ShaderSpec.
342 *
343 * Binding indices are assigned in declaration order starting at 1.
344 */
345 class Assemble {
346 public:
347 Assemble() = default;
348
349 /** @brief Set the kernel template. Defaults to Elementwise. */
351 {
352 m_tmpl = t;
353 return *this;
354 }
355
356 /**
357 * @brief Set the named operation the emitter will lower to SPIR-V.
358 * @param o KernelOp value.
359 */
361 {
362 m_op = o;
363 return *this;
364 }
365
366 Assemble& start_set(uint32_t s)
367 {
368 m_set = s;
369 return *this;
370 }
371
373 {
374 m_next_binding = n;
375 return *this;
376 }
377
378 /**
379 * @brief Declare an SSBO binding.
380 */
381 Assemble& ssbo(std::string name, BindingDirection direction, Kakshya::GpuDataFormat format,
383 {
384 m_bindings.push_back({
385 .name = std::move(name),
386 .direction = direction,
387 .format = format,
388 .modality = modality,
389 .set = m_set,
390 .binding_index = m_next_binding++,
391 });
392 return *this;
393 }
394
395 /**
396 * @brief Declare a sampled image binding (sampler2D).
397 */
398 Assemble& texture(std::string name)
399 {
400 m_bindings.push_back({
401 .name = std::move(name),
402 .direction = BindingDirection::Input,
405 .binding_index = m_next_binding++,
406 });
407 return *this;
408 }
409
410 /**
411 * @brief Declare a storage image binding (image2D).
412 */
414 std::string name,
416 {
417 m_bindings.push_back({
418 .name = std::move(name),
419 .direction = direction,
422 .binding_index = m_next_binding++,
423 });
424 return *this;
425 }
426
427 /**
428 * @brief Declare a push constant field with explicit format.
429 */
430 Assemble& pc(std::string name, Kakshya::GpuDataFormat format)
431 {
432 m_pc_fields.push_back({ .name = std::move(name), .format = format });
433 return *this;
434 }
435
436 /** @brief Declare a float push constant field. */
437 Assemble& pc(std::string name)
438 {
439 return pc(std::move(name), Kakshya::GpuDataFormat::FLOAT32);
440 }
441
442 /** @brief Override workgroup size. Defaults to {256, 1, 1}. */
443 Assemble& workgroup(uint32_t x, uint32_t y = 1, uint32_t z = 1)
444 {
445 m_workgroup = { x, y, z };
446 return *this;
447 }
448
449 /**
450 * @brief Supply a user kernel via MF_KERNEL. When set, KernelOp is ignored.
451 * @param ks KernelSource produced by MF_KERNEL.
452 */
454 {
455 m_kernel = std::move(ks);
456 return *this;
457 }
458
459 /** @brief Finalise and return the ShaderSpec. */
460 [[nodiscard]] ShaderSpec build()
461 {
462 uint32_t pc_bytes = 0;
463 for (const auto& f : m_pc_fields)
464 pc_bytes += Kakshya::gpu_data_format_bytes(f.format);
465
466 return ShaderSpec {
467 .tmpl = m_tmpl,
468 .op = m_op,
469 .bindings = std::move(m_bindings),
470 .pc_fields = std::move(m_pc_fields),
471 .workgroup_size = m_workgroup,
472 .push_constant_bytes = pc_bytes,
473 .kernel = std::move(m_kernel),
474 };
475 }
476
477 private:
480 std::vector<BindingSlot> m_bindings;
481 std::vector<PushConstantField> m_pc_fields;
482 std::array<uint32_t, 3> m_workgroup { 256, 1, 1 };
483 uint32_t m_set { 0 };
484 uint32_t m_next_binding { 0 };
485 std::optional<KernelSource> m_kernel;
486 };
487};
488
489/**
490 * @brief Stringifies a kernel lambda for use with ShaderSpec::Assemble::kernel().
491 *
492 * The lambda is never evaluated. Its text is captured at the call site and
493 * parsed into parameter names and a body by KernelSource::parse(). Parameter
494 * names must match the binding and push constant field names declared on the
495 * enclosing ShaderSpec in declaration order.
496 *
497 * @code
498 * .kernel(MF_KERNEL([](float* sig, float gain, uint32_t i) {
499 * sig[i] *= gain;
500 * }))
501 * @endcode
502 */
503#define MF_KERNEL(lambda) \
504 MayaFlux::Portal::Graphics::KernelSource::parse(#lambda)
505
506} // namespace MayaFlux::Portal::Graphics
Assemble & op(KernelOp o)
Set the named operation the emitter will lower to SPIR-V.
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
@ 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.
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.
Complete declarative description of a generated compute shader.