Skip to content

peterigz/zest

Repository files navigation

Zest

A Lightweight Single-Header Rendering Library

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.

Features

  • Minimal Setup: drop zest.h and zest_[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

Who is this for?

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.

Current Requirements

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

Screenshots

PBR Forward Shadow Mapping
PBR Forward Rendering Shadow Mapping
Particle Effects Deferred Rendering
Down/Upsample Bloom Dear Imgui Implementation

Getting Started

Integration

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.

Building the Examples

# Configure
cmake -B build

# Build
cmake --build build --config Release

# Run the minimal example
./build/examples/SDL2/Release/zest-minimal-template

Optional CMake flags:

Flag Description
-DZEST_ENABLE_SLANG=ON Enable the Slang shader compiler (requires VULKAN_SDK env var)

Frame Graph Example

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.

Multi-Pass Example

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);
}

Current to do

  • 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.

Project Structure

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.)

Included Examples

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

Architecture Overview

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.

Links

There's an initial first draft of documentation here but it still needs a lot of work: peterigz.github.io/zest

License

MIT License. See LICENSE for details.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors