diff --git a/Cargo.toml b/Cargo.toml index fd1ec4c..4378c76 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,10 @@ [workspace] members = ["dunge", "dunge_macros", "dunge_shader", "helpers"] -default-members = ["dunge"] resolver = "2" [workspace.dependencies] -bytemuck = { version = "1.13" } +bytemuck = "1.13" +glam = "0.25" [workspace.lints.clippy] use_self = "deny" diff --git a/dunge/Cargo.toml b/dunge/Cargo.toml index 78e6f18..0086178 100644 --- a/dunge/Cargo.toml +++ b/dunge/Cargo.toml @@ -14,6 +14,7 @@ rust-version = "1.71" dunge_macros = { version = "0.2.3", path = "../dunge_macros" } dunge_shader = { version = "0.2.3", path = "../dunge_shader" } bytemuck = { workspace = true } +glam = { workspace = true } instant = "0.1" log = "0.4" wgpu = { version = "0.18", default-features = false, features = ["naga"] } @@ -38,7 +39,6 @@ features = ["android-native-activity"] [dev-dependencies] helpers = { path = "../helpers" } -glam = "0.25" [lints] workspace = true diff --git a/dunge/src/lib.rs b/dunge/src/lib.rs index f803d3d..c6decca 100644 --- a/dunge/src/lib.rs +++ b/dunge/src/lib.rs @@ -9,5 +9,10 @@ pub mod state; pub mod texture; #[allow(dead_code)] mod time; +pub mod vertex; -pub use dunge_shader::{sl, vertex}; +pub use { + dunge_macros::Vertex, + dunge_shader::{group::Group, sl, types, vertex::Vertex}, + glam, +}; diff --git a/dunge/src/shader.rs b/dunge/src/shader.rs index ff9dfd5..301e1da 100644 --- a/dunge/src/shader.rs +++ b/dunge/src/shader.rs @@ -1,8 +1,9 @@ use { crate::{ group::TypedGroup, - sl::{GroupMemberType, InputInfo, IntoModule, Module, Stages, VectorType}, + sl::{InputInfo, IntoModule, Module, Stages}, state::State, + types::{GroupMemberType, VectorType}, }, std::marker::PhantomData, wgpu::{PipelineLayout, ShaderModule, VertexAttribute, VertexBufferLayout}, diff --git a/dunge/src/vertex.rs b/dunge/src/vertex.rs new file mode 100644 index 0000000..6677ae9 --- /dev/null +++ b/dunge/src/vertex.rs @@ -0,0 +1,66 @@ +use crate::{ + sl::{ReadInput, Ret}, + types::{self, VectorType}, +}; + +pub use dunge_shader::vertex::*; + +pub trait InputProjection { + const TYPE: VectorType; + type Field; + fn input_projection(id: u32, index: u32) -> Self::Field; +} + +impl InputProjection for [f32; 2] { + const TYPE: VectorType = VectorType::Vec2f; + type Field = Ret>; + + fn input_projection(id: u32, index: u32) -> Self::Field { + ReadInput::new(id, index) + } +} + +impl InputProjection for [f32; 3] { + const TYPE: VectorType = VectorType::Vec3f; + type Field = Ret>; + + fn input_projection(id: u32, index: u32) -> Self::Field { + ReadInput::new(id, index) + } +} + +impl InputProjection for [f32; 4] { + const TYPE: VectorType = VectorType::Vec4f; + type Field = Ret>; + + fn input_projection(id: u32, index: u32) -> Self::Field { + ReadInput::new(id, index) + } +} + +impl InputProjection for glam::Vec2 { + const TYPE: VectorType = VectorType::Vec2f; + type Field = Ret>; + + fn input_projection(id: u32, index: u32) -> Self::Field { + ReadInput::new(id, index) + } +} + +impl InputProjection for glam::Vec3 { + const TYPE: VectorType = VectorType::Vec3f; + type Field = Ret>; + + fn input_projection(id: u32, index: u32) -> Self::Field { + ReadInput::new(id, index) + } +} + +impl InputProjection for glam::Vec4 { + const TYPE: VectorType = VectorType::Vec4f; + type Field = Ret>; + + fn input_projection(id: u32, index: u32) -> Self::Field { + ReadInput::new(id, index) + } +} diff --git a/dunge/tests/triangle.rs b/dunge/tests/triangle.rs index b42d9f0..d37a496 100644 --- a/dunge/tests/triangle.rs +++ b/dunge/tests/triangle.rs @@ -5,7 +5,7 @@ use { draw, sl::{self, Index, Out}, state::{Options, Render}, - texture::{Data, Format}, + texture::{self, Format}, }, glam::Vec4, helpers::Image, @@ -34,6 +34,8 @@ fn render() -> Result<(), Error> { let shader = cx.make_shader(triangle); let layer = cx.make_layer(Format::RgbAlpha, &shader); let view = { + use texture::Data; + let data = Data::empty(SIZE, Format::RgbAlpha)?.with_draw().with_copy(); cx.make_texture(data) }; diff --git a/dunge/tests/triangle_color.png b/dunge/tests/triangle_color.png new file mode 100644 index 0000000..28882f9 Binary files /dev/null and b/dunge/tests/triangle_color.png differ diff --git a/dunge/tests/triangle_color.rs b/dunge/tests/triangle_color.rs new file mode 100644 index 0000000..54de706 --- /dev/null +++ b/dunge/tests/triangle_color.rs @@ -0,0 +1,90 @@ +use { + dunge::{ + color::Rgba, + context::Context, + draw, mesh, + sl::{self, Input, Out}, + state::{Options, Render}, + texture::{self, Format}, + Vertex, + }, + glam::{Vec2, Vec3}, + helpers::Image, + std::{error, fs}, +}; + +type Error = Box; + +#[test] +fn render() -> Result<(), Error> { + const SIZE: (u32, u32) = (300, 300); + + #[repr(C)] + #[derive(Vertex)] + struct Vert { + pos: Vec2, + col: Vec3, + } + + let triangle = |input: Input| Out { + place: sl::compose(input.pos, Vec2::new(0., 1.)), + color: sl::vec4_with(sl::fragment(input.col), 1.), + }; + + let cx = helpers::block_on(Context::new())?; + let shader = cx.make_shader(triangle); + let layer = cx.make_layer(Format::RgbAlpha, &shader); + let view = { + use texture::Data; + + let data = Data::empty(SIZE, Format::RgbAlpha)?.with_draw().with_copy(); + cx.make_texture(data) + }; + + let mesh = { + use mesh::Data; + + const VERTS: [Vert; 3] = [ + Vert { + pos: Vec2::new(0., -0.75), + col: Vec3::new(1., 0., 0.), + }, + Vert { + pos: Vec2::new(0.866, 0.75), + col: Vec3::new(0., 1., 0.), + }, + Vert { + pos: Vec2::new(-0.866, 0.75), + col: Vec3::new(0., 0., 1.), + }, + ]; + + let data = Data::from_verts(&VERTS); + cx.make_mesh(&data) + }; + + let buffer = cx.make_copy_buffer(SIZE); + let options = Options::default().with_clear(Rgba::from_standard([0., 0., 0., 1.])); + let draw = draw::from_fn(|mut frame| { + frame.layer(&layer, options).bind_empty().draw(&mesh); + frame.copy_texture(&buffer, &view); + }); + + let mut render = Render::default(); + cx.draw_to_texture(&mut render, &view, draw); + + let mapped = helpers::block_on({ + let (tx, rx) = helpers::oneshot(); + cx.map_view(buffer.view(), tx, rx) + }); + + let data = mapped.data(); + let image = Image::from_fn(SIZE, |x, y| { + let (width, _) = buffer.size(); + let idx = x + y * width; + data[idx as usize] + }); + + fs::write("tests/triangle_color.png", image.encode())?; + Ok(()) +} diff --git a/dunge_macros/src/lib.rs b/dunge_macros/src/lib.rs index 2f77efe..4674755 100644 --- a/dunge_macros/src/lib.rs +++ b/dunge_macros/src/lib.rs @@ -3,10 +3,8 @@ mod vertex; use proc_macro::TokenStream; /// Derive implementation for the vector type. -#[proc_macro_derive(Vertex, attributes(position, color, texture))] +#[proc_macro_derive(Vertex)] pub fn derive_vertex(input: TokenStream) -> TokenStream { - use syn::DeriveInput; - - let derive = syn::parse_macro_input!(input as DeriveInput); - vertex::impl_vertex(derive).into() + let input = syn::parse_macro_input!(input); + vertex::impl_vertex(input).into() } diff --git a/dunge_macros/src/vertex.rs b/dunge_macros/src/vertex.rs index ab2cfc5..ee3c008 100644 --- a/dunge_macros/src/vertex.rs +++ b/dunge_macros/src/vertex.rs @@ -1,25 +1,25 @@ use { proc_macro2::TokenStream, - syn::{spanned::Spanned, Attribute, Data, DataStruct, DeriveInput, Field, Type}, + syn::{meta::ParseNestedMeta, spanned::Spanned, Attribute, Data, DataStruct, DeriveInput}, }; -pub(crate) fn impl_vertex(derive: DeriveInput) -> TokenStream { - use quote::ToTokens; +pub(crate) fn impl_vertex(input: DeriveInput) -> TokenStream { + use std::{borrow::Cow, iter}; - let Data::Struct(DataStruct { fields, .. }) = derive.data else { - return quote::quote_spanned! { derive.ident.span() => + let Data::Struct(DataStruct { fields, .. }) = input.data else { + return quote::quote_spanned! { input.ident.span() => ::std::compile_error!("the vertex type must be a struct"); }; }; - if !derive.generics.params.is_empty() { - return quote::quote_spanned! { derive.generics.params.span() => + if !input.generics.params.is_empty() { + return quote::quote_spanned! { input.generics.params.span() => ::std::compile_error!("the vertex struct cannot have generic parameters"); }; } - if !derive.attrs.iter().any(is_repr_c) { - return quote::quote_spanned! { derive.ident.span() => + if !input.attrs.iter().any(is_repr_c) { + return quote::quote_spanned! { input.ident.span() => ::std::compile_error!("the vertex struct must have the `#[repr(C)]` attribute"); }; } @@ -30,98 +30,104 @@ pub(crate) fn impl_vertex(derive: DeriveInput) -> TokenStream { }; } - let mut fl = fields.iter().peekable(); - let Some(Fl { kind: Kind::Pos, ty: pos }) = fl.next().and_then(Fl::new) else { - return quote::quote_spanned! { fields.span() => - ::std::compile_error!("the first field must have the `#[position]` attribute"); - }; + let make_ident = |index: u32, ident| match ident { + Some(ident) => Cow::Borrowed(ident), + None => Cow::Owned(quote::format_ident!("f{index}")), }; - let col = match fl.peek().copied().and_then(Fl::new) { - Some(Fl { - kind: Kind::Col, - ty, - }) => { - _ = fl.next(); - Some(ty) - } - _ => None, - }; + let name = input.ident; + let projection_name = quote::format_ident!("{name}Projection"); + let vector_types = fields.iter().map(|field| { + let ty = &field.ty; + quote::quote! { <#ty as ::dunge::vertex::InputProjection>::TYPE } + }); + + let projection_fields = iter::zip(0.., &fields).map(|(index, field)| { + let ident = make_ident(index, field.ident.as_ref()); + let ty = &field.ty; + quote::quote! { #ident: <#ty as ::dunge::vertex::InputProjection>::Field } + }); + + let projection_inputs = iter::zip(0.., &fields).map(|(index, field)| { + let ident = make_ident(index, field.ident.as_ref()); + let ty = &field.ty; + quote::quote! { #ident: <#ty as ::dunge::vertex::InputProjection>::input_projection(id, #index) } + }); - let tex = match fl.peek().copied().and_then(Fl::new) { - Some(Fl { - kind: Kind::Tex, - ty, - }) => { - _ = fl.next(); - Some(ty) + quote::quote! { + unsafe impl ::dunge::vertex::Vertex for #name { + type Projection = #projection_name; + const DECL: ::dunge::vertex::DeclareInput = ::dunge::vertex::DeclareInput::new(&[ + #(#vector_types),*, + ]); } - _ => None, - }; - if let Some(field) = fl.next() { - let msg = match Kind::from_field(field) { - Some(_) => "this field is redundant", - None => "this field is unspecified", - }; - - return quote::quote_spanned! { field.span() => ::std::compile_error!(#msg); }; - } + struct #projection_name { + #(#projection_fields),*, + } - let type_name = derive.ident; - let col = col.map_or(quote::quote!(()), ToTokens::into_token_stream); - let tex = tex.map_or(quote::quote!(()), ToTokens::into_token_stream); - quote::quote! { - unsafe impl ::dunge::vertex::Vertex for #type_name { - type Position = #pos; - type Color = #col; - type Texture = #tex; + impl ::dunge::vertex::Projection for #projection_name { + fn projection(id: u32) -> Self { + Self { + #(#projection_inputs),*, + } + } } } } fn is_repr_c(attr: &Attribute) -> bool { - attr.path().is_ident("repr") - && attr - .parse_nested_meta(|meta| { - if meta.path.is_ident("C") { - Ok(()) - } else { - Err(meta.error("unrecognized repr")) - } - }) - .is_ok() -} + let parse_meta = |meta: ParseNestedMeta| { + if meta.path.is_ident("C") { + Ok(()) + } else { + Err(meta.error("unrecognized repr")) + } + }; -struct Fl<'a> { - kind: Kind, - ty: &'a Type, + attr.path().is_ident("repr") && attr.parse_nested_meta(parse_meta).is_ok() } -impl<'a> Fl<'a> { - fn new(field: &'a Field) -> Option { - Some(Self { - kind: Kind::from_field(field)?, - ty: &field.ty, - }) - } -} - -enum Kind { - Pos, - Col, - Tex, -} +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn derive_vertex() { + let input = quote::quote! { + #[repr(C)] + struct Vert { + pos: [f32; 2], + col: [f32; 3], + } + }; -impl Kind { - fn from_field(field: &Field) -> Option { - use syn::Meta; + let input = syn::parse2(input).expect("parse input"); + let actual = impl_vertex(input); + let expectd = quote::quote! { + unsafe impl ::dunge::vertex::Vertex for Vert { + type Projection = VertProjection; + const DECL: ::dunge::vertex::DeclareInput = ::dunge::vertex::DeclareInput::new(&[ + <[f32; 2] as ::dunge::vertex::InputProjection>::TYPE, + <[f32; 3] as ::dunge::vertex::InputProjection>::TYPE, + ]); + } + + struct VertProjection { + pos: <[f32; 2] as ::dunge::vertex::InputProjection>::Field, + col: <[f32; 3] as ::dunge::vertex::InputProjection>::Field, + } + + impl ::dunge::vertex::Projection for VertProjection { + fn projection(id: u32) -> Self { + Self { + pos: <[f32; 2] as ::dunge::vertex::InputProjection>::input_projection(id, 0u32), + col: <[f32; 3] as ::dunge::vertex::InputProjection>::input_projection(id, 1u32), + } + } + } + }; - field.attrs.iter().find_map(|attr| match &attr.meta { - Meta::Path(path) if path.is_ident("position") => Some(Self::Pos), - Meta::Path(path) if path.is_ident("color") => Some(Self::Col), - Meta::Path(path) if path.is_ident("texture") => Some(Self::Tex), - _ => None, - }) + assert_eq!(actual.to_string(), expectd.to_string()); } } diff --git a/dunge_shader/Cargo.toml b/dunge_shader/Cargo.toml index a3fcd8f..64a27d5 100644 --- a/dunge_shader/Cargo.toml +++ b/dunge_shader/Cargo.toml @@ -8,7 +8,7 @@ documentation = "https://docs.rs/dunge" repository = "https://github.com/nanoqsh/dunge" [dependencies] -glam = "0.25" +glam = { workspace = true } naga = "0.14" [lints] diff --git a/dunge_shader/src/group.rs b/dunge_shader/src/group.rs index e837b4c..e6075ea 100644 --- a/dunge_shader/src/group.rs +++ b/dunge_shader/src/group.rs @@ -3,24 +3,7 @@ use { std::{iter, slice}, }; -#[derive(Clone, Copy)] -pub struct DeclareGroup(&'static [GroupMemberType]); - -impl DeclareGroup { - pub const fn new(ts: &'static [GroupMemberType]) -> Self { - Self(ts) - } -} - -impl IntoIterator for DeclareGroup { - type Item = GroupMemberType; - type IntoIter = iter::Copied>; - - fn into_iter(self) -> Self::IntoIter { - self.0.iter().copied() - } -} - +/// Group description. pub trait Group { type Projection: Projection + 'static; const DECL: DeclareGroup; @@ -41,3 +24,21 @@ pub trait Visitor { fn visit_texture(&mut self, texture: Self::Texture); fn visit_sampler(&mut self, sampler: Self::Sampler); } + +#[derive(Clone, Copy)] +pub struct DeclareGroup(&'static [GroupMemberType]); + +impl DeclareGroup { + pub const fn new(ts: &'static [GroupMemberType]) -> Self { + Self(ts) + } +} + +impl IntoIterator for DeclareGroup { + type Item = GroupMemberType; + type IntoIter = iter::Copied>; + + fn into_iter(self) -> Self::IntoIter { + self.0.iter().copied() + } +} diff --git a/dunge_shader/src/lib.rs b/dunge_shader/src/lib.rs index aa2871c..f87c1ae 100644 --- a/dunge_shader/src/lib.rs +++ b/dunge_shader/src/lib.rs @@ -6,13 +6,12 @@ mod math; mod module; mod ret; mod texture; -mod types; +pub mod types; mod vector; pub mod vertex; pub mod sl { pub use crate::{ - context::*, convert::*, eval::*, math::*, module::*, ret::*, texture::*, types::*, - vector::*, + context::*, convert::*, eval::*, math::*, module::*, ret::*, texture::*, vector::*, }; } diff --git a/dunge_shader/src/module.rs b/dunge_shader/src/module.rs index 7d6bef5..0a52bfa 100644 --- a/dunge_shader/src/module.rs +++ b/dunge_shader/src/module.rs @@ -73,7 +73,11 @@ where } } -pub struct Out { +pub struct Out +where + P: Eval>, + C: Eval>, +{ pub place: P, pub color: C, } diff --git a/dunge_shader/src/vector.rs b/dunge_shader/src/vector.rs index 3ef9a94..a752f35 100644 --- a/dunge_shader/src/vector.rs +++ b/dunge_shader/src/vector.rs @@ -47,7 +47,7 @@ where } } -type Vector2 = Ret, types::Vec4>; +type Vector2 = Ret, types::Vec2>; pub const fn vec2(x: X, y: Y) -> Vector2 where @@ -58,7 +58,7 @@ where Ret::new(New((x, y))) } -type Vector3 = Ret, types::Vec4>; +type Vector3 = Ret, types::Vec3>; pub const fn vec3(x: X, y: Y, z: Z) -> Vector3 where @@ -103,26 +103,29 @@ where } } -pub const fn compose_vec2(a: A, b: B) -> Ret, types::Vec2> +pub const fn compose(a: A, b: B) -> Ret, types::Vec4> where - A: Eval, - B: Eval, + A: Eval>, + B: Eval>, + S: Scalar, { Ret::new(Compose { a, b }) } -pub const fn compose_vec3(a: A, b: B) -> Ret, types::Vec3> +pub const fn vec3_with(a: A, b: B) -> Ret, types::Vec3> where A: Eval>, B: Eval, + B::Out: Scalar, { Ret::new(Compose { a, b }) } -pub const fn compose_vec4(a: A, b: B) -> Ret, types::Vec4> +pub const fn vec4_with(a: A, b: B) -> Ret, types::Vec4> where A: Eval>, B: Eval, + B::Out: Scalar, { Ret::new(Compose { a, b }) } diff --git a/dunge_shader/src/vertex.rs b/dunge_shader/src/vertex.rs index 85029d2..c2de0e7 100644 --- a/dunge_shader/src/vertex.rs +++ b/dunge_shader/src/vertex.rs @@ -3,25 +3,28 @@ use { std::{iter, mem, slice}, }; -#[derive(Clone, Copy)] -pub struct DeclareInput(&'static [VectorType]); - -impl DeclareInput { - pub const fn new(ts: &'static [VectorType]) -> Self { - Self(ts) - } -} - -impl IntoIterator for DeclareInput { - type Item = VectorType; - type IntoIter = iter::Copied>; - - fn into_iter(self) -> Self::IntoIter { - self.0.iter().copied() - } -} - -#[allow(clippy::missing_safety_doc)] +/// Vertex type description. +/// +/// # Safety +/// The fields of [`DeclareInput`] must exactly match the actual struct fields. +/// To do this, the fields must be ordered, so the struct must have the `#[repr(C)]` attribute +/// and the fields must have the same order as specified in [`DeclareInput`]. +/// +/// # Example +/// TODO +/// +/// Note that the implementation of the trait requires `unsafe` code, +/// so instead of writing this yourself you can derive it: +/// ```rust,ignore +/// use dunge::Vertex; +/// +/// #[repr(C)] +/// #[derive(Vertex)] +/// struct Vert { +/// /* TODO */ +/// } +/// ``` +/// pub unsafe trait Vertex { type Projection: Projection + 'static; const DECL: DeclareInput; @@ -39,3 +42,21 @@ where pub trait Projection { fn projection(id: u32) -> Self; } + +#[derive(Clone, Copy)] +pub struct DeclareInput(&'static [VectorType]); + +impl DeclareInput { + pub const fn new(ts: &'static [VectorType]) -> Self { + Self(ts) + } +} + +impl IntoIterator for DeclareInput { + type Item = VectorType; + type IntoIter = iter::Copied>; + + fn into_iter(self) -> Self::IntoIter { + self.0.iter().copied() + } +}