Pipelines

Pipelines combine compiled shaders with fixed-function rendering state. Goldy provides RenderPipeline for raster, MeshPipeline for mesh shading, and ComputePipeline for compute.

A graphics pipeline is a typed connection:

vertex input → vertex/mesh stage → interpolated payload → fragment stage

Think of it as RasterPipeline<VertexIn, Varying, Color> — without repeating those types in Rust. Payloads stay shader-owned. Goldy reflects each stage, links them structurally by semantic, and merges per-stage virtual-main resources into one named contract.

Three things a draw needs

ConceptWho owns itExample
Pipeline payloadShader structs with semantics (SV_Position, TEXCOORD0)Varying returned by the vertex/mesh stage and consumed by the fragment stage
Runtime resource bindingsNamed virtual-main parameters, bound at record timeShaderBinding::read("scene", &scene)
Invocation builtinsSystem-value wrappersVertexId, ThreadId, IsFrontFace

RenderPipeline::interface() exposes the reflected vertex input, payload links, fragment outputs, and merged resource contract for diagnostics.

Render Pipelines

A RenderPipeline pairs vertex and fragment shaders with raster state: vertex input layout, primitive assembly, depth testing, and the output format.

Creating a Render Pipeline

The builder reflects and links stages automatically. Existing RenderPipeline::new delegates to the same linker.

#![allow(unused)]
fn main() {
use goldy::{RenderPipeline, ShaderBinding, ShaderModule, Vertex2D};

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::builder(&device)
    .vertex(&vs)
    .fragment(&fs)
    .vertex_layout(Vertex2D::layout())
    .topology(PrimitiveTopology::TriangleList)
    .target_format(surface.format())
    .build()?;
}

Each stage may declare only the resources it uses. Shared names (scene on both VS and FS) merge into one slot; unique names append after the fragment list (raster) or the mesh list (mesh pipelines).

Named draw bindings

#![allow(unused)]
fn main() {
pass.with_shader_bindings(&[
    ShaderBinding::read("scene", &scene),
    ShaderBinding::read("albedo", &albedo),
    ShaderBinding::sampler("nearest", &sampler),
]);
pass.set_pipeline(&pipeline);
}

Extra names are allowed so one pass-level set can serve several pipeline switches. Missing required names and wrong resource categories fail with an error that names the pipeline parameter.

Positional with_shader_resources remains valid: slots follow the merged contract order (fragment-first for raster, mesh-first for mesh).

RenderPipelineDesc

RenderPipeline::new(&device, &vs, &fs, &desc) still works. The descriptor is the same raster state the builder sets:

#![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 Your First Compute Shader 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.

Compute is the one place Goldy will compile an extra program after startup: a retained dispatch whose with_param scalars hold still is moved onto a baked variant of its shader. That is at most one specialized pipeline per dispatch site, not a permutation of feature flags, and it is an implementation detail of scheme submit rather than something ComputePipeline::new enumerates. See Shader Specialization Prediction.

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: &Runtime, 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())?,
        })
    }
}
}

Mesh pipelines

MeshPipeline replaces the vertex stage with a mesh shader ([goldy_mesh] / mesh_main) and a fragment shader. Amplification / task shaders are optional ([goldy_amplification] / amp_main) when RuntimeCapabilities::amplification_shaders is set.

Create the pipeline only when device.capabilities().mesh_shaders is true. Record with set_mesh_pipeline and dispatch_mesh instead of draw. Vulkan, DX12, and Metal implement this.

#![allow(unused)]
fn main() {
let pipeline = MeshPipeline::builder(&device)
    .mesh(&shader)
    .fragment(&shader)
    .target_format(rt_format)
    .build()?;

let mut pass = scheme.render_pass("mesh", &rt, TargetLoad::Discard);
pass.set_mesh_pipeline(&pipeline);
pass.dispatch_mesh(1, 1, 1);
pass.finish();
}