First Application¶
Let's build your first Zest application step by step. We'll create a minimal app that displays a blank screen - the simplest possible frame graph.
Complete Code¶
Here's the full example (from zest-minimal-template):
#define ZEST_IMPLEMENTATION
#define ZEST_VULKAN_IMPLEMENTATION
#include <SDL.h>
#include <zest.h>
struct minimal_app_t {
zest_device device;
zest_context context;
};
void BlankScreen(const zest_command_list command_list, void *user_data) {
// Render commands go here
}
void MainLoop(minimal_app_t *app) {
int running = 1;
SDL_Event event;
while (running) {
while (SDL_PollEvent(&event)) {
if (event.type == SDL_QUIT) running = 0;
}
zest_UpdateDevice(app->device);
zest_frame_graph_cache_key_t cache_key = zest_InitialiseCacheKey(app->context, 0, 0);
if (zest_BeginFrame(app->context)) {
zest_frame_graph frame_graph = zest_GetCachedFrameGraph(app->context, &cache_key);
if (!frame_graph) {
if (zest_BeginFrameGraph(app->context, "Render Graph", &cache_key)) {
zest_ImportSwapchainResource();
zest_BeginRenderPass("Draw Nothing"); {
zest_ConnectSwapChainOutput();
zest_SetPassTask(BlankScreen, 0);
zest_EndPass();
}
frame_graph = zest_EndFrameGraph();
}
}
zest_EndFrame(app->context, frame_graph);
}
}
}
int main(int argc, char *argv[]) {
minimal_app_t app = {};
zest_window_data_t window = zest_implsdl2_CreateWindow(50, 50, 1280, 768, 0, "Minimal Example");
app.device = zest_implsdl2_CreateVulkanDevice(&window, false);
zest_create_context_info_t create_info = zest_CreateContextInfo();
app.context = zest_CreateContext(app.device, &window, &create_info);
MainLoop(&app);
zest_DestroyDevice(app.device);
return 0;
}
Step-by-Step Breakdown¶
1. Include Headers and Define Implementation¶
The ZEST_IMPLEMENTATION macros tell Zest to include the actual implementation, not just declarations. Do this in one file only.
2. Create the Device¶
zest_window_data_t window = zest_implsdl2_CreateWindow(50, 50, 1280, 768, 0, "Minimal Example");
app.device = zest_implsdl2_CreateVulkanDevice(&window, false);
The device is a singleton that manages:
- Vulkan instance and physical device selection
- Shader library
- Bindless descriptor sets
- Memory pools
The false parameter disables validation layers (use true during development).
3. Create the Context¶
zest_create_context_info_t create_info = zest_CreateContextInfo();
app.context = zest_CreateContext(app.device, &window, &create_info);
The context is tied to a window/swapchain and manages:
- Frame resources (command buffers, synchronization)
- Frame graph compilation and execution
- Linear allocators for temporary data
One device can serve multiple contexts (multiple windows).
4. The Main Loop¶
while (running) {
while (SDL_PollEvent(&event)) { // Handle window events
if (event.type == SDL_QUIT) running = 0;
}
zest_UpdateDevice(app.device); // Update device state
if (zest_BeginFrame(app.context)) {
// Build or get cached frame graph...
zest_EndFrame(app.context, frame_graph); // Execute and present
}
}
Every frame:
zest_UpdateDevice()- Updates device-level statezest_BeginFrame()- Acquires swapchain image, returns false if window minimized- Build or get cached frame graph
zest_EndFrame(context, frame_graph)- Executes the graph and presents the frame
5. Frame Graph Caching¶
zest_frame_graph_cache_key_t cache_key = zest_InitialiseCacheKey(app->context, 0, 0);
zest_frame_graph frame_graph = zest_GetCachedFrameGraph(app->context, &cache_key);
Frame graphs can be cached to avoid recompilation every frame. The cache key identifies a specific frame graph configuration.
6. Building the Frame Graph¶
if (zest_BeginFrameGraph(app->context, "Render Graph", &cache_key)) {
zest_ImportSwapchainResource();
zest_BeginRenderPass("Draw Nothing"); {
zest_ConnectSwapChainOutput();
zest_SetPassTask(BlankScreen, 0);
zest_EndPass();
}
frame_graph = zest_EndFrameGraph();
}
When building a frame graph:
- Import resources -
zest_ImportSwapchainResource()makes the swapchain available - Define passes - Each pass has inputs, outputs, and a task callback
- Connect resources -
zest_ConnectSwapChainOutput()declares this pass writes to the swapchain - Set the task - The callback function that records GPU commands
- End and compile -
zest_EndFrameGraph()compiles barriers and returns the graph
7. Execute the Frame Graph¶
This executes the frame graph and presents the result to the window. The frame graph is passed directly to zest_EndFrame() which handles execution and presentation.
8. The Render Callback¶
void BlankScreen(const zest_command_list command_list, void *user_data) {
// Usually you'd have commands like:
// zest_cmd_BindPipeline(command_list, pipeline);
// zest_cmd_Draw(command_list, vertex_count, 1, 0, 0);
}
This callback receives a command list for recording GPU commands. The user_data parameter lets you pass your application state.
What's Next?¶
Now that you understand the basic structure:
- Architecture Overview - Deeper explanation of Device, Context, and Frame Graph
- Adding ImGui Tutorial - Add a UI to your application
- Frame Graph Concept - Full details on the frame graph system