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.