Buffers¶
Buffers are GPU memory regions for vertex data, indices, uniforms, and general storage. Zest provides flexible buffer creation and management.
Buffer Types¶
| Type | Usage | Memory |
|---|---|---|
| Vertex | Vertex attributes | GPU only |
| Index | Index data | GPU only |
| Uniform | Shader uniforms | CPU visible |
| Storage | Shader read/write | GPU only |
| Staging | CPU to GPU transfer | CPU visible |
Creating Buffers¶
Basic Buffer¶
zest_buffer_info_t info = zest_CreateBufferInfo(
zest_buffer_type_vertex, // Type
zest_memory_usage_gpu_only // Memory location
);
zest_buffer buffer = zest_CreateBuffer(device, size_in_bytes, &info);
Buffer Types¶
// Vertex buffer (for vertex attributes)
zest_buffer_info_t info = zest_CreateBufferInfo(zest_buffer_type_vertex, zest_memory_usage_gpu_only);
// Index buffer
zest_buffer_info_t info = zest_CreateBufferInfo(zest_buffer_type_index, zest_memory_usage_gpu_only);
// Storage buffer (for compute shaders)
zest_buffer_info_t info = zest_CreateBufferInfo(zest_buffer_type_storage, zest_memory_usage_gpu_only);
// Vertex + storage (readable in vertex/fragment shaders)
zest_buffer_info_t info = zest_CreateBufferInfo(zest_buffer_type_vertex_storage, zest_memory_usage_gpu_only);
Memory Usage¶
zest_memory_usage_gpu_only // Fast GPU access, no CPU access
zest_memory_usage_cpu_to_gpu // CPU can write, used for uploads
zest_memory_usage_gpu_to_cpu // GPU writes, CPU can read back
Uploading Data¶
Staging Buffer Approach¶
For large uploads, use a staging buffer:
// Create staging buffer with data
zest_buffer staging = zest_CreateStagingBuffer(device, data_size, cpu_data);
// Copy to GPU buffer
zest_queue queue = zest_imm_BeginCommandBuffer(device, zest_queue_transfer);
zest_imm_CopyBuffer(queue, staging, gpu_buffer, data_size);
zest_imm_EndCommandBuffer(queue);
// Free staging buffer
zest_FreeBuffer(staging);
In Frame Graph¶
Copy operations in a transfer pass:
zest_BeginTransferPass("Upload"); {
zest_ConnectOutput(buffer_resource);
zest_SetPassTask(UploadCallback, app);
zest_EndPass();
}
void UploadCallback(zest_command_list cmd, void* user_data) {
...
zest_cmd_CopyBuffer(cmd, staging, dest, size);
}
Uniform Buffers¶
Per-frame uniform data with automatic multi-buffering. This means that inside the uniform buffer is a buffer for each frame in flight so you can safely modify one while the other is being accessed by the GPU.
// Create uniform buffer (returns a handle)
zest_uniform_buffer_handle ubo = zest_CreateUniformBuffer(context, "camera_ubo", sizeof(camera_t));
// Get pointer to current frame's data
uniform_data_t* ubo = (uniform_data_t*)zest_GetUniformBufferData(ubo);
ubo->view = view_matrix;
ubo->projection = proj_matrix;
// Get descriptor index for shader. Each frame each have a different index (one for each frame in flight)
zest_uint ubo_index = zest_GetUniformBufferDescriptorIndex(ubo);
Using Uniform Buffers in Shaders¶
layout(set = 0, binding = 7) uniform CameraUBO {
mat4 view;
mat4 projection;
} camera[];
void main() {
mat4 view = camera[push.ubo_index].view;
}
Buffer Operations¶
Resize and Grow¶
// Resize buffer (may reallocate) - takes pointer to buffer
zest_ResizeBuffer(&buffer, new_size);
// Grow buffer (only if needed) - takes pointer, unit size, and minimum bytes
zest_GrowBuffer(&buffer, unit_size, minimum_bytes);
// Get current size
zest_size size = zest_GetBufferSize(buffer);
Accessing Buffer Data¶
For CPU-visible buffers, you can directly access the mapped memory:
// Get pointer to buffer data (persistently mapped)
void* data = zest_BufferData(buffer);
memcpy(data, src, size);
// Get pointer to end of buffer (for bounds checking)
void* end = zest_BufferDataEnd(buffer);
Free Buffer¶
Memory Pools¶
Buffers are allocated from named memory pools managed by the device. Pool sizes can be configured after device creation:
// Configure GPU buffer pool (minimum allocation size, total pool size)
zest_SetGPUBufferPoolSize(device, zloc__KILOBYTE(64), zloc__MEGABYTE(128));
// Configure GPU buffer pool for small buffers (minimum allocation size, total pool size)
zest_SetGPUSmallBufferPoolSize(device, zloc__KILOBYTE(1), zloc__MEGABYTE(8));
// Configure staging buffer pool
zest_SetStagingBufferPoolSize(device, zloc__KILOBYTE(64), zloc__MEGABYTE(64));
Why Separate Small Buffer Pools?¶
Zest uses a TLSF (Two-Level Segregated Fit) allocator that tracks memory blocks using proxy structures. Each allocatable block in the pool requires a small proxy header to track its state (free/used, size, neighbors). The minimum block size determines how many potential blocks exist in a pool.
With a large pool (e.g., 128MB) and a small minimum block size (e.g., 1KB), you would have up to 128K potential blocks, each requiring proxy overhead. This wastes memory on bookkeeping. A larger minimum block size (e.g., 64KB) reduces proxy count to ~2K blocks, which is much more efficient.
However, if your application allocates many small buffers (under 64KB), a large minimum block size wastes space due to internal fragmentation - each small allocation rounds up to the minimum size.
The solution is separate pools: - Large buffer pool: Large minimum block size (64KB+), efficient for big allocations - Small buffer pool: Small minimum block size (1KB), smaller total pool size to limit proxy overhead
This also helps with external fragmentation. Small allocations mixed with large ones can fragment a pool over time, making it hard to find contiguous space for large allocations. Keeping them separate prevents small buffers from fragmenting the large buffer pool.
New pools are allocated when they run out of space. See Memory Management for detailed pool configuration.
Command Buffer Operations¶
Bind Buffers¶
// Bind vertex buffer (first_binding, binding_count, buffer)
zest_cmd_BindVertexBuffer(cmd, 0, 1, buffer);
// Bind index buffer
zest_cmd_BindIndexBuffer(cmd, buffer);
Copy Buffers¶
Transient Buffers¶
Temporary buffers within a frame graph:
zest_buffer_resource_info_t info = {
.size = particle_count * sizeof(particle_t),
.usage_hints = zest_resource_usage_hint_vertex_buffer
};
zest_resource_node particles = zest_AddTransientBufferResource("Particles", &info);
Transient buffers:
- Are allocated from a shared pool
- Can share memory with non-overlapping resources
- Are automatically freed after frame graph execution
Best Practices¶
- Use staging buffers - For large GPU-only uploads
- Prefer GPU-only memory - Faster for rendering
- Use uniform buffers - For frequently updated small data
- Size pools appropriately - Based on your application's needs. The defaults will be fine to start with.
- Use transient buffers - For intermediate data in multi-pass rendering
Example: Particle Buffer¶
// Create particle buffer
zest_buffer_info_t info = zest_CreateBufferInfo(
zest_buffer_type_vertex_storage,
zest_memory_usage_gpu_only
);
zest_buffer particles = zest_CreateBuffer(device, MAX_PARTICLES * sizeof(particle_t), &info);
// Initial upload
zest_buffer staging = zest_CreateStagingBuffer(device, initial_size, initial_data);
zest_queue queue = zest_imm_BeginCommandBuffer(device, zest_queue_transfer);
zest_imm_CopyBuffer(queue, staging, particles, initial_size);
zest_imm_EndCommandBuffer(queue);
zest_FreeBuffer(staging);
// Use in render callback
void RenderParticles(zest_command_list cmd, void* user_data) {
zest_cmd_BindVertexBuffer(cmd, 0, 1, particles);
zest_cmd_Draw(cmd, particle_count, 1, 0, 0);
}
See Also¶
- Memory Management - Pool configuration
- Buffer API - Complete function reference
- Compute Tutorial - Storage buffers with compute