Daxa 3.6 is now available Learn more
Vulkan for Modern GPUs
Daxa is a Vulkan-powered GPU abstraction purpose-built for modern graphics cards. It keeps Vulkan's explicit control but drops the parts that stopped paying off on modern hardware. The result is less code and fewer places to get something wrong.
Everything raw Vulkan makes you write by hand, handled for you.
Fully bindless
Access buffers, images, samplers, and acceleration structures by ID or pointer, on the host and inside shaders alike. Daxa removes most of the descriptor and binding boilerplate that Vulkan normally requires.
// host: pass the buffer's address straight to the shader
recorder.push_constant(MyPush{
.vertices = device.get_buffer_device_address(buffer).value(),
});
// main.glsl: read it like a pointer, no bindings
DAXA_DECL_PUSH_CONSTANT(MyPush, push)
void main() {
MyVertex v = deref_i(push.vertices, gl_VertexIndex);
}Modern GPUs only
Daxa targets Turing and newer, RDNA3 and newer, and Intel Arc. It abstracts render passes and uses a general image layout, so you never hand-manage layout transitions.
// images use a general layout everywhere, so the
// default view works for sampling and storage alike
daxa::ImageId image = device.create_image({
.format = daxa::Format::R8G8B8A8_UNORM,
.size = {1920, 1080, 1},
.usage = daxa::ImageUsageFlagBits::SHADER_SAMPLED |
daxa::ImageUsageFlagBits::SHADER_STORAGE,
.name = "render target",
});
daxa::ImageViewId view = image.default_view();Shader integration
Daxa ships a shader build system with #include resolution, hot reloading, and SPIR-V caching, plus GLSL and Slang headers. Define a struct once in a shared .inl file and use it from both C++ and your shaders.
// shared.inl: one definition, used by C++ and GLSL
#include <daxa/daxa.inl>
struct MyVertex {
daxa_f32vec3 position;
daxa_f32vec2 uv;
};
DAXA_DECL_BUFFER_PTR(MyVertex)
// host: edit a shader and it recompiles while running
pipeline_manager.reload_all();TaskGraph
You declare what each task reads and writes. TaskGraph generates the barriers, reorders passes to reduce how many it needs, and aliases transient memory. Record the graph once, then run it as many times as you like.
// say what each task touches; the graph does the sync
tg.add_task(daxa::Task::Compute("blur")
.writes(image)
.executes(blur));
tg.add_task(daxa::Task::Raster("present")
.reads(image)
.executes(present));The same frame, a fraction of the code.
Bind a resource
// Vulkan: expose one storage buffer to a shader
VkDescriptorSetLayoutBinding bind{
.binding = 0,
.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
.descriptorCount = 1,
.stageFlags = VK_SHADER_STAGE_VERTEX_BIT,
};
VkDescriptorSetLayoutCreateInfo lci{
.bindingCount = 1, .pBindings = &bind,
};
vkCreateDescriptorSetLayout(device, &lci, nullptr, &layout);
VkDescriptorPoolSize size{ VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1 };
VkDescriptorPoolCreateInfo pci{
.maxSets = 1, .poolSizeCount = 1, .pPoolSizes = &size,
};
vkCreateDescriptorPool(device, &pci, nullptr, &pool);
VkDescriptorSetAllocateInfo ai{
.descriptorPool = pool, .descriptorSetCount = 1, .pSetLayouts = &layout,
};
vkAllocateDescriptorSets(device, &ai, &set);
VkDescriptorBufferInfo info{ buffer, 0, VK_WHOLE_SIZE };
VkWriteDescriptorSet write{
.dstSet = set, .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
.pBufferInfo = &info,
};
vkUpdateDescriptorSets(device, 1, &write, 0, nullptr);
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
pipe_layout, 0, 1, &set, 0, nullptr);// Daxa: hand the shader a pointer. That is the binding model.
recorder.push_constant(MyPush{
.vertices = device.get_buffer_device_address(buffer).value(),
});
// main.glsl
DAXA_DECL_PUSH_CONSTANT(MyPush, push)
void main() {
MyVertex v = deref_i(push.vertices, gl_VertexIndex);
}Synchronize a pass
// Vulkan: barrier a compute write before a fragment read
VkImageMemoryBarrier2 barrier{
.srcStageMask = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT,
.srcAccessMask = VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT,
.dstStageMask = VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT,
.dstAccessMask = VK_ACCESS_2_SHADER_SAMPLED_READ_BIT,
.oldLayout = VK_IMAGE_LAYOUT_GENERAL,
.newLayout = VK_IMAGE_LAYOUT_GENERAL,
.image = image,
.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 },
};
VkDependencyInfo dep{
.imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &barrier,
};
vkCmdPipelineBarrier2(cmd, &dep);
// ...and again for every resource, on every pass, every frame.// Daxa: declare intent, TaskGraph emits the barriers
tg.add_task(daxa::Task::Compute("blur")
.writes(image)
.executes(blur));
tg.add_task(daxa::Task::Raster("present")
.reads(image)
.executes(present));
// Barriers and reordering: generated from the reads/writes.
Patrick Ahrens
Matěj Sakmary
Gabe Rundlett
Lucas Stallknecht

lukasino1214

Nathan George
Derpius
Jaisiero