use std::collections::BTreeMap;
use engawa::{
BindingKind, Material, Node, NodeId, RenderGraph, ResourceId, ResourceKind,
ShaderSource, UniformBinding,
};
use thiserror::Error;
use crate::parse::Span;
use crate::sexpr::{Sexpr, SexprKind};
#[derive(Debug, Error, Clone, PartialEq)]
pub enum LowerError {
#[error("expected form, got non-list at line {line}, column {col}")]
NotAForm { line: usize, col: usize },
#[error("expected form name (symbol) at line {line}, column {col}")]
MissingFormName { line: usize, col: usize },
#[error("unknown form {form:?} at line {line}, column {col}")]
UnknownForm {
form: String,
line: usize,
col: usize,
},
#[error("form {form:?} expects {expected} arguments, got {got} at line {line}")]
Arity {
form: String,
expected: usize,
got: usize,
line: usize,
},
#[error("form {form:?} expects argument {position} to be a {kind} at line {line}")]
BadArgKind {
form: String,
position: usize,
kind: &'static str,
line: usize,
},
#[error("could not parse {value:?} as {kind} at line {line}")]
BadNumber {
value: String,
kind: &'static str,
line: usize,
},
#[error("unknown resource kind {found:?} at line {line}")]
UnknownResourceKind { found: String, line: usize },
#[error("unknown binding kind {found:?} at line {line}")]
UnknownBindingKind { found: String, line: usize },
#[error("unknown node kind {found:?} at line {line}")]
UnknownNodeKind { found: String, line: usize },
#[error("material {name:?} referenced but not defined")]
UndefinedMaterial { name: String },
}
pub fn lower_to_graph(forms: &[Sexpr]) -> Result<RenderGraph, LowerError> {
let mut materials: BTreeMap<String, Material> = BTreeMap::new();
let mut resources: BTreeMap<ResourceId, ResourceKind> = BTreeMap::new();
let mut graph_form: Option<&Sexpr> = None;
for form in forms {
let items = require_list(form)?;
let head_name = require_symbol(&items[0], "form-head", 0, form.span)?;
match head_name {
"defresource" => {
let (id, kind) = lower_resource(items, form.span)?;
resources.insert(id, kind);
}
"defmaterial" => {
let mat = lower_material(items, form.span)?;
materials.insert(mat.name.clone(), mat);
}
"defgraph" => {
if graph_form.is_some() {
return Err(LowerError::UnknownForm {
form: "defgraph (multiple)".to_string(),
line: form.span.line,
col: form.span.column,
});
}
graph_form = Some(form);
}
other => {
return Err(LowerError::UnknownForm {
form: other.to_string(),
line: form.span.line,
col: form.span.column,
});
}
}
}
let Some(graph_form) = graph_form else {
let mut g = RenderGraph::default();
for (id, kind) in resources {
g = g.with_resource(id, kind);
}
return Ok(g);
};
let items = require_list(graph_form)?;
let mut g = RenderGraph::default();
for (id, kind) in &resources {
g = g.with_resource(id.clone(), kind.clone());
}
let body = &items[1..];
let _graph_name = require_symbol(&items[1], "defgraph", 1, graph_form.span)?;
for clause in &body[1..] {
let clause_items = require_list(clause)?;
let head = require_symbol(&clause_items[0], "graph-clause", 0, clause.span)?;
match head {
"input" => {
let id = lower_resource_ref(clause_items, "input", clause.span)?;
g = g.with_input(id);
}
"output" => {
let id = lower_resource_ref(clause_items, "output", clause.span)?;
g = g.with_output(id);
}
"node" => {
let node = lower_node(clause_items, clause.span, &materials)?;
g = g.with_node(node);
}
other => {
return Err(LowerError::UnknownForm {
form: other.to_string(),
line: clause.span.line,
col: clause.span.column,
});
}
}
}
Ok(g)
}
fn lower_resource(
items: &[Sexpr],
span: Span,
) -> Result<(ResourceId, ResourceKind), LowerError> {
if items.len() < 3 {
return Err(LowerError::Arity {
form: "defresource".into(),
expected: 3,
got: items.len(),
line: span.line,
});
}
let id_str = require_symbol(&items[1], "defresource", 1, span)?;
let id = ResourceId::new(id_str);
let kind_form = &items[2];
let kind = match &kind_form.kind {
SexprKind::Symbol(s) if s == "external" => ResourceKind::External,
SexprKind::Symbol(s) if s == "sampler" => ResourceKind::Sampler,
SexprKind::List(inner) => {
let head = require_symbol(&inner[0], "resource-kind", 0, kind_form.span)?;
match head {
"texture" => {
let w = parse_u32(&inner[1], "texture-width", kind_form.span)?;
let h = parse_u32(&inner[2], "texture-height", kind_form.span)?;
ResourceKind::Texture {
width: Some(w),
height: Some(h),
}
}
"uniform" => {
let n = parse_u32(&inner[1], "uniform-size", kind_form.span)?;
ResourceKind::Uniform { size_bytes: n }
}
"storage" => {
let n = parse_u32(&inner[1], "storage-size", kind_form.span)?;
ResourceKind::Storage { size_bytes: n }
}
other => {
return Err(LowerError::UnknownResourceKind {
found: other.to_string(),
line: kind_form.span.line,
});
}
}
}
_ => {
return Err(LowerError::UnknownResourceKind {
found: format!("{:?}", kind_form.kind),
line: kind_form.span.line,
});
}
};
Ok((id, kind))
}
fn lower_material(items: &[Sexpr], span: Span) -> Result<Material, LowerError> {
if items.len() < 3 {
return Err(LowerError::Arity {
form: "defmaterial".into(),
expected: 3,
got: items.len(),
line: span.line,
});
}
let name = require_symbol(&items[1], "defmaterial", 1, span)?.to_string();
let mut shader: Option<ShaderSource> = None;
let mut bindings: Vec<UniformBinding> = Vec::new();
for clause in &items[2..] {
let inner = require_list(clause)?;
let head = require_symbol(&inner[0], "material-clause", 0, clause.span)?;
match head {
"shader" => {
let body = require_list(&inner[1])?;
let kind_name = require_symbol(&body[0], "shader-kind", 0, clause.span)?;
let payload = require_string(&body[1], "shader-payload", 1, clause.span)?;
shader = Some(match kind_name {
"inline" => ShaderSource::inline(payload),
"path" => ShaderSource::path(payload),
other => {
return Err(LowerError::UnknownForm {
form: format!("shader-kind:{other}"),
line: clause.span.line,
col: clause.span.column,
});
}
});
}
"bindings" => {
for b in &inner[1..] {
bindings.push(lower_binding(b)?);
}
}
other => {
return Err(LowerError::UnknownForm {
form: other.to_string(),
line: clause.span.line,
col: clause.span.column,
});
}
}
}
Ok(Material {
name,
shader: shader.unwrap_or_else(|| ShaderSource::inline("")),
bindings,
})
}
fn lower_binding(form: &Sexpr) -> Result<UniformBinding, LowerError> {
let items = require_list(form)?;
let head = require_symbol(&items[0], "binding", 0, form.span)?;
if head != "binding" {
return Err(LowerError::UnknownForm {
form: head.to_string(),
line: form.span.line,
col: form.span.column,
});
}
let n = parse_u32(&items[1], "binding-index", form.span)?;
let kind_str = require_symbol(&items[2], "binding-kind", 2, form.span)?;
let kind = match kind_str {
"uniform" => BindingKind::Uniform,
"storage_read" | "storage-read" => BindingKind::StorageRead,
"storage_read_write" | "storage-read-write" => BindingKind::StorageReadWrite,
"texture" => BindingKind::Texture,
"sampler" => BindingKind::Sampler,
other => {
return Err(LowerError::UnknownBindingKind {
found: other.to_string(),
line: form.span.line,
});
}
};
let resource_str = require_string(&items[3], "binding-resource", 3, form.span)?;
Ok(UniformBinding {
binding: n,
kind,
resource: ResourceId::new(resource_str),
})
}
fn lower_resource_ref(
items: &[Sexpr],
form: &'static str,
span: Span,
) -> Result<ResourceId, LowerError> {
if items.len() != 2 {
return Err(LowerError::Arity {
form: form.into(),
expected: 2,
got: items.len(),
line: span.line,
});
}
let s = require_symbol(&items[1], form, 1, span)?;
Ok(ResourceId::new(s))
}
fn lower_node(
items: &[Sexpr],
span: Span,
materials: &BTreeMap<String, Material>,
) -> Result<Node, LowerError> {
if items.len() < 3 {
return Err(LowerError::Arity {
form: "node".into(),
expected: 3,
got: items.len(),
line: span.line,
});
}
let node_id_str = require_symbol(&items[1], "node", 1, span)?;
let node_id = NodeId::new(node_id_str);
let mut kind: Option<String> = None;
let mut material: Option<Material> = None;
let mut inputs: Vec<ResourceId> = Vec::new();
let mut outputs: Vec<ResourceId> = Vec::new();
for clause in &items[2..] {
let inner = require_list(clause)?;
let head = require_symbol(&inner[0], "node-clause", 0, clause.span)?;
match head {
"kind" => {
let s = require_symbol(&inner[1], "kind", 1, clause.span)?;
kind = Some(s.to_string());
}
"material" => {
let s = require_symbol(&inner[1], "material", 1, clause.span)?;
let m = materials.get(s).ok_or_else(|| LowerError::UndefinedMaterial {
name: s.to_string(),
})?;
material = Some(m.clone());
}
"input" => {
let s = require_symbol(&inner[1], "input", 1, clause.span)?;
inputs.push(ResourceId::new(s));
}
"output" => {
let s = require_symbol(&inner[1], "output", 1, clause.span)?;
outputs.push(ResourceId::new(s));
}
other => {
return Err(LowerError::UnknownForm {
form: other.to_string(),
line: clause.span.line,
col: clause.span.column,
});
}
}
}
let pass = engawa::PassKind::Render;
let _ = kind; Ok(Node {
id: node_id,
pass,
inputs,
outputs,
material,
})
}
fn require_list(s: &Sexpr) -> Result<&[Sexpr], LowerError> {
s.as_list().ok_or(LowerError::NotAForm {
line: s.span.line,
col: s.span.column,
})
}
fn require_symbol<'a>(
s: &'a Sexpr,
form: &'static str,
position: usize,
span: Span,
) -> Result<&'a str, LowerError> {
s.as_symbol().ok_or(LowerError::BadArgKind {
form: form.into(),
position,
kind: "symbol",
line: span.line,
})
}
fn require_string<'a>(
s: &'a Sexpr,
form: &'static str,
position: usize,
span: Span,
) -> Result<&'a str, LowerError> {
s.as_string().ok_or(LowerError::BadArgKind {
form: form.into(),
position,
kind: "string",
line: span.line,
})
}
fn parse_u32(s: &Sexpr, kind: &'static str, span: Span) -> Result<u32, LowerError> {
let txt = s.as_number().ok_or(LowerError::BadArgKind {
form: kind.into(),
position: 0,
kind: "number",
line: span.line,
})?;
txt.parse::<u32>().map_err(|_| LowerError::BadNumber {
value: txt.to_string(),
kind: "u32",
line: span.line,
})
}