use alpha2_gpu::CapabilityTier;
use alpha2_render::{
EffectDef, EffectInputs, EffectKind, EffectResourceKind, ParamSpec, ParamType, ParamUnit,
ParamValue, ResourceSpec,
};
use serde::Deserialize;
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct EffectRequest {
id: String,
version: u32,
kind: Kind,
inputs: u8,
#[serde(default)]
params: Vec<ParameterRequest>,
#[serde(default)]
resources: Vec<ResourceRequest>,
wgsl_body: String,
#[serde(default = "default_tier")]
tier_min: Tier,
#[serde(default)]
license: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ResourceRequest {
name: String,
kind: ResourceKind,
}
#[derive(Copy, Clone, Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
enum ResourceKind {
Lut3d,
}
#[derive(Copy, Clone, Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
enum Kind {
Effect,
Transition,
}
#[derive(Copy, Clone, Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
enum Tier {
A,
B,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ParameterRequest {
name: String,
#[serde(rename = "type")]
param_type: ParameterType,
default: serde_json::Value,
#[serde(default)]
min: Option<f32>,
#[serde(default)]
max: Option<f32>,
#[serde(default)]
step: Option<f32>,
unit: ParameterUnit,
#[serde(default)]
enum_values: Vec<String>,
}
#[derive(Copy, Clone, Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
enum ParameterType {
Float,
Vec2,
Vec4,
Color,
Enum,
Bool,
}
#[derive(Copy, Clone, Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
enum ParameterUnit {
Normalized,
Degrees,
Color,
Enum,
Bool,
}
impl EffectRequest {
pub(crate) fn parse(json: &str) -> Result<EffectDef, String> {
serde_json::from_str::<Self>(json)
.map_err(|error| error.to_string())?
.into_definition()
}
fn into_definition(self) -> Result<EffectDef, String> {
let kind = match self.kind {
Kind::Effect => EffectKind::Effect,
Kind::Transition => EffectKind::Transition,
};
let inputs = match self.inputs {
1 => EffectInputs::One,
2 => EffectInputs::Two,
count => return Err(format!("inputs must be 1 or 2, received {count}")),
};
let params = self
.params
.into_iter()
.map(ParameterRequest::into_spec)
.collect::<Result<Vec<_>, _>>()?;
Ok(EffectDef {
id: self.id,
version: self.version,
kind,
inputs,
params,
resources: self
.resources
.into_iter()
.map(|resource| ResourceSpec {
name: resource.name,
kind: match resource.kind {
ResourceKind::Lut3d => EffectResourceKind::Lut3d,
},
})
.collect(),
wgsl_body: self.wgsl_body,
tier_min: match self.tier_min {
Tier::A => CapabilityTier::TierA,
Tier::B => CapabilityTier::TierB,
},
license: self.license,
})
}
}
impl ParameterRequest {
fn into_spec(self) -> Result<ParamSpec, String> {
let (param_type, default) = match self.param_type {
ParameterType::Float => (
ParamType::Float,
ParamValue::Float(value(&self.default, &self.name)?),
),
ParameterType::Vec2 => (
ParamType::Vec2,
ParamValue::Vec2(array(&self.default, &self.name)?),
),
ParameterType::Vec4 => (
ParamType::Vec4,
ParamValue::Vec4(array(&self.default, &self.name)?),
),
ParameterType::Color => (
ParamType::Color,
ParamValue::Color(array(&self.default, &self.name)?),
),
ParameterType::Enum => (
ParamType::Enum,
ParamValue::Enum(
self.default
.as_u64()
.and_then(|value| u32::try_from(value).ok())
.ok_or_else(|| default_error(&self.name))?,
),
),
ParameterType::Bool => (
ParamType::Bool,
ParamValue::Bool(
self.default
.as_bool()
.ok_or_else(|| default_error(&self.name))?,
),
),
};
Ok(ParamSpec {
name: self.name,
param_type,
default,
min: self.min,
max: self.max,
step: self.step,
unit: match self.unit {
ParameterUnit::Normalized => ParamUnit::Normalized,
ParameterUnit::Degrees => ParamUnit::Degrees,
ParameterUnit::Color => ParamUnit::Color,
ParameterUnit::Enum => ParamUnit::Enum,
ParameterUnit::Bool => ParamUnit::Bool,
},
enum_values: self.enum_values,
})
}
}
fn default_tier() -> Tier {
Tier::B
}
fn value(json: &serde_json::Value, name: &str) -> Result<f32, String> {
serde_json::from_value::<f32>(json.clone()).map_err(|_| default_error(name))
}
fn array<const N: usize>(json: &serde_json::Value, name: &str) -> Result<[f32; N], String> {
let values = json.as_array().ok_or_else(|| default_error(name))?;
if values.len() != N {
return Err(default_error(name));
}
let mut output = [0.0; N];
for (output, input) in output.iter_mut().zip(values) {
*output = value(input, name)?;
}
Ok(output)
}
fn default_error(name: &str) -> String {
format!("parameter {name} has a default that does not match its type")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_the_typescript_wire_schema() {
let definition = EffectRequest::parse(
r#"{
"id":"example.tint","version":1,"kind":"effect","inputs":1,
"params":[{
"name":"tint","type":"color","default":[1,0.5,0.25,1],
"unit":"color"
}],
"wgslBody":"fn effect(uv: vec2f, src: vec4f) -> vec4f { return src; }"
}"#,
)
.expect("valid wire definition");
assert_eq!(definition.id, "example.tint");
assert_eq!(definition.inputs, EffectInputs::One);
assert_eq!(definition.params.len(), 1);
assert_eq!(definition.tier_min, CapabilityTier::TierB);
}
#[test]
fn rejects_a_default_with_the_wrong_shape() {
let error = EffectRequest::parse(
r#"{
"id":"bad","version":1,"kind":"effect","inputs":1,
"params":[{"name":"offset","type":"vec2","default":[1],"unit":"normalized"}],
"wgslBody":""
}"#,
)
.expect_err("wrong vector shape is rejected");
assert!(error.contains("offset"));
}
}