Shaders: runtime and shader node codegen (#2985)

* shader-rt: initial

* shader-rt: fix recursion when generating shader node

* shader-rt: replace gpu node's args and ret types with `Raster<GPU>`

* shader-rt: properly cfg out the gpu node

* shader-rt: fix `impl Context` in the wrong places

* shader-rt: disable gpu blend node, needs two images

* shader-rt: connect shader runtime

* shader-rt: pass WgpuExecutor by reference

* shader-rt: correct bindings with derpy arg buffer

* shader-rt: manual pipeline layout, fixing errors when bindings got DCE'd

* shader-rt: correct RT format, working invert gpu node

* shader-rt: cleanup codegen with common sym struct

* shader-rt: correct arg buffer handling

* shader-nodes feature: put shader nodes behind feature gate

* shader-nodes feature: rename any `gpu_node` to `shader-node`

* shaders-rt: fix wgpu label name

* shaders-rt: explain fullscreen_vertex coordinates with a drawing
This commit is contained in:
Firestar99
2025-09-05 08:33:53 +02:00
committed by GitHub
parent 7dc86b36ca
commit 5d441c2e18
15 changed files with 619 additions and 63 deletions

View File

@@ -3,24 +3,38 @@ use crate::shader_nodes::per_pixel_adjust::PerPixelAdjust;
use proc_macro2::{Ident, TokenStream};
use quote::quote;
use strum::VariantNames;
use syn::Error;
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::{Error, Token};
pub mod per_pixel_adjust;
pub const STD_FEATURE_GATE: &str = "std";
pub const SHADER_NODES_FEATURE_GATE: &str = "shader-nodes";
pub fn modify_cfg(attributes: &NodeFnAttributes) -> TokenStream {
match (&attributes.cfg, &attributes.shader_node) {
(Some(cfg), Some(_)) => quote!(#[cfg(all(#cfg, feature = #STD_FEATURE_GATE))]),
(Some(cfg), None) => quote!(#[cfg(#cfg)]),
(None, Some(_)) => quote!(#[cfg(feature = #STD_FEATURE_GATE)]),
(None, None) => quote!(),
}
let feature_gate = match &attributes.shader_node {
// shader node cfg is done on the mod
Some(ShaderNodeType::ShaderNode) => quote!(),
Some(_) => quote!(feature = #STD_FEATURE_GATE),
None => quote!(),
};
let cfgs: Punctuated<_, Token![,]> = match &attributes.cfg {
None => [&feature_gate].into_iter().collect(),
Some(cfg) => [cfg, &feature_gate].into_iter().collect(),
};
quote!(#[cfg(all(#cfgs))])
}
#[derive(Debug, VariantNames)]
#[derive(Debug, Clone, VariantNames)]
pub(crate) enum ShaderNodeType {
/// Marker for this node being in a gpu node crate, but not having a gpu implementation. This is distinct from not
/// declaring `shader_node` at all, as it will wrap the CPU node with a `#[cfg(feature = "std")]` feature gate.
None,
/// Marker for this node being a generated gpu node implementation, that should not emit anything to prevent
/// recursively generating more gpu nodes. But it still counts as a gpu node and will get the
/// `#[cfg(feature = "std")]` feature gate around it's impl.
ShaderNode,
PerPixelAdjust(PerPixelAdjust),
}
@@ -28,24 +42,37 @@ impl Parse for ShaderNodeType {
fn parse(input: ParseStream) -> syn::Result<Self> {
let ident: Ident = input.parse()?;
Ok(match ident.to_string().as_str() {
"None" => ShaderNodeType::None,
"PerPixelAdjust" => ShaderNodeType::PerPixelAdjust(PerPixelAdjust::parse(input)?),
_ => return Err(Error::new_spanned(&ident, format!("attr 'shader_node' must be one of {:?}", Self::VARIANTS))),
})
}
}
pub trait CodegenShaderEntryPoint {
fn codegen_shader_entry_point(&self, parsed: &ParsedNodeFn) -> syn::Result<TokenStream>;
pub trait ShaderCodegen {
fn codegen(&self, parsed: &ParsedNodeFn) -> syn::Result<ShaderTokens>;
}
impl CodegenShaderEntryPoint for ShaderNodeType {
fn codegen_shader_entry_point(&self, parsed: &ParsedNodeFn) -> syn::Result<TokenStream> {
if parsed.is_async {
return Err(Error::new_spanned(&parsed.fn_name, "Shader nodes must not be async"));
impl ShaderCodegen for ShaderNodeType {
fn codegen(&self, parsed: &ParsedNodeFn) -> syn::Result<ShaderTokens> {
match self {
ShaderNodeType::None | ShaderNodeType::ShaderNode => (),
_ => {
if parsed.is_async {
return Err(Error::new_spanned(&parsed.fn_name, "Shader nodes must not be async"));
}
}
}
match self {
ShaderNodeType::PerPixelAdjust(x) => x.codegen_shader_entry_point(parsed),
ShaderNodeType::None | ShaderNodeType::ShaderNode => Ok(ShaderTokens::default()),
ShaderNodeType::PerPixelAdjust(x) => x.codegen(parsed),
}
}
}
#[derive(Clone, Default)]
pub struct ShaderTokens {
pub shader_entry_point: TokenStream,
pub gpu_node: TokenStream,
}

View File

@@ -1,11 +1,15 @@
use crate::parsing::{ParsedFieldType, ParsedNodeFn, RegularParsedField};
use crate::shader_nodes::CodegenShaderEntryPoint;
use crate::parsing::{Input, NodeFnAttributes, ParsedField, ParsedFieldType, ParsedNodeFn, RegularParsedField};
use crate::shader_nodes::{SHADER_NODES_FEATURE_GATE, ShaderCodegen, ShaderNodeType, ShaderTokens};
use convert_case::{Case, Casing};
use proc_macro_crate::FoundCrate;
use proc_macro2::{Ident, TokenStream};
use quote::{ToTokens, format_ident, quote};
use std::borrow::Cow;
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::{PatIdent, Type, parse_quote};
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct PerPixelAdjust {}
impl Parse for PerPixelAdjust {
@@ -14,53 +18,113 @@ impl Parse for PerPixelAdjust {
}
}
impl CodegenShaderEntryPoint for PerPixelAdjust {
fn codegen_shader_entry_point(&self, parsed: &ParsedNodeFn) -> syn::Result<TokenStream> {
impl ShaderCodegen for PerPixelAdjust {
fn codegen(&self, parsed: &ParsedNodeFn) -> syn::Result<ShaderTokens> {
let fn_name = &parsed.fn_name;
let gpu_mod = format_ident!("{}_gpu", parsed.fn_name);
let spirv_image_ty = quote!(Image2d);
// bindings for images start at 1
let mut binding_cnt = 0;
let params = parsed
.fields
.iter()
.map(|f| {
let ident = &f.pat_ident;
match &f.ty {
ParsedFieldType::Node { .. } => Err(syn::Error::new_spanned(ident, "PerPixelAdjust shader nodes cannot accept other nodes as generics")),
ParsedFieldType::Regular(RegularParsedField { gpu_image: false, ty, .. }) => Ok(Param {
ident: Cow::Borrowed(&ident.ident),
ty: Cow::Owned(ty.to_token_stream()),
param_type: ParamType::Uniform,
}),
ParsedFieldType::Regular(RegularParsedField { gpu_image: true, .. }) => {
binding_cnt += 1;
Ok(Param {
ident: Cow::Owned(format_ident!("image_{}", &ident.ident)),
ty: Cow::Borrowed(&spirv_image_ty),
param_type: ParamType::Image { binding: binding_cnt },
})
let mut params;
let has_uniform;
{
// categorize params
params = parsed
.fields
.iter()
.map(|f| {
let ident = &f.pat_ident;
match &f.ty {
ParsedFieldType::Node { .. } => Err(syn::Error::new_spanned(ident, "PerPixelAdjust shader nodes cannot accept other nodes as generics")),
ParsedFieldType::Regular(RegularParsedField { gpu_image: false, ty, .. }) => Ok(Param {
ident: Cow::Borrowed(&ident.ident),
ty: ty.to_token_stream(),
param_type: ParamType::Uniform,
}),
ParsedFieldType::Regular(RegularParsedField { gpu_image: true, .. }) => {
let param = Param {
ident: Cow::Owned(format_ident!("image_{}", &ident.ident)),
ty: quote!(Image2d),
param_type: ParamType::Image { binding: 0 },
};
Ok(param)
}
}
}
})
.collect::<syn::Result<Vec<_>>>()?;
})
.collect::<syn::Result<Vec<_>>>()?;
let uniform_members = params
has_uniform = params.iter().any(|p| matches!(p.param_type, ParamType::Uniform));
// assign image bindings
// if an arg_buffer exists, bindings for images start at 1 to leave 0 for arg buffer
let mut binding_cnt = if has_uniform { 1 } else { 0 };
for p in params.iter_mut() {
match &mut p.param_type {
ParamType::Image { binding } => {
*binding = binding_cnt;
binding_cnt += 1;
}
ParamType::Uniform => {}
}
}
}
let entry_point_mod = format_ident!("{}_gpu_entry_point", fn_name);
let entry_point_name_ident = format_ident!("ENTRY_POINT_NAME");
let entry_point_name = quote!(#entry_point_mod::#entry_point_name_ident);
let uniform_struct_ident = format_ident!("Uniform");
let uniform_struct = quote!(#entry_point_mod::#uniform_struct_ident);
let shader_node_mod = format_ident!("{}_shader_node", fn_name);
let codegen = PerPixelAdjustCodegen {
parsed,
params,
has_uniform,
entry_point_mod,
entry_point_name_ident,
entry_point_name,
uniform_struct_ident,
uniform_struct,
shader_node_mod,
};
Ok(ShaderTokens {
shader_entry_point: codegen.codegen_shader_entry_point()?,
gpu_node: codegen.codegen_gpu_node()?,
})
}
}
pub struct PerPixelAdjustCodegen<'a> {
parsed: &'a ParsedNodeFn,
params: Vec<Param<'a>>,
has_uniform: bool,
entry_point_mod: Ident,
entry_point_name_ident: Ident,
entry_point_name: TokenStream,
uniform_struct_ident: Ident,
uniform_struct: TokenStream,
shader_node_mod: Ident,
}
impl PerPixelAdjustCodegen<'_> {
fn codegen_shader_entry_point(&self) -> syn::Result<TokenStream> {
let fn_name = &self.parsed.fn_name;
let uniform_members = self
.params
.iter()
.filter_map(|Param { ident, ty, param_type }| match param_type {
ParamType::Image { .. } => None,
ParamType::Uniform => Some(quote! {#ident: #ty}),
})
.collect::<Vec<_>>();
let image_params = params
let image_params = self
.params
.iter()
.filter_map(|Param { ident, ty, param_type }| match param_type {
ParamType::Image { binding } => Some(quote! {#[spirv(descriptor_set = 0, binding = #binding)] #ident: &#ty}),
ParamType::Uniform => None,
})
.collect::<Vec<_>>();
let call_args = params
let call_args = self
.params
.iter()
.map(|Param { ident, param_type, .. }| match param_type {
ParamType::Image { .. } => quote!(Color::from_vec4(#ident.fetch_with(texel_coord, lod(0)))),
@@ -69,8 +133,11 @@ impl CodegenShaderEntryPoint for PerPixelAdjust {
.collect::<Vec<_>>();
let context = quote!(());
let entry_point_mod = &self.entry_point_mod;
let entry_point_name = &self.entry_point_name_ident;
let uniform_struct_ident = &self.uniform_struct_ident;
Ok(quote! {
pub mod #gpu_mod {
pub mod #entry_point_mod {
use super::*;
use graphene_core_shaders::color::Color;
use spirv_std::spirv;
@@ -78,8 +145,12 @@ impl CodegenShaderEntryPoint for PerPixelAdjust {
use spirv_std::image::{Image2d, ImageWithMethods};
use spirv_std::image::sample_with::lod;
pub struct Uniform {
#(#uniform_members),*
pub const #entry_point_name: &str = core::concat!(core::module_path!(), "::entry_point");
#[repr(C)]
#[derive(Copy, Clone, bytemuck::NoUninit)]
pub struct #uniform_struct_ident {
#(pub #uniform_members),*
}
#[spirv(fragment)]
@@ -96,11 +167,162 @@ impl CodegenShaderEntryPoint for PerPixelAdjust {
}
})
}
fn codegen_gpu_node(&self) -> syn::Result<TokenStream> {
let gcore = match &self.parsed.crate_name {
FoundCrate::Itself => format_ident!("crate"),
FoundCrate::Name(name) => format_ident!("{name}"),
};
// adapt fields for gpu node
let raster_gpu: Type = parse_quote!(#gcore::table::Table<#gcore::raster_types::Raster<#gcore::raster_types::GPU>>);
let mut fields = self
.parsed
.fields
.iter()
.map(|f| match &f.ty {
ParsedFieldType::Regular(reg @ RegularParsedField { gpu_image: true, .. }) => Ok(ParsedField {
pat_ident: PatIdent {
mutability: None,
by_ref: None,
..f.pat_ident.clone()
},
ty: ParsedFieldType::Regular(RegularParsedField {
ty: raster_gpu.clone(),
implementations: Punctuated::default(),
..reg.clone()
}),
..f.clone()
}),
ParsedFieldType::Regular(RegularParsedField { gpu_image: false, .. }) => Ok(ParsedField {
pat_ident: PatIdent {
mutability: None,
by_ref: None,
..f.pat_ident.clone()
},
..f.clone()
}),
ParsedFieldType::Node { .. } => Err(syn::Error::new_spanned(&f.pat_ident, "PerPixelAdjust shader nodes cannot accept other nodes as generics")),
})
.collect::<syn::Result<Vec<_>>>()?;
// insert wgpu_executor field
let wgpu_executor = format_ident!("__wgpu_executor");
fields.push(ParsedField {
pat_ident: PatIdent {
attrs: vec![],
by_ref: None,
mutability: None,
ident: parse_quote!(#wgpu_executor),
subpat: None,
},
name: None,
description: "".to_string(),
widget_override: Default::default(),
ty: ParsedFieldType::Regular(RegularParsedField {
ty: parse_quote!(&'a WgpuExecutor),
exposed: false,
value_source: Default::default(),
number_soft_min: None,
number_soft_max: None,
number_hard_min: None,
number_hard_max: None,
number_mode_range: None,
implementations: Default::default(),
gpu_image: false,
}),
number_display_decimal_places: None,
number_step: None,
unit: None,
});
// find exactly one gpu_image field, runtime doesn't support more than 1 atm
let gpu_image_field = {
let mut iter = fields.iter().filter(|f| matches!(f.ty, ParsedFieldType::Regular(RegularParsedField { gpu_image: true, .. })));
match (iter.next(), iter.next()) {
(Some(v), None) => Ok(v),
(Some(_), Some(more)) => Err(syn::Error::new_spanned(&more.pat_ident, "No more than one parameter must be annotated with `#[gpu_image]`")),
(None, _) => Err(syn::Error::new_spanned(&self.parsed.fn_name, "At least one parameter must be annotated with `#[gpu_image]`")),
}?
};
let gpu_image = &gpu_image_field.pat_ident.ident;
// uniform buffer struct construction
let has_uniform = self.has_uniform;
let uniform_buffer = if has_uniform {
let uniform_struct = &self.uniform_struct;
let uniform_members = self
.params
.iter()
.filter_map(|p| match p.param_type {
ParamType::Image { .. } => None,
ParamType::Uniform => Some(p.ident.as_ref()),
})
.collect::<Vec<_>>();
quote!(Some(&super::#uniform_struct {
#(#uniform_members),*
}))
} else {
// explicit generics placed here cause it's easier than explicitly writing `run_per_pixel_adjust::<()>`
quote!(Option::<&()>::None)
};
// node function body
let entry_point_name = &self.entry_point_name;
let body = quote! {
{
#wgpu_executor.shader_runtime.run_per_pixel_adjust(&::wgpu_executor::shader_runtime::per_pixel_adjust_runtime::Shaders {
wgsl_shader: crate::WGSL_SHADER,
fragment_shader_name: super::#entry_point_name,
has_uniform: #has_uniform,
}, #gpu_image, #uniform_buffer).await
}
};
// call node codegen
let mut parsed_node_fn = ParsedNodeFn {
vis: self.parsed.vis.clone(),
attributes: NodeFnAttributes {
shader_node: Some(ShaderNodeType::ShaderNode),
..self.parsed.attributes.clone()
},
fn_name: self.shader_node_mod.clone(),
struct_name: format_ident!("{}", self.shader_node_mod.to_string().to_case(Case::Pascal)),
mod_name: self.shader_node_mod.clone(),
fn_generics: vec![parse_quote!('a: 'n)],
where_clause: None,
input: Input {
pat_ident: self.parsed.input.pat_ident.clone(),
ty: parse_quote!(impl #gcore::context::Ctx),
implementations: Default::default(),
},
output_type: raster_gpu,
is_async: true,
fields,
body,
crate_name: self.parsed.crate_name.clone(),
description: "".to_string(),
};
parsed_node_fn.replace_impl_trait_in_input();
let gpu_node_impl = crate::codegen::generate_node_code(&parsed_node_fn)?;
// wrap node in `mod #gpu_node_mod`
let shader_node_mod = &self.shader_node_mod;
Ok(quote! {
#[cfg(feature = #SHADER_NODES_FEATURE_GATE)]
mod #shader_node_mod {
use super::*;
use wgpu_executor::WgpuExecutor;
#gpu_node_impl
}
})
}
}
struct Param<'a> {
ident: Cow<'a, Ident>,
ty: Cow<'a, TokenStream>,
ty: TokenStream,
param_type: ParamType,
}