That certainly is the question. Well, it's one of many when it comes to graphics programming (or really any kind of programming). This question came up in the comments on one of my YouTube videos and I answered it there, but figured I'd go more in depth here.
The question, in its original form (TL; DR below):
Hi Travis, I hope I can ask you a question about Vulkan wrappers.. Because I can see that you have a lot of them, like a wrapping struct around all (or most) Vulkan objects, but also wrappers around some Vulkan functions as well. So I was wondering: In your experience, what are the tradeoffs/advantages of this approach, in contrast to not having wrappers but using Vulkan directly? Another popular youtuber (Arseny Kapoulkine) was arguing against creating such wrappers saying "I wish people would not do that" [If I remember correctly]. After creating a bunch of wrappers and (sometimes quite heavy) classes, e.g. around a Swapchain, for my own engine, I was struggling. Because my wrappers made code shorter and easier to read + more reusable, but at the same time it was less flexible and required more maintenance as the API is changing going forward, and I had wrappers around even some enum types to enforce correct parameter passing (some Vulkan functions support only a subset of a certain enum type for their parameters). I'm still struggling to better understand this balance. Do you have some philosophical (or practical) advice for me? In advance, thank you a lot. And thank you for creating this series!
- Comment by maestro on episode #39: https://www.youtube.com/watch?v=sXuf_wHIHgM&lc=UgzhBeGBJblVXvAgpN54AaABAg
TL;DR: What are your thoughts on using wrappers around Vulkan functions and structures as opposed to not using them?
The Short(er) Answer
There are always trade-offs for anything you do.
Not having a wrapper can mean that your code can run marginally (or, depending on weight, vastly) quicker and is arguably somewhat easier to maintain at that level. The trade-off for not having a wrapper that you are stuck with only doing things the way that API expects.
Having a wrapper is nice because it keeps the graphics API way of doing things abstracted from the rest of the engine, and that generally allows for less code and easier readability. The trade-off there is two-fold; this makes the code a little heavier because abstraction is never free and refactoring can be a pain.
It's preference, but also depends on project needs, scope and team size.
The Long Answer
Now let's dive a bit deeper into these options to get a better view of why one way or the other might be better in some circumstances, but not others.
No Wrapper
Let's first break down the potential costs of not using a wrapper. The first thing that comes to mind is that you wind up with tight coupling between the graphics API and the rest of your codebase. For example, when using DirectX, it comes with its own types for vectors (XMVECTOR
), matrices (XMMATRIX
) to be used with its math library and other functions. This can be a boon or a hindrance depending on the project. If the only platforms you'll ever target are Windows and Xbox then you're golden. If you want anything else, you can't use those types because you won't have the libraries available. Another example might be with Vulkan, which is known to be verbose. There's a lot to certain aspects of it, and sometimes having so much code mixed in with your renderer can be... messy at best (I'm looking at you, VkPipeline
). There's also the cost of it being harder to look at from a bird's-eye view, but if you're familiar enough with such concepts that you are diving into Vulkan, perhaps bird's-eye view isn't needed as much. No wrappers also tends to mean your code is all in one spot, which can lead to longer files (this bothers some people more than others).
So those are the costs, what of the potential benefits of not using a wrapper? The first and foremost gain here is one of performance. Not matter how you slice it, not calling functions (or layers of functions) is always faster; there's no way around that. It also means there's less overall code, because there isn't a need to code up all the layers of functions, classes or structures to abstract the concepts. It also generally means all of your code is in one of two places, which can make it arguably easier to maintain. Fewer files also generally means quicker compile times.
Wrapper
Okay, so now let's have a look at using a wrapper, starting with the costs as before. As discussed before, having wrappers generally means also having more code, and that code is spread out between more files. This can increase complexity and definitely increases compile times. Maintenance can also be trickier for the simple reason that if you abstract in the wrong way that the Refactor Tractor is going to get more use. Refactoring is a realistic part of any project, but it's always best to keep it to a dull roar. If you opt for an object-oriented approach, this can be even worse because of how that pattern insists things be split up and "concerns" be separated. Of course, this also means in a lot of cases you'll have layers of code for "translation" purposes - which is to transform data between a generic type and the underlying data formats. This can be heavy, especially in tight-loop situations.
Since we've analyzed the costs, let's have a look now at the benefits of using a wrapper, of which there are arguably many. First, the top-level code becomes more easily readable and there generally tends to be far less of it on the surface. This can make concepts easier to grasp at higher levels, and keeps the "ugly" code hidden away from the average user. This is generally the chosen approach for projects involving teams of people as it's easier to convey ideas this way (or so at least many will agree). The biggest advantage here by far though is that it abstracts the graphics API away from the rest of the engine, which means the "under-the-hood" code can be swapped out far easier. This is the primary reason most teams choose this approach, and honestly it's a danged good reason. Coming with that, of course, is the concept of portability (meaning multiple graphics APIs can be targeted, varying on platform). This is also a common approach with platform (read: operating-system interface) logic.
So, Where do I Stand?
Basically, I'm a fan of relatively light wrappers around the graphics API because it keeps the rest of the engine from knowing or caring about what the renderer is doing. Yes, I sacrifice some performance, but it's a cost I am willing to pay because realistically that isn't my performance bottleneck... well, ever in my experience.
Another thing to note is that making progress is the most important thing. If wrappers are getting in the way of that then it's time for them to go (at least temporarily). Generally whenever I add a new feature, I do it without any kind of wrapper at first, then move stuff out as I finalize it.
The bottom line is that there is no one-size-fits-all approach though. You have to take it on a case-by-case basis and sometimes discover what does and does not work and pivot when needed. We've done that a time or two on Kohi, and there are more coming I'm sure. The more I work on Kohi, the more I am inclined to remove some of the wrappers I have in place because they don't add any value (fences and framebuffers are examples of this, where I had them and later removed them). I originally started off with a _lot_ of them, but now I find myself wanting to remove most of them, and combine parts of it.
Ideally, after having worked on this project for over a year, if I were to start it again I'd have a wrapper, for sure. The difference would be that most, if not all, of the internally-wrapped things (such as the Swapchain, Renderpass and Pipeline) would just exist when and where needed. Chances are that's going to wind up happening anyway. I think the thinnest possible wrapper is the way to go in terms of maintainability, compilation and runtime speed. More than likely the renderer backend (i.e. the wrapper around the graphics API) will be mostly behind a single, flat interface.
Word to the wise - if you're going to wrap the graphics API that's fine, but keep it thin.