Goldy: GPU runtime for the Fondaco Machine
Goldy is a Rust GPU library that realizes the Fondaco Machine.
Maturity: Goldy 0.2 is the Fondaco Machine public API. SemVer applies within 0.2.x; expect breaking changes at 0.3.
The Fondaco model in Goldy
| Fondaco | Goldy |
|---|---|
| Scheme | Scheme — retained graph, resubmitted each frame |
| Parcel | Parcel, Buffer, Texture |
| Dispatch | Compute, render, copy, and present nodes inside a scheme |
| Exchange | SurfaceExchange, MemoryExchange |
| Settlement | Transaction → Claim |
Goldy abstracts where bytes live (descriptor slots, residency, relocation) but exposes what access costs (access patterns, resize cost, readback path). See the design thesis.
Typed bindless shaders
Shaders use Slang with goldy_exp virtual entry points. Resources are typed parameters — Goldy resolves bindless slots automatically:
import goldy_exp;
[goldy_compute]
[numthreads(64, 1, 1)]
void cs_main(MyUniforms cfg, Scattered<uint> data, ThreadId id) {
data[id.x] = data[id.x] + cfg.base;
}
| Type | Use |
|---|---|
Scattered<T> | Read/write storage |
BufRO<T> | Read-only storage |
DirectSpatial<T> | Read/write texture |
Interpolated<T> | Sampled texture |
Broadcast (struct param) | Per-dispatch constants |
Scheme
Record once, submit every frame. Goldy inserts barriers, parallelizes independent nodes, and aliases transient resources:
#![allow(unused)] fn main() { let mut scheme = Scheme::new(&ctx); scheme .node("simulate", &sim_pipeline) .with_parcel(&particles, NodeAccess::ReadWrite) .dispatch(group_count, 1, 1); let submission = scheme.submit()?; }
Compute-to-surface
Compute shaders can write swapchain drawables directly — no graphics pipeline or raster pass:
#![allow(unused)] fn main() { let surface = SurfaceExchange::new(&ctx, &window, SurfaceConfig::default())?; let mut scheme = Scheme::new(&ctx); let (lease, present) = surface.bind_destination(&mut scheme)?; scheme .node("render", &compute_pipeline) .with_parcel(&uniforms, NodeAccess::Read) .with_present(&lease) .dispatch(wg_x, wg_y, 1); let mut submission = scheme.submit()?; present.claim(&mut submission)?.consume()?; }
Backends and bindings
| Platform | Backend |
|---|---|
| Windows | DX12 (default), Vulkan |
| Linux | Vulkan (Wayland surfaces) |
| macOS | Metal |
CUDA and WebGPU backends are in progress. A Tenstorrent backend (Torus) is planned. See Backend Architecture.
Bindings: Python, .NET, C++, Rust FFI Client.
Quick links
- Installation
- Your First Triangle
- Your First Compute Shader
- Fondaco overview
- Goldy runtime mapping
- GitHub
License
MIT — see License.
Installation
Requirements
- Rust stable (recent version recommended)
- A supported GPU
Adding Goldy to Your Project
[dependencies]
goldy = "0.2"
Or with cargo:
cargo add goldy
Feature Flags
| Feature | Default | Description |
|---|---|---|
vulkan | yes | Vulkan 1.4+ backend (Linux, Windows); implies graphics and gpu |
dx12 | yes | DirectX 12 backend (Windows); implies graphics and gpu |
metal | yes | Metal Tier 2+ backend (macOS); implies graphics and gpu |
graphics | yes | Raster pipelines, render targets, surfaces, and presentation |
gpu | yes | Implied by every real GPU backend (not mock). Do not enable alone. |
cuda | no | CUDA backend (in progress; NVIDIA compute; implies gpu, not graphics) |
webgpu | no | WebGPU backend (in progress; via wgpu; implies graphics and gpu) |
instrumentation | yes | Structured tracing via tracing-subscriber (zero-cost when disabled) |
graphics is implied by the native backends. gpu is implied by every real GPU
backend (not mock). Textures and samplers remain available without graphics for GPGPU workloads. CUDA is not a platform default; it auto-selects only in --no-default-features --features cuda builds (otherwise set GOLDY_BACKEND=cuda):
cargo test --no-default-features --features cuda --test scheme_compute_integration
Platform-inappropriate features are no-ops — enabling metal on Linux or dx12 on macOS compiles cleanly but does nothing.
To build with only specific backends:
[dependencies]
goldy = { version = "0.2", default-features = false, features = ["vulkan"] }
Shader Toolchain
Goldy uses Slang as its shader language. The Rust build.rs downloads (if needed) and embeds the pinned Slang version at compile time; at runtime Goldy extracts and loads it automatically. Application developers do not install Slang separately.
Set GOLDY_SLANG_PATH only to override with a custom Slang build.
Verifying Installation
use goldy::{DeviceDescriptor, Instance, RequestAdapterOptions}; fn main() -> anyhow::Result<()> { let instance = Instance::new()?; println!("Available GPUs:"); for adapter in instance.enumerate_adapters() { println!(" {} ({:?})", adapter.name, adapter.device_type); } let device = instance .request_adapter(&RequestAdapterOptions::default())? .request_device(&DeviceDescriptor::default())?; println!("\nUsing: {}", device.adapter_info().name); Ok(()) }
cargo run
Expected output:
Available GPUs:
NVIDIA GeForce RTX 4060 Ti (DiscreteGpu)
Intel(R) UHD Graphics 770 (IntegratedGpu)
Using: NVIDIA GeForce RTX 4060 Ti
Backend Selection
Goldy selects the best backend for your platform automatically:
| Platform | Default Backend |
|---|---|
| Windows | DX12 |
| Linux | Vulkan |
| macOS | Metal |
Override at runtime with GOLDY_BACKEND:
GOLDY_BACKEND=vulkan cargo run
Platform-Specific Setup
Windows
DX12 is used by default and requires no additional setup. For the Vulkan backend, install the Vulkan SDK. Ensure your GPU drivers are up to date.
Linux
Install Vulkan development packages:
# Ubuntu/Debian
sudo apt install libvulkan-dev vulkan-tools
# Fedora
sudo dnf install vulkan-loader-devel vulkan-tools
# Arch
sudo pacman -S vulkan-icd-loader vulkan-tools
macOS
Goldy uses the native Metal backend — no MoltenVK or Vulkan SDK needed. Ensure macOS 12+ and Xcode command-line tools are installed:
xcode-select --install
Windowing (for examples)
Examples require the examples feature (winit is gated):
cargo run --features examples --example triangle --release
Next Steps
- Your First Triangle — draw a colored triangle
- Your First Compute Shader — write pixels from compute
Your First Triangle
This tutorial draws a colored triangle in a window using Goldy's render pipeline and present-on-scheme API (SurfaceExchange + Scheme + Transaction).
See examples/triangle.rs for the full source.
Recording the Scheme
Once at init (and again on resize), record a retained scheme: offscreen render pass → copy to surface via SurfaceExchange::bind_render_target.
#![allow(unused)] fn main() { use goldy::{ shader::builtins, Buffer, BufferKind, Color, DeviceDescriptor, Instance, Lease, LeaseRenderTarget, NodeAccess, RenderPipeline, RenderPipelineDesc, RequestAdapterOptions, RetainedPool, Scheme, ShaderModule, SurfaceConfig, SurfaceExchange, Transaction, Vertex2D, }; fn record_scheme( scheme: &mut Scheme, surface: &SurfaceExchange, pipeline: &RenderPipeline, vertex_buffer: &Buffer, scene_rt: &Lease<LeaseRenderTarget>, bg_color: Color, ) -> anyhow::Result<Transaction> { let mut pass = scheme.render_pass("triangle", scene_rt, TargetLoad::Clear(bg_color)); pass.with_parcel(vertex_buffer, NodeAccess::Read); pass.set_pipeline(pipeline); pass.set_vertex_buffer(0, vertex_buffer); pass.draw(0..3, 0..1); pass.finish(); surface.bind_render_target(scheme, scene_rt).map_err(Into::into) } }
Per-Frame Submit
Each frame submits the retained scheme and consumes the surface claim:
#![allow(unused)] fn main() { let mut submission = scheme.submit()?; present.claim(&mut submission)?.consume()?; }
Walkthrough
Instance, Device, and Context
#![allow(unused)] fn main() { let instance = Instance::new()?; let device = Arc::new( instance .request_adapter(&RequestAdapterOptions::default())? .request_device(&DeviceDescriptor::default())?, ); let ctx = device.create_context()?; }
Instance discovers available GPUs. create_context opens the submission context used by Scheme.
Vertex Buffer
#![allow(unused)] fn main() { let vertices = [ Vertex2D::new(0.0, -0.5, Color::RED), Vertex2D::new(-0.5, 0.5, Color::GREEN), Vertex2D::new(0.5, 0.5, Color::BLUE), ]; let mut pool = RetainedPool::new(device.clone()); let vertex_buffer = pool.acquire_buffer_with_data(&vertices, BufferKind::Scattered)?; }
Vertex2D is a built-in vertex type with position and color. Keep the pool alive for the buffer's lifetime.
Shader and Pipeline
#![allow(unused)] fn main() { let shader = ShaderModule::from_slang(&device, builtins::VERTEX_COLOR_2D)?; let surface = SurfaceExchange::new(&ctx, window.as_ref(), SurfaceConfig::default())?; let pipeline = RenderPipeline::new( &device, &shader, &shader, &RenderPipelineDesc { vertex_layout: Vertex2D::layout(), target_format: surface.format(), ..Default::default() }, )?; }
builtins::VERTEX_COLOR_2D uses [goldy_vertex] and [goldy_fragment] virtual entry points from the goldy_exp library.
Surface and Presentation
#![allow(unused)] fn main() { let mut scheme = Scheme::new(&ctx); let scene_rt = scheme.lease_render_target(width, height, surface.format(), None)?; let present = record_scheme(&mut scheme, &surface, &pipeline, &vertex_buffer, &scene_rt, bg_color)?; // Each frame: let mut submission = scheme.submit()?; present.claim(&mut submission)?.consume()?; }
SurfaceExchange manages the OS swapchain. Scene color is rendered to a scheme-leased offscreen target, copied to the drawable, and displayed when the claim is consumed. Rendering stays on the GPU — no CPU readback.
On resize, rebuild the scheme and transaction with the new dimensions (see examples/triangle.rs).
Run It
cargo run --example triangle
You should see a window with a colored triangle on a dark blue background.
Next Steps
- Your First Compute Shader — bypass the graphics pipeline entirely
- Examples — more complex demos
Your First Compute Shader
This tutorial renders an animated plasma effect by dispatching a compute shader directly to a swapchain drawable — no graphics pipeline, no vertex buffers, no render passes.
See examples/compute_to_surface.rs for the full source.
The Shader
The compute shader uses goldy_exp virtual entry points. It reads uniforms via BufRO<Uniforms> and writes pixels via DirectSpatial<float4>:
import goldy_exp;
struct Uniforms {
uint width;
uint height;
float time;
float _padding;
};
[goldy_compute]
[numthreads(8, 8, 1)]
void cs_main(BufRO<Uniforms> uniforms_buf, DirectSpatial<float4> output, ThreadId tid) {
Uniforms u = uniforms_buf[0];
if (tid.x >= u.width || tid.y >= u.height)
return;
float2 uv = float2(float(tid.x) / float(u.width),
float(tid.y) / float(u.height));
float2 p = uv * 2.0 - 1.0;
p.x *= float(u.width) / float(u.height);
float t = u.time;
float v = 0.0;
v += sin(p.x * 6.0 + t);
v += sin(p.y * 6.0 + t * 1.3);
v += sin((p.x + p.y) * 4.0 + t * 0.7);
v += sin(length(p) * 8.0 - t * 2.0);
v *= 0.25;
float3 col = float3(0.5 + 0.5 * sin(v * 3.14159 + 0.0),
0.5 + 0.5 * sin(v * 3.14159 + 2.094),
0.5 + 0.5 * sin(v * 3.14159 + 4.188));
output[tid.xy] = float4(col, 1.0);
}
Key points:
BufRO<Uniforms>is a read-only structured buffer. Index with[0]to load the single element.DirectSpatial<float4>is anRWTexture2D<float4>— write to it withoutput[tid.xy].ThreadIdmaps toSV_DispatchThreadID. Each thread handles one pixel.- The
[goldy_compute]attribute tells the Goldy compiler to wire up bindless slots automatically.
Rust Side
Uniform Buffer
Define the uniform struct on the Rust side with matching layout:
#![allow(unused)] fn main() { #[repr(C)] #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] struct Uniforms { width: u32, height: u32, time: f32, _padding: f32, } impl goldy::StructuredBufferElement for Uniforms {} }
Create the buffer with BufferKind::Scattered so it gets a bindless descriptor:
#![allow(unused)] fn main() { let mut retained_pool = RetainedPool::new(device.clone()); let uniform_buffer = retained_pool.acquire_buffer_with_data( &[Uniforms { width, height, time: 0.0, _padding: 0.0 }], BufferKind::Scattered, )?; }
Compute Pipeline and Scheme
Compile the Slang source, create a ComputePipeline, and record a retained scheme once via SurfaceExchange::bind_destination:
#![allow(unused)] fn main() { let shader = ShaderModule::from_slang(&device, COMPUTE_SHADER)?; let compute_pipeline = ComputePipeline::new(&device, &shader)?; let surface = SurfaceExchange::new_with_depth(&ctx, window.as_ref(), 3, SurfaceConfig::default())?; let mut scheme = Scheme::new(&ctx); let wg_x = width.div_ceil(8); let wg_y = height.div_ceil(8); let (lease, present) = surface.bind_destination(&mut scheme)?; scheme .node("compute", &compute_pipeline) .with_parcel(&uniform_buffer, NodeAccess::Read) .with_present(&lease) .dispatch(wg_x, wg_y, 1); }
Rendering a Frame
Each frame: upload new uniform values via a small upload scheme with a bound deposit, submit the main scheme, claim and consume the surface transaction.
#![allow(unused)] fn main() { fn render_frame(state: &mut RenderState) -> Result<()> { let (width, height) = state.surface.size(); let elapsed = state.start_time.elapsed().as_secs_f32(); let uniforms = Uniforms { width, height, time: elapsed, _padding: 0.0, }; state.uniform_deposit.write( &mut state.upload_scheme, 0, bytemuck::bytes_of(&uniforms), )?; state.upload_scheme.submit()?; let mut submission = state.scheme.submit()?; state.present.claim(&mut submission)?.consume()?; Ok(()) } }
At init, bind the deposit once on a retained upload scheme:
#![allow(unused)] fn main() { let mut upload_scheme = Scheme::new(&ctx); let uniform_deposit = MemoryExchange::new(&ctx).bind_deposit_buffer( &mut upload_scheme, &uniform_buffer, std::mem::size_of::<Uniforms>() as u64, )?; }
Step by Step
Update uniforms — MemoryExchange::bind_deposit_buffer records the upload topology once; each frame call deposit.write on the upload scheme before the main submit.
Record the scheme once — SurfaceExchange::bind_destination registers the present exchange and returns a PresentLease plus a Transaction. scheme.node() creates a compute node bound to a pipeline. with_parcel() declares the uniform buffer dependency. with_present() binds the drawable lease. dispatch() sets the workgroup count.
Submit and present — scheme.submit() records and submits GPU work. present.claim(&mut submission)?.consume() presents the swapchain image. The compute shader already wrote the pixels — there is no blit or copy step.
Run It
cargo run --example compute_to_surface
You should see an animated plasma pattern filling the window, rendered entirely from compute.
Next Steps
- Compute to Surface — present-on-scheme details
- Examples — particles, game of life, and more compute examples
Parcels
A parcel is the unit of data Goldy schemes actually operate on: a whole buffer, a range within a buffer, or a texture. Every resource you acquire from a RetainedPool or a transient allocator hands you one or more parcels, and every with_parcel call on a scheme node passes exactly one.
#![allow(unused)] fn main() { use goldy::{BufferKind, ResourceAccess}; let parcel = retained_pool.acquire_buffer_with_data(&particles, BufferKind::Scattered)?; let handle = parcel.handle(ResourceAccess::Write).unwrap(); let again = parcel.handle(ResourceAccess::Write).unwrap(); assert_eq!(handle, again); }
You never declare layouts, allocate descriptor pools, or manage binding slots yourself. You acquire a parcel, bind it to a node with an access mode, and Goldy figures out the rest at dispatch time.
Categories
Every parcel belongs to one of five categories, matching the shape of access a shader can perform on it:
| Category | What it holds | Shader-side type |
|---|---|---|
Scattered | Read/write structured data | Scattered<T> |
BufRO | Read-only structured data | BufRO<T> |
Broadcast | Small uniform data shared by every invocation | a plain struct parameter |
Interpolated | Sampled texture data | Interpolated<T> |
DirectSpatial | Read/write texture data | DirectSpatial<T> |
Filter | Sampler state | Filter |
A parcel's category is fixed when it's created (BufferKind::Scattered, BufferKind::Broadcast, etc.) and determines which shader-side type it can satisfy. Categories are also independent identity spaces: a Scattered parcel and a Broadcast parcel are unrelated even if they happen to occupy "slot 3" internally — that internal indexing is not something client code ever sees or reasons about.
ResourceHandle and ResourceAccess
ResourceAccess (Read, Write, ReadWrite) describes the kind of access a piece of shader-visible data supports — for example, whether a buffer is exposed to the shader as read-only or read/write. parcel.handle(access) returns a ResourceHandle: an opaque, comparable identity for that parcel/access pair.
ResourceHandle is intentionally opaque. You can compare two handles for equality (useful for deciding whether a retained scheme needs to be re-recorded after a resource was reallocated), but there's nothing else to extract from one — it's an identity, not a number you're meant to interpret.
This is distinct from NodeAccess (Read, Write, ReadWrite, Overwrite), which is what you actually pass to with_parcel. NodeAccess describes how a scheme node uses a parcel for scheduling and hazard tracking (including Overwrite for "I'm replacing this data wholesale, don't preserve prior contents"); ResourceAccess is the narrower, resolved access a shader parameter requires.
Binding Parcels to Schemes
You bind parcels to compute or render nodes with with_parcel, in the same order the shader declares its resource parameters:
#![allow(unused)] fn main() { scheme .node("update", &pipeline) .with_parcel(¶ms_buf, NodeAccess::Read) .with_parcel(&particle_buf, NodeAccess::ReadWrite) .dispatch((particle_count + 63) / 64, 1, 1); }
At dispatch time, Goldy checks each bound parcel's category against what the shader's reflected signature expects. If slot 0 expects Broadcast (from the shader's SimParams params parameter) but you bound a Scattered parcel there, binding fails with a clear error instead of silently producing garbage or undefined behavior.
Typed Resource Parameters in Shaders
On the shader side, goldy_exp provides types that mirror the categories above and map directly to underlying Slang resource types. These appear as ordinary parameters on virtual entry points:
| Goldy Type | Underlying Slang Type | Usage |
|---|---|---|
Scattered<T> | RWStructuredBuffer<T> | Read/write buffer: data[i], data[i].field = v |
BufRO<T> | StructuredBuffer<T> | Read-only buffer: buf[i] |
Interpolated<T> | Texture2D<T> | Sampled texture: tex.Sample(samp, uv) |
DirectSpatial<T> | RWTexture2D<T> | Writable texture: img[int2(x,y)] |
ByteAddress | RWByteAddressBuffer | Raw byte access: .Load(), .Store(), .Interlocked*() |
Filter | SamplerState | Sampler for texture filtering |
Any user-defined struct type (e.g. MyUniforms) declared as a parameter is automatically treated as Broadcast — no wrapper type needed.
Contrast with Traditional Binding
| Traditional (Vulkan/DX12) | Goldy Parcels | |
|---|---|---|
| Setup | Declare descriptor set layouts, allocate pools, create and update descriptor sets | Acquire a parcel; category is fixed at creation |
| Binding | Bind descriptor sets before each draw/dispatch | Pass parcels via with_parcel on scheme nodes |
| Shader access | layout(set=0, binding=1) buffer ... | Scattered<T> data as a function parameter |
| Validation | Runtime errors or silent corruption on mismatch | Category checks at dispatch time |
| Cross-backend | Layout declarations differ per API | Same shader code on Vulkan, DX12, and Metal |
Example: Compute Shader with Parcels
Shader (particle_update.slang):
import goldy_exp;
struct SimParams {
float dt;
uint count;
};
struct Particle {
float2 pos;
float2 vel;
};
[goldy_compute]
[numthreads(64, 1, 1)]
void cs_main(SimParams params, Scattered<Particle> particles, ThreadId id) {
if (id.x >= params.count) return;
Particle p = particles[id.x];
p.pos += p.vel * params.dt;
particles[id.x] = p;
}
Rust dispatch:
#![allow(unused)] fn main() { let params_buf = retained_pool.acquire_buffer_with_data(&[sim_params], BufferKind::Broadcast)?; let particle_buf = retained_pool.acquire_buffer_with_data(&particles, BufferKind::Scattered)?; let shader = ShaderModule::from_slang(&device, PARTICLE_UPDATE_SOURCE)?; let pipeline = ComputePipeline::new(&device, &shader)?; let mut scheme = Scheme::new(&ctx); scheme .node("update", &pipeline) .with_parcel(¶ms_buf, NodeAccess::Read) .with_parcel(&particle_buf, NodeAccess::ReadWrite) .dispatch((particle_count + 63) / 64, 1, 1); scheme.submit()?; }
The shader author writes natural function parameters. The Rust side binds parcels in declaration order via with_parcel. Everything below that — slot packing, descriptor heaps, cross-backend plumbing — is an implementation detail you never need to think about.
Virtual Entry Points
Goldy's virtual entry points let you write shader entry points with clean, typed parameters instead of raw uniform uint slots and SV_* semantics. You annotate your function with [goldy_compute], [goldy_vertex], or [goldy_fragment], and a source-to-source transform generates the real Slang [shader("...")] entry point with all the bindless plumbing wired up.
The Attributes
| Attribute | Stage | Generated Slang Attribute |
|---|---|---|
[goldy_compute] | Compute | [shader("compute")] |
[goldy_vertex] | Vertex | [shader("vertex")] |
[goldy_fragment] | Fragment | [shader("fragment")] |
A minimal example:
import goldy_exp;
[goldy_compute]
[numthreads(64, 1, 1)]
void cs_main(Scattered<uint> data, ThreadId id) {
data[id.x] = data[id.x] * 2;
}
This is equivalent to manually writing a [shader("compute")] entry point with uniform uint push-constant parameters, descriptor heap lookups, and SV_DispatchThreadID — but without any of that boilerplate.
What Virtual Entry Points Accept
Resource Parameters
Each resource parameter occupies one bindless slot (a 16-bit index packed into push constants). The generated wrapper calls the corresponding goldy_* free function to resolve the slot to a live GPU handle.
| Parameter Type | Resolves Via | Description |
|---|---|---|
Scattered<T> | goldy_scattered<T>(slot) | Read/write storage buffer |
BufRO<T> | goldy_buf_ro<T>(slot) | Read-only storage buffer |
Interpolated<T> | goldy_interpolated<T>(slot) | Sampled 2D texture |
DirectSpatial<T> | goldy_direct_spatial<T>(slot) | Read/write 2D texture |
ByteAddress | goldy_byte_address(slot) | Raw byte-address buffer |
Filter | goldy_filter(slot) | Sampler state |
Broadcast Parameters
Any user-defined struct type that isn't a recognized resource or system-value type is treated as a broadcast (constant buffer). The generated code calls goldy_broadcast<T>(slot) to fetch the entire struct from a uniform buffer:
struct SimParams { float dt; uint count; };
[goldy_compute]
[numthreads(64, 1, 1)]
void cs_main(SimParams params, Scattered<Particle> data, ThreadId id) {
// params is fetched from a constant buffer automatically
}
In vertex and fragment shaders, the last unrecognized struct is treated as the stage input (vertex attributes or fragment varyings) rather than a broadcast. All preceding unrecognized structs are broadcasts.
System-Value Parameters
System-value wrapper types are mapped to SV_* semantics. The generated entry point declares the raw semantic parameter and constructs the wrapper:
| Wrapper Type | Maps To | Available Fields |
|---|---|---|
ThreadId | SV_DispatchThreadID | .x, .y, .z, .xy, .xyz |
GroupThreadId | SV_GroupThreadID | .x, .y, .z, .xy, .xyz |
GroupId | SV_GroupID | .x, .y, .z, .xy, .xyz |
VertexId | SV_VertexID | .value |
InstanceId | SV_InstanceID | .value |
IsFrontFace | SV_IsFrontFace | .value |
Scalar Parameters
Plain scalar types (uint, float, int, bool, and vector variants) become user parameters — full-precision u32 words in a separate region of the push constants. Bind them with with_param on the compute node builder (after with_parcel calls for resource params):
#![allow(unused)] fn main() { scheme .node("offset", &pipeline) .with_parcel(&data, NodeAccess::ReadWrite) .with_param(offset) .dispatch(1, 1, 1); }
Pass-Through Parameters
In vertex and fragment shaders, the last unrecognized struct parameter passes through as a stage input (vertex attributes or interpolated varyings). It appears directly in the generated entry point signature without bindless resolution:
[goldy_fragment]
float4 fs_main(MyUniforms cfg, FullscreenVarying input) : SV_Target {
// cfg → broadcast (slot 0)
// input → pass-through stage input (interpolated varyings)
return float4(cfg.time, 0, 0, 1);
}
The Source-to-Source Transform
The transform (implemented in slang/virtual_main.rs) runs before Slang compilation and performs three operations:
- Generates a wrapper function with the real
[shader("...")]attribute and a fixed 16-word push-constant signature. - Renames the user function from
cs_mainto_goldy_user_cs_mainso both can coexist. - Removes the
[goldy_*]attribute and[numthreads]from the renamed user function (they live on the generated wrapper).
Push Constant Layout
The generated entry point always declares a fixed signature regardless of how many parameters the user function has:
Words 0–7: _bw0.._bw7 — 16 × u16 bindless indices packed 2 per word
Words 8–15: _uw0.._uw7 — 8 × u32 user scalar parameters
Bindless indices are packed as pairs into 32-bit words: the low 16 bits of _bw0 hold slot 0, the high 16 bits hold slot 1, and so on. This fits up to 16 resource/broadcast parameters and 8 scalar parameters in 64 bytes of push constants.
Before and After
What you write:
[goldy_compute]
[numthreads(64, 1, 1)]
void cs_main(TimeUniforms cfg, Scattered<uint> data, ThreadId id) {
data[id.x] = data[id.x] + cfg.base;
}
What gets compiled (generated wrapper prepended, user function renamed):
[shader("compute")]
[numthreads(64, 1, 1)]
void cs_main(uniform uint _bw0, ..., uniform uint _bw7,
uniform uint _uw0, ..., uniform uint _uw7,
uint3 _sv0 : SV_DispatchThreadID) {
TimeUniforms cfg = goldy_broadcast<TimeUniforms>(_bw0 & 0xFFFFu);
Scattered<uint> data = goldy_scattered<uint>((_bw0 >> 16u) & 0xFFFFu);
ThreadId id = ThreadId(_sv0);
_goldy_user_cs_main(cfg, data, id);
}
// Original function, renamed:
void _goldy_user_cs_main(TimeUniforms cfg, Scattered<uint> data, ThreadId id) {
data[id.x] = data[id.x] + cfg.base;
}
The #line 1 directive is inserted between the generated wrapper and the user source so that compiler diagnostics report correct line numbers.
Vertex/Fragment Example
[goldy_vertex]
VSOutput vs_main(SceneUniforms scene, Scattered<Instance> instances, VertexId vid, InstanceId iid) {
// scene → broadcast (slot 0)
// instances → scattered (slot 1)
// vid → SV_VertexID
// iid → SV_InstanceID
Instance inst = instances[iid.value];
VSOutput out;
// ... transform vertex ...
return out;
}
[goldy_fragment]
float4 fs_main(SceneUniforms scene, Interpolated<float4> albedo, Filter samp,
VSOutput input) : SV_Target {
// scene → broadcast (slot 0)
// albedo → texture (slot 1)
// samp → sampler (slot 2)
// input → pass-through stage varying
return albedo.Sample(samp, input.uv) * scene.tint;
}
Both entry points share the same push-constant layout. Fragment shader slot expectations take precedence when Goldy extracts category metadata (since resource binding typically lives there in a vertex+fragment pair).
Preprocessor Conditionals
Virtual entry points support #ifdef/#else/#endif blocks directly inside the parameter list. This is useful for shader variants like MSAA:
[goldy_compute]
[numthreads(4, 16, 1)]
void cs_main(BufRO<uint> config,
#ifdef msaa
BufRO<uint> mask_lut, DirectSpatial<float4> out_tex,
#else
DirectSpatial<float4> out_tex,
#endif
ThreadId tid) {
// ...
}
The transform generates conditional blocks in the wrapper's signature, body, and call arguments so that the correct branch is selected at compile time based on preprocessor defines.
Rust Compute Kernels
Goldy can lower a restricted Rust GPU dialect into canonical [goldy_compute]
Slang at compile time, then prepare and record through the normal Scheme path.
This is the initial design for issue #78. It is not arbitrary Rust, a second
runtime compiler, or CUDA <<<>>> syntax. Slang remains the runtime backend
compiler; the proc-macro is an AOT frontend that produces structured
KernelDef metadata and typed record helpers.
To step the same kernel on the CPU without a handwritten Rust twin, see CPU host-callable shaders (issue #292).
Quick example
#![allow(unused)] fn main() { use goldy::gpu; #[goldy::compute(workgroup_size = [256, 1, 1])] fn saxpy(x: &[f32], y: &mut [f32], a: f32) { let i = gpu::global_id().x; if i < y.len() { y[i] = a * x[i] + y[i]; } } // Host: let kernel = saxpy::Kernel::prepare(&device)?; kernel .record(&mut scheme, "saxpy", &x, &y, a) .over_1d(n); // or exact grid counts: kernel .record(&mut scheme, "saxpy", &x, &y, a) .groups([n.div_ceil(256), 1, 1]); }
prepare compiles (or hits the shader cache) once. record only appends Scheme
topology — it does not launch into a stream. Use use goldy::gpu; (or
goldy::gpu::global_id()) for builtins.
Signature mapping
| Rust parameter | Slang / Scheme |
|---|---|
&[T] | BufRO<T>, NodeAccess::Read |
&mut [T] | Scattered<T>, NodeAccess::ReadWrite |
gpu::Out<T> | Scattered<T>, NodeAccess::Write |
gpu::Uniform<T> | broadcast resource, NodeAccess::Read |
u32 / i32 / f32 / bool | typed scalar push words (no manual to_bits) |
Hidden builtins (appended to the Slang signature when used):
| Rust | Slang |
|---|---|
gpu::global_id() | ThreadId |
gpu::local_id() | GroupThreadId |
gpu::workgroup_id() | GroupId |
workgroup_size is fixed on the attribute / KernelDef. .groups / .over_*
only control the grid. A different workgroup size is a different pipeline.
Architecture
Rust kernel
│
▼
goldy_derive::compute
├── syn AST validation (GPU dialect)
├── goldy_shader_ir
├── canonical [goldy_compute] Slang
└── KernelDef / KernelParam ABI
│
▼
Kernel::prepare(device)
└── existing ShaderModule + ComputePipeline + cache
│
▼
typed record() → SchemeNodeBuilder bindings in declaration order
Raw hand-written [goldy_compute] shaders continue to work. Simple sources can
also be parsed into the same KernelDef shape via
goldy::slang::try_kernel_def_from_source, and wrappers can be emitted from ABI
metadata with emit_wrapper_from_kernel_def so both paths share frame-table /
PushLayout lowering.
Supported dialect (MVP)
Allowed: scalar arithmetic/comparisons, let / let mut, assignment,
field/index access, if/else, while, for i in 0..n, casts, selected math
intrinsics (abs/min/max/floor/ceil/sqrt), buffer .len(), return,
and the ID builtins above.
Rejected with span diagnostics: allocation, iterators/closures, traits/dyn,
recursion, async, panics, arbitrary std calls, usize/isize, references
except resource parameters, and unsupported patterns.
Element types for buffer slices are currently u32 / i32 / f32 / bool.
Diagnostics and dumps
- Proc-macro errors at Rust compile time for unsupported syntax.
- Slang / pipeline errors during
Kernel::prepare. - Backend errors after target compilation.
Set GOLDY_DUMP_RUST_KERNELS=1 (or a directory path) to dump canonical Slang and
ABI metadata at prepare time.
Out of scope (later)
Graphics stages, GpuType derive, CUDA scalar parity polish, dynamic shared
memory, specialization, and broad Rust compatibility belong to later phases /
the wider goldy-jit roadmap.
Slang in One Source
Goldy uses Slang as its single shader language across all backends. You write one .slang file and Goldy compiles it to the native format for whichever GPU API is in use — no manual HLSL/GLSL/MSL translation, no per-backend shader files.
Compilation Targets
| Backend | Target Format | API Requirement | Status |
|---|---|---|---|
| Vulkan | SPIR-V | Vulkan 1.4+ | Shipped |
| DirectX 12 | DXIL | Windows 10+ | Shipped |
| Metal | Metal IR | Metal Tier 2+ (Argument Buffers) | Shipped |
| CUDA | PTX | NVIDIA CUDA | In progress |
| WebGPU | WGSL | WebGPU (via wgpu) | In progress |
Slang compiles through its native slang.dll / libslang.dylib — the same compiler used by NVIDIA, Khronos, and major game engines. Goldy links it directly; there is no intermediate translation step.
Why Slang
- One source: Vertex, fragment, and compute shaders all live in a single
.slangfile. No preprocessor gymnastics to target different backends. - HLSL-compatible syntax: If you know HLSL, you already know Slang. Standard types (
float4,uint3,Texture2D), standard intrinsics (mul,lerp,smoothstep), standard semantics (SV_Position,SV_Target). - Modern language features: Modules (
import), generics, interfaces, operator overloading, and automatic differentiation — features that HLSL and GLSL lack. - Khronos governance: Long-term stability under open-source stewardship.
Cross-Backend Matrix Layout Consistency
Slang normalizes matrix layout across all backends. HLSL defaults to column-major storage, GLSL to column-major, and Metal to column-major — but the conventions for how mul(matrix, vector) is interpreted differ. Slang's compilation ensures that a float4x4 in your shader has identical memory layout and multiplication semantics whether it compiles to SPIR-V, DXIL, or Metal IR.
This means your Rust-side #[repr(C)] matrix types can use the same byte layout regardless of which backend the application runs on.
Shader Module Creation
Basic Compilation
ShaderModule::from_slang() compiles a Slang source string into GPU bytecode:
#![allow(unused)] fn main() { let shader = ShaderModule::from_slang(&device, r#" import goldy_exp; [goldy_compute] [numthreads(64, 1, 1)] void cs_main(Scattered<float> data, ThreadId id) { data[id.x] = data[id.x] * 2.0; } "#)?; }
The goldy_exp library is pre-registered on every device — import goldy_exp works without any setup.
Additional Search Paths
ShaderModule::from_slang_with_paths() adds filesystem directories to the Slang module search path:
#![allow(unused)] fn main() { let shader = ShaderModule::from_slang_with_paths( &device, source, &["my_project/shaders"], )?; }
Preprocessor Defines
ShaderModule::from_slang_with_paths_and_defines() passes preprocessor defines for shader variants:
#![allow(unused)] fn main() { let shader = ShaderModule::from_slang_with_paths_and_defines( &device, source, &[], &[("msaa", "1"), ("SAMPLE_COUNT", "4")], )?; }
Full Options
ShaderModule::from_slang_with_options() provides complete control — search paths, defines, optimization level, and layout validation checks:
#![allow(unused)] fn main() { let shader = ShaderModule::from_slang_with_options( &device, source, &["shaders/"], &[("DEBUG", "1")], OptimizationLevel::Default, &[TimeUniforms::LAYOUT_CHECK], )?; }
Built-in Shader Modules
Goldy ships a few complete shaders as Rust string constants in goldy::shader::builtins:
| Constant | Description |
|---|---|
VERTEX_COLOR_2D | 2D vertex+fragment shader with per-vertex color |
SOLID_COLOR | Solid color fragment shader with a uniform |
These are self-contained (no import needed) and useful for bootstrapping:
#![allow(unused)] fn main() { use goldy::shader::builtins; let shader = ShaderModule::from_slang(&device, builtins::VERTEX_COLOR_2D)?; }
Shader Libraries
Shader libraries are reusable Slang modules registered with a Device. Once registered, any shader compiled on that device can import the library.
The Built-in goldy_exp Library
Every device comes with goldy_exp pre-registered. It provides:
- Resource type aliases (
Scattered<T>,BufRO<T>,Interpolated<T>, etc.) - System-value wrappers (
ThreadId,VertexId,InstanceId, etc.) - Vertex formats (
FullscreenVarying,ColoredVarying, etc.) - Math utilities (
hash(),center_uv(),smootherstep(), etc.) - Color utilities (
rainbow(),palette(),hsv_to_rgb(), etc.) - Procedural geometry (
quad_position(),billboard_position(), etc.)
Registering Custom Libraries
#![allow(unused)] fn main() { use goldy::ShaderLibrary; device.register_library(ShaderLibrary::from_source("myutils", r#" module myutils; public float3 my_effect(float t) { return float3(t, t * 0.5, 1.0 - t); } "#))?; }
Now any shader can import myutils:
import myutils;
[goldy_fragment]
float4 fs_main(FullscreenVarying input) : SV_Target {
return float4(my_effect(input.uv.x), 1.0);
}
Multi-Module Libraries
For larger libraries with internal sub-modules:
#![allow(unused)] fn main() { let lib = ShaderLibrary::from_embedded("effects", &[ ("effects", r#" module effects; __include "effects/blur"; __include "effects/bloom"; "#), ("effects/blur", r#" implementing effects; public float4 gaussian_blur(Texture2D<float4> tex, SamplerState s, float2 uv) { ... } "#), ("effects/bloom", r#" implementing effects; public float4 bloom(Texture2D<float4> tex, SamplerState s, float2 uv, float threshold) { ... } "#), ]); device.register_library(lib)?; }
Loading from the Filesystem
#![allow(unused)] fn main() { let lib = ShaderLibrary::from_directory("effects", Path::new("shaders/effects/"))?; device.register_library(lib)?; }
Library Management
#![allow(unused)] fn main() { device.has_library("goldy_exp"); // true — always registered device.list_libraries(); // ["goldy_exp", "myutils", ...] device.unregister_library("myutils"); // remove a custom library }
Layout Validation
When Rust structs are passed to shaders as uniform data (e.g. via Broadcast), the memory layout must match exactly. Goldy can validate this at compile time using Slang reflection.
Setup
- Derive
LayoutCheckableon your Rust struct:
#![allow(unused)] fn main() { #[derive(LayoutCheckable)] #[repr(C)] struct TimeUniforms { time: f32, delta_time: f32, frame: u32, _pad: u32, } }
- Pass the layout check to shader compilation:
#![allow(unused)] fn main() { let shader = ShaderModule::from_slang_with_options( &device, source, &[], &[], OptimizationLevel::Default, &[TimeUniforms::LAYOUT_CHECK], )?; }
- Enable validation via environment variable:
GOLDY_VALIDATE_LAYOUTS=1 cargo run
# or
GOLDY_VALIDATION=layout cargo run
# or enable everything:
GOLDY_VALIDATION=all cargo run
What Gets Validated
- Field offsets: Each field's byte offset in the Rust struct is compared against the Slang reflection data.
- Struct size: Total size must match.
- Buffer element stride: At dispatch time, the buffer's recorded element stride is checked against what the shader expects.
Validation is zero-cost when disabled — the checks are skipped entirely, not compiled out. The environment variable is read at runtime so it can be toggled without recompiling.
GOLDY_VALIDATION
The GOLDY_VALIDATION environment variable controls multiple validation categories:
| Value | Layout Checks | GPU API Validation |
|---|---|---|
layout | Yes | No |
api | No | Yes |
layout,api | Yes | Yes |
all | Yes | Yes |
1 / true / yes | No | Yes |
GOLDY_VALIDATE_LAYOUTS=1 is a standalone toggle that enables layout checks regardless of GOLDY_VALIDATION.
Settlement
Goldy makes GPU completion observable as settlement of concrete objects — submissions, parcels, and exchange claims — not as raw timeline numbers.
Internal clearing still uses a monotonic device clock. That clock is crate-private. Clients wait for work or resources to settle.
Submission settlement
Every successful Scheme::submit
returns a Submission:
#![allow(unused)] fn main() { let submission = scheme.submit()?; if !submission.is_settled() { submission.wait_until_settled()?; } }
Bounded wait:
#![allow(unused)] fn main() { let done = submission.wait_until_settled_timeout(1000)?; // milliseconds if !done { // GPU has not finished yet } }
The submission owns the context it was submitted on; callers do not pass a Context to wait.
Parcel and resource settlement
Before reusing or dropping a resource that may still be referenced by in-flight GPU work:
#![allow(unused)] fn main() { if !parcel.is_settled() { parcel.wait_until_settled()?; } }
The same methods exist on Buffer
and Texture.
Direct host writes on CPU-writable buffers require the buffer to be settled (or never
GPU-referenced). Prefer MemoryExchange deposits for uploads.
Exchange claims (unchanged)
Surface and memory exchanges still settle occurrences via consume/discard:
#![allow(unused)] fn main() { let mut submission = scheme.submit()?; // Present transaction.claim(&mut submission)?.consume()?; // Readback — consume waits for the submission internally let bytes = withdraw.claim(&mut submission)?.consume()?; }
A live linear claim is unsettled until consume or discard. Dropping an unsettled claim
discards it.
Multi-frame pipelining
For production renderers, use FrameOrchestrator. It bounds
CPU/GPU depth using submissions — not raw epochs:
#![allow(unused)] fn main() { let mut orch = FrameOrchestrator::new(&ctx, 3); loop { let handle = orch.begin_frame()?; let submission = scheme.submit()?; orch.end_frame_standalone(handle, &submission)?; } orch.drain_all()?; }
How this differs from fence-based APIs
Traditional GPU APIs expose fence objects or timeline counters to the application.
Goldy keeps those as runtime clearing instruments (finance analogy: sequence numbers in
a clearinghouse). Application code holds receipts (Submission) and parcels
(Parcel) and asks when those are settled.
| Fence / timeline counter | Settlement | |
|---|---|---|
| Query | Poll a fence or compare u64 | obj.is_settled() |
| Wait | Wait on fence / wait_until(tv) | obj.wait_until_settled() |
| Identity | Opaque fence or epoch number | Concrete submission or parcel |
| Portability | Tied to native timeline primitives | Backend may use fences, events, or onSubmittedWorkDone |
Resource lifetime
Dropping a Buffer or Texture may be deferred internally until GPU work that referenced
it has retired. Prefer settling before dropping when you need deterministic reclaim timing
(for example Metal heap-sensitive resize paths).
Pipelined Frames
Goldy's FrameOrchestrator manages CPU/GPU frame pacing: an in-flight ring,
a depth cap, and present-path settlement patching. It does not own GPU
bytes or run cleanup callbacks — recycle lives in TransientPool
and RetainedPool.
The problem it solves
Every pipelined renderer needs the same bookkeeping:
- A ring of in-flight frame receipts.
- A pipeline-depth cap — block the CPU when the ring is full to prevent unbounded memory growth.
- Deferred retirement of slots when their submissions settle.
- Present-path patching — stamp the most recent slot after
Claim::consumevianote_presented.
Without shared infrastructure, every consumer reimplements this independently. FrameOrchestrator centralizes the pacing half of it.
Core API
#![allow(unused)] fn main() { use goldy::{FrameOrchestrator, FrameHandle}; // max_depth: how many frames may be in-flight before begin_frame blocks let mut orch = FrameOrchestrator::new(&ctx, 3); }
Standalone (headless / render-to-texture) path
#![allow(unused)] fn main() { loop { // 1. Open a new frame slot; drains completed older slots. // Blocks if max_depth frames are already in flight. let handle = orch.begin_frame()?; // 2. Submit retained scheme work (recorded earlier or this frame). let submission = scheme.submit()?; // 3. Register the slot from the submission. orch.end_frame_standalone(handle, &submission)?; } }
Present-on-scheme (swapchain) path
#![allow(unused)] fn main() { loop { let handle = orch.begin_frame()?; let mut submission = scheme.submit()?; present.claim(&mut submission)?.consume()?; orch.end_frame_for_present(handle, &submission)?; orch.note_presented(&submission); } }
Externally ordered path
When scheme submit sidecars / present easement already enforce cross-frame
ordering, close with end_frame_externally_ordered so no ring slot is created
and the next begin_frame does not wait on a coarse frame timeline.
Mid-frame submit boundaries
Split a frame into multiple scheme submissions so the GPU can begin earlier phases while the CPU records later ones:
#![allow(unused)] fn main() { let handle = orch.begin_frame()?; // Coarse phase let _coarse = coarse_scheme.submit()?; // Fine phase — GPU executes coarse while CPU records/submits this let fine = fine_scheme.submit()?; orch.end_frame_standalone(handle, &fine)?; }
Each Scheme::submit creates a real command-buffer boundary on all backends. Because Metal (and Vulkan/DX12) execute command buffers on the same queue in submission order, the fine submission automatically waits for the coarse one — no explicit fence is required.
CPU/GPU overlap
FrameOrchestrator enables two distinct layers of CPU/GPU overlap:
Frame-level — begin_frame drains completed slots without blocking when under the depth cap, so the CPU can start recording frame N+1 while the GPU executes frame N. The depth cap (max_depth) prevents the CPU from running too far ahead.
Intra-frame — multiple scheme.submit() calls in one frame split the command stream into multiple GPU submissions. The GPU starts executing the first submission before the CPU finishes the last one.
Inspecting orchestrator state
#![allow(unused)] fn main() { orch.pending_frames(); // slots currently in the ring orch.max_depth(); // cap configured at construction orch.has_open_frame(); // true between begin_frame and end_frame_* }
Under allocation pressure, orch.wait_for_progress() blocks on the oldest ring
slot (or flushes deferred deletions when the ring is empty).
Design notes
Present path settlement is always deferred
On the swapchain path the final scanout settlement may arrive only after Claim::consume. The orchestrator holds the slot unset until note_presented arrives.
Relationship to resource recycling
FrameOrchestrator owns the frame-slot ring only. Transient buffer/texture recycling lives in the per-context TransientPool (leases via acquire_transient_* / return_transient_*). They are independent: the orchestrator does not call into the pool, and clients must not hang byte reclaim on orchestrator callbacks (there are none).
Compute to Surface
Compute-to-surface lets a compute shader write directly to a swapchain drawable, bypassing the rasterization pipeline entirely. There is no RenderPipeline, no vertex buffers, and no raster pass — just a compute dispatch that fills pixels.
When to use compute-to-surface
Use compute-to-surface when your rendering is naturally a per-pixel computation rather than geometry rasterization:
- Fullscreen image effects (plasma, fractals, ray marching)
- GPU-driven 2D renderers where the compute shader owns the output layout
- Post-processing that doesn't need triangle rasterization
- Prototyping visual effects without setting up a render pipeline
Use traditional rendering when you need the rasterization pipeline's features: triangle assembly, depth testing, MSAA, alpha blending, or vertex/fragment shader stages.
Surface exchange
Create a SurfaceExchange and call bind_destination to register direct compute-to-present in the scheme:
#![allow(unused)] fn main() { let surface = SurfaceExchange::new_with_depth(&ctx, &window, 3, SurfaceConfig::default())?; let (lease, present) = surface.bind_destination(&mut scheme)?; }
Bind the returned lease in a compute node with with_present(&lease). Goldy handles barrier insertion between compute writes and the presentation engine.
On CUDA+DX12, present scratch is a depth-3 imported staging ring (not the DXGI backbuffer). Compute does not reuse frame N's scratch in frame N+1; that extra memory is the interop tradeoff until CUDA/DX12 synchronization APIs improve.
Building the scheme
Record a retained scheme with a compute node that writes to the present lease:
#![allow(unused)] fn main() { let wg_x = width.div_ceil(8); let wg_y = height.div_ceil(8); let mut scheme = Scheme::new(&ctx); let (lease, present) = surface.bind_destination(&mut scheme)?; scheme .node("compute", &compute_pipeline) .with_parcel(&uniform_buffer, NodeAccess::Read) .with_present(&lease) .dispatch(wg_x, wg_y, 1); }
Submitting and presenting
Each frame, submit the scheme and consume the surface claim:
#![allow(unused)] fn main() { let mut submission = scheme.submit()?; present.claim(&mut submission)?.consume()?; }
submit resolves transient resources, compiles the scheme into a command stream, and submits to the GPU. Presentation happens when you call claim(...).consume() — the compute shader has already written the pixels.
The compute shader
The shader receives the output texture as a DirectSpatial<float4> — a read-write 2D texture accessed by integer coordinates:
import goldy_exp;
struct Uniforms {
uint width;
uint height;
float time;
float _padding;
};
[goldy_compute]
[numthreads(8, 8, 1)]
void cs_main(BufRO<Uniforms> uniforms_buf, DirectSpatial<float4> output, ThreadId tid) {
Uniforms u = uniforms_buf[0];
if (tid.x >= u.width || tid.y >= u.height)
return;
float2 uv = float2(float(tid.x) / float(u.width),
float(tid.y) / float(u.height));
// Compute pixel color...
float3 col = my_color_function(uv, u.time);
output[tid.xy] = float4(col, 1.0);
}
The [numthreads(8, 8, 1)] workgroup size maps naturally to 2D image tiles. Dispatch enough workgroups to cover the full resolution:
#![allow(unused)] fn main() { let wg_x = width.div_ceil(8); let wg_y = height.div_ceil(8); }
Guard against out-of-bounds writes in the shader when the resolution isn't a multiple of the workgroup size.
Full example sketch
#![allow(unused)] fn main() { use goldy::{ BufferKind, ComputePipeline, DeviceDescriptor, Instance, MemoryExchange, NodeAccess, PresentMode, RequestAdapterOptions, RetainedPool, Scheme, ShaderModule, SurfaceConfig, SurfaceExchange, }; let instance = Instance::new()?; let device = instance .request_adapter(&RequestAdapterOptions::default())? .request_device(&DeviceDescriptor::default())?; let ctx = device.create_context()?; let surface = SurfaceExchange::new_with_config( &ctx, &window, SurfaceConfig { present_mode: PresentMode::Fifo, depth_format: None, }, )?; let shader = ShaderModule::from_slang(&device, COMPUTE_SHADER)?; let compute_pipeline = ComputePipeline::new(&device, &shader)?; let mut retained_pool = RetainedPool::new(device.clone()); let uniform_buffer = retained_pool.acquire_buffer_with_data( &[Uniforms { width, height, time: 0.0, _padding: 0.0 }], BufferKind::Scattered, )?; let mut scheme = Scheme::new(&ctx); let (lease, present) = surface.bind_destination(&mut scheme)?; scheme .node("compute", &compute_pipeline) .with_parcel(&uniform_buffer, NodeAccess::Read) .with_present(&lease) .dispatch(width.div_ceil(8), height.div_ceil(8), 1); // --- Render loop --- let mut upload = Scheme::new(&ctx); let uniform_deposit = MemoryExchange::new(&ctx).bind_deposit_buffer( &mut upload, &uniform_buffer, std::mem::size_of::<Uniforms>() as u64, )?; uniform_deposit.write( &mut upload, 0, bytemuck::bytes_of(&Uniforms { width, height, time: elapsed, _padding: 0.0 }), )?; upload.submit()?; let mut submission = scheme.submit()?; present.claim(&mut submission)?.consume()?; }
See examples/compute_to_surface.rs for the complete winit application.
Pipelines
Pipelines combine compiled shaders with fixed-function rendering state. Goldy provides RenderPipeline for graphics and ComputePipeline for compute workloads.
Render Pipelines
A RenderPipeline pairs vertex and fragment shaders with a RenderPipelineDesc that configures vertex input, primitive assembly, depth testing, and the output format.
Creating a Render Pipeline
#![allow(unused)] fn main() { use goldy::{ RenderPipeline, RenderPipelineDesc, ShaderModule, Vertex2D, TextureFormat, PrimitiveTopology, }; let vs = ShaderModule::from_slang(&device, include_str!("shaders/tri.vs.slang"))?; let fs = ShaderModule::from_slang(&device, include_str!("shaders/tri.fs.slang"))?; let pipeline = RenderPipeline::new(&device, &vs, &fs, &RenderPipelineDesc { vertex_layout: Vertex2D::layout(), topology: PrimitiveTopology::TriangleList, target_format: surface.format(), depth_stencil: None, })?; }
RenderPipelineDesc
#![allow(unused)] fn main() { pub struct RenderPipelineDesc { pub vertex_layout: VertexBufferLayout, pub topology: PrimitiveTopology, pub target_format: TextureFormat, pub depth_stencil: Option<DepthStencilState>, } }
| Field | Purpose | Default |
|---|---|---|
vertex_layout | Describes vertex buffer stride and attributes | Empty (no vertex input) |
topology | How vertices are assembled into primitives | TriangleList |
target_format | Pixel format of the render target — must match surface.format() or the format passed to RenderTarget::new() | Rgba8Unorm |
depth_stencil | Depth/stencil test configuration, or None to disable | None |
The default descriptor is valid for fullscreen passes that generate geometry from SV_VertexID and render to an Rgba8Unorm target without depth testing.
Format Matching
The pipeline's target_format must match the render target it will draw into. Mismatched formats produce backend errors or undefined output.
#![allow(unused)] fn main() { let desc = RenderPipelineDesc { target_format: surface.format(), ..Default::default() }; }
Vertex Buffer Layouts
A VertexBufferLayout tells the pipeline how to interpret vertex buffer memory. For passes that do not use vertex buffers (fullscreen triangles, quad instancing), the default empty layout is correct.
For typed vertex input, use the from_formats builder or a built-in type's layout() method. See Vertex Types and Layouts for details.
#![allow(unused)] fn main() { let layout = VertexBufferLayout::from_formats::<MyVertex>(&[ VertexFormat::Float32x3, // position VertexFormat::Float32x2, // uv ]); }
Primitive Topology
Controls how the vertex stream is assembled into geometric primitives:
#![allow(unused)] fn main() { pub enum PrimitiveTopology { PointList, LineList, LineStrip, TriangleList, // default TriangleStrip, } }
PointList: • • • •
LineList: •——• •——•
LineStrip: •——•——•——•
TriangleList: △ △
TriangleStrip: △▽△▽
Depth/Stencil State
Enable depth testing by setting depth_stencil. The surface or render target must have been created with a matching depth format.
#![allow(unused)] fn main() { use goldy::{DepthStencilState, DepthFormat, CompareFunction}; let pipeline = RenderPipeline::new(&device, &vs, &fs, &RenderPipelineDesc { vertex_layout: Vertex2D::layout(), target_format: surface.format(), topology: PrimitiveTopology::TriangleList, depth_stencil: Some(DepthStencilState { format: DepthFormat::Depth32Float, depth_write_enabled: true, depth_compare: CompareFunction::Less, }), })?; }
DepthStencilState fields:
| Field | Purpose | Default |
|---|---|---|
format | Depth texture format (Depth16Unorm, Depth24Plus, Depth32Float, etc.) | Depth24Plus |
depth_write_enabled | Whether fragments write to the depth buffer | true |
depth_compare | Comparison function — Less, LessEqual, Greater, Always, etc. | Less |
Available depth formats:
| Format | Bits | Stencil |
|---|---|---|
Depth16Unorm | 16-bit | No |
Depth24Plus | 24-bit (may use 32 internally) | No |
Depth24PlusStencil8 | 24-bit + 8-bit stencil | Yes |
Depth32Float | 32-bit float | No |
Depth32FloatStencil8 | 32-bit float + 8-bit stencil | Yes |
For reverse-Z rendering, use CompareFunction::Greater and clear depth to 0.0.
Compute Pipelines
ComputePipeline wraps a single compute shader. See the compute documentation for the full compute API.
#![allow(unused)] fn main() { use goldy::{ComputePipeline, ShaderModule}; let cs = ShaderModule::from_slang(&device, include_str!("shaders/sim.cs.slang"))?; let pipeline = ComputePipeline::new(&device, &cs)?; }
Why Goldy Has Fewer Pipelines
Pipeline State Object (PSO) explosion is one of the biggest pain points in modern graphics. Engines routinely manage thousands of pipeline permutations and ship massive shader caches. Goldy eliminates most combinatorial dimensions:
| Dimension | Traditional Vulkan/DX12 | Goldy |
|---|---|---|
| Render pass compatibility | N render passes × M subpasses | Eliminated — dynamic rendering |
| Descriptor set layouts | Per-material layout permutations | One global bindless layout |
| Pipeline layouts | Per-material | One shared layout |
| Viewport / scissor | Baked into PSO | Dynamic state |
| Vertex format | Baked | Baked (unavoidable) |
| Target format | Baked | Baked (unavoidable) |
RenderPipelineDesc has exactly four fields. The permutation space is vertex_layouts × topologies × target_formats × depth_configs — deliberately small.
Performance
Pipelines are expensive to create (shader compilation, PSO allocation) but cheap to bind during rendering. Create them once at startup and reuse across frames.
#![allow(unused)] fn main() { struct Renderer { scene_pipeline: RenderPipeline, ui_pipeline: RenderPipeline, wireframe_pipeline: RenderPipeline, } impl Renderer { fn new(device: &Device, surface: &Surface) -> Result<Self> { // Create all pipelines upfront Ok(Self { scene_pipeline: create_scene_pipeline(device, surface.format())?, ui_pipeline: create_ui_pipeline(device, surface.format())?, wireframe_pipeline: create_wireframe_pipeline(device, surface.format())?, }) } } }
Render Pass Nodes
Goldy has no command buffers and no command lists. A graphics draw is a render pass node inside a Scheme — the same retained dependency graph that holds compute dispatches, copies, and present nodes. scheme.render_pass(...) returns a builder; what you call on that builder is recorded into the node, not executed immediately. Nothing touches the GPU until scheme.submit().
This matters for how you think about the API: there is no "encoder" you open and close per frame. You build the graph once — typically at init and on resize — and resubmit it every frame. See Settlement for what happens after submit(), and Pipelines for how RenderPipeline fits into a pass.
Recording a Render Pass Node
scheme.render_pass(label, target, color_load) opens a builder bound to one leased render target:
#![allow(unused)] fn main() { use goldy::{Color, NodeAccess, Scheme, TargetLoad}; let mut pass = scheme.render_pass("triangle", &scene_rt, TargetLoad::Clear(Color::CORNFLOWER_BLUE)); pass.with_parcel(&vertex_buffer, NodeAccess::Read); pass.set_pipeline(&pipeline); pass.set_vertex_buffer(0, &vertex_buffer); pass.draw(0..3, 0..1); pass.finish(); }
finish() pushes the node into the scheme's graph. The builder cannot be reused after finish() — record a new pass for the next node.
Color Load
Load behavior is a property of the pass node, not a separate clear call:
| Variant | Effect |
|---|---|
TargetLoad::Load | Preserve prior color contents (the node reads the target) |
TargetLoad::Clear(color) | Clear to color at pass start (private-inaugural — the node owns the target outright) |
TargetLoad::Discard | Prior contents are irrelevant; the pass must fully overwrite every pixel |
This is a scheduling input, not cosmetic: Clear/Discard tell Goldy the pass does not depend on the target's previous contents, which affects how the runtime orders and aliases transient render targets across the scheme.
Depth
#![allow(unused)] fn main() { pass.clear_depth(1.0); }
Depth clear is declared the same way — as part of the node, before drawing.
Declaring Dependencies
A render pass node participates in the scheme's dependency graph the same way a compute node does. Declare every parcel it reads or writes so Goldy can derive barriers and track parcel lifetimes:
#![allow(unused)] fn main() { pass.with_parcel(&vertex_buffer, NodeAccess::Read); pass.with_parcel(&uniform_buf, NodeAccess::Read); }
with_parcel also registers the parcel for typed bindless binding, in call order, the next time set_pipeline is called — so declare dependencies for a draw before calling set_pipeline for it.
For a Buffer you want to depend on without binding it as a shader resource (e.g. a geometry buffer accessed only through set_vertex_buffer/set_index_buffer), use with_buffer_dependency instead — it registers the dependency without claiming a bindless slot:
#![allow(unused)] fn main() { pass.with_buffer_dependency(&geometry, NodeAccess::Read); }
Pipeline, Buffers, and Drawing
#![allow(unused)] fn main() { pass.set_pipeline(&pipeline); pass.set_vertex_buffer(0, &vertex_buffer); pass.set_index_buffer(&indices, IndexFormat::Uint16); pass.draw(0..3, 0..1); // non-indexed: vertex range, instance range pass.draw_indexed(0..6, 0..1); // indexed: index range, instance range pass.draw_fullscreen(); // shorthand for draw(0..3, 0..1) }
set_pipeline binds a RenderPipeline and, if any parcels were declared with with_parcel beforehand, resolves and binds their bindless handles for that pipeline's typed shader parameters. Calling set_pipeline again mid-pass starts a new binding scope for subsequent draws — declare each draw's parcels right before the set_pipeline call that will consume them.
For fullscreen or procedurally-generated geometry (no vertex buffer at all), skip set_vertex_buffer entirely and generate positions from SV_VertexID in the shader — see Vertex Types and Layouts.
Offscreen-Only (Tests, Readback)
Headless rendering — no window, no SurfaceExchange — records the same render pass node, then withdraws pixels through MemoryExchange:
#![allow(unused)] fn main() { let memory = MemoryExchange::new(&ctx); let mut scheme = Scheme::new(&ctx); let rt = scheme.lease_render_target(800, 600, TextureFormat::Rgba8Unorm, None)?; let mut pass = scheme.render_pass("clear", &rt, TargetLoad::Clear(Color::RED)); pass.finish(); scheme.copy_to_texture(&rt, &readback_texture); let withdraw = memory.bind_withdraw(&mut scheme, &readback_texture)?; let mut submission = scheme.submit()?; let pixels = withdraw.claim(&mut submission)?.consume()?; }
Windowed Rendering
A windowed frame is the same render pass node, plus a present binding recorded once against the scheme via SurfaceExchange:
#![allow(unused)] fn main() { use goldy::{Color, NodeAccess, Scheme, SurfaceExchange, TargetLoad}; // Record once, at init and on resize: let mut pass = scheme.render_pass("main", &scene_rt, TargetLoad::Clear(Color::CORNFLOWER_BLUE)); pass.with_parcel(&vertex_buffer, NodeAccess::Read); pass.set_pipeline(&pipeline); pass.set_vertex_buffer(0, &vertex_buffer); pass.draw(0..3, 0..1); pass.finish(); let present = surface.bind_render_target(&mut scheme, &scene_rt)?; // Each frame: let mut submission = scheme.submit()?; present.claim(&mut submission)?.consume()?; }
The graph is recorded once; every frame just resubmits it and settles the present claim. See examples/triangle.rs for the full loop, including resize handling (rebuild the scheme and transaction when the surface size changes).
Compute and Graphics in One Scheme
Because render pass nodes and compute nodes live in the same graph, a hybrid frame is just multiple node(...) and render_pass(...) calls on one Scheme, submitted together:
#![allow(unused)] fn main() { let memory = MemoryExchange::new(&ctx); let deposit = memory.bind_deposit_buffer(&mut scheme, &staging, data.len() as u64)?; deposit.write(&mut scheme, 0, &data)?; scheme.node("sim", &compute_pipeline) .with_parcel(&state_buf, NodeAccess::ReadWrite) .dispatch(wg, 1, 1); let mut pass = scheme.render_pass("draw", &scene_rt, TargetLoad::Discard); pass.with_parcel(&state_buf, NodeAccess::Read); pass.set_pipeline(&pipeline); pass.set_vertex_buffer(0, &vertex_buffer); pass.draw(0..3, 0..1); pass.finish(); let present = surface.bind_render_target(&mut scheme, &scene_rt)?; let mut submission = scheme.submit()?; present.claim(&mut submission)?.consume()?; }
Goldy derives the ordering between the compute node and the render pass node from their declared parcel accesses — the simulation's write to state_buf is ordered before the pass's read, with no barrier authored by hand.
Notes
- A render pass builder is single-use: call
finish()(Rust) once recording is complete, beforescheme.submit(). In the FFI bindings (C++, .NET,goldy-ffi-client), the equivalent is a RAII scope that finishes on drop or block exit. - A pass node is scoped to one leased render target for its lifetime — draw into a different target by opening a new
render_pass(...)node. - Nothing in this page executes anything: recording is pure graph-building. Execution, barrier insertion, and transient aliasing all happen inside
scheme.submit().
Vertex Types and Layouts
Goldy provides built-in vertex types for common 2D rendering and a layout builder for custom vertex formats. Vertex data is described by a VertexBufferLayout that tells the pipeline how to interpret buffer memory.
Built-in Vertex Types
Vertex2D
Position + color. Use for colored primitives, particles, and debug visualization.
#![allow(unused)] fn main() { use goldy::{Vertex2D, Color}; let vertices = vec![ Vertex2D::new(-0.5, -0.5, Color::RED), Vertex2D::new( 0.5, -0.5, Color::GREEN), Vertex2D::new( 0.0, 0.5, Color::BLUE), ]; }
Memory layout (24 bytes per vertex):
| Location | Field | Format | Offset |
|---|---|---|---|
| 0 | position | Float32x2 | 0 |
| 1 | color | Float32x4 | 8 |
Get the pipeline layout with Vertex2D::layout().
Vertex2DUv
Position + texture coordinates. Use for textured quads, sprites, and shader effects.
#![allow(unused)] fn main() { use goldy::Vertex2DUv; let vertices = vec![ Vertex2DUv::new(-1.0, -1.0, 0.0, 1.0), Vertex2DUv::new( 1.0, -1.0, 1.0, 1.0), Vertex2DUv::new( 0.0, 1.0, 0.5, 0.0), ]; }
Memory layout (16 bytes per vertex):
| Location | Field | Format | Offset |
|---|---|---|---|
| 0 | position | Float32x2 | 0 |
| 1 | uv | Float32x2 | 8 |
Get the pipeline layout with Vertex2DUv::layout().
Using Built-in Types in Pipelines
Both types provide a layout() method that returns the correct VertexBufferLayout:
#![allow(unused)] fn main() { let pipeline = RenderPipeline::new(&device, &vs, &fs, &RenderPipelineDesc { vertex_layout: Vertex2D::layout(), target_format: surface.format(), ..Default::default() })?; }
Both types implement StructuredBufferElement, so they can also be stored via RetainedPool::acquire_buffer_with_data.
Custom Vertex Layouts
Defining a Custom Vertex
Custom vertex types must be #[repr(C)] and derive bytemuck::Pod and bytemuck::Zeroable:
#![allow(unused)] fn main() { #[repr(C)] #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] struct MyVertex { position: [f32; 3], normal: [f32; 3], uv: [f32; 2], color: u32, } }
Building a Layout with from_formats
VertexBufferLayout::from_formats::<T> infers locations (sequential from 0) and offsets (accumulated from format sizes), then validates that the total matches size_of::<T>():
#![allow(unused)] fn main() { use goldy::types::{VertexBufferLayout, VertexFormat}; let layout = VertexBufferLayout::from_formats::<MyVertex>(&[ VertexFormat::Float32x3, // position (12 bytes) VertexFormat::Float32x3, // normal (12 bytes) VertexFormat::Float32x2, // uv (8 bytes) VertexFormat::Uint32, // color (4 bytes) ]); // stride = 36, 4 attributes }
The builder panics if the summed format sizes don't equal size_of::<T>(), catching field-list mismatches at pipeline creation rather than producing silent GPU corruption.
Manual Layout
For full control, construct the layout directly:
#![allow(unused)] fn main() { use goldy::types::{VertexBufferLayout, VertexAttribute, VertexFormat}; let layout = VertexBufferLayout { stride: 32, attributes: vec![ VertexAttribute { location: 0, format: VertexFormat::Float32x3, offset: 0 }, VertexAttribute { location: 1, format: VertexFormat::Float32x3, offset: 12 }, VertexAttribute { location: 2, format: VertexFormat::Float32x2, offset: 24 }, ], }; }
Empty Layout
When the vertex shader generates geometry from SV_VertexID (fullscreen triangles, instanced quads), use the default empty layout:
#![allow(unused)] fn main() { let pipeline = RenderPipeline::new(&device, &vs, &fs, &RenderPipelineDesc { vertex_layout: VertexBufferLayout::empty(), ..Default::default() })?; }
VertexBufferLayout::default() also returns an empty layout.
Vertex Formats
Available formats for vertex attributes:
| Format | Rust Type | Size |
|---|---|---|
Float32 | f32 | 4 |
Float32x2 | [f32; 2] | 8 |
Float32x3 | [f32; 3] | 12 |
Float32x4 | [f32; 4] | 16 |
Uint32 | u32 | 4 |
Sint32 | i32 | 4 |
Uint8x4 | [u8; 4] (packed) | 4 |
Unorm8x4 | [u8; 4] (normalized) | 4 |
Vertex Data Flow
In Slang shaders, vertex attributes arrive through the [goldy_vertex] virtual entry point. The pipeline's VertexBufferLayout determines which attributes the hardware feeds into the shader's input struct. Attribute locations in the layout must match the shader's declared input locations.
For passes that bypass vertex buffers entirely, Slang helpers like vs_fullscreen_triangle() and quad_position() in goldy_exp.primitives generate geometry from SV_VertexID and SV_InstanceID.
Rendering Outputs
Windowed rendering uses present-on-scheme: SurfaceExchange + Transaction + Claim. Record copy or compute-to-present once in a retained Scheme; submit each frame and settle the claim.
All windowed Rust examples use this path.
SurfaceExchange
A SurfaceExchange wraps the platform window and records how scheme output reaches the swapchain:
#![allow(unused)] fn main() { use goldy::{SurfaceExchange, SurfaceConfig, PresentMode, DepthFormat}; let surface = SurfaceExchange::new(&ctx, &window)?; // With explicit configuration and in-flight depth let surface = SurfaceExchange::new_with_depth( &ctx, &window, 3, SurfaceConfig { present_mode: PresentMode::Fifo, depth_format: Some(DepthFormat::Depth32Float), }, )?; }
Depth testing uses an offscreen scheme-leased render target, not the swapchain drawable.
Bind helpers
| Method | Use |
|---|---|
bind_render_target(scheme, scene_rt) | Offscreen render pass → surface copy |
bind(scheme, texture) | Texture → surface copy |
bind_destination(scheme) | Compute or other direct writes via with_present(&lease) |
Each bind returns a reusable Transaction. After scheme.submit(), extract the per-frame claim with transaction.claim(&mut submission)? and settle with claim.consume().
SurfaceConfig
#![allow(unused)] fn main() { pub struct SurfaceConfig { pub present_mode: PresentMode, pub depth_format: Option<DepthFormat>, } }
| Field | Purpose | Default |
|---|---|---|
present_mode | Vsync strategy | Auto |
depth_format | Depth buffer format, or None to disable | None |
Present Modes
| Mode | Behavior | Backend Mapping |
|---|---|---|
Fifo | Vsync — wait for display refresh. No tearing, capped at monitor Hz. | Metal displaySyncEnabled=YES, Vulkan FIFO, DX12 Present(1) |
Mailbox | Triple-buffered — latest frame queued, older dropped. Low latency + no tearing. | Vulkan MAILBOX. Falls back to Fifo on Metal and some DX12 configurations. |
Immediate | No sync, may tear. Maximum throughput for benchmarks. | Metal displaySyncEnabled=NO, Vulkan IMMEDIATE, DX12 Present(0) |
Auto | Goldy chooses (Mailbox if available, then Fifo). | — |
Change the present mode at runtime:
#![allow(unused)] fn main() { surface.set_present_mode(PresentMode::Immediate)?; let current = surface.present_mode(); }
Present-on-Scheme Frame Cycle
Record once at init (and on resize), submit each frame:
#![allow(unused)] fn main() { let mut pass = scheme.render_pass("main", &scene_rt, TargetLoad::Clear(Color::CORNFLOWER_BLUE)); pass.with_parcel(&vertex_buffer, NodeAccess::Read); pass.set_pipeline(&pipeline); pass.set_vertex_buffer(0, &vertices); pass.draw(0..3, 0..1); pass.finish(); let present = surface.bind_render_target(&mut scheme, &scene_rt)?; // Each frame: let mut submission = scheme.submit()?; present.claim(&mut submission)?.consume()?; }
For pure compute-to-surface, use bind_destination and bind the returned lease in a compute node with with_present(&lease) instead of a render pass + copy.
Surface Queries
#![allow(unused)] fn main() { surface.width(); surface.height(); surface.size(); // (width, height) surface.format(); // TextureFormat of the swapchain images }
Always use surface.format() when creating pipelines to ensure a match:
#![allow(unused)] fn main() { let desc = RenderPipelineDesc { target_format: surface.format(), ..Default::default() }; }
Resize Handling
Call resize() when the window size changes. Zero-size dimensions are silently
ignored (common during window minimize). Rebuild the scheme when
surface.size() changes.
SurfaceExchange::resize records the new extent immediately (and advances the
pool generation) but defers the DXGI/ResizeBuffers work until the next
drawable acquire. A burst of window-size events therefore only pays for one
structural rebuild per presented frame.
#![allow(unused)] fn main() { surface.resize(width, height)?; // rebuild scheme + transaction using surface.size() }
Transaction Lifetime
- Record a bind (
bind_render_target,bind, orbind_destination) once when building the scheme. - Each frame:
scheme.submit()thentransaction.claim(&mut submission)?.consume()?. - Each submission may be claimed at most once per transaction.
#![allow(unused)] fn main() { let mut submission = scheme.submit()?; present.claim(&mut submission)?.consume()?; // claim consumed — do not reuse this submission's claim slot }
Buffers
Buffer is a GPU memory allocation for storing typed data — uniforms, vertex data, index data, compute storage, or anything a shader needs to read or write.
Creating buffers (recommended)
For application-owned GPU memory, use RetainedPool and bind the returned Parcel in a scheme (with_parcel, set_vertex_buffer, MemoryExchange deposits). All Rust, Python, FFI, and .NET examples use this path.
#![allow(unused)] fn main() { use goldy::{BufferFlags, BufferKind, RetainedPool}; let mut pool = RetainedPool::new(device.clone()); let vertices = [/* Vertex2D ... */]; let vertex_parcel = pool.acquire_buffer_with_data(&vertices, BufferKind::Scattered)?; // Uninitialized storage (e.g. a uniform updated each frame via MemoryExchange deposit): let uniform = pool.acquire_buffer_sized::<MyUniforms>(1, BufferKind::Broadcast, BufferFlags::empty())?; }
See retained-pool.md for textures, mosaics, and release.
With Raw Bytes
When the data is naturally &[u8], pass an explicit element stride to acquire_buffer:
#![allow(unused)] fn main() { use goldy::{BufferFlags, BufferKind, RetainedPool}; let mut pool = RetainedPool::new(device.clone()); // Stride defaults to 1 when omitted (byte-addressable) let parcel = pool.acquire_buffer( raw_bytes.len() as u64, BufferKind::Scattered, None, BufferFlags::empty(), Some(&raw_bytes), )?; // Explicit stride for structured buffer views let parcel = pool.acquire_buffer( raw_bytes.len() as u64, BufferKind::Scattered, Some(16), BufferFlags::empty(), Some(&raw_bytes), )?; // With flags (e.g. CPU_READABLE) let parcel = pool.acquire_buffer( raw_bytes.len() as u64, BufferKind::Scattered, Some(16), BufferFlags::CPU_READABLE, Some(&raw_bytes), )?; }
Empty Buffer
#![allow(unused)] fn main() { let parcel = pool.acquire_buffer( 4096, BufferKind::Scattered, None, BufferFlags::empty(), None, )?; // With a specific element stride let parcel = pool.acquire_buffer( 4096, BufferKind::Scattered, Some(64), BufferFlags::empty(), None, )?; }
Low-level Device::alloc_* (crate-internal)
The runtime routes standalone allocations through VramAllocator via
crate-internal Device::alloc_buffer helpers. Application code should not call these;
use RetainedPool above.
Data Access Patterns
The access pattern describes how shader threads access the buffer. This drives hardware optimizations and determines the bindless descriptor category.
#![allow(unused)] fn main() { pub enum BufferKind { Scattered, // default — any thread, any address, read/write Broadcast, // all threads read the same address } }
| Pattern | Shader Mapping | Use When |
|---|---|---|
Scattered | StructuredBuffer<T>, RWStructuredBuffer<T> | General storage: particles, meshes, compute I/O |
Broadcast | ConstantBuffer / uniform buffer | Uniform data: transforms, time, settings |
For read-only input buffers that don't need write access, create with BufferKind::Scattered and access through goldy_buf_ro<T> in the shader. This enables hardware read-cache optimizations without requiring a separate access pattern.
BufferFlags
#![allow(unused)] fn main() { bitflags! { pub struct BufferFlags: u32 { const COPY_SRC = 1 << 0; const COPY_DST = 1 << 1; const CPU_READABLE = 1 << 2; const CPU_WRITABLE = 1 << 4; } } }
| Flag | Purpose |
|---|---|
COPY_SRC | Buffer can be a copy source |
COPY_DST | Buffer can be a copy destination |
CPU_READABLE | Medium hint for host-visible storage. Prefer MemoryExchange::bind_withdraw for observation. Not a public host-read API. |
CPU_WRITABLE | Host-mapped staging for deposits / upload copies. Prefer MemoryExchange::bind_deposit_buffer for application uploads. |
Query DeviceCapabilities::has_zero_copy_storage_readback to detect whether withdraw staging can elide a GPU copy on the current backend.
Writing Data
Prefer MemoryExchange::bind_deposit_buffer for CPU→GPU uploads. Direct host writes on CPU_WRITABLE staging parcels remain for deposit/staging internals:
Raw bytes
#![allow(unused)] fn main() { buffer.write(offset, &bytes)?; }
Typed data
#![allow(unused)] fn main() { buffer.write_data(offset, &[1.0f32, 2.0, 3.0])?; }
Both methods write at a byte offset from the start of the buffer.
Reading Data
Use a memory exchange withdraw bound into a scheme:
#![allow(unused)] fn main() { let memory = MemoryExchange::new(&ctx); let withdraw = memory.bind_withdraw(&mut scheme, buffer.whole())?; let mut submission = scheme.submit()?; let bytes = withdraw.claim(&mut submission)?.consume()?; }
Clearing
Zero-fill a region of the buffer:
#![allow(unused)] fn main() { buffer.clear(&device, offset, size)?; }
Bindless Descriptors
Every buffer with Scattered or Broadcast access is registered in the global bindless descriptor set. Schemes bind parcels via with_parcel; the opaque ResourceHandle is available for identity / retention checks:
#![allow(unused)] fn main() { // Opaque typed identity — equality / hashing only; no public heap index let handle = buffer.handle(ResourceAccess::Read).unwrap(); // Read-only SRV vs write UAV are distinct handles when both exist let srv_handle = buffer.handle(ResourceAccess::Read).unwrap(); }
BufferView
A BufferView is a sub-region of an existing Buffer with its own bindless descriptor. The shader sees the sub-region as a zero-based buffer.
Creating Views
#![allow(unused)] fn main() { // Raw byte view — offset, size, optional element stride let view = buffer.create_view(1024, 512, Some(16))?; // Typed view — first element index, element count let view = buffer.create_typed_view::<[f32; 4]>(0, 256)?; }
Using Views
Views implement BufferSource, so they work anywhere a Buffer does — set_vertex_buffer, set_index_buffer, write_data, clear, and scheme parcel binding:
#![allow(unused)] fn main() { let view_handle = view.handle(ResourceAccess::Read).unwrap(); pass.set_vertex_buffer(0, &view); }
Lifetime
Dropping a BufferView unregisters its descriptor but does not free the parent buffer's memory. Multiple views of the same buffer can exist simultaneously.
StructuredBufferElement
The StructuredBufferElement trait marks types safe for RetainedPool::acquire_buffer_with_data.
It is implemented for common multi-byte primitives (u16, u32, f32, f64, etc.), fixed-size arrays of those types, and #[repr(C)] structs via #[derive(goldy_derive::StructuredBufferElement)].
Not implemented for u8/i8 — passing &[u8] would set stride to 1, which almost never matches the shader's expected struct stride. Use RetainedPool::acquire_buffer with an explicit element stride for raw bytes.
Matrix Convention
Goldy uses column-major matrix layout in uniform/constant buffers across all backends. Rust math libraries (glam, nalgebra, ultraviolet) already store matrices column-major, so upload directly without transposing:
#![allow(unused)] fn main() { let uniforms = MyUniforms { projection: proj.to_cols_array_2d(), modelview: view.to_cols_array_2d(), }; buffer.write_data(0, &[uniforms])?; }
Goldy sets SLANG_MATRIX_LAYOUT_COLUMN_MAJOR at the Slang session level, so DX12, Vulkan, and Metal all interpret float4x4 the same way.
RetainedPool, Buffer, and Parcel
RetainedPool is the public door for retained GPU memory. Acquire returns a Buffer (possibly partitioned) or a texture Parcel. Bind parcels, not raw aggregates — each parcel is one bindable unit (whole buffer, buffer range, or texture).
Quick start
#![allow(unused)] fn main() { use goldy::{BufferKind, BufferFlags, MemoryExchange, RetainedPool, field, Init, NodeAccess, Scheme}; let mut pool = RetainedPool::new(device.clone()); // Single-unit buffer (derefs to whole parcel): let vertices = [/* ... */]; let vb = pool.acquire_buffer_with_data(&vertices, BufferKind::Scattered)?; // Raw bytes with explicit stride: let uniform_buf = pool.acquire_buffer( raw_bytes.len() as u64, BufferKind::Scattered, Some(16), BufferFlags::empty(), Some(&raw_bytes), )?; // Uninitialized buffer (rewrite each frame with a MemoryExchange deposit): let uniform = pool.acquire_buffer_sized::<MyUniforms>(1, BufferKind::Broadcast, BufferFlags::empty())?; // Texture parcel: let tex = pool.acquire_texture(w, h, format, access, flags, Some(&pixels))?; // Partitioned record (ping-pong, level geometry): let cells = pool.acquire_record([ field("a", Init::data(&grid_a)), field("b", Init::zeros::<u32>(n)), ])?; }
Scheme binding
#![allow(unused)] fn main() { let memory = MemoryExchange::new(&ctx); let mut upload = Scheme::new(&ctx); let deposit = memory.bind_deposit_buffer(&mut upload, &*uniform, std::mem::size_of::<MyUniforms>() as u64)?; deposit.write(&mut upload, 0, bytemuck::bytes_of(&data))?; upload.submit()?; let mut pass = scheme.render_pass("draw", &rt); pass.with_parcel(&*vb, NodeAccess::Read); pass.set_vertex_buffer(0, &*vb); pass.draw(0..3, 0..1); // Partitioned buffer: bind one field/range pass.with_parcel(&cells["a"], NodeAccess::Read); // Geometry bound via BufferSource only — register dependency without descriptor: pass.with_buffer_dependency(&geometry, NodeAccess::Read); }
Binding a multi-unit Buffer as one descriptor panics; index into fields instead.
Release
Call pool.release(&ctx, hold) when resizing or tearing down. While held, buffers need no epoch polling — the runtime stamps each parcel at submit.
Bindings
| Language | Types | Acquire |
|---|---|---|
| Rust | RetainedPool, Buffer, Parcel | acquire_buffer*, acquire_record, acquire_texture |
| Python | goldy.RetainedPool, goldy.Buffer, goldy.Parcel | acquire_buffer, acquire_record, acquire_texture |
| C# | RetainedPool, Buffer, Parcel, RecordBuilder | AcquireBuffer, Record(), AcquireTexture |
| C / ffi-client | GoldyRetainedPool, GoldyBuffer, GoldyParcel | goldy_retained_pool_acquire_buffer, goldy_record_builder_* |
All examples under goldy/examples/, python/examples/, dotnet/Goldy.Examples/, cpp/examples/, and ffi-client/examples/ use this API. See the bindings section for language-specific guides.
Textures and Samplers
Texture holds image data on the GPU. Sampler controls how that data is filtered and addressed when read in shaders. Together, they provide the standard texture sampling pipeline.
Creating a Texture
#![allow(unused)] fn main() { use goldy::{Texture, TextureKind, TextureFormat, TextureFlags}; let texture = Texture::new( &device, 512, 512, TextureFormat::Rgba8Unorm, TextureKind::Interpolated, TextureFlags::COPY_DST, )?; }
With Initial Data
Data must be raw bytes matching width × height × bytes_per_pixel:
#![allow(unused)] fn main() { let pixels: Vec<u8> = load_image_rgba("sprite.png"); let texture = Texture::with_data( &device, &pixels, 256, 256, TextureFormat::Rgba8Unorm, TextureKind::Interpolated, TextureFlags::COPY_DST, )?; }
Spatial Access Patterns
The access pattern determines how the texture is bound and accessed in shaders:
| Access | Shader Mapping | Use When |
|---|---|---|
Interpolated | Texture2D with sampler | Image data filtered between texels — sprites, materials, UI |
Direct | RWTexture2D | Storage images, compute output, exact pixel reads/writes |
Texture Formats
| Format | BPP | Notes |
|---|---|---|
R8Unorm | 1 | Single-channel (masks, SDFs) |
Rg8Unorm | 2 | Two-channel (normal maps, motion vectors) |
Rgba8Unorm | 4 | Standard 8-bit RGBA |
Rgba8UnormSrgb | 4 | sRGB color space |
Bgra8UnormSrgb | 4 | sRGB, swapped channels (common swapchain format) |
Bgra8Unorm | 4 | Linear, swapped channels |
Rgba16Float | 8 | HDR |
Rgba32Float | 16 | Full precision |
TextureFlags
#![allow(unused)] fn main() { bitflags! { pub struct TextureFlags: u32 { const COPY_SRC = 1 << 0; const COPY_DST = 1 << 1; const RENDER_TARGET = 1 << 2; } } }
| Flag | Purpose |
|---|---|
COPY_SRC | Texture can be a copy source (needed for withdraw / GPU copies) |
COPY_DST | Texture can be a copy destination (needed for deposits / copies) |
RENDER_TARGET | Texture can be used as a color attachment |
Writing Data
Prefer MemoryExchange::bind_deposit_texture for batched, non-blocking uploads. The synchronous methods below are deprecated and stall the GPU:
#![allow(unused)] fn main() { #[allow(deprecated)] texture.write(&pixels)?; #[allow(deprecated)] texture.write_region(x, y, width, height, ®ion_pixels)?; }
Reading Data
Use a memory exchange withdraw bound into a scheme. The texture must have been created with TextureFlags::COPY_SRC and a storage-writable kind:
#![allow(unused)] fn main() { let memory = MemoryExchange::new(&ctx); let withdraw = memory.bind_withdraw(&mut scheme, &texture)?; let mut submission = scheme.submit()?; let bytes = withdraw.claim(&mut submission)?.consume()?; }
Texture Queries
#![allow(unused)] fn main() { texture.width(); texture.height(); texture.format(); texture.byte_size(); // width * height * bytes_per_pixel texture.access(); // TextureKind texture.flags(); // TextureFlags texture.is_owned(); // true if dropping destroys the GPU resource }
Bindless Descriptors
Textures are registered in the global bindless descriptor set. The category depends on the access pattern: Interpolated maps to ResourceCategory::Texture, Direct maps to ResourceCategory::StorageImage. Schemes bind via with_parcel; the opaque handle is for identity checks:
#![allow(unused)] fn main() { let handle = texture.handle(ResourceAccess::Read).unwrap(); }
Texture Borrowing
Texture::borrow() creates a non-owning view that shares the GPU resource. Dropping a borrowed texture does not destroy the underlying resource. Use this when handing a texture reference into a system that may drop it before the owner is done.
#![allow(unused)] fn main() { let borrowed = texture.borrow(); assert!(!borrowed.is_owned()); // dropping `borrowed` does not free GPU memory }
Depth Textures
Depth textures are created through SurfaceConfig or [Scheme::lease_render_target], not directly via Texture::new. Available depth formats:
| Format | Bits | Stencil |
|---|---|---|
Depth16Unorm | 16 | No |
Depth24Plus | 24 | No |
Depth24PlusStencil8 | 24 + 8 | Yes |
Depth32Float | 32 | No |
Depth32FloatStencil8 | 32 + 8 | Yes |
#![allow(unused)] fn main() { let surface = SurfaceExchange::new_with_depth( &ctx, &window, 3, SurfaceConfig { depth_format: Some(DepthFormat::Depth32Float), ..Default::default() }, )?; }
Texture as Render Target
A texture created with TextureFlags::RENDER_TARGET can be used as a color attachment for offscreen rendering.
#![allow(unused)] fn main() { let offscreen = Texture::new( &device, 1920, 1080, TextureFormat::Rgba16Float, TextureKind::Interpolated, TextureFlags::RENDER_TARGET | TextureFlags::COPY_SRC, )?; }
Samplers
A Sampler defines how texture coordinates are interpreted — filtering between texels and handling coordinates outside [0, 1].
Creating a Sampler
#![allow(unused)] fn main() { use goldy::{Sampler, SamplerDesc, FilterMode, AddressMode}; let sampler = Sampler::new(&device, &SamplerDesc { mag_filter: FilterMode::Linear, min_filter: FilterMode::Linear, mipmap_filter: FilterMode::Linear, address_mode_u: AddressMode::Repeat, address_mode_v: AddressMode::Repeat, ..Default::default() })?; }
Convenience Constructors
#![allow(unused)] fn main() { let nearest = Sampler::nearest(&device)?; // nearest filter, clamp to edge let linear = Sampler::linear(&device)?; // linear filter, clamp to edge let tiling = Sampler::linear_repeat(&device)?; // linear filter, repeat addressing let default = Sampler::default_sampler(&device)?; // nearest filter, clamp to edge }
SamplerDesc
#![allow(unused)] fn main() { pub struct SamplerDesc { pub address_mode_u: AddressMode, // default: ClampToEdge pub address_mode_v: AddressMode, // default: ClampToEdge pub address_mode_w: AddressMode, // default: ClampToEdge pub mag_filter: FilterMode, // default: Nearest pub min_filter: FilterMode, // default: Nearest pub mipmap_filter: FilterMode, // default: Nearest pub max_anisotropy: f32, // default: 1.0 (disabled) pub compare: Option<CompareFunction>, // default: None pub lod_min_clamp: f32, // default: 0.0 pub lod_max_clamp: f32, // default: 32.0 } }
Filter Modes
| Mode | Effect |
|---|---|
Nearest | Pixelated — nearest texel, no interpolation |
Linear | Smooth — bilinear interpolation between neighbors |
Address Modes
| Mode | Effect for UVs outside [0, 1] |
|---|---|
ClampToEdge | Stretches the border texel |
Repeat | Tiles the texture |
MirrorRepeat | Tiles with alternating mirror flips |
Depth Comparison Samplers
For shadow mapping and depth-based effects, set the compare field:
#![allow(unused)] fn main() { let shadow_sampler = Sampler::new(&device, &SamplerDesc { compare: Some(CompareFunction::LessEqual), mag_filter: FilterMode::Linear, min_filter: FilterMode::Linear, ..Default::default() })?; }
Bindless Descriptors
Samplers are registered under ResourceCategory::Sampler:
#![allow(unused)] fn main() { let handle = sampler.handle(ResourceAccess::Read).unwrap(); }
Binding Textures and Samplers in Shaders
Pass texture and sampler parcels through [ShaderResourceSlot] bindings:
#![allow(unused)] fn main() { use goldy::ShaderResourceSlot; pass.with_shader_resources(&[ ShaderResourceSlot::Parcel { parcel: &texture_parcel, access: NodeAccess::Read, }, ShaderResourceSlot::Sampler(&sampler), ]); pass.set_pipeline(&pipeline); }
In Slang:
import goldy_exp;
[goldy_fragment]
float4 fs_main(Interpolated<float4> tex, Filter smp, float2 uv : TEXCOORD) {
return tex.Sample(smp, uv);
}
Pooling and Sub-Allocation
GPU resource allocation is expensive. Creating many small buffers or textures each frame produces allocation overhead, descriptor churn, and VRAM fragmentation. Goldy routes client allocation through two doors:
| Door | Permanence | Acquire |
|---|---|---|
RetainedPool | Cross-submission identity (deeds) | acquire_texture / acquire_buffer / acquire_record |
| Context transient pool | One-submission tenancy (leases) | Context::acquire_transient_texture / acquire_transient_buffer, or Scheme::lease_texture / lease_buffer |
The runtime owns reclaim: retained release transfers into the transient pool
with a ready_after stamp; transient bins reissue only after GPU retirement.
Partitioned retained buffers (one backing, many bindable fields) use internal
scattered suballocation — see RetainedPool::acquire_record. Do not construct
bump arenas or whole-object texture free-lists in client code.
Transient Allocation
Rendering pipelines allocate many short-lived GPU buffers and textures each frame — scratch storage, per-pass intermediates, filter pyramids. After submission those resources are dead until the GPU finishes, at which point the memory can be recycled. Clients must not poll timeline clocks to decide when reuse is safe.
Client door: TransientPool
Goldy exposes one transient door per Context:
| Acquire | Return |
|---|---|
Context::acquire_transient_buffer | Context::return_transient_buffer |
Context::acquire_transient_texture | Context::return_transient_texture |
Scheme leases (Scheme::lease_buffer / lease_texture) realize through the same
pool. Relinquished retained parcels enter via StampedParcel / ready_after;
the pool reissues only after every stamped epoch has retired.
#![allow(unused)] fn main() { let scratch = ctx.acquire_transient_buffer( size, BufferKind::Scattered, BufferFlags::GPU_ONLY, Some(stride), )?; // ... bind, submit ... ctx.return_transient_buffer(scratch); }
See also Pooling and Sub-Allocation.
What was removed
The former public TransientAllocator strategies (BumpReset, Heap) and the
internal scattered bump arena (BufferPool) were deleted: they had no in-tree
consumers once in-tree callers moved to RetainedPool / TransientPool.
Whole-object epoch-gated recycle bins are the supported transient path; any
future suballocation belongs behind that door, not as a parallel public API.
VRAM Allocator
All GPU buffer and texture allocations route through the device's internal
allocator (pools call Device::alloc_*). Clients obtain bytes via
RetainedPool / TransientPool;
the allocator itself is not a public customization point.
Allocation Policy (Tracking and Budget)
Install a BudgetPolicy to track live GPU
bytes and optionally enforce a cap:
#![allow(unused)] fn main() { use goldy::BudgetPolicy; use std::sync::Arc; let policy = Arc::new(BudgetPolicy::with_budget(512 * 1024 * 1024)); // 512 MiB device.ensure_allocation_policy(policy)?; println!("GPU memory in use: {} bytes", device.tracked_vram_bytes()); }
Use BudgetPolicy::new() when you only need telemetry without a hard budget.
Relationship to pools
RetainedPool/TransientPool— recycling policy (when deeds and leases may be reissued after GPU retirement).- Device allocator +
BudgetPolicy— provenance and optional byte budget.
Both pools allocate through the device, so an installed BudgetPolicy covers
retained and transient parcels automatically.
Backend Architecture
Goldy ships three GPU backends today, each implemented natively against the platform graphics API — no translation layers (like MoltenVK) are involved. Two additional backends are in active development; a Tenstorrent backend is planned.
| Backend | Status | API Level | Platforms | Rust Crate |
|---|---|---|---|---|
| Vulkan | Shipped | 1.4+ | Windows, Linux | ash |
| DX12 | Shipped | Direct3D 12 | Windows | windows + gpu-allocator |
| Metal | Shipped | Tier 2+ | macOS | metal |
| CUDA | In progress | CUDA Driver API | NVIDIA GPUs | cudarc |
| WebGPU | In progress | WebGPU (via wgpu) | Cross-platform | wgpu |
| CPU | In progress (compute-only) | Slang host-callable JIT | Host | — |
| Tenstorrent | Planned | TT-Metalium / TT-MLIR | Tenstorrent accelerators | — |
Native Implementations
Each backend maps Goldy concepts directly to the most natural primitives of its target API:
┌─────────────────────────────────────────────────────────────┐
│ Goldy Core API │
│ │
│ Device, Buffer, Texture, Pipeline, Scheme, ... │
└─────────────────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ Vulkan 1.4+ │ │ Metal 2+ │ │ DX12 │
│ │ │ │ │ │
│ • ash crate │ │ • metal-rs │ │ • windows-rs │
│ • Dynamic │ │ • Argument │ │ • Root │
│ rendering │ │ buffers │ │ signatures │
│ • Descriptor │ │ • Native │ │ • Descriptor │
│ indexing │ │ hazard │ │ heaps │
│ • Buffer │ │ tracking │ │ │
│ device addr │ │ │ │ │
└───────────────┘ └───────────────┘ └───────────────┘
Translation layers introduce overhead from API mismatches, incompatible synchronization models, and extra validation. Native backends can leverage each API's strengths directly — for example, Metal's built-in hazard tracking, or Vulkan's descriptor indexing for bindless rendering.
Backend Selection
Default Selection
Goldy selects the platform-preferred backend automatically:
| Platform | Default Backend |
|---|---|
| macOS | Metal |
| Windows | DX12 |
| Linux | Vulkan |
Runtime Override — GOLDY_BACKEND
Override the backend at runtime with the GOLDY_BACKEND environment variable:
GOLDY_BACKEND=vulkan cargo run --example triangle
GOLDY_BACKEND=dx12 cargo run --example triangle
Accepted values (case-insensitive):
| Value | Backend | Status |
|---|---|---|
vulkan, vk | Vulkan | Shipped |
dx12, d3d12, directx | DX12 | Shipped |
metal, mtl | Metal | Shipped |
cuda | CUDA | In progress |
webgpu, wgpu | WebGPU | In progress |
An unrecognized value produces a clear error listing the valid options.
Programmatic Selection
Query the active backend at runtime:
#![allow(unused)] fn main() { let instance = Instance::new()?; println!("Backend: {:?}", instance.backend_type()); // Prints: Backend: Dx12 (on Windows) // Prints: Backend: Vulkan (on Linux) // Prints: Backend: Metal (on macOS) }
Compile-Time Selection (Feature Flags)
You can also restrict which backends are compiled in via Cargo features. This excludes both the code and the dependencies of unselected backends:
cargo build --no-default-features --features vulkan
See Conditional Compilation for details on feature flags, dependency exclusion, and CI setup.
Adapter Enumeration
After creating an Instance, enumerate available GPU adapters to inspect
what hardware is present:
#![allow(unused)] fn main() { let instance = Instance::new()?; let adapters = instance.enumerate_adapters(); for adapter in &adapters { println!("{}: {} ({})", adapter.id(), adapter.name(), adapter.vendor()); println!(" Type: {:?}", adapter.device_type()); } }
DeviceType
Each adapter reports a DeviceType:
| Variant | Meaning |
|---|---|
DiscreteGpu | Dedicated graphics card with its own VRAM |
IntegratedGpu | GPU integrated into the CPU (shared memory) |
Cpu | Software renderer (e.g. WARP on DX12, lavapipe on Vulkan) |
Other | Unknown or unrecognized device class |
Creating a Device
Request a device with a preferred DeviceType. If no adapter matches,
Goldy falls back to the first available adapter:
#![allow(unused)] fn main() { let device = instance .request_adapter(&RequestAdapterOptions { power_preference: PowerPreference::HighPerformance, ..Default::default() })? .request_device(&DeviceDescriptor::default())?; // Or target a specific adapter by ID: let device = instance.create_device_for_adapter(adapter.id())?; }
Backend Capabilities
Device Capabilities
Query format preferences and backend-specific capabilities after creating a device:
#![allow(unused)] fn main() { let caps = device.capabilities(); println!("Surface format: {:?}", caps.preferred_surface_format); println!("Render target fmt: {:?}", caps.preferred_render_target_format); println!("Zero-copy readback: {}", caps.has_zero_copy_storage_readback); }
| Capability | Vulkan | DX12 | Metal |
|---|---|---|---|
| Zero-copy CPU storage readback | Yes | No (requires GPU copy to readback heap) | Yes |
| Preferred surface format | Bgra8UnormSrgb | Bgra8UnormSrgb | Bgra8UnormSrgb |
Vulkan Backend
The Vulkan backend requires Vulkan 1.4+ and uses:
- Dynamic rendering (
VK_KHR_dynamic_rendering) — noVkRenderPassorVkFramebufferobjects - Descriptor indexing — bindless resource access by index in shaders
- Buffer device address — 64-bit GPU pointers for direct memory access in shaders
DX12 Backend
The DX12 backend uses the windows crate and provides:
- Root signatures for resource binding
- Descriptor heaps for efficient bindless resource management
- Shader compilation via Slang to DXIL
- WARP software rasterizer for headless/CI use (
GOLDY_DX12_FORCE_WARP=1) - GPU-Based Validation for deep debugging (
GOLDY_DX12_GBV=1)
Metal Backend
The Metal backend uses the metal crate (native Metal, not MoltenVK):
- Argument buffers for bindless resource binding
- Native hazard tracking — Metal tracks resource hazards automatically
- Shader compilation via Slang to Metal Shading Language
CUDA Backend (in progress)
Compute-focused prototype targeting NVIDIA GPUs via the CUDA Driver API (CUDA 13.1+
required for device-updatable graph nodes). Slang compiles to PTX; dispatches use the
CUDA launch model. The cuda feature does not imply graphics. Buffer schemes,
uploads/readbacks, timelines, indirect dispatch, and 2D textures/samplers
(CUDA arrays + texture/surface objects) work.
Windows presentation: when cuda, graphics, and dx12 are all enabled, each
CUDA device opens a LUID-matched DX12 companion. Surface frames expose an
Rgba8Unorm shared scratch texture from a depth-3 staging ring independent of
the DXGI swapchain image. Typical schemes (including Ekrano) write a CUDA-owned
staging texture then CopyTexture into that imported scratch — the same
local-then-copy pattern as native DX12 — before present's same-format CopyResource
onto the R8G8B8A8_UNORM DXGI swapchain. CUDA signals a ready fence; DX12 waits
it, copies, then signals a recycle fence. CUDA waits recycle only when wrapping
the ring, so compute N+1 does not serialize behind present-copy N. Adapter mismatch,
WARP, and linked-node adapters fail at device creation. A first-slice raster path is
also available under the same feature gate: offscreen Rgba32Float and Rgba8Unorm render targets, indexed and non-indexed
point/line/triangle pipelines (Slang → DXIL), bindless render bindings, optional
DX12-only depth attachments / depth-stencil PSOs / ClearDepth, and
CopyRenderTarget into present scratch / CUDA textures. Depth is not CUDA-imported
(compute cannot sample it yet); stencil ops remain off. Vulkan interop is not
supported.
Enable with the cuda Cargo feature (--no-default-features --features cuda
auto-selects CUDA; in default builds use GOLDY_BACKEND=cuda):
cargo test --no-default-features --features cuda --test scheme_compute_integration
# Windows presentation:
GOLDY_BACKEND=cuda cargo run --example compute_to_surface --features examples
Texture notes for CUDA:
- Sampled formats:
R8Unorm,Rg8Unorm,Rgba8Unorm,Rgba8UnormSrgb,Rgba16Float,Rgba32Float. BGRA is rejected (no matching CUDA array swizzle). - Writable shader access (
DirectSpatial<T>) supports size-matched pairs and Goldy’s typed-UAV emulation for convertible pairs:DirectSpatial<float4>↔Rgba32Float(identity surface store)DirectSpatial<float4>↔Rgba8Unorm(lazy PTX specialization: pack/unpack view overuint8_t4, DX12-styleround(saturate(x)*255)on store). Partitions that launch this specialized variant stay on stream-replay segments between CUDA graph islands (or use full op-list retention when no graph-safe island remains).DirectSpatial<half4>↔Rgba16FloatDirectSpatial<uint8_t4>↔Rgba8Unorm(Slang has nouchar4alias)- Upload/copy/readback of other supported sampled formats still works.
- Surfaces expose
Rgba8Unormimported scratch from a depth-3 ring (CUDA+DX12 interop tradeoff: extra staging textures so compute does not reuse frame N's scratch in N+1). Prefer writing a CUDA-ownedRgba8Unormtexture (or render target) and exporting withCopyTexture; direct launches onto imported scratch remain supported but are costlier under WDDM. The DXGI swapchain is matchingR8G8B8A8_UNORMso present is a singleCopyResource. DeviceCapabilitieson CUDA advertisepreferred_surface_format = Rgba8Unormandpreferred_render_target_format = Rgba8Unorm(no BGRA in supported lists).- CUDA has no separate sampler object — filtering is baked into each
CUtexObject. A dispatch may use at most one distinctFilterconfiguration; additional distinct samplers are rejected.
Retainable partitions are split into alternating CUDA graph islands (contiguous
graph-safe kernel launches) and stream-replayed boundary segments (clears, copies,
format-specialized launches, present exports). Islands are captured on first submit
and relaunched on clean resubmits; stream segments re-execute between them on the
same CUDA stream. Indirect dispatches in graph islands use CUDA 13.1
device-updatable kernel nodes: an in-graph updater reads the GPU-resident
DispatchShape and updates the consumer node's grid (or disables it for a zero /
oversized shape). Uploads and other fully graph-unsafe partitions stay on the
stream command-replay path, where indirect grids are resolved with a worker-side DtoH
before cuLaunchKernel. Dynamic waits and completion events remain outside the
captured graph. Stream capture is skipped when CUDA_LAUNCH_BLOCKING is set
(including under GOLDY_VALIDATION=api).
With GOLDY_VALIDATION=api (or all), the CUDA backend enables Driver diagnostics: PTX JIT error/info logs on module load, host-side launch-limit checks, StructuredBuffer ABI checks, and per-op stream synchronize with labeled errors. It may set CUDA_LAUNCH_BLOCKING=1 when unset. Deep memory/race checking still requires external compute-sanitizer, not GOLDY_VALIDATION.
WebGPU Backend (in progress)
Cross-platform prototype built on wgpu. Intended for broader
portability and browser-adjacent targets. Enable with the webgpu Cargo feature. Not yet at
parity with the shipped Vulkan/DX12/Metal backends.
Compute buffers, scalar uniforms, indirect dispatch, and 2D textures/samplers work.
Submit is non-blocking: the context timeline advances from wgpu's
on_submitted_work_done callback (pumped by Device::poll). Host waits
(Context::wait_until, withdraw) block on the submission index, not on submit
itself. Resources bind as a single @group(0) in shader-parameter order (no bindless heap). Texture
notes:
- Sampled formats:
R8Unorm,Rg8Unorm,Rgba8Unorm,Rgba8UnormSrgb,Bgra8Unorm,Bgra8UnormSrgb,Rgba16Float,Rgba32Float(subject to adapter format features). DirectSpatial<T>storage textures follow WGSL: the shader type encodes the format. IdentityDirectSpatial<float4>isrgba32float. Goldy specializes packed 8-bit surfaces at dispatch:Rgba8Unorm→rgba8unorm,Bgra8Unorm→bgra8unorm(the latter needs wgpuBGRA8UNORM_STORAGE). sRGB formats are rejected for storage.- Uploads use
queue.write_texture. Texture withdraw staging uses WebGPU's 256-byte row pitch;query_texture_copy_footprintreports the padded layout. - Surfaces:
begin_frameacquires the wgpu drawable. Present picks Copy (same-format storage scratch → swapchain) when the scratch format can be a UAV (Rgba8Unorm, orBgra8UnormwithBGRA8UNORM_STORAGE), otherwise Blit (Rgba8Unormscratch + fullscreen pass for BGRA/sRGB). Direct compute-to-swapchain is not selected: wgpu 28 swapchain images only exposeRENDER_ATTACHMENT, so storage bind groups fail even when the surface advertisesSTORAGE_BINDING. Override withGOLDY_WEBGPU_PRESENT=copy|blit.DirectSpatial<float4>shaders do not change; packed storage is specialized to the compute format (surface_format()).finish_presentdrops the acquired image and publishesSwapchainReturned.surface_resizereconfigures the swapchain and recreates scratch. - Raster: offscreen render targets, graphics PSOs (Slang → WGSL
vs_main/fs_main), indexed and non-indexed draws, optional depth-stencil, andCopyRenderTarget. Vertex/fragment resources use the same packed@group(0)lowering as compute (no bindless heap). Thewebgpufeature impliesgraphics. - Compute sampling must use
SampleLevel(WGSL has no implicit derivatives in compute).
cargo test --no-default-features --features webgpu --lib backend::webgpu
Tenstorrent Backend (planned)
Torus is a planned Fondaco runtime for Tenstorrent Tensix hardware. No implementation ships in Goldy today.
The GpuBackend Trait
All backends implement the GpuBackend trait, which defines the full
interface for device management, resource creation, shader compilation,
pipeline management, rendering, and compute dispatch:
#![allow(unused)] fn main() { pub trait GpuBackend: Send + Sync { fn backend_type(&self) -> BackendType; fn enumerate_adapters(&self) -> Vec<AdapterInfo>; fn create_device(&mut self, adapter_id: u32) -> Result<DeviceHandle>; fn create_buffer(&mut self, device: DeviceHandle, ...) -> Result<BufferHandle>; fn create_shader_with_paths(&mut self, device: DeviceHandle, ...) -> Result<ShaderHandle>; fn create_pipeline(&mut self, device: DeviceHandle, ...) -> Result<PipelineHandle>; // ... rendering, compute, surface, texture, sampler, timeline ... } }
Resources are identified by opaque u64 handles (DeviceHandle,
BufferHandle, ShaderHandle, etc.) that each backend maps to native
API objects internally.
Conditional Compilation
Most users should use GOLDY_BACKEND for runtime switching — see
Backend Architecture.
Compile-time feature flags are useful when you need smaller binaries, faster builds, or want to verify that each backend compiles independently in CI.
When to Use Compile-Time Features
Use --no-default-features --features <backend> when you need:
- Smaller binaries — exclude unused backend code
- Faster builds — skip compiling heavy backend dependencies
- Missing SDK — build on a system that lacks the Vulkan SDK or Windows SDK
- CI matrix — verify each backend compiles independently
- Compute-only builds — CUDA without raster, surfaces, or presentation
Feature Flags
Goldy defines one feature per backend, plus gpu, graphics, and instrumentation:
[features]
default = ["vulkan", "metal", "dx12", "instrumentation", "graphics"]
graphics = ["dep:raw-window-handle"]
gpu = [] # implied by every real GPU backend (not mock)
vulkan = ["dep:ash", "graphics", "gpu"]
dx12 = ["dep:windows", "dep:gpu-allocator", "dep:windows-core", "graphics", "gpu"]
metal = ["dep:metal", "dep:cocoa", "dep:objc", "dep:core-graphics-types",
"dep:foreign-types", "dep:block", "graphics", "gpu"]
cuda = ["dep:cudarc", "gpu"]
webgpu = ["dep:wgpu", "dep:pollster", "graphics", "gpu"]
instrumentation = ["dep:tracing-subscriber"]
graphics
graphics enables raster pipelines, render targets, surfaces, and presentation.
Native backends (vulkan, dx12, metal) imply graphics, so enabling any of
them keeps the full graphics+compute API.
Textures and samplers remain available without graphics — they are part of
the GPGPU compute surface (storage images, sampling, copies, deposits/withdrawals).
gpu is an empty umbrella enabled by vulkan, dx12, metal, cuda, and
webgpu. Use cfg(feature = "gpu") for tests that need Instance::new() rather
than the always-compiled mock backend. Enabling gpu alone does not compile a
backend.
cuda does not imply graphics.
Neither cude nor webgpu are a platform default (Metal / DX12 / Vulkan
remain the defaults in normal builds). When you compile only cuda or
webgpu — no native backend — Instance::new() selects that backend
automatically. In a default multi-backend build, opt in with
GOLDY_BACKEND=cuda or GOLDY_BACKEND=webgpu.
On Windows, enabling cuda together with graphics and dx12 (the usual case
when adding cuda on top of default features) attaches a DX12 presentation
companion to each CUDA device: LUID-matched DXGI adapter, shared float4 scratch
textures, and swapchain present. The same gate enables a first-slice raster path
(offscreen Rgba32Float targets, indexed/non-indexed point/line/triangle
pipelines, bindless bindings, and optional DX12-only depth). Vulkan interop
remains unsupported. Without that full gate, surface/present/raster APIs still
return compute-only errors.
# CUDA compute-only
cargo test --no-default-features --features cuda --test scheme_compute_integration
# CUDA + DX12 presentation + first-slice raster (Windows)
cargo check --no-default-features --features cuda,graphics,dx12
GOLDY_BACKEND=cuda cargo run --example compute_to_surface --features examples
cargo test --no-default-features --features cuda,graphics,dx12 --test cuda_dx12_raster
cargo test --no-default-features --features cuda,graphics,dx12 --test cuda_dx12_presentation
cargo test --no-default-features --features cuda,graphics,dx12 --test cuda_dx12_surface_lifecycle
Dependency Exclusion
Building with only one backend excludes both the code and the dependencies for the others:
| Feature | Dependencies |
|---|---|
gpu | none (umbrella; implied by each backend below) |
vulkan | ash (+ graphics / raw-window-handle) |
dx12 | windows, gpu-allocator, windows-core (+ graphics) |
metal | metal, cocoa, objc, core-graphics-types, foreign-types, block (+ graphics) |
cuda | cudarc |
webgpu | wgpu, pollster |
# Default build on Windows — compiles Vulkan + DX12 dependencies
cargo build
# Vulkan-only build — downloads only ash (and enables graphics)
cargo build --no-default-features --features vulkan
# DX12-only build
cargo build --no-default-features --features dx12
# CUDA compute-only (no raster; surfaces need dx12+graphics on Windows)
cargo build --no-default-features --features cuda
# CUDA + DX12 presentation companion (Windows)
cargo build --no-default-features --features cuda,graphics,dx12
This can significantly reduce build times and binary size.
Platform-Specific Considerations
| Backend | Available On | Notes |
|---|---|---|
vulkan | Windows, Linux (any platform with a Vulkan loader) | Broadest platform support; implies graphics |
dx12 | Windows only | Gated by #[cfg(target_os = "windows")] — the feature is ignored on other platforms; implies graphics |
metal | macOS only | Gated by #[cfg(target_os = "macos")] — the feature is ignored on other platforms; implies graphics |
cuda | Any platform with CUDA toolkit | Compute prototype; on Windows with cuda+graphics+dx12, DX12 presentation companion + first-slice raster (Rgba32Float / Rgba8Unorm, indexed draws, DX12-only depth) are enabled. Does not imply graphics by itself. Vulkan interop still pending. |
webgpu | Cross-platform | via wgpu; implies graphics |
On macOS, the default backend is native Metal. Goldy does not require MoltenVK.
Default Features
The default feature set enables all three native backends plus instrumentation and graphics:
default = ["vulkan", "metal", "dx12", "instrumentation", "graphics"]
To override, use --no-default-features and enable only what you need:
# Only Vulkan (graphics implied)
cargo build --no-default-features --features vulkan
# Vulkan + instrumentation
cargo build --no-default-features --features vulkan,instrumentation
# Metal-only on macOS
cargo build --no-default-features --features metal
# CUDA compute-only
cargo build --no-default-features --features cuda
FFI and Python Feature Passthrough
The goldy-ffi and goldy-py crates propagate features to the core
goldy crate, so you can control backend selection in downstream builds.
The same goldy-ffi build is consumed by C++, .NET, and goldy-ffi-client.
# FFI bindings with only Vulkan backend
cargo build -p goldy-ffi --no-default-features --features vulkan
# FFI with CUDA compute-only
cargo build -p goldy-ffi --no-default-features --features cuda
# Python bindings with only DX12 backend
cargo build -p goldy-py --no-default-features --features dx12
This is useful for creating platform-specific binary distributions.
Cross-Compilation
When cross-compiling, keep in mind that platform-gated features are silently ignored if the target platform doesn't match:
# Targeting macOS — dx12 feature is silently ignored, only metal + vulkan
# are active
cargo build --target aarch64-apple-darwin
# Targeting Windows — metal feature is silently ignored
cargo build --target x86_64-pc-windows-msvc --no-default-features --features dx12
For cross-compilation to work, you need the appropriate system SDKs
available. Vulkan is the most portable backend since the ash crate only
needs a Vulkan loader at runtime, not at compile time.
CI Matrix Example
Verify each backend compiles independently in CI:
# GitHub Actions
jobs:
lint:
strategy:
matrix:
include:
- os: ubuntu-latest
features: vulkan
- os: windows-latest
features: vulkan
- os: windows-latest
features: dx12
- os: macos-latest
features: metal
- os: ubuntu-latest
features: webgpu
- os: windows-latest
features: webgpu
- os: macos-latest
features: webgpu
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- run: cargo clippy --no-default-features --features ${{ matrix.features }} -- -D warnings
Checking the Active Backend
At runtime, query which backend was selected:
#![allow(unused)] fn main() { let instance = Instance::new()?; println!("Backend: {:?}", instance.backend_type()); }
If no backend feature is enabled for the current platform, Instance::new()
returns an error:
No GPU backend available — enable 'vulkan', 'dx12', 'metal', 'cuda', or 'webgpu'
In a default build (Vulkan + DX12 + Metal), use GOLDY_BACKEND=cuda or
GOLDY_BACKEND=webgpu to opt into the in-progress compute prototypes.
Debugging and Observability
Goldy provides validation layers, structured instrumentation, and environment variable controls that together cover the full debugging workflow — from catching API misuse to profiling frame timing.
Validation
GOLDY_VALIDATION Environment Variable
The primary control for runtime validation. Accepts a comma-, semicolon-, or whitespace-separated list of categories:
| Value | Effect |
|---|---|
api | Enable backend GPU API validation (see below) |
layout | Enable Rust ↔ Slang struct layout checks and buffer stride checks |
host_access | Page-protect CPU-visible GPU copies (CPU backend parcels; stray host pointers fault) |
all | Enable api, layout, timeline, scheme, and host_access |
1, true, yes | GPU API validation only (legacy shorthand; does not enable layout checks) |
Categories can be combined:
# API validation only
GOLDY_VALIDATION=api cargo run --example triangle
# Layout validation only
GOLDY_VALIDATION=layout cargo run --example triangle
# Both
GOLDY_VALIDATION=all cargo run --example triangle
GOLDY_VALIDATION=layout,api cargo run --example triangle
API Validation
When GOLDY_VALIDATION includes api (or 1/true/yes), Goldy
enables backend-specific validation:
| Backend | What Gets Enabled |
|---|---|
| Vulkan | VK_LAYER_KHRONOS_validation + VK_EXT_debug_utils at instance creation |
| Metal | Sets MTL_SHADER_VALIDATION=1 (if not already set) before the first device is created |
| DX12 | See DX12 Debug Layer below |
| WebGPU | wgpu validation error scopes on shader/PSO create (always in debug builds; in release when GPU API validation is on) and on bind-group create (GPU API validation only) |
On Vulkan, GOLDY_VALIDATION_FATAL=1 treats messenger ERROR records as hard failures (Err on later Goldy Result calls; panic on backend drop).
For Vulkan, validation is also enabled when VK_INSTANCE_LAYERS contains
VK_LAYER_KHRONOS_validation (the standard loader-driven workflow).
Layout Validation
Layout validation catches mismatches between Rust struct layouts and their Slang shader counterparts at shader compile time, and buffer element-stride mismatches at dispatch time.
Enable via either:
GOLDY_VALIDATION=layout cargo run
GOLDY_VALIDATE_LAYOUTS=1 cargo run # legacy variable, equivalent
#[derive(LayoutCheckable)]
Annotate Rust structs that mirror Slang types to opt into automatic validation:
#![allow(unused)] fn main() { #[derive(LayoutCheckable)] #[repr(C)] struct SceneUniforms { projection: [[f32; 4]; 4], view: [[f32; 4]; 4], time: f32, } }
The derive macro generates a LAYOUT_CHECK constant containing the
struct's name, total size, and per-field offsets. Pass it when creating a
shader module:
#![allow(unused)] fn main() { let shader = ShaderModule::from_slang_with_options( &device, source, &[], // extra search paths &[], // defines Default::default(), &[SceneUniforms::LAYOUT_CHECK], )?; }
When layout validation is enabled, Goldy compiles the Slang shader, reflects each named struct, and compares:
- Total struct size — Rust
size_ofvs. Slang reflection - Field offsets — each named field's byte offset
A mismatch produces an error naming the struct, the field, and the expected vs. actual offset — immediately surfacing padding or alignment bugs. When validation is disabled, the checks are skipped at zero cost.
Buffer Stride Validation
At dispatch time (when layout validation is enabled), Goldy also checks
that each bound buffer's element_stride matches the stride the shader
expects from Slang reflection. A mismatch produces an error like:
buffer element-stride mismatch in shader `my_shader`:
slot 0: shader expects element stride 16 but buffer has 4
DX12-Specific Debugging
DX12 Debug Layer
| Variable | Values | Effect |
|---|---|---|
GOLDY_DX12_DEBUG | 1 | Force-enable the D3D12 debug layer (even in release builds) |
GOLDY_DX12_NO_DEBUG | 1 | Disable the D3D12 debug layer (useful for parallel tests that crash the debug layer) |
GOLDY_DX12_GBV | 1 | Enable GPU-Based Validation (very slow; requires the debug layer) |
GPU-Based Validation (GBV) instruments shaders on the GPU to detect issues that the CPU-side debug layer cannot catch — such as out-of-bounds descriptor accesses and uninitialized resource reads. Expect a significant performance hit.
WARP Software Rasterizer
WARP is Microsoft's software implementation of D3D12. It runs on the CPU, so it works on headless CI runners with no GPU.
GOLDY_DX12_FORCE_WARP=1 cargo nextest run
After the first WARP device is created, Goldy prints a confirmation:
[WARP] d3d10warp.dll loaded from: C:\WINDOWS\SYSTEM32\d3d10warp.dll
On Windows, DX12 is the default backend, so GOLDY_DX12_FORCE_WARP=1 is
the only variable you need to run tests on a machine without a GPU.
Structured Instrumentation
Goldy includes a structured instrumentation system built on the tracing
crate. It provides named observation points with hierarchical
dot-notation names and structured context data.
Enabling Instrumentation
Instrumentation requires the instrumentation Cargo feature (enabled by
default). When disabled, all macros compile to no-ops at zero cost.
# Explicitly enable
cargo build --features instrumentation
# Disable (zero-cost removal)
cargo build --no-default-features --features vulkan
goldy_span! — Timed Sections
Create a span to measure the duration of a code section:
#![allow(unused)] fn main() { use goldy::goldy_span; fn compile_shader(&self) { let _span = goldy_span!("slang.compile", target = "metal").entered(); // ... compilation code ... // Duration is recorded automatically when _span is dropped } }
goldy_event! — Instant Markers
Emit a one-shot structured event:
#![allow(unused)] fn main() { use goldy::goldy_event; goldy_event!("slang.library.load", path = %lib_path.display(), success = true ); }
Built-in Observation Points
Goldy instruments its own internals at these observation points:
| Category | Point Name | Emitted Data |
|---|---|---|
| Slang | slang.library.load | path, success |
slang.compile.start | target, entry_points, bindless | |
slang.compile.end | duration_ms, output_size, success | |
slang.reflection.extract | parameter_blocks, fields | |
| Shader | shader.module.create | backend, shader_type |
shader.pipeline.create | pipeline_type, bind_groups | |
| Resource | resource.buffer.create | size, usage |
resource.texture.create | dimensions, format | |
resource.bind_group.create | bindings_count | |
| Render | render.frame.start | frame_id |
render.compute.dispatch | workgroups, pipeline | |
render.draw | vertices, instances | |
render.frame.end | frame_id, duration_ms |
JSON Logging
Install a JSON file logger to capture all instrumentation output as structured JSON:
#![allow(unused)] fn main() { use goldy::instrumentation::install_json_logger; install_json_logger("/tmp/goldy-debug.json")?; // All subsequent goldy_span!/goldy_event! calls are written to the file }
Filtering with RUST_LOG
Use the standard RUST_LOG environment variable to control verbosity.
All Goldy instrumentation uses the goldy target:
RUST_LOG=goldy=debug cargo run --example triangle
RUST_LOG=goldy::render=trace cargo run --example triangle
Environment Variables Summary
| Variable | Values | Effect |
|---|---|---|
GOLDY_BACKEND | vulkan/vk, dx12/d3d12/directx, metal/mtl | Override backend selection |
GOLDY_VALIDATION | api, layout, host_access, all, 1/true/yes | Enable validation categories |
GOLDY_VALIDATION_FATAL | 1, true, yes | Fail on Vulkan Khronos ERROR messages |
GOLDY_VALIDATE_LAYOUTS | 1, true, yes | Enable layout validation (legacy; prefer GOLDY_VALIDATION=layout) |
GOLDY_DX12_FORCE_WARP | 1 | Use WARP software rasterizer |
GOLDY_DX12_DEBUG | 1 | Force-enable D3D12 debug layer in release |
GOLDY_DX12_NO_DEBUG | 1 | Disable D3D12 debug layer |
GOLDY_DX12_GBV | 1 | Enable GPU-Based Validation |
GOLDY_SHADER_TIMING | 1 | Print Slang/PSO startup timings to stderr |
GOLDY_CPU_SHADERS | 1 | Documented gate for debug CPU host-callable kernels (goldy::cpu_shaders) |
RUST_LOG | e.g. goldy=debug | Filter instrumentation output |
Common Debugging Patterns
Catch API misuse early
GOLDY_VALIDATION=api cargo run --example my_app
Turn on API validation during development to catch invalid GPU API calls. On Vulkan this enables the Khronos validation layer; on Metal it enables shader validation.
Diagnose struct layout bugs
GOLDY_VALIDATION=layout cargo test
If a LayoutCheckable struct diverges from its Slang counterpart (due to
padding, alignment, or a field being added on only one side), the error
message names the exact struct and field.
Catch stray CPU pointers into GPU copies
GOLDY_VALIDATION=host_access cargo test --test cpu_backend
GOLDY_VALIDATION=all cargo run
When host_access is on, the CPU backend allocates each parcel in its own
page-aligned mapping (plus a guard page) and leaves it inaccessible except
during upload, kernel dispatch, and withdraw. A leftover host pointer then
faults instead of silently reading GPU-owned bytes. This is a debug allocator:
slower, not complete (native device-local VRAM is not mapped), and meant to
grow to staging buffers on other backends.
Headless CI on Windows
GOLDY_DX12_FORCE_WARP=1 cargo nextest run
WARP gives you a fully functional D3D12 device on machines with no GPU.
Combine with GOLDY_VALIDATION=api for maximum coverage.
Profile frame timing
#![allow(unused)] fn main() { use goldy::instrumentation::install_json_logger; install_json_logger("/tmp/goldy-profile.json")?; // Run your application, then inspect the JSON output for // render.frame.start / render.frame.end durations }
Deep DX12 debugging
GOLDY_DX12_DEBUG=1 GOLDY_DX12_GBV=1 cargo run --example my_app
GPU-Based Validation catches GPU-side issues the CPU debug layer cannot see, at a significant performance cost. Use it when you suspect descriptor or resource access bugs.
CPU host-callable shaders (debug)
Goldy can JIT the same Slang compute kernels it runs on GPU and execute them
on the host via Slang SLANG_SHADER_HOST_CALLABLE (getEntryPointHostCallable).
This is an opt-in debug path so you can step a stage in a CPU debugger
without maintaining a second handwritten Rust implementation.
Standalone compile/dispatch on host slices is the original debug path. Scheme
submit on a compute-only CPU device is available with GOLDY_BACKEND=cpu.
That backend is not a CPU renderer and not a replacement for Vulkan / DX12 /
Metal / CUDA / lavapipe / WARP.
When to use it
- Stepping a
#[goldy::compute]/[goldy_compute]kernel in a native debugger - Checking buffer math on host slices before wiring GPU parcels
- Replacing deleted CPU twins in clients (for example Ekrano) with the real Slang
How to run a kernel
#![allow(unused)] fn main() { use goldy::cpu_shaders::{self, CpuBinding}; use goldy::slang::SlangCompiler; let compiler = SlangCompiler::new()?; let kernel = cpu_shaders::compile_kernel(&compiler, &kernel_def, &["shaders"])?; let mut data: Vec<u32> = (0..64).collect(); kernel.dispatch_1d(64, &mut [CpuBinding::u32s(&mut data)])?; }
cpu_shaders::compile accepts [goldy_compute] source (or raw
[shader("compute")] after you pack bindings yourself). The CPU wrapper keeps
BufRO / Scattered as typed uniform entry-point parameters instead of
Goldy bindless slot indices.
Set GOLDY_CPU_SHADERS=1 when you want the documented env gate (reserved for a
future Device debug option). The compile APIs above are already opt-in; GPU
paths ignore the variable.
Host-callable JIT uses vendored slang-llvm next to libslang. No extra C++
toolchain is required when that library is present. Do not set Slang
SLANG_TARGET_FLAG_GENERATE_WHOLE_PROGRAM with the current vendored Slang:
getEntryPointHostCallable SIGSEGVs. Goldy omits that flag.
What lowers
| Type | CPU ABI |
|---|---|
BufRO<T>, Scattered<T> (T = uint / int / float / bool) | { T* data; size_t count } |
Scalar uint / int / float / bool | 4-byte word |
ThreadId, GroupThreadId, GroupId | SV_DispatchThreadID / SV_GroupThreadID / SV_GroupID |
goldy_buf_len(buf) | GetDimensions on the CPU structured buffer |
Workgroups run serially through the Slang CPU prelude (ComputeVaryingInput
start/end group IDs).
What does not lower yet
| Type | Notes |
|---|---|
Broadcast / gpu::Uniform<T> / constant-buffer structs | Needs a CPU constant-buffer view |
ByteAddress | CPU prelude has byte-address types; Goldy ABI packing is not wired |
Interpolated<T> (sampled textures) | No software texture path |
DirectSpatial<T> (storage images) | No software texture path |
Filter / samplers | Texture-only |
[goldy_vertex] / [goldy_fragment] | Compute only |
| Goldy bindless frame table (native wrapper) | CPU uses the CUDA-shaped typed uniform preamble; scheme submit maps bindless indices onto host {data, count} views |
| Broadcast / textures / graphics | Still unsupported on GOLDY_BACKEND=cpu |
Fine rasterization stays GPU-only until textures work. Interlocked / groupshared
behavior follows the Slang CPU prelude (typically mutex or sequential atomics)
and is not a substitute for GPU memory-model testing.
Related
Python Bindings
Goldy provides Python bindings via PyO3, offering a Pythonic API for GPU programming with seamless NumPy integration.
Installation
From PyPI
pip install goldy
From Source
git clone https://github.com/koubaa/goldy.git
cd goldy/python
python -m venv .venv
source .venv/Scripts/activate # platform-specific
pip install -e ".[dev]"
Slang is embedded when the extension is compiled; you do not run build-slang.py for
local development. Rebuild after editing python/src/*.rs with maturin develop.
Requirements
- Python 3.9+
- NumPy 1.20+
- A GPU with Vulkan 1.4+, DX12, or Metal Tier 2+ support (CUDA and WebGPU backends are in progress; Tenstorrent is planned)
Optional Dependencies
pip install goldy[dev] # pytest, pillow
pip install pillow # image output only
Quick Start
import goldy
import numpy as np
instance = goldy.Instance()
device = instance.request_adapter().request_device()
ctx = device.create_context()
retained_pool = goldy.RetainedPool(device)
vertices = np.array([
0.0, -0.5, 1.0, 0.0, 0.0, 1.0,
-0.5, 0.5, 0.0, 1.0, 0.0, 1.0,
0.5, 0.5, 0.0, 0.0, 1.0, 1.0,
], dtype=np.float32)
vertex_parcel = retained_pool.acquire_buffer(vertices, goldy.BufferKind.SCATTERED)[0]
shader = goldy.ShaderModule.from_slang(device, goldy.Builtins.VERTEX_COLOR_2D)
pipeline = goldy.RenderPipeline(device, shader, shader, goldy.RenderPipelineDesc())
readback = retained_pool.acquire_texture(
100, 100, goldy.TextureFormat.RGBA8_UNORM,
goldy.TextureKind.DIRECT, copy_src=True, copy_dst=True,
)
scheme = goldy.Scheme(ctx)
rt = scheme.lease_render_target(100, 100, goldy.TextureFormat.RGBA8_UNORM)
with scheme.render_pass("triangle", rt, goldy.TargetLoad.clear(goldy.Color(0.1, 0.1, 0.2, 1.0))) as rp:
rp.with_parcel(vertex_parcel, goldy.NodeAccess.READ)
rp.set_pipeline(pipeline)
rp.set_vertex_buffer_parcel(0, vertex_parcel)
rp.draw(vertex_count=3)
scheme.copy_to_texture(rt, readback)
memory = goldy.MemoryExchange(ctx)
withdraw = memory.bind_withdraw_texture(scheme, readback)
submission = scheme.submit()
pixels = np.frombuffer(withdraw.claim(submission).consume(), dtype=np.uint8).reshape(100, 100, 4)
NumPy Integration
Creating GPU Parcels from Arrays
vertices = np.array([
# x, y, r, g, b, a
0.0, -0.5, 1.0, 0.0, 0.0, 1.0,
0.5, 0.5, 0.0, 1.0, 0.0, 1.0,
-0.5, 0.5, 0.0, 0.0, 1.0, 1.0,
], dtype=np.float32)
retained_pool = goldy.RetainedPool(device)
parcel = retained_pool.acquire_buffer(vertices, goldy.BufferKind.SCATTERED)
Supported dtypes
| NumPy dtype | Typical use case |
|---|---|
np.float32 | Vertex positions, colors, uniforms |
np.float64 | High-precision data |
np.uint32 | Index buffers, compute data |
np.int32 | Signed integer data |
np.uint16 | 16-bit index buffers |
np.uint8 | Raw byte data |
Reading Results Back to NumPy
Use MemoryExchange.bind_withdraw / bind_withdraw_texture, then claim and consume after submit:
memory = goldy.MemoryExchange(ctx)
withdraw = memory.bind_withdraw(scheme, parcel)
submission = scheme.submit()
output = np.frombuffer(withdraw.claim(submission).consume(), dtype=np.float32)
Performance Tips
- Create once, update often — avoid allocating new parcels every frame. Reuse retained buffers and update via upload schemes when needed.
- Use
np.float32— match the GPU's expected dtype to avoid an extra conversion. - Ensure contiguity — sliced arrays may not be contiguous. Call
np.ascontiguousarray()before uploading if needed.
Compute Shaders
Goldy supports GPU compute from Python using Slang shaders.
Basic Example
import goldy
import numpy as np
instance = goldy.Instance()
device = instance.request_adapter().request_device()
ctx = device.create_context()
data = np.arange(256, dtype=np.float32)
retained_pool = goldy.RetainedPool(device)
parcel = retained_pool.acquire_buffer(data, goldy.BufferKind.SCATTERED)[0]
SHADER = """
import goldy_exp;
[goldy_compute]
[numthreads(64, 1, 1)]
void cs_main(Scattered<float> data, ThreadId id) {
data[id.x] = data[id.x] * 2.0;
}
"""
shader = goldy.ShaderModule.from_slang(device, SHADER)
pipeline = goldy.ComputePipeline(device, shader)
scheme = goldy.Scheme(ctx)
scheme.node("double", pipeline).with_parcel(
parcel, goldy.NodeAccess.READ_WRITE
).dispatch(4, 1, 1)
memory = goldy.MemoryExchange(ctx)
withdraw = memory.bind_withdraw(scheme, parcel)
submission = scheme.submit()
output = np.frombuffer(withdraw.claim(submission).consume(), dtype=np.float32)
Ping-Pong Buffers
For iterative algorithms, alternate two buffer fields as input/output within one scheme (see python/examples/game_of_life.py).
Combining Compute and Graphics
Hybrid compute + render workflows use a single Scheme with both compute nodes and render passes (see python/examples/game_of_life.py and goldy/examples/game_of_life.rs).
Key Differences from Rust
| Aspect | Rust | Python |
|---|---|---|
| Instance creation | Instance::new()? | goldy.Instance() |
| Error handling | Result<T, GoldyError> | Raises goldy.GoldyError |
| Retained buffer | retained_pool.acquire_buffer_with_data(&data, access) | retained_pool.acquire_buffer(numpy_array, access) → Parcel |
| Render pass | scheme.render_pass(...) | with scheme.render_pass(...) as rp: |
| Compute node | scheme.node(...).dispatch(...) | scheme.node(...).with_parcel(...).dispatch(...) |
| Readback | grant.consume(&submission) | grant.consume(submission) |
| Resource lifetime | Explicit Arc<Device> ownership | Managed by Python GC via PyO3 |
Backend Selection
Goldy auto-selects the best backend per platform (DX12 on Windows, Vulkan on Linux). Override with GOLDY_BACKEND:
import os
os.environ["GOLDY_BACKEND"] = "vulkan" # set before importing goldy
import goldy
instance = goldy.Instance()
API Reference
Core Classes
Instance
instance = goldy.Instance()
instance.backend_type # BackendType (Vulkan, DX12, Metal; CUDA and WebGPU in progress)
instance.enumerate_adapters() # list of AdapterInfo
instance.request_adapter() # Adapter
Device / Context
device = instance.request_adapter().request_device()
ctx = device.create_context()
RetainedPool and Parcel
pool = goldy.RetainedPool(device)
parcel = pool.acquire_buffer(data, access) # data: numpy array or bytes
parcel.byte_size # int (bytes)
Scheme
scheme = goldy.Scheme(ctx)
rt = scheme.lease_render_target(w, h, goldy.TextureFormat.RGBA8_UNORM)
with scheme.render_pass("main", rt, goldy.TargetLoad.clear(goldy.Color.BLACK)) as rp:
rp.with_parcel(buf, goldy.NodeAccess.READ)
rp.set_pipeline(pipeline)
rp.draw(vertex_count=3)
scheme.node("update", compute_pipeline).with_parcel(
buf, goldy.NodeAccess.READ_WRITE
).dispatch(wg_x, wg_y, 1)
surface = goldy.SurfaceExchange.from_glfw(ctx, window)
present = surface.bind_render_target(scheme, rt)
submission = scheme.submit()
present.claim(submission).consume()
ShaderModule / RenderPipeline / ComputePipeline
Standard pipeline construction — see python/examples/triangle_headless.py.
Enums
goldy.DeviceType.DISCRETE_GPU | INTEGRATED_GPU | CPU | OTHER
goldy.TextureFormat.RGBA8_UNORM | RGBA8_UNORM_SRGB | BGRA8_UNORM
goldy.BufferKind.SCATTERED | BROADCAST
goldy.NodeAccess.READ | WRITE | READ_WRITE | OVERWRITE
Exceptions
All errors are raised as goldy.GoldyError:
try:
device = instance.request_adapter().request_device()
except goldy.GoldyError as e:
print(f"GPU error: {e}")
.NET Bindings
Goldy provides first-class C# bindings via P/Invoke interop over the native Rust FFI layer.
Installation
NuGet Package
dotnet add package Goldy
Or add to your .csproj directly:
<PackageReference Include="Goldy" Version="0.2.*" />
The NuGet package bundles native Goldy + Slang libraries for all supported platforms — no separate native installation is needed.
Building from Source
cargo build --package goldy-ffi --release
dotnet add reference path/to/goldy/dotnet/Goldy/Goldy.csproj
Requirements
- .NET 8.0 or later
- Windows x64, Linux x64, or macOS (x64 / arm64)
- A GPU with Vulkan 1.4+, DX12, or Metal Tier 2+ support (CUDA and WebGPU backends are in progress; Tenstorrent is planned)
Quick Start
Headless Rendering
using Goldy;
using var instance = new Instance();
using var device = instance.RequestAdapter().RequestDevice();
using var ctx = device.CreateContext();
using var retainedPool = new RetainedPool(device);
using var readback = retainedPool.AcquireTexture(
100, 100, TextureFormat.Rgba8Unorm, TextureKind.Direct,
TextureFlags.CopySrc | TextureFlags.CopyDst);
using var scheme = new Scheme(ctx);
using var rt = scheme.LeaseRenderTarget(100, 100, TextureFormat.Rgba8Unorm);
using (var pass = scheme.RenderPass("clear", rt))
pass.Clear(Color.CornflowerBlue);
scheme.CopyToTexture(rt, readback);
using var memory = new MemoryExchange(ctx);
using var withdraw = memory.BindWithdrawTexture(scheme, readback);
using var submission = scheme.Submit();
using var claim = withdraw.Claim(submission);
byte[] pixels = claim.Consume();
See Goldy.Examples/TriangleHeadless.cs for a full triangle readback demo.
Windowed Rendering
Record a retained scheme once, submit each frame, consume the present grant:
using var scheme = new Scheme(ctx);
var (sceneRt, present) = RecordScheme(scheme, swapchain, pipeline, vertexParcel, screen, bg);
using var submission = scheme.Submit();
present.Consume(submission);
See Goldy.Examples/TriangleWindow.cs and GameOfLifeWindow.cs.
Shaders (Slang)
Goldy uses Slang as its shader language across all backends:
var source = """
[shader("vertex")]
float4 vs_main(float2 pos : POSITION) : SV_Position {
return float4(pos, 0.0, 1.0);
}
[shader("fragment")]
float4 fs_main() : SV_Target {
return float4(1.0, 0.5, 0.0, 1.0);
}
""";
using var shader = new ShaderModule(device, source);
using var pipeline = new RenderPipeline(device, shader, new RenderPipelineDesc
{
TargetFormat = TextureFormat.Rgba8Unorm,
Topology = PrimitiveTopology.TriangleList,
});
Resource Management
All Goldy objects implement IDisposable. Use using declarations or using blocks to ensure GPU resources are released promptly:
using var device = instance.RequestAdapter().RequestDevice();
using var ctx = device.CreateContext();
using var scheme = new Scheme(ctx);
Key Differences from Rust
| Aspect | Rust | C# |
|---|---|---|
| Instance creation | Instance::new()? | new Instance() |
| Error handling | Result<T, GoldyError> | Exceptions |
| Device lifetime | Arc<Device> | IDisposable / using |
| Retained buffer | retained_pool.acquire_buffer_with_data(&data, access) | retainedPool.AcquireBuffer<T>(data, access) → Parcel |
| Submission | scheme.submit()? | scheme.Submit() → SchemeSubmission |
| Enums | DeviceType::DiscreteGpu | DeviceType.DiscreteGpu |
API Reference
Scheme
public sealed class Scheme : IDisposable
{
public Scheme(Context ctx);
public SchemeComputeNodeScope ComputeNode(string label, ComputePipeline pipeline);
public SchemeRenderTargetLease LeaseRenderTarget(uint width, uint height, TextureFormat format, ...);
public SchemeRenderPassScope RenderPass(string label, SchemeRenderTargetLease lease);
public void CopyToTexture(SchemeRenderTargetLease src, Texture dst);
public void CopyToPresent(SchemeRenderTargetLease src, PresentLease dst);
public SchemeSubmission Submit();
}
public sealed class MemoryExchange : IDisposable
{
public MemoryExchange(Context ctx);
public WithdrawTransaction BindWithdraw(Scheme scheme, Parcel parcel);
public WithdrawTransaction BindWithdrawTexture(Scheme scheme, Texture texture);
public DepositTransaction BindDepositBuffer(Scheme scheme, Parcel destination, ulong capacity);
}
public sealed class WithdrawTransaction
{
public WithdrawClaim Claim(SchemeSubmission submission);
}
public sealed class WithdrawClaim : IDisposable
{
public byte[] Consume();
public void Discard();
}
SchemeRenderPassScope / SchemeComputeNodeScope
using (var pass = scheme.RenderPass("main", rt))
{
pass.WithParcel(vertexParcel, NodeAccess.Read);
pass.Clear(Color.CornflowerBlue);
pass.SetPipeline(pipeline);
pass.SetVertexBuffer(0, vertexParcel);
pass.Draw(3);
}
using (var node = scheme.ComputeNode("update", computePipeline))
{
node.WithParcel(stateBuf, NodeAccess.ReadWrite);
node.Dispatch(wgX, wgY, 1);
}
SurfaceExchange / Transaction / Claim
using var surface = GlfwSurfaceExchange.Create(ctx, window);
var present = surface.BindRenderTarget(scheme, sceneRt);
// each frame:
using var submission = scheme.Submit();
present.Claim(submission).Consume();
Graphics and compute both go through Scheme.
Enums
public enum DeviceType { DiscreteGpu, IntegratedGpu, Cpu, Other }
public enum BackendType { Vulkan, Metal, Dx12 } // CUDA and WebGPU in progress in core Goldy
public enum BufferKind { Scattered, Broadcast }
public enum NodeAccess { Read, Write, ReadWrite, Overwrite }
Headless vs windowed submission
Headless: record a scheme, bind a MemoryExchange withdraw, Submit(), then withdraw.Claim(submission).Consume().
Windowed: record once with SurfaceExchange.BindRenderTarget (or BindDestination for compute-to-surface); each frame call Submit(), then transaction.Claim(submission).Consume().
C++ Bindings
Goldy provides C and C++ bindings over the native goldy-ffi library. The C++ layer (goldy.hpp) wraps the auto-generated C API (goldy.h) with RAII types and exceptions.
Installation
vcpkg
# Add to your vcpkg.json
{
"dependencies": ["goldy"]
}
# Or install directly
vcpkg install goldy
Conan
# conanfile.txt
[requires]
goldy/0.2.0
Building from Source
# Build the native library (requires Rust: https://rustup.rs)
cargo build --package goldy-ffi --release
# Configure and build examples
cd cpp
cmake -B build -DGOLDY_BUILD_FROM_SOURCE=ON
cmake --build build --target triangle_headless
On Windows, if MSVC cannot find standard headers, use x64 Native Tools Command Prompt for VS 2022 or run cpp/build.bat, which sets up the MSVC environment before invoking CMake.
Requirements
- C++20 compiler (MSVC 2019+, GCC 10+, Clang 12+)
- Rust toolchain (for building
goldy-ffifrom source) - A GPU with Vulkan 1.4+, DX12, or Metal Tier 2+ support (CUDA and WebGPU backends are in progress; Tenstorrent is planned)
- Slang is embedded in the Goldy build — no separate SDK install for normal use
Native Library Deployment
The goldy_ffi shared library and Slang runtime DLLs must be on the loader path at runtime. CMake post-build steps copy them next to example binaries when building from source. For your own applications, ship goldy_ffi.dll / libgoldy_ffi.so / libgoldy_ffi.dylib alongside your executable, with the Slang shared libraries in the same directory.
Quick Start
Headless Rendering
#include <goldy.hpp>
#include <cstdint>
#include <iostream>
struct Vertex {
float position[2];
float color[4];
};
int main() {
try {
goldy::Instance instance;
goldy::Device device = instance.request_adapter().request_device();
goldy::Context ctx(device);
const Vertex vertices[] = {
{{0.0f, -0.5f}, {1.0f, 0.0f, 0.0f, 1.0f}},
{{-0.5f, 0.5f}, {0.0f, 1.0f, 0.0f, 1.0f}},
{{0.5f, 0.5f}, {0.0f, 0.0f, 1.0f, 1.0f}},
};
goldy::RetainedPool pool(device);
goldy::Buffer vertex_buffer = pool.acquire_buffer_with_data(
std::span<const Vertex>(vertices),
goldy::BufferKind::Scattered);
goldy::ShaderModule shader(device, goldy::ShaderModule::builtin_vertex_color_2d());
GoldyVertexAttribute attributes[] = {
{0, GOLDY_VERTEX_FORMAT_FLOAT32X2, 0},
{1, GOLDY_VERTEX_FORMAT_FLOAT32X4, static_cast<uint32_t>(sizeof(float) * 2)},
};
GoldyRenderPipelineDesc desc{};
desc.vertex_attributes = attributes;
desc.vertex_attribute_count = static_cast<uint32_t>(std::size(attributes));
desc.vertex_stride = sizeof(Vertex);
desc.topology = GOLDY_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
desc.target_format = GOLDY_TEXTURE_FORMAT_RGBA8_UNORM;
goldy::RenderPipeline pipeline(device, shader, shader, desc);
GoldyTextureFlags readback_flags{};
readback_flags._0 = goldy::TextureFlags::CopySrc | goldy::TextureFlags::CopyDst;
goldy::Texture readback = pool.acquire_texture(
800, 600, GOLDY_TEXTURE_FORMAT_RGBA8_UNORM,
GOLDY_TEXTURE_KIND_DIRECT, readback_flags);
goldy::Scheme scheme(ctx);
goldy::SchemeRenderTargetLease rt = scheme.lease_render_target(
800, 600, GOLDY_TEXTURE_FORMAT_RGBA8_UNORM, nullptr);
{
auto pass = scheme.render_pass("triangle", rt, goldy::TargetLoad::clear(goldy::Color::cornflower_blue()));
pass.with_field(vertex_buffer, 0, goldy::NodeAccess::Read)
.set_pipeline(pipeline)
.set_vertex_buffer(0, vertex_buffer)
.draw(0, 3);
}
scheme.copy_to_texture(rt, readback);
goldy::MemoryExchange memory(ctx);
goldy::WithdrawTransaction withdraw = memory.bind_withdraw_texture(scheme, readback);
goldy::SchemeSubmission submission = scheme.submit();
goldy::WithdrawBytes bytes = withdraw.claim(submission).consume();
std::cout << "Rendered " << bytes.size() << " bytes\n";
return 0;
} catch (const goldy::Exception& e) {
std::cerr << "Goldy error: " << e.what() << '\n';
return 1;
}
}
See cpp/examples/triangle_headless.cpp for the full example.
Windowed Rendering
Use goldy::SurfaceExchange for swapchain presentation. See cpp/examples/triangle.cpp (Win32 / macOS).
Shaders (Slang)
Goldy uses Slang as its shader language across all backends:
const char* source = R"(
import goldy_exp;
[goldy_vertex]
float4 vs_main(Vertex2D v) : SV_Position {
return float4(v.position, 0.0, 1.0);
}
[goldy_fragment]
float4 fs_main(Vertex2D v) : SV_Target {
return float4(v.color);
}
)";
goldy::ShaderModule shader(device, source);
Resource Management
All C++ wrapper types use RAII — destructors release GPU handles automatically. Operations that can fail throw goldy::Exception:
try {
goldy::Instance instance;
// ...
} catch (const goldy::Exception& e) {
std::cerr << "Goldy error: " << e.what() << "\n";
}
Key Differences from Rust
| Aspect | Rust | C++ |
|---|---|---|
| Instance creation | Instance::new()? | goldy::Instance instance |
| Error handling | Result<T, GoldyError> | goldy::Exception |
| Device lifetime | Arc<Device> | RAII destructor |
| Retained buffer | pool.acquire_buffer_with_data(&data, access) | pool.acquire_buffer_with_data(span, access) |
| Render pass | scheme.render_pass(...) | scheme.render_pass(...) (RAII scope) |
| Readback | claim.consume(&submission) | withdraw.claim(submission).consume() |
API Reference
Core Classes
| Class | Description |
|---|---|
goldy::Instance | Entry point, adapter enumeration |
goldy::Device / goldy::Context | GPU device and execution context |
goldy::RetainedPool | Retained buffer/texture acquisition |
goldy::RecordBuilder | Partitioned buffer records (ping-pong fields) |
goldy::Scheme | Retained dependency graph |
goldy::MemoryExchange | CPU↔GPU withdraw/deposit |
goldy::SurfaceExchange | Window swapchain (Win32 / macOS / Wayland) |
goldy::ShaderModule | Compiled Slang shader |
goldy::RenderPipeline / goldy::ComputePipeline | Graphics/compute pipelines |
goldy::Sampler | Texture sampler |
Scheme
goldy::Scheme scheme(ctx);
goldy::SchemeRenderTargetLease rt = scheme.lease_render_target(w, h, format, nullptr);
{
auto pass = scheme.render_pass("main", rt, goldy::TargetLoad::clear(color));
pass.with_field(buf, 0, goldy::NodeAccess::Read)
.set_pipeline(pipeline)
.set_vertex_buffer(0, buf)
.draw(0, 3);
}
auto node = scheme.compute_node("update", compute_pipeline);
node.with_field(buf, 0, goldy::NodeAccess::ReadWrite)
.dispatch(wg_x, wg_y, 1);
goldy::SchemeSubmission submission = scheme.submit();
MemoryExchange / SurfaceExchange
goldy::MemoryExchange memory(ctx);
goldy::WithdrawTransaction withdraw = memory.bind_withdraw_texture(scheme, texture);
goldy::SchemeSubmission submission = scheme.submit();
goldy::WithdrawBytes pixels = withdraw.claim(submission).consume();
goldy::SurfaceExchange surface(ctx, window_handle, width, height);
auto present = surface.bind_render_target(scheme, rt);
goldy::SchemeSubmission submission = scheme.submit();
present.claim(submission).consume();
Raw C API
For C code or when you need low-level control, use goldy.h directly. Failed calls return null or error codes; call goldy_get_last_error() for details:
#include <goldy.h>
GoldyInstance* instance = goldy_instance_create();
if (!instance) {
const char* error = goldy_get_last_error();
// handle error
}
GoldyAdapterInfo info = {};
goldy_instance_get_adapter(instance, 0, &info);
GoldyDevice* device = goldy_instance_create_device_for_adapter(instance, info.id);
// ...
goldy_device_destroy(device);
goldy_instance_destroy(instance);
Platform Support
| Platform | Headless Scheme | Windowed Surface |
|---|---|---|
| Windows x64 | Yes | Yes |
| Linux x64 | Yes | Yes (Wayland; X11 not supported) |
| macOS x64 / ARM64 | Yes | Yes |
Backend Selection
Goldy auto-selects the best backend per platform. Override with GOLDY_BACKEND (set before creating an Instance):
GOLDY_BACKEND=vulkan ./my_app
When building goldy-ffi for a specific platform, pass backend features through:
cargo build -p goldy-ffi --no-default-features --features vulkan
Examples
| Example | Description |
|---|---|
cpp/examples/triangle_headless.cpp | Offscreen triangle + readback |
cpp/examples/triangle.cpp | Windowed triangle (GLFW) |
cpp/examples/compute_simple.cpp | Compute dispatch |
cpp/examples/game_of_life.cpp | Hybrid compute + render |
Rust FFI Client
goldy-ffi-client is a Rust crate that loads the goldy-ffi native library at runtime and exposes the same RAII API as the core goldy crate. Instead of statically linking the goldy library, it calls the stable C ABI through libloading (LoadLibrary on Windows, dlopen on Unix).
This is the same native boundary used by the C++ and .NET bindings. Python is different — it links the core goldy crate directly via PyO3.
When to Use
| Use case | Crate |
|---|---|
| Normal Rust applications | goldy (static link, published on crates.io) |
| FFI integration tests | goldy-ffi-client |
| Validating the C ABI from Rust | goldy-ffi-client |
| Swapping the native library without recompiling the client | goldy-ffi-client |
The ffi-client API mirrors the core Rust crate: Instance, Scheme, RetainedPool, MemoryExchange, SurfaceExchange, and the rest of the Fondaco programming model are available with the same names and patterns.
Installation
goldy-ffi-client is a workspace crate — it is not published to crates.io. Add it as a path dependency:
[dependencies]
goldy-ffi-client = { path = "../ffi-client" }
Build the native library first:
cargo build -p goldy-ffi
Then build or run ffi-client examples:
cd ffi-client
cargo run --example triangle_headless
Requirements
- Rust 2021 edition
- A built
goldy_ffishared library (goldy_ffi.dll/libgoldy_ffi.so/libgoldy_ffi.dylib) - A GPU with Vulkan 1.4+, DX12, or Metal Tier 2+ support (CUDA and WebGPU backends are in progress; Tenstorrent is planned)
Library Discovery
At runtime, ffi-client searches for the native library in this order:
GOLDY_FFI_PATH— full path to thegoldy_ffidylibGOLDY_FFI_LIB_DIR— compile-time directory from thegoldy-ffibuild- The directory containing the running executable
On Windows, ffi-client also calls SetDllDirectoryW so Slang DLLs next to goldy_ffi.dll are found.
# Point at a specific build of the native library
GOLDY_FFI_PATH=/path/to/libgoldy_ffi.so cargo run --example triangle_headless
Quick Start
Headless Rendering
use goldy_ffi_client::{ shader::builtins, BufferKind, Color, Context, DeviceDescriptor, Instance, NodeAccess, RenderPipeline, RenderPipelineDesc, RequestAdapterOptions, RetainedPool, Scheme, ShaderModule, TargetLoad, TextureFlags, TextureFormat, TextureKind, Vertex2D, }; fn main() -> goldy_ffi_client::Result<()> { let instance = Instance::new()?; let device = instance .request_adapter(&RequestAdapterOptions::default())? .request_device(&DeviceDescriptor::default())?; let ctx = Context::new(&device)?; let vertices = [ Vertex2D { position: [0.0, -0.5], color: [1.0, 0.0, 0.0, 1.0] }, Vertex2D { position: [-0.5, 0.5], color: [0.0, 1.0, 0.0, 1.0] }, Vertex2D { position: [0.5, 0.5], color: [0.0, 0.0, 1.0, 1.0] }, ]; let mut pool = RetainedPool::new(&device)?; let vertex_buffer = pool.acquire_buffer_with_data(&vertices, BufferKind::Scattered)?; let readback = pool.acquire_texture( 64, 64, TextureFormat::Rgba8Unorm, TextureKind::Direct, TextureFlags::COPY_SRC.union(TextureFlags::COPY_DST), None, )?; let shader = ShaderModule::from_slang(&device, builtins::VERTEX_COLOR_2D)?; let pipeline = RenderPipeline::new( &device, &shader, &shader, &RenderPipelineDesc { vertex_layout: Vertex2D::layout(), target_format: TextureFormat::Rgba8Unorm, ..Default::default() }, )?; let mut scheme = Scheme::new(&ctx)?; let rt = scheme.lease_render_target(64, 64, TextureFormat::Rgba8Unorm, None)?; { let mut pass = scheme.render_pass("triangle", &rt, TargetLoad::Clear(Color::BLACK)); pass.with_buffer(&vertex_buffer, NodeAccess::Read); pass.set_pipeline(&pipeline); pass.set_vertex_buffer(0, &vertex_buffer); pass.draw(0..3, 0..1); pass.finish_recorded(); } scheme.copy_to_texture(&rt, &readback)?; let memory = goldy_ffi_client::MemoryExchange::new(&ctx)?; let withdraw = memory.bind_withdraw_texture(&mut scheme, &readback)?; let mut submission = scheme.submit()?; let pixels = withdraw.claim(&mut submission)?.consume()?; println!("Rendered {} bytes", pixels.len()); Ok(()) }
See ffi-client/examples/triangle_headless.rs for the full example.
Windowed Rendering
See ffi-client/examples/triangle.rs and ffi-client/examples/game_of_life.rs (winit).
Compute
#![allow(unused)] fn main() { use goldy_ffi_client::{ComputePipeline, Context, Instance, MemoryExchange, NodeAccess, Scheme, ShaderModule}; let mut scheme = Scheme::new(&ctx)?; let mut node = scheme.compute_node("double", &pipeline); node.with_buffer(&buf, NodeAccess::ReadWrite); node.dispatch(1, 1, 1); let memory = MemoryExchange::new(&ctx)?; let withdraw = memory.bind_withdraw(&mut scheme, &buf.field(0)?)?; let mut submission = scheme.submit()?; let bytes = withdraw.claim(&mut submission)?.consume()?; }
See ffi-client/examples/compute_simple.rs.
Resource Management
All ffi-client types use RAII via Drop. Errors are returned as goldy_ffi_client::Result<T> with GoldyError — the same pattern as the core goldy crate.
Key Differences from Core goldy
| Aspect | goldy | goldy-ffi-client |
|---|---|---|
| Linking | Static (compiled into your binary) | Dynamic (libloading at runtime) |
| Distribution | crates.io | Workspace path dependency |
| API surface | Reference implementation | Mirrors core API over C ABI |
| Native library | Embedded in your binary | Separate goldy_ffi dylib required |
| Crate name | goldy | goldy_ffi_client |
Functionally, application code looks nearly identical. The main difference is build and deployment: ffi-client binaries need the goldy_ffi shared library (and Slang DLLs) available at runtime.
Backend Selection
GOLDY_BACKEND works the same as with the core crate — set it before creating an Instance:
GOLDY_BACKEND=vulkan cargo run --example triangle_headless
When building goldy-ffi, pass backend features through:
cargo build -p goldy-ffi --no-default-features --features vulkan
Examples
| Example | Description |
|---|---|
ffi-client/examples/triangle_headless.rs | Offscreen triangle + readback |
ffi-client/examples/triangle.rs | Windowed triangle (winit) |
ffi-client/examples/compute_simple.rs | Compute dispatch |
ffi-client/examples/game_of_life.rs | Hybrid compute + render (windowed) |
ffi-client/examples/game_of_life_headless.rs | Game of Life readback |
Examples Gallery
Goldy ships with 21 Rust examples demonstrating scheme recording, compute-to-surface, graphics pipelines, and multi-window workflows. Every example uses Slang shaders and runs on shipped backends (Vulkan 1.4+, DX12, Metal Tier 2+). CUDA and WebGPU backends are in progress; Tenstorrent is planned.
Running Examples
cd goldy
cargo run --features examples --example <name> --release
All windowed examples support Escape to exit and automatic window-resize handling.
Bindless Basics
These examples cover fundamental Goldy patterns: vertex buffers, surfaces, uniforms, and fragment shaders.
| Example | What it demonstrates | Source |
|---|---|---|
triangle | Minimal windowed program: retained scheme, offscreen render pass, present via SurfaceExchange. | triangle.rs |
gradient | Animated full-screen gradient driven by a time uniform. Uses vertex-less rendering and optional GOLDY_VALIDATE_LAYOUTS. | gradient.rs |
checkerboard | Procedural animated checkerboard via UV distortion in a fragment shader. | checkerboard.rs |
Compute Workflows
Examples that use ComputePipeline and Scheme for GPU-side data processing, including compute-to-surface.
| Example | What it demonstrates | Source |
|---|---|---|
compute_particles | Compute updates particle positions; graphics renders instanced quads. Retained scheme scheduling. | compute_particles.rs |
game_of_life | Conway's Game of Life on the GPU with ping-pong sub-views in one retained mosaic parcel. | game_of_life.rs |
compute_to_surface | Pure compute rendering — no RenderPipeline. Writes swapchain via SurfaceExchange::bind_destination. | compute_to_surface.rs |
Graphics Pipelines
Classic rendering techniques: depth testing, textures, instancing, and 3D projection.
| Example | What it demonstrates | Source |
|---|---|---|
solid_cube | Solid 3D cube with per-face colors and depth buffer. | solid_cube.rs |
spinning_cube | 3D wireframe cube using line primitives. | spinning_cube.rs |
depth_quads | Depth buffer proves draw-order independence. | depth_quads.rs |
textured_quad | Procedural checkerboard texture on a quad. | textured_quad.rs |
instancing | GPU-driven instancing with compute-updated transforms. | instancing.rs |
bouncing_lines | LINE_LIST topology with simple physics. | bouncing_lines.rs |
waveform | LINE_STRIP waveform visualizer. | waveform.rs |
Advanced Patterns
Fragment Shader Effects
| Example | What it demonstrates | Source |
|---|---|---|
plasma | Demoscene plasma effect. | plasma.rs |
tunnel | Flying-through-a-tunnel polar-coordinate effect. | tunnel.rs |
metaballs | Metaball field rendering. | metaballs.rs |
mandelbrot | Interactive Mandelbrot explorer. | mandelbrot.rs |
Interactive and Multi-Window
| Example | What it demonstrates | Source |
|---|---|---|
digital_clock | 7-segment clock display. | digital_clock.rs |
starfield | 3D starfield with depth. | starfield.rs |
particles | Rain/snow particle system. | particles.rs |
multi_window | Multiple windows sharing one device. | multi_window.rs |
Headless and Validation
| Example | What it demonstrates | Source |
|---|---|---|
headless_triangle | Offscreen render + CPU readback via MemoryExchange. | headless_triangle.rs |
scheme_screenshot | Scheme-based screenshot capture for tests. | scheme_screenshot.rs |
Run all windowed examples interactively:
./run_all_examples.sh
Motivation
Goldy implements the Fondaco Machine on modern GPUs: programs describe parcels and schemes; the runtime manages the physical medium, derives precedences from ownership, and mediates present/readback through exchanges. For the normative machine spec see the Machine Specification; for what Goldy ships today see the runtime mapping.
The Problem with "Modern" Graphics APIs
DX12, Vulkan, and Metal are commonly called modern APIs, but they were designed over a decade ago for hardware that has since changed dramatically. The GPU architectures those APIs targeted lacked coherent caches, bindless descriptors, and 64-bit pointers. The APIs compensated with layers of indirection — descriptor sets, render pass objects, explicit image layout transitions, pipeline layouts as first-class objects — that served as hints and contracts for hardware that needed them.
Furthermore, high-performance GPU programs tend to converge on the same shape, whether they are written with PyTorch, CUDA, Metal, Vulkan, or something else. They are not best understood as a stream of API calls. They are graphs whose nodes are kernels, copies, and foreign operations, and whose edges describe data dependencies. Independent nodes may run concurrently; dependent nodes require an ordering mechanism such as a barrier, event, semaphore, or stream dependency.
PyTorch makes this especially visible. A model is a graph of tensor operations; autograd constructs another graph for the backward pass, and compilers such as TorchInductor capture, specialize, fuse, and schedule that work. Beneath it, CUDA libraries submit kernels and transfers to streams, use events to express cross-stream dependencies, reuse temporary allocations according to tensor lifetimes, and fuse adjacent operations to reduce launch and memory-traffic costs. Hand-written CUDA programs eventually acquire the same machinery: stream graphs, memory pools, dependency tracking, and explicit synchronization around shared buffers.
Graphics workloads arrive at the same structure from another direction. A frame graph records render, compute, copy, and presentation passes together with how each pass reads or writes resources. From those declarations, an engine derives execution order, barriers, transient-memory aliasing, queue placement, and opportunities for overlap. The APIs differ, but the optimization problem is the same: preserve data dependencies while minimizing synchronization, allocation, launch, and memory-traffic costs.
This convergence suggests that the graph and its resource relationships are the durable program model. Descriptor updates, barriers, command buffers, streams, and semaphores - and yes, sometimes even CPU waits - are mechanisms a runtime can derive from that model for a particular GPU.
Yet every application using graphics APIs still pays the complexity cost of the old model, and do not idiomatically map to the model of the best GPU programs.
Why Bindless Matters
Traditional GPU programming organizes resources into descriptor sets — fixed layouts of bindings that must be declared ahead of time, allocated from pools, and swapped between draw calls. This model creates a cascade of complexity:
- Pipeline layout explosion: Every unique combination of descriptor set layouts produces a distinct pipeline layout, and each pipeline layout dimension multiplies the total pipeline state permutation count.
- CPU overhead: Updating and binding descriptor sets each frame is a significant portion of CPU-side draw call cost.
- Shader inflexibility: Shaders are coupled to their binding layout; changing which resources a shader accesses means changing the pipeline.
Bindless resource access replaces all of this with a single concept: resources live in GPU-visible memory, and shaders access them by index. There are no set layouts to declare, no pools to manage, no binding points to track. A shader that needs buffer #7 just reads slot 7 from a flat descriptor heap.
This isn't exotic — it's how game engines have been working internally for years. Goldy makes it the public API rather than hiding it behind compatibility abstractions.
Why a Dependency Graph (Scheme)
Bindless access means shaders can read any resource at any time. The traditional model of inserting barriers at the call site ("I'm about to read this buffer, so transition it now") breaks down when the set of resources a dispatch touches isn't known until the shader runs.
Goldy uses a retained scheme — a dependency graph you record once and submit many times — to solve this. You declare nodes and their resource dependencies; Goldy derives the barriers, layout transitions, and execution order automatically. This is both safer (no missed barriers) and simpler (no manual synchronization) than the alternative.
The scheme also enables Goldy to batch and reorder work across the frame, which matters for compute-heavy workloads where multiple dispatches feed into each other before anything reaches the screen.
Why Slang
The shader language landscape is fragmented. GLSL, HLSL, MSL, and WGSL each target a subset of platforms, and none is a clean superset of the others. Libraries that support multiple shading languages maintain translation layers and per-language workarounds, which is a significant source of bugs and complexity.
Slang solves this at the source level. A single Slang source file compiles to SPIR-V (Vulkan), DXIL (DX12), and MSL (Metal). It uses HLSL-familiar syntax with additions that matter for modern GPU programming:
| Feature | Why it matters |
|---|---|
Modules and import | True separate compilation, no #include fragility |
| Generics | Type-safe reusable shader code |
| Automatic differentiation | First-class for ML and physics workloads |
| Khronos governance | Long-term stability and active development |
By committing to Slang as the sole shader language, Goldy eliminates an entire category of cross-platform bugs and keeps its codebase focused on GPU work rather than shader translation. By embedding a verified compatible version of slang, goldy simplifies packaging.
What Goldy Sheds
Goldy's bindless model and modern-hardware baseline make several traditional GPU programming concepts unnecessary. These aren't missing features — they're intentional design choices that keep the API small and the programming model coherent.
No Descriptor Set Management
Traditional APIs require you to declare descriptor set layouts, allocate descriptor pools, write descriptor sets, and bind them before each draw or dispatch. A typical Vulkan pipeline touches three to four descriptor set objects before anything reaches the GPU.
Goldy replaces all of this with a flat bindless heap. Resources get a slot index when created, and shaders access them by that index. There are no layouts, no pools, no binding calls.
// Shader receives resources by index — no descriptor sets
[goldy_compute]
[numthreads(64, 1, 1)]
void cs_main(Scattered<Particle> particles, ThreadId id) {
particles[id.x].position += particles[id.x].velocity;
}
This also eliminates pipeline layouts as objects. In Vulkan, each unique combination of descriptor set layouts produces a pipeline layout, which is baked into the pipeline at creation time. Goldy's single global bindless layout means one pipeline layout for all pipelines.
No Manual Barrier Insertion
In Vulkan and DX12, you manually insert memory barriers and image layout transitions to tell the GPU when a resource changes from "written by compute" to "read by fragment" (or any other transition). Missing a barrier is a silent correctness bug; inserting too many is a performance bug.
Goldy's scheme (dependency graph) handles this automatically. You declare what each node reads and writes; Goldy derives the minimal set of barriers and transitions. This is both safer and typically more efficient than hand-placed barriers, because the scheme has a global view of the frame.
No Shader Permutation Systems
Traditional engines maintain thousands of shader variants — combinations of feature flags, render pass compatibility, descriptor set layout versions, and pipeline state. Some ship dedicated cloud infrastructure just to compile and cache them all.
Goldy collapses most of the dimensions that drive permutation counts:
| Traditional dimension | Goldy equivalent |
|---|---|
| Render pass compatibility | Dynamic rendering — no render pass objects |
| Descriptor set layout | One global bindless layout |
| Pipeline layout | Implicit from the global layout |
| Viewport/scissor state | Dynamic state, not baked into PSO |
What remains — shader source × vertex format × target format × depth config — is a small, manageable space. Goldy addresses pipeline variety by having fewer pipelines, not by building infrastructure to manage many variants.
Minimal Pipeline State Management
A Vulkan VkGraphicsPipelineCreateInfo touches blend state, depth/stencil state, rasterizer state, multisample state, input assembly, viewport/scissor, dynamic state flags, render pass, subpass, pipeline layout, and shader stages. Many of these are baked in at pipeline creation time, producing the combinatorial explosion that drives PSO caches.
Goldy uses dynamic rendering and dynamic state to move viewport, scissor, and render target configuration out of the pipeline object. The remaining pipeline state is intentionally minimal:
#![allow(unused)] fn main() { let pipeline = RenderPipeline::new(&device, &shader, &shader, &desc)?; }
Blend mode, depth testing, and vertex format are still part of the pipeline — they represent genuine hardware configuration. But the many compatibility dimensions that traditional APIs bake in are gone.
No Separate Compute API
OpenCL introduced compute to GPUs as an entirely separate API with its own device model, memory model, and dispatch semantics. Even "unified" APIs like Vulkan treat compute as a second-class citizen — compute pipelines and graphics pipelines share almost no code paths.
In Goldy, compute is a first-class citizen on the same footing as graphics. Compute shaders use the same bindless resource model, the same buffer types, and the same scheme. A compute dispatch that writes to a buffer and a draw call that reads from it are just nodes in the same dependency graph.
#![allow(unused)] fn main() { // Compute updates particles, render draws them — same scheme scheme.node("update", &compute_pipeline) .with_parcel(&particle_buf, NodeAccess::ReadWrite) .dispatch(workgroups, 1, 1); let mut pass = scheme.render_pass("draw", &scene_rt); pass.with_parcel(&particle_buf, NodeAccess::Read); // ... }
The Design Principle
Each of these omissions follows the same logic: if modern hardware doesn't need a concept for correctness or performance, Goldy doesn't expose it. The result is an API where the concepts that remain — buffers, textures, shaders, pipelines, scheme — each carry their weight.
Goldy vs wgpu
Both Goldy and wgpu are Rust GPU libraries with multi-backend support. They make different tradeoffs that suit different use cases.
At a Glance
| wgpu | Goldy | |
|---|---|---|
| Identity | WebGPU implementation for Rust | Modern Rust GPU library |
| Spec governance | W3C WebGPU specification | Independent, opinionated |
| Browser support | Yes (WebGPU) | No |
| Minimum hardware | Wide compatibility (Vulkan 1.0+) | Modern only (Vulkan 1.4+, DX12, Metal 2+) |
| Shader language | WGSL (primary), SPIR-V, GLSL, naga | Slang (compiles to SPIR-V, DXIL, MSL) |
| Resource model | Descriptor-based (bind groups) | Typed bindless |
| Synchronization | Manual pass ordering | Retained scheme (dependency graph) |
| Metal support | Via MoltenVK or wgpu-hal | Native Metal backend |
| Compute model | Supported but secondary | First-class (compute-to-surface) |
Resource Binding: Descriptors vs Bindless
wgpu uses bind groups — the WebGPU equivalent of Vulkan descriptor sets. You declare a bind group layout, create bind groups that match it, and bind them before each draw or dispatch:
#![allow(unused)] fn main() { // wgpu: declare layout, create group, bind before draw let layout = device.create_bind_group_layout(&desc); let group = device.create_bind_group(&wgpu::BindGroupDescriptor { layout: &layout, entries: &[wgpu::BindGroupEntry { binding: 0, resource: buffer.as_entire_binding() }], .. }); pass.set_bind_group(0, &group, &[]); }
Goldy uses bindless access. Resources get a slot index at creation time, and shaders access them directly by index. There are no layouts, groups, or binding calls:
#![allow(unused)] fn main() { // Goldy: bindless parcel already has a slot; bind it in the scheme let mut pool = RetainedPool::new(device.clone()); let parcel = pool.acquire_buffer_with_data(&data, BufferKind::Scattered)?; pass.with_parcel(&parcel, NodeAccess::Read); }
The bindless approach eliminates an entire layer of API surface and the pipeline layout permutations that come with it.
Synchronization: Manual vs Dependency Graph
wgpu provides implicit synchronization within a render/compute pass but requires you to order passes correctly. Resource transitions between passes are handled by wgpu internally, following WebGPU's implicit rules.
Goldy uses a retained scheme — a dependency graph recorded once and submitted each frame. You declare nodes and their resource dependencies; Goldy derives barriers, layout transitions, and execution order. This gives the runtime a global view of the frame for optimal scheduling and makes synchronization bugs structurally impossible.
Shader Language: WGSL vs Slang
wgpu's primary shader language is WGSL, the WebGPU Shading Language. WGSL is designed for safety and portability across web and native targets, but it lacks features like modules, generics, and automatic differentiation.
Goldy uses Slang exclusively. Slang compiles a single source file to SPIR-V (Vulkan), DXIL (DX12), and MSL (Metal). It provides modules with true separate compilation, generics, and HLSL-familiar syntax. The goldy_exp shader library builds on Slang's module system to provide shared types and utilities:
import goldy_exp;
[goldy_compute]
[numthreads(64, 1, 1)]
void cs_main(Scattered<Particle> particles, ThreadId id) {
particles[id.x].position += particles[id.x].velocity;
}
Compute as First-Class Citizen
wgpu supports compute shaders, but the API is oriented around render passes. Compute-to-render workflows require manual buffer management and pass ordering.
Goldy treats compute and graphics as peers. Compute-to-surface is a built-in pattern: a compute dispatch writes to a buffer or texture, and a subsequent render pass reads from it, with the scheme handling the dependency automatically.
Metal: Native vs MoltenVK
wgpu supports Metal through its wgpu-hal Metal backend or via MoltenVK (Vulkan-on-Metal translation). MoltenVK adds a translation layer that can introduce overhead and compatibility limitations.
Goldy has a native Metal backend that uses Metal APIs directly — Argument Buffers Tier 2 for bindless, MSL compiled from Slang, and native Metal types throughout. No translation layer sits between Goldy and the Metal driver.
Architecture
wgpu:
Application → wgpu (WebGPU API) → wgpu-hal → Vulkan / Metal / DX12 / WebGPU
Goldy:
Application → Goldy (native API) → Vulkan 1.4+ / Metal 2+ / DX12 (shipped); CUDA / WebGPU (in progress); Tenstorrent (planned)
wgpu implements the WebGPU specification faithfully, then maps it onto each backend through an internal HAL. Goldy talks to each backend directly using native idioms.
When to Choose Which
Choose wgpu when:
- You need browser deployment via WebGPU
- You need to support older GPUs or wide device compatibility
- You want the stability of a specification-driven API
- You need the wgpu ecosystem (examples, community, tooling)
Choose Goldy when:
- You target only modern desktop/mobile hardware (2018+)
- You want a minimal API surface with bindless as the default
- You want native Metal without a translation layer
- You want Slang's module system and shader language features
- Compute workloads are central to your application
Both libraries are valid choices — the right one depends on your hardware requirements, deployment targets, and whether you value broad compatibility or API simplicity.
Target Hardware
Goldy targets modern GPUs exclusively. This is a deliberate design choice — by requiring hardware from roughly 2018 onward, Goldy can use bindless descriptors, dynamic rendering, and coherent caches as baseline assumptions rather than optional features.
Backend Requirements
Vulkan 1.4+
Goldy requires Vulkan 1.4, which promotes several extensions that were optional in earlier versions to core:
| Feature | Vulkan history | Goldy usage |
|---|---|---|
| Dynamic rendering | VK_KHR_dynamic_rendering (1.3) | No render pass objects |
| Descriptor indexing | VK_EXT_descriptor_indexing (1.2) | Bindless resource access |
| Buffer device address | VK_KHR_buffer_device_address (1.2) | 64-bit GPU pointers |
| Synchronization2 | VK_KHR_synchronization2 (1.3) | Simplified barrier model |
| Push descriptors | Core in 1.4 | Efficient uniform updates |
Supported hardware:
- NVIDIA: Turing and later (RTX 2000 / GTX 1600 series, 2018+)
- AMD: RDNA 1 and later (RX 5000 series, 2019+)
- Intel: Xe architecture and later (Arc, 2022+)
- Qualcomm: Adreno 650+ (2019+, driver dependent)
DX12
Goldy's DX12 backend requires:
| Requirement | Details |
|---|---|
| D3D12 Enhanced Barriers | Windows 11 + WDDM 3.0+ driver |
ResourceDescriptorHeap | SM 6.6 bindless (Shader Model 6.6) |
| Root constants | Push constants equivalent |
Enhanced Barriers are mandatory — Goldy does not fall back to legacy resource state transitions. This effectively requires Windows 11 with a modern driver.
For software rendering and CI, Goldy supports the WARP software rasterizer via GOLDY_DX12_FORCE_WARP=1.
Metal Tier 2+
Goldy's Metal backend is native (no MoltenVK) and requires Argument Buffers Tier 2 for bindless resource access:
| Requirement | Details |
|---|---|
| Argument Buffers Tier 2 | Bindless via ParameterBlock |
| MSL (via Slang) | Slang compiles directly to Metal Shading Language |
Supported hardware:
- Apple Silicon: All models (M1/M2/M3/M4, A14+)
- Intel Macs: 2017+ (different iGPUs; some very early Intel UHD may not qualify)
- AMD discrete GPUs in Macs: 2015+
Older Intel integrated GPUs (pre-2017 Macs) are not supported — they lack Argument Buffers Tier 2.
What "Modern GPU" Means for Goldy
Goldy's hardware floor is defined by a set of architectural capabilities, not specific product names:
| Capability | Why Goldy needs it |
|---|---|
| Coherent L2 cache | No manual cache flush/invalidate logic |
| Bindless descriptors | Single global descriptor model, no set layouts |
| Dynamic rendering | No render pass objects or framebuffer compatibility |
| 64-bit buffer addresses | Direct pointer access in shaders |
| Unified or REBAR memory | Simplified CPU-GPU data transfer |
GPUs from roughly 2018 onward universally support these features. The specific API version requirements (Vulkan 1.4, DX12 Enhanced Barriers, Metal Tier 2) are the mechanism by which Goldy enforces this floor.
Additional Backends
| Backend | Status | Notes |
|---|---|---|
| CUDA | In progress | NVIDIA compute prototype; cuda Cargo feature |
| WebGPU | In progress | Cross-platform prototype via wgpu; webgpu Cargo feature |
| Tenstorrent | Planned | Torus Fondaco runtime design (not yet implemented) |
These backends are not yet supported for production use. See Backend Architecture for details.
What This Excludes
| Excluded | Reason |
|---|---|
| NVIDIA GTX 900 series (Maxwell) | No Vulkan 1.4 support |
| AMD GCN (RX 400/500) | Driver support ended; limited bindless |
| Intel Gen9 (HD 500/600) | Incomplete Vulkan feature coverage |
| Intel integrated GPUs pre-2017 (Mac) | No Argument Buffers Tier 2 |
| Pre-Windows 11 DX12 | No Enhanced Barriers |
Checking Compatibility
Goldy reports unsupported devices at initialization:
#![allow(unused)] fn main() { let instance = Instance::new()?; for adapter in instance.enumerate_adapters() { println!("{}: {:?}", adapter.name, adapter.device_type); } // request_device returns an error on unsupported hardware let device = instance .request_adapter(&RequestAdapterOptions::default())? .request_device(&DeviceDescriptor::default())?; }
The Tradeoff
By drawing a line at modern hardware, Goldy avoids the fallback paths, compatibility checks, and feature-level negotiation that dominate traditional GPU libraries. Every code path in Goldy assumes the full feature set is available. This keeps the implementation small and the API surface predictable.
The cost is clear: Goldy cannot run on the long tail of older hardware. For applications that need broad device support, wgpu is the better choice.
Fondaco Machine (research)
Goldy realizes the Fondaco Machine — an abstract model of cooperative GPU computation. These chapters are the normative research material adapted for this book.
- Terminology — vocabulary and status labels
- Machine Specification — normative abstract machine
- Goldy Runtime Mapping — what Goldy ships today
- Design Thesis — why Goldy exists
Terminology
Vocabulary and status labels used throughout the Fondaco chapters and the rest of this book.
| Authority | Document |
|---|---|
| Machine semantics | Machine Specification |
| Goldy realization | Goldy Runtime Mapping |
| Shipped behavior | Goldy source, tests, and examples |
Status labels
| Label | Meaning |
|---|---|
| Shipped | Available in the public Goldy crate today (0.2.x) |
| Designed | Specified and intended; not yet implemented, or only partially implemented |
| Experimental | Behind a feature flag, alpha binding, or unstable API |
| Speculative | Research or exploration; not committed to the roadmap |
| Historical | Superseded design kept for context |
Do not describe Designed, Experimental, or Speculative capabilities as if they were Shipped.
Fondaco machine terms
| Term | Definition |
|---|---|
| Merchant | The sovereign client: describes parcels and executes schemes; owns every parcel |
| Scheme | First-class computation: dispatches plus precedences (a partial order) |
| Dispatch | Atomic unit of work admitted to the machine |
| Script | Opaque procedure evaluated by a computing dispatch |
| Yielding script | Script with structured yield points where the runtime may be petitioned |
| Parcel | Stable identity for data held by the runtime in trust for the merchant |
| Ownership / claim | Access right over a parcel for the duration of a dispatch (public, private, or private-inaugural) |
| Ledger | Runtime standing record of claims across schemes (not merchant-addressable) |
| Gate | Interval between adjacent dispatches where the runtime has full intervention powers |
| Exchange | Stable mediated relationship with a foreign subsystem; each execution may publish a linear claim |
| Warehouse | Runtime-imposed bound on the total extent of parcels the merchant may hold |
| Petition | Structured service request filed at a yield point |
Goldy API map
| Fondaco term | Goldy type / concept | Notes |
|---|---|---|
| Scheme | Scheme, internal GraphIR | Public recording and submission API |
| Dispatch | Scheme node (compute, render, copy, clear, present) | Workgroup grid for compute/render |
| Script | Slang via [goldy_*] virtual entry points | Sole script language (Goldy choice) |
| Parcel | Parcel, Buffer, Texture | Stable handles; bindless indexing is backend-internal |
| Ownership | NodeAccess on scheme nodes | Precedences derived from access modes |
| Ledger | Cross-submission sync (ParcelStamp, timeline) | Crate-private; clients use settlement APIs |
| Gate | Submission gate, Context::boundary_crossed | Epoch-driven reclamation |
| Exchange | SurfaceExchange, MemoryExchange | Transaction → Claim → consume / discard |
| Warehouse | BudgetPolicy, VramAllocator | Bound on committed parcel extent |
| Lease | Lease<T>, LeaseRenderTarget | Temporary view of a parcel for scheme recording |
Internal terms
| Term | Location | Role |
|---|---|---|
| GraphIR | task_graph | Internal scheme representation |
| Wave / partition analysis | task_graph::analysis | Submission partitioning and transient coloring |
| Bindless heap | Backends | Descriptor indexing; not public ABI |
Reading order
- Machine Specification — normative semantics
- Goldy Runtime Mapping — what Goldy ships vs designs
- Design Thesis — why this model on modern GPUs
- The rest of this book — tutorials, programming model, and APIs
Machine Specification
Status: Draft v0.12
This chapter specifies the Fondaco abstract machine: what a merchant is, what parcels and ownership mean, what execution is, and what a runtime must do. It does not specify a particular hardware mapping, calling convention, or API. Those belong to implementations — Goldy's mapping is in Goldy Runtime Mapping.
The machine is a positive specification: it states what is, and what is not stated does not exist. Analogies to other abstract machines appear only in the appendix and are leaky. The Fondaco terms in the body are authoritative. See also Terminology.
1. The machine
The Fondaco machine is an abstract machine for cooperative computation, implemented by a runtime.
Parcels are data. Schemes are computation.
A merchant describes parcels and executes schemes via dispatches. The merchant is the sovereign — every parcel belongs to it; the runtime holds them in trust. The merchant assigns ownership claims through schemes; the runtime manages their physical realization.
The remainder of this chapter specifies schemes, dispatches, scripts (including yielding), parcels and ownership, exchanges, gates, petitions, and the latitude runtimes have to transform schemes.
2. The scheme
A scheme consists of:
- A set of dispatches
- A set of precedences between dispatches
A precedence A → B asserts that dispatch A must complete before dispatch B begins. The transitive closure is a partial order over the dispatches.
Two dispatches are unordered if neither is downstream of the other. The runtime may execute unordered dispatches in any order, including concurrently or fused.
A scheme is first-class data. Schemes compose: a scheme may contain another as a sub-scheme, and the composition of two schemes is a scheme.
A scheme is well-formed if its precedence set is consistent with its ownership claims (§5). Specifically: no two unordered dispatches may claim the same parcel where at least one holds private ownership. A merchant may add precedences beyond those ownership requires — for throughput or occupancy — but may not omit a required precedence.
Presenting an ill-formed scheme has unspecified behavior. Conforming runtimes may refuse to admit it (§10). Goldy refuses ill-formed schemes rather than producing unspecified results.
The precedence set need not be acyclic. A cyclic scheme describes a non-halting computation; whether to admit it is left to the runtime.
3. Dispatches
A dispatch is the atomic unit of work admitted to the machine. Once begun, it runs to completion. It is not preempted or reordered internally.
A dispatch runs over parcels. If arbitrary computation is required, the runtime schedules a script (the execution model is left unspecified); otherwise the runtime performs the dispatch directly.
4. Scripts and yielding
A script is the procedure a computing dispatch evaluates. The language is unspecified by the machine; runtimes may fix one or more. The script is opaque to the runtime: control flow is not visible. A script may keep private resources that are invisible to the runtime during a dispatch.
Yielding
A script may yield if it contains yield points that transfer control to the runtime and later resume.
At a yield point:
- The dispatch reaches the yield point and petitions the runtime (§7).
- The script's private resources are undisturbed while suspended.
- The runtime may modify claims while suspended, as long as it restores the state the dispatch observed before the yield.
- The runtime unsuspends the dispatch; the script resumes from the yield point.
A yield point does not create a gate. The dispatch has not ended. Visibility is limited to servicing that petition and scheduling — not the full gate powers of §6.
Non-yielding scripts
A script with no yield points runs start to finish with no runtime intervention. The runtime is invisible for the duration of a non-yielding script.
Goldy today ships only non-yielding scripts; yielding is Designed (see Goldy Runtime Mapping).
5. Parcels and ownership
A parcel is a stable identity for data held by the runtime in trust for the merchant. Parcels are the sole channel of communication between dispatches within a scheme or across schemes. Dispatches are not aware of physical realization; they rely on stable identity.
Each parcel has:
- A type, fixed at creation (type system unspecified by the machine)
- A size — extent or shape, possibly hinted, possibly fixed
- A claim — ownership granted by the merchant
Ownership
Ownership is expressed as claims: access rights for the duration of a dispatch.
- Public — read. Multiple dispatches may hold public ownership of the same parcel concurrently.
- Private — read and write. Exclusive: no other concurrent claim on that parcel.
- Private-inaugural — write without depending on prior contents. Exclusive, but no precedence from a prior owner is required; prior state is abandoned.
Ownership transfer
If dispatch A holds a private claim on parcel X and dispatch B (ordered after A) claims X, ownership transfers at the gate between them. If no later dispatch claims a parcel after its last holder completes, claims drop and ownership reverts to the runtime, which may destroy the parcel if the merchant no longer needs it.
The ledger
The ledger is the runtime's standing record of claims over parcels. Well-formedness (§2) is a property of one scheme in isolation; the ledger is the aggregate account between schemes. When one scheme completes and another is admitted, later claims serialize against prior owners the ledger records.
At every instant, at most one private claim — or any number of public claims — stands over a given parcel. The runtime mutates the ledger only by admitting schemes, acting at gates (§6), and settling exchanges. Pending foreign access from an exchange is a standing constraint until that access expires.
The ledger is an invariant, invisible to the merchant. The machine does not require a particular data structure — only that the runtime behave as though such a record is conserved.
Exchanges
An exchange is a stable, runtime-mediated relationship between a scheme and an entity outside the machine. Through an exchange, a scheme may periodically hand parcels to, or receive parcels from, a foreign subsystem.
Establishing an exchange records the relationship and its ownership constraints; it does not itself perform a foreign operation. Executions may produce exchange claims, distinct from ownership claims.
Settlement is either:
- Consume — perform the foreign operation defined by the exchange
- Discard — settle without exercising it
Settlement is terminal even if it reports failure. A runtime must define safe settlement for claims abandoned by the merchant. Representation and delivery of claims are defined by the exchange, not the machine.
Foreign access constrains ownership until the runtime knows the access has expired (reads) or completed with a usable produced state (writes). Which foreign subsystems exist and how completion is observed are unspecified by the machine. A runtime with no exchange conventions simply permits no foreign I/O.
The warehouse
The runtime need not provide an infinite warehouse. It may impose a warehouse — a bound on total parcel extent the merchant may hold. The merchant remains sovereign within that bound.
Exceeding the warehouse is a runtime-defined condition; the runtime need not admit the scheme. The warehouse may expand or contract. On contraction, the runtime may reclaim medium at the next gate for parcels whose claims have been relinquished, but must not destroy a parcel whose claim the merchant still holds.
A preferred warehouse size is a hint: the runtime honors min(declared, available). No preference means the runtime default.
The physical medium
Physical backing is managed entirely by the runtime. At any gate it may reorganize, relocate, or reclaim medium. None of this is observable to the merchant: only claims and parcel identity are preserved across physical activity.
6. Gates
A gate is the interval between two adjacent dispatches in an execution order.
At a gate, the runtime may:
- Transfer ownership between dispatches
- Inspect or modify contents of parcels it holds in trust
- Relocate physical medium
- Reclaim medium for parcels whose claims have been relinquished
- Insert additional dispatches into the scheme
Within a dispatch (including at yield points), the runtime may exercise gate powers only if they are not observable to the dispatch upon resumption.
7. Petitions
A petition is how a dispatch requests a service from the runtime. It may be filed only at a yield point (§4). The dispatch suspends, the runtime services the petition, then the dispatch resumes.
At a yield point
- The script reaches the yield point and signs the petition.
- The runtime may perform the service — including scheduling other work, delivering parcels to the merchant, or internal bookkeeping.
- The runtime resumes the script with script-state intact.
Petition conventions (encodings, services offered, how yield points are declared) are unspecified by the machine. A runtime that defines none provides no services beyond execution.
8. Scheme transformations
The runtime may transform a scheme before or during execution if observable behavior is unchanged. For any well-formed scheme it admits, it may:
- Fuse adjacent dispatches into one, producing the same parcel states
- Split one dispatch into several with precedences, producing the same parcel states
- Reorder unordered dispatches freely, including concurrent execution
- Elide dispatches whose effects can be derived without execution
- Insert bookkeeping dispatches that do not alter merchant-observable parcel states
These are algorithms over schemes (§9). Merchants express natural granularity; the runtime reshapes for hardware. Final parcel states are the invariant.
9. Algorithms over schemes
A scheme is first-class data. Execution — producing parcel states consistent with the partial order — is the defining algorithm, but not the only one.
Others include (without limit): fusion, splitting, specialization, differentiation, distribution across runtimes, scheduling annotations, analysis without execution, and composition.
A runtime need only provide execution; the machine admits all possible algorithms over schemes.
10. Conformance
A runtime conforms if and only if, for every well-formed scheme it admits, it produces parcel states consistent with at least one execution that respects:
- The scheme's partial order
- Ownership rules in §5
- Exchange constraints in §5
- Parcel-identity contracts in §5 and §6
- Yield-point petition constraints in §7 (no full gate powers at yield points)
A conforming runtime may decline to admit a scheme. It need not support every script language, parcel type, or physical extent — only those it advertises. It need not support yielding scripts.
The machine does not specify performance, latency, energy, or resource consumption — only the meaning of what the merchant executes.
Appendix: Glossary (non-normative)
Loose analogues for readers familiar with other models. The Fondaco terms above are authoritative.
| Fondaco term | Loose analogue | Note |
|---|---|---|
| Merchant | Program, application, client | Sovereign owner of all parcels |
| Scheme | Command buffer, render graph, dataflow graph | First-class; algorithms operate over it |
| Dispatch | Kernel launch, shader invocation | Atomic; runs to completion |
| Script | Kernel body, shader source | Opaque except at yield points |
| Yielding script | Coroutine, fiber body | Structured suspend/resume |
| Yield point | Suspension point, syscall boundary | Collectively transfers control to the runtime |
| Claims | Descriptor set, root signature, argument buffer | Names mapped to parcels with ownership |
| Parcel | Buffer, texture, resource | Stable identity; medium may move |
| Ledger | Resource-state / hazard tracker | Conserved across schemes; not merchant-addressable |
| Exchange | Swapchain present, DMA, host-visible mapping | Linear per-execution settlement |
| Ownership (public) | SRV, sampled image, read-only binding | Concurrent reads |
| Ownership (private) | UAV, storage image, read-write binding | Exclusive tenant |
| Ownership (private-inaugural) | Discard/clear load op | Exclusive write; prior state abandoned |
| Warehouse | Device memory budget | Bound relative to other merchants |
| Gate | Fence, barrier, semaphore | Full intervention between dispatches |
| Petition | System call, trap | Mid-dispatch; limited service, not full gate powers |
| Scheme transformation | Compiler pass, kernel fusion | Observable parcel states must be preserved |
| Runtime | OS kernel, driver, command queue | Conceptually one agent; may be distributed |
Analogues are not equivalences. In particular: a scheme is the program, not merely a scheduling artifact; parcel identity must stay opaque (descriptor heaps are backend-private); petitions at yield points do not grant full gate powers.
Goldy Runtime Mapping
Status: Implementation note for Goldy 0.2.x. Claims use the labels in Terminology.
How Goldy realizes the Fondaco machine from Machine Specification. Goldy is a runtime, not the machine. Where this chapter disagrees with the spec, the spec governs.
Hardware terms (GPU, Vulkan, Metal, DX12, shader, fence) appear because Goldy must speak them. They have no normative meaning in the Fondaco machine.
For why Goldy looks this way, see Design Thesis. For day-to-day usage, start at the Introduction.
1. Goldy realizes a Fondaco machine
Shipped. Goldy is a Rust library that admits dispatches, honors scheme partial orders, maintains parcel identity across physical activity, and acts at gates as the machine requires.
It targets 2020-era heterogeneous compute: a host processor plus one or more GPUs via Vulkan 1.4+, DX12, or native Metal (macOS).
The spec's runtime is a single agent. Goldy splits it into:
- Host (Rust): parcel identity, schemes, ledger analysis, gates, exchanges
- Device (GPU queue): executes admitted dispatches
That split is a substrate artifact, not a machine requirement.
2. Status overview
| Machine concept | Goldy realization | Status |
|---|---|---|
| Scheme | Scheme + internal GraphIR | Shipped |
| Dispatch | Compute / render / copy / clear / present nodes | Shipped |
| Script | Slang via [goldy_*] virtual entry points | Shipped |
| Parcel | Parcel, Buffer, Texture (stable handles) | Shipped |
| Ownership / claims | NodeAccess → derived precedences | Shipped |
| Ledger | Cross-submission sync (ParcelStamp, timeline) | Shipped (internal) |
| Gate | Submission gate, Context::boundary_crossed | Shipped |
| Exchange | SurfaceExchange, MemoryExchange | Shipped |
| Exchange claim | Transaction → Claim → consume / discard | Shipped |
| Warehouse / budget | BudgetPolicy, VramAllocator | Shipped (partial) |
| Growable buffers | Buffer::resize_to, stable handles | Shipped |
| Retained resubmit | Clean schemes replay with zero re-record | Shipped |
| Compute-to-surface | SurfaceExchange::bind_destination | Shipped |
| Pipelined frames | FrameOrchestrator, surface depth | Shipped |
Yielding scripts / $yield | Slang intrinsic + petition servicing | Designed |
| Scheme fusion (mega-kernel) | Merge adjacent dispatches | Designed |
| Scheme splitting (wavefront) | Split at yield points | Designed |
| Defragmentation | VramAllocator::defragment | Designed |
| Memory-pressure events | MemoryPressureEvent | Designed |
| Promise / continuation API | Indirect continuation dispatch | Designed |
WASI host (goldy-host) | GPU to WASM guests | Speculative |
| Pre-2020 bindless-free backend | Traditional binding backend | Speculative |
3. Dispatches
Shipped. A Goldy dispatch is a compute or graphics submission to the accelerator.
- Script: Slang compiled through
virtual_mainto SPIR-V, DXIL, or Metal IR. Goldy fixes Slang; the machine does not require it. See Virtual Entry Points. - Execution: Workgroup grid (threadgroups on Metal).
dispatch_indirectwhere the backend allows. - Claims:
NodeAccesson scheme nodes — read, write, read-write — mapped to public / private / private-inaugural ownership.
Non-computing dispatches also Shipped: buffer copy, buffer write, texture upload, buffer clear, present / copy-to-swapchain.
Goldy does not preserve shader invocation identity across dispatch gates; logical threads must persist state in parcels.
4. Parcels and bindless internals
Shipped. A Goldy parcel is a stable handle. Programs never author raw (category, index) bindless slots.
Bindless descriptor indexing is backend-internal:
- Rust: Public types are
Parcel,Buffer,Texture,Scheme, exchanges. Bindless resolution happens at scheme record / submit. - Slang: Typed parameters (
Scattered<T>,BufRO<T>, …).virtual_maingenerates slot packing.
Program-visible are access-pattern categories (Layer B): Scattered, BufRO, Broadcast, Interpolated, DirectSpatial, Filter. See Parcels and Design Thesis.
Parcel identity and reslot
Shipped. Identity is the handle, not the descriptor slot. Handles stay stable across physical growth (Buffer::resize_to), transient aliasing within an epoch, and backend pool rotation.
When backing changes (reslot), Goldy:
- Keeps the handle unchanged
- Gives the new allocation a new descriptor slot; old slots remain valid until in-flight work retires
- Invalidates retained command buffers that embedded stale slots
This follows DX12 / Vulkan / Metal descriptor versioning. See Buffers and VRAM Allocator.
5. Warehouse and memory
Shipped (partial). Physical medium is managed by VramAllocator, RetainedPool, and TransientPool. See RetainedPool and Parcel and Transient Allocation.
Goldy distinguishes three quantities:
| Quantity | Owner | Meaning |
|---|---|---|
| Logical warehouse | Program + runtime | Sum of parcel extents (Fondaco warehouse) |
| Committed | Runtime | Bytes handed out (commit charge) |
| Resident | OS | Bytes in the fast tier now |
Budget enforcement keys on committed. Resident enters reactively via OS memory-pressure signals.
Shipped residency models per backend: ManagedAllocation (discrete Vulkan / DX12), PageOnFault (Apple Metal), plus capability queries for resize cost (Constant, PageBind, Copy).
Designed: defragmentation, proactive memory-pressure petition delivery at gates.
6. Exchanges
Shipped. Primary exchange: surface presentation.
#![allow(unused)] fn main() { let transaction = surface_exchange.bind_render_target(&mut scheme, &scene_rt)?; let mut submission = scheme.submit()?; let claim = transaction.claim(&mut submission)?; claim.consume()?; // present }
- Binding does not acquire a drawable; acquire runs at submit when the partition needs it
Claim::consumeis terminal- The program never passes raw GPU addresses to the compositor
Shipped CPU readback: MemoryExchange with WithdrawTransaction / WithdrawClaim. See Settlement and Compute to Surface.
Designed: video-encoder exchange (foreign read continues after enqueue).
7. Schemes and GraphIR
Shipped. Public type: Scheme. Internally Goldy holds GraphIR — nodes, ownership-derived edges, wave / partition analysis, retention fingerprints.
On Scheme::submit:
- Dependency analysis inserts barriers
- Transient regions are colored for aliasing
- Partitions may acquire exchange backing
- Retained command buffers replay when bindings are unchanged
Designed scheme transformations (spec §8): fusion (mega-kernel), splitting at yield points (wavefront), dead-dispatch elision beyond basic analysis.
Goldy refuses ill-formed schemes (conflicting unordered private claims) rather than producing unspecified results — a deliberate narrowing of spec latitude.
8. Gates and ordering
Shipped. A gate is the interval between Scheme::submit calls and retirement via Context::boundary_crossed(T).
At a gate Goldy may reclaim deferred allocations, flush VRAM deferred rings, and service timeline signals.
Shipped cross-submission ordering: the runtime enforces ledger precedences across schemes on the same Context, using GPU barriers or host waits as needed. Clients must not assume which lever is used.
Pipeline depth (in-flight submissions) is client pacing — surface depth, FrameOrchestrator, when to consume claims. See Pipelined Frames.
9. Scripts: non-yielding today
Shipped. Public shaders today are non-yielding: [goldy_compute], [goldy_vertex], [goldy_fragment].
Designed yielding scripts:
$yieldintrinsic in the virtual-entry-point transform- Script-state preservation (register spin-wait, workgroup-local, or parcel-backed)
- Yield-point petitions (limited runtime power, not full gate powers)
10. Calling conventions
Shipped:
- Slang as sole script language — Slang in One Source
- Virtual entry points — typed parameters;
virtual_maingenerates platform wrappers - Push-constant layout — bindless indices + scalars prepended per dispatch (backend-internal)
- Access categories — validated at scheme record time
Designed: $yield petition descriptors, promise / continuation bindless category, paged-parcel fault servicing.
Portable programs depend on typed access categories and scheme structure, not on bindless heap layout.
11. Where Goldy constrains the spec
Deliberate restrictions for modern desktop / laptop workloads:
- Refuses ill-formed schemes
- Fixed Slang scripts
- Closed typed-access category set
- Workgroup-grid execution model
- Single accelerator queue per device (heterogeneous multi-queue: Designed)
- 2020+ hardware floor (Vulkan 1.4+, DX12 Enhanced Barriers, Metal Tier 2+)
For older hardware or maximum portability, use wgpu. See Goldy vs wgpu and Target Hardware.
12. Abstract the medium, expose cost
Normative for Goldy design.
- Layer A (medium): VRAM, residency, relocation, descriptor slots — abstracted; runtime-owned
- Layer B (cost): Registers, occupancy, coalescing, access patterns, first-touch latency — exposed and queryable
Goldy must not present Layer A operations as uniform-cost or hide them entirely. Access-pattern types (Scattered vs Broadcast vs Interpolated) exist because hardware treats them differently.
Capability queries report backend, residency model, resize cost, zero-copy readback, and optional features honestly.
Appendix: Fondaco ↔ Goldy
| Fondaco term | Goldy / GPU analogue |
|---|---|
| Scheme | Scheme, GraphIR |
| Dispatch | Kernel launch, draw / dispatch command |
| Script | Slang shader |
| Parcel | Buffer / Texture handle |
| Merchant | Program |
| Exchange | SurfaceExchange, MemoryExchange |
| Claim (exchange) | Claim, WithdrawClaim |
| Gate | Fence epoch, boundary_crossed |
| Warehouse | BudgetPolicy, VramAllocator |
| Ledger | Cross-submit sync analysis |
Analogues are not equivalences. A scheme is the program's computation, not merely a scheduling artifact. An exchange preserves program sovereignty over parcels; a raw swapchain handle does not.
Full vocabulary: Terminology.
Design Thesis
Why Goldy exists, and how it differs from a conventional GPU library. Machine semantics live in Machine Specification; what is shipped today is in Goldy Runtime Mapping. Vocabulary: Terminology.
Executive summary
Goldy is a GPU runtime for the Fondaco Machine — programs own parcels (data) and express computation as schemes (dispatches + ownership-derived precedences). It targets modern native APIs (Vulkan 1.4+, DX12, Metal Tier 2+) with no translation layers, a single shader language (Slang), and a scheme-first API that sheds descriptor sets, explicit barriers, and swapchain ceremony.
The Fondaco model on GPU
Traditional GPU programming exposes descriptor set layouts, image layout transitions, render pass objects, pipeline layouts, and raw swapchain images with semaphores.
Fondaco instead gives the program:
| Concept | Role |
|---|---|
| Parcel | Stable identity for data; physical medium is runtime-managed |
| Scheme | First-class computation graph; precedences from ownership |
| Exchange | Mediated foreign I/O (present, readback) via linear claims |
| Gate | Where the runtime may relocate, reclaim, or insert work |
Goldy's public API (Scheme, Parcel, SurfaceExchange, MemoryExchange) implements this model.
Access patterns, not graphics categories
Goldy names resources for what the hardware does, not which API invented the term:
| Goldy term | Hardware behavior |
|---|---|
| Scattered | Any-thread read/write |
| BufRO | Read-only scattered (stronger cache hints) |
| Broadcast | Wave-broadcast constant fetch |
| Interpolated | Dedicated texture filtering silicon |
| DirectSpatial | 2D/3D indexed access, no filtering |
| Filter | Sampler configuration |
Shaders declare these as typed Slang parameters on [goldy_*] entry points. The CPU side uses matching buffer / texture kinds and scheme NodeAccess. See Parcels.
What Goldy sheds
Because Goldy requires modern baseline hardware, it drops:
| Legacy concept | Goldy approach |
|---|---|
| Render pass objects | Dynamic rendering |
| Descriptor set layouts | Bindless (backend-internal) |
| Separate transfer queues | Unified queue model |
| OpenGL fixed function | Shaders only |
| Multiple shader languages | Slang only |
Details: What Goldy Sheds and Goldy vs wgpu.
Slang and virtual entry points
Goldy uses Slang as its sole shader language, compiled at runtime to SPIR-V / DXIL / MSL. The compiler is embedded in the crate.
import goldy_exp;
[goldy_compute]
[numthreads(64, 1, 1)]
void cs_main(MyUniforms cfg, Scattered<uint> data, ThreadId id) {
data[id.x] = data[id.x] + cfg.base;
}
The virtual_main transform generates platform entry points with bindless slot resolution — see Virtual Entry Points. The goldy_exp standard library provides access functions, math, color utilities, vertex formats, and workgroup collectives.
Unified graphics and compute
Goldy treats graphics and compute as one scheme:
- Compute simulation → raster present in one retained scheme
- Compute-to-surface: compute writes swapchain drawables directly (no
RenderPipeline) — Compute to Surface - Cross-scheme ordering via context timeline / ledger
This matches how modern engines and CUDA-style workloads converge on the same memory-access patterns. Goldy provides the primitives; performance patterns remain the developer's responsibility.
Backends
| Platform | Backend | Notes |
|---|---|---|
| Windows | DX12 (default), Vulkan | PIX on DX12 |
| Linux | Vulkan | Wayland surfaces |
| macOS | Metal | Native, not MoltenVK |
Auto-selection with GOLDY_BACKEND override. Capability queries reflect backend-specific features honestly — Backend Architecture.
Goldy vs wgpu
| wgpu | Goldy | |
|---|---|---|
| Identity | WebGPU for Rust | Fondaco GPU runtime |
| Governance | W3C spec | Independent |
| Legacy floor | Vulkan 1.0+, web LCD | Vulkan 1.4+, modern only |
| Binding model | WebGPU bind groups | Typed bindless + schemes |
| Browser | Yes | No (native only) |
Use wgpu for web and maximum compatibility. Use Goldy for scheme-first Fondaco semantics and the modern feature union. Full write-up: Goldy vs wgpu.
Inspirations
| Source | Contribution |
|---|---|
| Sebastian Aaltonen — "No Graphics API" | Target modern hardware; drop legacy ceremony |
| Ralph Levien — piet-gpu-hal post-mortem | Abstract meaning, expose cost |
| Wayland compositor model | Complete frames, explicit sync, mediated present |
| Slang | One shader language, multi-backend |
| wgpu | Instance / device ergonomics (adapted to schemes) |
| TU Darmstadt HAL paper | Minimal necessary feature analysis |
See also Motivation.
Roadmap posture
Shipped in 0.2.x: schemes, exchanges, compute-to-surface, growable buffers, retained replay, language bindings, Rust examples.
Designed: yielding scripts, scheme fusion / splitting, defragmentation, compute algorithm libraries (scan, sort, BLAS-class).
Speculative: WASI goldy-host, CUDA backend exploration, pre-2020 traditional-binding backend.
Do not treat Designed or Speculative items as shipped. Status table: Goldy Runtime Mapping.
Further reading
- Machine Specification
- Goldy Runtime Mapping
- Terminology
- Motivation · What Goldy Sheds · Target Hardware
Slang Quick Reference
Goldy uses Slang as its sole shading language. This page covers what you need to write Goldy shaders — not a full Slang language reference.
Basics
Slang uses HLSL-style syntax. If you've written HLSL or GLSL, most of it will look familiar.
Scalar Types
float f = 1.0;
int i = -5;
uint u = 10;
bool b = true;
Vector and Matrix Types
float2 v2 = float2(1.0, 2.0);
float3 v3 = float3(1.0, 2.0, 3.0);
float4 v4 = float4(1.0, 2.0, 3.0, 4.0);
// Swizzling
float2 xy = v4.xy;
float3 rgb = v4.rgb;
// Matrices
float4x4 mvp;
float4 transformed = mul(mvp, float4(pos, 1.0));
Structs
struct Particle {
float2 position;
float2 velocity;
float age;
};
Functions
float square(float x) { return x * x; }
// Public functions are exported from modules
public float3 my_effect(float2 uv) { return float3(uv, 0.5); }
Modules
Slang has a real module system (not #include). Modules are separate compilation units:
// In mylib.slang
module mylib;
public float3 effect(float2 uv) { return float3(uv, 1.0); }
// In shader.slang
import mylib;
float3 c = effect(uv);
goldy_exp Resource Types
The goldy_exp module defines type aliases that map to native Slang buffer and texture types. When used as parameters in [goldy_*] entry points, the Goldy compiler automatically resolves slot indices to live resource handles.
Buffer Types
| Type alias | Underlying type | Access pattern | Usage |
|---|---|---|---|
Scattered<T> | StorageBuffer<T> (RWStructuredBuffer<T>) | Read/write, any thread, any address | data[i], data[i].field = v |
BufRO<T> | ReadOnlyBuffer<T> (StructuredBuffer<T>) | Read-only, hardware read-cache hint | data[i] |
ByteAddress | ByteAddressView (RWByteAddressBuffer) | Raw byte-level access | .Load(addr), .Store(addr, v), .InterlockedMin(...) |
Texture Types
| Type alias | Underlying type | Access pattern | Usage |
|---|---|---|---|
Interpolated<T> | Texture2D<T> | Hardware-filtered sampling | tex.Sample(samp, uv), tex.Load(loc) |
DirectSpatial<T> | RWTexture2D<T> | Direct 2D read/write, no filtering | img[int2(x,y)], img.GetDimensions(w,h) |
Sampler Type
| Type alias | Underlying type | Usage |
|---|---|---|
Filter | SamplerState | Pass to tex.Sample(filter, uv) |
Broadcast (Constant Buffer)
To pass uniform data (same value for all threads), declare a struct type directly as a parameter — no wrapper needed. The codegen recognizes any non-resource, non-system-value struct as a constant-buffer broadcast:
struct TimeUniforms { float time; float delta_time; };
[goldy_compute]
[numthreads(64, 1, 1)]
void cs_main(TimeUniforms cfg, Scattered<Particle> particles, ThreadId id) {
particles[id.x].position += particles[id.x].velocity * cfg.delta_time;
}
System-Value Types
Declare these as parameters in [goldy_*] entry points to receive GPU-provided values. The codegen maps each type to its SV_* semantic automatically.
Compute
| Type | Maps to | Components |
|---|---|---|
ThreadId | SV_DispatchThreadID | .x, .y, .z, .xy, .xyz |
GroupThreadId | SV_GroupThreadID | .x, .y, .z, .xy, .xyz |
GroupId | SV_GroupID | .x, .y, .z, .xy, .xyz |
Graphics
| Type | Maps to | Components |
|---|---|---|
VertexId | SV_VertexID | .value |
InstanceId | SV_InstanceID | .value |
IsFrontFace | SV_IsFrontFace | .value |
Entry Point Attributes
[goldy_compute]
Marks a compute shader entry point. The Goldy compiler generates the real [shader("compute")] wrapper that resolves resource slots and system values.
import goldy_exp;
[goldy_compute]
[numthreads(64, 1, 1)]
void cs_main(Scattered<uint> data, uint offset, ThreadId id) {
data[id.x + offset] += 1;
}
[goldy_vertex]
Marks a vertex shader entry point.
import goldy_exp;
struct VSOutput {
float4 position : SV_Position;
float4 color : COLOR;
};
[goldy_vertex]
VSOutput vs_main(BufRO<Vertex> verts, VertexId vid) {
Vertex v = verts[vid.value];
VSOutput o;
o.position = float4(v.pos, 0.0, 1.0);
o.color = v.color;
return o;
}
[goldy_fragment]
Marks a fragment shader entry point.
import goldy_exp;
[goldy_fragment]
float4 fs_main(Interpolated<float4> tex, Filter samp, float2 uv : TEXCOORD0) : SV_Target {
return tex.Sample(samp, uv);
}
Common Patterns
Accessing Buffers by Index
All Scattered<T> and BufRO<T> parameters support standard array indexing. Field-level writes work directly on Scattered<T>:
[goldy_compute]
[numthreads(64, 1, 1)]
void cs_main(Scattered<Particle> particles, ThreadId id) {
Particle p = particles[id.x];
p.position += p.velocity;
particles[id.x] = p;
// Or field-level write:
particles[id.x].age += 1.0;
}
Sampling Textures
[goldy_fragment]
float4 fs_main(Interpolated<float4> albedo, Filter samp, float2 uv : TEXCOORD0) : SV_Target {
return albedo.Sample(samp, uv);
}
Writing to Storage Images
[goldy_compute]
[numthreads(8, 8, 1)]
void cs_main(DirectSpatial<float4> output, ThreadId id) {
output[int2(id.x, id.y)] = float4(float(id.x) / 512.0, float(id.y) / 512.0, 0.5, 1.0);
}
Fullscreen Triangle (Vertex-less)
Use vs_fullscreen_triangle() from goldy_exp to render fullscreen effects without a vertex buffer:
import goldy_exp;
[shader("vertex")]
FullscreenVarying vs_main(uint vertex_id : SV_VertexID) {
return vs_fullscreen_triangle(vertex_id);
}
[shader("fragment")]
float4 fs_main(FullscreenVarying input) : SV_Target {
return float4(input.uv, 0.5, 1.0);
}
Compute + Render Buffer Sharing
Compute shaders and graphics shaders share the same bindless buffers. The scheme handles the dependency:
// Compute: update particles
[goldy_compute]
[numthreads(64, 1, 1)]
void cs_update(TimeUniforms cfg, Scattered<Particle> particles, ThreadId id) {
particles[id.x].position += particles[id.x].velocity * cfg.delta_time;
}
// Vertex: read particles for rendering
[goldy_vertex]
VSOutput vs_draw(BufRO<Particle> particles, InstanceId iid, VertexId vid) {
Particle p = particles[iid.value];
// Generate quad geometry from particle position...
}
Rust-Side Resource Binding
Resources are bound in declaration order (left to right in the shader signature) via
with_parcel. Graph access (NodeAccess) drives dependency analysis; the descriptor
kind (SRV vs UAV) is chosen from pipeline reflection at dispatch / set_pipeline:
#![allow(unused)] fn main() { scheme .node("update", &pipeline) .with_parcel(&cfg_buf, NodeAccess::Read) .with_parcel(&particle_buf, NodeAccess::ReadWrite) .dispatch(workgroups, 1, 1); }
Use NodeAccess::Overwrite when a compute node fully replaces a parcel without reading
prior contents (ping-pong write sides, clears). Plain scalar parameters (uint offset)
are also push-constant bindings — no wrapper struct needed.
goldy_exp Utility Modules
| Module | Contents |
|---|---|
goldy_exp/math.slang | PI, TAU, hash(), hash2(), center_uv(), scale_uv(), to_polar(), smootherstep() |
goldy_exp/color.slang | rainbow(), palette(), heat(), hsv_to_rgb(), luminance(), gamma_correct() |
goldy_exp/primitives.slang | quad_position(), quad_position_rotated(), billboard_position(), fullscreen_position(), fullscreen_uv() |
goldy_exp/types.slang | Particle2D, Particle3D, FrameUniforms, Transform2D, DispatchShape |
goldy_exp/vertex.slang | FullscreenVarying, ColoredVertex, ColoredVarying, vs_fullscreen_triangle() |
goldy_exp/access.slang | Resource type aliases and system-value types (documented above) |
goldy_exp/interlocked.slang | Interlocked<T> cells; InterlockedLoad/Store/Add/Or/Xor/Min/Max/Exchange |
Further Reading
Environment Variables
Goldy reads several environment variables at runtime for backend selection, validation, debugging, and Slang configuration.
General
| Variable | Values | Default | Description |
|---|---|---|---|
GOLDY_BACKEND | vulkan, vk, dx12, d3d12, directx, metal, mtl, cuda, webgpu, wgpu, cpu | Platform default (macOS → Metal, Windows → DX12, Linux → Vulkan) | Override backend selection at runtime. Shipped: Vulkan, DX12, Metal. In progress: CUDA, WebGPU. cpu is a compute-only host-callable JIT path (never a platform default). |
GOLDY_SLANG_PATH | File path | (not set) | Override the path to the Slang shared library (slang.dll / libslang.dylib / libslang.so). Bypasses the default search order (vendored next to executable → extracted from embedded). |
GOLDY_FFI_PATH | File path | (not set) | Full path to the goldy_ffi shared library (goldy_ffi.dll / libgoldy_ffi.so / libgoldy_ffi.dylib). Used by goldy-ffi-client for runtime library loading. |
Validation
| Variable | Values | Default | Description |
|---|---|---|---|
GOLDY_VALIDATION | Comma/semicolon/whitespace-separated list: api, layout, layouts, host_access, all; or 1 / true / yes | (not set) | Enable validation categories. api enables GPU API validation (Vulkan validation layers + debug messenger, Metal shader validation, CUDA Driver diagnostics: PTX JIT logs, eager stream sync, launch-limit checks; WebGPU/wgpu validation error scopes on shader/PSO create and bind groups). layout enables Rust/Slang struct layout and buffer stride checks. host_access page-protects CPU-visible GPU copies (CPU backend parcels; slower, not complete). all enables layout, api, timeline, scheme, and host_access. The shorthand 1 / true / yes enables GPU API only (layout stays opt-in). Deep CUDA memory/race checking is not covered — use external compute-sanitizer. |
GOLDY_VALIDATION_FATAL | 1, true, yes | (not set) | Separate from GOLDY_VALIDATION. When GPU API validation is on, treat Vulkan Khronos ERROR messages as hard failures (Err on later Goldy Result calls; panic on backend drop so cargo test fails). Without this, messages are logged (goldy::validation) and successful vk* calls still succeed. WebGPU validation scopes already return Err without this flag. |
GOLDY_VALIDATE_LAYOUTS | 1, true, yes | (not set) | Legacy toggle for layout validation only. Equivalent to GOLDY_VALIDATION=layout. |
Validation Examples
# GPU API validation (Vulkan layers, Metal shader validation, CUDA Driver diagnostics)
GOLDY_VALIDATION=api cargo run --example triangle
# Layout + stride checks only
GOLDY_VALIDATION=layout cargo run --example triangle
# Everything
GOLDY_VALIDATION=all cargo run --example triangle
# Fail Goldy calls / tests when Vulkan records an ERROR
GOLDY_VALIDATION_FATAL=1 GOLDY_VALIDATION=all cargo test --features vulkan
GOLDY_VALIDATION_FATAL=1 GOLDY_VALIDATION=api cargo test --features vulkan
# Shorthand for GPU API only
GOLDY_VALIDATION=1 cargo run --example triangle
# CUDA with API validation (JIT logs, eager sync, launch limits)
GOLDY_BACKEND=cuda GOLDY_VALIDATION=api cargo test --features cuda
DX12-Specific
| Variable | Values | Default | Description |
|---|---|---|---|
GOLDY_DX12_DEBUG | 1, true | On in debug builds | Enable the D3D12 debug layer. On by default in debug builds; set explicitly for release builds. |
GOLDY_DX12_NO_DEBUG | 1, true | (not set) | Force-disable the D3D12 debug layer even in debug builds. Useful to avoid debug-layer crashes in parallel test threads. |
GOLDY_DX12_GBV | 1, true | (not set) | Enable D3D12 GPU-Based Validation. Catches UAV/SRV descriptor mismatches, resource state errors, and out-of-bounds access on the GPU timeline. Very slow — use for targeted debugging only. |
GOLDY_DX12_FORCE_WARP | 1, true | (not set) | Force the DX12 backend to use the WARP software rasterizer, even when hardware GPUs are present. Use for headless CI or reproducing WARP-specific rendering bugs. |
GOLDY_DX12_ALLOW_WARP | 1, true | (not set) | Allow the WARP adapter to appear in device enumeration. Without this or GOLDY_DX12_FORCE_WARP, WARP is hidden. |
Debugging
| Variable | Values | Default | Description |
|---|---|---|---|
GOLDY_DUMP_SHADERS | Directory path | (not set) | Dump compiled shaders to the specified directory at compile time. Vulkan: {entry}_h{handle}_vulkan.spv. DX12: {entry}_h{handle}_dx12.dxil. Metal: {idx}_{entry}.metal. CUDA: {entry}_h{handle}_{spec}_cuda.cu (Slang CUDA C++) and .ptx (NVRTC/Slang PTX loaded by the CUDA driver), plus goldy_apply_dispatch_shape.{cu,ptx} for the graph updater. |
GOLDY_DUMP_RUST_KERNELS | 1 / true / directory path | (not set) | Dump canonical [goldy_compute] Slang and structured ABI metadata produced by #[goldy::compute] during Kernel::prepare. 1/true writes under the process temp dir (goldy_rust_kernels/). |
GOLDY_CPU_SHADERS | 1, true, yes | (not set) | Documented gate for the standalone goldy::cpu_shaders APIs. GPU backends ignore this. Scheme submit uses GOLDY_BACKEND=cpu instead. |
GOLDY_GPU_PROFILE | Any non-empty value; optional chrome[=path] | (not set) | Enable GPU timestamp profiling logs. On Vulkan/DX12, records per-dispatch GPU durations. On Metal, records command-buffer GPU duration. chrome / chrome=/path.json also writes a Perfetto Chrome-trace JSON file. Disables retained CB reuse while active. |
GOLDY_SHADER_TIMING | 1 / any value other than 0 | (not set) | Print stderr wall-clock breakdown of Slang cache lookup, search-path hashing, and PSO create during shader compile |
GOLDY_METAL_CAPTURE | 1 / path / path,skip=N,frames=M | (not set) | Metal only. Opt-in programmatic MTLCaptureManager GPU capture for Xcode Metal Debugger. 1/true/yes captures to Developer Tools; a path writes a .gputrace. Optional skip=N (default 60) skips warm-up submits; frames=M (default 1) captures M submits. Automatically sets METAL_CAPTURE_ENABLED=1 if unset. Open the .gputrace in Xcode → Performance to inspect register pressure, occupancy, and per-line shader costs. |
GOLDY_API_LOG | File path | (not set) | Metal only. Append NDJSON Metal API call traces (dispatches, encoder open/close, commits) to the given file. |
GOLDY_API_LOG_SYNC | 1 | (not set) | Force synchronous GOLDY_API_LOG writes (for tests). |
Metal capture example
# Warm up 120 submits, then write one .gputrace for Xcode Metal Debugger
GOLDY_METAL_CAPTURE=/tmp/capture-tiger.gputrace,skip=120,frames=1 \
target/release/with_winit_bin --timeout-secs 12 --no-vsync
# Open /tmp/capture-tiger.gputrace in Xcode → click Performance → select fine_area
Interop with System Variables
Goldy also respects these non-Goldy environment variables:
| Variable | Backend | Description |
|---|---|---|
VK_INSTANCE_LAYERS | Vulkan | If set to include VK_LAYER_KHRONOS_validation, Goldy enables Vulkan validation regardless of GOLDY_VALIDATION. |
VK_LAYER_PATH | Vulkan | Standard Vulkan loader variable for locating validation layer manifests. |
MTL_SHADER_VALIDATION | Metal | When GOLDY_VALIDATION enables API validation and this variable is unset, Goldy sets it to 1 before creating the first Metal device. If you set it yourself, Goldy does not override it. |
METAL_CAPTURE_ENABLED | Metal | Required for programmatic GPU capture outside Xcode. Goldy sets this to 1 automatically when GOLDY_METAL_CAPTURE is set (if unset). |
CUDA_LAUNCH_BLOCKING | CUDA | When GOLDY_VALIDATION enables API validation and this variable is unset, Goldy sets it to 1 before CUDA driver init. If you set it yourself, Goldy does not override it. Forces synchronous kernel launches so errors surface at the launch site. |
WGPU_BACKEND | WebGPU | wgpu instance backend mask (vulkan, metal, dx12, gl, …). Goldy passes this through wgpu::InstanceDescriptor::from_env_or_default(). CI uses vulkan on Linux, metal on macOS, and dx12 on Windows. |
WGPU_FORCE_FALLBACK_ADAPTER | WebGPU | When set (1/true), wgpu prefers a software adapter (WARP on Windows). Used in Windows CI because hosted runners have no discrete GPU. |
License
Goldy is licensed under the MIT License.
MIT License
You may use Goldy freely in any project — including proprietary and commercial software:
- Use, modify, and distribute Goldy
- Static or dynamic linking in proprietary software
- No obligation to release your own source code
- Include the MIT copyright notice and license text in distributions
See LICENSE in the repository for the full text.
Dependencies
Goldy depends on various open-source libraries with their own licenses:
| Dependency | License |
|---|---|
| ash | MIT/Apache-2.0 |
| anyhow | MIT/Apache-2.0 |
| thiserror | MIT/Apache-2.0 |
| tracing | MIT |
| bitflags | MIT/Apache-2.0 |
| bytemuck | Zlib/MIT/Apache-2.0 |
All dependencies are permissively licensed.