Device & Context¶
Zest organizes rendering around two core objects: the Device and the Context. Understanding their responsibilities is fundamental to using the library.
Device (zest_device)¶
The device is a singleton that represents your GPU and all shared resources.
Creation¶
An example of creating a device with SDL2:
// Create a window first - the device needs it to query required Vulkan extensions
zest_window_data_t window_data = zest_implsdl2_CreateWindow(50, 50, 1280, 768, 0, "My Window");
zest_device device = zest_implsdl2_CreateVulkanDevice(&window_data, false);
If you need more control, you can use the device builder directly:
// Create the device using Vulkan as the platform layer
zest_device_builder device_builder = zest_BeginVulkanDeviceBuilder();
// Add your required extensions here
zest_AddDeviceBuilderExtensions(device_builder, extensions, count);
if (enable_validation) {
zest_AddDeviceBuilderValidation(device_builder);
zest_DeviceBuilderLogToConsole(device_builder);
}
zest_device device = zest_EndDeviceBuilder(device_builder);
Ensure that you include the SDL2 header before zest.h when using the SDL2 helper functions.
What the Device Manages¶
| Resource | Description |
|---|---|
| Platform Instance | The Platform API entry point (Currently Vulkan, DX/Metal/WebGPU(?) added in future) |
| Physical Device | GPU selection and feature queries |
| Shaders | Compiled/Loaded shaders |
| Pipeline Templates | Cached pipeline configurations |
| Compute Pipelines | Cached pipelines for compute dispatches |
| Bindless Descriptors | Global descriptor set for all resources |
| Memory Pools | GPU memory allocation (TLSF) and CPU side memory pools |
| Samplers | Texture samplers for use in shaders |
| Images | All textures stored on the GPU |
Device Lifecycle¶
int main() {
// Create once at startup
zest_window_data_t window_data = zest_implsdl2_CreateWindow(50, 50, 1280, 768, 0, "My Window");
zest_device device = zest_implsdl2_CreateVulkanDevice(&window_data, false);
// ... create contexts, run application ...
// Destroy at shutdown
zest_DestroyDevice(device);
}
Per-Frame Device Update¶
zest_UpdateDevice() handles:
- Deferred resource destruction
- Memory pool maintenance
Context (zest_context)¶
A context represents a render target - typically a window with its swapchain.
Creation¶
// Create window handles (platform-specific)
zest_window_data_t window = zest_implsdl2_CreateWindow(
50, 50, // Position
1280, 768, // Size
0, // Maximised (0 = false)
"My Window" // Title
);
// Configure context
zest_create_context_info_t info = zest_CreateContextInfo();
ZEST__UNFLAG(info.flags, zest_context_init_flag_enable_vsync); // Disable vsync
// Create context
zest_context context = zest_CreateContext(device, &window, &info);
What the Context Manages¶
| Resource | Description |
|---|---|
| Swapchain | Presentation images |
| Command Pools | Per-frame command buffer allocation |
| Synchronization | Uses semaphores for frame pacing |
| Frame Graph Cache | Compiled frame graphs |
| Linear Allocators | Fast per-frame memory |
Context Lifecycle¶
// Create context for each window
zest_context context1 = zest_CreateContext(device, &window1, &info);
zest_context context2 = zest_CreateContext(device, &window2, &info);
// ... run application ...
// Contexts are destroyed with the device
zest_DestroyDevice(device);
Frame Boundaries¶
Each frame is bracketed by zest_BeginFrame() and zest_EndFrame(). You must build or retrieve a frame graph between these calls:
// Create a cache key for the frame graph (typically done once per frame)
zest_frame_graph_cache_key_t cache_key = zest_InitialiseCacheKey(context, NULL, 0);
if (zest_BeginFrame(context)) {
// Try to retrieve cached frame graph
zest_frame_graph frame_graph = zest_GetCachedFrameGraph(context, &cache_key);
if (!frame_graph) {
// Build new frame graph if not cached
if (zest_BeginFrameGraph(context, "Main Graph", &cache_key)) {
// ... define passes ...
frame_graph = zest_EndFrameGraph();
}
}
// Execute frame graph and present
zest_EndFrame(context, frame_graph);
}
zest_BeginFrame() returns false if the window is minimized or if the swap chain could not be acquired (window resized or other state change), allowing you to skip rendering. See Frame Graph for details on building and caching frame graphs.
Multiple Contexts (Multi-Window)¶
One device can serve multiple contexts:
zest_window_data_t window_data = zest_implsdl2_CreateWindow(50, 50, 1280, 768, 0, "My Window");
zest_device device = zest_implsdl2_CreateVulkanDevice(&window_data, false);
// Main window
zest_window_data_t main_window = zest_implsdl2_CreateWindow(...);
zest_context main_context = zest_CreateContext(device, &main_window, &info);
// Secondary window
zest_window_data_t debug_window = zest_implsdl2_CreateWindow(...);
zest_context debug_context = zest_CreateContext(device, &debug_window, &info);
while (running) {
zest_UpdateDevice(device);
// Render to main window
if (zest_BeginFrame(main_context)) {
zest_frame_graph main_graph = /* build or retrieve frame graph */;
zest_EndFrame(main_context, main_graph);
}
// Render to debug window
if (zest_BeginFrame(debug_context)) {
zest_frame_graph debug_graph = /* build or retrieve frame graph */;
zest_EndFrame(debug_context, debug_graph);
}
}
Configuration Options¶
Context Flags¶
zest_create_context_info_t info = zest_CreateContextInfo();
// Disable vsync
ZEST__UNFLAG(info.flags, zest_context_init_flag_enable_vsync);
// Enable vsync (default)
ZEST__FLAG(info.flags, zest_context_init_flag_enable_vsync);
Memory Pool Sizes¶
The device manages multiple named memory pools for different buffer types (device buffers, staging buffers, transient buffers, etc.). Default pool sizes are configured automatically, but can be customized after device creation using zest_SetDevicePoolSize() and zest_SetStagingBufferPoolSize().
See Memory Management for details on pool configuration.
Utility Functions¶
Window Information¶
// Get screen dimensions
zest_uint width = zest_ScreenWidth(context);
zest_uint height = zest_ScreenHeight(context);
// Float versions
float w = zest_ScreenWidthf(context);
float h = zest_ScreenHeightf(context);
// DPI scale
float dpi = zest_DPIScale(context);
// Native window handle (for platform APIs)
void* native = zest_NativeWindow(context);
SDL_Window* sdl_window = (SDL_Window*)zest_Window(context);
Swapchain Information¶
zest_swapchain swapchain = zest_GetSwapchain(context);
zest_format format = zest_GetSwapchainFormat(swapchain);
zest_extent2d_t extent = zest_GetSwapChainExtent(context);
// Set clear color
zest_SetSwapchainClearColor(context, 0.1f, 0.1f, 0.1f, 1.0f);
Frame State¶
// Current frame-in-flight index (0 to ZEST_MAX_FIF - 1, default is 2 frames in flight)
zest_uint fif = zest_CurrentFIF(context);
// Check if swapchain was recreated (window resize)
if (zest_SwapchainWasRecreated(context)) {
// Rebuild resolution-dependent resources
}
Best Practices¶
- Create device first - Before any contexts or resources
- Call
zest_UpdateDevice()every frame - Even if not rendering - Check
zest_BeginFrame()return value - Returns false when minimized or swapchain was not acquired - Share resources via device - Textures, pipelines work across contexts
- Destroy device last - After all rendering is complete
See Also¶
- Architecture Overview - High-level design
- Frame Graph - Execution model
- Memory Management - Pool configuration