Sorry it’s been so long for an update but basically I think after the last update I spent 3 months restructuring somethings in the TimelineFX library and then decided that the renderer really needed some work as it was a little cumbersome to work with. So I made the decision to spend some time refactoring it and then 10 months later here we are!
Was it worth it? Yes definitely, I am very pleased with where my renderer is at and will have it as a separate open source project that I might start pushing out more. This is a renderer that I’ve been tinkering with since 2016:
Zest
Zest is the name of my single header* rendering API. I have the asterix there because it’s single header + the platform layer that you want to use which is currently Vulkan only. here’s a list of all the refactoring I did:
- Developed a Frame Graph API. This is probably the best thing about the API now. One of the most annoying things about modern rendering, particularly with Vulkan is the resource management you have to do between frames and passes to make sure you have no race conditions – because rendering these days is so heavily parallel. So what the frame graph does is handle all that for you automatically, all you do is declare your resources, then connect them as input and output to passes (render, transfer or compute) and then the frame graph handles the rest. It’s a big headache removed.
- Completely separated out Vulkan and moved it into a separate platform layer (zest_vulkan.h). This means that in the future it will be a lot easier to add DirectX and Metal as additional layers. Although with Vulkan it does mean that it’s already working on Windows, Linux and Mac.
- Huge amount of clean up and added a test suite to make it as robust as possible although I sure there’s bugs.
- Created a bunch of examples mainly to help me test the functionality of the frame graph but they serve well to show how to use the library. A lot of these examples I converted from Sascha Willems Vulkan examples so thanks to him for creating those!
- Switched to bindless descriptor set indexing. This just means that it’s so much more easier to reference resources in shaders now then it was before.
- Switched to dynamic render passes. This too makes things a lot easier, however the caveat here is that the renderer will now only support GPUs from 2014/2015 and onwards. Having said this I may be able to add another platform layer like opengl to support much older cards but we’ll how that works out. Given the current GPU pricing it probably makes more sense at the moment to try and support older cards as well but we’ll see.
- Much more optimised buffer and image handling.
- Overall it’s a much cleaner API and easier to use then it was before.
An example of the new frame graph API:
if (zest_BeginFrameGraph(app->context, "Compute Particles", &cache_key)) {
//Resources
//Import the particle buffer that we created an populated with all the particles
zest_resource_node particle_buffer = zest_ImportBufferResource("read particle buffer", app->particle_buffer, 0);
//Import the swapchain so we can output to it.
zest_resource_node swapchain_node = zest_ImportSwapchainResource();
//Get the compute pointer from the handle
zest_compute compute = zest_GetCompute(app->compute);
//---------------------------------Compute Pass------------------------------------------------
zest_BeginComputePass(compute, "Compute Particles"); {
//Connect the particle buffer as input and output as the compute shader will read and write
//from/to it
zest_ConnectInput(particle_buffer);
zest_ConnectOutput(particle_buffer);
//Set the pass task to the callback function. This is called when the frame graph is executed
zest_SetPassTask(RecordComputeCommands, app);
zest_EndPass();
}
//---------------------------------------------------------------------------------------------
//---------------------------------Render Pass-------------------------------------------------
zest_BeginRenderPass("Graphics Pass"); {
//Connect the particle buffer as input. This will create a dependency chain with the compute pass
zest_ConnectInput(particle_buffer);
zest_ConnectSwapChainOutput();
zest_SetPassTask(RecordComputeSprites, app);
zest_EndPass();
}
//---------------------------------------------------------------------------------------------
//------------------------ ImGui Pass ---------------------------------------------------------
//If there's imgui to draw then draw it
zest_pass_node imgui_pass = zest_imgui_BeginPass(&app->imgui, app->imgui.main_viewport); {
if (imgui_pass) {
zest_ConnectSwapChainOutput();
zest_EndPass();
}
}
//----------------------------------------------------------------------------------------------
//You must call zest_EndFrameGraph to finalise and compile the graph, ready to be executed. If it
//compiled ok without errors then it will also be cached (assuming a cache_key was used);
frame_graph = zest_EndFrameGraph();
}
You can check out the library here if you’re interested: https://github.com/peterigz/zest
Back to TimelineFX
So now that’s done it’s back to TimelineFX. For the first 3 months of 2025 I did make quite a few changes to the editor and library to prepare it to start developing compute shaders. I’ve pretty much reached the conclusion that I’ll make the library 100% compute to update the particles. I don’t really want to have to maintain 2 sides of the library, the cpu side and the compute, I’d rather just focus on one and now that the renderer is in a much better place I can focus on the compute side of things.
Of course TimelineFX will remain render agnostic and I’ll have to think about how people can easily take the shaders of TimelineFX to use in their own renderers. For this I’ll probably use slang as that can be used to compile to glsl, hlsl and metal.
There will be some breaking changes in the next version. I’ll think what I’ll do is refactor to make TimelineFX work with the new version of the renderer and release that and then focus on switching to compute after that.
The next update should be a lot sooner!
