Goldy Logo

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

FondacoGoldy
SchemeScheme — retained graph, resubmitted each frame
ParcelParcel, Buffer, Texture
DispatchCompute, render, copy, and present nodes inside a scheme
ExchangeSurfaceExchange, MemoryExchange
SettlementTransactionClaim

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;
}
TypeUse
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

PlatformBackend
WindowsDX12 (default), Vulkan
LinuxVulkan (Wayland surfaces)
macOSMetal

CUDA and WebGPU backends are in progress. A Tenstorrent backend (Torus) is planned. See Backend Architecture.

Bindings: Python, .NET, C++, Rust FFI Client.

License

MIT — see License.

Installation

Requirements

Adding Goldy to Your Project

[dependencies]
goldy = "0.2"

Or with cargo:

cargo add goldy

Feature Flags

FeatureDefaultDescription
vulkanyesVulkan 1.4+ backend (Linux, Windows); implies graphics and gpu
dx12yesDirectX 12 backend (Windows); implies graphics and gpu
metalyesMetal Tier 2+ backend (macOS); implies graphics and gpu
graphicsyesRaster pipelines, render targets, surfaces, and presentation
gpuyesImplied by every real GPU backend (not mock). Do not enable alone.
cudanoCUDA backend (in progress; NVIDIA compute; implies gpu, not graphics)
webgpunoWebGPU backend (in progress; via wgpu; implies graphics and gpu)
instrumentationyesStructured 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:

PlatformDefault Backend
WindowsDX12
LinuxVulkan
macOSMetal

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

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

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 an RWTexture2D<float4> — write to it with output[tid.xy].
  • ThreadId maps to SV_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 uniformsMemoryExchange::bind_deposit_buffer records the upload topology once; each frame call deposit.write on the upload scheme before the main submit.

Record the scheme onceSurfaceExchange::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 presentscheme.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

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:

CategoryWhat it holdsShader-side type
ScatteredRead/write structured dataScattered<T>
BufRORead-only structured dataBufRO<T>
BroadcastSmall uniform data shared by every invocationa plain struct parameter
InterpolatedSampled texture dataInterpolated<T>
DirectSpatialRead/write texture dataDirectSpatial<T>
FilterSampler stateFilter

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(&params_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 TypeUnderlying Slang TypeUsage
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)]
ByteAddressRWByteAddressBufferRaw byte access: .Load(), .Store(), .Interlocked*()
FilterSamplerStateSampler 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
SetupDeclare descriptor set layouts, allocate pools, create and update descriptor setsAcquire a parcel; category is fixed at creation
BindingBind descriptor sets before each draw/dispatchPass parcels via with_parcel on scheme nodes
Shader accesslayout(set=0, binding=1) buffer ...Scattered<T> data as a function parameter
ValidationRuntime errors or silent corruption on mismatchCategory checks at dispatch time
Cross-backendLayout declarations differ per APISame 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(&params_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

AttributeStageGenerated 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 TypeResolves ViaDescription
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
ByteAddressgoldy_byte_address(slot)Raw byte-address buffer
Filtergoldy_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 TypeMaps ToAvailable Fields
ThreadIdSV_DispatchThreadID.x, .y, .z, .xy, .xyz
GroupThreadIdSV_GroupThreadID.x, .y, .z, .xy, .xyz
GroupIdSV_GroupID.x, .y, .z, .xy, .xyz
VertexIdSV_VertexID.value
InstanceIdSV_InstanceID.value
IsFrontFaceSV_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:

  1. Generates a wrapper function with the real [shader("...")] attribute and a fixed 16-word push-constant signature.
  2. Renames the user function from cs_main to _goldy_user_cs_main so both can coexist.
  3. 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 parameterSlang / 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 / booltyped scalar push words (no manual to_bits)

Hidden builtins (appended to the Slang signature when used):

RustSlang
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

  1. Proc-macro errors at Rust compile time for unsupported syntax.
  2. Slang / pipeline errors during Kernel::prepare.
  3. 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

BackendTarget FormatAPI RequirementStatus
VulkanSPIR-VVulkan 1.4+Shipped
DirectX 12DXILWindows 10+Shipped
MetalMetal IRMetal Tier 2+ (Argument Buffers)Shipped
CUDAPTXNVIDIA CUDAIn progress
WebGPUWGSLWebGPU (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 .slang file. 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:

ConstantDescription
VERTEX_COLOR_2D2D vertex+fragment shader with per-vertex color
SOLID_COLORSolid 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

  1. Derive LayoutCheckable on your Rust struct:
#![allow(unused)]
fn main() {
#[derive(LayoutCheckable)]
#[repr(C)]
struct TimeUniforms {
    time: f32,
    delta_time: f32,
    frame: u32,
    _pad: u32,
}
}
  1. 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],
)?;
}
  1. 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:

ValueLayout ChecksGPU API Validation
layoutYesNo
apiNoYes
layout,apiYesYes
allYesYes
1 / true / yesNoYes

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 counterSettlement
QueryPoll a fence or compare u64obj.is_settled()
WaitWait on fence / wait_until(tv)obj.wait_until_settled()
IdentityOpaque fence or epoch numberConcrete submission or parcel
PortabilityTied to native timeline primitivesBackend 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:

  1. A ring of in-flight frame receipts.
  2. A pipeline-depth cap — block the CPU when the ring is full to prevent unbounded memory growth.
  3. Deferred retirement of slots when their submissions settle.
  4. Present-path patching — stamp the most recent slot after Claim::consume via note_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-levelbegin_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>,
}
}
FieldPurposeDefault
vertex_layoutDescribes vertex buffer stride and attributesEmpty (no vertex input)
topologyHow vertices are assembled into primitivesTriangleList
target_formatPixel format of the render target — must match surface.format() or the format passed to RenderTarget::new()Rgba8Unorm
depth_stencilDepth/stencil test configuration, or None to disableNone

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:

FieldPurposeDefault
formatDepth texture format (Depth16Unorm, Depth24Plus, Depth32Float, etc.)Depth24Plus
depth_write_enabledWhether fragments write to the depth buffertrue
depth_compareComparison function — Less, LessEqual, Greater, Always, etc.Less

Available depth formats:

FormatBitsStencil
Depth16Unorm16-bitNo
Depth24Plus24-bit (may use 32 internally)No
Depth24PlusStencil824-bit + 8-bit stencilYes
Depth32Float32-bit floatNo
Depth32FloatStencil832-bit float + 8-bit stencilYes

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:

DimensionTraditional Vulkan/DX12Goldy
Render pass compatibilityN render passes × M subpassesEliminated — dynamic rendering
Descriptor set layoutsPer-material layout permutationsOne global bindless layout
Pipeline layoutsPer-materialOne shared layout
Viewport / scissorBaked into PSODynamic state
Vertex formatBakedBaked (unavoidable)
Target formatBakedBaked (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:

VariantEffect
TargetLoad::LoadPreserve 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::DiscardPrior 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, before scheme.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):

LocationFieldFormatOffset
0positionFloat32x20
1colorFloat32x48

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):

LocationFieldFormatOffset
0positionFloat32x20
1uvFloat32x28

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:

FormatRust TypeSize
Float32f324
Float32x2[f32; 2]8
Float32x3[f32; 3]12
Float32x4[f32; 4]16
Uint32u324
Sint32i324
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

MethodUse
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>,
}
}
FieldPurposeDefault
present_modeVsync strategyAuto
depth_formatDepth buffer format, or None to disableNone

Present Modes

ModeBehaviorBackend Mapping
FifoVsync — wait for display refresh. No tearing, capped at monitor Hz.Metal displaySyncEnabled=YES, Vulkan FIFO, DX12 Present(1)
MailboxTriple-buffered — latest frame queued, older dropped. Low latency + no tearing.Vulkan MAILBOX. Falls back to Fifo on Metal and some DX12 configurations.
ImmediateNo sync, may tear. Maximum throughput for benchmarks.Metal displaySyncEnabled=NO, Vulkan IMMEDIATE, DX12 Present(0)
AutoGoldy 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, or bind_destination) once when building the scheme.
  • Each frame: scheme.submit() then transaction.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.

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
}
}
PatternShader MappingUse When
ScatteredStructuredBuffer<T>, RWStructuredBuffer<T>General storage: particles, meshes, compute I/O
BroadcastConstantBuffer / uniform bufferUniform 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;
    }
}
}
FlagPurpose
COPY_SRCBuffer can be a copy source
COPY_DSTBuffer can be a copy destination
CPU_READABLEMedium hint for host-visible storage. Prefer MemoryExchange::bind_withdraw for observation. Not a public host-read API.
CPU_WRITABLEHost-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

LanguageTypesAcquire
RustRetainedPool, Buffer, Parcelacquire_buffer*, acquire_record, acquire_texture
Pythongoldy.RetainedPool, goldy.Buffer, goldy.Parcelacquire_buffer, acquire_record, acquire_texture
C#RetainedPool, Buffer, Parcel, RecordBuilderAcquireBuffer, Record(), AcquireTexture
C / ffi-clientGoldyRetainedPool, GoldyBuffer, GoldyParcelgoldy_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:

AccessShader MappingUse When
InterpolatedTexture2D with samplerImage data filtered between texels — sprites, materials, UI
DirectRWTexture2DStorage images, compute output, exact pixel reads/writes

Texture Formats

FormatBPPNotes
R8Unorm1Single-channel (masks, SDFs)
Rg8Unorm2Two-channel (normal maps, motion vectors)
Rgba8Unorm4Standard 8-bit RGBA
Rgba8UnormSrgb4sRGB color space
Bgra8UnormSrgb4sRGB, swapped channels (common swapchain format)
Bgra8Unorm4Linear, swapped channels
Rgba16Float8HDR
Rgba32Float16Full 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;
    }
}
}
FlagPurpose
COPY_SRCTexture can be a copy source (needed for withdraw / GPU copies)
COPY_DSTTexture can be a copy destination (needed for deposits / copies)
RENDER_TARGETTexture 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, &region_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:

FormatBitsStencil
Depth16Unorm16No
Depth24Plus24No
Depth24PlusStencil824 + 8Yes
Depth32Float32No
Depth32FloatStencil832 + 8Yes
#![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

ModeEffect
NearestPixelated — nearest texel, no interpolation
LinearSmooth — bilinear interpolation between neighbors

Address Modes

ModeEffect for UVs outside [0, 1]
ClampToEdgeStretches the border texel
RepeatTiles the texture
MirrorRepeatTiles 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:

DoorPermanenceAcquire
RetainedPoolCross-submission identity (deeds)acquire_texture / acquire_buffer / acquire_record
Context transient poolOne-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:

AcquireReturn
Context::acquire_transient_bufferContext::return_transient_buffer
Context::acquire_transient_textureContext::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.

BackendStatusAPI LevelPlatformsRust Crate
VulkanShipped1.4+Windows, Linuxash
DX12ShippedDirect3D 12Windowswindows + gpu-allocator
MetalShippedTier 2+macOSmetal
CUDAIn progressCUDA Driver APINVIDIA GPUscudarc
WebGPUIn progressWebGPU (via wgpu)Cross-platformwgpu
CPUIn progress (compute-only)Slang host-callable JITHost
TenstorrentPlannedTT-Metalium / TT-MLIRTenstorrent 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:

PlatformDefault Backend
macOSMetal
WindowsDX12
LinuxVulkan

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):

ValueBackendStatus
vulkan, vkVulkanShipped
dx12, d3d12, directxDX12Shipped
metal, mtlMetalShipped
cudaCUDAIn progress
webgpu, wgpuWebGPUIn 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:

VariantMeaning
DiscreteGpuDedicated graphics card with its own VRAM
IntegratedGpuGPU integrated into the CPU (shared memory)
CpuSoftware renderer (e.g. WARP on DX12, lavapipe on Vulkan)
OtherUnknown 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);
}
CapabilityVulkanDX12Metal
Zero-copy CPU storage readbackYesNo (requires GPU copy to readback heap)Yes
Preferred surface formatBgra8UnormSrgbBgra8UnormSrgbBgra8UnormSrgb

Vulkan Backend

The Vulkan backend requires Vulkan 1.4+ and uses:

  • Dynamic rendering (VK_KHR_dynamic_rendering) — no VkRenderPass or VkFramebuffer objects
  • 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 over uint8_t4, DX12-style round(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>Rgba16Float
    • DirectSpatial<uint8_t4>Rgba8Unorm (Slang has no uchar4 alias)
    • Upload/copy/readback of other supported sampled formats still works.
  • Surfaces expose Rgba8Unorm imported 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-owned Rgba8Unorm texture (or render target) and exporting with CopyTexture; direct launches onto imported scratch remain supported but are costlier under WDDM. The DXGI swapchain is matching R8G8B8A8_UNORM so present is a single CopyResource.
  • DeviceCapabilities on CUDA advertise preferred_surface_format = Rgba8Unorm and preferred_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 distinct Filter configuration; 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. Identity DirectSpatial<float4> is rgba32float. Goldy specializes packed 8-bit surfaces at dispatch: Rgba8Unormrgba8unorm, Bgra8Unormbgra8unorm (the latter needs wgpu BGRA8UNORM_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_footprint reports the padded layout.
  • Surfaces: begin_frame acquires the wgpu drawable. Present picks Copy (same-format storage scratch → swapchain) when the scratch format can be a UAV (Rgba8Unorm, or Bgra8Unorm with BGRA8UNORM_STORAGE), otherwise Blit (Rgba8Unorm scratch + fullscreen pass for BGRA/sRGB). Direct compute-to-swapchain is not selected: wgpu 28 swapchain images only expose RENDER_ATTACHMENT, so storage bind groups fail even when the surface advertises STORAGE_BINDING. Override with GOLDY_WEBGPU_PRESENT=copy|blit. DirectSpatial<float4> shaders do not change; packed storage is specialized to the compute format (surface_format()). finish_present drops the acquired image and publishes SwapchainReturned. surface_resize reconfigures 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, and CopyRenderTarget. Vertex/fragment resources use the same packed @group(0) lowering as compute (no bindless heap). The webgpu feature implies graphics.
  • 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:

FeatureDependencies
gpunone (umbrella; implied by each backend below)
vulkanash (+ graphics / raw-window-handle)
dx12windows, gpu-allocator, windows-core (+ graphics)
metalmetal, cocoa, objc, core-graphics-types, foreign-types, block (+ graphics)
cudacudarc
webgpuwgpu, 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

BackendAvailable OnNotes
vulkanWindows, Linux (any platform with a Vulkan loader)Broadest platform support; implies graphics
dx12Windows onlyGated by #[cfg(target_os = "windows")] — the feature is ignored on other platforms; implies graphics
metalmacOS onlyGated by #[cfg(target_os = "macos")] — the feature is ignored on other platforms; implies graphics
cudaAny platform with CUDA toolkitCompute 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.
webgpuCross-platformvia 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:

ValueEffect
apiEnable backend GPU API validation (see below)
layoutEnable Rust ↔ Slang struct layout checks and buffer stride checks
host_accessPage-protect CPU-visible GPU copies (CPU backend parcels; stray host pointers fault)
allEnable api, layout, timeline, scheme, and host_access
1, true, yesGPU 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:

BackendWhat Gets Enabled
VulkanVK_LAYER_KHRONOS_validation + VK_EXT_debug_utils at instance creation
MetalSets MTL_SHADER_VALIDATION=1 (if not already set) before the first device is created
DX12See DX12 Debug Layer below
WebGPUwgpu 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_of vs. 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

VariableValuesEffect
GOLDY_DX12_DEBUG1Force-enable the D3D12 debug layer (even in release builds)
GOLDY_DX12_NO_DEBUG1Disable the D3D12 debug layer (useful for parallel tests that crash the debug layer)
GOLDY_DX12_GBV1Enable 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:

CategoryPoint NameEmitted Data
Slangslang.library.loadpath, success
slang.compile.starttarget, entry_points, bindless
slang.compile.endduration_ms, output_size, success
slang.reflection.extractparameter_blocks, fields
Shadershader.module.createbackend, shader_type
shader.pipeline.createpipeline_type, bind_groups
Resourceresource.buffer.createsize, usage
resource.texture.createdimensions, format
resource.bind_group.createbindings_count
Renderrender.frame.startframe_id
render.compute.dispatchworkgroups, pipeline
render.drawvertices, instances
render.frame.endframe_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

VariableValuesEffect
GOLDY_BACKENDvulkan/vk, dx12/d3d12/directx, metal/mtlOverride backend selection
GOLDY_VALIDATIONapi, layout, host_access, all, 1/true/yesEnable validation categories
GOLDY_VALIDATION_FATAL1, true, yesFail on Vulkan Khronos ERROR messages
GOLDY_VALIDATE_LAYOUTS1, true, yesEnable layout validation (legacy; prefer GOLDY_VALIDATION=layout)
GOLDY_DX12_FORCE_WARP1Use WARP software rasterizer
GOLDY_DX12_DEBUG1Force-enable D3D12 debug layer in release
GOLDY_DX12_NO_DEBUG1Disable D3D12 debug layer
GOLDY_DX12_GBV1Enable GPU-Based Validation
GOLDY_SHADER_TIMING1Print Slang/PSO startup timings to stderr
GOLDY_CPU_SHADERS1Documented gate for debug CPU host-callable kernels (goldy::cpu_shaders)
RUST_LOGe.g. goldy=debugFilter 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

TypeCPU ABI
BufRO<T>, Scattered<T> (T = uint / int / float / bool){ T* data; size_t count }
Scalar uint / int / float / bool4-byte word
ThreadId, GroupThreadId, GroupIdSV_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

TypeNotes
Broadcast / gpu::Uniform<T> / constant-buffer structsNeeds a CPU constant-buffer view
ByteAddressCPU 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 / samplersTexture-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 / graphicsStill 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.

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 dtypeTypical use case
np.float32Vertex positions, colors, uniforms
np.float64High-precision data
np.uint32Index buffers, compute data
np.int32Signed integer data
np.uint1616-bit index buffers
np.uint8Raw 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

AspectRustPython
Instance creationInstance::new()?goldy.Instance()
Error handlingResult<T, GoldyError>Raises goldy.GoldyError
Retained bufferretained_pool.acquire_buffer_with_data(&data, access)retained_pool.acquire_buffer(numpy_array, access)Parcel
Render passscheme.render_pass(...)with scheme.render_pass(...) as rp:
Compute nodescheme.node(...).dispatch(...)scheme.node(...).with_parcel(...).dispatch(...)
Readbackgrant.consume(&submission)grant.consume(submission)
Resource lifetimeExplicit Arc<Device> ownershipManaged 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

AspectRustC#
Instance creationInstance::new()?new Instance()
Error handlingResult<T, GoldyError>Exceptions
Device lifetimeArc<Device>IDisposable / using
Retained bufferretained_pool.acquire_buffer_with_data(&data, access)retainedPool.AcquireBuffer<T>(data, access)Parcel
Submissionscheme.submit()?scheme.Submit()SchemeSubmission
EnumsDeviceType::DiscreteGpuDeviceType.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-ffi from 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

AspectRustC++
Instance creationInstance::new()?goldy::Instance instance
Error handlingResult<T, GoldyError>goldy::Exception
Device lifetimeArc<Device>RAII destructor
Retained bufferpool.acquire_buffer_with_data(&data, access)pool.acquire_buffer_with_data(span, access)
Render passscheme.render_pass(...)scheme.render_pass(...) (RAII scope)
Readbackclaim.consume(&submission)withdraw.claim(submission).consume()

API Reference

Core Classes

ClassDescription
goldy::InstanceEntry point, adapter enumeration
goldy::Device / goldy::ContextGPU device and execution context
goldy::RetainedPoolRetained buffer/texture acquisition
goldy::RecordBuilderPartitioned buffer records (ping-pong fields)
goldy::SchemeRetained dependency graph
goldy::MemoryExchangeCPU↔GPU withdraw/deposit
goldy::SurfaceExchangeWindow swapchain (Win32 / macOS / Wayland)
goldy::ShaderModuleCompiled Slang shader
goldy::RenderPipeline / goldy::ComputePipelineGraphics/compute pipelines
goldy::SamplerTexture 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

PlatformHeadless SchemeWindowed Surface
Windows x64YesYes
Linux x64YesYes (Wayland; X11 not supported)
macOS x64 / ARM64YesYes

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

ExampleDescription
cpp/examples/triangle_headless.cppOffscreen triangle + readback
cpp/examples/triangle.cppWindowed triangle (GLFW)
cpp/examples/compute_simple.cppCompute dispatch
cpp/examples/game_of_life.cppHybrid 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 caseCrate
Normal Rust applicationsgoldy (static link, published on crates.io)
FFI integration testsgoldy-ffi-client
Validating the C ABI from Rustgoldy-ffi-client
Swapping the native library without recompiling the clientgoldy-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_ffi shared 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:

  1. GOLDY_FFI_PATH — full path to the goldy_ffi dylib
  2. GOLDY_FFI_LIB_DIR — compile-time directory from the goldy-ffi build
  3. 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

Aspectgoldygoldy-ffi-client
LinkingStatic (compiled into your binary)Dynamic (libloading at runtime)
Distributioncrates.ioWorkspace path dependency
API surfaceReference implementationMirrors core API over C ABI
Native libraryEmbedded in your binarySeparate goldy_ffi dylib required
Crate namegoldygoldy_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

ExampleDescription
ffi-client/examples/triangle_headless.rsOffscreen triangle + readback
ffi-client/examples/triangle.rsWindowed triangle (winit)
ffi-client/examples/compute_simple.rsCompute dispatch
ffi-client/examples/game_of_life.rsHybrid compute + render (windowed)
ffi-client/examples/game_of_life_headless.rsGame 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.

ExampleWhat it demonstratesSource
triangleMinimal windowed program: retained scheme, offscreen render pass, present via SurfaceExchange.triangle.rs
gradientAnimated full-screen gradient driven by a time uniform. Uses vertex-less rendering and optional GOLDY_VALIDATE_LAYOUTS.gradient.rs
checkerboardProcedural 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.

ExampleWhat it demonstratesSource
compute_particlesCompute updates particle positions; graphics renders instanced quads. Retained scheme scheduling.compute_particles.rs
game_of_lifeConway's Game of Life on the GPU with ping-pong sub-views in one retained mosaic parcel.game_of_life.rs
compute_to_surfacePure 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.

ExampleWhat it demonstratesSource
solid_cubeSolid 3D cube with per-face colors and depth buffer.solid_cube.rs
spinning_cube3D wireframe cube using line primitives.spinning_cube.rs
depth_quadsDepth buffer proves draw-order independence.depth_quads.rs
textured_quadProcedural checkerboard texture on a quad.textured_quad.rs
instancingGPU-driven instancing with compute-updated transforms.instancing.rs
bouncing_linesLINE_LIST topology with simple physics.bouncing_lines.rs
waveformLINE_STRIP waveform visualizer.waveform.rs

Advanced Patterns

Fragment Shader Effects

ExampleWhat it demonstratesSource
plasmaDemoscene plasma effect.plasma.rs
tunnelFlying-through-a-tunnel polar-coordinate effect.tunnel.rs
metaballsMetaball field rendering.metaballs.rs
mandelbrotInteractive Mandelbrot explorer.mandelbrot.rs

Interactive and Multi-Window

ExampleWhat it demonstratesSource
digital_clock7-segment clock display.digital_clock.rs
starfield3D starfield with depth.starfield.rs
particlesRain/snow particle system.particles.rs
multi_windowMultiple windows sharing one device.multi_window.rs

Headless and Validation

ExampleWhat it demonstratesSource
headless_triangleOffscreen render + CPU readback via MemoryExchange.headless_triangle.rs
scheme_screenshotScheme-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:

FeatureWhy it matters
Modules and importTrue separate compilation, no #include fragility
GenericsType-safe reusable shader code
Automatic differentiationFirst-class for ML and physics workloads
Khronos governanceLong-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 dimensionGoldy equivalent
Render pass compatibilityDynamic rendering — no render pass objects
Descriptor set layoutOne global bindless layout
Pipeline layoutImplicit from the global layout
Viewport/scissor stateDynamic 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

wgpuGoldy
IdentityWebGPU implementation for RustModern Rust GPU library
Spec governanceW3C WebGPU specificationIndependent, opinionated
Browser supportYes (WebGPU)No
Minimum hardwareWide compatibility (Vulkan 1.0+)Modern only (Vulkan 1.4+, DX12, Metal 2+)
Shader languageWGSL (primary), SPIR-V, GLSL, nagaSlang (compiles to SPIR-V, DXIL, MSL)
Resource modelDescriptor-based (bind groups)Typed bindless
SynchronizationManual pass orderingRetained scheme (dependency graph)
Metal supportVia MoltenVK or wgpu-halNative Metal backend
Compute modelSupported but secondaryFirst-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:

FeatureVulkan historyGoldy usage
Dynamic renderingVK_KHR_dynamic_rendering (1.3)No render pass objects
Descriptor indexingVK_EXT_descriptor_indexing (1.2)Bindless resource access
Buffer device addressVK_KHR_buffer_device_address (1.2)64-bit GPU pointers
Synchronization2VK_KHR_synchronization2 (1.3)Simplified barrier model
Push descriptorsCore in 1.4Efficient 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:

RequirementDetails
D3D12 Enhanced BarriersWindows 11 + WDDM 3.0+ driver
ResourceDescriptorHeapSM 6.6 bindless (Shader Model 6.6)
Root constantsPush 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:

RequirementDetails
Argument Buffers Tier 2Bindless 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:

CapabilityWhy Goldy needs it
Coherent L2 cacheNo manual cache flush/invalidate logic
Bindless descriptorsSingle global descriptor model, no set layouts
Dynamic renderingNo render pass objects or framebuffer compatibility
64-bit buffer addressesDirect pointer access in shaders
Unified or REBAR memorySimplified 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

BackendStatusNotes
CUDAIn progressNVIDIA compute prototype; cuda Cargo feature
WebGPUIn progressCross-platform prototype via wgpu; webgpu Cargo feature
TenstorrentPlannedTorus Fondaco runtime design (not yet implemented)

These backends are not yet supported for production use. See Backend Architecture for details.

What This Excludes

ExcludedReason
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 DX12No 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 used throughout the Fondaco chapters and the rest of this book.

AuthorityDocument
Machine semanticsMachine Specification
Goldy realizationGoldy Runtime Mapping
Shipped behaviorGoldy source, tests, and examples

Status labels

LabelMeaning
ShippedAvailable in the public Goldy crate today (0.2.x)
DesignedSpecified and intended; not yet implemented, or only partially implemented
ExperimentalBehind a feature flag, alpha binding, or unstable API
SpeculativeResearch or exploration; not committed to the roadmap
HistoricalSuperseded design kept for context

Do not describe Designed, Experimental, or Speculative capabilities as if they were Shipped.

Fondaco machine terms

TermDefinition
MerchantThe sovereign client: describes parcels and executes schemes; owns every parcel
SchemeFirst-class computation: dispatches plus precedences (a partial order)
DispatchAtomic unit of work admitted to the machine
ScriptOpaque procedure evaluated by a computing dispatch
Yielding scriptScript with structured yield points where the runtime may be petitioned
ParcelStable identity for data held by the runtime in trust for the merchant
Ownership / claimAccess right over a parcel for the duration of a dispatch (public, private, or private-inaugural)
LedgerRuntime standing record of claims across schemes (not merchant-addressable)
GateInterval between adjacent dispatches where the runtime has full intervention powers
ExchangeStable mediated relationship with a foreign subsystem; each execution may publish a linear claim
WarehouseRuntime-imposed bound on the total extent of parcels the merchant may hold
PetitionStructured service request filed at a yield point

Goldy API map

Fondaco termGoldy type / conceptNotes
SchemeScheme, internal GraphIRPublic recording and submission API
DispatchScheme node (compute, render, copy, clear, present)Workgroup grid for compute/render
ScriptSlang via [goldy_*] virtual entry pointsSole script language (Goldy choice)
ParcelParcel, Buffer, TextureStable handles; bindless indexing is backend-internal
OwnershipNodeAccess on scheme nodesPrecedences derived from access modes
LedgerCross-submission sync (ParcelStamp, timeline)Crate-private; clients use settlement APIs
GateSubmission gate, Context::boundary_crossedEpoch-driven reclamation
ExchangeSurfaceExchange, MemoryExchangeTransactionClaimconsume / discard
WarehouseBudgetPolicy, VramAllocatorBound on committed parcel extent
LeaseLease<T>, LeaseRenderTargetTemporary view of a parcel for scheme recording

Internal terms

TermLocationRole
GraphIRtask_graphInternal scheme representation
Wave / partition analysistask_graph::analysisSubmission partitioning and transient coloring
Bindless heapBackendsDescriptor indexing; not public ABI

Reading order

  1. Machine Specification — normative semantics
  2. Goldy Runtime Mapping — what Goldy ships vs designs
  3. Design Thesis — why this model on modern GPUs
  4. 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:

  1. The dispatch reaches the yield point and petitions the runtime (§7).
  2. The script's private resources are undisturbed while suspended.
  3. The runtime may modify claims while suspended, as long as it restores the state the dispatch observed before the yield.
  4. 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

  1. The script reaches the yield point and signs the petition.
  2. The runtime may perform the service — including scheduling other work, delivering parcels to the merchant, or internal bookkeeping.
  3. 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 termLoose analogueNote
MerchantProgram, application, clientSovereign owner of all parcels
SchemeCommand buffer, render graph, dataflow graphFirst-class; algorithms operate over it
DispatchKernel launch, shader invocationAtomic; runs to completion
ScriptKernel body, shader sourceOpaque except at yield points
Yielding scriptCoroutine, fiber bodyStructured suspend/resume
Yield pointSuspension point, syscall boundaryCollectively transfers control to the runtime
ClaimsDescriptor set, root signature, argument bufferNames mapped to parcels with ownership
ParcelBuffer, texture, resourceStable identity; medium may move
LedgerResource-state / hazard trackerConserved across schemes; not merchant-addressable
ExchangeSwapchain present, DMA, host-visible mappingLinear per-execution settlement
Ownership (public)SRV, sampled image, read-only bindingConcurrent reads
Ownership (private)UAV, storage image, read-write bindingExclusive tenant
Ownership (private-inaugural)Discard/clear load opExclusive write; prior state abandoned
WarehouseDevice memory budgetBound relative to other merchants
GateFence, barrier, semaphoreFull intervention between dispatches
PetitionSystem call, trapMid-dispatch; limited service, not full gate powers
Scheme transformationCompiler pass, kernel fusionObservable parcel states must be preserved
RuntimeOS kernel, driver, command queueConceptually 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 conceptGoldy realizationStatus
SchemeScheme + internal GraphIRShipped
DispatchCompute / render / copy / clear / present nodesShipped
ScriptSlang via [goldy_*] virtual entry pointsShipped
ParcelParcel, Buffer, Texture (stable handles)Shipped
Ownership / claimsNodeAccess → derived precedencesShipped
LedgerCross-submission sync (ParcelStamp, timeline)Shipped (internal)
GateSubmission gate, Context::boundary_crossedShipped
ExchangeSurfaceExchange, MemoryExchangeShipped
Exchange claimTransactionClaimconsume / discardShipped
Warehouse / budgetBudgetPolicy, VramAllocatorShipped (partial)
Growable buffersBuffer::resize_to, stable handlesShipped
Retained resubmitClean schemes replay with zero re-recordShipped
Compute-to-surfaceSurfaceExchange::bind_destinationShipped
Pipelined framesFrameOrchestrator, surface depthShipped
Yielding scripts / $yieldSlang intrinsic + petition servicingDesigned
Scheme fusion (mega-kernel)Merge adjacent dispatchesDesigned
Scheme splitting (wavefront)Split at yield pointsDesigned
DefragmentationVramAllocator::defragmentDesigned
Memory-pressure eventsMemoryPressureEventDesigned
Promise / continuation APIIndirect continuation dispatchDesigned
WASI host (goldy-host)GPU to WASM guestsSpeculative
Pre-2020 bindless-free backendTraditional binding backendSpeculative

3. Dispatches

Shipped. A Goldy dispatch is a compute or graphics submission to the accelerator.

  • Script: Slang compiled through virtual_main to 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_indirect where the backend allows.
  • Claims: NodeAccess on 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_main generates 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:

  1. Keeps the handle unchanged
  2. Gives the new allocation a new descriptor slot; old slots remain valid until in-flight work retires
  3. 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:

QuantityOwnerMeaning
Logical warehouseProgram + runtimeSum of parcel extents (Fondaco warehouse)
CommittedRuntimeBytes handed out (commit charge)
ResidentOSBytes 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::consume is 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:

  • $yield intrinsic 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_main generates 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 termGoldy / GPU analogue
SchemeScheme, GraphIR
DispatchKernel launch, draw / dispatch command
ScriptSlang shader
ParcelBuffer / Texture handle
MerchantProgram
ExchangeSurfaceExchange, MemoryExchange
Claim (exchange)Claim, WithdrawClaim
GateFence epoch, boundary_crossed
WarehouseBudgetPolicy, VramAllocator
LedgerCross-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:

ConceptRole
ParcelStable identity for data; physical medium is runtime-managed
SchemeFirst-class computation graph; precedences from ownership
ExchangeMediated foreign I/O (present, readback) via linear claims
GateWhere 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 termHardware behavior
ScatteredAny-thread read/write
BufRORead-only scattered (stronger cache hints)
BroadcastWave-broadcast constant fetch
InterpolatedDedicated texture filtering silicon
DirectSpatial2D/3D indexed access, no filtering
FilterSampler 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 conceptGoldy approach
Render pass objectsDynamic rendering
Descriptor set layoutsBindless (backend-internal)
Separate transfer queuesUnified queue model
OpenGL fixed functionShaders only
Multiple shader languagesSlang 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

PlatformBackendNotes
WindowsDX12 (default), VulkanPIX on DX12
LinuxVulkanWayland surfaces
macOSMetalNative, not MoltenVK

Auto-selection with GOLDY_BACKEND override. Capability queries reflect backend-specific features honestly — Backend Architecture.

Goldy vs wgpu

wgpuGoldy
IdentityWebGPU for RustFondaco GPU runtime
GovernanceW3C specIndependent
Legacy floorVulkan 1.0+, web LCDVulkan 1.4+, modern only
Binding modelWebGPU bind groupsTyped bindless + schemes
BrowserYesNo (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

SourceContribution
Sebastian Aaltonen — "No Graphics API"Target modern hardware; drop legacy ceremony
Ralph Levien — piet-gpu-hal post-mortemAbstract meaning, expose cost
Wayland compositor modelComplete frames, explicit sync, mediated present
SlangOne shader language, multi-backend
wgpuInstance / device ergonomics (adapted to schemes)
TU Darmstadt HAL paperMinimal 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

  1. Machine Specification
  2. Goldy Runtime Mapping
  3. Terminology
  4. 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 aliasUnderlying typeAccess patternUsage
Scattered<T>StorageBuffer<T> (RWStructuredBuffer<T>)Read/write, any thread, any addressdata[i], data[i].field = v
BufRO<T>ReadOnlyBuffer<T> (StructuredBuffer<T>)Read-only, hardware read-cache hintdata[i]
ByteAddressByteAddressView (RWByteAddressBuffer)Raw byte-level access.Load(addr), .Store(addr, v), .InterlockedMin(...)

Texture Types

Type aliasUnderlying typeAccess patternUsage
Interpolated<T>Texture2D<T>Hardware-filtered samplingtex.Sample(samp, uv), tex.Load(loc)
DirectSpatial<T>RWTexture2D<T>Direct 2D read/write, no filteringimg[int2(x,y)], img.GetDimensions(w,h)

Sampler Type

Type aliasUnderlying typeUsage
FilterSamplerStatePass 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

TypeMaps toComponents
ThreadIdSV_DispatchThreadID.x, .y, .z, .xy, .xyz
GroupThreadIdSV_GroupThreadID.x, .y, .z, .xy, .xyz
GroupIdSV_GroupID.x, .y, .z, .xy, .xyz

Graphics

TypeMaps toComponents
VertexIdSV_VertexID.value
InstanceIdSV_InstanceID.value
IsFrontFaceSV_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

ModuleContents
goldy_exp/math.slangPI, TAU, hash(), hash2(), center_uv(), scale_uv(), to_polar(), smootherstep()
goldy_exp/color.slangrainbow(), palette(), heat(), hsv_to_rgb(), luminance(), gamma_correct()
goldy_exp/primitives.slangquad_position(), quad_position_rotated(), billboard_position(), fullscreen_position(), fullscreen_uv()
goldy_exp/types.slangParticle2D, Particle3D, FrameUniforms, Transform2D, DispatchShape
goldy_exp/vertex.slangFullscreenVarying, ColoredVertex, ColoredVarying, vs_fullscreen_triangle()
goldy_exp/access.slangResource type aliases and system-value types (documented above)
goldy_exp/interlocked.slangInterlocked<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

VariableValuesDefaultDescription
GOLDY_BACKENDvulkan, vk, dx12, d3d12, directx, metal, mtl, cuda, webgpu, wgpu, cpuPlatform 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_PATHFile 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_PATHFile 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

VariableValuesDefaultDescription
GOLDY_VALIDATIONComma/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_FATAL1, 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_LAYOUTS1, 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

VariableValuesDefaultDescription
GOLDY_DX12_DEBUG1, trueOn in debug buildsEnable the D3D12 debug layer. On by default in debug builds; set explicitly for release builds.
GOLDY_DX12_NO_DEBUG1, 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_GBV1, 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_WARP1, 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_WARP1, true(not set)Allow the WARP adapter to appear in device enumeration. Without this or GOLDY_DX12_FORCE_WARP, WARP is hidden.

Debugging

VariableValuesDefaultDescription
GOLDY_DUMP_SHADERSDirectory 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_KERNELS1 / 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_SHADERS1, 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_PROFILEAny 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_TIMING1 / 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_CAPTURE1 / 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_LOGFile path(not set)Metal only. Append NDJSON Metal API call traces (dispatches, encoder open/close, commits) to the given file.
GOLDY_API_LOG_SYNC1(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:

VariableBackendDescription
VK_INSTANCE_LAYERSVulkanIf set to include VK_LAYER_KHRONOS_validation, Goldy enables Vulkan validation regardless of GOLDY_VALIDATION.
VK_LAYER_PATHVulkanStandard Vulkan loader variable for locating validation layer manifests.
MTL_SHADER_VALIDATIONMetalWhen 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_ENABLEDMetalRequired for programmatic GPU capture outside Xcode. Goldy sets this to 1 automatically when GOLDY_METAL_CAPTURE is set (if unset).
CUDA_LAUNCH_BLOCKINGCUDAWhen 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_BACKENDWebGPUwgpu 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_ADAPTERWebGPUWhen 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:

DependencyLicense
ashMIT/Apache-2.0
anyhowMIT/Apache-2.0
thiserrorMIT/Apache-2.0
tracingMIT
bitflagsMIT/Apache-2.0
bytemuckZlib/MIT/Apache-2.0

All dependencies are permissively licensed.