Skip to content

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

// Immediately frees the buffer
zest_FreeBuffer(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

// Copy entire buffer
zest_cmd_CopyBuffer(cmd, src, dst, size);

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

  1. Use staging buffers - For large GPU-only uploads
  2. Prefer GPU-only memory - Faster for rendering
  3. Use uniform buffers - For frequently updated small data
  4. Size pools appropriately - Based on your application's needs. The defaults will be fine to start with.
  5. 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