Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ To run all examples in a row interactively, use

## Debugging

For debugging tips, see DEBUGGING.md
For conditional compilation, see [Conditional Compilation](docs/src/architecture/conditional-compilation.md)
For debugging tips, see [DEBUGGING.md](DEBUGGING.md).

For conditional compilation, see [Conditional Compilation](docs/src/architecture/conditional-compilation.md).
4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[workspace]
members = [".", "python", "ffi"]
members = [".", "python", "ffi", "derive"]
resolver = "2"

[package]
Expand All @@ -17,6 +17,8 @@ categories = ["graphics", "rendering"]
unexpected_cfgs = { level = "warn", check-cfg = ["cfg(feature, values(\"cargo-clippy\"))"] }

[dependencies]
goldy_derive = { version = "0.1.0", path = "derive" }

# Vulkan bindings (native only)
ash = { version = "0.38", optional = true }

Expand Down
33 changes: 33 additions & 0 deletions DEBUGGING.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,39 @@ Dump the SPIR-V using `GOLDY_DUMP_SHADERS` (see [Inspecting Compiled Shader Asse

See [shaders/README.md](shaders/README.md#preprocessor-defines) for Slang-specific preprocessor behavior that can cause cross-platform issues.

## Rust vs Slang struct layout validation

Wrong `#[repr(C)]` layouts for uniforms or structured-buffer types often show up as subtle bugs (garbage values, misaligned reads). Goldy can compare your Rust layout to Slang’s reflection on the **same** shader compile that emits SPIR-V / DXIL / MSL—no second compile.

### Enabling validation

Set **`GOLDY_VALIDATE_LAYOUTS`** to a truthy value before creating the device or compiling shaders:

| Value | Effect |
|---------|---------------|
| (unset) | No validation |
| `1` | Validate |
| `true` | Validate |
| `yes` | Validate |

```bash
GOLDY_VALIDATE_LAYOUTS=1 cargo run --example gradient --release
```

If a layout check fails, compilation returns an error describing size / field offset / name mismatches.

### In application code

1. Match the Rust struct name to the Slang `struct` name you want checked (reflection uses `FindTypeByName`).
2. Add **`#[derive(LayoutCheckable)]`** (re-exported from the `goldy` crate).
3. Pass **`&[YourStruct::LAYOUT_CHECK]`** as the last argument to **`ShaderModule::from_slang_with_options`** (other `from_slang*` helpers pass empty checks).

When the env var is off, those checks are skipped and `from_slang_with_options` behaves like a normal compile path.

The **`gradient`** and **`checkerboard`** examples demonstrate this with `TimeUniforms` vs `struct TimeUniforms` in the shader sources.

Standalone reflection without shader creation remains available via **`Device::reflect_struct`** and **`SlangCompiler::reflect_struct_layout`**.

## Inspecting Compiled Shader Assembly

When a shader produces unexpected results, inspecting the compiled bytecode can reveal codegen issues that aren't visible in the source. This is useful when:
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ Goldy uses [Slang](https://github.com/shader-slang/slang) for shader compilation

For FFI bindings (Python, .NET, C++), Slang libraries are bundled automatically by the respective build scripts. See [PACKAGING.md](PACKAGING.md) for architecture details and [DEBUGGING.md](DEBUGGING.md) for troubleshooting.

Optional **Rust vs Slang struct layout checks** at shader compile time: set `GOLDY_VALIDATE_LAYOUTS=1` and pass `LayoutCheck` data from `#[derive(LayoutCheckable)]` into `ShaderModule::from_slang_with_options` (see [DEBUGGING.md](DEBUGGING.md) and the `gradient` / `checkerboard` examples).

## Documentation

📖 **[Full Documentation](https://koubaa.github.io/goldy/)**
Expand Down
14 changes: 11 additions & 3 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,24 @@ Follow the sections below for each package manager.

### Publish

Publish **`goldy_derive` first** (proc-macro crate), then **`goldy`**. Keep their `version` fields in sync in `derive/Cargo.toml` and the root `Cargo.toml`.

```bash
# Publish core library
# 1. Proc-macro crate (no separate GitHub release needed)
cargo publish -p goldy_derive

# 2. Core library (depends on goldy_derive on crates.io)
cargo publish -p goldy

# Publish FFI library (if needed)
# 3. FFI library (if needed)
cargo publish -p goldy-ffi
```

The root `Cargo.toml` lists `goldy_derive` with both `path` and `version` so local builds use the workspace crate and `cargo publish` resolves the dependency from the registry.

### Version Files
- `Cargo.toml` - Update `version = "X.Y.Z"`
- `Cargo.toml` (goldy) - Update `version = "X.Y.Z"` and bump `goldy_derive` dependency version to match
- `derive/Cargo.toml` - Same `version = "X.Y.Z"` as goldy for releases

---

Expand Down
14 changes: 14 additions & 0 deletions derive/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "goldy_derive"
version = "0.1.0"
edition = "2021"
license = "MIT"
description = "Derive macros for goldy (LayoutCheckable, etc.)"

[lib]
proc-macro = true

[dependencies]
syn = { version = "2", features = ["full"] }
quote = "1"
proc-macro2 = "1"
76 changes: 76 additions & 0 deletions derive/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, Data, DeriveInput, Fields};

/// Derive a `LAYOUT_CHECK` constant for `#[repr(C)]` structs.
///
/// Generates a `const LAYOUT_CHECK: goldy::LayoutCheck<'static>` that captures
/// each field's name, offset, and size — ready to pass to
/// [`ShaderModule::from_slang_with_options`](goldy::ShaderModule::from_slang_with_options).
///
/// # Example
///
/// ```rust,ignore
/// #[derive(LayoutCheckable)]
/// #[repr(C)]
/// struct SceneUniforms {
/// projection: [[f32; 4]; 4],
/// modelview: [[f32; 4]; 4],
/// time: f32,
/// }
///
/// // Now use SceneUniforms::LAYOUT_CHECK
/// ```
#[proc_macro_derive(LayoutCheckable)]
pub fn derive_layout_checkable(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let name_str = name.to_string();

let fields = match &input.data {
Data::Struct(data) => match &data.fields {
Fields::Named(named) => &named.named,
_ => {
return syn::Error::new_spanned(name, "LayoutCheckable requires named fields")
.to_compile_error()
.into();
}
},
_ => {
return syn::Error::new_spanned(name, "LayoutCheckable can only be derived on structs")
.to_compile_error()
.into();
}
};

let field_entries = fields.iter().map(|f| {
let ident = f.ident.as_ref().unwrap();
let ident_str = ident.to_string();
let ty = &f.ty;
quote! {
(#ident_str, std::mem::offset_of!(#name, #ident), std::mem::size_of::<#ty>())
}
});

let n = fields.len();

let expanded = quote! {
impl #name {
/// Struct layout descriptor for Slang reflection validation.
///
/// Auto-generated by `#[derive(LayoutCheckable)]`.
pub const LAYOUT_CHECK: goldy::LayoutCheck<'static> = goldy::LayoutCheck {
type_name: #name_str,
rust_size: std::mem::size_of::<#name>(),
rust_fields: {
const FIELDS: [(&str, usize, usize); #n] = [
#(#field_entries),*
];
&FIELDS
},
};
}
};

expanded.into()
}
12 changes: 12 additions & 0 deletions docs/src/concepts/shaders.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,18 @@ let shader = ShaderModule::from_slang(&device, r#"
"#)?;
```

## Rust vs Slang struct layout (optional)

If your Rust `#[repr(C)]` types must match Slang `struct` layouts (uniforms, structured buffers), you can validate them on the **same** compile that produces GPU bytecode—no extra Slang invocation.

1. Name the Rust struct like the Slang type (reflection uses `FindTypeByName`).
2. Add **`#[derive(LayoutCheckable)]`** (from the `goldy` crate).
3. Pass **`&[YourType::LAYOUT_CHECK]`** to **`ShaderModule::from_slang_with_options`** as the last argument.

Validation runs only when **`GOLDY_VALIDATE_LAYOUTS`** is set to `1`, `true`, or `yes`; otherwise the checks are skipped.

The **`gradient`** and **`checkerboard`** examples use this pattern with `TimeUniforms`. For tables of other environment variables, logging, and shader dumps, see **[DEBUGGING.md](https://github.com/koubaa/goldy/blob/main/DEBUGGING.md)** in the repository.

## The `goldy_exp` Library (Experimental)

Every `Device` comes with the `goldy_exp` shader library pre-registered.
Expand Down
6 changes: 4 additions & 2 deletions docs/src/examples/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@ All examples support:
- **Escape** - Exit the application
- **Window resize** - Automatic adaptation

The **`gradient`** and **`checkerboard`** examples pass `LayoutCheck` metadata from `#[derive(LayoutCheckable)]` into `ShaderModule::from_slang_with_options`. Run with **`GOLDY_VALIDATE_LAYOUTS=1`** to assert Rust uniform layouts match Slang at shader compile time (see [Shaders: layout validation](../concepts/shaders.md#rust-vs-slang-struct-layout-optional) and [DEBUGGING.md](https://github.com/koubaa/goldy/blob/main/DEBUGGING.md)).

## Example Gallery

### Basic

| Example | Description | Key Concepts |
|---------|-------------|--------------|
| `triangle` | Colored triangle | Vertex buffers, basic pipeline |
| `gradient` | Animated gradient | Fragment shaders, UV coordinates |
| `gradient` | Animated gradient | Fragment shaders, UV coordinates, optional `GOLDY_VALIDATE_LAYOUTS` |
| `window` | Triangle with animation | Surface API basics |

### Classic Demoscene
Expand All @@ -46,7 +48,7 @@ All examples support:
|---------|-------------|--------------|
| `bouncing_lines` | Lines bouncing off walls | Line primitive, physics |
| `spinning_cube` | 3D wireframe cube | 3D projection, rotation matrices |
| `checkerboard` | Animated procedural texture | UV distortion, patterns |
| `checkerboard` | Animated procedural texture | UV distortion, patterns, optional `GOLDY_VALIDATE_LAYOUTS` |
| `waveform` | Audio waveform visualizer | Line strips, multiple draw calls |
| `instancing` | 400 rotating quads | Many objects, HSV colors |

Expand Down
27 changes: 18 additions & 9 deletions examples/checkerboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
//! Demonstrates procedural texturing in fragment shader using Surface API.
//! Uses vertex-less fullscreen triangle (Goldy-native pattern).
//!
//! Run with: cargo run --example checkerboard
//! Run with: `cargo run --example checkerboard`
//!
//! Optional layout validation: `GOLDY_VALIDATE_LAYOUTS=1 cargo run --example checkerboard`

use goldy::{
shaders, Buffer, Color, CommandEncoder, DataAccess, DeviceType, Instance, RenderPipeline,
RenderPipelineDesc, ShaderModule, Surface, VertexBufferLayout,
shaders, Buffer, Color, CommandEncoder, DataAccess, DeviceType, Instance, LayoutCheckable,
RenderPipeline, RenderPipelineDesc, ShaderModule, Surface, VertexBufferLayout,
};
use std::sync::Arc;
use std::time::Instant;
Expand All @@ -19,10 +21,10 @@ use winit::{
window::{Window, WindowId},
};

/// Uniform buffer data (must match shader cbuffer layout)
/// Uniform buffer data — name and fields must match `struct TimeUniforms` in `shaders/checkerboard.slang`.
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct Uniforms {
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable, LayoutCheckable)]
struct TimeUniforms {
time: f32,
}

Expand Down Expand Up @@ -54,7 +56,14 @@ impl App {
fn init_gpu(&mut self, window: &Arc<Window>) -> anyhow::Result<()> {
let device = Arc::new(self.instance.create_device(DeviceType::DiscreteGpu)?);
let surface = Surface::new(&device, window.as_ref())?;
let shader = ShaderModule::from_slang(&device, shaders::CHECKERBOARD)?;
let shader = ShaderModule::from_slang_with_options(
&device,
shaders::CHECKERBOARD,
&[],
&[],
Default::default(),
&[TimeUniforms::LAYOUT_CHECK],
)?;

// Create pipeline - no vertex buffer needed, shader uses SV_VertexID
let pipeline = RenderPipeline::new(
Expand All @@ -71,7 +80,7 @@ impl App {
// Create uniform buffer for time
let uniform_buffer = Buffer::new(
device.as_ref(),
std::mem::size_of::<Uniforms>() as u64,
std::mem::size_of::<TimeUniforms>() as u64,
DataAccess::Broadcast,
)?;

Expand All @@ -96,7 +105,7 @@ impl App {

// Update uniform buffer with current time
let time = self.start_time.elapsed().as_secs_f32();
let uniforms = Uniforms { time };
let uniforms = TimeUniforms { time };
uniform_buffer.write_data(0, &[uniforms])?;

let frame = surface.acquire()?;
Expand Down
28 changes: 19 additions & 9 deletions examples/gradient.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
//! Demonstrates fragment shader with time-based animation using the Surface API.
//! Uses vertex-less fullscreen triangle (Goldy-native pattern).
//!
//! Run with: cargo run --example gradient
//! Run with: `cargo run --example gradient`
//!
//! Optional: validate the Rust `TimeUniforms` layout against Slang on the shader compile path:
//! `GOLDY_VALIDATE_LAYOUTS=1 cargo run --example gradient`

use goldy::{
shaders, Buffer, Color, CommandEncoder, DataAccess, DeviceType, Instance, RenderPipeline,
RenderPipelineDesc, ShaderModule, Surface, VertexBufferLayout,
shaders, Buffer, Color, CommandEncoder, DataAccess, DeviceType, Instance, LayoutCheckable,
RenderPipeline, RenderPipelineDesc, ShaderModule, Surface, VertexBufferLayout,
};
use std::sync::Arc;
use std::time::Instant;
Expand All @@ -19,10 +22,10 @@ use winit::{
window::{Window, WindowId},
};

/// Uniform buffer data (must match shader cbuffer layout)
/// Uniform buffer data — name and fields must match `struct TimeUniforms` in `shaders/gradient.slang`.
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct Uniforms {
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable, LayoutCheckable)]
struct TimeUniforms {
time: f32,
}

Expand Down Expand Up @@ -54,7 +57,14 @@ impl App {
fn init_gpu(&mut self, window: &Arc<Window>) -> anyhow::Result<()> {
let device = Arc::new(self.instance.create_device(DeviceType::DiscreteGpu)?);
let surface = Surface::new(&device, window.as_ref())?;
let shader = ShaderModule::from_slang(&device, shaders::GRADIENT)?;
let shader = ShaderModule::from_slang_with_options(
&device,
shaders::GRADIENT,
&[],
&[],
Default::default(),
&[TimeUniforms::LAYOUT_CHECK],
)?;

// Create pipeline - no vertex buffer needed, shader uses SV_VertexID
let pipeline = RenderPipeline::new(
Expand All @@ -71,7 +81,7 @@ impl App {
// Create uniform buffer for time
let uniform_buffer = Buffer::new(
device.as_ref(),
std::mem::size_of::<Uniforms>() as u64,
std::mem::size_of::<TimeUniforms>() as u64,
DataAccess::Broadcast,
)?;

Expand All @@ -96,7 +106,7 @@ impl App {

// Update uniform buffer with current time
let time = self.start_time.elapsed().as_secs_f32();
let uniforms = Uniforms { time };
let uniforms = TimeUniforms { time };
uniform_buffer.write_data(0, &[uniforms])?;

let frame = surface.acquire()?;
Expand Down
Loading
Loading