Shaders: add BufferStruct to support bool and enums (#3109)

* node-macro: modernize `node` macro

* node-macro: add `CrateIdent` struct containing resolved crate paths

* shaders: add trait `BufferStruct` and derive macro

* shaders: `gamma_correction` and `channel_mixer` gpu nodes

* shaders: `selective_color` gpu node

* shaders: `brightness_contrast_classic` gpu node

* shaders: append GPU to display name

* node-macro: fixup doc links

* shaders: consistently append " GPU" to all shader node names
This commit is contained in:
Firestar99
2025-09-05 18:32:41 +02:00
committed by GitHub
parent acd7ba38cc
commit f12b4da549
22 changed files with 788 additions and 118 deletions

View File

@@ -22,10 +22,14 @@ std = [
"glam/serde",
"half/std",
"half/serde",
"num-traits/std"
"num-traits/std",
"num_enum/std",
]
[dependencies]
# Local dependencies
node-macro = { workspace = true }
# Local std dependencies
dyn-any = { workspace = true, optional = true }
@@ -35,6 +39,8 @@ glam = { workspace = true }
half = { workspace = true, default-features = false }
num-derive = { workspace = true }
num-traits = { workspace = true }
num_enum = { workspace = true }
spirv-std = { workspace = true }
# Workspace std dependencies
serde = { workspace = true, optional = true }

View File

@@ -1,9 +1,11 @@
use core::fmt::Display;
use core::hash::{Hash, Hasher};
use node_macro::BufferStruct;
use num_enum::{FromPrimitive, IntoPrimitive};
#[cfg(not(feature = "std"))]
use num_traits::float::Float;
#[derive(Debug, Clone, Copy, PartialEq)]
#[derive(Debug, Clone, Copy, PartialEq, BufferStruct)]
#[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "std", serde(default))]
pub struct AlphaBlending {
@@ -66,7 +68,7 @@ impl AlphaBlending {
}
#[repr(i32)]
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash, bytemuck::NoUninit)]
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash, BufferStruct, FromPrimitive, IntoPrimitive)]
#[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))]
pub enum BlendMode {
// Basic group

View File

@@ -5,6 +5,7 @@ use core::fmt::Debug;
use core::hash::Hash;
use glam::Vec4;
use half::f16;
use node_macro::BufferStruct;
#[cfg(not(feature = "std"))]
use num_traits::Euclid;
#[cfg(not(feature = "std"))]
@@ -215,7 +216,7 @@ impl Pixel for Luma {}
/// The other components (RGB) are stored as `f32` that range from `0.0` up to `f32::MAX`,
/// the values encode the brightness of each channel proportional to the light intensity in cd/m² (nits) in HDR, and `0.0` (black) to `1.0` (white) in SDR color.
#[repr(C)]
#[derive(Debug, Default, Clone, Copy, PartialEq, Pod, Zeroable)]
#[derive(Debug, Default, Clone, Copy, PartialEq, Pod, Zeroable, BufferStruct)]
#[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))]
pub struct Color {
red: f32,

View File

@@ -5,6 +5,7 @@ pub mod choice_type;
pub mod color;
pub mod context;
pub mod registry;
pub mod shaders;
pub use context::Ctx;
pub use glam;

View File

@@ -0,0 +1,114 @@
use crate::shaders::buffer_struct::BufferStruct;
macro_rules! glam_array {
($t:ty, $a:ty) => {
unsafe impl BufferStruct for $t {
type Buffer = $a;
#[inline]
fn write(from: Self) -> Self::Buffer {
<$t>::to_array(&from)
}
#[inline]
fn read(from: Self::Buffer) -> Self {
<$t>::from_array(from)
}
}
};
}
macro_rules! glam_cols_array {
($t:ty, $a:ty) => {
unsafe impl BufferStruct for $t {
type Buffer = $a;
#[inline]
fn write(from: Self) -> Self::Buffer {
<$t>::to_cols_array(&from)
}
#[inline]
fn read(from: Self::Buffer) -> Self {
<$t>::from_cols_array(&from)
}
}
};
}
glam_array!(glam::Vec2, [f32; 2]);
glam_array!(glam::Vec3, [f32; 3]);
// glam_array!(Vec3A, [f32; 4]);
glam_array!(glam::Vec4, [f32; 4]);
glam_array!(glam::Quat, [f32; 4]);
glam_cols_array!(glam::Mat2, [f32; 4]);
glam_cols_array!(glam::Mat3, [f32; 9]);
// glam_cols_array!(Mat3A, [f32; 4]);
glam_cols_array!(glam::Mat4, [f32; 16]);
glam_cols_array!(glam::Affine2, [f32; 6]);
glam_cols_array!(glam::Affine3A, [f32; 12]);
glam_array!(glam::DVec2, [f64; 2]);
glam_array!(glam::DVec3, [f64; 3]);
glam_array!(glam::DVec4, [f64; 4]);
glam_array!(glam::DQuat, [f64; 4]);
glam_cols_array!(glam::DMat2, [f64; 4]);
glam_cols_array!(glam::DMat3, [f64; 9]);
glam_cols_array!(glam::DMat4, [f64; 16]);
glam_cols_array!(glam::DAffine2, [f64; 6]);
glam_cols_array!(glam::DAffine3, [f64; 12]);
glam_array!(glam::I16Vec2, [i16; 2]);
glam_array!(glam::I16Vec3, [i16; 3]);
glam_array!(glam::I16Vec4, [i16; 4]);
glam_array!(glam::U16Vec2, [u16; 2]);
glam_array!(glam::U16Vec3, [u16; 3]);
glam_array!(glam::U16Vec4, [u16; 4]);
glam_array!(glam::IVec2, [i32; 2]);
glam_array!(glam::IVec3, [i32; 3]);
glam_array!(glam::IVec4, [i32; 4]);
glam_array!(glam::UVec2, [u32; 2]);
glam_array!(glam::UVec3, [u32; 3]);
glam_array!(glam::UVec4, [u32; 4]);
glam_array!(glam::I64Vec2, [i64; 2]);
glam_array!(glam::I64Vec3, [i64; 3]);
glam_array!(glam::I64Vec4, [i64; 4]);
glam_array!(glam::U64Vec2, [u64; 2]);
glam_array!(glam::U64Vec3, [u64; 3]);
glam_array!(glam::U64Vec4, [u64; 4]);
unsafe impl BufferStruct for glam::Vec3A {
type Buffer = [f32; 4];
#[inline]
fn write(from: Self) -> Self::Buffer {
glam::Vec4::to_array(&from.extend(0.))
}
#[inline]
fn read(from: Self::Buffer) -> Self {
glam::Vec3A::from_vec4(glam::Vec4::from_array(from))
}
}
/// do NOT use slices, otherwise spirv will fail to compile
unsafe impl BufferStruct for glam::Mat3A {
type Buffer = [f32; 12];
#[inline]
fn write(from: Self) -> Self::Buffer {
let a = from.to_cols_array();
[a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], 0., 0., 0.]
}
#[inline]
fn read(from: Self::Buffer) -> Self {
let a = from;
glam::Mat3A::from_cols_array(&[a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8]])
}
}

View File

@@ -0,0 +1,63 @@
//! I (@firestar99) copied this entire mod from one of my projects, as I haven't uploaded that lib to crates. Hopefully
//! rust-gpu improves and this entire thing becomes unnecessary in the future.
//!
//! https://github.com/Firestar99/nanite-at-home/tree/008dac8df656959c71efeddd2d3ddabcb801771c/rust-gpu-bindless/crates/buffer-content
use bytemuck::Pod;
mod glam;
mod primitive;
/// A BufferStruct is a "parallel representation" of the original struct with some fundamental types remapped. This
/// struct hierarchy represents how data is stored in GPU Buffers, where all types must be [`Pod`] to allow
/// transmuting them to `&[u8]` with [`bytemuck`].
///
/// Notable type remappings (original: buffer):
/// * bool: u32 of 0 or 1
/// * any repr(u32) enum: u32 with remapping via [`num_enum`]
///
/// By adding `#[derive(ShaderStruct)]` to your struct (or enum), a parallel `{name}Buffer` struct is created with all
/// the members of the original struct, but with their types using the associated remapped types as specified by this
/// trait.
///
/// # Origin
/// I (@firestar99) copied this entire mod from my [Nanite-at-home] project, specifically the [buffer-content] crate
/// and the [buffer_struct] proc macro. The variant here has quite some modifications, to both cleaned up some of the
/// mistakes my implementation has and to customize it a bit for graphite.
///
/// Hopefully rust-gpu improves to the point where this remapping becomes unnecessary.
///
/// [Nanite-at-home]: https://github.com/Firestar99/nanite-at-home
/// [buffer-content]: https://github.com/Firestar99/nanite-at-home/tree/008dac8df656959c71efeddd2d3ddabcb801771c/rust-gpu-bindless/crates/buffer-content
/// [buffer_struct]: https://github.com/Firestar99/nanite-at-home/blob/008dac8df656959c71efeddd2d3ddabcb801771c/rust-gpu-bindless/crates/macros/src/buffer_struct.rs
///
/// # Safety
/// The associated type Transfer must be the same on all targets. Writing followed by reading back a value must result
/// in the same value.
pub unsafe trait BufferStruct: Copy + Send + Sync + 'static {
type Buffer: Pod + Send + Sync;
fn write(from: Self) -> Self::Buffer;
fn read(from: Self::Buffer) -> Self;
}
/// Trait marking all [`BufferStruct`] whose read and write methods are identity. While [`BufferStruct`] only
/// requires `t == read(write(t))`, this trait additionally requires `t == read(t) == write(t)`. As this removes the
/// conversion requirement for writing to or reading from a buffer, one can acquire slices from buffers created of these
/// types.
///
/// Implementing this type is completely safe due to the [`Pod`] requirement.
pub trait BufferStructIdentity: Pod + Send + Sync {}
unsafe impl<T: BufferStructIdentity> BufferStruct for T {
type Buffer = Self;
fn write(from: Self) -> Self::Buffer {
from
}
fn read(from: Self::Buffer) -> Self {
from
}
}

View File

@@ -0,0 +1,135 @@
use crate::shaders::buffer_struct::{BufferStruct, BufferStructIdentity};
use bytemuck::Pod;
use core::marker::PhantomData;
use core::num::Wrapping;
use spirv_std::arch::IndexUnchecked;
macro_rules! identity {
($t:ty) => {
impl BufferStructIdentity for $t {}
};
}
identity!(());
identity!(u8);
identity!(u16);
identity!(u32);
identity!(u64);
identity!(u128);
identity!(usize);
identity!(i8);
identity!(i16);
identity!(i32);
identity!(i64);
identity!(i128);
identity!(isize);
identity!(f32);
identity!(f64);
identity!(spirv_std::arch::SubgroupMask);
identity!(spirv_std::memory::Semantics);
identity!(spirv_std::ray_tracing::RayFlags);
identity!(spirv_std::indirect_command::DrawIndirectCommand);
identity!(spirv_std::indirect_command::DrawIndexedIndirectCommand);
identity!(spirv_std::indirect_command::DispatchIndirectCommand);
identity!(spirv_std::indirect_command::DrawMeshTasksIndirectCommandEXT);
identity!(spirv_std::indirect_command::TraceRaysIndirectCommandKHR);
// not pod
// identity!(spirv_std::indirect_command::TraceRaysIndirectCommand2KHR);
unsafe impl BufferStruct for bool {
type Buffer = u32;
#[inline]
fn write(from: Self) -> Self::Buffer {
from as u32
}
#[inline]
fn read(from: Self::Buffer) -> Self {
from != 0
}
}
unsafe impl<T: BufferStruct> BufferStruct for Wrapping<T>
where
// unfortunately has to be Pod, even though AnyBitPattern would be sufficient,
// due to bytemuck doing `impl<T: Pod> AnyBitPattern for T {}`
// see https://github.com/Lokathor/bytemuck/issues/164
T::Buffer: Pod,
{
type Buffer = Wrapping<T::Buffer>;
#[inline]
fn write(from: Self) -> Self::Buffer {
Wrapping(T::write(from.0))
}
#[inline]
fn read(from: Self::Buffer) -> Self {
Wrapping(T::read(from.0))
}
}
unsafe impl<T: BufferStruct + 'static> BufferStruct for PhantomData<T> {
type Buffer = PhantomData<T>;
#[inline]
fn write(_: Self) -> Self::Buffer {
PhantomData {}
}
#[inline]
fn read(_: Self::Buffer) -> Self {
PhantomData {}
}
}
/// Potential problem: you can't impl this for an array of BufferStruct, as it'll conflict with this impl due to the
/// blanket impl on all BufferStructPlain types.
unsafe impl<T: BufferStruct, const N: usize> BufferStruct for [T; N]
where
// rust-gpu does not like `[T; N].map()` nor `core::array::from_fn()` nor transmuting arrays with a const generic
// length, so for now we need to require T: Default and T::Transfer: Default for all arrays.
T: Default,
// unfortunately has to be Pod, even though AnyBitPattern would be sufficient,
// due to bytemuck doing `impl<T: Pod> AnyBitPattern for T {}`
// see https://github.com/Lokathor/bytemuck/issues/164
T::Buffer: Pod + Default,
{
type Buffer = [T::Buffer; N];
#[inline]
fn write(from: Self) -> Self::Buffer {
unsafe {
let mut ret = [T::Buffer::default(); N];
for i in 0..N {
*ret.index_unchecked_mut(i) = T::write(*from.index_unchecked(i));
}
ret
}
}
#[inline]
fn read(from: Self::Buffer) -> Self {
unsafe {
let mut ret = [T::default(); N];
for i in 0..N {
*ret.index_unchecked_mut(i) = T::read(*from.index_unchecked(i));
}
ret
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn roundtrip_bool() {
for x in [false, true] {
assert_eq!(x, <bool as BufferStruct>::read(<bool as BufferStruct>::write(x)));
}
}
}

View File

@@ -0,0 +1,10 @@
//! supporting infrastructure for shaders
pub mod buffer_struct;
pub mod __private {
pub use bytemuck;
pub use glam;
pub use num_enum;
pub use spirv_std;
}