Zest is a minimal, zero-bloat rendering library written in c that tames modern GPU APIs like Vulkan. It handles synchronization, barriers, image transitions, and resource management through a clean C API, letting you focus on what you actually want to render.
In it's current state I would say that it's an early but usable version, there will definitely be shortcomings and bugs so please report any issues!
This is a more forward looking renderer currently in that it supports bindless descriptors and dynamic render passes, this is mainly supported by GPUs from 2014/15 onwards, however I recently added a fallback to legacy renderpasses to help support older cards as well.
- Minimal Setup: drop
zest.handzest_[platform](currently vulkan only) into your project and go - Frame graph compiler: declare passes and resources; Zest handles barriers, semaphores, pass culling, async queue scheduling, and transient memory automatically
- Immediate command buffers: For easy one off tasks like uploading data to the gpu or running a compute shader.
- Bindless descriptors: one global descriptor layout, resources indexed by handle
- Dynamic rendering: Vulkan 1.3 dynamic render passes, no baked render pass objects (legacy renderpass fallback for older cards).
- TLSF memory allocator: Internal low-fragmentation allocator for both CPU and GPU memory to keep memory usage neat and tidy.
- Multi-window support: one device, multiple contexts (swapchains)
- Headless contexts: For just running compute shaders only.
- Cross-platform: Windows, Linux, macOS (currently via MoltenVK until I add a metal backend)
- Simple profiling: Can output to a debug overlay or a ImGui implementation
- Shader Hot Reloading: Flag shaders so that modifications are monitored and hot reloaded automatically
- Slang implementation: Optionally compile slang shaders
I wrote this renderer because I wanted something simple and straightforward that I could use for all my current and future projects.
Zest is for developers who understand rendering pipelines and shaders but don't want to wrangle thousands of lines of Rendering API (like Vulkan) boilerplate. If you want a minimal-dependency layer that stays out of your way while handling the tedious parts of modern GPU programming, Zest is a good fit.
You should be comfortable with concepts like vertex buffers, pipelines and render passes. Zest doesn't abstract these away it makes them manageable.
The current requirements will change overtime as I add more platform layers like DirectX and Metal.
| Requirement | Minimum |
|---|---|
| Vulkan | 1.2 |
| GPU Features | Bindless descriptors, dynamic rendering (falls back to legacy render passes if gpu doesn't support dynamic), synchronization 2 |
| C Standard | C11 (also compiles as C++) |
| CMake | 3.7+ |
Supported windowing libraries in the API (But it's easy to add anything else you want): GLFW and SDL2
| PBR Forward Rendering | Shadow Mapping |
| Down/Upsample Bloom | Dear Imgui Implementation |
Add zest.h and zest_vulkan.h to your project. In exactly one source file, define the implementation macros before including:
#define ZEST_IMPLEMENTATION
#define ZEST_VULKAN_IMPLEMENTATION
#include <zest.h>Optionally include zest_utilities.h for helpers like image loading, font rendering, and GLFW/SDL2 window setup. This file is there to support the examples but also serves as an example of how to use the API in various ways.
# Configure
cmake -B build
# Build
cmake --build build --config Release
# Run the minimal example
./build/examples/SDL2/Release/zest-minimal-templateOptional CMake flags:
| Flag | Description |
|---|---|
-DZEST_ENABLE_SLANG=ON |
Enable the Slang shader compiler (requires VULKAN_SDK env var) |
The frame graph is the core of Zest. You declare what you want to happen, and the compiler figures out how to execute it efficiently:
zest_BeginFrameGraph(context, "Simple example", 0);
// Import the swapchain as a resource the graph can write to
zest_resource swap = zest_ImportSwapchainResource();
// Declare a render pass
zest_BeginRenderPass("main");
zest_ConnectSwapChainOutput();
zest_SetPassTask(my_render_callback, user_data);
zest_EndPass();
zest_EndFrameGraph();The compiler automatically inserts barriers, manages image layout transitions, handles semaphore signaling between passes, and culls unused passes. Frame graphs can be cached with zest_InitialiseCacheKey() to avoid recompilation when the graph structure doesn't change.
if (zest_BeginFrameGraph(app->context, "PBR Forward Renderer", &cache_key)) {
zest_resource_node mesh_layer_resource = zest_AddTransientLayerResource("Mesh Layer", mesh_layer, false);
zest_resource_node skybox_layer_resource = zest_AddTransientLayerResource("Sky Box Layer", skybox_layer, false);
zest_resource_node depth_buffer = zest_AddTransientImageResource("Depth Buffer", &depth_info);
zest_resource_node swapchain_node = zest_ImportSwapchainResource();
zest_resource_group group = zest_CreateResourceGroup();
zest_AddSwapchainToGroup(group);
zest_AddResourceToGroup(group, depth_buffer);
//-------------------------Transfer Pass------------------------------------------------
//Upload instanced mesh data (mesh geometry already uploaded outside the frame graph)
zest_BeginTransferPass("Upload Mesh Data"); {
zest_ConnectOutput(mesh_layer_resource);
zest_ConnectOutput(skybox_layer_resource);
zest_SetPassTask(UploadMeshData, app);
zest_EndPass();
}
//-------------------------Skybox Pass--------------------------------------------------
//Render skybox first
zest_BeginRenderPass("Skybox Pass"); {
zest_ConnectInput(skybox_layer_resource);
zest_ConnectOutputGroup(group);
zest_SetPassTask(zest_DrawInstanceMeshLayer, skybox_layer);
zest_EndPass();
}
//-------------------------PBR Mesh Pass------------------------------------------------
//Render PBR objects over skybox
zest_BeginRenderPass("Instance Mesh Pass"); {
zest_ConnectInput(mesh_layer_resource);
zest_ConnectOutputGroup(group);
zest_SetPassTask(zest_DrawInstanceMeshLayer, mesh_layer);
zest_EndPass();
}
//-------------------------ImGui Pass---------------------------------------------------
zest_pass_node imgui_pass = zest_imgui_BeginPass(&app->imgui, app->imgui.main_viewport); {
if (imgui_pass) {
zest_ConnectOutputGroup(group);
} else {
zest_BeginRenderPass("Draw Nothing");
zest_ConnectOutputGroup(group);
zest_SetPassTask(zest_EmptyRenderPass, 0);
}
zest_EndPass();
}
frame_graph = zest_EndFrameGraph();
}Transient resources are automatically allocated, aliased where possible, and freed at the end of the frame.
In your pass task callback you might have something like:
void RecordComputeSprites(zest_command_list command_list, void *user_data) {
//Grab the app object from the user_data that we set in the frame graph when adding this function callback
ComputeExample *app = (ComputeExample*)user_data;
//Get the pipeline from the template that we created. This will compile and cache the pipeline if it hasn't
//been already. Otherwise it will just fetch the cached pipeline.
zest_pipeline pipeline = zest_GetPipeline(app->particle_pipeline, command_list);
//Bind the pipeline
zest_cmd_BindPipeline(command_list, pipeline);
//The shader needs to know the indexes into the descriptor array for the textures so we use push constants to
//do so. You could also use a uniform buffer if you wanted.
ParticlePushConsts push;
push.particle_index = app->particle_image_index;
push.gradient_index = app->gradient_image_index;
push.sampler_index = app->sampler_index;
//Send the push constant
zest_cmd_SendPushConstants(command_list, &push, sizeof(ParticlePushConsts));
//Set the viewport with this helper function. Pipelines are created with dynamic viewports by default so you
//must always set the view port for each draw call.
zest_cmd_SetScreenSizedViewport(command_list, 0.f, 1.f);
//Bind the vertex buffer with the particle buffer containing the location of all the point sprite particles
zest_cmd_BindVertexBuffer(command_list, 0, 1, app->particle_buffer);
//Draw the point sprites
zest_cmd_Draw(command_list, PARTICLE_COUNT, 1, 0, 0);
}- There's a few more examples/tests I'd like to write to test more functionality.
- Other platform layers: DirectX 12, Metal and maybe WebGPU and even opengl if it makes sense.
- Multithreaded command buffer recording.
- General bug fixes as I go.
zest.h Main API
zest_vulkan.h Vulkan backend
zest_utilities.h Optional helpers to support the examples. (image loading, fonts, GLFW/SDL2)
implementations/
impl_imgui.cpp/h Dear ImGui integration
impl_timelinefx.h/c TimelineVFX particle system
impl_slang.hpp Slang shader compiler support
examples/
SDL2/ Example projects (PBR, shadows, compute, particles, etc.)
Credit to Sascha Willems for some of these examples where I re-implemented his original Vulkan examples in Zest.
I highly recommend checking out the examples to learn how to use the library, all examples are heavily commented so they're a good resource to learn how to use Zest.
For convenience all examples use SDL2 for the windowing, but you can use whatever you want in your own projects.
| Example | Description |
|---|---|
zest-minimal-template |
Bare-minimum setup |
zest-pbr-forward |
Physically-based forward rendering |
zest-pbr-deferred |
Deferred rendering pipeline |
zest-shadow-mapping |
Shadow maps |
zest-cascading-shadows |
Cascaded shadow maps |
zest-compute-example |
Compute shaders with Slang |
zest-fonts |
MSDF font rendering |
zest-instancing |
Instanced draw calls |
zest-indirect-draw |
Indirect drawing with compute culling |
zest-render-targets |
Off-screen render targets |
zest-timelinefx |
Particle effects |
zest-vaders |
Space Invaders demo |
zest-imgui-template |
Dear ImGui integration |
Application
│
▼
┌───────────┐ ┌────────────┐ ┌────────────┐
│ Device │ ---> │ Context A │ │ Context B │
│ │ │ (Window 1) │ │ (Window 2) │
│ Shaders │ │ │ │ │
│ Pipelines │ │ FrameGraph │ │ FrameGraph │
│ Bindless │ │ Swapchain │ │ Swapshain │
│ Resources │ └────────────┘ └────────────┘
└───────────┘
Device -- One per application. Owns the Vulkan instance, GPU resources, shader library, pipeline templates, and the global bindless descriptor set.
Context -- One per window/swapchain. Manages frame resources, compiles and executes the frame graph.
There's an initial first draft of documentation here but it still needs a lot of work: peterigz.github.io/zest
MIT License. See LICENSE for details.