#![cfg(all(feature = "par", feature = "gpu-wgpu"))]
use alloc::string::String;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum OperatioReductionis {
Additio,
Multiplicatio,
Minimum,
Maximum,
}
impl OperatioReductionis {
#[inline]
pub fn parse(op: &str) -> Option<Self> {
match op {
"+" => Some(Self::Additio),
"*" => Some(Self::Multiplicatio),
"min" => Some(Self::Minimum),
"max" => Some(Self::Maximum),
_ => None,
}
}
#[inline]
pub fn identity(self, type_name: &str) -> &'static str {
match (self, type_name) {
(Self::Additio, "f32") => "0.0",
(Self::Additio, _) => "0",
(Self::Multiplicatio, "f32") => "1.0",
(Self::Multiplicatio, _) => "1",
(Self::Minimum, "f32") => "3.4028235e38",
(Self::Minimum, "i32") => "2147483647",
(Self::Minimum, _) => "4294967295u",
(Self::Maximum, "f32") => "-3.4028235e38",
(Self::Maximum, "i32") => "-2147483647 - 1",
(Self::Maximum, _) => "0u",
}
}
#[inline]
pub fn combine(self, a: &str, b: &str) -> alloc::string::String {
use alloc::format;
match self {
Self::Additio => format!("{a} + {b}"),
Self::Multiplicatio => format!("{a} * {b}"),
Self::Minimum => format!("min({a}, {b})"),
Self::Maximum => format!("max({a}, {b})"),
}
}
}
#[inline]
pub fn generate_map_shader(
entry_point: &str,
operation: &str,
workgroup_size: u32,
type_name: &str,
) -> String {
let mut out =
String::with_capacity(320 + entry_point.len() + operation.len() + type_name.len());
use core::fmt::Write as _;
let _ = write!(
out,
r"// Input buffer
@group(0) @binding(0)
var<storage, read> input: array<{type_name}>;
// Output buffer
@group(0) @binding(1)
var<storage, read_write> output: array<{type_name}>;
struct Params {{
element_count: u32,
}}
@group(0) @binding(2)
var<uniform> params: Params;
@compute @workgroup_size({workgroup_size})
fn {entry_point}(@builtin(global_invocation_id) global_id: vec3<u32>) {{
let index = global_id.x;
// Bounds check against the LOGICAL element count. The storage buffer's
// raw byte capacity may exceed the live data (first-fit pool reuse) —
// never bound on that physical capacity.
if (index >= params.element_count) {{
return;
}}
// Apply operation
let x = input[index];
output[index] = {operation};
}}
"
);
out
}
#[inline]
pub fn generate_reduce_shader(
entry_point: &str,
op: OperatioReductionis,
workgroup_size: u32,
type_name: &str,
) -> String {
let identity = op.identity(type_name);
let combine = op.combine(
"shared_data[local_index]",
"shared_data[local_index + offset]",
);
let mut out = String::with_capacity(580 + entry_point.len() + combine.len() + type_name.len());
use core::fmt::Write as _;
let _ = write!(
out,
r"// Input buffer
@group(0) @binding(0)
var<storage, read> input: array<{type_name}>;
// Output buffer (reduced value)
@group(0) @binding(1)
var<storage, read_write> output: array<{type_name}>;
struct Params {{
element_count: u32,
}}
@group(0) @binding(2)
var<uniform> params: Params;
// Shared memory for workgroup reduction
var<workgroup> shared_data: array<{type_name}, {workgroup_size}>;
@compute @workgroup_size({workgroup_size})
fn {entry_point}(@builtin(global_invocation_id) global_id: vec3<u32>, @builtin(local_invocation_index) local_index: u32) {{
let index = global_id.x;
// Load data into shared memory, bounded by the LOGICAL element count.
// The storage buffer's raw byte capacity may exceed the live data
// (first-fit pool reuse) — never bound on that physical capacity.
if (index < params.element_count) {{
shared_data[local_index] = input[index];
}} else {{
shared_data[local_index] = {identity};
}}
workgroupBarrier();
// Tree reduction
var offset = {workgroup_size}u / 2u;
while (offset > 0u) {{
if (local_index < offset) {{
shared_data[local_index] = {combine};
}}
workgroupBarrier();
offset = offset / 2u;
}}
// Write result
if (local_index == 0u) {{
// Write to output at the workgroup index
let workgroup_id = global_id.x / {workgroup_size}u;
output[workgroup_id] = shared_data[0];
}}
}}
"
);
out
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct KernelKey {
pub shader_source: String,
pub entry_point: String,
}
impl KernelKey {
#[inline]
pub fn new(shader_source: String, entry_point: String) -> Self {
Self {
shader_source,
entry_point,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reduce_shader_bounds_on_params_not_array_length() {
let s = generate_reduce_shader("reduce", OperatioReductionis::Additio, 64, "f32");
assert!(
s.contains("var<uniform> params: Params"),
"missing params uniform:\n{s}"
);
assert!(
s.contains("index < params.element_count"),
"must bound on logical count:\n{s}"
);
assert!(
!s.contains("arrayLength"),
"physical buffer length must not gate loads:\n{s}"
);
}
#[test]
fn reduce_identity_matches_operator() {
let mul = generate_reduce_shader("reduce", OperatioReductionis::Multiplicatio, 64, "f32");
assert!(
mul.contains("= 1.0;"),
"f32 product identity must be 1.0:\n{mul}"
);
let min_u = generate_reduce_shader("reduce", OperatioReductionis::Minimum, 64, "u32");
assert!(
min_u.contains("= 4294967295u;"),
"u32 min identity must be u32 max:\n{min_u}"
);
let max_i = generate_reduce_shader("reduce", OperatioReductionis::Maximum, 64, "i32");
assert!(
max_i.contains("= -2147483647 - 1;"),
"i32 max identity must be i32::MIN:\n{max_i}"
);
}
#[test]
fn min_max_emit_call_syntax_not_infix() {
let s = generate_reduce_shader("reduce", OperatioReductionis::Minimum, 64, "f32");
assert!(
s.contains("min(shared_data[local_index], shared_data[local_index + offset])"),
"{s}"
);
assert!(
!s.contains("] min shared_data"),
"infix `a min b` is invalid WGSL:\n{s}"
);
}
#[test]
fn parse_rejects_unknown_ops() {
assert!(OperatioReductionis::parse("+").is_some());
assert!(OperatioReductionis::parse("min").is_some());
assert!(OperatioReductionis::parse("^").is_none());
assert!(OperatioReductionis::parse("").is_none());
}
#[test]
fn map_shader_bounds_on_params() {
let s = generate_map_shader("map", "x * 2.0", 64, "f32");
assert!(s.contains("var<uniform> params: Params"), "{s}");
assert!(s.contains("index >= params.element_count"), "{s}");
assert!(!s.contains("arrayLength"), "{s}");
}
}