Writing a bindless GPU abstraction layerKevin Gibson
Writing a bindless GPU abstraction layer
May 2, 2026
11 minutes read
Back in December 2025, Sebastian Aaltonen published a blog titled “No Graphics API” - it presented a great history of the evolution of GPU hardware, and gave an opinionated perspective on how we could simplify the modern graphics APIs on modern hardware. Like many graphics programmers, I read Sebastian’s blog post and really enjoyed it - I was inspired by it, and decided to see how close to the API he describes I could get today, layered on top of existing platform APIs. The answer turns out to be “pretty darn close”. The result is a project I’m calling Loon GPU, and I’ve put it up on Github. While the library is still rough, poorly tested and surely filled with bugs, it is usable and I wanted to share it early in case other folks were interested. Currently it has a Vulkan 1.3 and Metal 4 backend, and here I want to do a high-level walkthrough to see how the API design maps on to those backends.<br>In brief, the API looks like this:<br>No buffer objects, instead use GPU pointers everywhere.<br>No vertex buffers. Use vertex pulling instead, it’s much simpler.<br>Textures and samplers are treated bindlessly, with indices into a big texture heap object.<br>No explicit bind groups, instead pass device pointers to shaders to feed them data.<br>Pointers are first-class<br>In the API, you get GPU memory from malloc, and free it with free. Unlike the backends it maps on to, there is no concept of a “Buffer” object explicitly, just memory. By default you get memory that is persistently mapped to the CPU and optimized for CPU writing to it, but you can request GPU-local memory or mapped memory optimized for CPU readback.<br>One change from the API described in the original blog is that the API returns GPU device pointers - this is the only pointer that all 3 memory types map on to, so calling malloc gives you a device pointer and you can call get_host_pointer() to turn that into a CPU-side pointer if you need it.<br>When you call draw() or dispatch() functions to do work on the GPU, you can provide GPU pointers to give arguments to your shaders. On Metal this is trivial - the API has support for binding arbitrary GPU pointers to an argument table. On Vulkan, we pass these to the shader via push constants.<br>Internally, we do create buffer objects. In Vulkan, every allocation gets a VkBuffer bound to the entire memory range allocated. In Metal, we create a MTLHeap and create a buffer that covers the entire range as well. In both underlying APIs, we sometimes need to map back from GPU pointers to a buffer + offset pair, so we store a sorted list of GPU pointers that are allocated and do a binary search whenever necessary. While I was working on this library, a new Vulkan extension VK_KHR_device_address_commands was published, which should eliminate the need for most of this mapping when its available. In general the goal is to treat allocations as fairly heavy, and user-space allocators should be built on top of them rather than allocating for every object needed. This should keep the size of the lookup array should stay small and ideally lookups are fast when they are needed.<br>While it doesn’t seem like it would make a huge difference, having the API just give you pointers really does make the CPU-side code feel a lot more natural. You can more easily create complex data structures just like they were objects in C, and with an ergonomic ring-buffer, constructing draw arguments is trivial. It really feels like it simplifies a lot of the CPU side code, as well as the GPU side. Combined with bindless textures, we don’t have to worry about pipeline layouts, bind groups, or any of the complexity of the Vulkan binding model.<br>Shaders<br>This wrapper is opinionated about how your shaders should look, and that is part of what makes it simpler to use than raw Vulkan or Metal. The idea is that all GPU data is bindless, and shaders get their inputs from a single pointer that is passed to them. In order to make this work, we need a shading language.<br>When I started this experiment I decided to use Slang. It has support for pointers, can compile to SPIRV and Metal, and looked like it would work for my use case. And it mostly does! But unfortunately there are some complications that I think make this the roughest part of trying to use Loon.<br>To understand the difficulties, let’s start with what I’d like to achieve. In Vulkan, I’d like have a push constant range containing a single pointer assigned to each shader stage. In Metal, I’d like to bind a single buffer to each stage. In Loon, it currently looks like this:<br>StageVulkan Push Constant Range (bytes)Metal Buffer IndexCompute[0, 8)0Vertex[0, 8)0Fragment[8, 16)1In terms of syntax, Slang offers a very nice way to map shader arguments to push constants. You can declare a shader stage (e.g. a vertex...