Contents

Shader Data and Binding

The C# and Slang declarations must agree on where each value is stored and how the shader accesses it. Vertex input layouts describe vertex attributes; shader buffer declarations describe parameters and arrays; resource handles identify the resources used by a shader.

Shader compilation

ZenithCompiler compiles a named Slang entry point into a ShaderDesc. The context's GraphicsApi selects the required target: DXIL for DirectX 12, a Metal library for Metal, or SPIR-V for Vulkan.

With a graphics context named context already created, compile the triangle shader from the copy placed beside the executable:

string shaderPath = Path.Combine(AppContext.BaseDirectory, "Triangle.slang");

Shader vertexShader = context.CreateShader(ZenithCompiler.CompileFromFile(context.GraphicsApi, shaderPath, "VSMain"));

The entry-point name VSMain must match the declaration in the shader file. Compile shaders and create pipelines during initialization, then reuse the pipelines while rendering. Once pipeline creation has completed, the shader objects may be released, as shown in First Triangle.

Data layout

For vertex input, InputLayout describes each attribute's format, offset, and semantic, together with the stride between vertices. The triangle places a Float3 position before a Float4 color: the color starts at byte offset 12, and each vertex occupies 28 bytes. InputLayout.Add calculates this packed layout from the element formats. If the C# struct contains padding, supply offsets and a stride that account for it.

Constant buffers hold shader parameters, and structured buffers hold arrays of typed elements. Their layouts follow the Slang declarations rather than InputLayout. Match the C# field offsets, element sizes, and padding to the compiled shader layout. A C# unmanaged type can be copied as bytes, but its memory layout does not necessarily match the shader's layout; the tightly packed float3 used for a vertex may require different alignment in a shader buffer.

Shader parameters can include resource handles as well as ordinary values. A handle identifies storage that already exists, so copying the handle into a constant buffer does not copy the resource's contents.

The compute sample's C# constants illustrate this arrangement with image dimensions and two texture handles. Explicit offsets specify where each field begins. The declaration uses System.Runtime.InteropServices and Zenith.NET:

[StructLayout(LayoutKind.Explicit, Size = 256)]
file struct Constants
{
    [FieldOffset(0)]
    public uint Width;

    [FieldOffset(4)]
    public uint Height;

    [FieldOffset(8)]
    public ResourceHandle Input;

    [FieldOffset(16)]
    public ResourceHandle Output;
}

The matching declaration in ComputeShader.slang is:

struct Constants
{
    uint Width;

    uint Height;

    DescriptorHandle<Texture2D> Input;

    DescriptorHandle<RWTexture2D<float4>> Output;
};

ConstantBuffer<Constants> constants;

The two dimensions occupy the first eight bytes, followed by two eight-byte handles. The C# sample reserves 256 bytes for the complete record. For another record, determine its size from its own fields and the required layout and alignment.

Matrix conventions

The compiler selects row-major matrix storage, keeping each row contiguous. The spinning cube renderer stores three Matrix4x4 values at offsets 0, 64, and 128. Its shader applies mul(vector, matrix) in model, view, and projection order.

Storage order specifies how matrix elements are arranged in memory; multiplication order specifies the transformation. Use matching conventions in C# and Slang.

Constant buffers and resource handles

SetConstantBuffer selects an existing buffer and byte offset from which the current pipeline reads its constants. Upload the parameter data into that storage before it is used by the shader. If the same bytes are still being read by earlier GPU work, wait for that work before updating them.

With a command buffer, compute pipeline, populated constant buffer, and thread-group counts ready, record the dispatch in this order. Select the pipeline first, because the constant-buffer binding applies to that pipeline:

commandBuffer.SetPipeline(computePipeline);
commandBuffer.SetConstantBuffer(constantBuffer, 0);
commandBuffer.Dispatch(groupCountX, groupCountY, 1);

Here, commandBuffer records a dispatch using computePipeline and the parameters stored in constantBuffer. The buffer requires BufferUsages.Constant usage, and offset 0 selects its beginning. If several constant records share one buffer, each record's starting offset must also meet the backend's binding alignment, independently of the layout of fields within the record.

The compute sample assigns inputTexture.SampledHandle to Input and outputTexture.StorageHandle to Output. In Slang, the type parameter of DescriptorHandle defines how the shader accesses each resource:

C# handle Slang field type Intended access
texture.SampledHandle DescriptorHandle<Texture2D> Texture reads
texture.StorageHandle DescriptorHandle<RWTexture2D<float4>> Storage-texture reads and writes
buffer.StorageReadOnlyHandle DescriptorHandle<StructuredBuffer<T>> Reads of structured elements of shader type T
buffer.StorageReadWriteHandle DescriptorHandle<RWStructuredBuffer<T>> Reads and writes of structured elements of shader type T
sampler.Handle DescriptorHandle<SamplerState> Texture sampling state

ResourceHandle contains two 32-bit fields whose interpretation depends on the backend. Its values are valid only within the context that created the resource.

Retain the resource and any view used to obtain its handle until the shader's final access completes. Replacing a texture or view requires updating constants that still contain the old handle. The ownership rules are described in Resource Management.

Thread groups

Dispatch specifies how many thread groups to execute. The compute sample declares [numthreads(16, 16, 1)], so each group contains 16 × 16 threads. To cover an image, divide its width and height by 16 and round each result up: a 17 × 19 image requires 2 × 2 groups. The shader checks each thread's coordinates and skips those outside the image.

The declared group size is available in ShaderDesc.ThreadGroupSize and can be used to calculate the group counts passed to Dispatch. ComputeShaderRenderer.cs shows the complete calculation. After the dispatch, access to the resulting image must follow the requirements described in Synchronization.

Search documentation

Search tutorials, concepts, samples, and the API reference.