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 Slang entry point and returns a ShaderDesc. The context's GraphicsApi selects the output format: DXIL for DirectX 12, metallib 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 C# fields occupy the first 24 bytes: eight for the dimensions and eight for each handle. The 256-byte record leaves the remaining bytes as padding. For a different parameter block, match the field offsets to its shader layout. If multiple records share a buffer, include enough padding between them to meet the required binding 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);

The constant buffer requires BufferUsages.Constant usage. If several constant records share one buffer, each record's starting offset must meet the required binding alignment. This is separate from the layout of fields within each 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 graphics API implementation. 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.