Ultralight uses seven shader programs to render geometry. Each program pairs a vertex shader with a pixel shader, and the SDK ships precompiled files and source headers for every supported graphics API.

On each draw, your driver binds the requested program and supplies vertices, uniforms, and textures in the format the shaders expect. If you're building on the reference drivers in the SDK's `platform` folder, they already handle these steps.

## Binding Shader Programs

Each draw command specifies its program in `GPUState::shader_type`. Your driver binds the matching vertex and pixel shader pair before drawing, typically inside a state helper such as `MyGPUDriver::ApplyState()` from [Implementing a GPUDriver](/docs/2.0/implementing-a-gpudriver).

A conforming driver must support every program in the table— the library doesn't query for per-program support, though it checks other capabilities through `GetDeviceCaps()`.

| Program | Shaders | Vertex Layout | Usage |
| :--- | :--- | :--- | :--- |
| `Fill` | `vertex_quad` + `fill` | Quad | Colors, images, gradients, rounded rectangles, and box shadows |
| `FillPath` | `vertex_path` + `fill_path` | Path | Tessellated vector paths |
| `FilterBasic` | `vertex_quad` + `filter_basic` | Quad | Basic CSS and SVG filters |
| `FilterBlur` | `vertex_quad` + `filter_blur` | Quad | Blur filters |
| `FilterDropShadow` | `vertex_quad` + `filter_dropshadow` | Quad | Drop-shadow filters |
| `FillPhoton` | `vertex_quad` + `fill_photon` | Quad | Analytic vector paths and text |
| `FillPhotonGrid` | `vertex_photon_grid` + `fill_photon_grid` | Instance | Grid-based analytic rendering |

## Configuring Vertex Layouts

The renderer provides geometry through vertex buffers described by `VertexBuffer::format`. Each format corresponds to a packed struct with no padding.

Positions use pixel coordinates. The `obj` field holds object-space coordinates used for anti-aliasing in path and quad geometry.

Although named an instance layout, `Vertex_2f_2ui` uses standard indexed draw calls without hardware instancing APIs.

| Struct | Format | Fields | Size |
| :--- | :--- | :--- | :--- |
| `Vertex_2f_4ub_2f` | `_2f_4ub_2f` | `pos[2]` float, `color[4]` RGBA byte, `obj[2]` float | 20 bytes |
| `Vertex_2f_4ub_2f_2f_28f` | `_2f_4ub_2f_2f_28f` | `pos[2]` float, `color[4]` RGBA byte, `tex[2]` float, `obj[2]` float, `data0`..`data6` (4 floats each) | 140 bytes |
| `Vertex_2f_2ui` | `_2f_2ui` | `pos[2]` float, `header_addr` uint32, `path_slot` uint32 | 16 bytes |

> 🚧 Bind Integer Attributes
>
> The `header_addr` and `path_slot` fields in `Vertex_2f_2ui` are unsigned 32-bit integers. Bind them through the integer attribute path in the graphics API (eg, `glVertexAttribIPointer` or an `R32_UINT` input element)— treating them as floating-point attributes causes the GPU to misread their values silently.

## Updating the Constant Block

Populate an 800-byte uniform buffer at register 0 for both shader stages using data from `GPUState`:

```cpp
///
/// The constant block the stock shaders read, filled from GPUState.
///
struct Uniforms {
  float state[4];         // 0, viewport width, viewport height, 1
  Matrix4x4 transform;    // GPUState::transform times the projection
  int32_t integer[8];     // GPUState::uniform_integer
  float scalar[8];        // GPUState::uniform_scalar
  vec4 vector[8];         // GPUState::uniform_vector
  int32_t clip_size[4];   // x = GPUState::clip_size, the rest 0
  Matrix4x4 clip[8];      // GPUState::clip
};
```

Copy the matrix data from `GPUState::clip` directly into the buffer. For the `transform` field, multiply `GPUState::transform` by the projection matrix, as described in [Implementing a GPUDriver](/docs/2.0/implementing-a-gpudriver).

## Binding Texture Slots

The driver binds textures from `GPUState::texture_1_id` through `texture_3_id` to registers 0 through 2, skipping any slot set to 0.

For vector draws with `FillPhoton` and `FillPhotonGrid`, the renderer passes raw data pages rather than sampled images. The shaders read these textures directly through texel loads, leaving slot 2 unused.

| Slot | Data | Format | Register |
| :--- | :--- | :--- | :--- |
| `texture_1_id` | Curve page | `RGBA16F` | 0 |
| `texture_3_id` | Constants, glyph LUT, and ramp | `RGBA32F` | 2 |
| `texture_4_id` | Index and header page | `RGBA16UI` | 3 |

Configure data page textures without filtering or mipmaps. The index page requires an unsigned integer texture view with its own register— it must not share a binding point with floating-point views.

When drawing with `FillPhotonGrid`, bind the data page textures to both the vertex and pixel shader stages.

## Shipped Shaders

The SDK provides stock shaders under `platform/shaders/generated/` in formats tailored for each graphics backend.

| Folder | Contents |
| :--- | :--- |
| `headers/d3d11/`, `headers/d3d12/` | Compiled Direct3D bytecode stored in C byte arrays |
| `headers/glsl/` | GLSL source code stored in C string headers, compiled at runtime |
| `metal/` | Metal Shading Language source code, compiled into libraries at build time |
| `spirv/` | SPIR-V binary files, suitable for Vulkan pipelines |

### SPIR-V Descriptor Bindings

The precompiled SPIR-V shaders place every resource into descriptor set 0 and share a single layout— one descriptor set layout serves every pipeline. Textures bind as separate sampled images, and a single sampler descriptor sits at binding 16.

| Binding | Descriptor | Data | Used By |
| :--- | :--- | :--- | :--- |
| 0 | Uniform buffer | Constant block | Every shader |
| 1 | Sampled image | `texture_1_id` | `fill`, `fill_photon`, `fill_photon_grid`, `filter_basic`, `filter_blur`, `filter_dropshadow` |
| 2 | Sampled image | `texture_2_id` | `fill`, `filter_basic`, `filter_dropshadow` |
| 3 | Sampled image | `texture_3_id` | `fill`, `fill_photon`, `vertex_photon_grid` |
| 4 | Sampled image (unsigned integer) | `texture_4_id` | `fill_photon`, `fill_photon_grid` |
| 16 | Sampler | Texture sampler | `fill`, `filter_basic`, `filter_blur`, `filter_dropshadow` |
